Consolidate bridge generation checks (#20379)

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
This commit is contained in:
Jacob Laursen
2026-03-18 22:48:25 +01:00
committed by GitHub
parent b1b4b3bd0a
commit a137e4d4bf
5 changed files with 154 additions and 57 deletions
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2010-2026 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hue.internal;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Utility class for extracting the generation of a Hue Bridge from its model ID.
* The generation is determined from the bridge model ID, which follows the pattern {@code BSBxxx}.
*
* Currently the following generations are known:
*
* <ul>
* <li>Generation 1: Original Hue Bridge (round white, model ID "BSB001")</li>
* <li>Generation 2: Hue Bridge (square white, model ID "BSB002")</li>
* <li>Generation 3: Hue Bridge Pro (square black, model ID "BSB003")</li>
* </ul>
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public final class HueBridgeModel {
/**
* Pattern to extract the generation number from the model ID of the Hue Bridge.
* The model ID follows the format {@code BSBxxx} where {@code xxx} is a zero-padded generation number,
* for example {@code BSB002} for generation 2 and {@code BSB003} for generation 3 (Pro).
*/
private static final Pattern BSB_MODEL_ID_PATTERN = Pattern.compile("^BSB(\\d{3})$");
private HueBridgeModel() {
// utility class
}
/**
* Parses the generation of the Hue Bridge from its model ID.
*
* @param modelId the model ID of the Hue Bridge, e.g., "BSB001", "BSB002", "BSB003"
* @return the generation number of the Hue Bridge, or 0 if the model ID is unknown or does not match the expected
* pattern
*/
public static int getGeneration(String modelId) {
Matcher matcher = BSB_MODEL_ID_PATTERN.matcher(modelId);
return matcher.matches() ? Integer.parseInt(matcher.group(1)) : 0;
}
}
@@ -58,21 +58,23 @@ public class Clip2ThingDiscoveryService extends AbstractThingHandlerDiscoverySer
private static final int DISCOVERY_INTERVAL_SECONDS = 600;
/**
* Two maps of resource types and respective thing types to be discovered for non- motion aware (v2) and motion
* aware (v3+) bridges respectively.
* Resource types and respective thing types to be discovered for v2 bridges.
*/
public static final Map<Boolean, Map<ResourceType, ThingTypeUID>> DISCOVERY_TYPES = Map.of( //
false, Map.of( // non- motion aware v2 bridge
ResourceType.DEVICE, THING_TYPE_DEVICE, //
ResourceType.ROOM, THING_TYPE_ROOM, //
ResourceType.ZONE, THING_TYPE_ZONE, //
ResourceType.BRIDGE_HOME, THING_TYPE_ZONE),
true, Map.of( // motion aware v3+ bridge
ResourceType.DEVICE, THING_TYPE_DEVICE, //
ResourceType.ROOM, THING_TYPE_ROOM, //
ResourceType.ZONE, THING_TYPE_ZONE, //
ResourceType.BRIDGE_HOME, THING_TYPE_ZONE, //
ResourceType.MOTION_AREA_CONFIGURATION, THING_TYPE_AREA));
public static final Map<ResourceType, ThingTypeUID> DISCOVERY_TYPES_V2 = Map.of( //
ResourceType.DEVICE, THING_TYPE_DEVICE, //
ResourceType.ROOM, THING_TYPE_ROOM, //
ResourceType.ZONE, THING_TYPE_ZONE, //
ResourceType.BRIDGE_HOME, THING_TYPE_ZONE);
/**
* Resource types and respective thing types to be discovered for v3+ bridges.
*/
public static final Map<ResourceType, ThingTypeUID> DISCOVERY_TYPES_V3 = Map.of( //
ResourceType.DEVICE, THING_TYPE_DEVICE, //
ResourceType.ROOM, THING_TYPE_ROOM, //
ResourceType.ZONE, THING_TYPE_ZONE, //
ResourceType.BRIDGE_HOME, THING_TYPE_ZONE, //
ResourceType.MOTION_AREA_CONFIGURATION, THING_TYPE_AREA);
private @Nullable ScheduledFuture<?> discoveryTask;
@@ -102,8 +104,9 @@ public class Clip2ThingDiscoveryService extends AbstractThingHandlerDiscoverySer
if (thingHandler.getThing().getStatus() == ThingStatus.ONLINE) {
try {
ThingUID bridgeUID = thingHandler.getThing().getUID();
for (Entry<ResourceType, ThingTypeUID> entry : DISCOVERY_TYPES.get(thingHandler.motionAware())
.entrySet()) {
for (Entry<ResourceType, ThingTypeUID> entry : (thingHandler.getBridgeGeneration() >= 3
? DISCOVERY_TYPES_V3
: DISCOVERY_TYPES_V2).entrySet()) {
for (Resource resource : thingHandler.getResources(new ResourceReference().setType(entry.getKey()))
.getResources()) {
@@ -19,13 +19,12 @@ import java.util.Dictionary;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.jmdns.ServiceInfo;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.hue.internal.HueBridgeModel;
import org.openhab.binding.hue.internal.connection.Clip2Bridge;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
@@ -56,26 +55,6 @@ import org.slf4j.LoggerFactory;
@NonNullByDefault
public class HueBridgeMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant {
private static final Pattern BSB_MODEL_ID_PATTERN = Pattern.compile("^BSB(\\d{3})$");
/**
* Checks if the given model ID is a BSB model and if its version is 003 or above.
*
* @param modelId the model ID to check
* @return true if the model ID is a BSB model with version 003 or above, false otherwise
*/
public static boolean modelIsOrAboveBSB003(@Nullable String modelId) {
if (modelId == null) {
return false;
}
Matcher matcher = BSB_MODEL_ID_PATTERN.matcher(modelId);
if (!matcher.matches()) {
return false;
}
int version = Integer.parseInt(matcher.group(1));
return version >= 3;
}
private static final String SERVICE_TYPE = "_hue._tcp.local.";
private static final String MDNS_PROPERTY_BRIDGE_ID = "bridgeid";
private static final String MDNS_PROPERTY_MODEL_ID = "modelid";
@@ -183,10 +162,19 @@ public class HueBridgeMDNSDiscoveryParticipant implements MDNSDiscoveryParticipa
String id = service.getPropertyString(MDNS_PROPERTY_BRIDGE_ID);
if (id != null && !id.isBlank()) {
id = id.toLowerCase();
if (modelIsOrAboveBSB003(service.getPropertyString(MDNS_PROPERTY_MODEL_ID))) {
String modelId = service.getPropertyString(MDNS_PROPERTY_MODEL_ID);
int generation = modelId != null ? HueBridgeModel.getGeneration(modelId) : 0;
if (generation >= 3) {
return new ThingUID(THING_TYPE_BRIDGE_API2, id);
} else if (generation == 1) {
// The original Hue Bridge (round white) does not support CLIP 2,
// so we can directly return the ThingUID for the original bridge without further checks.
return new ThingUID(THING_TYPE_BRIDGE, id);
}
try {
// For square white bridges of generation 2, we need to check if CLIP 2 is supported
// to determine the correct ThingTypeUID. This is because some bridges could still
// be running older firmware without CLIP 2 support.
return Clip2Bridge.isClip2Supported(service.getHostAddresses()[0])
? new ThingUID(THING_TYPE_BRIDGE_API2, id)
: new ThingUID(THING_TYPE_BRIDGE, id);
@@ -32,6 +32,7 @@ import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.hue.internal.HueBridgeModel;
import org.openhab.binding.hue.internal.api.dto.clip2.MetaData;
import org.openhab.binding.hue.internal.api.dto.clip2.ProductData;
import org.openhab.binding.hue.internal.api.dto.clip2.Resource;
@@ -45,7 +46,6 @@ import org.openhab.binding.hue.internal.config.Clip2BridgeConfig;
import org.openhab.binding.hue.internal.connection.Clip2Bridge;
import org.openhab.binding.hue.internal.connection.HueTlsTrustManagerProvider;
import org.openhab.binding.hue.internal.discovery.Clip2ThingDiscoveryService;
import org.openhab.binding.hue.internal.discovery.HueBridgeMDNSDiscoveryParticipant;
import org.openhab.binding.hue.internal.exceptions.ApiException;
import org.openhab.binding.hue.internal.exceptions.AssetNotLoadedException;
import org.openhab.binding.hue.internal.exceptions.HttpUnauthorizedException;
@@ -110,14 +110,18 @@ public class Clip2BridgeHandler extends BaseBridgeHandler {
private static final String AUTOMATION_CHANNEL_DESCRIPTION_KEY = "dynamic-channel.automation-enable.description";
/**
* Two lists of resource references that need to be mass down loaded for non- motion aware (v2) and motion
* aware (v3+) bridges respectively.
* NOTE: the SCENE resources must be mass down loaded first!
* Resource references that need to be mass downloaded for v2 bridges.
* NOTE: the SCENE resources must be mass downloaded first!
*/
public static final Map<Boolean, List<ResourceReference>> MASS_DOWNLOAD_RESOURCE_REFERENCES = Map.of( //
false, List.of(SCENE, DEVICE, ROOM, ZONE), // non- motion aware v2 bridge
true, List.of(SCENE, DEVICE, ROOM, ZONE, AREA) // motion aware v3+ bridge
);
public static final List<ResourceReference> MASS_DOWNLOAD_RESOURCE_REFERENCES_V2 = List.of(SCENE, DEVICE, ROOM,
ZONE);
/**
* Resource references that need to be mass downloaded for v3+ bridges.
* NOTE: the SCENE resources must be mass downloaded first!
*/
public static final List<ResourceReference> MASS_DOWNLOAD_RESOURCE_REFERENCES_V3 = List.of(SCENE, DEVICE, ROOM,
ZONE, AREA);
private final Logger logger = LoggerFactory.getLogger(Clip2BridgeHandler.class);
@@ -143,7 +147,13 @@ public class Clip2BridgeHandler extends BaseBridgeHandler {
private boolean assetsLoaded;
private int applKeyRetriesRemaining;
private int connectRetriesRemaining;
private boolean motionAware; // true if the bridge is a v3+ model that supports the motion aware feature
/**
* The generation of the bridge model, as returned by the DEVICE resource, used to determine if certain features are
* supported.
* For example, the motion aware feature is only supported on v3+ models.
*/
private int bridgeGeneration;
public Clip2BridgeHandler(Bridge bridge, HttpClientFactory httpClientFactory, ThingRegistry thingRegistry,
LocaleProvider localeProvider, TranslationProvider translationProvider) {
@@ -502,8 +512,8 @@ public class Clip2BridgeHandler extends BaseBridgeHandler {
return;
}
boolean useSignifyCaCertificateVersion2 = HueBridgeMDNSDiscoveryParticipant
.modelIsOrAboveBSB003(thing.getProperties().get(Thing.PROPERTY_MODEL_ID));
String modelId = thing.getProperties().get(Thing.PROPERTY_MODEL_ID);
boolean useSignifyCaCertificateVersion2 = modelId != null && HueBridgeModel.getGeneration(modelId) >= 3;
HueTlsTrustManagerProvider trustManagerProvider = new HueTlsTrustManagerProvider(ipAddress + ":443",
config.useSelfSignedCertificate, useSignifyCaCertificateVersion2);
@@ -706,7 +716,7 @@ public class Clip2BridgeHandler extends BaseBridgeHandler {
properties.put(PROPERTY_PRODUCT_ARCHETYPE, productData.getProductArchetype().toString());
properties.put(PROPERTY_PRODUCT_CERTIFIED, productData.getCertified().toString());
motionAware = !"BSB002".equals(productData.getModelId());
bridgeGeneration = HueBridgeModel.getGeneration(productData.getModelId());
}
break; // we only needed the BRIDGE_V2 or BRIDGE_V3 resource
}
@@ -779,7 +789,8 @@ public class Clip2BridgeHandler extends BaseBridgeHandler {
logger.debug("updateThingsNow()");
try {
Clip2Bridge bridge = getClip2Bridge();
for (ResourceReference reference : MASS_DOWNLOAD_RESOURCE_REFERENCES.get(motionAware)) {
for (ResourceReference reference : bridgeGeneration >= 3 ? MASS_DOWNLOAD_RESOURCE_REFERENCES_V3
: MASS_DOWNLOAD_RESOURCE_REFERENCES_V2) {
ResourceType resourceType = reference.getType();
List<Resource> resourceList = bridge.getResources(reference).getResources();
switch (resourceType) {
@@ -947,11 +958,11 @@ public class Clip2BridgeHandler extends BaseBridgeHandler {
}
/**
* Getter for whether the bridge is motion aware
*
* @return
* Getter for the bridge generation, as determined from the model id.
*
* @return the bridge generation, or 0 if the generation is unknown.
*/
public boolean motionAware() {
return motionAware;
public int getBridgeGeneration() {
return bridgeGeneration;
}
}
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2010-2026 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hue.internal;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
/**
* Unit tests for {@link HueBridgeModel}.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public class HueBridgeModelTest {
@Test
public void testHueBridgeModelGenerationParsing() {
assertThat(HueBridgeModel.getGeneration("UNKNOWN"), is(0));
assertThat(HueBridgeModel.getGeneration("BSB001"), is(1));
assertThat(HueBridgeModel.getGeneration("BSB002"), is(2));
assertThat(HueBridgeModel.getGeneration("BSB003"), is(3));
}
}