2 * Copyright (c) 2010-2023 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.boschshc.internal.discovery;
15 import java.util.AbstractMap;
16 import java.util.Date;
17 import java.util.List;
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
24 import org.openhab.binding.boschshc.internal.devices.bridge.BridgeHandler;
25 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
26 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Room;
27 import org.openhab.core.config.discovery.AbstractDiscoveryService;
28 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
29 import org.openhab.core.thing.ThingTypeUID;
30 import org.openhab.core.thing.ThingUID;
31 import org.openhab.core.thing.binding.ThingHandler;
32 import org.openhab.core.thing.binding.ThingHandlerService;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
37 * The {@link ThingDiscoveryService} is responsible to discover Bosch Smart Home things.
38 * The paired SHC BridgeHandler is required to get the lists of rooms and devices.
39 * With this data the openhab things are discovered.
41 * The order to make this work is
42 * 1. SHC bridge is created, e.v via openhab UI
43 * 2. Service is instantiated setBridgeHandler of this service is called
44 * 3. Service is activated
45 * 4. Service registers itself as discoveryLister at the bridge
46 * 5. bridge calls startScan after bridge is paired and things can be discovered
48 * @author Gerd Zanker - Initial contribution
51 public class ThingDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
52 private static final int SEARCH_TIME = 1;
54 private final Logger logger = LoggerFactory.getLogger(ThingDiscoveryService.class);
55 private @Nullable BridgeHandler shcBridgeHandler;
57 protected static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(
58 BoschSHCBindingConstants.THING_TYPE_INWALL_SWITCH, BoschSHCBindingConstants.THING_TYPE_TWINGUARD,
59 BoschSHCBindingConstants.THING_TYPE_WINDOW_CONTACT, BoschSHCBindingConstants.THING_TYPE_MOTION_DETECTOR,
60 BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL, BoschSHCBindingConstants.THING_TYPE_THERMOSTAT,
61 BoschSHCBindingConstants.THING_TYPE_CLIMATE_CONTROL, BoschSHCBindingConstants.THING_TYPE_WALL_THERMOSTAT,
62 BoschSHCBindingConstants.THING_TYPE_CAMERA_360, BoschSHCBindingConstants.THING_TYPE_CAMERA_EYES,
63 BoschSHCBindingConstants.THING_TYPE_INTRUSION_DETECTION_SYSTEM,
64 BoschSHCBindingConstants.THING_TYPE_SMART_PLUG_COMPACT, BoschSHCBindingConstants.THING_TYPE_SMART_BULB,
65 BoschSHCBindingConstants.THING_TYPE_SMOKE_DETECTOR);
68 protected static final Map<String, ThingTypeUID> DEVICEMODEL_TO_THINGTYPE_MAP = Map.ofEntries(
69 new AbstractMap.SimpleEntry<>("BBL", BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL),
70 new AbstractMap.SimpleEntry<>("TWINGUARD", BoschSHCBindingConstants.THING_TYPE_TWINGUARD),
71 new AbstractMap.SimpleEntry<>("BSM", BoschSHCBindingConstants.THING_TYPE_INWALL_SWITCH),
72 new AbstractMap.SimpleEntry<>("PSM", BoschSHCBindingConstants.THING_TYPE_SMART_PLUG_COMPACT),
73 new AbstractMap.SimpleEntry<>("PLUG_COMPACT", BoschSHCBindingConstants.THING_TYPE_SMART_PLUG_COMPACT),
74 new AbstractMap.SimpleEntry<>("CAMERA_360", BoschSHCBindingConstants.THING_TYPE_CAMERA_360),
75 new AbstractMap.SimpleEntry<>("CAMERA_EYES", BoschSHCBindingConstants.THING_TYPE_CAMERA_EYES),
76 new AbstractMap.SimpleEntry<>("BWTH", BoschSHCBindingConstants.THING_TYPE_WALL_THERMOSTAT), // wall thermostat
77 new AbstractMap.SimpleEntry<>("THB", BoschSHCBindingConstants.THING_TYPE_WALL_THERMOSTAT), // wall thermostat with batteries
78 new AbstractMap.SimpleEntry<>("SD", BoschSHCBindingConstants.THING_TYPE_SMOKE_DETECTOR),
79 new AbstractMap.SimpleEntry<>("MD", BoschSHCBindingConstants.THING_TYPE_MOTION_DETECTOR),
80 new AbstractMap.SimpleEntry<>("ROOM_CLIMATE_CONTROL", BoschSHCBindingConstants.THING_TYPE_CLIMATE_CONTROL),
81 new AbstractMap.SimpleEntry<>("INTRUSION_DETECTION_SYSTEM", BoschSHCBindingConstants.THING_TYPE_INTRUSION_DETECTION_SYSTEM),
82 new AbstractMap.SimpleEntry<>("HUE_LIGHT", BoschSHCBindingConstants.THING_TYPE_SMART_BULB),
83 new AbstractMap.SimpleEntry<>("LEDVANCE_LIGHT", BoschSHCBindingConstants.THING_TYPE_SMART_BULB),
84 new AbstractMap.SimpleEntry<>("SWD", BoschSHCBindingConstants.THING_TYPE_WINDOW_CONTACT),
85 new AbstractMap.SimpleEntry<>("TRV", BoschSHCBindingConstants.THING_TYPE_THERMOSTAT)
86 // Future Extension: map deviceModel names to BoschSHC Thing Types when they are supported
87 // new AbstractMap.SimpleEntry<>("SMOKE_DETECTION_SYSTEM", BoschSHCBindingConstants.),
88 // new AbstractMap.SimpleEntry<>("PRESENCE_SIMULATION_SERVICE", BoschSHCBindingConstants.),
89 // new AbstractMap.SimpleEntry<>("VENTILATION_SERVICE", BoschSHCBindingConstants.),
90 // new AbstractMap.SimpleEntry<>("HUE_BRIDGE", BoschSHCBindingConstants.)
91 // new AbstractMap.SimpleEntry<>("HUE_BRIDGE_MANAGER*", BoschSHCBindingConstants.)
92 // new AbstractMap.SimpleEntry<>("HUE_LIGHT_ROOM_CONTROL", BoschSHCBindingConstants.)
96 public ThingDiscoveryService() {
97 super(SUPPORTED_THING_TYPES, SEARCH_TIME);
101 public void activate() {
102 logger.trace("activate");
103 final BridgeHandler handler = shcBridgeHandler;
104 if (handler != null) {
105 handler.registerDiscoveryListener(this);
110 public void deactivate() {
111 logger.trace("deactivate");
112 final BridgeHandler handler = shcBridgeHandler;
113 if (handler != null) {
114 removeOlderResults(new Date().getTime(), handler.getThing().getUID());
115 handler.unregisterDiscoveryListener();
122 protected void startScan() {
123 if (shcBridgeHandler == null) {
124 logger.debug("The shcBridgeHandler is empty, no manual scan is currently possible");
130 } catch (InterruptedException e) {
131 // Restore interrupted state...
132 Thread.currentThread().interrupt();
137 protected synchronized void stopScan() {
138 logger.debug("Stop manual scan on bridge {}",
139 shcBridgeHandler != null ? shcBridgeHandler.getThing().getUID() : "?");
141 final BridgeHandler handler = shcBridgeHandler;
142 if (handler != null) {
143 removeOlderResults(getTimestampOfLastScan(), handler.getThing().getUID());
148 public void setThingHandler(@Nullable ThingHandler handler) {
149 if (handler instanceof BridgeHandler bridgeHandler) {
150 logger.trace("Set bridge handler {}", handler);
151 shcBridgeHandler = bridgeHandler;
156 public @Nullable ThingHandler getThingHandler() {
157 return shcBridgeHandler;
160 public void doScan() throws InterruptedException {
161 logger.debug("Start manual scan on bridge {}", shcBridgeHandler.getThing().getUID());
162 // use shcBridgeHandler to getDevices()
163 List<Room> rooms = shcBridgeHandler.getRooms();
164 logger.debug("SHC has {} rooms", rooms.size());
165 List<Device> devices = shcBridgeHandler.getDevices();
166 logger.debug("SHC has {} devices", devices.size());
168 // Write found devices into openhab.log to support manual configuration
169 for (Device d : devices) {
170 logger.debug("Found device: name={} id={}", d.name, d.id);
171 if (d.deviceServiceIds != null) {
172 for (String s : d.deviceServiceIds) {
173 logger.debug(".... service: {}", s);
178 addDevices(devices, rooms);
181 protected void addDevices(List<Device> devices, List<Room> rooms) {
182 for (Device device : devices) {
183 addDevice(device, getRoomNameForDevice(device, rooms));
187 protected String getRoomNameForDevice(Device device, List<Room> rooms) {
188 return rooms.stream().filter(room -> room.id.equals(device.roomId)).findAny().map(r -> r.name).orElse("");
191 protected void addDevice(Device device, String roomName) {
192 // see startScan for the runtime null check of shcBridgeHandler
193 assert shcBridgeHandler != null;
195 logger.trace("Discovering device {}", device.name);
196 logger.trace("- details: id {}, roomId {}, deviceModel {}", device.id, device.roomId, device.deviceModel);
198 ThingTypeUID thingTypeUID = getThingTypeUID(device);
199 if (thingTypeUID == null) {
203 logger.trace("- got thingTypeID '{}' for deviceModel '{}'", thingTypeUID.getId(), device.deviceModel);
205 ThingUID thingUID = new ThingUID(thingTypeUID, shcBridgeHandler.getThing().getUID(),
206 device.id.replace(':', '_'));
208 logger.trace("- got thingUID '{}' for device: '{}'", thingUID, device);
210 DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID)
211 .withProperty("id", device.id).withLabel(getNiceName(device.name, roomName));
212 if (null != shcBridgeHandler) {
213 discoveryResult.withBridge(shcBridgeHandler.getThing().getUID());
215 if (!roomName.isEmpty()) {
216 discoveryResult.withProperty("Location", roomName);
218 thingDiscovered(discoveryResult.build());
220 logger.debug("Discovered device '{}' with thingTypeUID={}, thingUID={}, id={}, deviceModel={}", device.name,
221 thingUID, thingTypeUID, device.id, device.deviceModel);
224 private String getNiceName(String name, String roomName) {
225 if (!name.startsWith("-")) {
229 // convert "-IntrusionDetectionSystem-" into "Intrusion Detection System"
230 // convert "-RoomClimateControl-" into "Room Climate Control myRoomName"
231 final char[] chars = name.toCharArray();
232 StringBuilder niceNameBuilder = new StringBuilder(32);
233 for (int pos = 0; pos < chars.length; pos++) {
235 if (chars[pos] == '-') {
238 // convert "CamelCase" into "Camel Case", skipping the first Uppercase after the "-"
239 if (pos > 1 && Character.getType(chars[pos]) == Character.UPPERCASE_LETTER) {
240 niceNameBuilder.append(" ");
242 niceNameBuilder.append(chars[pos]);
244 // append roomName for "Room Climate Control", because it appears for each room with a thermostat
245 if (!roomName.isEmpty() && niceNameBuilder.toString().startsWith("Room Climate Control")) {
246 niceNameBuilder.append(" ").append(roomName);
248 return niceNameBuilder.toString();
251 protected @Nullable ThingTypeUID getThingTypeUID(Device device) {
253 ThingTypeUID thingTypeId = DEVICEMODEL_TO_THINGTYPE_MAP.get(device.deviceModel);
254 if (thingTypeId != null) {
255 return new ThingTypeUID(BoschSHCBindingConstants.BINDING_ID, thingTypeId.getId());
257 logger.debug("Unknown deviceModel '{}'! Please create a support request issue for this unknown device model.",