]> git.basschouten.com Git - openhab-addons.git/blob
131cc81d65cf833019f698f8d30afeae31a678f5
[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.component;
14
15 import java.util.ArrayList;
16 import java.util.List;
17 import java.util.Map;
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;
23
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;
50
51 import com.google.gson.Gson;
52 import com.hubspot.jinjava.Jinjava;
53
54 /**
55  * A HomeAssistant component is comparable to a channel group.
56  * It has a name and consists of multiple channels.
57  *
58  * @author David Graeff - Initial contribution
59  * @param <C> Config class derived from {@link AbstractChannelConfiguration}
60  */
61 @NonNullByDefault
62 public abstract class AbstractComponent<C extends AbstractChannelConfiguration> {
63     private static final String JINJA_PREFIX = "JINJA:";
64
65     // Component location fields
66     protected final ComponentConfiguration componentConfiguration;
67     protected final @Nullable ChannelGroupUID channelGroupUID;
68     protected final HaID haID;
69
70     // Channels and configuration
71     protected final Map<String, ComponentChannel> channels = new TreeMap<>();
72     protected final List<ComponentChannel> hiddenChannels = new ArrayList<>();
73
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;
79
80     protected boolean configSeen;
81     protected final boolean singleChannelComponent;
82     protected final String groupId;
83     protected final String uniqueId;
84
85     public AbstractComponent(ComponentFactory.ComponentConfiguration componentConfiguration, Class<C> clazz,
86             boolean newStyleChannels) {
87         this(componentConfiguration, clazz, newStyleChannels, false);
88     }
89
90     /**
91      * Creates component based on generic configuration and component configuration type.
92      *
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)
98      */
99     public AbstractComponent(ComponentFactory.ComponentConfiguration componentConfiguration, Class<C> clazz,
100             boolean newStyleChannels, boolean singleChannelComponent) {
101         this.componentConfiguration = componentConfiguration;
102         this.singleChannelComponent = newStyleChannels && singleChannelComponent;
103
104         this.channelConfigurationJson = componentConfiguration.getConfigJSON();
105         this.channelConfiguration = componentConfiguration.getConfig(clazz);
106         this.configHash = channelConfigurationJson.hashCode();
107
108         this.haID = componentConfiguration.getHaID();
109
110         String name = channelConfiguration.getName();
111         if (name != null && !name.isEmpty()) {
112             groupId = this.haID.getGroupId(channelConfiguration.getUniqueId(), newStyleChannels);
113
114             this.channelGroupUID = this.singleChannelComponent ? null
115                     : new ChannelGroupUID(componentConfiguration.getThingUID(), groupId);
116         } else {
117             this.groupId = this.singleChannelComponent ? haID.component : "";
118             this.channelGroupUID = null;
119         }
120         uniqueId = this.haID.getGroupId(channelConfiguration.getUniqueId(), false);
121
122         this.configSeen = false;
123
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;
131             };
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);
139                 }
140                 componentConfiguration.getTracker().addAvailabilityTopic(availability.getTopic(),
141                         availability.getPayloadAvailable(), availability.getPayloadNotAvailable(),
142                         availabilityTemplates);
143             }
144         } else {
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);
152                 }
153                 componentConfiguration.getTracker().addAvailabilityTopic(availabilityTopic,
154                         this.channelConfiguration.getPayloadAvailable(),
155                         this.channelConfiguration.getPayloadNotAvailable(), availabilityTemplates);
156             }
157         }
158     }
159
160     protected ComponentChannel.Builder buildChannel(String channelID, ComponentChannelType channelType,
161             Value valueState, String label, ChannelStateUpdateListener channelStateUpdateListener) {
162         if (singleChannelComponent) {
163             channelID = groupId;
164         }
165         return new ComponentChannel.Builder(this, channelID, channelType.getChannelTypeUID(), valueState, label,
166                 channelStateUpdateListener);
167     }
168
169     public void setConfigSeen() {
170         this.configSeen = true;
171     }
172
173     /**
174      * Subscribes to all state channels of the component and adds all channels to the provided channel type provider.
175      *
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
180      *         errors.
181      */
182     public CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection, ScheduledExecutorService scheduler,
183             int timeout) {
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));
187     }
188
189     /**
190      * Unsubscribes from all state channels of the component.
191      *
192      * @return A future that completes as soon as all subscriptions removals have been performed. Completes
193      *         exceptionally on errors.
194      */
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));
200     }
201
202     /**
203      * Add all state and command descriptions to the state description provider.
204      *
205      * @param stateDescriptionProvider The state description provider
206      */
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);
212             }
213             CommandDescription commandDescription = channel.getCommandDescription();
214             if (commandDescription != null) {
215                 stateDescriptionProvider.setDescription(channel.getChannel().getUID(), commandDescription);
216             }
217         });
218     }
219
220     public ChannelUID buildChannelUID(String channelID) {
221         final ChannelGroupUID groupUID = channelGroupUID;
222         if (groupUID != null) {
223             return new ChannelUID(groupUID, channelID);
224         }
225         return new ChannelUID(componentConfiguration.getThingUID(), channelID);
226     }
227
228     public String getGroupId() {
229         return groupId;
230     }
231
232     /**
233      * Component (Channel Group) name.
234      */
235     public String getName() {
236         String result = channelConfiguration.getName();
237
238         Device device = channelConfiguration.getDevice();
239         if (result == null && device != null) {
240             result = device.getName();
241         }
242         if (result == null) {
243             result = haID.objectID;
244         }
245         return result;
246     }
247
248     /**
249      * Each component consists of multiple Channels.
250      */
251     public Map<String, ComponentChannel> getChannelMap() {
252         return channels;
253     }
254
255     /**
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}.
259      *
260      * @param channelID The channel ID
261      * @return A components channel
262      */
263     public @Nullable ComponentChannel getChannel(String channelID) {
264         return channels.get(channelID);
265     }
266
267     /**
268      * @return Returns the configuration hash value for easy comparison.
269      */
270     public int getConfigHash() {
271         return configHash;
272     }
273
274     /**
275      * Return the channel group type.
276      */
277     public @Nullable ChannelGroupType getChannelGroupType(String prefix) {
278         if (channelGroupUID == null) {
279             return null;
280         }
281         return ChannelGroupTypeBuilder.instance(getChannelGroupTypeUID(prefix), getName())
282                 .withChannelDefinitions(getAllChannelDefinitions()).build();
283     }
284
285     public List<ChannelDefinition> getChannelDefinitions() {
286         if (channelGroupUID != null) {
287             return List.of();
288         }
289         return getAllChannelDefinitions();
290     }
291
292     private List<ChannelDefinition> getAllChannelDefinitions() {
293         return channels.values().stream().map(ComponentChannel::channelDefinition).toList();
294     }
295
296     public List<Channel> getChannels() {
297         return channels.values().stream().map(ComponentChannel::getChannel).toList();
298     }
299
300     /**
301      * Resets all channel states to state UNDEF. Call this method after the connection
302      * to the MQTT broker got lost.
303      */
304     public void resetState() {
305         channels.values().forEach(ComponentChannel::resetState);
306     }
307
308     /**
309      * Return the channel group definition for this component.
310      */
311     public @Nullable ChannelGroupDefinition getGroupDefinition(String prefix) {
312         if (channelGroupUID == null) {
313             return null;
314         }
315         return new ChannelGroupDefinition(channelGroupUID.getId(), getChannelGroupTypeUID(prefix), getName(), null);
316     }
317
318     public boolean hasGroup() {
319         return channelGroupUID != null;
320     }
321
322     public HaID getHaID() {
323         return haID;
324     }
325
326     public String getChannelConfigurationJson() {
327         return channelConfigurationJson;
328     }
329
330     public boolean isEnabledByDefault() {
331         return channelConfiguration.isEnabledByDefault();
332     }
333
334     public Gson getGson() {
335         return componentConfiguration.getGson();
336     }
337
338     public Jinjava getJinjava() {
339         return componentConfiguration.getJinjava();
340     }
341
342     public C getChannelConfiguration() {
343         return channelConfiguration;
344     }
345
346     private ChannelGroupTypeUID getChannelGroupTypeUID(String prefix) {
347         return new ChannelGroupTypeUID(MqttBindingConstants.BINDING_ID, prefix + "_" + uniqueId);
348     }
349 }