]> git.basschouten.com Git - openhab-addons.git/blob
46366d60073a04f7e1e8b363fdce29f84f894d22
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.PROPERTY_FIRMWARE_VERSION;
16 import static org.openhab.binding.openwebnet.OpenWebNetBindingConstants.PROPERTY_SERIAL_NO;
17 import static org.openhab.binding.openwebnet.OpenWebNetBindingConstants.THING_TYPE_ZB_GATEWAY;
18
19 import java.util.Collection;
20 import java.util.Collections;
21 import java.util.Map;
22 import java.util.Set;
23 import java.util.concurrent.ConcurrentHashMap;
24 import java.util.concurrent.TimeUnit;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.openwebnet.OpenWebNetBindingConstants;
29 import org.openhab.binding.openwebnet.handler.config.OpenWebNetBusBridgeConfig;
30 import org.openhab.binding.openwebnet.handler.config.OpenWebNetZigBeeBridgeConfig;
31 import org.openhab.binding.openwebnet.internal.discovery.OpenWebNetDeviceDiscoveryService;
32 import org.openhab.core.config.core.status.ConfigStatusMessage;
33 import org.openhab.core.thing.Bridge;
34 import org.openhab.core.thing.ChannelUID;
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.openwebnet4j.BUSGateway;
42 import org.openwebnet4j.GatewayListener;
43 import org.openwebnet4j.OpenDeviceType;
44 import org.openwebnet4j.OpenGateway;
45 import org.openwebnet4j.USBGateway;
46 import org.openwebnet4j.communication.OWNAuthException;
47 import org.openwebnet4j.communication.OWNException;
48 import org.openwebnet4j.message.Automation;
49 import org.openwebnet4j.message.BaseOpenMessage;
50 import org.openwebnet4j.message.EnergyManagement;
51 import org.openwebnet4j.message.FrameException;
52 import org.openwebnet4j.message.GatewayMgmt;
53 import org.openwebnet4j.message.Lighting;
54 import org.openwebnet4j.message.OpenMessage;
55 import org.openwebnet4j.message.What;
56 import org.openwebnet4j.message.Where;
57 import org.openwebnet4j.message.WhereZigBee;
58 import org.openwebnet4j.message.Who;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61
62 /**
63  * The {@link OpenWebNetBridgeHandler} is responsible for handling communication with gateways and handling events.
64  *
65  * @author Massimo Valla - Initial contribution
66  * @author Andrea Conte - Energy management
67  */
68 @NonNullByDefault
69 public class OpenWebNetBridgeHandler extends ConfigStatusBridgeHandler implements GatewayListener {
70
71     private final Logger logger = LoggerFactory.getLogger(OpenWebNetBridgeHandler.class);
72
73     private static final int GATEWAY_ONLINE_TIMEOUT_SEC = 20; // Time to wait for the gateway to become connected
74
75     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = OpenWebNetBindingConstants.BRIDGE_SUPPORTED_THING_TYPES;
76
77     // ConcurrentHashMap of devices registered to this BridgeHandler
78     // association is: ownId (String) -> OpenWebNetThingHandler, with ownId = WHO.WHERE
79     private Map<String, @Nullable OpenWebNetThingHandler> registeredDevices = new ConcurrentHashMap<>();
80     private Map<String, Long> discoveringDevices = new ConcurrentHashMap<>();
81
82     protected @Nullable OpenGateway gateway;
83     private boolean isBusGateway = false;
84
85     private boolean isGatewayConnected = false;
86
87     public @Nullable OpenWebNetDeviceDiscoveryService deviceDiscoveryService;
88     private boolean reconnecting = false; // we are trying to reconnect to gateway
89     private boolean scanIsActive = false; // a device scan has been activated by OpenWebNetDeviceDiscoveryService;
90     private boolean discoveryByActivation;
91
92     public OpenWebNetBridgeHandler(Bridge bridge) {
93         super(bridge);
94     }
95
96     public boolean isBusGateway() {
97         return isBusGateway;
98     }
99
100     @Override
101     public void initialize() {
102         ThingTypeUID thingType = getThing().getThingTypeUID();
103         OpenGateway gw;
104         if (thingType.equals(THING_TYPE_ZB_GATEWAY)) {
105             gw = initZigBeeGateway();
106         } else {
107             gw = initBusGateway();
108             isBusGateway = true;
109         }
110         if (gw != null) {
111             gateway = gw;
112             gw.subscribe(this);
113             if (gw.isConnected()) { // gateway is already connected, device can go ONLINE
114                 isGatewayConnected = true;
115                 updateStatus(ThingStatus.ONLINE);
116             } else {
117                 updateStatus(ThingStatus.UNKNOWN);
118                 logger.debug("Trying to connect gateway {}... ", gw);
119                 try {
120                     gw.connect();
121                     scheduler.schedule(() -> {
122                         // if status is still UNKNOWN after timer ends, set the device as OFFLINE
123                         if (thing.getStatus().equals(ThingStatus.UNKNOWN)) {
124                             logger.info("status still UNKNOWN. Setting device={} to OFFLINE", thing.getUID());
125                             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
126                                     "Could not connect to gateway before " + GATEWAY_ONLINE_TIMEOUT_SEC + "s");
127                         }
128                     }, GATEWAY_ONLINE_TIMEOUT_SEC, TimeUnit.SECONDS);
129                     logger.debug("bridge {} initialization completed", thing.getUID());
130                 } catch (OWNException e) {
131                     logger.debug("gw.connect() returned OWNException: {}", e.getMessage());
132                     // status is updated by callback onConnectionError()
133                 }
134             }
135         }
136     }
137
138     /**
139      * Init a ZigBee gateway based on config
140      */
141     private @Nullable OpenGateway initZigBeeGateway() {
142         logger.debug("Initializing ZigBee USB Gateway");
143         OpenWebNetZigBeeBridgeConfig zbBridgeConfig = getConfigAs(OpenWebNetZigBeeBridgeConfig.class);
144         String serialPort = zbBridgeConfig.getSerialPort();
145         if (serialPort == null || serialPort.isEmpty()) {
146             logger.warn("Cannot connect ZigBee USB Gateway. No serial port has been provided in Bridge configuration.");
147             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
148                     "@text/offline.conf-error-no-serial-port");
149             return null;
150         } else {
151             return new USBGateway(serialPort);
152         }
153     }
154
155     /**
156      * Init a BUS gateway based on config
157      */
158     private @Nullable OpenGateway initBusGateway() {
159         logger.debug("Initializing BUS gateway");
160         OpenWebNetBusBridgeConfig busBridgeConfig = getConfigAs(OpenWebNetBusBridgeConfig.class);
161         String host = busBridgeConfig.getHost();
162         if (host == null || host.isEmpty()) {
163             logger.warn("Cannot connect to BUS Gateway. No host/IP has been provided in Bridge configuration.");
164             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
165                     "@text/offline.conf-error-no-ip-address");
166             return null;
167         } else {
168             int port = busBridgeConfig.getPort().intValue();
169             String passwd = busBridgeConfig.getPasswd();
170             String passwdMasked;
171             if (passwd.length() >= 4) {
172                 passwdMasked = "******" + passwd.substring(passwd.length() - 3, passwd.length());
173             } else {
174                 passwdMasked = "******";
175             }
176             discoveryByActivation = busBridgeConfig.getDiscoveryByActivation();
177             logger.debug("Creating new BUS gateway with config properties: {}:{}, pwd={}, discoveryByActivation={}",
178                     host, port, passwdMasked, discoveryByActivation);
179             return new BUSGateway(host, port, passwd);
180         }
181     }
182
183     @Override
184     public void handleCommand(ChannelUID channelUID, Command command) {
185         logger.debug("handleCommand (command={} - channel={})", command, channelUID);
186         OpenGateway gw = gateway;
187         if (gw != null && !gw.isConnected()) {
188             logger.warn("Gateway is NOT connected, skipping command");
189             return;
190         } else {
191             logger.warn("Channel not supported: channel={}", channelUID);
192         }
193     }
194
195     @Override
196     public Collection<ConfigStatusMessage> getConfigStatus() {
197         return Collections.emptyList();
198     }
199
200     @Override
201     public void handleRemoval() {
202         disconnectGateway();
203         super.handleRemoval();
204     }
205
206     @Override
207     public void dispose() {
208         disconnectGateway();
209         super.dispose();
210     }
211
212     private void disconnectGateway() {
213         OpenGateway gw = gateway;
214         if (gw != null) {
215             gw.closeConnection();
216             gw.unsubscribe(this);
217             logger.debug("Gateway {} connection closed and unsubscribed", gw.toString());
218             gateway = null;
219         }
220         reconnecting = false;
221     }
222
223     @Override
224     public Collection<Class<? extends ThingHandlerService>> getServices() {
225         return Collections.singleton(OpenWebNetDeviceDiscoveryService.class);
226     }
227
228     /**
229      * Search for devices connected to this bridge handler's gateway
230      *
231      * @param listener to receive device found notifications
232      */
233     public synchronized void searchDevices() {
234         scanIsActive = true;
235         logger.debug("------$$ scanIsActive={}", scanIsActive);
236         OpenGateway gw = gateway;
237         if (gw != null) {
238             if (!gw.isDiscovering()) {
239                 if (!gw.isConnected()) {
240                     logger.debug("------$$ Gateway '{}' is NOT connected, cannot search for devices", gw);
241                     return;
242                 }
243                 logger.info("------$$ STARTED active SEARCH for devices on bridge '{}'", thing.getUID());
244                 try {
245                     gw.discoverDevices();
246                 } catch (OWNException e) {
247                     logger.warn("------$$ OWNException while discovering devices on bridge '{}': {}", thing.getUID(),
248                             e.getMessage());
249                 }
250             } else {
251                 logger.debug("------$$ Searching devices on bridge '{}' already activated", thing.getUID());
252                 return;
253             }
254         } else {
255             logger.warn("------$$ Cannot search devices: no gateway associated to this handler");
256         }
257     }
258
259     @Override
260     public void onNewDevice(@Nullable Where w, @Nullable OpenDeviceType deviceType, @Nullable BaseOpenMessage message) {
261         OpenWebNetDeviceDiscoveryService discService = deviceDiscoveryService;
262         if (discService != null) {
263             if (w != null && deviceType != null) {
264                 discService.newDiscoveryResult(w, deviceType, message);
265             } else {
266                 logger.warn("onNewDevice with null where/deviceType, msg={}", message);
267             }
268         } else {
269             logger.warn("onNewDevice but null deviceDiscoveryService");
270         }
271     }
272
273     @Override
274     public void onDiscoveryCompleted() {
275         logger.info("------$$ FINISHED active SEARCH for devices on bridge '{}'", thing.getUID());
276     }
277
278     /**
279      * Notifies that the scan has been stopped/aborted by OpenWebNetDeviceDiscoveryService
280      */
281     public void scanStopped() {
282         scanIsActive = false;
283         logger.debug("------$$ scanIsActive={}", scanIsActive);
284     }
285
286     private void discoverByActivation(BaseOpenMessage baseMsg) {
287         logger.debug("discoverByActivation: msg={}", baseMsg);
288         OpenWebNetDeviceDiscoveryService discService = deviceDiscoveryService;
289         if (discService == null) {
290             logger.warn("discoverByActivation: null OpenWebNetDeviceDiscoveryService, ignoring msg={}", baseMsg);
291             return;
292         }
293         if (baseMsg instanceof Lighting || baseMsg instanceof Automation || baseMsg instanceof EnergyManagement) { // we
294                                                                                                                    // support
295                                                                                                                    // these
296                                                                                                                    // types
297                                                                                                                    // only
298             BaseOpenMessage bmsg = baseMsg;
299             if (baseMsg instanceof Lighting) {
300                 What what = baseMsg.getWhat();
301                 if (Lighting.WHAT.OFF.equals(what)) { // skipping OFF msg: cannot distinguish dimmer/switch
302                     logger.debug("discoverByActivation: skipping OFF msg: cannot distinguish dimmer/switch");
303                     return;
304                 }
305                 if (Lighting.WHAT.ON.equals(what)) { // if not already done just now, request light status to
306                                                      // distinguish dimmer from switch
307                     if (discoveringDevices.containsKey(ownIdFromMessage(baseMsg))) {
308                         logger.debug(
309                                 "discoverByActivation: we just requested status for this device and it's ON -> it's a switch");
310                     } else {
311                         OpenGateway gw = gateway;
312                         if (gw != null) {
313                             try {
314                                 discoveringDevices.put(ownIdFromMessage(baseMsg),
315                                         Long.valueOf(System.currentTimeMillis()));
316                                 gw.send(Lighting.requestStatus(baseMsg.getWhere().value()));
317                                 return;
318                             } catch (OWNException e) {
319                                 logger.warn("discoverByActivation: Exception while requesting light state: {}",
320                                         e.getMessage());
321                                 return;
322                             }
323                         }
324                     }
325                 }
326                 discoveringDevices.remove(ownIdFromMessage(baseMsg));
327             }
328             OpenDeviceType type = null;
329             try {
330                 type = bmsg.detectDeviceType();
331             } catch (FrameException e) {
332                 logger.warn("Exception while detecting device type: {}", e.getMessage());
333             }
334             if (type != null) {
335                 discService.newDiscoveryResult(bmsg.getWhere(), type, bmsg);
336             } else {
337                 logger.debug("discoverByActivation: no device type detected from msg: {}", bmsg);
338             }
339         }
340     }
341
342     /**
343      * Register a device ThingHandler to this BridgHandler
344      *
345      * @param ownId the device OpenWebNet id
346      * @param thingHandler the thing handler to be registered
347      */
348     protected void registerDevice(String ownId, OpenWebNetThingHandler thingHandler) {
349         if (registeredDevices.containsKey(ownId)) {
350             logger.warn("registering device with an existing ownId={}", ownId);
351         }
352         registeredDevices.put(ownId, thingHandler);
353         logger.debug("registered device ownId={}, thing={}", ownId, thingHandler.getThing().getUID());
354     }
355
356     /**
357      * Un-register a device from this bridge handler
358      *
359      * @param ownId the device OpenWebNet id
360      */
361     protected void unregisterDevice(String ownId) {
362         if (registeredDevices.remove(ownId) != null) {
363             logger.debug("un-registered device ownId={}", ownId);
364         } else {
365             logger.warn("could not un-register ownId={} (not found)", ownId);
366         }
367     }
368
369     /**
370      * Get an already registered device on this bridge handler
371      *
372      * @param ownId the device OpenWebNet id
373      * @return the registered device Thing handler or null if the id cannot be found
374      */
375     public @Nullable OpenWebNetThingHandler getRegisteredDevice(String ownId) {
376         return registeredDevices.get(ownId);
377     }
378
379     @Override
380     public void onEventMessage(@Nullable OpenMessage msg) {
381         logger.trace("RECEIVED <<<<< {}", msg);
382         if (msg == null) {
383             logger.warn("received event msg is null");
384             return;
385         }
386         if (msg.isACK() || msg.isNACK()) {
387             return; // we ignore ACKS/NACKS
388         }
389         // GATEWAY MANAGEMENT
390         if (msg instanceof GatewayMgmt) {
391             // noop
392             return;
393         }
394
395         BaseOpenMessage baseMsg = (BaseOpenMessage) msg;
396         // let's try to get the Thing associated with this message...
397         if (baseMsg instanceof Lighting || baseMsg instanceof Automation || baseMsg instanceof EnergyManagement) {
398             String ownId = ownIdFromMessage(baseMsg);
399             logger.debug("ownIdFromMessage({}) --> {}", baseMsg, ownId);
400             OpenWebNetThingHandler deviceHandler = registeredDevices.get(ownId);
401             if (deviceHandler == null) {
402                 OpenGateway gw = gateway;
403                 if (isBusGateway && ((gw != null && !gw.isDiscovering() && scanIsActive)
404                         || (discoveryByActivation && !scanIsActive))) {
405                     discoverByActivation(baseMsg);
406                 } else {
407                     logger.debug("ownId={} has NO DEVICE associated, ignoring it", ownId);
408                 }
409             } else {
410                 deviceHandler.handleMessage(baseMsg);
411             }
412         } else {
413             logger.debug("BridgeHandler ignoring frame {}. WHO={} is not supported by this binding", baseMsg,
414                     baseMsg.getWho());
415         }
416     }
417
418     @Override
419     public void onConnected() {
420         isGatewayConnected = true;
421         Map<String, String> properties = editProperties();
422         boolean propertiesChanged = false;
423         OpenGateway gw = gateway;
424         if (gw == null) {
425             logger.warn("received onConnected() but gateway is null");
426             return;
427         }
428         if (gw instanceof USBGateway) {
429             logger.info("---- CONNECTED to ZigBee USB gateway bridge '{}' (serialPort: {})", thing.getUID(),
430                     ((USBGateway) gw).getSerialPortName());
431         } else {
432             logger.info("---- CONNECTED to BUS gateway bridge '{}' ({}:{})", thing.getUID(),
433                     ((BUSGateway) gw).getHost(), ((BUSGateway) gw).getPort());
434             // update serial number property (with MAC address)
435             if (properties.get(PROPERTY_SERIAL_NO) != gw.getMACAddr().toUpperCase()) {
436                 properties.put(PROPERTY_SERIAL_NO, gw.getMACAddr().toUpperCase());
437                 propertiesChanged = true;
438                 logger.debug("updated property gw serialNumber: {}", properties.get(PROPERTY_SERIAL_NO));
439             }
440         }
441         if (properties.get(PROPERTY_FIRMWARE_VERSION) != gw.getFirmwareVersion()) {
442             properties.put(PROPERTY_FIRMWARE_VERSION, gw.getFirmwareVersion());
443             propertiesChanged = true;
444             logger.debug("updated property gw firmware version: {}", properties.get(PROPERTY_FIRMWARE_VERSION));
445         }
446         if (propertiesChanged) {
447             updateProperties(properties);
448             logger.info("properties updated for bridge '{}'", thing.getUID());
449         }
450         updateStatus(ThingStatus.ONLINE);
451     }
452
453     @Override
454     public void onConnectionError(@Nullable OWNException error) {
455         String errMsg;
456         if (error == null) {
457             errMsg = "unknown error";
458         } else {
459             errMsg = error.getMessage();
460         }
461         logger.info("---- ON CONNECTION ERROR for gateway {}: {}", gateway, errMsg);
462         isGatewayConnected = false;
463         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, errMsg);
464         tryReconnectGateway();
465     }
466
467     @Override
468     public void onConnectionClosed() {
469         isGatewayConnected = false;
470         logger.debug("onConnectionClosed() - isGatewayConnected={}", isGatewayConnected);
471         // NOTE: cannot change to OFFLINE here because we are already in REMOVING state
472     }
473
474     @Override
475     public void onDisconnected(@Nullable OWNException e) {
476         isGatewayConnected = false;
477         String errMsg;
478         if (e == null) {
479             errMsg = "unknown error";
480         } else {
481             errMsg = e.getMessage();
482         }
483         logger.info("---- DISCONNECTED from gateway {}. OWNException: {}", gateway, errMsg);
484         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
485                 "Disconnected from gateway (onDisconnected - " + errMsg + ")");
486         tryReconnectGateway();
487     }
488
489     private void tryReconnectGateway() {
490         OpenGateway gw = gateway;
491         if (gw != null) {
492             if (!reconnecting) {
493                 reconnecting = true;
494                 logger.info("---- Starting RECONNECT cycle to gateway {}", gw);
495                 try {
496                     gw.reconnect();
497                 } catch (OWNAuthException e) {
498                     logger.info("---- AUTH error from gateway. Stopping re-connect");
499                     reconnecting = false;
500                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
501                             "Authentication error. Check gateway password in Thing Configuration Parameters (" + e
502                                     + ")");
503                 }
504             } else {
505                 logger.debug("---- reconnecting=true, do nothing");
506             }
507         } else {
508             logger.warn("---- cannot start RECONNECT, gateway is null");
509         }
510     }
511
512     @Override
513     public void onReconnected() {
514         reconnecting = false;
515         OpenGateway gw = gateway;
516         logger.info("---- RE-CONNECTED to bridge {}", thing.getUID());
517         if (gw != null) {
518             updateStatus(ThingStatus.ONLINE);
519             if (gw.getFirmwareVersion() != null) {
520                 this.updateProperty(PROPERTY_FIRMWARE_VERSION, gw.getFirmwareVersion());
521                 logger.debug("gw firmware version: {}", gw.getFirmwareVersion());
522             }
523         }
524     }
525
526     /**
527      * Return a ownId string (=WHO.WHERE) from the device Where address and handler
528      *
529      * @param where the Where address (to be normalized)
530      * @param handler the device handler
531      * @return the ownId String
532      */
533     protected String ownIdFromDeviceWhere(Where where, OpenWebNetThingHandler handler) {
534         return handler.ownIdPrefix() + "." + normalizeWhere(where);
535     }
536
537     /**
538      * Returns a ownId string (=WHO.WHERE) from a Who and Where address
539      *
540      * @param who the Who
541      * @param where the Where address (to be normalized)
542      * @return the ownId String
543      */
544     public String ownIdFromWhoWhere(Who who, Where where) {
545         return who.value() + "." + normalizeWhere(where);
546     }
547
548     /**
549      * Return a ownId string (=WHO.WHERE) from a BaseOpenMessage
550      *
551      * @param baseMsg the BaseOpenMessage
552      * @return the ownId String
553      */
554     public String ownIdFromMessage(BaseOpenMessage baseMsg) {
555         return baseMsg.getWho().value() + "." + normalizeWhere(baseMsg.getWhere());
556     }
557
558     /**
559      * Transform a Where address into a Thing id string
560      *
561      * @param where the Where address
562      * @return the thing Id string
563      */
564     public String thingIdFromWhere(Where where) {
565         return normalizeWhere(where); // '#' cannot be used in ThingUID;
566     }
567
568     /**
569      * Normalize a Where address
570      *
571      * @param where the Where address
572      * @return the normalized address as String
573      */
574     public String normalizeWhere(Where where) {
575         String str = where.value();
576         if (where instanceof WhereZigBee) {
577             str = ((WhereZigBee) where).valueWithUnit(WhereZigBee.UNIT_ALL); // 76543210X#9 --> 765432100#9
578         } else {
579             if (str.indexOf("#4#") == -1) { // skip APL#4#bus case
580                 if (str.indexOf('#') == 0) { // Thermo central unit (#0) or zone via central unit (#Z, Z=[1-99]) --> Z
581                     str = str.substring(1);
582                 } else if (str.indexOf('#') > 0) { // Thermo zone Z and actuator N (Z#N, Z=[1-99], N=[1-9]) --> Z
583                     str = str.substring(0, str.indexOf('#'));
584                 }
585             }
586         }
587         return str.replace('#', 'h');
588     }
589 }