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.deconz.internal.discovery;
15 import static org.openhab.binding.deconz.internal.BindingConstants.*;
17 import java.util.HashMap;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22 import java.util.stream.Collectors;
23 import java.util.stream.Stream;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.deconz.internal.Util;
28 import org.openhab.binding.deconz.internal.dto.GroupMessage;
29 import org.openhab.binding.deconz.internal.dto.LightMessage;
30 import org.openhab.binding.deconz.internal.dto.SensorMessage;
31 import org.openhab.binding.deconz.internal.handler.DeconzBridgeHandler;
32 import org.openhab.binding.deconz.internal.handler.LightThingHandler;
33 import org.openhab.binding.deconz.internal.handler.SensorThermostatThingHandler;
34 import org.openhab.binding.deconz.internal.handler.SensorThingHandler;
35 import org.openhab.binding.deconz.internal.types.GroupType;
36 import org.openhab.binding.deconz.internal.types.LightType;
37 import org.openhab.core.config.discovery.AbstractDiscoveryService;
38 import org.openhab.core.config.discovery.DiscoveryResult;
39 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
40 import org.openhab.core.config.discovery.DiscoveryService;
41 import org.openhab.core.thing.Thing;
42 import org.openhab.core.thing.ThingTypeUID;
43 import org.openhab.core.thing.ThingUID;
44 import org.openhab.core.thing.binding.ThingHandler;
45 import org.openhab.core.thing.binding.ThingHandlerService;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
50 * Every bridge will add its discovered sensors to this discovery service to make them
51 * available to the framework.
53 * @author David Graeff - Initial contribution
56 public class ThingDiscoveryService extends AbstractDiscoveryService implements DiscoveryService, ThingHandlerService {
57 private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
58 .of(LightThingHandler.SUPPORTED_THING_TYPE_UIDS, SensorThingHandler.SUPPORTED_THING_TYPES,
59 SensorThermostatThingHandler.SUPPORTED_THING_TYPES)
60 .flatMap(Set::stream).collect(Collectors.toSet());
61 private final Logger logger = LoggerFactory.getLogger(ThingDiscoveryService.class);
63 private @Nullable DeconzBridgeHandler handler;
64 private @Nullable ScheduledFuture<?> scanningJob;
65 private @Nullable ThingUID bridgeUID;
67 public ThingDiscoveryService() {
68 super(SUPPORTED_THING_TYPES_UIDS, 30);
72 public void startScan() {
73 final DeconzBridgeHandler handler = this.handler;
74 if (handler != null) {
75 handler.getBridgeFullState().thenAccept(fullState -> {
77 removeOlderResults(getTimestampOfLastScan());
78 fullState.ifPresent(state -> {
79 state.sensors.forEach(this::addSensor);
80 state.lights.forEach(this::addLight);
81 state.groups.forEach(this::addGroup);
89 protected void startBackgroundDiscovery() {
90 final ScheduledFuture<?> scanningJob = this.scanningJob;
91 if (scanningJob == null || scanningJob.isCancelled()) {
92 this.scanningJob = scheduler.scheduleWithFixedDelay(this::startScan, 0, 5, TimeUnit.MINUTES);
97 protected void stopBackgroundDiscovery() {
98 final ScheduledFuture<?> scanningJob = this.scanningJob;
99 if (scanningJob != null) {
100 scanningJob.cancel(true);
101 this.scanningJob = null;
106 * Add a group to the discovery inbox.
108 * @param groupId The id of the light
109 * @param group The group description
111 private void addGroup(String groupId, GroupMessage group) {
112 final ThingUID bridgeUID = this.bridgeUID;
113 if (bridgeUID == null) {
114 logger.warn("Received a message from non-existent bridge. This most likely is a bug.");
118 ThingTypeUID thingTypeUID;
119 GroupType groupType = group.type;
121 if (groupType == null) {
122 logger.warn("No group type reported for group {} ({})", group.modelid, group.name);
126 Map<String, Object> properties = new HashMap<>();
127 properties.put(CONFIG_ID, groupId);
131 thingTypeUID = THING_TYPE_LIGHTGROUP;
135 "Found group: {} ({}), type {} but no thing type defined for that type. This should be reported.",
136 group.id, group.name, group.type);
140 ThingUID uid = new ThingUID(thingTypeUID, bridgeUID, group.id);
141 DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID).withLabel(group.name)
142 .withProperties(properties).withRepresentationProperty(CONFIG_ID).build();
143 thingDiscovered(discoveryResult);
147 * Add a light device to the discovery inbox.
149 * @param lightId The id of the light
150 * @param light The light description
152 private void addLight(String lightId, LightMessage light) {
153 final ThingUID bridgeUID = this.bridgeUID;
154 if (bridgeUID == null) {
155 logger.warn("Received a message from non-existent bridge. This most likely is a bug.");
159 ThingTypeUID thingTypeUID;
160 LightType lightType = light.type;
162 if (lightType == null) {
163 logger.warn("No light type reported for light {} ({})", light.modelid, light.name);
167 Map<String, Object> properties = new HashMap<>();
168 properties.put(CONFIG_ID, lightId);
169 properties.put(UNIQUE_ID, light.uniqueid);
170 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, light.swversion);
171 properties.put(Thing.PROPERTY_VENDOR, light.manufacturername);
172 properties.put(Thing.PROPERTY_MODEL_ID, light.modelid);
174 Integer ctmax = light.ctmax;
175 Integer ctmin = light.ctmin;
176 if (ctmax != null && ctmin != null) {
177 properties.put(PROPERTY_CT_MAX, Integer.toString(Util.constrainToRange(ctmax, ZCL_CT_MIN, ZCL_CT_MAX)));
178 properties.put(PROPERTY_CT_MIN, Integer.toString(Util.constrainToRange(ctmin, ZCL_CT_MIN, ZCL_CT_MAX)));
183 case ON_OFF_PLUGIN_UNIT:
185 thingTypeUID = THING_TYPE_ONOFF_LIGHT;
188 case DIMMABLE_PLUGIN_UNIT:
189 thingTypeUID = THING_TYPE_DIMMABLE_LIGHT;
191 case COLOR_TEMPERATURE_LIGHT:
192 thingTypeUID = THING_TYPE_COLOR_TEMPERATURE_LIGHT;
194 case COLOR_DIMMABLE_LIGHT:
196 thingTypeUID = THING_TYPE_COLOR_LIGHT;
198 case EXTENDED_COLOR_LIGHT:
199 thingTypeUID = THING_TYPE_EXTENDED_COLOR_LIGHT;
201 case WINDOW_COVERING_DEVICE:
202 thingTypeUID = THING_TYPE_WINDOW_COVERING;
205 thingTypeUID = THING_TYPE_WARNING_DEVICE;
208 thingTypeUID = THING_TYPE_DOORLOCK;
210 case CONFIGURATION_TOOL:
211 // ignore configuration tool device
215 "Found light: {} ({}), type {} but no thing type defined for that type. This should be reported.",
216 light.modelid, light.name, light.type);
220 ThingUID uid = new ThingUID(thingTypeUID, bridgeUID, light.uniqueid.replaceAll("[^a-z0-9\\[\\]]", ""));
221 DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID)
222 .withLabel(light.name + " (" + light.manufacturername + ")").withProperties(properties)
223 .withRepresentationProperty(UNIQUE_ID).build();
224 thingDiscovered(discoveryResult);
228 * Add a sensor device to the discovery inbox.
230 * @param sensorID The id of the sensor
231 * @param sensor The sensor description
233 private void addSensor(String sensorID, SensorMessage sensor) {
234 final ThingUID bridgeUID = this.bridgeUID;
235 if (bridgeUID == null) {
236 logger.warn("Received a message from non-existent bridge. This most likely is a bug.");
239 ThingTypeUID thingTypeUID;
241 Map<String, Object> properties = new HashMap<>();
242 properties.put(CONFIG_ID, sensorID);
243 properties.put(UNIQUE_ID, sensor.uniqueid);
244 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, sensor.swversion);
245 properties.put(Thing.PROPERTY_VENDOR, sensor.manufacturername);
246 properties.put(Thing.PROPERTY_MODEL_ID, sensor.modelid);
248 if (sensor.type.contains("Daylight")) { // deCONZ specific: Software simulated daylight sensor
249 thingTypeUID = THING_TYPE_DAYLIGHT_SENSOR;
250 } else if (sensor.type.contains("Power")) { // ZHAPower, CLIPPower
251 thingTypeUID = THING_TYPE_POWER_SENSOR;
252 } else if (sensor.type.contains("ZHAConsumption")) { // ZHAConsumption
253 thingTypeUID = THING_TYPE_CONSUMPTION_SENSOR;
254 } else if (sensor.type.contains("Presence")) { // ZHAPresence, CLIPPrensence
255 thingTypeUID = THING_TYPE_PRESENCE_SENSOR;
256 } else if (sensor.type.contains("Switch")) { // ZHASwitch
257 if (sensor.modelid.contains("RGBW")) {
258 thingTypeUID = THING_TYPE_COLOR_CONTROL;
260 thingTypeUID = THING_TYPE_SWITCH;
262 } else if (sensor.type.contains("LightLevel")) { // ZHALightLevel
263 thingTypeUID = THING_TYPE_LIGHT_SENSOR;
264 } else if (sensor.type.contains("ZHATemperature")) { // ZHATemperature
265 thingTypeUID = THING_TYPE_TEMPERATURE_SENSOR;
266 } else if (sensor.type.contains("ZHAHumidity")) { // ZHAHumidity
267 thingTypeUID = THING_TYPE_HUMIDITY_SENSOR;
268 } else if (sensor.type.contains("ZHAPressure")) { // ZHAPressure
269 thingTypeUID = THING_TYPE_PRESSURE_SENSOR;
270 } else if (sensor.type.contains("ZHAOpenClose")) { // ZHAOpenClose
271 thingTypeUID = THING_TYPE_OPENCLOSE_SENSOR;
272 } else if (sensor.type.contains("ZHAWater")) { // ZHAWater
273 thingTypeUID = THING_TYPE_WATERLEAKAGE_SENSOR;
274 } else if (sensor.type.contains("ZHAFire")) {
275 thingTypeUID = THING_TYPE_FIRE_SENSOR; // ZHAFire
276 } else if (sensor.type.contains("ZHAAlarm")) {
277 thingTypeUID = THING_TYPE_ALARM_SENSOR; // ZHAAlarm
278 } else if (sensor.type.contains("ZHAVibration")) {
279 thingTypeUID = THING_TYPE_VIBRATION_SENSOR; // ZHAVibration
280 } else if (sensor.type.contains("ZHABattery")) {
281 thingTypeUID = THING_TYPE_BATTERY_SENSOR; // ZHABattery
282 } else if (sensor.type.contains("ZHAThermostat")) {
283 thingTypeUID = THING_TYPE_THERMOSTAT; // ZHAThermostat
284 } else if (sensor.type.contains("ZHAAirQuality")) {
285 thingTypeUID = THING_TYPE_AIRQUALITY_SENSOR;
287 logger.debug("Unknown type {}", sensor.type);
291 ThingUID uid = new ThingUID(thingTypeUID, bridgeUID, sensor.uniqueid.replaceAll("[^a-z0-9\\[\\]]", ""));
293 DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID)
294 .withLabel(sensor.name + " (" + sensor.manufacturername + ")").withProperties(properties)
295 .withRepresentationProperty(UNIQUE_ID).build();
296 thingDiscovered(discoveryResult);
300 public void setThingHandler(@Nullable ThingHandler handler) {
301 if (handler instanceof DeconzBridgeHandler) {
302 this.handler = (DeconzBridgeHandler) handler;
303 this.bridgeUID = handler.getThing().getUID();
308 public @Nullable ThingHandler getThingHandler() {
313 public void activate() {
314 super.activate(null);
318 public void deactivate() {