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