Skip to content

Commit fb16e90

Browse files
authored
[hue] Restore UPnP discovery for old bridges (openhab#14914)
* [hue] Restore UPnP discovery for old bridges Signed-off-by: Laurent Garnier <[email protected]>
1 parent 26a52a1 commit fb16e90

File tree

5 files changed

+179
-4
lines changed

5 files changed

+179
-4
lines changed

bundles/org.openhab.binding.hue/src/main/feature/feature.xml

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<feature name="openhab-binding-hue" description="Hue Binding" version="${project.version}">
66
<feature>openhab-runtime-base</feature>
77
<feature>openhab-transport-mdns</feature>
8+
<feature>openhab-transport-upnp</feature>
89
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.hue/${project.version}</bundle>
910
</feature>
1011
</features>

bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java

+2
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ public class HueBindingConstants {
8989

9090
// Bridge config properties
9191
public static final String HOST = "ipAddress";
92+
public static final String PORT = "port";
93+
public static final String PROTOCOL = "protocol";
9294
public static final String USER_NAME = "userName";
9395

9496
// Thing configuration properties

bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeMDNSDiscoveryParticipant.java

+2-4
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,6 @@ public class HueBridgeMDNSDiscoveryParticipant implements MDNSDiscoveryParticipa
5454
private static final String MDNS_PROPERTY_BRIDGE_ID = "bridgeid";
5555
private static final String MDNS_PROPERTY_MODEL_ID = "modelid";
5656

57-
private static final String CONFIG_PROPERTY_REMOVAL_GRACE_PERIOD = "removalGracePeriod";
58-
5957
private final Logger logger = LoggerFactory.getLogger(HueBridgeMDNSDiscoveryParticipant.class);
6058

6159
private long removalGracePeriod = 0L;
@@ -79,12 +77,12 @@ private void activateOrModifyService(ComponentContext componentContext) {
7977
if (autoDiscoveryPropertyValue != null && !autoDiscoveryPropertyValue.isBlank()) {
8078
isAutoDiscoveryEnabled = Boolean.valueOf(autoDiscoveryPropertyValue);
8179
}
82-
String removalGracePeriodPropertyValue = (String) properties.get(CONFIG_PROPERTY_REMOVAL_GRACE_PERIOD);
80+
String removalGracePeriodPropertyValue = (String) properties.get(REMOVAL_GRACE_PERIOD);
8381
if (removalGracePeriodPropertyValue != null && !removalGracePeriodPropertyValue.isBlank()) {
8482
try {
8583
removalGracePeriod = Long.parseLong(removalGracePeriodPropertyValue);
8684
} catch (NumberFormatException e) {
87-
logger.warn("Configuration property '{}' has invalid value: {}", CONFIG_PROPERTY_REMOVAL_GRACE_PERIOD,
85+
logger.warn("Configuration property '{}' has invalid value: {}", REMOVAL_GRACE_PERIOD,
8886
removalGracePeriodPropertyValue);
8987
}
9088
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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.hue.internal.discovery;
14+
15+
import static org.openhab.binding.hue.internal.HueBindingConstants.*;
16+
17+
import java.net.URL;
18+
import java.util.Dictionary;
19+
import java.util.Map;
20+
import java.util.Set;
21+
import java.util.regex.Matcher;
22+
import java.util.regex.Pattern;
23+
import java.util.regex.PatternSyntaxException;
24+
25+
import org.eclipse.jdt.annotation.NonNullByDefault;
26+
import org.eclipse.jdt.annotation.Nullable;
27+
import org.jupnp.model.meta.DeviceDetails;
28+
import org.jupnp.model.meta.ModelDetails;
29+
import org.jupnp.model.meta.RemoteDevice;
30+
import org.openhab.binding.hue.internal.handler.HueBridgeHandler;
31+
import org.openhab.core.config.discovery.DiscoveryResult;
32+
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
33+
import org.openhab.core.config.discovery.DiscoveryService;
34+
import org.openhab.core.config.discovery.upnp.UpnpDiscoveryParticipant;
35+
import org.openhab.core.config.discovery.upnp.internal.UpnpDiscoveryService;
36+
import org.openhab.core.thing.Thing;
37+
import org.openhab.core.thing.ThingTypeUID;
38+
import org.openhab.core.thing.ThingUID;
39+
import org.osgi.service.component.ComponentContext;
40+
import org.osgi.service.component.annotations.Activate;
41+
import org.osgi.service.component.annotations.Component;
42+
import org.osgi.service.component.annotations.Modified;
43+
import org.slf4j.Logger;
44+
import org.slf4j.LoggerFactory;
45+
46+
/**
47+
* The {@link HueBridgeUPNPDiscoveryParticipant} is responsible for discovering new and removed Hue Bridges. It uses the
48+
* central {@link UpnpDiscoveryService}.
49+
*
50+
* The discovery through UPnP was replaced by mDNS discovery for recent bridges (V2).
51+
* For old bridges (V1), the UPnP discovery is still required (as mDNS is not implemented).
52+
* This class allows discovering only old bridges using UPnP.
53+
*
54+
* @author Laurent Garnier - Initial contribution
55+
*/
56+
@Component(configurationPid = "discovery.hue")
57+
@NonNullByDefault
58+
public class HueBridgeUPNPDiscoveryParticipant implements UpnpDiscoveryParticipant {
59+
60+
private static final String EXPECTED_MODEL_NAME_PREFIX = "Philips hue bridge";
61+
62+
private final Logger logger = LoggerFactory.getLogger(HueBridgeUPNPDiscoveryParticipant.class);
63+
64+
private long removalGracePeriod = 50L;
65+
66+
private boolean isAutoDiscoveryEnabled = true;
67+
68+
@Activate
69+
protected void activate(ComponentContext componentContext) {
70+
activateOrModifyService(componentContext);
71+
}
72+
73+
@Modified
74+
protected void modified(ComponentContext componentContext) {
75+
activateOrModifyService(componentContext);
76+
}
77+
78+
private void activateOrModifyService(ComponentContext componentContext) {
79+
Dictionary<String, @Nullable Object> properties = componentContext.getProperties();
80+
String autoDiscoveryPropertyValue = (String) properties
81+
.get(DiscoveryService.CONFIG_PROPERTY_BACKGROUND_DISCOVERY);
82+
if (autoDiscoveryPropertyValue != null && !autoDiscoveryPropertyValue.isBlank()) {
83+
isAutoDiscoveryEnabled = Boolean.valueOf(autoDiscoveryPropertyValue);
84+
}
85+
String removalGracePeriodPropertyValue = (String) properties.get(REMOVAL_GRACE_PERIOD);
86+
if (removalGracePeriodPropertyValue != null && !removalGracePeriodPropertyValue.isBlank()) {
87+
try {
88+
removalGracePeriod = Long.parseLong(removalGracePeriodPropertyValue);
89+
} catch (NumberFormatException e) {
90+
logger.warn("Configuration property '{}' has invalid value: {}", REMOVAL_GRACE_PERIOD,
91+
removalGracePeriodPropertyValue);
92+
}
93+
}
94+
}
95+
96+
@Override
97+
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
98+
return HueBridgeHandler.SUPPORTED_THING_TYPES;
99+
}
100+
101+
@Override
102+
public @Nullable DiscoveryResult createResult(RemoteDevice device) {
103+
if (!isAutoDiscoveryEnabled) {
104+
return null;
105+
}
106+
DeviceDetails details = device.getDetails();
107+
ThingUID uid = getThingUID(device);
108+
if (details == null || uid == null) {
109+
return null;
110+
}
111+
URL baseUrl = details.getBaseURL();
112+
String serialNumber = details.getSerialNumber();
113+
if (baseUrl == null || serialNumber == null || serialNumber.isBlank()) {
114+
return null;
115+
}
116+
String label = String.format(DISCOVERY_LABEL_PATTERN, baseUrl.getHost());
117+
String modelName = EXPECTED_MODEL_NAME_PREFIX;
118+
ModelDetails modelDetails = details.getModelDetails();
119+
if (modelDetails != null && modelDetails.getModelName() != null && modelDetails.getModelNumber() != null) {
120+
modelName = String.format("%s (%s)", modelDetails.getModelName(), modelDetails.getModelNumber());
121+
}
122+
return DiscoveryResultBuilder.create(uid) //
123+
.withProperties(Map.of( //
124+
HOST, baseUrl.getHost(), //
125+
PORT, baseUrl.getPort(), //
126+
PROTOCOL, baseUrl.getProtocol(), //
127+
Thing.PROPERTY_MODEL_ID, modelName, //
128+
Thing.PROPERTY_SERIAL_NUMBER, serialNumber.toLowerCase())) //
129+
.withLabel(label) //
130+
.withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER) //
131+
.build();
132+
}
133+
134+
@Override
135+
public @Nullable ThingUID getThingUID(RemoteDevice device) {
136+
DeviceDetails details = device.getDetails();
137+
if (details == null) {
138+
return null;
139+
}
140+
String serialNumber = details.getSerialNumber();
141+
ModelDetails modelDetails = details.getModelDetails();
142+
if (serialNumber == null || serialNumber.isBlank() || modelDetails == null) {
143+
return null;
144+
}
145+
String modelName = modelDetails.getModelName();
146+
// Model name has the format "Philips hue bridge <year>" with <year> being 2012
147+
// for a hue bridge V1 or 2015 for a hue bridge V2.
148+
if (modelName == null || !modelName.startsWith(EXPECTED_MODEL_NAME_PREFIX)) {
149+
return null;
150+
}
151+
try {
152+
Pattern pattern = Pattern.compile("\\d{4}");
153+
Matcher matcher = pattern.matcher(modelName);
154+
int year = Integer.parseInt(matcher.find() ? matcher.group() : "9999");
155+
// The bridge is ignored if year is greater or equal to 2015
156+
if (year >= 2015) {
157+
return null;
158+
}
159+
} catch (PatternSyntaxException | NumberFormatException e) {
160+
// No int value found, this bridge is ignored
161+
return null;
162+
}
163+
return new ThingUID(THING_TYPE_BRIDGE, serialNumber.toLowerCase());
164+
}
165+
166+
@Override
167+
public long getRemovalGracePeriodSeconds(RemoteDevice device) {
168+
return removalGracePeriod;
169+
}
170+
}

itests/org.openhab.binding.hue.tests/itest.bndrun

+4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Fragment-Host: org.openhab.binding.hue
3636
org.osgi.util.promise;version='[1.2.0,1.2.1)',\
3737
org.eclipse.jdt.annotation;version='[2.2.100,2.2.101)',\
3838
javax.jmdns;version='[3.5.8,3.5.9)',\
39+
org.jupnp;version='[2.7.0,2.7.1)',\
3940
ch.qos.logback.classic;version='[1.2.11,1.2.12)',\
4041
ch.qos.logback.core;version='[1.2.11,1.2.12)',\
4142
biz.aQute.tester.junit-platform;version='[6.4.0,6.4.1)',\
@@ -45,9 +46,11 @@ Fragment-Host: org.openhab.binding.hue
4546
org.openhab.core.config.core;version='[4.0.0,4.0.1)',\
4647
org.openhab.core.config.discovery;version='[4.0.0,4.0.1)',\
4748
org.openhab.core.config.discovery.mdns;version='[4.0.0,4.0.1)',\
49+
org.openhab.core.config.discovery.upnp;version='[4.0.0,4.0.1)',\
4850
org.openhab.core.io.console;version='[4.0.0,4.0.1)',\
4951
org.openhab.core.io.net;version='[4.0.0,4.0.1)',\
5052
org.openhab.core.io.transport.mdns;version='[4.0.0,4.0.1)',\
53+
org.openhab.core.io.transport.upnp;version='[4.0.0,4.0.1)',\
5154
org.openhab.core.test;version='[4.0.0,4.0.1)',\
5255
org.openhab.core.thing;version='[4.0.0,4.0.1)',\
5356
com.google.gson;version='[2.9.1,2.9.2)',\
@@ -68,6 +71,7 @@ Fragment-Host: org.openhab.binding.hue
6871
org.eclipse.jetty.websocket.client;version='[9.4.50,9.4.51)',\
6972
org.eclipse.jetty.websocket.common;version='[9.4.50,9.4.51)',\
7073
org.ops4j.pax.logging.pax-logging-api;version='[2.2.0,2.2.1)',\
74+
org.ops4j.pax.web.pax-web-api;version='[8.0.15,8.0.16)',\
7175
org.osgi.service.component;version='[1.5.0,1.5.1)',\
7276
junit-jupiter-api;version='[5.9.2,5.9.3)',\
7377
junit-jupiter-engine;version='[5.9.2,5.9.3)',\

0 commit comments

Comments
 (0)