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