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