2 * Copyright (c) 2010-2023 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.List;
17 import java.util.Objects;
18 import java.util.TreeMap;
19 import java.util.concurrent.CompletableFuture;
20 import java.util.concurrent.ScheduledExecutorService;
21 import java.util.stream.Collectors;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.mqtt.generic.AvailabilityTracker;
26 import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
27 import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
28 import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
29 import org.openhab.binding.mqtt.generic.utils.FutureCollector;
30 import org.openhab.binding.mqtt.generic.values.Value;
31 import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
32 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
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.ChannelGroupUID;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.type.ChannelDefinition;
43 import org.openhab.core.thing.type.ChannelGroupDefinition;
44 import org.openhab.core.thing.type.ChannelGroupType;
45 import org.openhab.core.thing.type.ChannelGroupTypeBuilder;
46 import org.openhab.core.thing.type.ChannelGroupTypeUID;
48 import com.google.gson.Gson;
51 * A HomeAssistant component is comparable to a channel group.
52 * It has a name and consists of multiple channels.
54 * @author David Graeff - Initial contribution
55 * @param <C> Config class derived from {@link AbstractChannelConfiguration}
58 public abstract class AbstractComponent<C extends AbstractChannelConfiguration> {
59 private static final String JINJA_PREFIX = "JINJA:";
61 // Component location fields
62 private final ComponentConfiguration componentConfiguration;
63 protected final @Nullable ChannelGroupTypeUID channelGroupTypeUID;
64 protected final @Nullable ChannelGroupUID channelGroupUID;
65 protected final HaID haID;
67 // Channels and configuration
68 protected final Map<String, ComponentChannel> channels = new TreeMap<>();
69 // The hash code ({@link String#hashCode()}) of the configuration string
70 // Used to determine if a component has changed.
71 protected final int configHash;
72 protected final String channelConfigurationJson;
73 protected final C channelConfiguration;
75 protected boolean configSeen;
78 * Creates component based on generic configuration and component configuration type.
80 * @param componentConfiguration generic componentConfiguration with not parsed JSON config
81 * @param clazz target configuration type
83 public AbstractComponent(ComponentFactory.ComponentConfiguration componentConfiguration, Class<C> clazz) {
84 this.componentConfiguration = componentConfiguration;
86 this.channelConfigurationJson = componentConfiguration.getConfigJSON();
87 this.channelConfiguration = componentConfiguration.getConfig(clazz);
88 this.configHash = channelConfigurationJson.hashCode();
90 this.haID = componentConfiguration.getHaID();
92 String name = channelConfiguration.getName();
93 if (name != null && !name.isEmpty()) {
94 String groupId = this.haID.getGroupId(channelConfiguration.getUniqueId());
96 this.channelGroupTypeUID = new ChannelGroupTypeUID(MqttBindingConstants.BINDING_ID, groupId);
97 this.channelGroupUID = new ChannelGroupUID(componentConfiguration.getThingUID(), groupId);
99 this.channelGroupTypeUID = null;
100 this.channelGroupUID = null;
103 this.configSeen = false;
105 final List<Availability> availabilities = channelConfiguration.getAvailability();
106 if (availabilities != null) {
107 AvailabilityMode mode = channelConfiguration.getAvailabilityMode();
108 AvailabilityTracker.AvailabilityMode availabilityTrackerMode = switch (mode) {
109 case ALL -> AvailabilityTracker.AvailabilityMode.ALL;
110 case ANY -> AvailabilityTracker.AvailabilityMode.ANY;
111 case LATEST -> AvailabilityTracker.AvailabilityMode.LATEST;
113 componentConfiguration.getTracker().setAvailabilityMode(availabilityTrackerMode);
114 for (Availability availability : availabilities) {
115 String availabilityTemplate = availability.getValueTemplate();
116 if (availabilityTemplate != null) {
117 availabilityTemplate = JINJA_PREFIX + availabilityTemplate;
119 componentConfiguration.getTracker().addAvailabilityTopic(availability.getTopic(),
120 availability.getPayloadAvailable(), availability.getPayloadNotAvailable(), availabilityTemplate,
121 componentConfiguration.getTransformationServiceProvider());
124 String availabilityTopic = this.channelConfiguration.getAvailabilityTopic();
125 if (availabilityTopic != null) {
126 String availabilityTemplate = this.channelConfiguration.getAvailabilityTemplate();
127 if (availabilityTemplate != null) {
128 availabilityTemplate = JINJA_PREFIX + availabilityTemplate;
130 componentConfiguration.getTracker().addAvailabilityTopic(availabilityTopic,
131 this.channelConfiguration.getPayloadAvailable(),
132 this.channelConfiguration.getPayloadNotAvailable(), availabilityTemplate,
133 componentConfiguration.getTransformationServiceProvider());
138 protected ComponentChannel.Builder buildChannel(String channelID, Value valueState, String label,
139 ChannelStateUpdateListener channelStateUpdateListener) {
140 return new ComponentChannel.Builder(this, channelID, valueState, label, channelStateUpdateListener);
143 public void setConfigSeen() {
144 this.configSeen = true;
148 * Subscribes to all state channels of the component and adds all channels to the provided channel type provider.
150 * @param connection connection to the MQTT broker
151 * @param scheduler thing scheduler
152 * @param timeout channel subscription timeout
153 * @return A future that completes as soon as all subscriptions have been performed. Completes exceptionally on
156 public CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection, ScheduledExecutorService scheduler,
158 return channels.values().stream().map(cChannel -> cChannel.start(connection, scheduler, timeout))
159 .collect(FutureCollector.allOf());
163 * Unsubscribes from all state channels of the component.
165 * @return A future that completes as soon as all subscriptions removals have been performed. Completes
166 * exceptionally on errors.
168 public CompletableFuture<@Nullable Void> stop() {
169 return channels.values().stream().map(ComponentChannel::stop).collect(FutureCollector.allOf());
173 * Add all channel types to the channel type provider.
175 * @param channelTypeProvider The channel type provider
177 public void addChannelTypes(MqttChannelTypeProvider channelTypeProvider) {
178 ChannelGroupTypeUID groupTypeUID = channelGroupTypeUID;
179 if (groupTypeUID != null) {
180 channelTypeProvider.setChannelGroupType(groupTypeUID, Objects.requireNonNull(getType()));
182 channels.values().forEach(v -> v.addChannelTypes(channelTypeProvider));
186 * Removes all channels from the channel type provider.
187 * Call this if the corresponding Thing handler gets disposed.
189 * @param channelTypeProvider The channel type provider
191 public void removeChannelTypes(MqttChannelTypeProvider channelTypeProvider) {
192 channels.values().forEach(v -> v.removeChannelTypes(channelTypeProvider));
193 ChannelGroupTypeUID groupTypeUID = channelGroupTypeUID;
194 if (groupTypeUID != null) {
195 channelTypeProvider.removeChannelGroupType(groupTypeUID);
199 public ChannelUID buildChannelUID(String channelID) {
200 final ChannelGroupUID groupUID = channelGroupUID;
201 if (groupUID != null) {
202 return new ChannelUID(groupUID, channelID);
204 return new ChannelUID(componentConfiguration.getThingUID(), channelID);
208 * Each HomeAssistant component corresponds to a Channel Group Type.
210 public @Nullable ChannelGroupTypeUID getGroupTypeUID() {
211 return channelGroupTypeUID;
215 * The unique id of this component.
217 public @Nullable ChannelGroupUID getGroupUID() {
218 return channelGroupUID;
222 * Component (Channel Group) name.
224 public String getName() {
225 String result = channelConfiguration.getName();
227 Device device = channelConfiguration.getDevice();
228 if (result == null && device != null) {
229 result = device.getName();
231 if (result == null) {
232 result = haID.objectID;
238 * Each component consists of multiple Channels.
240 public Map<String, ComponentChannel> getChannelMap() {
245 * Return a components channel. A HomeAssistant MQTT component consists of multiple functions
246 * and those are mapped to one or more channels. The channel IDs are constants within the
247 * derived Component, like the {@link Switch#SWITCH_CHANNEL_ID}.
249 * @param channelID The channel ID
250 * @return A components channel
252 public @Nullable ComponentChannel getChannel(String channelID) {
253 return channels.get(channelID);
257 * @return Returns the configuration hash value for easy comparison.
259 public int getConfigHash() {
264 * Return the channel group type.
266 public @Nullable ChannelGroupType getType() {
267 ChannelGroupTypeUID groupTypeUID = channelGroupTypeUID;
268 if (groupTypeUID == null) {
271 final List<ChannelDefinition> channelDefinitions = channels.values().stream().map(ComponentChannel::type)
272 .collect(Collectors.toList());
273 return ChannelGroupTypeBuilder.instance(groupTypeUID, getName()).withChannelDefinitions(channelDefinitions)
277 public List<ChannelDefinition> getChannels() {
278 return channels.values().stream().map(ComponentChannel::type).collect(Collectors.toList());
282 * Resets all channel states to state UNDEF. Call this method after the connection
283 * to the MQTT broker got lost.
285 public void resetState() {
286 channels.values().forEach(ComponentChannel::resetState);
290 * Return the channel group definition for this component.
292 public @Nullable ChannelGroupDefinition getGroupDefinition() {
293 ChannelGroupTypeUID groupTypeUID = channelGroupTypeUID;
294 if (groupTypeUID == null) {
297 return new ChannelGroupDefinition(channelGroupUID.getId(), groupTypeUID, getName(), null);
300 public HaID getHaID() {
304 public String getChannelConfigurationJson() {
305 return channelConfigurationJson;
309 public TransformationServiceProvider getTransformationServiceProvider() {
310 return componentConfiguration.getTransformationServiceProvider();
313 public boolean isEnabledByDefault() {
314 return channelConfiguration.isEnabledByDefault();
317 public Gson getGson() {
318 return componentConfiguration.getGson();