[mqtt.homeassistant] Fix thing consistency for existing things when a device adds or removes components (#17851)

* [mqtt.homeassistant] gracefully handle a component's discovery info being deleted

Signed-off-by: Cody Cutrer <cody@cutrer.us>
This commit is contained in:
Cody Cutrer 2024-12-05 14:31:58 -07:00 committed by GitHub
parent 1190c14f75
commit 35630e1074
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 267 additions and 127 deletions

View File

@ -69,6 +69,8 @@ public class DiscoverComponents implements MqttMessageSubscriber {
*/
public static interface ComponentDiscovered {
void componentDiscovered(HaID homeAssistantTopicID, AbstractComponent<?> component);
void componentRemoved(HaID homeAssistantTopicID);
}
/**
@ -121,7 +123,9 @@ public class DiscoverComponents implements MqttMessageSubscriber {
logger.warn("HomeAssistant discover error: {}", e.getMessage());
}
} else {
logger.warn("Configuration of HomeAssistant thing {} is empty", haID.objectID);
if (discoveredListener != null) {
discoveredListener.componentRemoved(haID);
}
}
}

View File

@ -18,18 +18,14 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@ -76,27 +72,14 @@ import com.google.gson.GsonBuilder;
public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
private final Logger logger = LoggerFactory.getLogger(HomeAssistantDiscovery.class);
private HomeAssistantConfiguration configuration;
protected final Map<String, Set<HaID>> componentsPerThingID = new TreeMap<>();
protected final Map<String, ThingUID> thingIDPerTopic = new TreeMap<>();
protected final Map<String, DiscoveryResult> results = new ConcurrentHashMap<>();
protected final Map<String, Set<HaID>> componentsPerThingID = new HashMap<>();
protected final Map<String, ThingUID> thingIDPerTopic = new HashMap<>();
protected final Map<String, DiscoveryResult> results = new HashMap<>();
protected final Map<String, DiscoveryResult> allResults = new HashMap<>();
private @Nullable ScheduledFuture<?> future;
private final Gson gson;
public static final Map<String, String> HA_COMP_TO_NAME = new TreeMap<>();
{
HA_COMP_TO_NAME.put("alarm_control_panel", "Alarm Control Panel");
HA_COMP_TO_NAME.put("binary_sensor", "Sensor");
HA_COMP_TO_NAME.put("camera", "Camera");
HA_COMP_TO_NAME.put("cover", "Blind");
HA_COMP_TO_NAME.put("fan", "Fan");
HA_COMP_TO_NAME.put("climate", "Climate Control");
HA_COMP_TO_NAME.put("light", "Light");
HA_COMP_TO_NAME.put("lock", "Lock");
HA_COMP_TO_NAME.put("sensor", "Sensor");
HA_COMP_TO_NAME.put("switch", "Switch");
}
static final String BASE_TOPIC = "homeassistant";
static final String BIRTH_TOPIC = "homeassistant/status";
static final String ONLINE_STATUS = "online";
@ -148,36 +131,8 @@ public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
return typeProvider.getThingTypes(null).stream().map(ThingType::getUID).collect(Collectors.toSet());
}
/**
* Summarize components such as {Switch, Switch, Sensor} into string "Sensor, 2x Switch"
*
* @param componentNames stream of component names
* @return summary string of component names and their counts
*/
static String getComponentNamesSummary(Stream<String> componentNames) {
StringBuilder summary = new StringBuilder();
Collector<String, ?, Long> countingCollector = Collectors.counting();
Map<String, Long> componentCounts = componentNames
.collect(Collectors.groupingBy(Function.identity(), countingCollector));
componentCounts.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(entry -> {
String componentName = entry.getKey();
long count = entry.getValue();
if (summary.length() > 0) {
// not the first entry, so let's add the separating comma
summary.append(", ");
}
if (count > 1) {
summary.append(count);
summary.append("x ");
}
summary.append(componentName);
});
return summary.toString();
}
@Override
public void receivedMessage(ThingUID connectionBridge, MqttBrokerConnection connection, String topic,
byte[] payload) {
public void receivedMessage(ThingUID bridgeUID, MqttBrokerConnection connection, String topic, byte[] payload) {
resetTimeout();
// For HomeAssistant we need to subscribe to a wildcard topic, because topics can either be:
@ -188,13 +143,7 @@ public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
return;
}
// Reset the found-component timer.
// We will collect components for the thing label description for another 2 seconds.
final ScheduledFuture<?> future = this.future;
if (future != null) {
future.cancel(false);
}
this.future = scheduler.schedule(this::publishResults, 2, TimeUnit.SECONDS);
resetPublishTimer();
// We will of course find multiple of the same unique Thing IDs, for each different component another one.
// Therefore the components are assembled into a list and given to the DiscoveryResult label for the user to
@ -206,45 +155,18 @@ public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
.fromString(new String(payload, StandardCharsets.UTF_8), gson);
final String thingID = config.getThingId(haID.objectID);
final ThingUID thingUID = new ThingUID(MqttBindingConstants.HOMEASSISTANT_MQTT_THING, connectionBridge,
thingID);
final ThingUID thingUID = new ThingUID(MqttBindingConstants.HOMEASSISTANT_MQTT_THING, bridgeUID, thingID);
synchronized (results) {
thingIDPerTopic.put(topic, thingUID);
// We need to keep track of already found component topics for a specific thing
final List<HaID> components;
{
Set<HaID> componentsUnordered = componentsPerThingID.computeIfAbsent(thingID,
key -> ConcurrentHashMap.newKeySet());
// Invariant. For compiler, computeIfAbsent above returns always
// non-null
Objects.requireNonNull(componentsUnordered);
componentsUnordered.add(haID);
components = componentsUnordered.stream().collect(Collectors.toList());
// We sort the components for consistent jsondb serialization order of 'topics' thing property
// Sorting key is HaID::toString, i.e. using the full topic string
components.sort(Comparator.comparing(HaID::toString));
}
final String componentNames = getComponentNamesSummary(
components.stream().map(id -> id.component).map(c -> HA_COMP_TO_NAME.getOrDefault(c, c)));
final List<String> topics = components.stream().map(HaID::toShortTopic).collect(Collectors.toList());
Map<String, Object> properties = new HashMap<>();
HandlerConfiguration handlerConfig = new HandlerConfiguration(haID.baseTopic, topics);
properties = handlerConfig.appendToProperties(properties);
properties = config.appendToProperties(properties);
properties.put("deviceId", thingID);
properties.put("newStyleChannels", "true");
// Because we need the new properties map with the updated "components" list
results.put(thingUID.getAsString(),
DiscoveryResultBuilder.create(thingUID).withProperties(properties)
.withRepresentationProperty("deviceId").withBridge(connectionBridge)
.withLabel(config.getThingName() + " (" + componentNames + ")").build());
buildResult(thingID, thingUID, config.getThingName(), haID, properties, bridgeUID);
}
} catch (ConfigurationException e) {
logger.warn("HomeAssistant discover error: invalid configuration of thing {} component {}: {}",
haID.objectID, haID.component, e.getMessage());
@ -273,23 +195,64 @@ public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
getDiscoveryService().publish(BIRTH_TOPIC, ONLINE_STATUS.getBytes(), 1, false);
}
private void resetPublishTimer() {
// Reset the found-component timer.
// We will collect components for the thing label description for another 2 seconds.
final ScheduledFuture<?> future = this.future;
if (future != null) {
future.cancel(false);
}
this.future = scheduler.schedule(this::publishResults, 2, TimeUnit.SECONDS);
}
private void buildResult(String thingID, ThingUID thingUID, String thingName, HaID haID,
Map<String, Object> properties, ThingUID bridgeUID) {
// We need to keep track of already found component topics for a specific thing
final List<HaID> components;
{
Set<HaID> componentsUnordered = componentsPerThingID.computeIfAbsent(thingID, key -> new HashSet<>());
// Invariant. For compiler, computeIfAbsent above returns always
// non-null
Objects.requireNonNull(componentsUnordered);
componentsUnordered.add(haID);
components = componentsUnordered.stream().collect(Collectors.toList());
// We sort the components for consistent jsondb serialization order of 'topics' thing property
// Sorting key is HaID::toString, i.e. using the full topic string
components.sort(Comparator.comparing(HaID::toString));
}
final List<String> topics = components.stream().map(HaID::toShortTopic).collect(Collectors.toList());
HandlerConfiguration handlerConfig = new HandlerConfiguration(haID.baseTopic, topics);
properties = handlerConfig.appendToProperties(properties);
DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withProperties(properties)
.withRepresentationProperty("deviceId").withBridge(bridgeUID).withLabel(thingName).build();
// Because we need the new properties map with the updated "components" list
results.put(thingUID.toString(), result);
allResults.put(thingUID.toString(), result);
}
protected void publishResults() {
Collection<DiscoveryResult> localResults;
synchronized (results) {
localResults = new ArrayList<>(results.values());
results.clear();
componentsPerThingID.clear();
}
for (DiscoveryResult result : localResults) {
thingDiscovered(result);
}
}
@Override
public void topicVanished(ThingUID connectionBridge, MqttBrokerConnection connection, String topic) {
public void topicVanished(ThingUID bridgeUID, MqttBrokerConnection connection, String topic) {
if (!topic.endsWith("/config")) {
return;
}
if (thingIDPerTopic.containsKey(topic)) {
synchronized (results) {
ThingUID thingUID = thingIDPerTopic.remove(topic);
if (thingUID != null) {
final String thingID = thingUID.getId();
@ -299,7 +262,20 @@ public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
Set<HaID> components = componentsPerThingID.getOrDefault(thingID, Collections.emptySet());
components.remove(haID);
if (components.isEmpty()) {
allResults.remove(thingUID.toString());
results.remove(thingUID.toString());
thingRemoved(thingUID);
} else {
resetPublishTimer();
DiscoveryResult existingThing = allResults.get(thingUID.toString());
if (existingThing == null) {
logger.warn("Could not find discovery result for removed component {}; this is a bug",
thingUID);
return;
}
Map<String, Object> properties = new HashMap<>(existingThing.getProperties());
buildResult(thingID, thingUID, existingThing.getLabel(), haID, properties, bridgeUID);
}
}
}

View File

@ -82,7 +82,7 @@ import com.hubspot.jinjava.Jinjava;
*/
@NonNullByDefault
public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
implements ComponentDiscovered, Consumer<List<AbstractComponent<?>>> {
implements ComponentDiscovered, Consumer<List<Object>> {
public static final String AVAILABILITY_CHANNEL = "availability";
private static final Comparator<AbstractComponent<?>> COMPONENT_COMPARATOR = Comparator
.comparing((AbstractComponent<?> component) -> component.hasGroup())
@ -96,12 +96,13 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
protected final ChannelTypeRegistry channelTypeRegistry;
protected final Jinjava jinjava;
public final int attributeReceiveTimeout;
protected final DelayedBatchProcessing<AbstractComponent<?>> delayedProcessing;
protected final DelayedBatchProcessing<Object> delayedProcessing;
protected final DiscoverComponents discoverComponents;
private final Gson gson;
protected final Map<@Nullable String, AbstractComponent<?>> haComponents = new HashMap<>();
protected final Map<@Nullable String, AbstractComponent<?>> haComponentsByUniqueId = new HashMap<>();
protected final Map<HaID, AbstractComponent<?>> haComponentsByHaId = new HashMap<>();
protected final Map<ChannelUID, ChannelState> channelStates = new HashMap<>();
protected HandlerConfiguration config = new HandlerConfiguration();
@ -267,12 +268,38 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
delayedProcessing.accept(component);
}
@Override
public void componentRemoved(HaID haID) {
delayedProcessing.accept(haID);
}
/**
* Callback of {@link DelayedBatchProcessing}.
* Add all newly discovered components to the Thing and start the components.
* Add all newly discovered and removed components to the Thing and start the components.
*/
@Override
public void accept(List<AbstractComponent<?>> discoveredComponentsList) {
public void accept(List<Object> actions) {
List<AbstractComponent<?>> discoveredComponents = new ArrayList<>();
List<HaID> removedComponents = new ArrayList<>();
for (Object item : actions) {
if (item instanceof AbstractComponent<?> component) {
discoveredComponents.add(component);
} else if (item instanceof HaID removedComponent) {
removedComponents.add(removedComponent);
}
}
if (!discoveredComponents.isEmpty()) {
addComponents(discoveredComponents);
}
if (!removedComponents.isEmpty()) {
removeComponents(removedComponents);
}
}
/**
* Add all newly discovered components to the Thing and start the components.
*/
private void addComponents(List<AbstractComponent<?>> discoveredComponentsList) {
MqttBrokerConnection connection = this.connection;
if (connection == null) {
return;
@ -293,6 +320,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
// The component will be replaced in a moment.
known.stop();
haComponentsByUniqueId.remove(discovered.getUniqueId());
haComponentsByHaId.remove(known.getHaID());
haComponents.remove(known.getComponentId());
if (!known.getComponentId().equals(discovered.getComponentId())) {
discovered.resolveConflict();
@ -321,6 +349,29 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
}
}
/**
* Remove all matching deleted components.
*/
private void removeComponents(List<HaID> removedComponentsList) {
synchronized (haComponents) {
boolean componentActuallyRemoved = false;
for (HaID removed : removedComponentsList) {
AbstractComponent<?> known = haComponentsByHaId.get(removed);
if (known != null) {
// Don't wait for the future to complete. We are also not interested in failures.
known.stop();
haComponentsByUniqueId.remove(known.getUniqueId());
haComponents.remove(known.getComponentId());
haComponentsByHaId.remove(removed);
componentActuallyRemoved = true;
}
}
if (componentActuallyRemoved) {
updateThingType(getThing().getThingTypeUID());
}
}
}
@Override
protected void updateThingStatus(boolean messageReceived, Optional<Boolean> availabilityTopicsSeen) {
if (availabilityTopicsSeen.orElse(messageReceived)) {
@ -402,7 +453,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
return true;
}
private ThingTypeUID calculateThingTypeUID(AbstractComponent component) {
private ThingTypeUID calculateThingTypeUID(AbstractComponent<?> component) {
return new ThingTypeUID(MqttBindingConstants.BINDING_ID, MqttBindingConstants.HOMEASSISTANT_MQTT_THING.getId()
+ "_" + component.getChannelConfiguration().getThingId(component.getHaID().objectID));
}
@ -428,8 +479,8 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
}
// should only be called when it's safe to access haComponents
private boolean addComponent(AbstractComponent component) {
AbstractComponent existing = haComponents.get(component.getComponentId());
private boolean addComponent(AbstractComponent<?> component) {
AbstractComponent<?> existing = haComponents.get(component.getComponentId());
if (existing != null) {
// DeviceTriggers that are for the same subtype, topic, and value template
// can be coalesced together
@ -455,6 +506,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
});
}
haComponentsByUniqueId.put(component.getUniqueId(), component);
haComponentsByHaId.put(component.getHaID(), component);
return false;
}
}
@ -467,6 +519,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
}
haComponents.put(component.getComponentId(), component);
haComponentsByUniqueId.put(component.getUniqueId(), component);
haComponentsByHaId.put(component.getHaID(), component);
return true;
}
@ -478,16 +531,16 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
ChannelUID channelUID) {
Object component = multiComponentChannelConfig.get("component");
Object nodeid = multiComponentChannelConfig.get("nodeid");
if ((multiComponentChannelConfig.get("objectid") instanceof List objectIds)
&& (multiComponentChannelConfig.get("config") instanceof List configurations)) {
if ((multiComponentChannelConfig.get("objectid") instanceof List<?> objectIds)
&& (multiComponentChannelConfig.get("config") instanceof List<?> configurations)) {
if (objectIds.size() != configurations.size()) {
logger.warn("objectid and config for channel {} do not have the same number of items; ignoring",
channelUID);
return List.of();
}
List<Configuration> result = new ArrayList();
Iterator<Object> objectIdIterator = objectIds.iterator();
Iterator<Object> configIterator = configurations.iterator();
List<Configuration> result = new ArrayList<>();
Iterator<?> objectIdIterator = objectIds.iterator();
Iterator<?> configIterator = configurations.iterator();
while (objectIdIterator.hasNext()) {
Configuration componentConfiguration = new Configuration();
componentConfiguration.put("component", component);

View File

@ -15,13 +15,13 @@ package org.openhab.binding.mqtt.homeassistant.internal.discovery;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@ -55,14 +55,6 @@ public class HomeAssistantDiscoveryTests extends AbstractHomeAssistantTests {
discovery = new TestHomeAssistantDiscovery(channelTypeProvider);
}
@Test
public void testComponentNameSummary() {
assertThat(
HomeAssistantDiscovery.getComponentNamesSummary(
Stream.of("Sensor", "Switch", "Sensor", "Foobar", "Foobar", "Foobar")), //
is("3x Foobar, 2x Sensor, Switch"));
}
@Test
public void testOneThingDiscovery() throws Exception {
var discoveryListener = new LatchDiscoveryListener();
@ -88,11 +80,107 @@ public class HomeAssistantDiscoveryTests extends AbstractHomeAssistantTests {
assertThat(result.getProperties().get(Thing.PROPERTY_VENDOR), is("TuYa"));
assertThat(result.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), is("Zigbee2MQTT 1.18.2"));
assertThat(result.getProperties().get(HandlerConfiguration.PROPERTY_BASETOPIC), is("homeassistant"));
assertThat(result.getLabel(), is("th1 (Climate Control, Switch)"));
assertThat(result.getLabel(), is("th1"));
assertThat((List<String>) result.getProperties().get(HandlerConfiguration.PROPERTY_TOPICS), hasItems(
"climate/0x847127fffe11dd6a_climate_zigbee2mqtt", "switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt"));
}
@Test
public void testComponentAddedToExistingThing() throws Exception {
var discoveryListener = new LatchDiscoveryListener();
var latch = discoveryListener.createWaitForThingsDiscoveredLatch(1);
// When discover one thing with two channels
discovery.addDiscoveryListener(discoveryListener);
discovery.receivedMessage(HA_UID, bridgeConnection,
"homeassistant/climate/0x847127fffe11dd6a_climate_zigbee2mqtt/config",
getResourceAsByteArray("component/configTS0601ClimateThermostat.json"));
// Then one thing found
assert latch.await(3, TimeUnit.SECONDS);
var discoveryResults = discoveryListener.getDiscoveryResults();
assertThat(discoveryResults.size(), is(1));
var result = discoveryResults.get(0);
assertThat(result.getBridgeUID(), is(HA_UID));
assertThat(result.getProperties().get(Thing.PROPERTY_MODEL_ID),
is("Radiator valve with thermostat (TS0601_thermostat)"));
assertThat(result.getProperties().get(Thing.PROPERTY_VENDOR), is("TuYa"));
assertThat(result.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), is("Zigbee2MQTT 1.18.2"));
assertThat(result.getProperties().get(HandlerConfiguration.PROPERTY_BASETOPIC), is("homeassistant"));
assertThat(result.getLabel(), is("th1"));
assertThat((List<String>) result.getProperties().get(HandlerConfiguration.PROPERTY_TOPICS),
hasItems("climate/0x847127fffe11dd6a_climate_zigbee2mqtt"));
// Now another component added to the same thing
latch = discoveryListener.createWaitForThingsDiscoveredLatch(1);
discovery.receivedMessage(HA_UID, bridgeConnection,
"homeassistant/switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt/config",
getResourceAsByteArray("component/configTS0601AutoLock.json"));
assert latch.await(3, TimeUnit.SECONDS);
discoveryResults = discoveryListener.getDiscoveryResults();
assertThat(discoveryResults.size(), is(1));
result = discoveryResults.get(0);
assertThat(result.getBridgeUID(), is(HA_UID));
assertThat(result.getProperties().get(Thing.PROPERTY_MODEL_ID),
is("Radiator valve with thermostat (TS0601_thermostat)"));
assertThat(result.getProperties().get(Thing.PROPERTY_VENDOR), is("TuYa"));
assertThat(result.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), is("Zigbee2MQTT 1.18.2"));
assertThat(result.getProperties().get(HandlerConfiguration.PROPERTY_BASETOPIC), is("homeassistant"));
assertThat(result.getLabel(), is("th1"));
assertThat((List<String>) result.getProperties().get(HandlerConfiguration.PROPERTY_TOPICS), hasItems(
"climate/0x847127fffe11dd6a_climate_zigbee2mqtt", "switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt"));
}
@Test
public void testComponentRemovedFromExistingThing() throws Exception {
var discoveryListener = new LatchDiscoveryListener();
var latch = discoveryListener.createWaitForThingsDiscoveredLatch(1);
// When discover one thing with two channels
discovery.addDiscoveryListener(discoveryListener);
discovery.receivedMessage(HA_UID, bridgeConnection,
"homeassistant/climate/0x847127fffe11dd6a_climate_zigbee2mqtt/config",
getResourceAsByteArray("component/configTS0601ClimateThermostat.json"));
discovery.receivedMessage(HA_UID, bridgeConnection,
"homeassistant/switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt/config",
getResourceAsByteArray("component/configTS0601AutoLock.json"));
// Then one thing found
assert latch.await(3, TimeUnit.SECONDS);
var discoveryResults = discoveryListener.getDiscoveryResults();
assertThat(discoveryResults.size(), is(1));
var result = discoveryResults.get(0);
assertThat(result.getBridgeUID(), is(HA_UID));
assertThat(result.getProperties().get(Thing.PROPERTY_MODEL_ID),
is("Radiator valve with thermostat (TS0601_thermostat)"));
assertThat(result.getProperties().get(Thing.PROPERTY_VENDOR), is("TuYa"));
assertThat(result.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), is("Zigbee2MQTT 1.18.2"));
assertThat(result.getProperties().get(HandlerConfiguration.PROPERTY_BASETOPIC), is("homeassistant"));
assertThat(result.getLabel(), is("th1"));
assertThat((List<String>) result.getProperties().get(HandlerConfiguration.PROPERTY_TOPICS), hasItems(
"climate/0x847127fffe11dd6a_climate_zigbee2mqtt", "switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt"));
// Now remove the second component
latch = discoveryListener.createWaitForThingsDiscoveredLatch(1);
discovery.topicVanished(HA_UID, bridgeConnection,
"homeassistant/switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt/config");
assert latch.await(3, TimeUnit.SECONDS);
discoveryResults = discoveryListener.getDiscoveryResults();
assertThat(discoveryResults.size(), is(1));
result = discoveryResults.get(0);
assertThat(result.getBridgeUID(), is(HA_UID));
assertThat(result.getProperties().get(Thing.PROPERTY_MODEL_ID),
is("Radiator valve with thermostat (TS0601_thermostat)"));
assertThat(result.getProperties().get(Thing.PROPERTY_VENDOR), is("TuYa"));
assertThat(result.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), is("Zigbee2MQTT 1.18.2"));
assertThat(result.getProperties().get(HandlerConfiguration.PROPERTY_BASETOPIC), is("homeassistant"));
assertThat(result.getLabel(), is("th1"));
assertThat((List<String>) result.getProperties().get(HandlerConfiguration.PROPERTY_TOPICS),
hasItems("climate/0x847127fffe11dd6a_climate_zigbee2mqtt"));
}
private static class TestHomeAssistantDiscovery extends HomeAssistantDiscovery {
public TestHomeAssistantDiscovery(MqttChannelTypeProvider typeProvider) {
super(null);
@ -122,8 +210,10 @@ public class HomeAssistantDiscoveryTests extends AbstractHomeAssistantTests {
return Collections.emptyList();
}
public CopyOnWriteArrayList<DiscoveryResult> getDiscoveryResults() {
return discoveryResults;
public List<DiscoveryResult> getDiscoveryResults() {
ArrayList<DiscoveryResult> localResults = new ArrayList<>(discoveryResults);
discoveryResults.clear();
return localResults;
}
public CountDownLatch createWaitForThingsDiscoveredLatch(int count) {

View File

@ -138,6 +138,26 @@ public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest {
"Connection " + haConnection.getClientId() + " not retrieving all topics");
}
private static class ComponentDiscoveredProxy implements ComponentDiscovered {
private final Map<String, AbstractComponent<?>> haComponents;
private final CountDownLatch latch;
public ComponentDiscoveredProxy(Map<String, AbstractComponent<?>> haComponents, CountDownLatch latch) {
this.haComponents = haComponents;
this.latch = latch;
}
@Override
public void componentDiscovered(HaID homeAssistantTopicID, AbstractComponent<?> component) {
haComponents.put(component.getComponentId(), component);
latch.countDown();
}
@Override
public void componentRemoved(HaID homeAssistantTopicID) {
}
}
@Test
public void parseHATree() throws Exception {
MqttChannelTypeProvider channelTypeProvider = mock(MqttChannelTypeProvider.class);
@ -154,10 +174,7 @@ public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest {
// In the following implementation we add the found component to the `haComponents` map
// and add the types to the channelTypeProvider, like in the real Thing handler.
final CountDownLatch latch = new CountDownLatch(1);
ComponentDiscovered cd = (haID, c) -> {
haComponents.put(c.getComponentId(), c);
latch.countDown();
};
ComponentDiscovered cd = new ComponentDiscoveredProxy(haComponents, latch);
// Start the discovery for 2000ms. Forced timeout after 4000ms.
HaID haID = new HaID(testObjectTopic + "/config");