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