2 * Copyright (c) 2010-2024 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.time.Instant;
16 import java.util.AbstractMap;
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.binding.boschshc.internal.devices.bridge.dto.UserDefinedState;
28 import org.openhab.core.config.discovery.AbstractThingHandlerDiscoveryService;
29 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
30 import org.openhab.core.thing.ThingTypeUID;
31 import org.openhab.core.thing.ThingUID;
32 import org.openhab.core.thing.binding.ThingHandlerService;
33 import org.osgi.service.component.annotations.Component;
34 import org.osgi.service.component.annotations.ServiceScope;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
39 * The {@link ThingDiscoveryService} is responsible to discover Bosch Smart Home things.
40 * The paired SHC BridgeHandler is required to get the lists of rooms and devices.
41 * With this data the openhab things are discovered.
43 * The order to make this work is
44 * 1. SHC bridge is created, e.v via openhab UI
45 * 2. Service is instantiated setBridgeHandler of this service is called
46 * 3. Service is activated
47 * 4. Service registers itself as discoveryLister at the bridge
48 * 5. bridge calls startScan after bridge is paired and things can be discovered
50 * @author Gerd Zanker - Initial contribution
52 @Component(scope = ServiceScope.PROTOTYPE, service = ThingHandlerService.class)
54 public class ThingDiscoveryService extends AbstractThingHandlerDiscoveryService<BridgeHandler> {
55 private static final int SEARCH_TIME = 1;
57 private final Logger logger = LoggerFactory.getLogger(ThingDiscoveryService.class);
60 * Device model representing logical child devices of Light Control II
62 static final String DEVICE_MODEL_LIGHT_CONTROL_CHILD_DEVICE = "MICROMODULE_LIGHT_ATTACHED";
64 protected static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(
65 BoschSHCBindingConstants.THING_TYPE_INWALL_SWITCH, BoschSHCBindingConstants.THING_TYPE_TWINGUARD,
66 BoschSHCBindingConstants.THING_TYPE_WINDOW_CONTACT, BoschSHCBindingConstants.THING_TYPE_WINDOW_CONTACT_2,
67 BoschSHCBindingConstants.THING_TYPE_MOTION_DETECTOR, BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL,
68 BoschSHCBindingConstants.THING_TYPE_THERMOSTAT, BoschSHCBindingConstants.THING_TYPE_CLIMATE_CONTROL,
69 BoschSHCBindingConstants.THING_TYPE_WALL_THERMOSTAT, BoschSHCBindingConstants.THING_TYPE_CAMERA_360,
70 BoschSHCBindingConstants.THING_TYPE_CAMERA_EYES,
71 BoschSHCBindingConstants.THING_TYPE_INTRUSION_DETECTION_SYSTEM,
72 BoschSHCBindingConstants.THING_TYPE_SMART_PLUG_COMPACT, BoschSHCBindingConstants.THING_TYPE_SMART_BULB,
73 BoschSHCBindingConstants.THING_TYPE_SMOKE_DETECTOR);
76 public static final Map<String, ThingTypeUID> DEVICEMODEL_TO_THINGTYPE_MAP = Map.ofEntries(
77 new AbstractMap.SimpleEntry<>("BBL", BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL),
78 new AbstractMap.SimpleEntry<>("TWINGUARD", BoschSHCBindingConstants.THING_TYPE_TWINGUARD),
79 new AbstractMap.SimpleEntry<>("BSM", BoschSHCBindingConstants.THING_TYPE_INWALL_SWITCH),
80 new AbstractMap.SimpleEntry<>("PSM", BoschSHCBindingConstants.THING_TYPE_SMART_PLUG_COMPACT),
81 new AbstractMap.SimpleEntry<>("PLUG_COMPACT", BoschSHCBindingConstants.THING_TYPE_SMART_PLUG_COMPACT),
82 new AbstractMap.SimpleEntry<>("CAMERA_360", BoschSHCBindingConstants.THING_TYPE_CAMERA_360),
83 new AbstractMap.SimpleEntry<>("CAMERA_EYES", BoschSHCBindingConstants.THING_TYPE_CAMERA_EYES),
84 new AbstractMap.SimpleEntry<>("BWTH", BoschSHCBindingConstants.THING_TYPE_WALL_THERMOSTAT), // wall thermostat
85 new AbstractMap.SimpleEntry<>("THB", BoschSHCBindingConstants.THING_TYPE_WALL_THERMOSTAT), // wall thermostat with batteries
86 new AbstractMap.SimpleEntry<>("SD", BoschSHCBindingConstants.THING_TYPE_SMOKE_DETECTOR),
87 new AbstractMap.SimpleEntry<>("MD", BoschSHCBindingConstants.THING_TYPE_MOTION_DETECTOR),
88 new AbstractMap.SimpleEntry<>("ROOM_CLIMATE_CONTROL", BoschSHCBindingConstants.THING_TYPE_CLIMATE_CONTROL),
89 new AbstractMap.SimpleEntry<>("INTRUSION_DETECTION_SYSTEM", BoschSHCBindingConstants.THING_TYPE_INTRUSION_DETECTION_SYSTEM),
90 new AbstractMap.SimpleEntry<>("HUE_LIGHT", BoschSHCBindingConstants.THING_TYPE_SMART_BULB),
91 new AbstractMap.SimpleEntry<>("LEDVANCE_LIGHT", BoschSHCBindingConstants.THING_TYPE_SMART_BULB),
92 new AbstractMap.SimpleEntry<>("SWD", BoschSHCBindingConstants.THING_TYPE_WINDOW_CONTACT),
93 new AbstractMap.SimpleEntry<>("SWD2", BoschSHCBindingConstants.THING_TYPE_WINDOW_CONTACT_2),
94 new AbstractMap.SimpleEntry<>("TRV", BoschSHCBindingConstants.THING_TYPE_THERMOSTAT),
95 new AbstractMap.SimpleEntry<>("WRC2", BoschSHCBindingConstants.THING_TYPE_UNIVERSAL_SWITCH),
96 new AbstractMap.SimpleEntry<>("SWITCH2", BoschSHCBindingConstants.THING_TYPE_UNIVERSAL_SWITCH_2),
97 new AbstractMap.SimpleEntry<>("SMOKE_DETECTOR2", BoschSHCBindingConstants.THING_TYPE_SMOKE_DETECTOR_2),
98 new AbstractMap.SimpleEntry<>("MICROMODULE_SHUTTER", BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL_2),
99 new AbstractMap.SimpleEntry<>("MICROMODULE_AWNING", BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL_2),
100 new AbstractMap.SimpleEntry<>("MICROMODULE_LIGHT_CONTROL", BoschSHCBindingConstants.THING_TYPE_LIGHT_CONTROL_2),
101 new AbstractMap.SimpleEntry<>("MICROMODULE_DIMMER", BoschSHCBindingConstants.THING_TYPE_DIMMER),
102 new AbstractMap.SimpleEntry<>("WLS", BoschSHCBindingConstants.THING_TYPE_WATER_DETECTOR)
103 // Future Extension: map deviceModel names to BoschSHC Thing Types when they are supported
104 // new AbstractMap.SimpleEntry<>("SMOKE_DETECTION_SYSTEM", BoschSHCBindingConstants.),
105 // new AbstractMap.SimpleEntry<>("PRESENCE_SIMULATION_SERVICE", BoschSHCBindingConstants.),
106 // new AbstractMap.SimpleEntry<>("VENTILATION_SERVICE", BoschSHCBindingConstants.),
107 // new AbstractMap.SimpleEntry<>("HUE_BRIDGE", BoschSHCBindingConstants.)
108 // new AbstractMap.SimpleEntry<>("HUE_BRIDGE_MANAGER*", BoschSHCBindingConstants.)
109 // new AbstractMap.SimpleEntry<>("HUE_LIGHT_ROOM_CONTROL", BoschSHCBindingConstants.)
113 public ThingDiscoveryService() {
114 super(BridgeHandler.class, SUPPORTED_THING_TYPES, SEARCH_TIME);
118 public void initialize() {
119 logger.trace("initialize");
120 thingHandler.registerDiscoveryListener(this);
125 public void dispose() {
127 logger.trace("dispose");
128 removeOlderResults(Instant.now().toEpochMilli(), thingHandler.getThing().getUID());
129 thingHandler.unregisterDiscoveryListener();
135 protected void startScan() {
138 } catch (InterruptedException e) {
139 // Restore interrupted state...
140 Thread.currentThread().interrupt();
145 protected synchronized void stopScan() {
146 logger.debug("Stop manual scan on bridge {}", thingHandler.getThing().getUID());
148 removeOlderResults(getTimestampOfLastScan(), thingHandler.getThing().getUID());
151 public void doScan() throws InterruptedException {
152 logger.debug("Start manual scan on bridge {}", thingHandler.getThing().getUID());
153 // use shcBridgeHandler to getDevices()
154 List<Room> rooms = thingHandler.getRooms();
155 logger.debug("SHC has {} rooms", rooms.size());
156 List<Device> devices = thingHandler.getDevices();
157 logger.debug("SHC has {} devices", devices.size());
158 List<UserDefinedState> userStates = thingHandler.getUserStates();
159 logger.debug("SHC has {} user-defined states", userStates.size());
161 // Write found devices into openhab.log to support manual configuration
162 for (Device d : devices) {
163 logger.debug("Found device: name={} id={}", d.name, d.id);
164 if (d.deviceServiceIds != null) {
165 for (String s : d.deviceServiceIds) {
166 logger.debug(".... service: {}", s);
170 for (UserDefinedState userState : userStates) {
171 logger.debug("Found user-defined state: name={} id={} state={}", userState.getName(), userState.getId(),
172 userState.isState());
175 addDevices(devices, rooms);
176 addUserStates(userStates);
179 protected void addUserStates(List<UserDefinedState> userStates) {
180 for (UserDefinedState userState : userStates) {
181 addUserState(userState);
185 private void addUserState(UserDefinedState userState) {
186 logger.trace("Discovering user-defined state {}", userState.getName());
187 logger.trace("- details: id {}, state {}", userState.getId(), userState.isState());
189 ThingTypeUID thingTypeUID = new ThingTypeUID(BoschSHCBindingConstants.BINDING_ID,
190 BoschSHCBindingConstants.THING_TYPE_USER_DEFINED_STATE.getId());
192 logger.trace("- got thingTypeID '{}' for user-defined state '{}'", thingTypeUID.getId(), userState.getName());
194 ThingUID thingUID = new ThingUID(thingTypeUID, thingHandler.getThing().getUID(),
195 userState.getId().replace(':', '_'));
197 logger.trace("- got thingUID '{}' for user-defined state: '{}'", thingUID, userState);
199 DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID)
200 .withProperty("id", userState.getId()).withLabel(userState.getName());
202 discoveryResult.withBridge(thingHandler.getThing().getUID());
204 thingDiscovered(discoveryResult.build());
206 logger.debug("Discovered user-defined state '{}' with thingTypeUID={}, thingUID={}, id={}, state={}",
207 userState.getName(), thingUID, thingTypeUID, userState.getId(), userState.isState());
210 protected void addDevices(List<Device> devices, List<Room> rooms) {
211 for (Device device : devices) {
212 addDevice(device, getRoomNameForDevice(device, rooms));
216 protected String getRoomNameForDevice(Device device, List<Room> rooms) {
217 return rooms.stream().filter(room -> room.id.equals(device.roomId)).findAny().map(r -> r.name).orElse("");
220 protected void addDevice(Device device, String roomName) {
221 // see startScan for the runtime null check of shcBridgeHandler
222 logger.trace("Discovering device {}", device.name);
223 logger.trace("- details: id {}, roomId {}, deviceModel {}", device.id, device.roomId, device.deviceModel);
225 ThingTypeUID thingTypeUID = getThingTypeUID(device);
226 if (thingTypeUID == null) {
230 logger.trace("- got thingTypeID '{}' for deviceModel '{}'", thingTypeUID.getId(), device.deviceModel);
232 ThingUID thingUID = new ThingUID(thingTypeUID, thingHandler.getThing().getUID(),
233 buildCompliantThingID(device.id));
235 logger.trace("- got thingUID '{}' for device: '{}'", thingUID, device);
237 DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID)
238 .withProperty("id", device.id).withLabel(getNiceName(device.name, roomName));
239 discoveryResult.withBridge(thingHandler.getThing().getUID());
241 if (!roomName.isEmpty()) {
242 discoveryResult.withProperty("Location", roomName);
244 thingDiscovered(discoveryResult.build());
246 logger.debug("Discovered device '{}' with thingTypeUID={}, thingUID={}, id={}, deviceModel={}", device.name,
247 thingUID, thingTypeUID, device.id, device.deviceModel);
251 * Translates a Bosch device ID to an openHAB-compliant thing ID.
253 * Characters that are not allowed in thing IDs are replaced by underscores.
255 * @param deviceId the Bosch device ID
256 * @return the translated openHAB-compliant thing ID
258 private String buildCompliantThingID(String deviceId) {
259 return deviceId.replace(':', '_').replace('#', '_');
262 private String getNiceName(String name, String roomName) {
263 if (!name.startsWith("-")) {
267 // convert "-IntrusionDetectionSystem-" into "Intrusion Detection System"
268 // convert "-RoomClimateControl-" into "Room Climate Control myRoomName"
269 final char[] chars = name.toCharArray();
270 StringBuilder niceNameBuilder = new StringBuilder(32);
271 for (int pos = 0; pos < chars.length; pos++) {
273 if (chars[pos] == '-') {
276 // convert "CamelCase" into "Camel Case", skipping the first Uppercase after the "-"
277 if (pos > 1 && Character.getType(chars[pos]) == Character.UPPERCASE_LETTER) {
278 niceNameBuilder.append(" ");
280 niceNameBuilder.append(chars[pos]);
282 // append roomName for "Room Climate Control", because it appears for each room with a thermostat
283 if (!roomName.isEmpty() && niceNameBuilder.toString().startsWith("Room Climate Control")) {
284 niceNameBuilder.append(" ").append(roomName);
286 return niceNameBuilder.toString();
289 protected @Nullable ThingTypeUID getThingTypeUID(Device device) {
291 ThingTypeUID thingTypeId = DEVICEMODEL_TO_THINGTYPE_MAP.get(device.deviceModel);
292 if (thingTypeId != null) {
293 return new ThingTypeUID(BoschSHCBindingConstants.BINDING_ID, thingTypeId.getId());
296 if (DEVICE_MODEL_LIGHT_CONTROL_CHILD_DEVICE.equals(device.deviceModel)) {
297 // Light Control II exposes a parent device and two child devices.
298 // We only add one thing for the parent device and the child devices are logically included.
299 // Therefore we do not need to add separate things for the child devices and need to suppress the
300 // log entry about the unknown device model.
304 logger.debug("Unknown deviceModel '{}'! Please create a support request issue for this unknown device model.",