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