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