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