2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.mqtt.homeassistant.internal.discovery;
15 import java.nio.charset.StandardCharsets;
16 import java.util.ArrayList;
17 import java.util.Collection;
18 import java.util.Collections;
19 import java.util.Comparator;
20 import java.util.HashMap;
21 import java.util.List;
23 import java.util.Objects;
25 import java.util.TreeMap;
26 import java.util.concurrent.ConcurrentHashMap;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
29 import java.util.stream.Collectors;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.mqtt.discovery.AbstractMQTTDiscovery;
34 import org.openhab.binding.mqtt.discovery.MQTTTopicDiscoveryService;
35 import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
36 import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
37 import org.openhab.binding.mqtt.homeassistant.internal.HaID;
38 import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
39 import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory;
40 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
41 import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
42 import org.openhab.core.config.discovery.DiscoveryResult;
43 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
44 import org.openhab.core.config.discovery.DiscoveryService;
45 import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
46 import org.openhab.core.thing.ThingTypeUID;
47 import org.openhab.core.thing.ThingUID;
48 import org.openhab.core.thing.type.ThingType;
49 import org.osgi.service.component.annotations.Component;
50 import org.osgi.service.component.annotations.Reference;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
54 import com.google.gson.Gson;
55 import com.google.gson.GsonBuilder;
58 * The {@link HomeAssistantDiscovery} is responsible for discovering device nodes that follow the
59 * Home Assistant MQTT discovery convention (https://www.home-assistant.io/docs/mqtt/discovery/).
61 * @author David Graeff - Initial contribution
63 @Component(service = DiscoveryService.class, configurationPid = "discovery.mqttha")
65 public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
66 @SuppressWarnings("unused")
67 private final Logger logger = LoggerFactory.getLogger(HomeAssistantDiscovery.class);
68 protected final Map<String, Set<HaID>> componentsPerThingID = new TreeMap<>();
69 protected final Map<String, ThingUID> thingIDPerTopic = new TreeMap<>();
70 protected final Map<String, DiscoveryResult> results = new ConcurrentHashMap<>();
72 private @Nullable ScheduledFuture<?> future;
73 private final Gson gson;
75 public static final Map<String, String> HA_COMP_TO_NAME = new TreeMap<>();
77 HA_COMP_TO_NAME.put("alarm_control_panel", "Alarm Control Panel");
78 HA_COMP_TO_NAME.put("binary_sensor", "Sensor");
79 HA_COMP_TO_NAME.put("camera", "Camera");
80 HA_COMP_TO_NAME.put("cover", "Blind");
81 HA_COMP_TO_NAME.put("fan", "Fan");
82 HA_COMP_TO_NAME.put("climate", "Climate Control");
83 HA_COMP_TO_NAME.put("light", "Light");
84 HA_COMP_TO_NAME.put("lock", "Lock");
85 HA_COMP_TO_NAME.put("sensor", "Sensor");
86 HA_COMP_TO_NAME.put("switch", "Switch");
89 static final String BASE_TOPIC = "homeassistant";
92 protected MqttChannelTypeProvider typeProvider;
95 protected MQTTTopicDiscoveryService mqttTopicDiscovery;
97 public HomeAssistantDiscovery() {
98 super(null, 3, true, BASE_TOPIC + "/#");
99 this.gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
103 public void setMQTTTopicDiscoveryService(MQTTTopicDiscoveryService service) {
104 mqttTopicDiscovery = service;
107 public void unsetMQTTTopicDiscoveryService(@Nullable MQTTTopicDiscoveryService service) {
108 mqttTopicDiscovery.unsubscribe(this);
109 this.mqttTopicDiscovery = null;
113 protected MQTTTopicDiscoveryService getDiscoveryService() {
114 return mqttTopicDiscovery;
118 protected void setTypeProvider(MqttChannelTypeProvider provider) {
119 this.typeProvider = provider;
122 protected void unsetTypeProvider(MqttChannelTypeProvider provider) {
123 this.typeProvider = null;
127 public Set<ThingTypeUID> getSupportedThingTypes() {
128 return typeProvider.getThingTypeUIDs();
132 public void receivedMessage(ThingUID connectionBridge, MqttBrokerConnection connection, String topic,
136 // For HomeAssistant we need to subscribe to a wildcard topic, because topics can either be:
137 // homeassistant/<component>/<node_id>/<object_id>/config OR
138 // homeassistant/<component>/<object_id>/config.
139 // We check for the last part to filter all non-config topics out.
140 if (!topic.endsWith("/config")) {
144 // Reset the found-component timer.
145 // We will collect components for the thing label description for another 2 seconds.
146 final ScheduledFuture<?> future = this.future;
147 if (future != null) {
148 future.cancel(false);
150 this.future = scheduler.schedule(this::publishResults, 2, TimeUnit.SECONDS);
152 // We will of course find multiple of the same unique Thing IDs, for each different component another one.
153 // Therefore the components are assembled into a list and given to the DiscoveryResult label for the user to
154 // easily recognize object capabilities.
155 HaID haID = new HaID(topic);
158 AbstractChannelConfiguration config = AbstractChannelConfiguration
159 .fromString(new String(payload, StandardCharsets.UTF_8), gson);
161 final String thingID = config.getThingId(haID.objectID);
163 final ThingTypeUID typeID = new ThingTypeUID(MqttBindingConstants.BINDING_ID,
164 MqttBindingConstants.HOMEASSISTANT_MQTT_THING.getId() + "_" + thingID);
166 final ThingUID thingUID = new ThingUID(typeID, connectionBridge, thingID);
168 thingIDPerTopic.put(topic, thingUID);
170 // We need to keep track of already found component topics for a specific thing
171 final List<HaID> components;
173 Set<HaID> componentsUnordered = componentsPerThingID.computeIfAbsent(thingID,
174 key -> ConcurrentHashMap.newKeySet());
176 // Invariant. For compiler, computeIfAbsent above returns always
178 Objects.requireNonNull(componentsUnordered);
179 componentsUnordered.add(haID);
181 components = componentsUnordered.stream().collect(Collectors.toList());
182 // We sort the components for consistent jsondb serialization order of 'topics' thing property
183 // Sorting key is HaID::toString, i.e. using the full topic string
184 components.sort(Comparator.comparing(HaID::toString));
187 final String componentNames = components.stream().map(id -> id.component)
188 .map(c -> HA_COMP_TO_NAME.getOrDefault(c, c)).collect(Collectors.joining(", "));
190 final List<String> topics = components.stream().map(HaID::toShortTopic).collect(Collectors.toList());
192 Map<String, Object> properties = new HashMap<>();
193 HandlerConfiguration handlerConfig = new HandlerConfiguration(haID.baseTopic, topics);
194 properties = handlerConfig.appendToProperties(properties);
195 properties = config.appendToProperties(properties);
196 properties.put("deviceId", thingID);
198 // Because we need the new properties map with the updated "components" list
199 results.put(thingUID.getAsString(),
200 DiscoveryResultBuilder.create(thingUID).withProperties(properties)
201 .withRepresentationProperty("deviceId").withBridge(connectionBridge)
202 .withLabel(config.getThingName() + " (" + componentNames + ")").build());
203 } catch (ConfigurationException e) {
204 logger.warn("HomeAssistant discover error: invalid configuration of thing {} component {}: {}",
205 haID.objectID, haID.component, e.getMessage());
206 } catch (Exception e) {
207 logger.warn("HomeAssistant discover error: {}", e.getMessage());
211 protected void publishResults() {
212 Collection<DiscoveryResult> localResults;
214 localResults = new ArrayList<>(results.values());
216 componentsPerThingID.clear();
217 for (DiscoveryResult result : localResults) {
218 final ThingTypeUID typeID = result.getThingTypeUID();
219 ThingType type = typeProvider.derive(typeID, MqttBindingConstants.HOMEASSISTANT_MQTT_THING).build();
220 typeProvider.setThingTypeIfAbsent(typeID, type);
222 thingDiscovered(result);
227 public void topicVanished(ThingUID connectionBridge, MqttBrokerConnection connection, String topic) {
228 if (!topic.endsWith("/config")) {
231 if (thingIDPerTopic.containsKey(topic)) {
232 ThingUID thingUID = thingIDPerTopic.remove(topic);
233 final String thingID = thingUID.getId();
235 HaID haID = new HaID(topic);
237 Set<HaID> components = componentsPerThingID.getOrDefault(thingID, Collections.emptySet());
238 components.remove(haID);
239 if (components.isEmpty()) {
240 thingRemoved(thingUID);