]> git.basschouten.com Git - openhab-addons.git/blob
b917aca2ae8618a9e83e8fa56f4178b805a65326
[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, @Nullable 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 oId the device OpenWebNet id
318      */
319     protected void unregisterDevice(String oId) {
320         if (registeredDevices.remove(oId) != null) {
321             logger.debug("un-registered device ownId={}", oId);
322         } else {
323             logger.warn("could not un-register ownId={} (not found)", oId);
324         }
325     }
326
327     @Override
328     public void onEventMessage(@Nullable OpenMessage msg) {
329         logger.trace("RECEIVED <<<<< {}", msg);
330         if (msg == null) {
331             logger.warn("received event msg is null");
332             return;
333         }
334         if (msg.isACK() || msg.isNACK()) {
335             return; // we ignore ACKS/NACKS
336         }
337         // GATEWAY MANAGEMENT
338         if (msg instanceof GatewayMgmt) {
339             // noop
340             return;
341         }
342
343         BaseOpenMessage baseMsg = (BaseOpenMessage) msg;
344         // let's try to get the Thing associated with this message...
345         if (baseMsg instanceof Lighting || baseMsg instanceof Automation) {
346             String ownId = ownIdFromMessage(baseMsg);
347             logger.debug("ownId={}", ownId);
348             OpenWebNetThingHandler deviceHandler = registeredDevices.get(ownId);
349             if (deviceHandler == null) {
350                 OpenGateway gw = gateway;
351                 if (isBusGateway && ((gw != null && !gw.isDiscovering() && scanIsActive)
352                         || (discoveryByActivation && !scanIsActive))) {
353                     discoverByActivation(baseMsg);
354                 } else {
355                     logger.debug("ownId={} has NO DEVICE associated, ignoring it", ownId);
356                 }
357             } else {
358                 deviceHandler.handleMessage(baseMsg);
359             }
360         } else {
361             logger.debug("BridgeHandler ignoring frame {}. WHO={} is not supported by this binding", baseMsg,
362                     baseMsg.getWho());
363         }
364     }
365
366     @Override
367     public void onConnected() {
368         isGatewayConnected = true;
369         Map<String, String> properties = editProperties();
370         boolean propertiesChanged = false;
371         OpenGateway gw = gateway;
372         if (gw == null) {
373             logger.warn("received onConnected() but gateway is null");
374             return;
375         }
376         if (gw instanceof USBGateway) {
377             logger.info("------------------- CONNECTED to USB (ZigBee) gateway - USB port: {}",
378                     ((USBGateway) gw).getSerialPortName());
379         } else {
380             logger.info("------------------- CONNECTED to BUS gateway '{}' ({}:{})", thing.getUID(),
381                     ((BUSGateway) gw).getHost(), ((BUSGateway) gw).getPort());
382             // update serial number property (with MAC address)
383             if (properties.get(PROPERTY_SERIAL_NO) != gw.getMACAddr().toUpperCase()) {
384                 properties.put(PROPERTY_SERIAL_NO, gw.getMACAddr().toUpperCase());
385                 propertiesChanged = true;
386                 logger.debug("updated property gw serialNumber: {}", properties.get(PROPERTY_SERIAL_NO));
387             }
388         }
389         if (properties.get(PROPERTY_FIRMWARE_VERSION) != gw.getFirmwareVersion()) {
390             properties.put(PROPERTY_FIRMWARE_VERSION, gw.getFirmwareVersion());
391             propertiesChanged = true;
392             logger.debug("updated property gw firmware version: {}", properties.get(PROPERTY_FIRMWARE_VERSION));
393         }
394         if (propertiesChanged) {
395             updateProperties(properties);
396             logger.info("properties updated for '{}'", thing.getUID());
397         }
398         updateStatus(ThingStatus.ONLINE);
399     }
400
401     @Override
402     public void onConnectionError(@Nullable OWNException error) {
403         String errMsg;
404         if (error == null) {
405             errMsg = "unknown error";
406         } else {
407             errMsg = error.getMessage();
408         }
409         logger.info("------------------- ON CONNECTION ERROR: {}", errMsg);
410         isGatewayConnected = false;
411         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, errMsg);
412         tryReconnectGateway();
413     }
414
415     @Override
416     public void onConnectionClosed() {
417         isGatewayConnected = false;
418         logger.debug("onConnectionClosed() - isGatewayConnected={}", isGatewayConnected);
419         // NOTE: cannot change to OFFLINE here because we are already in REMOVING state
420     }
421
422     @Override
423     public void onDisconnected(@Nullable OWNException e) {
424         isGatewayConnected = false;
425         String errMsg;
426         if (e == null) {
427             errMsg = "unknown error";
428         } else {
429             errMsg = e.getMessage();
430         }
431         logger.info("------------------- DISCONNECTED from gateway. OWNException={}", errMsg);
432         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
433                 "Disconnected from gateway (onDisconnected - " + errMsg + ")");
434         tryReconnectGateway();
435     }
436
437     private void tryReconnectGateway() {
438         OpenGateway gw = gateway;
439         if (gw != null) {
440             if (!reconnecting) {
441                 reconnecting = true;
442                 logger.info("------------------- Starting RECONNECT cycle to gateway");
443                 try {
444                     gw.reconnect();
445                 } catch (OWNAuthException e) {
446                     logger.info("------------------- AUTH error from gateway. Stopping reconnect");
447                     reconnecting = false;
448                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
449                             "Authentication error. Check gateway password in Thing Configuration Parameters (" + e
450                                     + ")");
451                 }
452             } else {
453                 logger.debug("------------------- reconnecting=true, do nothing");
454             }
455         } else {
456             logger.debug("------------------- cannot start RECONNECT, gateway is null");
457         }
458     }
459
460     @Override
461     public void onReconnected() {
462         reconnecting = false;
463         logger.info("------------------- RE-CONNECTED to gateway!");
464         OpenGateway gw = gateway;
465         if (gw != null) {
466             updateStatus(ThingStatus.ONLINE);
467             if (gw.getFirmwareVersion() != null) {
468                 this.updateProperty(PROPERTY_FIRMWARE_VERSION, gw.getFirmwareVersion());
469                 logger.debug("gw firmware version: {}", gw.getFirmwareVersion());
470             }
471         }
472     }
473
474     /**
475      * Return a ownId string (=WHO.WHERE) from a deviceWhere thing config parameter (already normalized) and its
476      * handler.
477      *
478      * @param deviceWhere the device WHERE config parameter
479      * @param handler the thing handler
480      * @return the ownId
481      */
482     protected String ownIdFromDeviceWhere(@Nullable String deviceWhere, OpenWebNetThingHandler handler) {
483         return handler.ownIdPrefix() + "." + deviceWhere;
484     }
485
486     /**
487      * Returns a ownId string (=WHO.WHERE) from a Where address and Who
488      *
489      * @param where the Where address (to be normalized)
490      * @param who the Who
491      * @return the ownId
492      */
493     public String ownIdFromWhoWhere(Where where, Who who) {
494         return who.value() + "." + normalizeWhere(where);
495     }
496
497     /**
498      * Return a ownId string (=WHO.WHERE) from a BaseOpenMessage
499      *
500      * @param baseMsg the BaseOpenMessage
501      * @return the ownId String
502      */
503     private String ownIdFromMessage(BaseOpenMessage baseMsg) {
504         return baseMsg.getWho().value() + "." + normalizeWhere(baseMsg.getWhere());
505     }
506
507     /**
508      * Transform a Where address into a Thing id string based on bridge type (BUS/USB ZigBee).
509      * '#' in WHERE are changed to 'h'
510      *
511      * @param where the Where address
512      * @return the thing Id
513      */
514     public String thingIdFromWhere(Where where) {
515         return normalizeWhere(where).replace('#', 'h'); // '#' cannot be used in ThingUID;
516     }
517
518     /**
519      * Normalize a Where address for Thermo and Zigbee devices
520      *
521      * @param where the Where address
522      * @return the normalized address
523      */
524     public String normalizeWhere(Where where) {
525         String str = "";
526         if (isBusGateway) {
527             if (where.value().indexOf('#') < 0) { // no hash present
528                 str = where.value();
529             } else if (where.value().indexOf("#4#") > 0) { // local bus: APL#4#bus
530                 str = where.value();
531             } else if (where.value().indexOf('#') == 0) { // thermo zone via central unit: #0 or #Z (Z=[1-99]) --> Z
532                 str = where.value().substring(1);
533             } else if (where.value().indexOf('#') > 0) { // thermo zone and actuator N: Z#N (Z=[1-99], N=[1-9]) -- > Z
534                 str = where.value().substring(0, where.value().indexOf('#'));
535             } else {
536                 logger.warn("normalizeWhere() unexpected WHERE: {}", where);
537                 str = where.value();
538             }
539             return str;
540         } else {
541             return where.value();
542         }
543     }
544 }