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.component;
15 import java.util.ArrayList;
16 import java.util.List;
18 import java.util.Objects;
19 import java.util.TreeMap;
20 import java.util.concurrent.CompletableFuture;
21 import java.util.concurrent.ScheduledExecutorService;
22 import java.util.stream.Stream;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.mqtt.generic.AvailabilityTracker;
27 import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
28 import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
29 import org.openhab.binding.mqtt.generic.values.Value;
30 import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
31 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
32 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
33 import org.openhab.binding.mqtt.homeassistant.internal.HaID;
34 import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory.ComponentConfiguration;
35 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
36 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Availability;
37 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AvailabilityMode;
38 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Device;
39 import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
40 import org.openhab.core.thing.Channel;
41 import org.openhab.core.thing.ChannelGroupUID;
42 import org.openhab.core.thing.ChannelUID;
43 import org.openhab.core.thing.type.ChannelDefinition;
44 import org.openhab.core.thing.type.ChannelGroupDefinition;
45 import org.openhab.core.thing.type.ChannelGroupType;
46 import org.openhab.core.thing.type.ChannelGroupTypeBuilder;
47 import org.openhab.core.thing.type.ChannelGroupTypeUID;
48 import org.openhab.core.types.CommandDescription;
49 import org.openhab.core.types.StateDescription;
51 import com.google.gson.Gson;
52 import com.hubspot.jinjava.Jinjava;
55 * A HomeAssistant component is comparable to a channel group.
56 * It has a name and consists of multiple channels.
58 * @author David Graeff - Initial contribution
59 * @param <C> Config class derived from {@link AbstractChannelConfiguration}
62 public abstract class AbstractComponent<C extends AbstractChannelConfiguration> {
63 private static final String JINJA_PREFIX = "JINJA:";
65 // Component location fields
66 protected final ComponentConfiguration componentConfiguration;
67 protected final @Nullable ChannelGroupUID channelGroupUID;
68 protected final HaID haID;
70 // Channels and configuration
71 protected final Map<String, ComponentChannel> channels = new TreeMap<>();
72 protected final List<ComponentChannel> hiddenChannels = new ArrayList<>();
74 // The hash code ({@link String#hashCode()}) of the configuration string
75 // Used to determine if a component has changed.
76 protected final int configHash;
77 protected final String channelConfigurationJson;
78 protected final C channelConfiguration;
80 protected boolean configSeen;
81 protected final boolean singleChannelComponent;
82 protected final String groupId;
83 protected final String uniqueId;
85 public AbstractComponent(ComponentFactory.ComponentConfiguration componentConfiguration, Class<C> clazz,
86 boolean newStyleChannels) {
87 this(componentConfiguration, clazz, newStyleChannels, false);
91 * Creates component based on generic configuration and component configuration type.
93 * @param componentConfiguration generic componentConfiguration with not parsed JSON config
94 * @param clazz target configuration type
95 * @param newStyleChannels if new style channels should be used
96 * @param singleChannelComponent if this component only ever has one channel, so should never be in a group
97 * (only if newStyleChannels is true)
99 public AbstractComponent(ComponentFactory.ComponentConfiguration componentConfiguration, Class<C> clazz,
100 boolean newStyleChannels, boolean singleChannelComponent) {
101 this.componentConfiguration = componentConfiguration;
102 this.singleChannelComponent = newStyleChannels && singleChannelComponent;
104 this.channelConfigurationJson = componentConfiguration.getConfigJSON();
105 this.channelConfiguration = componentConfiguration.getConfig(clazz);
106 this.configHash = channelConfigurationJson.hashCode();
108 this.haID = componentConfiguration.getHaID();
110 String name = channelConfiguration.getName();
111 if (name != null && !name.isEmpty()) {
112 groupId = this.haID.getGroupId(channelConfiguration.getUniqueId(), newStyleChannels);
114 this.channelGroupUID = this.singleChannelComponent ? null
115 : new ChannelGroupUID(componentConfiguration.getThingUID(), groupId);
117 this.groupId = this.singleChannelComponent ? haID.component : "";
118 this.channelGroupUID = null;
120 uniqueId = this.haID.getGroupId(channelConfiguration.getUniqueId(), false);
122 this.configSeen = false;
124 final List<Availability> availabilities = channelConfiguration.getAvailability();
125 if (availabilities != null) {
126 AvailabilityMode mode = channelConfiguration.getAvailabilityMode();
127 AvailabilityTracker.AvailabilityMode availabilityTrackerMode = switch (mode) {
128 case ALL -> AvailabilityTracker.AvailabilityMode.ALL;
129 case ANY -> AvailabilityTracker.AvailabilityMode.ANY;
130 case LATEST -> AvailabilityTracker.AvailabilityMode.LATEST;
132 componentConfiguration.getTracker().setAvailabilityMode(availabilityTrackerMode);
133 for (Availability availability : availabilities) {
134 String availabilityTemplate = availability.getValueTemplate();
135 List<String> availabilityTemplates = List.of();
136 if (availabilityTemplate != null) {
137 availabilityTemplate = JINJA_PREFIX + availabilityTemplate;
138 availabilityTemplates = List.of(availabilityTemplate);
140 componentConfiguration.getTracker().addAvailabilityTopic(availability.getTopic(),
141 availability.getPayloadAvailable(), availability.getPayloadNotAvailable(),
142 availabilityTemplates);
145 String availabilityTopic = this.channelConfiguration.getAvailabilityTopic();
146 if (availabilityTopic != null) {
147 String availabilityTemplate = this.channelConfiguration.getAvailabilityTemplate();
148 List<String> availabilityTemplates = List.of();
149 if (availabilityTemplate != null) {
150 availabilityTemplate = JINJA_PREFIX + availabilityTemplate;
151 availabilityTemplates = List.of(availabilityTemplate);
153 componentConfiguration.getTracker().addAvailabilityTopic(availabilityTopic,
154 this.channelConfiguration.getPayloadAvailable(),
155 this.channelConfiguration.getPayloadNotAvailable(), availabilityTemplates);
160 protected ComponentChannel.Builder buildChannel(String channelID, ComponentChannelType channelType,
161 Value valueState, String label, ChannelStateUpdateListener channelStateUpdateListener) {
162 if (singleChannelComponent) {
165 return new ComponentChannel.Builder(this, channelID, channelType.getChannelTypeUID(), valueState, label,
166 channelStateUpdateListener);
169 public void setConfigSeen() {
170 this.configSeen = true;
174 * Subscribes to all state channels of the component and adds all channels to the provided channel type provider.
176 * @param connection connection to the MQTT broker
177 * @param scheduler thing scheduler
178 * @param timeout channel subscription timeout
179 * @return A future that completes as soon as all subscriptions have been performed. Completes exceptionally on
182 public CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection, ScheduledExecutorService scheduler,
184 return Stream.concat(channels.values().stream(), hiddenChannels.stream())
185 .map(v -> v.start(connection, scheduler, timeout)) //
186 .reduce(CompletableFuture.completedFuture(null), (f, v) -> f.thenCompose(b -> v));
190 * Unsubscribes from all state channels of the component.
192 * @return A future that completes as soon as all subscriptions removals have been performed. Completes
193 * exceptionally on errors.
195 public CompletableFuture<@Nullable Void> stop() {
196 return Stream.concat(channels.values().stream(), hiddenChannels.stream()) //
197 .filter(Objects::nonNull) //
198 .map(ComponentChannel::stop) //
199 .reduce(CompletableFuture.completedFuture(null), (f, v) -> f.thenCompose(b -> v));
203 * Add all state and command descriptions to the state description provider.
205 * @param stateDescriptionProvider The state description provider
207 public void addStateDescriptions(MqttChannelStateDescriptionProvider stateDescriptionProvider) {
208 channels.values().forEach(channel -> {
209 StateDescription stateDescription = channel.getStateDescription();
210 if (stateDescription != null) {
211 stateDescriptionProvider.setDescription(channel.getChannel().getUID(), stateDescription);
213 CommandDescription commandDescription = channel.getCommandDescription();
214 if (commandDescription != null) {
215 stateDescriptionProvider.setDescription(channel.getChannel().getUID(), commandDescription);
220 public ChannelUID buildChannelUID(String channelID) {
221 final ChannelGroupUID groupUID = channelGroupUID;
222 if (groupUID != null) {
223 return new ChannelUID(groupUID, channelID);
225 return new ChannelUID(componentConfiguration.getThingUID(), channelID);
228 public String getGroupId() {
233 * Component (Channel Group) name.
235 public String getName() {
236 String result = channelConfiguration.getName();
238 Device device = channelConfiguration.getDevice();
239 if (result == null && device != null) {
240 result = device.getName();
242 if (result == null) {
243 result = haID.objectID;
249 * Each component consists of multiple Channels.
251 public Map<String, ComponentChannel> getChannelMap() {
256 * Return a components channel. A HomeAssistant MQTT component consists of multiple functions
257 * and those are mapped to one or more channels. The channel IDs are constants within the
258 * derived Component, like the {@link Switch#SWITCH_CHANNEL_ID}.
260 * @param channelID The channel ID
261 * @return A components channel
263 public @Nullable ComponentChannel getChannel(String channelID) {
264 return channels.get(channelID);
268 * @return Returns the configuration hash value for easy comparison.
270 public int getConfigHash() {
275 * Return the channel group type.
277 public @Nullable ChannelGroupType getChannelGroupType(String prefix) {
278 if (channelGroupUID == null) {
281 return ChannelGroupTypeBuilder.instance(getChannelGroupTypeUID(prefix), getName())
282 .withChannelDefinitions(getAllChannelDefinitions()).build();
285 public List<ChannelDefinition> getChannelDefinitions() {
286 if (channelGroupUID != null) {
289 return getAllChannelDefinitions();
292 private List<ChannelDefinition> getAllChannelDefinitions() {
293 return channels.values().stream().map(ComponentChannel::channelDefinition).toList();
296 public List<Channel> getChannels() {
297 return channels.values().stream().map(ComponentChannel::getChannel).toList();
301 * Resets all channel states to state UNDEF. Call this method after the connection
302 * to the MQTT broker got lost.
304 public void resetState() {
305 channels.values().forEach(ComponentChannel::resetState);
309 * Return the channel group definition for this component.
311 public @Nullable ChannelGroupDefinition getGroupDefinition(String prefix) {
312 if (channelGroupUID == null) {
315 return new ChannelGroupDefinition(channelGroupUID.getId(), getChannelGroupTypeUID(prefix), getName(), null);
318 public boolean hasGroup() {
319 return channelGroupUID != null;
322 public HaID getHaID() {
326 public String getChannelConfigurationJson() {
327 return channelConfigurationJson;
330 public boolean isEnabledByDefault() {
331 return channelConfiguration.isEnabledByDefault();
334 public Gson getGson() {
335 return componentConfiguration.getGson();
338 public Jinjava getJinjava() {
339 return componentConfiguration.getJinjava();
342 public C getChannelConfiguration() {
343 return channelConfiguration;
346 private ChannelGroupTypeUID getChannelGroupTypeUID(String prefix) {
347 return new ChannelGroupTypeUID(MqttBindingConstants.BINDING_ID, prefix + "_" + uniqueId);