]> git.basschouten.com Git - openhab-addons.git/blob
3187de37e3b65d8f9a3b8937e56b00b1d149c8b8
[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.Where;
53 import org.openwebnet4j.message.Who;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
56
57 /**
58  * The {@link OpenWebNetBridgeHandler} is responsible for handling communication with gateways and handling events.
59  *
60  * @author Massimo Valla - Initial contribution
61  */
62 @NonNullByDefault
63 public class OpenWebNetBridgeHandler extends ConfigStatusBridgeHandler implements GatewayListener {
64
65     private final Logger logger = LoggerFactory.getLogger(OpenWebNetBridgeHandler.class);
66
67     private static final int GATEWAY_ONLINE_TIMEOUT_SEC = 20; // Time to wait for the gateway to become connected
68
69     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = OpenWebNetBindingConstants.BRIDGE_SUPPORTED_THING_TYPES;
70
71     // ConcurrentHashMap of devices registered to this BridgeHandler
72     // association is: ownId (String) -> OpenWebNetThingHandler, with ownId = WHO.WHERE
73     private Map<String, OpenWebNetThingHandler> registeredDevices = new ConcurrentHashMap<>();
74
75     protected @Nullable OpenGateway gateway;
76     private boolean isBusGateway = false;
77
78     private boolean isGatewayConnected = false;
79
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;
84
85     public OpenWebNetBridgeHandler(Bridge bridge) {
86         super(bridge);
87     }
88
89     public boolean isBusGateway() {
90         return isBusGateway;
91     }
92
93     @Override
94     public void initialize() {
95         ThingTypeUID thingType = getThing().getThingTypeUID();
96         OpenGateway gw;
97         if (thingType.equals(THING_TYPE_ZB_GATEWAY)) {
98             gw = initZigBeeGateway();
99         } else {
100             gw = initBusGateway();
101             isBusGateway = true;
102         }
103         if (gw != null) {
104             gateway = gw;
105             gw.subscribe(this);
106             if (gw.isConnected()) { // gateway is already connected, device can go ONLINE
107                 isGatewayConnected = true;
108                 updateStatus(ThingStatus.ONLINE);
109             } else {
110                 updateStatus(ThingStatus.UNKNOWN);
111                 logger.debug("Trying to connect gateway...");
112                 try {
113                     gw.connect();
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");
120                         }
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()
125                 }
126             }
127         }
128     }
129
130     /**
131      * Init a ZigBee gateway based on config
132      */
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");
141             return null;
142         } else {
143             return new USBGateway(serialPort);
144         }
145     }
146
147     /**
148      * Init a BUS gateway based on config
149      */
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");
158             return null;
159         } else {
160             int port = busBridgeConfig.getPort().intValue();
161             String passwd = busBridgeConfig.getPasswd();
162             String passwdMasked;
163             if (passwd.length() >= 4) {
164                 passwdMasked = "******" + passwd.substring(passwd.length() - 3, passwd.length());
165             } else {
166                 passwdMasked = "******";
167             }
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);
172         }
173     }
174
175     @Override
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");
181             return;
182         } else {
183             logger.warn("Channel not supported: channel={}", channelUID);
184         }
185     }
186
187     @Override
188     public Collection<ConfigStatusMessage> getConfigStatus() {
189         return Collections.emptyList();
190     }
191
192     @Override
193     public void handleRemoval() {
194         disconnectGateway();
195         super.handleRemoval();
196     }
197
198     @Override
199     public void dispose() {
200         disconnectGateway();
201         super.dispose();
202     }
203
204     private void disconnectGateway() {
205         OpenGateway gw = gateway;
206         if (gw != null) {
207             gw.closeConnection();
208             gw.unsubscribe(this);
209             logger.debug("gateway {} connection closed and unsubscribed", gw.toString());
210             gateway = null;
211         }
212         reconnecting = false;
213     }
214
215     @Override
216     public Collection<Class<? extends ThingHandlerService>> getServices() {
217         return Collections.singleton(OpenWebNetDeviceDiscoveryService.class);
218     }
219
220     /**
221      * Search for devices connected to this bridge handler's gateway
222      *
223      * @param listener to receive device found notifications
224      */
225     public synchronized void searchDevices() {
226         scanIsActive = true;
227         logger.debug("------$$ scanIsActive={}", scanIsActive);
228         OpenGateway gw = gateway;
229         if (gw != null) {
230             if (!gw.isDiscovering()) {
231                 if (!gw.isConnected()) {
232                     logger.debug("------$$ Gateway is NOT connected, cannot search for devices");
233                     return;
234                 }
235                 logger.info("------$$ STARTED active SEARCH for devices on gateway '{}'", this.getThing().getUID());
236                 try {
237                     gw.discoverDevices();
238                 } catch (OWNException e) {
239                     logger.warn("------$$ OWNException while discovering devices on gateway {}: {}",
240                             this.getThing().getUID(), e.getMessage());
241                 }
242             } else {
243                 logger.debug("------$$ Searching devices on gateway {} already activated", this.getThing().getUID());
244                 return;
245             }
246         } else {
247             logger.debug("------$$ Cannot search devices: no gateway associated to this handler");
248         }
249     }
250
251     @Override
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);
257             } else {
258                 logger.warn("onNewDevice with null where/deviceType, msg={}", message);
259             }
260         } else {
261             logger.warn("onNewDevice but null deviceDiscoveryService");
262         }
263     }
264
265     @Override
266     public void onDiscoveryCompleted() {
267         logger.info("------$$ FINISHED active SEARCH for devices on gateway '{}'", this.getThing().getUID());
268     }
269
270     /**
271      * Notifies that the scan has been stopped/aborted by OpenWebNetDeviceDiscoveryService
272      */
273     public void scanStopped() {
274         scanIsActive = false;
275         logger.debug("------$$ scanIsActive={}", scanIsActive);
276     }
277
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);
283             return;
284         }
285         if (baseMsg instanceof Lighting) {
286             OpenDeviceType type = null;
287             try {
288                 type = baseMsg.detectDeviceType();
289             } catch (FrameException e) {
290                 logger.warn("Exception while detecting device type: {}", e.getMessage());
291             }
292             if (type != null) {
293                 discService.newDiscoveryResult(baseMsg.getWhere(), type, baseMsg);
294             } else {
295                 logger.debug("discoverByActivation: no device type detected from msg: {}", baseMsg);
296             }
297         }
298     }
299
300     /**
301      * Register a device ThingHandler to this BridgHandler
302      *
303      * @param ownId the device OpenWebNet id
304      * @param thingHandler the thing handler to be registered
305      */
306     protected void registerDevice(String ownId, OpenWebNetThingHandler thingHandler) {
307         if (registeredDevices.containsKey(ownId)) {
308             logger.warn("registering device with an existing ownId={}", ownId);
309         }
310         registeredDevices.put(ownId, thingHandler);
311         logger.debug("registered device ownId={}, thing={}", ownId, thingHandler.getThing().getUID());
312     }
313
314     /**
315      * Un-register a device from this bridge handler
316      *
317      * @param ownId the device OpenWebNet id
318      */
319     protected void unregisterDevice(String ownId) {
320         if (registeredDevices.remove(ownId) != null) {
321             logger.debug("un-registered device ownId={}", ownId);
322         } else {
323             logger.warn("could not un-register ownId={} (not found)", ownId);
324         }
325     }
326
327     /**
328      * Get an already registered device on this bridge handler
329      *
330      * @param ownId the device OpenWebNet id
331      * @return the registered device Thing handler or null if the id cannot be found
332      */
333     public @Nullable OpenWebNetThingHandler getRegisteredDevice(String ownId) {
334         return registeredDevices.get(ownId);
335     }
336
337     @Override
338     public void onEventMessage(@Nullable OpenMessage msg) {
339         logger.trace("RECEIVED <<<<< {}", msg);
340         if (msg == null) {
341             logger.warn("received event msg is null");
342             return;
343         }
344         if (msg.isACK() || msg.isNACK()) {
345             return; // we ignore ACKS/NACKS
346         }
347         // GATEWAY MANAGEMENT
348         if (msg instanceof GatewayMgmt) {
349             // noop
350             return;
351         }
352
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);
364                 } else {
365                     logger.debug("ownId={} has NO DEVICE associated, ignoring it", ownId);
366                 }
367             } else {
368                 deviceHandler.handleMessage(baseMsg);
369             }
370         } else {
371             logger.debug("BridgeHandler ignoring frame {}. WHO={} is not supported by this binding", baseMsg,
372                     baseMsg.getWho());
373         }
374     }
375
376     @Override
377     public void onConnected() {
378         isGatewayConnected = true;
379         Map<String, String> properties = editProperties();
380         boolean propertiesChanged = false;
381         OpenGateway gw = gateway;
382         if (gw == null) {
383             logger.warn("received onConnected() but gateway is null");
384             return;
385         }
386         if (gw instanceof USBGateway) {
387             logger.info("------------------- CONNECTED to USB (ZigBee) gateway - USB port: {}",
388                     ((USBGateway) gw).getSerialPortName());
389         } else {
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));
397             }
398         }
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));
403         }
404         if (propertiesChanged) {
405             updateProperties(properties);
406             logger.info("properties updated for '{}'", thing.getUID());
407         }
408         updateStatus(ThingStatus.ONLINE);
409     }
410
411     @Override
412     public void onConnectionError(@Nullable OWNException error) {
413         String errMsg;
414         if (error == null) {
415             errMsg = "unknown error";
416         } else {
417             errMsg = error.getMessage();
418         }
419         logger.info("------------------- ON CONNECTION ERROR: {}", errMsg);
420         isGatewayConnected = false;
421         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, errMsg);
422         tryReconnectGateway();
423     }
424
425     @Override
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
430     }
431
432     @Override
433     public void onDisconnected(@Nullable OWNException e) {
434         isGatewayConnected = false;
435         String errMsg;
436         if (e == null) {
437             errMsg = "unknown error";
438         } else {
439             errMsg = e.getMessage();
440         }
441         logger.info("------------------- DISCONNECTED from gateway. OWNException={}", errMsg);
442         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
443                 "Disconnected from gateway (onDisconnected - " + errMsg + ")");
444         tryReconnectGateway();
445     }
446
447     private void tryReconnectGateway() {
448         OpenGateway gw = gateway;
449         if (gw != null) {
450             if (!reconnecting) {
451                 reconnecting = true;
452                 logger.info("------------------- Starting RECONNECT cycle to gateway");
453                 try {
454                     gw.reconnect();
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
460                                     + ")");
461                 }
462             } else {
463                 logger.debug("------------------- reconnecting=true, do nothing");
464             }
465         } else {
466             logger.debug("------------------- cannot start RECONNECT, gateway is null");
467         }
468     }
469
470     @Override
471     public void onReconnected() {
472         reconnecting = false;
473         logger.info("------------------- RE-CONNECTED to gateway!");
474         OpenGateway gw = gateway;
475         if (gw != null) {
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());
480             }
481         }
482     }
483
484     /**
485      * Return a ownId string (=WHO.WHERE) from a deviceWhere thing config parameter (already normalized) and its
486      * handler.
487      *
488      * @param deviceWhere the device WHERE config parameter
489      * @param handler the thing handler
490      * @return the ownId
491      */
492     protected String ownIdFromDeviceWhere(@Nullable String deviceWhere, OpenWebNetThingHandler handler) {
493         return handler.ownIdPrefix() + "." + deviceWhere;
494     }
495
496     /**
497      * Returns a ownId string (=WHO.WHERE) from a Where address and Who
498      *
499      * @param where the Where address (to be normalized)
500      * @param who the Who
501      * @return the ownId
502      */
503     public String ownIdFromWhoWhere(Where where, Who who) {
504         return who.value() + "." + normalizeWhere(where);
505     }
506
507     /**
508      * Return a ownId string (=WHO.WHERE) from a BaseOpenMessage
509      *
510      * @param baseMsg the BaseOpenMessage
511      * @return the ownId String
512      */
513     private String ownIdFromMessage(BaseOpenMessage baseMsg) {
514         return baseMsg.getWho().value() + "." + normalizeWhere(baseMsg.getWhere());
515     }
516
517     /**
518      * Transform a Where address into a Thing id string based on bridge type (BUS/USB ZigBee).
519      * '#' in WHERE are changed to 'h'
520      *
521      * @param where the Where address
522      * @return the thing Id
523      */
524     public String thingIdFromWhere(Where where) {
525         return normalizeWhere(where).replace('#', 'h'); // '#' cannot be used in ThingUID;
526     }
527
528     /**
529      * Normalize a Where address for Thermo and Zigbee devices
530      *
531      * @param where the Where address
532      * @return the normalized address
533      */
534     public String normalizeWhere(Where where) {
535         String str = "";
536         if (isBusGateway) {
537             if (where.value().indexOf('#') < 0) { // no hash present
538                 str = where.value();
539             } else if (where.value().indexOf("#4#") > 0) { // local bus: APL#4#bus
540                 str = where.value();
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('#'));
545             } else {
546                 logger.warn("normalizeWhere() unexpected WHERE: {}", where);
547                 str = where.value();
548             }
549             return str;
550         } else {
551             return where.value();
552         }
553     }
554 }