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.hue.internal.discovery;
15 import static org.openhab.binding.hue.internal.HueBindingConstants.*;
17 import java.util.AbstractMap.SimpleEntry;
18 import java.util.Date;
19 import java.util.HashMap;
20 import java.util.List;
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.hue.internal.api.dto.clip1.FullGroup;
29 import org.openhab.binding.hue.internal.api.dto.clip1.FullHueObject;
30 import org.openhab.binding.hue.internal.api.dto.clip1.FullLight;
31 import org.openhab.binding.hue.internal.api.dto.clip1.FullSensor;
32 import org.openhab.binding.hue.internal.handler.HueBridgeHandler;
33 import org.openhab.binding.hue.internal.handler.HueGroupHandler;
34 import org.openhab.binding.hue.internal.handler.HueLightHandler;
35 import org.openhab.binding.hue.internal.handler.sensors.ClipHandler;
36 import org.openhab.binding.hue.internal.handler.sensors.DimmerSwitchHandler;
37 import org.openhab.binding.hue.internal.handler.sensors.GeofencePresenceHandler;
38 import org.openhab.binding.hue.internal.handler.sensors.LightLevelHandler;
39 import org.openhab.binding.hue.internal.handler.sensors.PresenceHandler;
40 import org.openhab.binding.hue.internal.handler.sensors.TapSwitchHandler;
41 import org.openhab.binding.hue.internal.handler.sensors.TemperatureHandler;
42 import org.openhab.core.config.discovery.AbstractDiscoveryService;
43 import org.openhab.core.config.discovery.DiscoveryResult;
44 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
45 import org.openhab.core.thing.Thing;
46 import org.openhab.core.thing.ThingTypeUID;
47 import org.openhab.core.thing.ThingUID;
48 import org.openhab.core.thing.binding.ThingHandler;
49 import org.openhab.core.thing.binding.ThingHandlerService;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
54 * The {@link HueDeviceDiscoveryService} tracks for Hue lights, sensors and groups which are connected
55 * to a paired Hue Bridge. The default search time for Hue is 60 seconds.
57 * @author Kai Kreuzer - Initial contribution
58 * @author Andre Fuechsel - changed search timeout, changed discovery result creation to support generic thing types;
59 * added representationProperty to discovery result
60 * @author Thomas Höfer - Added representation
61 * @author Denis Dudnik - switched to internally integrated source of Jue library
62 * @author Samuel Leisering - Added support for sensor API
63 * @author Christoph Weitkamp - Added support for sensor API
64 * @author Meng Yiqi - Added support for CLIP sensor
65 * @author Laurent Garnier - Added support for groups
68 public class HueDeviceDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
70 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Stream
71 .of(HueLightHandler.SUPPORTED_THING_TYPES.stream(), DimmerSwitchHandler.SUPPORTED_THING_TYPES.stream(),
72 TapSwitchHandler.SUPPORTED_THING_TYPES.stream(), PresenceHandler.SUPPORTED_THING_TYPES.stream(),
73 GeofencePresenceHandler.SUPPORTED_THING_TYPES.stream(),
74 TemperatureHandler.SUPPORTED_THING_TYPES.stream(), LightLevelHandler.SUPPORTED_THING_TYPES.stream(),
75 ClipHandler.SUPPORTED_THING_TYPES.stream(), HueGroupHandler.SUPPORTED_THING_TYPES.stream())
76 .flatMap(i -> i).collect(Collectors.toUnmodifiableSet());
79 private static final Map<String, String> TYPE_TO_ZIGBEE_ID_MAP = Map.ofEntries(
80 new SimpleEntry<>("on_off_light", "0000"),
81 new SimpleEntry<>("on_off_plug_in_unit", "0010"),
82 new SimpleEntry<>("dimmable_light", "0100"),
83 new SimpleEntry<>("dimmable_plug_in_unit", "0110"),
84 new SimpleEntry<>("color_light", "0200"),
85 new SimpleEntry<>("extended_color_light", "0210"),
86 new SimpleEntry<>("color_temperature_light", "0220"),
87 new SimpleEntry<>("zllswitch", "0820"),
88 new SimpleEntry<>("zgpswitch", "0830"),
89 new SimpleEntry<>("clipgenericstatus", "0840"),
90 new SimpleEntry<>("clipgenericflag", "0850"),
91 new SimpleEntry<>("zllpresence", "0107"),
92 new SimpleEntry<>("geofence", "geofencesensor"),
93 new SimpleEntry<>("zlltemperature", "0302"),
94 new SimpleEntry<>("zlllightlevel", "0106"));
97 private static final int SEARCH_TIME = 10;
99 private final Logger logger = LoggerFactory.getLogger(HueDeviceDiscoveryService.class);
101 private @Nullable HueBridgeHandler hueBridgeHandler;
102 private @Nullable ThingUID bridgeUID;
104 public HueDeviceDiscoveryService() {
105 super(SUPPORTED_THING_TYPES, SEARCH_TIME);
109 public void setThingHandler(@Nullable ThingHandler handler) {
110 if (handler instanceof HueBridgeHandler) {
111 HueBridgeHandler localHandler = (HueBridgeHandler) handler;
112 hueBridgeHandler = localHandler;
113 bridgeUID = handler.getThing().getUID();
114 i18nProvider = localHandler.getI18nProvider();
115 localeProvider = localHandler.getLocaleProvider();
120 public @Nullable ThingHandler getThingHandler() {
121 return hueBridgeHandler;
125 public void activate() {
126 final HueBridgeHandler handler = hueBridgeHandler;
127 if (handler != null) {
128 handler.registerDiscoveryListener(this);
133 public void deactivate() {
134 removeOlderResults(new Date().getTime(), bridgeUID);
135 final HueBridgeHandler handler = hueBridgeHandler;
136 if (handler != null) {
137 handler.unregisterDiscoveryListener();
142 public Set<ThingTypeUID> getSupportedThingTypes() {
143 return SUPPORTED_THING_TYPES;
147 public void startScan() {
148 final HueBridgeHandler handler = hueBridgeHandler;
149 if (handler != null) {
150 List<FullLight> lights = handler.getFullLights();
151 for (FullLight l : lights) {
152 addLightDiscovery(l);
154 List<FullSensor> sensors = handler.getFullSensors();
155 for (FullSensor s : sensors) {
156 addSensorDiscovery(s);
158 List<FullGroup> groups = handler.getFullGroups();
159 for (FullGroup g : groups) {
160 addGroupDiscovery(g);
162 // search for unpaired lights
163 handler.startSearch();
168 protected synchronized void stopScan() {
170 final HueBridgeHandler handler = hueBridgeHandler;
171 if (handler != null) {
172 removeOlderResults(getTimestampOfLastScan(), handler.getThing().getUID());
176 public void addLightDiscovery(FullLight light) {
177 ThingUID thingUID = getThingUID(light);
178 ThingTypeUID thingTypeUID = getThingTypeUID(light);
180 String modelId = light.getNormalizedModelID();
182 if (thingUID != null && thingTypeUID != null) {
183 Map<String, Object> properties = new HashMap<>();
184 properties.put(LIGHT_ID, light.getId());
185 if (modelId != null) {
186 properties.put(Thing.PROPERTY_MODEL_ID, modelId);
188 String uniqueID = light.getUniqueID();
189 if (uniqueID != null) {
190 properties.put(UNIQUE_ID, uniqueID);
193 DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID)
194 .withProperties(properties).withBridge(bridgeUID).withRepresentationProperty(UNIQUE_ID)
195 .withLabel(light.getName()).build();
197 thingDiscovered(discoveryResult);
199 logger.debug("discovered unsupported light of type '{}' and model '{}' with id {}", light.getType(),
200 modelId, light.getId());
204 public void removeLightDiscovery(FullLight light) {
205 ThingUID thingUID = getThingUID(light);
207 if (thingUID != null) {
208 thingRemoved(thingUID);
212 private @Nullable ThingUID getThingUID(FullHueObject hueObject) {
213 ThingUID localBridgeUID = bridgeUID;
214 if (localBridgeUID != null) {
215 ThingTypeUID thingTypeUID = getThingTypeUID(hueObject);
217 if (thingTypeUID != null && getSupportedThingTypes().contains(thingTypeUID)) {
218 return new ThingUID(thingTypeUID, localBridgeUID, hueObject.getId());
224 private @Nullable ThingTypeUID getThingTypeUID(FullHueObject hueObject) {
225 String thingTypeId = TYPE_TO_ZIGBEE_ID_MAP
226 .get(hueObject.getType().replaceAll(NORMALIZE_ID_REGEX, "_").toLowerCase());
227 return thingTypeId != null ? new ThingTypeUID(BINDING_ID, thingTypeId) : null;
230 public void addSensorDiscovery(FullSensor sensor) {
231 ThingUID thingUID = getThingUID(sensor);
232 ThingTypeUID thingTypeUID = getThingTypeUID(sensor);
234 String modelId = sensor.getNormalizedModelID();
235 if (thingUID != null && thingTypeUID != null) {
236 Map<String, Object> properties = new HashMap<>();
237 properties.put(SENSOR_ID, sensor.getId());
238 if (modelId != null) {
239 properties.put(Thing.PROPERTY_MODEL_ID, modelId);
241 String uniqueID = sensor.getUniqueID();
242 if (uniqueID != null) {
243 properties.put(UNIQUE_ID, uniqueID);
246 DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID)
247 .withProperties(properties).withBridge(bridgeUID).withRepresentationProperty(UNIQUE_ID)
248 .withLabel(sensor.getName()).build();
250 thingDiscovered(discoveryResult);
252 logger.debug("discovered unsupported sensor of type '{}' and model '{}' with id {}", sensor.getType(),
253 modelId, sensor.getId());
257 public void removeSensorDiscovery(FullSensor sensor) {
258 ThingUID thingUID = getThingUID(sensor);
260 if (thingUID != null) {
261 thingRemoved(thingUID);
265 public void addGroupDiscovery(FullGroup group) {
266 // Ignore the Hue Entertainment Areas
267 if ("Entertainment".equalsIgnoreCase(group.getType())) {
271 ThingUID localBridgeUID = bridgeUID;
272 if (localBridgeUID != null) {
273 ThingUID thingUID = new ThingUID(THING_TYPE_GROUP, localBridgeUID, group.getId());
275 Map<String, Object> properties = new HashMap<>();
276 properties.put(GROUP_ID, group.getId());
279 if ("0".equals(group.getId())) {
280 name = "@text/discovery.group.all-lights.label";
281 } else if ("Room".equals(group.getType())) {
282 name = group.getName();
284 name = String.format("%s (%s)", group.getName(), group.getType());
286 DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withThingType(THING_TYPE_GROUP)
287 .withProperties(properties).withBridge(localBridgeUID).withRepresentationProperty(GROUP_ID)
288 .withLabel(name).build();
290 thingDiscovered(discoveryResult);
294 public void removeGroupDiscovery(FullGroup group) {
295 ThingUID localBridgeUID = bridgeUID;
296 if (localBridgeUID != null) {
297 ThingUID thingUID = new ThingUID(THING_TYPE_GROUP, localBridgeUID, group.getId());
298 thingRemoved(thingUID);