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