2 * Copyright (c) 2010-2024 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;
15 import java.util.ArrayList;
16 import java.util.Collection;
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;
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.
28 * This helper class can split up an MQTT topic into such parts.
30 * Implementation note: This is an immutable class.
32 * @author David Graeff - Initial contribution
36 public final String baseTopic;
37 public final String component;
38 public final String nodeID;
39 public final String objectID;
41 private final String topic;
44 * Creates a {@link HaID} object for a given HomeAssistant MQTT topic.
46 * @param mqttTopic A topic like "homeassistant/binary_sensor/garden/config" or
47 * "homeassistant/binary_sensor/0/garden/config"
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)!");
54 if (!"config".equals(strings[strings.length - 1])) {
55 throw new IllegalArgumentException("MQTT topic not a HomeAssistant topic ('config' missing)!");
58 baseTopic = strings[0];
59 component = strings[1];
61 if (strings.length == 5) {
63 objectID = strings[3];
66 objectID = strings[2];
69 this.topic = createTopic(this);
77 * Creates a {@link HaID} by providing all components separately.
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
84 private HaID(String baseTopic, String objectID, String nodeID, String component) {
85 this.baseTopic = baseTopic;
86 this.objectID = objectID;
88 this.component = component;
89 this.topic = createTopic(this);
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('/');
98 str.append(id.objectID).append('/');
99 return str.toString();
103 * Extract the HaID information from a channel configuration.
105 * <code>objectid</code>, <code>nodeid</code>, and <code>component</code> values are fetched from the configuration.
107 * @param baseTopic base topic
108 * @param config config
109 * @return newly created HaID
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);
119 * Add the HaID information to a channel configuration.
121 * <code>objectid</code>, <code>nodeid</code>, and <code>component</code> values are added to the configuration.
123 * @param config config
124 * @return the modified configuration
126 public Configuration toConfig(Configuration config) {
127 config.put("objectid", objectID);
128 config.put("nodeid", nodeID);
129 config.put("component", component);
134 * Extract the HaID information from a thing configuration.
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>.
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.
142 * @param config config
143 * @return newly created HaID
145 public static Collection<HaID> fromConfig(HandlerConfiguration config) {
146 Collection<HaID> result = new ArrayList<>();
148 for (String topic : config.topics) {
149 String[] parts = topic.split("/");
151 switch (parts.length) {
153 result.add(new HaID(config.basetopic, parts[1], "", parts[0]));
156 result.add(new HaID(config.basetopic, parts[2], parts[1], parts[0]));
159 throw new IllegalArgumentException(
160 "Bad configuration. topic must be <component>/<objectId> or <component>/<nodeId>/<objectId>!");
167 * Return the topic to put into the HandlerConfiguration for this component.
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.
173 * @return the short topic.
175 public String toShortTopic() {
176 String objectID = this.objectID;
177 if (!nodeID.isBlank()) {
178 objectID = nodeID + "/" + objectID;
181 return component + "/" + objectID;
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.
190 public String getGroupId(@Nullable final String uniqueId, boolean newStyleChannels) {
191 String result = uniqueId;
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('-', '_');
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();
207 if (!nodeID.isBlank()) {
208 str.append(nodeID).append('_');
210 str.append(objectID).append('_').append(component);
211 result = str.toString();
214 return UIDUtils.encode(result);
218 * Return the topic for this component, without /config
220 public String getTopic() {
225 * Return a topic, which can be used for a mqtt subscription.
226 * Defined values for suffix are:
232 * @return fallback group id
234 public String getTopic(String suffix) {
235 return topic + suffix;
239 public int hashCode() {
240 final int prime = 31;
242 result = prime * result + baseTopic.hashCode();
243 result = prime * result + component.hashCode();
244 result = prime * result + nodeID.hashCode();
245 result = prime * result + objectID.hashCode();
250 public boolean equals(@Nullable Object obj) {
257 if (getClass() != obj.getClass()) {
260 HaID other = (HaID) obj;
261 if (!baseTopic.equals(other.baseTopic)) {
264 if (!component.equals(other.component)) {
267 if (!nodeID.equals(other.nodeID)) {
270 return objectID.equals(other.objectID);
274 public String toString() {