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