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