2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.openwebnet.internal.handler;
15 import static org.openhab.binding.openwebnet.internal.OpenWebNetBindingConstants.*;
17 import java.util.Collection;
18 import java.util.Collections;
19 import java.util.List;
21 import java.util.Objects;
23 import java.util.concurrent.ConcurrentHashMap;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.openwebnet.internal.OpenWebNetBindingConstants;
30 import org.openhab.binding.openwebnet.internal.discovery.OpenWebNetDeviceDiscoveryService;
31 import org.openhab.binding.openwebnet.internal.handler.config.OpenWebNetBusBridgeConfig;
32 import org.openhab.binding.openwebnet.internal.handler.config.OpenWebNetZigBeeBridgeConfig;
33 import org.openhab.core.config.core.status.ConfigStatusMessage;
34 import org.openhab.core.thing.Bridge;
35 import org.openhab.core.thing.ChannelUID;
36 import org.openhab.core.thing.Thing;
37 import org.openhab.core.thing.ThingStatus;
38 import org.openhab.core.thing.ThingStatusDetail;
39 import org.openhab.core.thing.ThingTypeUID;
40 import org.openhab.core.thing.binding.ConfigStatusBridgeHandler;
41 import org.openhab.core.thing.binding.ThingHandlerService;
42 import org.openhab.core.types.Command;
43 import org.openhab.core.types.RefreshType;
44 import org.openwebnet4j.BUSGateway;
45 import org.openwebnet4j.GatewayListener;
46 import org.openwebnet4j.OpenDeviceType;
47 import org.openwebnet4j.OpenGateway;
48 import org.openwebnet4j.USBGateway;
49 import org.openwebnet4j.communication.OWNAuthException;
50 import org.openwebnet4j.communication.OWNException;
51 import org.openwebnet4j.message.Automation;
52 import org.openwebnet4j.message.BaseOpenMessage;
53 import org.openwebnet4j.message.CEN;
54 import org.openwebnet4j.message.EnergyManagement;
55 import org.openwebnet4j.message.FrameException;
56 import org.openwebnet4j.message.GatewayMgmt;
57 import org.openwebnet4j.message.Lighting;
58 import org.openwebnet4j.message.OpenMessage;
59 import org.openwebnet4j.message.Thermoregulation;
60 import org.openwebnet4j.message.What;
61 import org.openwebnet4j.message.Where;
62 import org.openwebnet4j.message.WhereZigBee;
63 import org.openwebnet4j.message.Who;
64 import org.slf4j.Logger;
65 import org.slf4j.LoggerFactory;
68 * The {@link OpenWebNetBridgeHandler} is responsible for handling communication with gateways and handling events.
70 * @author Massimo Valla - Initial contribution
71 * @author Andrea Conte - Energy management, Thermoregulation
72 * @author Gilberto Cocchi - Thermoregulation
75 public class OpenWebNetBridgeHandler extends ConfigStatusBridgeHandler implements GatewayListener {
77 private final Logger logger = LoggerFactory.getLogger(OpenWebNetBridgeHandler.class);
79 private static final int GATEWAY_ONLINE_TIMEOUT_SEC = 20; // Time to wait for the gateway to become connected
81 private static final int REFRESH_ALL_DEVICES_DELAY_MSEC = 500; // Delay to wait before trying again another all
82 // devices refresh request after a connect/reconnect
83 private static final int REFRESH_ALL_DEVICES_DELAY_MAX_MSEC = 15000; // Maximum delay to wait for all devices
84 // refresh after a connect/reconnect
86 private static final int REFRESH_ALL_CHECK_DELAY_SEC = 20; // Delay to wait to check which devices are
89 private long lastRegisteredDeviceTS = -1; // timestamp when the last device has been associated to the bridge
90 private long refreshAllDevicesDelay = 0; // delay waited before starting all devices refresh
92 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = OpenWebNetBindingConstants.BRIDGE_SUPPORTED_THING_TYPES;
94 // ConcurrentHashMap of devices registered to this BridgeHandler
95 // association is: ownId (String) -> OpenWebNetThingHandler, with ownId = WHO.WHERE
96 private Map<String, @Nullable OpenWebNetThingHandler> registeredDevices = new ConcurrentHashMap<>();
97 private Map<String, Long> discoveringDevices = new ConcurrentHashMap<>();
99 protected @Nullable OpenGateway gateway;
100 private boolean isBusGateway = false;
102 private boolean isGatewayConnected = false;
104 public @Nullable OpenWebNetDeviceDiscoveryService deviceDiscoveryService;
105 private boolean reconnecting = false; // we are trying to reconnect to gateway
106 private @Nullable ScheduledFuture<?> refreshAllSchedule;
107 private @Nullable ScheduledFuture<?> connectSchedule;
109 private boolean scanIsActive = false; // a device scan has been activated by OpenWebNetDeviceDiscoveryService;
110 private boolean discoveryByActivation;
112 public OpenWebNetBridgeHandler(Bridge bridge) {
116 public boolean isBusGateway() {
121 public void initialize() {
122 ThingTypeUID thingType = getThing().getThingTypeUID();
124 if (thingType.equals(THING_TYPE_ZB_GATEWAY)) {
125 gw = initZigBeeGateway();
127 gw = initBusGateway();
133 if (gw.isConnected()) { // gateway is already connected, device can go ONLINE
134 isGatewayConnected = true;
135 updateStatus(ThingStatus.ONLINE);
137 updateStatus(ThingStatus.UNKNOWN);
138 logger.debug("Trying to connect gateway {}... ", gw);
141 connectSchedule = scheduler.schedule(() -> {
142 // if status is still UNKNOWN after timer ends, set the device OFFLINE
143 if (thing.getStatus().equals(ThingStatus.UNKNOWN)) {
144 logger.info("status still UNKNOWN. Setting device={} to OFFLINE", thing.getUID());
145 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
146 "@text/offline.comm-error-timeout");
148 }, GATEWAY_ONLINE_TIMEOUT_SEC, TimeUnit.SECONDS);
149 logger.debug("bridge {} initialization completed", thing.getUID());
150 } catch (OWNException e) {
151 logger.debug("gw.connect() returned OWNException: {}", e.getMessage());
152 // status is updated by callback onConnectionError()
159 * Init a ZigBee gateway based on config
161 private @Nullable OpenGateway initZigBeeGateway() {
162 logger.debug("Initializing ZigBee USB Gateway");
163 OpenWebNetZigBeeBridgeConfig zbBridgeConfig = getConfigAs(OpenWebNetZigBeeBridgeConfig.class);
164 String serialPort = zbBridgeConfig.getSerialPort();
165 if (serialPort == null || serialPort.isEmpty()) {
166 logger.warn("Cannot connect ZigBee USB Gateway. No serial port has been provided in Bridge configuration.");
167 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
168 "@text/offline.conf-error-no-serial-port");
171 return new USBGateway(serialPort);
176 * Init a BUS gateway based on config
178 private @Nullable OpenGateway initBusGateway() {
179 logger.debug("Initializing BUS gateway");
181 OpenWebNetBusBridgeConfig busBridgeConfig = getConfigAs(OpenWebNetBusBridgeConfig.class);
182 String host = busBridgeConfig.getHost();
183 if (host == null || host.isEmpty()) {
184 logger.warn("Cannot connect to BUS Gateway. No host/IP has been provided in Bridge configuration.");
185 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
186 "@text/offline.conf-error-no-ip-address");
189 int port = busBridgeConfig.getPort().intValue();
190 String passwd = busBridgeConfig.getPasswd();
192 if (passwd.length() >= 4) {
193 passwdMasked = "******" + passwd.substring(passwd.length() - 3, passwd.length());
195 passwdMasked = "******";
197 discoveryByActivation = busBridgeConfig.getDiscoveryByActivation();
198 logger.debug("Creating new BUS gateway with config properties: {}:{}, pwd={}, discoveryByActivation={}",
199 host, port, passwdMasked, discoveryByActivation);
200 return new BUSGateway(host, port, passwd);
205 public void handleCommand(ChannelUID channelUID, Command command) {
206 logger.debug("handleCommand (command={} - channel={})", command, channelUID);
207 OpenGateway gw = gateway;
208 if (gw == null || !gw.isConnected()) {
209 logger.warn("Gateway is NOT connected, skipping command");
212 if (command instanceof RefreshType) {
213 refreshAllBridgeDevices();
215 logger.warn("Command or channel not supported: channel={} command={}", channelUID, command);
221 public Collection<ConfigStatusMessage> getConfigStatus() {
222 return Collections.emptyList();
226 public void handleRemoval() {
228 super.handleRemoval();
232 public void dispose() {
233 ScheduledFuture<?> rSc = refreshAllSchedule;
237 ScheduledFuture<?> cs = connectSchedule;
245 private void disconnectGateway() {
246 OpenGateway gw = gateway;
248 gw.closeConnection();
249 gw.unsubscribe(this);
250 logger.debug("Gateway {} connection closed and unsubscribed", gw.toString());
253 reconnecting = false;
257 public Collection<Class<? extends ThingHandlerService>> getServices() {
258 return Collections.singleton(OpenWebNetDeviceDiscoveryService.class);
262 * Search for devices connected to this bridge handler's gateway
264 * @param listener to receive device found notifications
266 public synchronized void searchDevices() {
268 logger.debug("------$$ scanIsActive={}", scanIsActive);
269 OpenGateway gw = gateway;
271 if (!gw.isDiscovering()) {
272 if (!gw.isConnected()) {
273 logger.debug("------$$ Gateway '{}' is NOT connected, cannot search for devices", gw);
276 logger.info("------$$ STARTED active SEARCH for devices on bridge '{}'", thing.getUID());
278 gw.discoverDevices();
279 } catch (OWNException e) {
280 logger.warn("------$$ OWNException while discovering devices on bridge '{}': {}", thing.getUID(),
284 logger.debug("------$$ Searching devices on bridge '{}' already activated", thing.getUID());
288 logger.warn("------$$ Cannot search devices: no gateway associated to this handler");
293 public void onNewDevice(@Nullable Where w, @Nullable OpenDeviceType deviceType, @Nullable BaseOpenMessage message) {
294 OpenWebNetDeviceDiscoveryService discService = deviceDiscoveryService;
295 if (discService != null) {
296 if (w != null && deviceType != null) {
297 discService.newDiscoveryResult(w, deviceType, message);
299 logger.warn("onNewDevice with null where/deviceType, msg={}", message);
302 logger.warn("onNewDevice but null deviceDiscoveryService");
307 public void onDiscoveryCompleted() {
308 logger.info("------$$ FINISHED active SEARCH for devices on bridge '{}'", thing.getUID());
312 * Notifies that the scan has been stopped/aborted by OpenWebNetDeviceDiscoveryService
314 public void scanStopped() {
315 scanIsActive = false;
316 logger.debug("------$$ scanIsActive={}", scanIsActive);
319 private void discoverByActivation(BaseOpenMessage baseMsg) {
320 logger.debug("discoverByActivation: msg={}", baseMsg);
321 OpenWebNetDeviceDiscoveryService discService = deviceDiscoveryService;
322 if (discService == null) {
323 logger.warn("discoverByActivation: null OpenWebNetDeviceDiscoveryService, ignoring msg={}", baseMsg);
326 // we support these types only
327 if (baseMsg instanceof Lighting || baseMsg instanceof Automation || baseMsg instanceof EnergyManagement
328 || baseMsg instanceof Thermoregulation || baseMsg instanceof CEN) {
329 BaseOpenMessage bmsg = baseMsg;
330 if (baseMsg instanceof Lighting) {
331 What what = baseMsg.getWhat();
332 if (Lighting.WhatLighting.OFF.equals(what)) { // skipping OFF msg: cannot distinguish dimmer/switch
333 logger.debug("discoverByActivation: skipping OFF msg: cannot distinguish dimmer/switch");
336 if (Lighting.WhatLighting.ON.equals(what)) { // if not already done just now, request light status to
337 // distinguish dimmer from switch
338 if (discoveringDevices.containsKey(ownIdFromMessage(baseMsg))) {
340 "discoverByActivation: we just requested status for this device and it's ON -> it's a switch");
342 OpenGateway gw = gateway;
345 discoveringDevices.put(ownIdFromMessage(baseMsg),
346 Long.valueOf(System.currentTimeMillis()));
347 gw.send(Lighting.requestStatus(baseMsg.getWhere().value()));
349 } catch (OWNException e) {
350 logger.warn("discoverByActivation: Exception while requesting light state: {}",
357 discoveringDevices.remove(ownIdFromMessage(baseMsg));
359 OpenDeviceType type = null;
361 type = bmsg.detectDeviceType();
362 } catch (FrameException e) {
363 logger.warn("Exception while detecting device type: {}", e.getMessage());
366 discService.newDiscoveryResult(bmsg.getWhere(), type, bmsg);
368 logger.debug("discoverByActivation: no device type detected from msg: {}", bmsg);
374 * Register a device ThingHandler to this BridgHandler
376 * @param ownId the device OpenWebNet id
377 * @param thingHandler the thing handler to be registered
379 protected void registerDevice(String ownId, OpenWebNetThingHandler thingHandler) {
380 if (registeredDevices.containsKey(ownId)) {
381 logger.warn("registering device with an existing ownId={}", ownId);
383 registeredDevices.put(ownId, thingHandler);
384 lastRegisteredDeviceTS = System.currentTimeMillis();
385 logger.debug("registered device ownId={}, thing={}", ownId, thingHandler.getThing().getUID());
389 * Un-register a device from this bridge handler
391 * @param ownId the device OpenWebNet id
393 protected void unregisterDevice(String ownId) {
394 if (registeredDevices.remove(ownId) != null) {
395 logger.debug("un-registered device ownId={}", ownId);
397 logger.warn("could not un-register ownId={} (not found)", ownId);
402 * Get an already registered device on this bridge handler
404 * @param ownId the device OpenWebNet id
405 * @return the registered device Thing handler or null if the id cannot be found
407 public @Nullable OpenWebNetThingHandler getRegisteredDevice(String ownId) {
408 return registeredDevices.get(ownId);
411 private void refreshAllBridgeDevices() {
412 logger.debug("--- --- ABOUT TO REFRESH ALL devices for bridge {}", thing.getUID());
414 final List<Thing> things = getThing().getThings();
415 int total = things.size();
416 logger.debug("--- FOUND {} things by getThings()", total);
418 if (registeredDevices.isEmpty()) { // no registered device yet
419 if (refreshAllDevicesDelay < REFRESH_ALL_DEVICES_DELAY_MAX_MSEC) {
420 logger.debug("--- REGISTER device not started yet... re-scheduling refreshAllBridgeDevices()");
421 refreshAllDevicesDelay += REFRESH_ALL_DEVICES_DELAY_MSEC * 3;
422 refreshAllSchedule = scheduler.schedule(this::refreshAllBridgeDevices,
423 REFRESH_ALL_DEVICES_DELAY_MSEC * 3, TimeUnit.MILLISECONDS);
427 "--- --- NONE OF {} CHILD DEVICE(S) has REGISTERED with bridge {}: check Things configuration (stopping refreshAllBridgeDevices)",
428 total, thing.getUID());
429 refreshAllDevicesDelay = 0;
432 } else if (System.currentTimeMillis() - lastRegisteredDeviceTS < REFRESH_ALL_DEVICES_DELAY_MSEC) {
433 // a device has been registered with the bridge just now, let's wait for other devices: re-schedule
435 logger.debug("--- REGISTER device just called... re-scheduling refreshAllBridgeDevices()");
436 refreshAllSchedule = scheduler.schedule(this::refreshAllBridgeDevices, REFRESH_ALL_DEVICES_DELAY_MSEC,
437 TimeUnit.MILLISECONDS);
440 for (Thing ownThing : things) {
441 OpenWebNetThingHandler hndlr = (OpenWebNetThingHandler) ownThing.getHandler();
444 logger.debug("--- REFRESHING ALL DEVICES FOR thing #{}/{}: {}", howMany, total, ownThing.getUID());
445 hndlr.refreshAllDevices();
447 logger.warn("--- No handler for thing {}", ownThing.getUID());
450 logger.debug("--- --- COMPLETED REFRESH all devices for bridge {}", thing.getUID());
451 refreshAllDevicesDelay = 0;
452 // set a check that all things are Online
453 refreshAllSchedule = scheduler.schedule(() -> checkAllRefreshed(things), REFRESH_ALL_CHECK_DELAY_SEC,
456 logger.debug("--- --- NO CHILD DEVICE to REFRESH for bridge {}", thing.getUID());
460 private void checkAllRefreshed(List<Thing> things) {
462 int total = things.size();
463 boolean allOnline = true;
464 for (Thing ownThing : things) {
466 ThingStatus ts = ownThing.getStatus();
467 if (ThingStatus.ONLINE == ts) {
468 logger.debug("--- CHECKED ONLINE thing #{}/{}: {}", howMany, total, ownThing.getUID());
470 logger.debug("--- CHECKED ^^^OFFLINE^^^ thing #{}/{}: {}", howMany, total, ownThing.getUID());
475 logger.debug("--- --- REFRESH CHECK COMPLETED: all things ONLINE for bridge {}", thing.getUID());
477 logger.debug("--- --- REFRESH CHECK COMPLETED: NOT all things ONLINE for bridge {}", thing.getUID());
482 public void onEventMessage(@Nullable OpenMessage msg) {
483 logger.trace("RECEIVED <<<<< {}", msg);
485 logger.warn("received event msg is null");
488 if (msg.isACK() || msg.isNACK()) {
489 return; // we ignore ACKS/NACKS
491 // GATEWAY MANAGEMENT
492 if (msg instanceof GatewayMgmt) {
497 BaseOpenMessage baseMsg = (BaseOpenMessage) msg;
498 // let's try to get the Thing associated with this message...
499 if (baseMsg instanceof Lighting || baseMsg instanceof Automation || baseMsg instanceof EnergyManagement
500 || baseMsg instanceof Thermoregulation || baseMsg instanceof CEN) {
501 String ownId = ownIdFromMessage(baseMsg);
502 logger.debug("ownIdFromMessage({}) --> {}", baseMsg, ownId);
503 OpenWebNetThingHandler deviceHandler = registeredDevices.get(ownId);
504 if (deviceHandler == null) {
505 OpenGateway gw = gateway;
506 if (isBusGateway && ((gw != null && !gw.isDiscovering() && scanIsActive)
507 || (discoveryByActivation && !scanIsActive))) {
508 discoverByActivation(baseMsg);
510 logger.debug("ownId={} has NO DEVICE associated, ignoring it", ownId);
513 deviceHandler.handleMessage(baseMsg);
516 logger.debug("BridgeHandler ignoring frame {}. WHO={} is not supported by this binding", baseMsg,
522 public void onConnected() {
523 isGatewayConnected = true;
524 Map<String, String> properties = editProperties();
525 boolean propertiesChanged = false;
526 OpenGateway gw = gateway;
528 logger.warn("received onConnected() but gateway is null");
531 if (gw instanceof USBGateway) {
532 logger.info("---- CONNECTED to ZigBee USB gateway bridge '{}' (serialPort: {})", thing.getUID(),
533 ((USBGateway) gw).getSerialPortName());
535 logger.info("---- CONNECTED to BUS gateway bridge '{}' ({}:{})", thing.getUID(),
536 ((BUSGateway) gw).getHost(), ((BUSGateway) gw).getPort());
537 // update serial number property (with MAC address)
538 if (!Objects.equals(properties.get(PROPERTY_SERIAL_NO), gw.getMACAddr().toUpperCase())) {
539 properties.put(PROPERTY_SERIAL_NO, gw.getMACAddr().toUpperCase());
540 propertiesChanged = true;
541 logger.debug("updated property gw serialNumber: {}", properties.get(PROPERTY_SERIAL_NO));
544 if (!Objects.equals(properties.get(PROPERTY_FIRMWARE_VERSION), gw.getFirmwareVersion())) {
545 properties.put(PROPERTY_FIRMWARE_VERSION, gw.getFirmwareVersion());
546 propertiesChanged = true;
547 logger.debug("updated property gw firmware version: {}", properties.get(PROPERTY_FIRMWARE_VERSION));
549 if (propertiesChanged) {
550 updateProperties(properties);
551 logger.info("properties updated for bridge '{}'", thing.getUID());
553 updateStatus(ThingStatus.ONLINE);
554 // schedule a refresh for all devices
555 refreshAllSchedule = scheduler.schedule(this::refreshAllBridgeDevices, REFRESH_ALL_DEVICES_DELAY_MSEC,
556 TimeUnit.MILLISECONDS);
560 public void onConnectionError(@Nullable OWNException error) {
563 errMsg = "unknown error";
565 errMsg = error.getMessage();
567 logger.info("---- ON CONNECTION ERROR for gateway {}: {}", gateway, errMsg);
568 isGatewayConnected = false;
569 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
570 "@text/offline.comm-error-connection" + " (onConnectionError - " + errMsg + ")");
571 tryReconnectGateway();
575 public void onConnectionClosed() {
576 isGatewayConnected = false;
577 logger.debug("onConnectionClosed() - isGatewayConnected={}", isGatewayConnected);
578 // NOTE: cannot change to OFFLINE here because we are already in REMOVING state
582 public void onDisconnected(@Nullable OWNException e) {
583 isGatewayConnected = false;
586 errMsg = "unknown error";
588 errMsg = e.getMessage();
590 logger.info("---- DISCONNECTED from gateway {}. OWNException: {}", gateway, errMsg);
591 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
592 "@text/offline.comm-error-disconnected" + " (onDisconnected - " + errMsg + ")");
593 tryReconnectGateway();
596 private void tryReconnectGateway() {
597 OpenGateway gw = gateway;
601 logger.info("---- Starting RECONNECT cycle to gateway {}", gw);
604 } catch (OWNAuthException e) {
605 logger.info("---- AUTH error from gateway. Stopping re-connect");
606 reconnecting = false;
607 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
608 "@text/offline.conf-error-auth" + " (" + e + ")");
611 logger.debug("---- already reconnecting");
614 logger.warn("---- cannot start RECONNECT, gateway is null");
619 public void onReconnected() {
620 reconnecting = false;
621 OpenGateway gw = gateway;
622 logger.info("---- RE-CONNECTED to bridge {}", thing.getUID());
624 updateStatus(ThingStatus.ONLINE);
625 if (gw.getFirmwareVersion() != null) {
626 this.updateProperty(PROPERTY_FIRMWARE_VERSION, gw.getFirmwareVersion());
627 logger.debug("gw firmware version: {}", gw.getFirmwareVersion());
629 // schedule a refresh for all devices
630 refreshAllSchedule = scheduler.schedule(this::refreshAllBridgeDevices, REFRESH_ALL_DEVICES_DELAY_MSEC,
631 TimeUnit.MILLISECONDS);
636 * Return a ownId string (=WHO.WHERE) from the device Where address and handler
638 * @param where the Where address (to be normalized)
639 * @param handler the device handler
640 * @return the ownId String
642 protected String ownIdFromDeviceWhere(Where where, OpenWebNetThingHandler handler) {
643 return handler.ownIdPrefix() + "." + normalizeWhere(where);
647 * Returns a ownId string (=WHO.WHERE) from a Who and Where address
650 * @param where the Where address (to be normalized)
651 * @return the ownId String
653 public String ownIdFromWhoWhere(Who who, Where where) {
654 return who.value() + "." + normalizeWhere(where);
658 * Return a ownId string (=WHO.WHERE) from a BaseOpenMessage
660 * @param baseMsg the BaseOpenMessage
661 * @return the ownId String
663 public String ownIdFromMessage(BaseOpenMessage baseMsg) {
664 return baseMsg.getWho().value() + "." + normalizeWhere(baseMsg.getWhere());
668 * Transform a Where address into a Thing id string
670 * @param where the Where address
671 * @return the thing Id string
673 public String thingIdFromWhere(Where where) {
674 return normalizeWhere(where); // '#' cannot be used in ThingUID;
678 * Normalize a Where address to generate ownId and Thing id
680 * @param where the Where address
681 * @return the normalized address as String
683 public String normalizeWhere(Where where) {
684 String str = where.value();
685 if (where instanceof WhereZigBee) {
686 str = ((WhereZigBee) where).valueWithUnit(WhereZigBee.UNIT_ALL); // 76543210X#9 --> 765432100#9
688 if (str.indexOf("#4#") == -1) { // skip APL#4#bus case
689 if (str.indexOf('#') == 0) { // Thermo central unit (#0) or zone via central unit (#Z, Z=[1-99]) --> Z
690 str = str.substring(1);
691 } else if (str.indexOf('#') > 0) { // Thermo zone Z and actuator N (Z#N, Z=[1-99], N=[1-9]) --> Z
692 str = str.substring(0, str.indexOf('#'));
696 return str.replace('#', 'h');