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.Collectors;
23 import java.util.stream.Stream;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.mqtt.generic.AvailabilityTracker;
28 import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
29 import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
30 import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
31 import org.openhab.binding.mqtt.generic.values.Value;
32 import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
33 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
34 import org.openhab.binding.mqtt.homeassistant.internal.HaID;
35 import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory.ComponentConfiguration;
36 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
37 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Availability;
38 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AvailabilityMode;
39 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Device;
40 import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
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;
49 import com.google.gson.Gson;
52 * A HomeAssistant component is comparable to a channel group.
53 * It has a name and consists of multiple channels.
55 * @author David Graeff - Initial contribution
56 * @param <C> Config class derived from {@link AbstractChannelConfiguration}
59 public abstract class AbstractComponent<C extends AbstractChannelConfiguration> {
60 private static final String JINJA_PREFIX = "JINJA:";
62 // Component location fields
63 private final ComponentConfiguration componentConfiguration;
64 protected final @Nullable ChannelGroupTypeUID channelGroupTypeUID;
65 protected final @Nullable ChannelGroupUID channelGroupUID;
66 protected final HaID haID;
68 // Channels and configuration
69 protected final Map<String, ComponentChannel> channels = new TreeMap<>();
70 protected final List<ComponentChannel> hiddenChannels = new ArrayList<>();
72 // The hash code ({@link String#hashCode()}) of the configuration string
73 // Used to determine if a component has changed.
74 protected final int configHash;
75 protected final String channelConfigurationJson;
76 protected final C channelConfiguration;
78 protected boolean configSeen;
81 * Creates component based on generic configuration and component configuration type.
83 * @param componentConfiguration generic componentConfiguration with not parsed JSON config
84 * @param clazz target configuration type
86 public AbstractComponent(ComponentFactory.ComponentConfiguration componentConfiguration, Class<C> clazz) {
87 this.componentConfiguration = componentConfiguration;
89 this.channelConfigurationJson = componentConfiguration.getConfigJSON();
90 this.channelConfiguration = componentConfiguration.getConfig(clazz);
91 this.configHash = channelConfigurationJson.hashCode();
93 this.haID = componentConfiguration.getHaID();
95 String name = channelConfiguration.getName();
96 if (name != null && !name.isEmpty()) {
97 String groupId = this.haID.getGroupId(channelConfiguration.getUniqueId());
99 this.channelGroupTypeUID = new ChannelGroupTypeUID(MqttBindingConstants.BINDING_ID, groupId);
100 this.channelGroupUID = new ChannelGroupUID(componentConfiguration.getThingUID(), groupId);
102 this.channelGroupTypeUID = null;
103 this.channelGroupUID = null;
106 this.configSeen = false;
108 final List<Availability> availabilities = channelConfiguration.getAvailability();
109 if (availabilities != null) {
110 AvailabilityMode mode = channelConfiguration.getAvailabilityMode();
111 AvailabilityTracker.AvailabilityMode availabilityTrackerMode = switch (mode) {
112 case ALL -> AvailabilityTracker.AvailabilityMode.ALL;
113 case ANY -> AvailabilityTracker.AvailabilityMode.ANY;
114 case LATEST -> AvailabilityTracker.AvailabilityMode.LATEST;
116 componentConfiguration.getTracker().setAvailabilityMode(availabilityTrackerMode);
117 for (Availability availability : availabilities) {
118 String availabilityTemplate = availability.getValueTemplate();
119 if (availabilityTemplate != null) {
120 availabilityTemplate = JINJA_PREFIX + availabilityTemplate;
122 componentConfiguration.getTracker().addAvailabilityTopic(availability.getTopic(),
123 availability.getPayloadAvailable(), availability.getPayloadNotAvailable(), availabilityTemplate,
124 componentConfiguration.getTransformationServiceProvider());
127 String availabilityTopic = this.channelConfiguration.getAvailabilityTopic();
128 if (availabilityTopic != null) {
129 String availabilityTemplate = this.channelConfiguration.getAvailabilityTemplate();
130 if (availabilityTemplate != null) {
131 availabilityTemplate = JINJA_PREFIX + availabilityTemplate;
133 componentConfiguration.getTracker().addAvailabilityTopic(availabilityTopic,
134 this.channelConfiguration.getPayloadAvailable(),
135 this.channelConfiguration.getPayloadNotAvailable(), availabilityTemplate,
136 componentConfiguration.getTransformationServiceProvider());
141 protected ComponentChannel.Builder buildChannel(String channelID, Value valueState, String label,
142 ChannelStateUpdateListener channelStateUpdateListener) {
143 return new ComponentChannel.Builder(this, channelID, valueState, label, channelStateUpdateListener);
146 public void setConfigSeen() {
147 this.configSeen = true;
151 * Subscribes to all state channels of the component and adds all channels to the provided channel type provider.
153 * @param connection connection to the MQTT broker
154 * @param scheduler thing scheduler
155 * @param timeout channel subscription timeout
156 * @return A future that completes as soon as all subscriptions have been performed. Completes exceptionally on
159 public CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection, ScheduledExecutorService scheduler,
161 return Stream.concat(channels.values().stream(), hiddenChannels.stream())
162 .map(v -> v.start(connection, scheduler, timeout)) //
163 .reduce(CompletableFuture.completedFuture(null), (f, v) -> f.thenCompose(b -> v));
167 * Unsubscribes from all state channels of the component.
169 * @return A future that completes as soon as all subscriptions removals have been performed. Completes
170 * exceptionally on errors.
172 public CompletableFuture<@Nullable Void> stop() {
173 return Stream.concat(channels.values().stream(), hiddenChannels.stream()) //
174 .filter(Objects::nonNull) //
175 .map(ComponentChannel::stop) //
176 .reduce(CompletableFuture.completedFuture(null), (f, v) -> f.thenCompose(b -> v));
180 * Add all channel types to the channel type provider.
182 * @param channelTypeProvider The channel type provider
184 public void addChannelTypes(MqttChannelTypeProvider channelTypeProvider) {
185 ChannelGroupTypeUID groupTypeUID = channelGroupTypeUID;
186 if (groupTypeUID != null) {
187 channelTypeProvider.setChannelGroupType(groupTypeUID, Objects.requireNonNull(getType()));
189 channels.values().forEach(v -> v.addChannelTypes(channelTypeProvider));
193 * Removes all channels from the channel type provider.
194 * Call this if the corresponding Thing handler gets disposed.
196 * @param channelTypeProvider The channel type provider
198 public void removeChannelTypes(MqttChannelTypeProvider channelTypeProvider) {
199 channels.values().forEach(v -> v.removeChannelTypes(channelTypeProvider));
200 ChannelGroupTypeUID groupTypeUID = channelGroupTypeUID;
201 if (groupTypeUID != null) {
202 channelTypeProvider.removeChannelGroupType(groupTypeUID);
206 public ChannelUID buildChannelUID(String channelID) {
207 final ChannelGroupUID groupUID = channelGroupUID;
208 if (groupUID != null) {
209 return new ChannelUID(groupUID, channelID);
211 return new ChannelUID(componentConfiguration.getThingUID(), channelID);
215 * Each HomeAssistant component corresponds to a Channel Group Type.
217 public @Nullable ChannelGroupTypeUID getGroupTypeUID() {
218 return channelGroupTypeUID;
222 * The unique id of this component.
224 public @Nullable ChannelGroupUID getGroupUID() {
225 return channelGroupUID;
229 * Component (Channel Group) name.
231 public String getName() {
232 String result = channelConfiguration.getName();
234 Device device = channelConfiguration.getDevice();
235 if (result == null && device != null) {
236 result = device.getName();
238 if (result == null) {
239 result = haID.objectID;
245 * Each component consists of multiple Channels.
247 public Map<String, ComponentChannel> getChannelMap() {
252 * Return a components channel. A HomeAssistant MQTT component consists of multiple functions
253 * and those are mapped to one or more channels. The channel IDs are constants within the
254 * derived Component, like the {@link Switch#SWITCH_CHANNEL_ID}.
256 * @param channelID The channel ID
257 * @return A components channel
259 public @Nullable ComponentChannel getChannel(String channelID) {
260 return channels.get(channelID);
264 * @return Returns the configuration hash value for easy comparison.
266 public int getConfigHash() {
271 * Return the channel group type.
273 public @Nullable ChannelGroupType getType() {
274 ChannelGroupTypeUID groupTypeUID = channelGroupTypeUID;
275 if (groupTypeUID == null) {
278 final List<ChannelDefinition> channelDefinitions = channels.values().stream().map(ComponentChannel::type)
279 .collect(Collectors.toList());
280 return ChannelGroupTypeBuilder.instance(groupTypeUID, getName()).withChannelDefinitions(channelDefinitions)
284 public List<ChannelDefinition> getChannels() {
285 return channels.values().stream().map(ComponentChannel::type).collect(Collectors.toList());
289 * Resets all channel states to state UNDEF. Call this method after the connection
290 * to the MQTT broker got lost.
292 public void resetState() {
293 channels.values().forEach(ComponentChannel::resetState);
297 * Return the channel group definition for this component.
299 public @Nullable ChannelGroupDefinition getGroupDefinition() {
300 ChannelGroupTypeUID groupTypeUID = channelGroupTypeUID;
301 if (groupTypeUID == null) {
304 return new ChannelGroupDefinition(channelGroupUID.getId(), groupTypeUID, getName(), null);
307 public HaID getHaID() {
311 public String getChannelConfigurationJson() {
312 return channelConfigurationJson;
316 public TransformationServiceProvider getTransformationServiceProvider() {
317 return componentConfiguration.getTransformationServiceProvider();
320 public boolean isEnabledByDefault() {
321 return channelConfiguration.isEnabledByDefault();
324 public Gson getGson() {
325 return componentConfiguration.getGson();