mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[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:
parent
1190c14f75
commit
35630e1074
@ -69,6 +69,8 @@ public class DiscoverComponents implements MqttMessageSubscriber {
|
|||||||
*/
|
*/
|
||||||
public static interface ComponentDiscovered {
|
public static interface ComponentDiscovered {
|
||||||
void componentDiscovered(HaID homeAssistantTopicID, AbstractComponent<?> component);
|
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());
|
logger.warn("HomeAssistant discover error: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.warn("Configuration of HomeAssistant thing {} is empty", haID.objectID);
|
if (discoveredListener != null) {
|
||||||
|
discoveredListener.componentRemoved(haID);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,18 +18,14 @@ import java.util.Collection;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeMap;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.stream.Collector;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
@ -76,27 +72,14 @@ import com.google.gson.GsonBuilder;
|
|||||||
public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
|
public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
|
||||||
private final Logger logger = LoggerFactory.getLogger(HomeAssistantDiscovery.class);
|
private final Logger logger = LoggerFactory.getLogger(HomeAssistantDiscovery.class);
|
||||||
private HomeAssistantConfiguration configuration;
|
private HomeAssistantConfiguration configuration;
|
||||||
protected final Map<String, Set<HaID>> componentsPerThingID = new TreeMap<>();
|
protected final Map<String, Set<HaID>> componentsPerThingID = new HashMap<>();
|
||||||
protected final Map<String, ThingUID> thingIDPerTopic = new TreeMap<>();
|
protected final Map<String, ThingUID> thingIDPerTopic = new HashMap<>();
|
||||||
protected final Map<String, DiscoveryResult> results = new ConcurrentHashMap<>();
|
protected final Map<String, DiscoveryResult> results = new HashMap<>();
|
||||||
|
protected final Map<String, DiscoveryResult> allResults = new HashMap<>();
|
||||||
|
|
||||||
private @Nullable ScheduledFuture<?> future;
|
private @Nullable ScheduledFuture<?> future;
|
||||||
private final Gson gson;
|
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 BASE_TOPIC = "homeassistant";
|
||||||
static final String BIRTH_TOPIC = "homeassistant/status";
|
static final String BIRTH_TOPIC = "homeassistant/status";
|
||||||
static final String ONLINE_STATUS = "online";
|
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());
|
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
|
@Override
|
||||||
public void receivedMessage(ThingUID connectionBridge, MqttBrokerConnection connection, String topic,
|
public void receivedMessage(ThingUID bridgeUID, MqttBrokerConnection connection, String topic, byte[] payload) {
|
||||||
byte[] payload) {
|
|
||||||
resetTimeout();
|
resetTimeout();
|
||||||
|
|
||||||
// For HomeAssistant we need to subscribe to a wildcard topic, because topics can either be:
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the found-component timer.
|
resetPublishTimer();
|
||||||
// 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);
|
|
||||||
|
|
||||||
// We will of course find multiple of the same unique Thing IDs, for each different component another one.
|
// 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
|
// 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);
|
.fromString(new String(payload, StandardCharsets.UTF_8), gson);
|
||||||
|
|
||||||
final String thingID = config.getThingId(haID.objectID);
|
final String thingID = config.getThingId(haID.objectID);
|
||||||
final ThingUID thingUID = new ThingUID(MqttBindingConstants.HOMEASSISTANT_MQTT_THING, connectionBridge,
|
final ThingUID thingUID = new ThingUID(MqttBindingConstants.HOMEASSISTANT_MQTT_THING, bridgeUID, thingID);
|
||||||
thingID);
|
|
||||||
|
|
||||||
thingIDPerTopic.put(topic, thingUID);
|
synchronized (results) {
|
||||||
|
thingIDPerTopic.put(topic, thingUID);
|
||||||
|
|
||||||
// We need to keep track of already found component topics for a specific thing
|
Map<String, Object> properties = new HashMap<>();
|
||||||
final List<HaID> components;
|
properties = config.appendToProperties(properties);
|
||||||
{
|
properties.put("deviceId", thingID);
|
||||||
Set<HaID> componentsUnordered = componentsPerThingID.computeIfAbsent(thingID,
|
properties.put("newStyleChannels", "true");
|
||||||
key -> ConcurrentHashMap.newKeySet());
|
|
||||||
|
|
||||||
// Invariant. For compiler, computeIfAbsent above returns always
|
buildResult(thingID, thingUID, config.getThingName(), haID, properties, bridgeUID);
|
||||||
// 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());
|
|
||||||
} catch (ConfigurationException e) {
|
} catch (ConfigurationException e) {
|
||||||
logger.warn("HomeAssistant discover error: invalid configuration of thing {} component {}: {}",
|
logger.warn("HomeAssistant discover error: invalid configuration of thing {} component {}: {}",
|
||||||
haID.objectID, haID.component, e.getMessage());
|
haID.objectID, haID.component, e.getMessage());
|
||||||
@ -273,23 +195,64 @@ public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
|
|||||||
getDiscoveryService().publish(BIRTH_TOPIC, ONLINE_STATUS.getBytes(), 1, false);
|
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() {
|
protected void publishResults() {
|
||||||
Collection<DiscoveryResult> localResults;
|
Collection<DiscoveryResult> localResults;
|
||||||
|
|
||||||
localResults = new ArrayList<>(results.values());
|
synchronized (results) {
|
||||||
results.clear();
|
localResults = new ArrayList<>(results.values());
|
||||||
componentsPerThingID.clear();
|
results.clear();
|
||||||
|
}
|
||||||
for (DiscoveryResult result : localResults) {
|
for (DiscoveryResult result : localResults) {
|
||||||
thingDiscovered(result);
|
thingDiscovered(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void topicVanished(ThingUID connectionBridge, MqttBrokerConnection connection, String topic) {
|
public void topicVanished(ThingUID bridgeUID, MqttBrokerConnection connection, String topic) {
|
||||||
if (!topic.endsWith("/config")) {
|
if (!topic.endsWith("/config")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (thingIDPerTopic.containsKey(topic)) {
|
synchronized (results) {
|
||||||
ThingUID thingUID = thingIDPerTopic.remove(topic);
|
ThingUID thingUID = thingIDPerTopic.remove(topic);
|
||||||
if (thingUID != null) {
|
if (thingUID != null) {
|
||||||
final String thingID = thingUID.getId();
|
final String thingID = thingUID.getId();
|
||||||
@ -299,7 +262,20 @@ public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
|
|||||||
Set<HaID> components = componentsPerThingID.getOrDefault(thingID, Collections.emptySet());
|
Set<HaID> components = componentsPerThingID.getOrDefault(thingID, Collections.emptySet());
|
||||||
components.remove(haID);
|
components.remove(haID);
|
||||||
if (components.isEmpty()) {
|
if (components.isEmpty()) {
|
||||||
|
allResults.remove(thingUID.toString());
|
||||||
|
results.remove(thingUID.toString());
|
||||||
thingRemoved(thingUID);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,7 @@ import com.hubspot.jinjava.Jinjava;
|
|||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
||||||
implements ComponentDiscovered, Consumer<List<AbstractComponent<?>>> {
|
implements ComponentDiscovered, Consumer<List<Object>> {
|
||||||
public static final String AVAILABILITY_CHANNEL = "availability";
|
public static final String AVAILABILITY_CHANNEL = "availability";
|
||||||
private static final Comparator<AbstractComponent<?>> COMPONENT_COMPARATOR = Comparator
|
private static final Comparator<AbstractComponent<?>> COMPONENT_COMPARATOR = Comparator
|
||||||
.comparing((AbstractComponent<?> component) -> component.hasGroup())
|
.comparing((AbstractComponent<?> component) -> component.hasGroup())
|
||||||
@ -96,12 +96,13 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
|||||||
protected final ChannelTypeRegistry channelTypeRegistry;
|
protected final ChannelTypeRegistry channelTypeRegistry;
|
||||||
protected final Jinjava jinjava;
|
protected final Jinjava jinjava;
|
||||||
public final int attributeReceiveTimeout;
|
public final int attributeReceiveTimeout;
|
||||||
protected final DelayedBatchProcessing<AbstractComponent<?>> delayedProcessing;
|
protected final DelayedBatchProcessing<Object> delayedProcessing;
|
||||||
protected final DiscoverComponents discoverComponents;
|
protected final DiscoverComponents discoverComponents;
|
||||||
|
|
||||||
private final Gson gson;
|
private final Gson gson;
|
||||||
protected final Map<@Nullable String, AbstractComponent<?>> haComponents = new HashMap<>();
|
protected final Map<@Nullable String, AbstractComponent<?>> haComponents = new HashMap<>();
|
||||||
protected final Map<@Nullable String, AbstractComponent<?>> haComponentsByUniqueId = 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 final Map<ChannelUID, ChannelState> channelStates = new HashMap<>();
|
||||||
|
|
||||||
protected HandlerConfiguration config = new HandlerConfiguration();
|
protected HandlerConfiguration config = new HandlerConfiguration();
|
||||||
@ -267,12 +268,38 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
|||||||
delayedProcessing.accept(component);
|
delayedProcessing.accept(component);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void componentRemoved(HaID haID) {
|
||||||
|
delayedProcessing.accept(haID);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback of {@link DelayedBatchProcessing}.
|
* 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
|
@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;
|
MqttBrokerConnection connection = this.connection;
|
||||||
if (connection == null) {
|
if (connection == null) {
|
||||||
return;
|
return;
|
||||||
@ -293,6 +320,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
|||||||
// The component will be replaced in a moment.
|
// The component will be replaced in a moment.
|
||||||
known.stop();
|
known.stop();
|
||||||
haComponentsByUniqueId.remove(discovered.getUniqueId());
|
haComponentsByUniqueId.remove(discovered.getUniqueId());
|
||||||
|
haComponentsByHaId.remove(known.getHaID());
|
||||||
haComponents.remove(known.getComponentId());
|
haComponents.remove(known.getComponentId());
|
||||||
if (!known.getComponentId().equals(discovered.getComponentId())) {
|
if (!known.getComponentId().equals(discovered.getComponentId())) {
|
||||||
discovered.resolveConflict();
|
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
|
@Override
|
||||||
protected void updateThingStatus(boolean messageReceived, Optional<Boolean> availabilityTopicsSeen) {
|
protected void updateThingStatus(boolean messageReceived, Optional<Boolean> availabilityTopicsSeen) {
|
||||||
if (availabilityTopicsSeen.orElse(messageReceived)) {
|
if (availabilityTopicsSeen.orElse(messageReceived)) {
|
||||||
@ -402,7 +453,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ThingTypeUID calculateThingTypeUID(AbstractComponent component) {
|
private ThingTypeUID calculateThingTypeUID(AbstractComponent<?> component) {
|
||||||
return new ThingTypeUID(MqttBindingConstants.BINDING_ID, MqttBindingConstants.HOMEASSISTANT_MQTT_THING.getId()
|
return new ThingTypeUID(MqttBindingConstants.BINDING_ID, MqttBindingConstants.HOMEASSISTANT_MQTT_THING.getId()
|
||||||
+ "_" + component.getChannelConfiguration().getThingId(component.getHaID().objectID));
|
+ "_" + 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
|
// should only be called when it's safe to access haComponents
|
||||||
private boolean addComponent(AbstractComponent component) {
|
private boolean addComponent(AbstractComponent<?> component) {
|
||||||
AbstractComponent existing = haComponents.get(component.getComponentId());
|
AbstractComponent<?> existing = haComponents.get(component.getComponentId());
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
// DeviceTriggers that are for the same subtype, topic, and value template
|
// DeviceTriggers that are for the same subtype, topic, and value template
|
||||||
// can be coalesced together
|
// can be coalesced together
|
||||||
@ -455,6 +506,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
haComponentsByUniqueId.put(component.getUniqueId(), component);
|
haComponentsByUniqueId.put(component.getUniqueId(), component);
|
||||||
|
haComponentsByHaId.put(component.getHaID(), component);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -467,6 +519,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
|||||||
}
|
}
|
||||||
haComponents.put(component.getComponentId(), component);
|
haComponents.put(component.getComponentId(), component);
|
||||||
haComponentsByUniqueId.put(component.getUniqueId(), component);
|
haComponentsByUniqueId.put(component.getUniqueId(), component);
|
||||||
|
haComponentsByHaId.put(component.getHaID(), component);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -478,16 +531,16 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
|||||||
ChannelUID channelUID) {
|
ChannelUID channelUID) {
|
||||||
Object component = multiComponentChannelConfig.get("component");
|
Object component = multiComponentChannelConfig.get("component");
|
||||||
Object nodeid = multiComponentChannelConfig.get("nodeid");
|
Object nodeid = multiComponentChannelConfig.get("nodeid");
|
||||||
if ((multiComponentChannelConfig.get("objectid") instanceof List objectIds)
|
if ((multiComponentChannelConfig.get("objectid") instanceof List<?> objectIds)
|
||||||
&& (multiComponentChannelConfig.get("config") instanceof List configurations)) {
|
&& (multiComponentChannelConfig.get("config") instanceof List<?> configurations)) {
|
||||||
if (objectIds.size() != configurations.size()) {
|
if (objectIds.size() != configurations.size()) {
|
||||||
logger.warn("objectid and config for channel {} do not have the same number of items; ignoring",
|
logger.warn("objectid and config for channel {} do not have the same number of items; ignoring",
|
||||||
channelUID);
|
channelUID);
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
List<Configuration> result = new ArrayList();
|
List<Configuration> result = new ArrayList<>();
|
||||||
Iterator<Object> objectIdIterator = objectIds.iterator();
|
Iterator<?> objectIdIterator = objectIds.iterator();
|
||||||
Iterator<Object> configIterator = configurations.iterator();
|
Iterator<?> configIterator = configurations.iterator();
|
||||||
while (objectIdIterator.hasNext()) {
|
while (objectIdIterator.hasNext()) {
|
||||||
Configuration componentConfiguration = new Configuration();
|
Configuration componentConfiguration = new Configuration();
|
||||||
componentConfiguration.put("component", component);
|
componentConfiguration.put("component", component);
|
||||||
|
@ -15,13 +15,13 @@ package org.openhab.binding.mqtt.homeassistant.internal.discovery;
|
|||||||
import static org.hamcrest.CoreMatchers.*;
|
import static org.hamcrest.CoreMatchers.*;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
@ -55,14 +55,6 @@ public class HomeAssistantDiscoveryTests extends AbstractHomeAssistantTests {
|
|||||||
discovery = new TestHomeAssistantDiscovery(channelTypeProvider);
|
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
|
@Test
|
||||||
public void testOneThingDiscovery() throws Exception {
|
public void testOneThingDiscovery() throws Exception {
|
||||||
var discoveryListener = new LatchDiscoveryListener();
|
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_VENDOR), is("TuYa"));
|
||||||
assertThat(result.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), is("Zigbee2MQTT 1.18.2"));
|
assertThat(result.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), is("Zigbee2MQTT 1.18.2"));
|
||||||
assertThat(result.getProperties().get(HandlerConfiguration.PROPERTY_BASETOPIC), is("homeassistant"));
|
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(
|
assertThat((List<String>) result.getProperties().get(HandlerConfiguration.PROPERTY_TOPICS), hasItems(
|
||||||
"climate/0x847127fffe11dd6a_climate_zigbee2mqtt", "switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt"));
|
"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 {
|
private static class TestHomeAssistantDiscovery extends HomeAssistantDiscovery {
|
||||||
public TestHomeAssistantDiscovery(MqttChannelTypeProvider typeProvider) {
|
public TestHomeAssistantDiscovery(MqttChannelTypeProvider typeProvider) {
|
||||||
super(null);
|
super(null);
|
||||||
@ -122,8 +210,10 @@ public class HomeAssistantDiscoveryTests extends AbstractHomeAssistantTests {
|
|||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public CopyOnWriteArrayList<DiscoveryResult> getDiscoveryResults() {
|
public List<DiscoveryResult> getDiscoveryResults() {
|
||||||
return discoveryResults;
|
ArrayList<DiscoveryResult> localResults = new ArrayList<>(discoveryResults);
|
||||||
|
discoveryResults.clear();
|
||||||
|
return localResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CountDownLatch createWaitForThingsDiscoveredLatch(int count) {
|
public CountDownLatch createWaitForThingsDiscoveredLatch(int count) {
|
||||||
|
@ -138,6 +138,26 @@ public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest {
|
|||||||
"Connection " + haConnection.getClientId() + " not retrieving all topics");
|
"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
|
@Test
|
||||||
public void parseHATree() throws Exception {
|
public void parseHATree() throws Exception {
|
||||||
MqttChannelTypeProvider channelTypeProvider = mock(MqttChannelTypeProvider.class);
|
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
|
// 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.
|
// and add the types to the channelTypeProvider, like in the real Thing handler.
|
||||||
final CountDownLatch latch = new CountDownLatch(1);
|
final CountDownLatch latch = new CountDownLatch(1);
|
||||||
ComponentDiscovered cd = (haID, c) -> {
|
ComponentDiscovered cd = new ComponentDiscoveredProxy(haComponents, latch);
|
||||||
haComponents.put(c.getComponentId(), c);
|
|
||||||
latch.countDown();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start the discovery for 2000ms. Forced timeout after 4000ms.
|
// Start the discovery for 2000ms. Forced timeout after 4000ms.
|
||||||
HaID haID = new HaID(testObjectTopic + "/config");
|
HaID haID = new HaID(testObjectTopic + "/config");
|
||||||
|
Loading…
Reference in New Issue
Block a user