[homekit] Support bridges with hardware embedded accessory things (#19965)

Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
This commit is contained in:
Andrew Fiddian-Green
2026-01-05 23:52:34 +01:00
committed by GitHub
parent bfbf36102d
commit 544b96bf03
5 changed files with 1103 additions and 17 deletions
@@ -17,6 +17,7 @@ There are three types of Things supported:
Things of type `bridge` and `accessory` both communicate directly with their HomeKit accessory device via the LAN.
Whereas `bridged-accessory` Things communicate via their respective `bridge` Thing.
Sometimes a `bridge` may contain a `bridged-accessory` that is physically embedded within the same hardware.
## Discovery
@@ -55,6 +56,7 @@ The following table shows the Thing configuration parameters for `bridged-access
As a general rule `accessoryID` is set by the auto-discovery process.
However you can configure it manually if you wish.
It must be the ID of the `bridged-accessory` within the `bridge`.
The `accessoryID` is probably "1" for a `bridged-accessory` that is physically embedded within the same hardware as its `bridge`.
## Thing Pairing
@@ -15,16 +15,21 @@ package org.openhab.binding.homekit.internal.discovery;
import static org.openhab.binding.homekit.internal.HomekitBindingConstants.*;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.homekit.internal.dto.Accessory;
import org.openhab.binding.homekit.internal.dto.Content;
import org.openhab.binding.homekit.internal.handler.HomekitBridgeHandler;
import org.openhab.core.config.discovery.AbstractThingHandlerDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingUID;
import org.osgi.framework.Bundle;
import org.osgi.service.component.annotations.Component;
/**
@@ -60,27 +65,35 @@ public class HomekitBridgedAccessoryDiscoveryService
@Override
public void startScan() {
if (thingHandler instanceof HomekitBridgeHandler handler) {
discoverBridgedAccessories(handler.getThing(), handler.getAccessories().values());
discoverBridgedAccessories(handler.getThing(), handler.getAccessories().values(),
handler.getTranslationProvider(), handler.getBundle());
}
}
private void discoverBridgedAccessories(Thing bridge, Collection<Accessory> accessories) {
private void discoverBridgedAccessories(Thing bridge, Collection<Accessory> accessories,
TranslationProvider i18nProvider, Bundle bundle) {
String bridgeUniqueId = thingHandler.getThing().getConfiguration()
.get(CONFIG_UNIQUE_ID) instanceof String uniqueId ? uniqueId : null;
if (bridgeUniqueId == null) {
return;
}
accessories.forEach(accessory -> {
if (accessory.aid instanceof Long aid && aid != 1L && accessory.services != null) {
if (accessory.aid instanceof Long aid && accessory.services != null) {
ThingUID uid = new ThingUID(THING_TYPE_BRIDGED_ACCESSORY, bridge.getUID(), aid.toString());
String uniqueId = STRING_AID_FMT.formatted(bridgeUniqueId, aid);
String label = THING_LABEL_FMT.formatted(accessory.getAccessoryInstanceLabel(), uniqueId);
thingDiscovered(DiscoveryResultBuilder.create(uid) //
.withBridge(bridge.getUID()) //
.withLabel(label) //
.withProperty(CONFIG_ACCESSORY_ID, aid.toString()) //
.withProperty(PROPERTY_UNIQUE_ID, uniqueId).withRepresentationProperty(PROPERTY_UNIQUE_ID)
.build());
if (aid != 1L || Optional.ofNullable(accessory.services).stream().flatMap(List::stream)
.flatMap(service -> Optional.ofNullable(service.characteristics).stream()).flatMap(List::stream)
.map(characteristic -> characteristic.getContent(uid, null, i18nProvider, bundle))
.anyMatch(Content.ChannelDefinition.class::isInstance)) {
// if aid #1 yields at least one channel definition then also discover an embedded thing for it
thingDiscovered(DiscoveryResultBuilder.create(uid) //
.withBridge(bridge.getUID()) //
.withLabel(label) //
.withProperty(CONFIG_ACCESSORY_ID, aid.toString()) //
.withProperty(PROPERTY_UNIQUE_ID, uniqueId).withRepresentationProperty(PROPERTY_UNIQUE_ID)
.build());
}
}
});
}
@@ -79,13 +79,16 @@ public class Characteristic {
* whereas characteristics with dynamic values return a {@code ChannelDefinition} record.
* Examines the characteristic type, data format, permissions, and other properties to determine the appropriate
* Content type and, where relevant, the channel type, item type, tags, category, and attributes. In the case of a
* 'ChannelDefinition' the method also builds a ChannelType and registers it with the provided HomekitTypeProvider.
* 'ChannelDefinition' the method also builds a ChannelType and if a HomekitTypeProvider is provided then registers
* it with that provider.
*
* @param thingUID the ThingUID to associate the ChannelDefinition with.
* @param typeProvider the HomekitTypeProvider to register the channel type with.
* @param typeProvider the HomekitTypeProvider to register the channel type with; may be null.
* @param i18nProvider the TranslationProvider for localizing option labels.
* @param bundle the Bundle for localization context.
* @return the {@link Content} or null if it cannot be mapped.
*/
public @Nullable Content getContent(ThingUID thingUID, HomekitTypeProvider typeProvider,
public @Nullable Content getContent(ThingUID thingUID, @Nullable HomekitTypeProvider typeProvider,
TranslationProvider i18nProvider, Bundle bundle) {
CharacteristicType characteristicType = getCharacteristicType();
DataFormatType dataFormatType;
@@ -833,9 +836,10 @@ public class Characteristic {
String channelTypeLabel = characteristicType.toString();
if (!isStateChannel) {
ChannelType channelType = ChannelTypeBuilder.trigger(channelTypeUid, channelTypeLabel).build();
typeProvider.putChannelType(channelType);
if (typeProvider != null) {
ChannelType channelType = ChannelTypeBuilder.trigger(channelTypeUid, channelTypeLabel).build();
typeProvider.putChannelType(channelType);
}
} else {
if (itemType == null) {
return null;
@@ -911,8 +915,10 @@ public class Characteristic {
}
// persist the (state) channel TYPE
ChannelType channelType = chanTypBldr.build();
typeProvider.putChannelType(channelType);
if (typeProvider != null) {
ChannelType channelType = chanTypBldr.build();
typeProvider.putChannelType(channelType);
}
}
/*
@@ -162,4 +162,12 @@ public class HomekitBridgeHandler extends HomekitBaseAccessoryHandler implements
// a bridge requires all enabled bridged-accessories to be ready
notReadyThings.addAll(getThing().getThings().stream().filter(thing -> thing.isEnabled()).toList());
}
public TranslationProvider getTranslationProvider() {
return i18nProvider;
}
public Bundle getBundle() {
return bundle;
}
}