]> git.basschouten.com Git - openhab-addons.git/blob
1e18259abe1c340cea43653fc8d3ad038e065366
[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.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 Set.of(OpenWebNetDeviceDiscoveryService.class);
272     }
273
274     /**
275      * Search for devices connected to this bridge handler's gateway
276      */
277     public synchronized void searchDevices() {
278         scanIsActive = true;
279         logger.debug("------$$ scanIsActive={}", scanIsActive);
280         OpenGateway gw = gateway;
281         if (gw != null) {
282             if (!gw.isDiscovering()) {
283                 if (!gw.isConnected()) {
284                     logger.debug("------$$ Gateway '{}' is NOT connected, cannot search for devices", gw);
285                     return;
286                 }
287                 logger.info("------$$ STARTED active SEARCH for devices on bridge '{}'", thing.getUID());
288                 try {
289                     gw.discoverDevices();
290                 } catch (OWNException e) {
291                     logger.warn("------$$ OWNException while discovering devices on bridge '{}': {}", thing.getUID(),
292                             e.getMessage());
293                 }
294             } else {
295                 logger.debug("------$$ Searching devices on bridge '{}' already activated", thing.getUID());
296                 return;
297             }
298         } else {
299             logger.warn("------$$ Cannot search devices: no gateway associated to this handler");
300         }
301     }
302
303     @Override
304     public void onNewDevice(@Nullable Where w, @Nullable OpenDeviceType deviceType, @Nullable BaseOpenMessage message) {
305         OpenWebNetDeviceDiscoveryService discService = deviceDiscoveryService;
306         if (discService != null) {
307             if (deviceType != null) {
308                 discService.newDiscoveryResult(w, deviceType, message);
309             } else {
310                 logger.warn("onNewDevice with null deviceType, msg={}", message);
311             }
312         } else {
313             logger.warn("onNewDevice but null deviceDiscoveryService");
314         }
315     }
316
317     @Override
318     public void onDiscoveryCompleted() {
319         logger.info("------$$ FINISHED active SEARCH for devices on bridge '{}'", thing.getUID());
320     }
321
322     /**
323      * Notifies that the scan has been stopped/aborted by
324      * OpenWebNetDeviceDiscoveryService
325      */
326     public void scanStopped() {
327         scanIsActive = false;
328         logger.debug("------$$ scanIsActive={}", scanIsActive);
329     }
330
331     private void discoverByActivation(BaseOpenMessage baseMsg) {
332         logger.debug("discoverByActivation: msg={}", baseMsg);
333         OpenWebNetDeviceDiscoveryService discService = deviceDiscoveryService;
334         if (discService == null) {
335             logger.warn("discoverByActivation: null OpenWebNetDeviceDiscoveryService, ignoring msg={}", baseMsg);
336             return;
337         }
338         // we support these types only
339         if (baseMsg instanceof Lighting || baseMsg instanceof Automation || baseMsg instanceof EnergyManagement
340                 || baseMsg instanceof Thermoregulation || baseMsg instanceof CEN || baseMsg instanceof Scenario
341                 || baseMsg instanceof Alarm) {
342             BaseOpenMessage bmsg = baseMsg;
343             if (baseMsg instanceof Lighting) {
344                 What what = baseMsg.getWhat();
345                 if (Lighting.WhatLighting.OFF.equals(what)) { // skipping OFF msg: cannot distinguish dimmer/switch
346                     logger.debug("discoverByActivation: skipping OFF msg: cannot distinguish dimmer/switch");
347                     return;
348                 }
349                 if (Lighting.WhatLighting.ON.equals(what)) { // if not already done just now, request light status to
350                     // distinguish dimmer from switch
351                     if (discoveringDevices.containsKey(ownIdFromMessage(baseMsg))) {
352                         logger.debug(
353                                 "discoverByActivation: we just requested status for this device and it's ON -> it's a switch");
354                     } else {
355                         OpenGateway gw = gateway;
356                         if (gw != null) {
357                             try {
358                                 discoveringDevices.put(ownIdFromMessage(baseMsg),
359                                         Long.valueOf(System.currentTimeMillis()));
360                                 gw.send(Lighting.requestStatus(baseMsg.getWhere().value()));
361                                 return;
362                             } catch (OWNException e) {
363                                 logger.warn("discoverByActivation: Exception while requesting light state: {}",
364                                         e.getMessage());
365                                 return;
366                             }
367                         }
368                     }
369                 }
370                 discoveringDevices.remove(ownIdFromMessage(baseMsg));
371             }
372             OpenDeviceType type = null;
373             try {
374                 type = bmsg.detectDeviceType();
375             } catch (FrameException e) {
376                 logger.warn("Exception while detecting device type: {}", e.getMessage());
377             }
378             if (type != null) {
379                 discService.newDiscoveryResult(bmsg.getWhere(), type, bmsg);
380             } else {
381                 logger.debug("discoverByActivation: no device type detected from msg: {}", bmsg);
382             }
383         }
384     }
385
386     /**
387      * Register a device ThingHandler to this BridgHandler
388      *
389      * @param ownId the device OpenWebNet id
390      * @param thingHandler the thing handler to be registered
391      */
392     protected void registerDevice(String ownId, OpenWebNetThingHandler thingHandler) {
393         if (registeredDevices.containsKey(ownId)) {
394             logger.warn("registering device with an existing ownId={}", ownId);
395         }
396         registeredDevices.put(ownId, thingHandler);
397         lastRegisteredDeviceTS = System.currentTimeMillis();
398         logger.debug("registered device ownId={}, thing={}", ownId, thingHandler.getThing().getUID());
399     }
400
401     /**
402      * Un-register a device from this bridge handler
403      *
404      * @param ownId the device OpenWebNet id
405      */
406     protected void unregisterDevice(String ownId) {
407         if (registeredDevices.remove(ownId) != null) {
408             logger.debug("un-registered device ownId={}", ownId);
409         } else {
410             logger.warn("could not un-register ownId={} (not found)", ownId);
411         }
412     }
413
414     /**
415      * Get an already registered device on this bridge handler
416      *
417      * @param ownId the device OpenWebNet id
418      * @return the registered device Thing handler or null if the id cannot be found
419      */
420     public @Nullable OpenWebNetThingHandler getRegisteredDevice(String ownId) {
421         return registeredDevices.get(ownId);
422     }
423
424     private void refreshAllBridgeDevices() {
425         logger.debug("--- --- ABOUT TO REFRESH ALL devices for bridge {}", thing.getUID());
426         int howMany = 0;
427         final List<Thing> things = getThing().getThings();
428         int total = things.size();
429         logger.debug("--- FOUND {} things by getThings()", total);
430         if (total > 0) {
431             if (registeredDevices.isEmpty()) { // no registered device yet
432                 if (refreshAllDevicesDelay < REFRESH_ALL_DEVICES_DELAY_MAX_MSEC) {
433                     logger.debug("--- REGISTER device not started yet... re-scheduling refreshAllBridgeDevices()");
434                     refreshAllDevicesDelay += REFRESH_ALL_DEVICES_DELAY_MSEC * 3;
435                     refreshAllSchedule = scheduler.schedule(this::refreshAllBridgeDevices,
436                             REFRESH_ALL_DEVICES_DELAY_MSEC * 3, TimeUnit.MILLISECONDS);
437                     return;
438                 } else {
439                     logger.warn(
440                             "--- --- NONE OF {} CHILD DEVICE(S) has REGISTERED with bridge {}: check Things configuration (stopping refreshAllBridgeDevices)",
441                             total, thing.getUID());
442                     refreshAllDevicesDelay = 0;
443                     return;
444                 }
445             } else if (System.currentTimeMillis() - lastRegisteredDeviceTS < REFRESH_ALL_DEVICES_DELAY_MSEC) {
446                 // a device has been registered with the bridge just now, let's wait for other
447                 // devices: re-schedule
448                 // refreshAllDevices
449                 logger.debug("--- REGISTER device just called... re-scheduling refreshAllBridgeDevices()");
450                 refreshAllSchedule = scheduler.schedule(this::refreshAllBridgeDevices, REFRESH_ALL_DEVICES_DELAY_MSEC,
451                         TimeUnit.MILLISECONDS);
452                 return;
453             }
454             for (Thing ownThing : things) {
455                 OpenWebNetThingHandler hndlr = (OpenWebNetThingHandler) ownThing.getHandler();
456                 if (hndlr != null) {
457                     howMany++;
458                     logger.debug("--- REFRESHING ALL DEVICES FOR thing #{}/{}: {}", howMany, total, ownThing.getUID());
459                     hndlr.refreshAllDevices();
460                 } else {
461                     logger.warn("--- No handler for thing {}", ownThing.getUID());
462                 }
463             }
464             logger.debug("--- --- COMPLETED REFRESH all devices for bridge {}", thing.getUID());
465             refreshAllDevicesDelay = 0;
466             // set a check that all things are Online
467             refreshAllSchedule = scheduler.schedule(() -> checkAllRefreshed(things), REFRESH_ALL_CHECK_DELAY_SEC,
468                     TimeUnit.SECONDS);
469         } else {
470             logger.debug("--- --- NO CHILD DEVICE to REFRESH for bridge {}", thing.getUID());
471         }
472     }
473
474     private void checkAllRefreshed(List<Thing> things) {
475         int howMany = 0;
476         int total = things.size();
477         boolean allOnline = true;
478         for (Thing ownThing : things) {
479             howMany++;
480             ThingStatus ts = ownThing.getStatus();
481             if (ThingStatus.ONLINE == ts) {
482                 logger.debug("--- CHECKED ONLINE thing #{}/{}: {}", howMany, total, ownThing.getUID());
483             } else {
484                 logger.debug("--- CHECKED ^^^OFFLINE^^^ thing #{}/{}: {}", howMany, total, ownThing.getUID());
485                 allOnline = false;
486             }
487         }
488         if (allOnline) {
489             logger.debug("--- --- REFRESH CHECK COMPLETED: all things ONLINE for bridge {}", thing.getUID());
490         } else {
491             logger.debug("--- --- REFRESH CHECK COMPLETED: NOT all things ONLINE for bridge {}", thing.getUID());
492         }
493     }
494
495     @Override
496     public void onEventMessage(@Nullable OpenMessage msg) {
497         logger.trace("RECEIVED <<<<< {}", msg);
498         if (msg == null) {
499             logger.warn("received event msg is null");
500             return;
501         }
502         if (msg.isACK() || msg.isNACK()) {
503             return; // we ignore ACKS/NACKS
504         }
505         // GATEWAY MANAGEMENT
506         if (msg instanceof GatewayMgmt gwMsg) {
507             if (dateTimeSynch && GatewayMgmt.DimGatewayMgmt.DATETIME.equals(gwMsg.getDim())) {
508                 checkDateTimeDiff(gwMsg);
509             }
510             return;
511         }
512
513         BaseOpenMessage baseMsg = (BaseOpenMessage) msg;
514         // let's try to get the Thing associated with this message...
515         if (baseMsg instanceof Lighting || baseMsg instanceof Automation || baseMsg instanceof EnergyManagement
516                 || baseMsg instanceof Thermoregulation || baseMsg instanceof CEN || baseMsg instanceof Auxiliary
517                 || baseMsg instanceof Scenario || baseMsg instanceof Alarm) {
518             String ownId = ownIdFromMessage(baseMsg);
519             logger.debug("ownIdFromMessage({}) --> {}", baseMsg, ownId);
520             OpenWebNetThingHandler deviceHandler = registeredDevices.get(ownId);
521             if (deviceHandler == null) {
522                 OpenGateway gw = gateway;
523                 if (isBusGateway && ((gw != null && !gw.isDiscovering() && scanIsActive)
524                         || (discoveryByActivation && !scanIsActive))) {
525                     discoverByActivation(baseMsg);
526                 } else {
527                     logger.debug("ownId={} has NO DEVICE associated to bridge {}: ignoring it", ownId, thing.getUID());
528                 }
529             } else {
530                 deviceHandler.handleMessage(baseMsg);
531             }
532         } else {
533             logger.debug("BridgeHandler ignoring frame {}. WHO={} is not supported by this binding", baseMsg,
534                     baseMsg.getWho());
535         }
536     }
537
538     private void checkDateTimeDiff(GatewayMgmt gwMsg) {
539         try {
540             ZonedDateTime now = ZonedDateTime.now();
541             ZonedDateTime gwTime = GatewayMgmt.parseDateTime(gwMsg);
542             long diff = Math.abs(Duration.between(now, gwTime).toSeconds());
543             if (diff > DATETIME_SYNCH_DIFF_SEC) {
544                 logger.debug("checkDateTimeDiff: difference is more than 60s: {}s", diff);
545                 OpenGateway gw = gateway;
546                 if (gw != null) {
547                     logger.debug("checkDateTimeDiff: synch DateTime to: {}", now);
548                     try {
549                         gw.send(GatewayMgmt.requestSetDateTime(now));
550                     } catch (OWNException e) {
551                         logger.warn("checkDateTimeDiff: Exception while sending set DateTime command: {}",
552                                 e.getMessage());
553                     }
554                 }
555             } else {
556                 logger.debug("checkDateTimeDiff: DateTime difference: {}s", diff);
557             }
558         } catch (FrameException e) {
559             logger.warn("checkDateTimeDiff: FrameException while parsing {}", e.getMessage());
560         }
561     }
562
563     @Override
564     public void onConnected() {
565         isGatewayConnected = true;
566         Map<String, String> properties = editProperties();
567         boolean propertiesChanged = false;
568         OpenGateway gw = gateway;
569         if (gw == null) {
570             logger.warn("received onConnected() but gateway is null");
571             return;
572         }
573         if (gw instanceof USBGateway usbGateway) {
574             logger.info("---- CONNECTED to Zigbee USB gateway bridge '{}' (serialPort: {})", thing.getUID(),
575                     usbGateway.getSerialPortName());
576         } else {
577             logger.info("---- CONNECTED to BUS gateway bridge '{}' ({}:{})", thing.getUID(),
578                     ((BUSGateway) gw).getHost(), ((BUSGateway) gw).getPort());
579             // update serial number property (with MAC address)
580             if (!Objects.equals(properties.get(PROPERTY_SERIAL_NO), gw.getMACAddr().toUpperCase())) {
581                 properties.put(PROPERTY_SERIAL_NO, gw.getMACAddr().toUpperCase());
582                 propertiesChanged = true;
583                 logger.debug("updated property gw serialNumber: {}", properties.get(PROPERTY_SERIAL_NO));
584             }
585         }
586         if (!Objects.equals(properties.get(PROPERTY_FIRMWARE_VERSION), gw.getFirmwareVersion())) {
587             properties.put(PROPERTY_FIRMWARE_VERSION, gw.getFirmwareVersion());
588             propertiesChanged = true;
589             logger.debug("updated property gw firmware version: {}", properties.get(PROPERTY_FIRMWARE_VERSION));
590         }
591         if (propertiesChanged) {
592             updateProperties(properties);
593             logger.info("properties updated for bridge '{}'", thing.getUID());
594         }
595         updateStatus(ThingStatus.ONLINE);
596         // schedule a refresh for all devices
597         refreshAllSchedule = scheduler.schedule(this::refreshAllBridgeDevices, REFRESH_ALL_DEVICES_DELAY_MSEC,
598                 TimeUnit.MILLISECONDS);
599     }
600
601     @Override
602     public void onConnectionError(@Nullable OWNException error) {
603         String errMsg;
604         if (error == null) {
605             errMsg = "unknown error";
606         } else {
607             errMsg = error.getMessage();
608         }
609         logger.info("---- ON CONNECTION ERROR for gateway {}: {}", gateway, errMsg);
610         isGatewayConnected = false;
611         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
612                 "@text/offline.comm-error-connection" + " (onConnectionError - " + errMsg + ")");
613         tryReconnectGateway();
614     }
615
616     @Override
617     public void onConnectionClosed() {
618         isGatewayConnected = false;
619         logger.debug("onConnectionClosed() - isGatewayConnected={}", isGatewayConnected);
620         // NOTE: cannot change to OFFLINE here because we are already in REMOVING state
621     }
622
623     @Override
624     public void onDisconnected(@Nullable OWNException e) {
625         isGatewayConnected = false;
626         String errMsg;
627         if (e == null) {
628             errMsg = "unknown error";
629         } else {
630             errMsg = e.getMessage();
631         }
632         logger.info("---- DISCONNECTED from gateway {}. OWNException: {}", gateway, errMsg);
633         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
634                 "@text/offline.comm-error-disconnected" + " (onDisconnected - " + errMsg + ")");
635         tryReconnectGateway();
636     }
637
638     private void tryReconnectGateway() {
639         OpenGateway gw = gateway;
640         if (gw != null) {
641             if (!reconnecting) {
642                 reconnecting = true;
643                 logger.info("---- Starting RECONNECT cycle to gateway {}", gw);
644                 try {
645                     gw.reconnect();
646                 } catch (OWNAuthException e) {
647                     logger.info("---- AUTH error from gateway. Stopping re-connect");
648                     reconnecting = false;
649                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
650                             "@text/offline.conf-error-auth" + " (" + e + ")");
651                 }
652             } else {
653                 logger.debug("---- already reconnecting");
654             }
655         } else {
656             logger.warn("---- cannot start RECONNECT, gateway is null");
657         }
658     }
659
660     @Override
661     public void onReconnected() {
662         reconnecting = false;
663         OpenGateway gw = gateway;
664         logger.info("---- RE-CONNECTED to bridge {}", thing.getUID());
665         if (gw != null) {
666             updateStatus(ThingStatus.ONLINE);
667             if (gw.getFirmwareVersion() != null) {
668                 this.updateProperty(PROPERTY_FIRMWARE_VERSION, gw.getFirmwareVersion());
669                 logger.debug("gw firmware version: {}", gw.getFirmwareVersion());
670             }
671             // schedule a refresh for all devices
672             refreshAllSchedule = scheduler.schedule(this::refreshAllBridgeDevices, REFRESH_ALL_DEVICES_DELAY_MSEC,
673                     TimeUnit.MILLISECONDS);
674         }
675     }
676
677     /**
678      * Return a ownId string (=WHO.WHERE) from the device Where address and handler
679      *
680      * @param where the Where address (to be normalized)
681      * @param handler the device handler
682      * @return the ownId String
683      */
684     protected String ownIdFromDeviceWhere(Where where, OpenWebNetThingHandler handler) {
685         return handler.ownIdPrefix() + "." + normalizeWhere(where);
686     }
687
688     /**
689      * Returns a ownId string (=WHO.WHERE) from a Who and Where address
690      *
691      * @param who the Who
692      * @param where the Where address (to be normalized)
693      * @return the ownId String
694      */
695     public String ownIdFromWhoWhere(Who who, Where where) {
696         return who.value() + "." + normalizeWhere(where);
697     }
698
699     /**
700      * Return a ownId string (=WHO.WHERE) from a BaseOpenMessage
701      *
702      * @param baseMsg the BaseOpenMessage
703      * @return the ownId String
704      */
705     public String ownIdFromMessage(BaseOpenMessage baseMsg) {
706         @Nullable
707         Where w = baseMsg.getWhere();
708         if (w != null) {
709             return baseMsg.getWho().value() + "." + normalizeWhere(w);
710         } else if (baseMsg instanceof Alarm) { // null and Alarm
711             return baseMsg.getWho().value() + "." + "0"; // Alarm System --> where=0
712         } else {
713             logger.warn("ownIdFromMessage with null where: {}", baseMsg);
714             return "";
715         }
716     }
717
718     /**
719      * Transform a Where address into a Thing id string
720      *
721      * @param where the Where address
722      * @return the thing Id string
723      */
724     public String thingIdFromWhere(Where where) {
725         return normalizeWhere(where); // '#' cannot be used in ThingUID;
726     }
727
728     /**
729      * Normalize a Where address to generate ownId and Thing id
730      *
731      * @param where the Where address
732      * @return the normalized address as String
733      */
734     public String normalizeWhere(Where where) {
735         String str = where.value();
736         if (where instanceof WhereZigBee whereZigBee) {
737             str = whereZigBee.valueWithUnit(WhereZigBee.UNIT_ALL); // 76543210X#9 --> 765432100#9
738         } else {
739             if (str.indexOf("#4#") == -1) { // skip APL#4#bus case
740                 if (str.indexOf('#') == 0) { // Thermo central unit (#0) or zone via central unit (#Z, Z=[1-99]) --> Z,
741                                              // Alarm Zone (#Z) --> Z
742                     str = str.substring(1);
743                 } else if (str.indexOf('#') > 0 && str.charAt(0) != '0') { // Thermo zone Z and actuator N (Z#N,
744                                                                            // Z=[1-99], N=[1-9]) --> Z
745                     str = str.substring(0, str.indexOf('#'));
746                 }
747             }
748         }
749         return str.replace('#', 'h');
750     }
751 }