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