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