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