]> git.basschouten.com Git - openhab-addons.git/blob
cd61ed8a85157cc55a36a165a36604dac2500ce1
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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;
14
15 import java.util.ArrayList;
16 import java.util.Collection;
17
18 import org.eclipse.jdt.annotation.NonNullByDefault;
19 import org.eclipse.jdt.annotation.Nullable;
20 import org.openhab.core.config.core.Configuration;
21 import org.openhab.core.util.UIDUtils;
22
23 /**
24  * HomeAssistant MQTT components use a specific MQTT topic layout,
25  * starting with a base prefix (usually "homeassistant"),
26  * followed by the component id, an optional node id and the object id.
27  *
28  * This helper class can split up an MQTT topic into such parts.
29  * <p>
30  * Implementation note: This is an immutable class.
31  *
32  * @author David Graeff - Initial contribution
33  */
34 @NonNullByDefault
35 public class HaID {
36     public final String baseTopic;
37     public final String component;
38     public final String nodeID;
39     public final String objectID;
40
41     private final String topic;
42
43     /**
44      * Creates a {@link HaID} object for a given HomeAssistant MQTT topic.
45      *
46      * @param mqttTopic A topic like "homeassistant/binary_sensor/garden/config" or
47      *            "homeassistant/binary_sensor/0/garden/config"
48      */
49     public HaID(String mqttTopic) {
50         String[] strings = mqttTopic.split("/");
51         if (strings.length < 4 || strings.length > 5) {
52             throw new IllegalArgumentException("MQTT topic not a HomeAssistant topic (wrong length)!");
53         }
54         if (!"config".equals(strings[strings.length - 1])) {
55             throw new IllegalArgumentException("MQTT topic not a HomeAssistant topic ('config' missing)!");
56         }
57
58         baseTopic = strings[0];
59         component = strings[1];
60
61         if (strings.length == 5) {
62             nodeID = strings[2];
63             objectID = strings[3];
64         } else {
65             nodeID = "";
66             objectID = strings[2];
67         }
68
69         this.topic = createTopic(this);
70     }
71
72     public HaID() {
73         this("", "", "", "");
74     }
75
76     /**
77      * Creates a {@link HaID} by providing all components separately.
78      *
79      * @param baseTopic The base topic. Usually "homeassistant".
80      * @param objectID The object ID
81      * @param nodeID The node ID (can be the empty string)
82      * @param component The component ID
83      */
84     private HaID(String baseTopic, String objectID, String nodeID, String component) {
85         this.baseTopic = baseTopic;
86         this.objectID = objectID;
87         this.nodeID = nodeID;
88         this.component = component;
89         this.topic = createTopic(this);
90     }
91
92     private static String createTopic(HaID id) {
93         StringBuilder str = new StringBuilder();
94         str.append(id.baseTopic).append('/').append(id.component).append('/');
95         if (!id.nodeID.isBlank()) {
96             str.append(id.nodeID).append('/');
97         }
98         str.append(id.objectID).append('/');
99         return str.toString();
100     }
101
102     /**
103      * Extract the HaID information from a channel configuration.
104      * <p>
105      * <code>objectid</code>, <code>nodeid</code>, and <code>component</code> values are fetched from the configuration.
106      *
107      * @param baseTopic base topic
108      * @param config config
109      * @return newly created HaID
110      */
111     public static HaID fromConfig(String baseTopic, Configuration config) {
112         String component = (String) config.get("component");
113         String nodeID = (String) config.getProperties().getOrDefault("nodeid", "");
114         String objectID = (String) config.get("objectid");
115         return new HaID(baseTopic, objectID, nodeID, component);
116     }
117
118     /**
119      * Add the HaID information to a channel configuration.
120      * <p>
121      * <code>objectid</code>, <code>nodeid</code>, and <code>component</code> values are added to the configuration.
122      *
123      * @param config config
124      * @return the modified configuration
125      */
126     public Configuration toConfig(Configuration config) {
127         config.put("objectid", objectID);
128         config.put("nodeid", nodeID);
129         config.put("component", component);
130         return config;
131     }
132
133     /**
134      * Extract the HaID information from a thing configuration.
135      * <p>
136      * <code>basetpoic</code> and <code>objectid</code> are taken from the configuration.
137      * The <code>objectid</code> string may be in the form <code>nodeid/objectid</code>.
138      * <p>
139      * The <code>component</code> component in the resulting HaID will be set to <code>+</code>.
140      * This enables the HaID to be used as an mqtt subscription topic.
141      *
142      * @param config config
143      * @return newly created HaID
144      */
145     public static Collection<HaID> fromConfig(HandlerConfiguration config) {
146         Collection<HaID> result = new ArrayList<>();
147
148         for (String topic : config.topics) {
149             String[] parts = topic.split("/");
150
151             switch (parts.length) {
152                 case 2:
153                     result.add(new HaID(config.basetopic, parts[1], "", parts[0]));
154                     break;
155                 case 3:
156                     result.add(new HaID(config.basetopic, parts[2], parts[1], parts[0]));
157                     break;
158                 default:
159                     throw new IllegalArgumentException(
160                             "Bad configuration. topic must be <component>/<objectId> or <component>/<nodeId>/<objectId>!");
161             }
162         }
163         return result;
164     }
165
166     /**
167      * Return the topic to put into the HandlerConfiguration for this component.
168      * <p>
169      * <code>objectid</code> in the thing configuration will be
170      * <code>nodeID/objectID</code> from the HaID, if <code>nodeID</code> is not empty.
171      * <p>
172      *
173      * @return the short topic.
174      */
175     public String toShortTopic() {
176         String objectID = this.objectID;
177         if (!nodeID.isBlank()) {
178             objectID = nodeID + "/" + objectID;
179         }
180
181         return component + "/" + objectID;
182     }
183
184     /**
185      * The default group id is the unique_id of the component, given in the config-json.
186      * If the unique id is not set, then a fallback is constructed from the HaID information.
187      *
188      * @return group id
189      */
190     public String getGroupId(@Nullable final String uniqueId, boolean newStyleChannels) {
191         String result = uniqueId;
192
193         // newStyleChannels are auto-discovered things with openHAB >= 4.3.0
194         // assuming the topic has both a node ID and an object ID, simply use
195         // the component type and object ID - without encoding(!)
196         // since the only character allowed in object IDs but not allowed in UID
197         // is `-`. It also doesn't need to be reversible, so it's okay to just
198         // collapse `-` to `_`.
199         if (!nodeID.isBlank() && newStyleChannels) {
200             return component + "_" + objectID.replace('-', '_');
201         }
202
203         // the null test is only here so the compile knows, result is not null afterwards
204         if (result == null || result.isBlank()) {
205             StringBuilder str = new StringBuilder();
206
207             if (!nodeID.isBlank()) {
208                 str.append(nodeID).append('_');
209             }
210             str.append(objectID).append('_').append(component);
211             result = str.toString();
212         }
213
214         return UIDUtils.encode(result);
215     }
216
217     /**
218      * Return the topic for this component, without /config
219      */
220     public String getTopic() {
221         return topic;
222     }
223
224     /**
225      * Return a topic, which can be used for a mqtt subscription.
226      * Defined values for suffix are:
227      * <ul>
228      * <li>config</li>
229      * <li>state</li>
230      * </ul>
231      *
232      * @return fallback group id
233      */
234     public String getTopic(String suffix) {
235         return topic + suffix;
236     }
237
238     @Override
239     public int hashCode() {
240         final int prime = 31;
241         int result = 1;
242         result = prime * result + baseTopic.hashCode();
243         result = prime * result + component.hashCode();
244         result = prime * result + nodeID.hashCode();
245         result = prime * result + objectID.hashCode();
246         return result;
247     }
248
249     @Override
250     public boolean equals(@Nullable Object obj) {
251         if (this == obj) {
252             return true;
253         }
254         if (obj == null) {
255             return false;
256         }
257         if (getClass() != obj.getClass()) {
258             return false;
259         }
260         HaID other = (HaID) obj;
261         if (!baseTopic.equals(other.baseTopic)) {
262             return false;
263         }
264         if (!component.equals(other.component)) {
265             return false;
266         }
267         if (!nodeID.equals(other.nodeID)) {
268             return false;
269         }
270         return objectID.equals(other.objectID);
271     }
272
273     @Override
274     public String toString() {
275         return topic;
276     }
277 }