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