]> git.basschouten.com Git - openhab-addons.git/blob
40dff2642854c26d4394343a90d6aba93e95d703
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.mqtt.homeassistant.internal.discovery;
14
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.HashMap;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Set;
23 import java.util.TreeMap;
24 import java.util.concurrent.ConcurrentHashMap;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27 import java.util.stream.Collectors;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.mqtt.discovery.AbstractMQTTDiscovery;
32 import org.openhab.binding.mqtt.discovery.MQTTTopicDiscoveryService;
33 import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
34 import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
35 import org.openhab.binding.mqtt.homeassistant.internal.HaID;
36 import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
37 import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory;
38 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
39 import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
40 import org.openhab.core.config.discovery.DiscoveryResult;
41 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
42 import org.openhab.core.config.discovery.DiscoveryService;
43 import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
44 import org.openhab.core.thing.ThingTypeUID;
45 import org.openhab.core.thing.ThingUID;
46 import org.openhab.core.thing.type.ThingType;
47 import org.osgi.service.component.annotations.Component;
48 import org.osgi.service.component.annotations.Reference;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51
52 import com.google.gson.Gson;
53 import com.google.gson.GsonBuilder;
54
55 /**
56  * The {@link HomeAssistantDiscovery} is responsible for discovering device nodes that follow the
57  * Home Assistant MQTT discovery convention (https://www.home-assistant.io/docs/mqtt/discovery/).
58  *
59  * @author David Graeff - Initial contribution
60  */
61 @Component(service = DiscoveryService.class, configurationPid = "discovery.mqttha")
62 @NonNullByDefault
63 public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
64     @SuppressWarnings("unused")
65     private final Logger logger = LoggerFactory.getLogger(HomeAssistantDiscovery.class);
66     protected final Map<String, Set<HaID>> componentsPerThingID = new TreeMap<>();
67     protected final Map<String, ThingUID> thingIDPerTopic = new TreeMap<>();
68     protected final Map<String, DiscoveryResult> results = new ConcurrentHashMap<>();
69
70     private @Nullable ScheduledFuture<?> future;
71     private final Gson gson;
72
73     public static final Map<String, String> HA_COMP_TO_NAME = new TreeMap<>();
74     {
75         HA_COMP_TO_NAME.put("alarm_control_panel", "Alarm Control Panel");
76         HA_COMP_TO_NAME.put("binary_sensor", "Sensor");
77         HA_COMP_TO_NAME.put("camera", "Camera");
78         HA_COMP_TO_NAME.put("cover", "Blind");
79         HA_COMP_TO_NAME.put("fan", "Fan");
80         HA_COMP_TO_NAME.put("climate", "Climate Control");
81         HA_COMP_TO_NAME.put("light", "Light");
82         HA_COMP_TO_NAME.put("lock", "Lock");
83         HA_COMP_TO_NAME.put("sensor", "Sensor");
84         HA_COMP_TO_NAME.put("switch", "Switch");
85     }
86
87     static final String BASE_TOPIC = "homeassistant";
88
89     @NonNullByDefault({})
90     protected MqttChannelTypeProvider typeProvider;
91
92     @NonNullByDefault({})
93     protected MQTTTopicDiscoveryService mqttTopicDiscovery;
94
95     public HomeAssistantDiscovery() {
96         super(null, 3, true, BASE_TOPIC + "/#");
97         this.gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
98     }
99
100     @Reference
101     public void setMQTTTopicDiscoveryService(MQTTTopicDiscoveryService service) {
102         mqttTopicDiscovery = service;
103     }
104
105     public void unsetMQTTTopicDiscoveryService(@Nullable MQTTTopicDiscoveryService service) {
106         mqttTopicDiscovery.unsubscribe(this);
107         this.mqttTopicDiscovery = null;
108     }
109
110     @Override
111     protected MQTTTopicDiscoveryService getDiscoveryService() {
112         return mqttTopicDiscovery;
113     }
114
115     @Reference
116     protected void setTypeProvider(MqttChannelTypeProvider provider) {
117         this.typeProvider = provider;
118     }
119
120     protected void unsetTypeProvider(MqttChannelTypeProvider provider) {
121         this.typeProvider = null;
122     }
123
124     @Override
125     public Set<ThingTypeUID> getSupportedThingTypes() {
126         return typeProvider.getThingTypeUIDs();
127     }
128
129     @Override
130     public void receivedMessage(ThingUID connectionBridge, MqttBrokerConnection connection, String topic,
131             byte[] payload) {
132         resetTimeout();
133
134         // For HomeAssistant we need to subscribe to a wildcard topic, because topics can either be:
135         // homeassistant/<component>/<node_id>/<object_id>/config OR
136         // homeassistant/<component>/<object_id>/config.
137         // We check for the last part to filter all non-config topics out.
138         if (!topic.endsWith("/config")) {
139             return;
140         }
141
142         // Reset the found-component timer.
143         // We will collect components for the thing label description for another 2 seconds.
144         final ScheduledFuture<?> future = this.future;
145         if (future != null) {
146             future.cancel(false);
147         }
148         this.future = scheduler.schedule(this::publishResults, 2, TimeUnit.SECONDS);
149
150         // We will of course find multiple of the same unique Thing IDs, for each different component another one.
151         // Therefore the components are assembled into a list and given to the DiscoveryResult label for the user to
152         // easily recognize object capabilities.
153         HaID haID = new HaID(topic);
154
155         try {
156             AbstractChannelConfiguration config = AbstractChannelConfiguration
157                     .fromString(new String(payload, StandardCharsets.UTF_8), gson);
158
159             final String thingID = config.getThingId(haID.objectID);
160
161             final ThingTypeUID typeID = new ThingTypeUID(MqttBindingConstants.BINDING_ID,
162                     MqttBindingConstants.HOMEASSISTANT_MQTT_THING.getId() + "_" + thingID);
163
164             final ThingUID thingUID = new ThingUID(typeID, connectionBridge, thingID);
165
166             thingIDPerTopic.put(topic, thingUID);
167
168             // We need to keep track of already found component topics for a specific thing
169             Set<HaID> components = componentsPerThingID.computeIfAbsent(thingID, key -> ConcurrentHashMap.newKeySet());
170             components.add(haID);
171
172             final String componentNames = components.stream().map(id -> id.component)
173                     .map(c -> HA_COMP_TO_NAME.getOrDefault(c, c)).collect(Collectors.joining(", "));
174
175             final List<String> topics = components.stream().map(HaID::toShortTopic).collect(Collectors.toList());
176
177             Map<String, Object> properties = new HashMap<>();
178             HandlerConfiguration handlerConfig = new HandlerConfiguration(haID.baseTopic, topics);
179             properties = handlerConfig.appendToProperties(properties);
180             properties = config.appendToProperties(properties);
181             properties.put("deviceId", thingID);
182
183             // Because we need the new properties map with the updated "components" list
184             results.put(thingUID.getAsString(),
185                     DiscoveryResultBuilder.create(thingUID).withProperties(properties)
186                             .withRepresentationProperty("deviceId").withBridge(connectionBridge)
187                             .withLabel(config.getThingName() + " (" + componentNames + ")").build());
188         } catch (ConfigurationException e) {
189             logger.warn("HomeAssistant discover error: invalid configuration of thing {} component {}: {}",
190                     haID.objectID, haID.component, e.getMessage());
191         } catch (Exception e) {
192             logger.warn("HomeAssistant discover error: {}", e.getMessage());
193         }
194     }
195
196     protected void publishResults() {
197         Collection<DiscoveryResult> localResults;
198
199         localResults = new ArrayList<>(results.values());
200         results.clear();
201         componentsPerThingID.clear();
202         for (DiscoveryResult result : localResults) {
203             final ThingTypeUID typeID = result.getThingTypeUID();
204             ThingType type = typeProvider.derive(typeID, MqttBindingConstants.HOMEASSISTANT_MQTT_THING).build();
205             typeProvider.setThingTypeIfAbsent(typeID, type);
206
207             thingDiscovered(result);
208         }
209     }
210
211     @Override
212     public void topicVanished(ThingUID connectionBridge, MqttBrokerConnection connection, String topic) {
213         if (!topic.endsWith("/config")) {
214             return;
215         }
216         if (thingIDPerTopic.containsKey(topic)) {
217             ThingUID thingUID = thingIDPerTopic.remove(topic);
218             final String thingID = thingUID.getId();
219
220             HaID haID = new HaID(topic);
221
222             Set<HaID> components = componentsPerThingID.getOrDefault(thingID, Collections.emptySet());
223             components.remove(haID);
224             if (components.isEmpty()) {
225                 thingRemoved(thingUID);
226             }
227         }
228     }
229 }