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