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.deconz.internal.discovery;
15 import static org.openhab.binding.deconz.internal.BindingConstants.*;
17 import java.util.Date;
18 import java.util.HashMap;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23 import java.util.stream.Collectors;
24 import java.util.stream.Stream;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.deconz.internal.Util;
29 import org.openhab.binding.deconz.internal.dto.GroupMessage;
30 import org.openhab.binding.deconz.internal.dto.LightMessage;
31 import org.openhab.binding.deconz.internal.dto.SensorMessage;
32 import org.openhab.binding.deconz.internal.handler.DeconzBridgeHandler;
33 import org.openhab.binding.deconz.internal.handler.LightThingHandler;
34 import org.openhab.binding.deconz.internal.handler.SensorThermostatThingHandler;
35 import org.openhab.binding.deconz.internal.handler.SensorThingHandler;
36 import org.openhab.binding.deconz.internal.types.GroupType;
37 import org.openhab.binding.deconz.internal.types.LightType;
38 import org.openhab.core.config.discovery.AbstractThingHandlerDiscoveryService;
39 import org.openhab.core.config.discovery.DiscoveryResult;
40 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
41 import org.openhab.core.config.discovery.DiscoveryService;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingTypeUID;
44 import org.openhab.core.thing.ThingUID;
45 import org.osgi.service.component.annotations.Activate;
46 import org.osgi.service.component.annotations.Component;
47 import org.osgi.service.component.annotations.ServiceScope;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
52 * Every bridge will add its discovered sensors to this discovery service to make them
53 * available to the framework.
55 * @author David Graeff - Initial contribution
57 @Component(scope = ServiceScope.PROTOTYPE, service = ThingDiscoveryService.class)
59 public class ThingDiscoveryService extends AbstractThingHandlerDiscoveryService<DeconzBridgeHandler>
60 implements DiscoveryService {
61 private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
62 .of(LightThingHandler.SUPPORTED_THING_TYPE_UIDS, SensorThingHandler.SUPPORTED_THING_TYPES,
63 SensorThermostatThingHandler.SUPPORTED_THING_TYPES)
64 .flatMap(Set::stream).collect(Collectors.toSet());
65 private final Logger logger = LoggerFactory.getLogger(ThingDiscoveryService.class);
67 private @Nullable ScheduledFuture<?> scanningJob;
68 private @Nullable ThingUID bridgeUID;
71 public ThingDiscoveryService() {
72 super(DeconzBridgeHandler.class, SUPPORTED_THING_TYPES_UIDS, 30);
76 public void startScan() {
77 thingHandler.getBridgeFullState().thenAccept(fullState -> {
79 fullState.ifPresent(state -> {
80 state.sensors.forEach(this::addSensor);
81 state.lights.forEach(this::addLight);
82 state.groups.forEach(this::addGroup);
89 protected synchronized void stopScan() {
90 removeOlderResults(getTimestampOfLastScan());
95 protected void startBackgroundDiscovery() {
96 final ScheduledFuture<?> scanningJob = this.scanningJob;
97 if (scanningJob == null || scanningJob.isCancelled()) {
98 this.scanningJob = scheduler.scheduleWithFixedDelay(this::startScan, 0, 5, TimeUnit.MINUTES);
103 protected void stopBackgroundDiscovery() {
104 final ScheduledFuture<?> scanningJob = this.scanningJob;
105 if (scanningJob != null) {
106 scanningJob.cancel(true);
107 this.scanningJob = null;
112 * Add a group to the discovery inbox.
114 * @param groupId The id of the light
115 * @param group The group description
117 private void addGroup(String groupId, GroupMessage group) {
118 final ThingUID bridgeUID = this.bridgeUID;
119 if (bridgeUID == null) {
120 logger.warn("Received a message from non-existent bridge. This most likely is a bug.");
124 ThingTypeUID thingTypeUID;
125 GroupType groupType = group.type;
127 if (groupType == null) {
128 logger.warn("No group type reported for group {} ({})", group.modelid, group.name);
132 Map<String, Object> properties = new HashMap<>();
133 properties.put(CONFIG_ID, groupId);
136 case LIGHT_GROUP -> thingTypeUID = THING_TYPE_LIGHTGROUP;
137 case LUMINAIRE, LIGHT_SOURCE, ROOM -> {
138 logger.debug("Group {} ({}), type {} ignored.", group.id, group.name, group.type);
143 "Found group: {} ({}), type {} but no thing type defined for that type. This should be reported.",
144 group.id, group.name, group.type);
149 ThingUID uid = new ThingUID(thingTypeUID, bridgeUID, group.id);
150 DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID).withLabel(group.name)
151 .withProperties(properties).withRepresentationProperty(CONFIG_ID).build();
152 thingDiscovered(discoveryResult);
156 * Add a light device to the discovery inbox.
158 * @param lightId The id of the light
159 * @param light The light description
161 private void addLight(String lightId, LightMessage light) {
162 final ThingUID bridgeUID = this.bridgeUID;
163 if (bridgeUID == null) {
164 logger.warn("Received a message from non-existent bridge. This most likely is a bug.");
168 ThingTypeUID thingTypeUID;
169 LightType lightType = light.type;
171 if (lightType == null) {
172 logger.warn("No light type reported for light {} ({})", light.modelid, light.name);
176 Map<String, Object> properties = new HashMap<>();
177 properties.put(CONFIG_ID, lightId);
178 properties.put(UNIQUE_ID, light.uniqueid);
179 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, light.swversion);
180 properties.put(Thing.PROPERTY_VENDOR, light.manufacturername);
181 properties.put(Thing.PROPERTY_MODEL_ID, light.modelid);
183 Integer ctmax = light.ctmax;
184 Integer ctmin = light.ctmin;
185 if (ctmax != null && ctmin != null) {
186 properties.put(PROPERTY_CT_MAX, Integer.toString(Util.constrainToRange(ctmax, ZCL_CT_MIN, ZCL_CT_MAX)));
187 properties.put(PROPERTY_CT_MIN, Integer.toString(Util.constrainToRange(ctmin, ZCL_CT_MIN, ZCL_CT_MAX)));
191 case ON_OFF_LIGHT, ON_OFF_PLUGIN_UNIT, SMART_PLUG -> thingTypeUID = THING_TYPE_ONOFF_LIGHT;
192 case DIMMABLE_LIGHT, DIMMABLE_PLUGIN_UNIT -> thingTypeUID = THING_TYPE_DIMMABLE_LIGHT;
193 case COLOR_TEMPERATURE_LIGHT -> thingTypeUID = THING_TYPE_COLOR_TEMPERATURE_LIGHT;
194 case COLOR_DIMMABLE_LIGHT, COLOR_LIGHT -> thingTypeUID = THING_TYPE_COLOR_LIGHT;
195 case EXTENDED_COLOR_LIGHT -> thingTypeUID = THING_TYPE_EXTENDED_COLOR_LIGHT;
196 case WINDOW_COVERING_DEVICE, WINDOW_COVERING_CONTROLLER -> thingTypeUID = THING_TYPE_WINDOW_COVERING;
197 case WARNING_DEVICE -> thingTypeUID = THING_TYPE_WARNING_DEVICE;
198 case DOORLOCK -> thingTypeUID = THING_TYPE_DOORLOCK;
199 case CONFIGURATION_TOOL -> {
200 // ignore configuration tool device
205 "Found light: {} ({}), type {} but no thing type defined for that type. This should be reported.",
206 light.modelid, light.name, light.type);
211 ThingUID uid = new ThingUID(thingTypeUID, bridgeUID, light.uniqueid.replaceAll("[^a-z0-9\\[\\]]", ""));
212 DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID)
213 .withLabel(light.name + " (" + light.manufacturername + ")").withProperties(properties)
214 .withRepresentationProperty(UNIQUE_ID).build();
215 thingDiscovered(discoveryResult);
219 * Add a sensor device to the discovery inbox.
221 * @param sensorID The id of the sensor
222 * @param sensor The sensor description
224 private void addSensor(String sensorID, SensorMessage sensor) {
225 final ThingUID bridgeUID = this.bridgeUID;
226 if (bridgeUID == null) {
227 logger.warn("Received a message from non-existent bridge. This most likely is a bug.");
230 ThingTypeUID thingTypeUID;
232 Map<String, Object> properties = new HashMap<>();
233 properties.put(CONFIG_ID, sensorID);
234 properties.put(UNIQUE_ID, sensor.uniqueid);
235 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, sensor.swversion);
236 properties.put(Thing.PROPERTY_VENDOR, sensor.manufacturername);
237 properties.put(Thing.PROPERTY_MODEL_ID, sensor.modelid);
239 if (sensor.type.contains("Daylight")) { // deCONZ specific: Software simulated daylight sensor
240 thingTypeUID = THING_TYPE_DAYLIGHT_SENSOR;
241 } else if (sensor.type.contains("Power")) { // ZHAPower, CLIPPower
242 thingTypeUID = THING_TYPE_POWER_SENSOR;
243 } else if (sensor.type.contains("ZHAConsumption")) { // ZHAConsumption
244 thingTypeUID = THING_TYPE_CONSUMPTION_SENSOR;
245 } else if (sensor.type.contains("Presence")) { // ZHAPresence, CLIPPrensence
246 thingTypeUID = THING_TYPE_PRESENCE_SENSOR;
247 } else if (sensor.type.contains("Switch")) { // ZHASwitch
248 if (sensor.modelid.contains("RGBW")) {
249 thingTypeUID = THING_TYPE_COLOR_CONTROL;
251 thingTypeUID = THING_TYPE_SWITCH;
253 } else if (sensor.type.contains("LightLevel")) { // ZHALightLevel
254 thingTypeUID = THING_TYPE_LIGHT_SENSOR;
255 } else if (sensor.type.contains("ZHAAirQuality")) { // ZHAAirQuality
256 thingTypeUID = THING_TYPE_AIRQUALITY_SENSOR;
257 } else if (sensor.type.contains("ZHATemperature")) { // ZHATemperature
258 thingTypeUID = THING_TYPE_TEMPERATURE_SENSOR;
259 } else if (sensor.type.contains("ZHAHumidity")) { // ZHAHumidity
260 thingTypeUID = THING_TYPE_HUMIDITY_SENSOR;
261 } else if (sensor.type.contains("ZHAPressure")) { // ZHAPressure
262 thingTypeUID = THING_TYPE_PRESSURE_SENSOR;
263 } else if (sensor.type.contains("ZHAOpenClose")) { // ZHAOpenClose
264 thingTypeUID = THING_TYPE_OPENCLOSE_SENSOR;
265 } else if (sensor.type.contains("ZHAWater")) { // ZHAWater
266 thingTypeUID = THING_TYPE_WATERLEAKAGE_SENSOR;
267 } else if (sensor.type.contains("ZHAFire")) {
268 thingTypeUID = THING_TYPE_FIRE_SENSOR; // ZHAFire
269 } else if (sensor.type.contains("ZHAAlarm")) {
270 thingTypeUID = THING_TYPE_ALARM_SENSOR; // ZHAAlarm
271 } else if (sensor.type.contains("ZHAVibration")) {
272 thingTypeUID = THING_TYPE_VIBRATION_SENSOR; // ZHAVibration
273 } else if (sensor.type.contains("ZHABattery")) {
274 thingTypeUID = THING_TYPE_BATTERY_SENSOR; // ZHABattery
275 } else if (sensor.type.contains("ZHAMoisture")) {
276 thingTypeUID = THING_TYPE_MOISTURE_SENSOR; // ZHAMoisture
277 } else if (sensor.type.contains("ZHAThermostat")) {
278 thingTypeUID = THING_TYPE_THERMOSTAT; // ZHAThermostat
280 logger.debug("Unknown type {}", sensor.type);
284 ThingUID uid = new ThingUID(thingTypeUID, bridgeUID, sensor.uniqueid.replaceAll("[^a-z0-9\\[\\]]", ""));
286 DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID)
287 .withLabel(sensor.name + " (" + sensor.manufacturername + ")").withProperties(properties)
288 .withRepresentationProperty(UNIQUE_ID).build();
289 thingDiscovered(discoveryResult);
293 public void initialize() {
294 bridgeUID = thingHandler.getThing().getUID();
299 public void dispose() {
301 removeOlderResults(new Date().getTime());