]> git.basschouten.com Git - openhab-addons.git/blob
111322a25392188b7cf84847ea1ef2859feb7858
[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.HomeAssistantChannelTransformation;
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.Channel;
42 import org.openhab.core.thing.ChannelGroupUID;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.thing.binding.generic.ChannelTransformation;
45 import org.openhab.core.thing.type.ChannelDefinition;
46 import org.openhab.core.thing.type.ChannelGroupDefinition;
47 import org.openhab.core.thing.type.ChannelGroupType;
48 import org.openhab.core.thing.type.ChannelGroupTypeBuilder;
49 import org.openhab.core.thing.type.ChannelGroupTypeUID;
50 import org.openhab.core.types.CommandDescription;
51 import org.openhab.core.types.StateDescription;
52
53 import com.google.gson.Gson;
54 import com.hubspot.jinjava.Jinjava;
55
56 /**
57  * A HomeAssistant component is comparable to a channel group.
58  * It has a name and consists of multiple channels.
59  *
60  * @author David Graeff - Initial contribution
61  * @param <C> Config class derived from {@link AbstractChannelConfiguration}
62  */
63 @NonNullByDefault
64 public abstract class AbstractComponent<C extends AbstractChannelConfiguration> {
65
66     // Component location fields
67     protected final ComponentConfiguration componentConfiguration;
68     protected final @Nullable ChannelGroupUID channelGroupUID;
69     protected final HaID haID;
70
71     // Channels and configuration
72     protected final Map<String, ComponentChannel> channels = new TreeMap<>();
73     protected final List<ComponentChannel> hiddenChannels = new ArrayList<>();
74
75     // The hash code ({@link String#hashCode()}) of the configuration string
76     // Used to determine if a component has changed.
77     protected final int configHash;
78     protected final String channelConfigurationJson;
79     protected final C channelConfiguration;
80
81     protected boolean configSeen;
82     protected final boolean singleChannelComponent;
83     protected final String groupId;
84     protected final String uniqueId;
85
86     public AbstractComponent(ComponentFactory.ComponentConfiguration componentConfiguration, Class<C> clazz,
87             boolean newStyleChannels) {
88         this(componentConfiguration, clazz, newStyleChannels, false);
89     }
90
91     /**
92      * Creates component based on generic configuration and component configuration type.
93      *
94      * @param componentConfiguration generic componentConfiguration with not parsed JSON config
95      * @param clazz target configuration type
96      * @param newStyleChannels if new style channels should be used
97      * @param singleChannelComponent if this component only ever has one channel, so should never be in a group
98      *            (only if newStyleChannels is true)
99      */
100     public AbstractComponent(ComponentFactory.ComponentConfiguration componentConfiguration, Class<C> clazz,
101             boolean newStyleChannels, boolean singleChannelComponent) {
102         this.componentConfiguration = componentConfiguration;
103         this.singleChannelComponent = newStyleChannels && singleChannelComponent;
104
105         this.channelConfigurationJson = componentConfiguration.getConfigJSON();
106         this.channelConfiguration = componentConfiguration.getConfig(clazz);
107         this.configHash = channelConfigurationJson.hashCode();
108
109         this.haID = componentConfiguration.getHaID();
110
111         String name = channelConfiguration.getName();
112         if (name != null && !name.isEmpty()) {
113             groupId = this.haID.getGroupId(channelConfiguration.getUniqueId(), newStyleChannels);
114
115             this.channelGroupUID = this.singleChannelComponent ? null
116                     : new ChannelGroupUID(componentConfiguration.getThingUID(), groupId);
117         } else {
118             this.groupId = this.singleChannelComponent ? haID.component : "";
119             this.channelGroupUID = null;
120         }
121         uniqueId = this.haID.getGroupId(channelConfiguration.getUniqueId(), false);
122
123         this.configSeen = false;
124
125         final List<Availability> availabilities = channelConfiguration.getAvailability();
126         if (availabilities != null) {
127             AvailabilityMode mode = channelConfiguration.getAvailabilityMode();
128             AvailabilityTracker.AvailabilityMode availabilityTrackerMode = switch (mode) {
129                 case ALL -> AvailabilityTracker.AvailabilityMode.ALL;
130                 case ANY -> AvailabilityTracker.AvailabilityMode.ANY;
131                 case LATEST -> AvailabilityTracker.AvailabilityMode.LATEST;
132             };
133             componentConfiguration.getTracker().setAvailabilityMode(availabilityTrackerMode);
134             for (Availability availability : availabilities) {
135                 String availabilityTemplate = availability.getValueTemplate();
136                 ChannelTransformation transformation = null;
137                 if (availabilityTemplate != null) {
138                     transformation = new HomeAssistantChannelTransformation(getJinjava(), this, availabilityTemplate);
139                 }
140                 componentConfiguration.getTracker().addAvailabilityTopic(availability.getTopic(),
141                         availability.getPayloadAvailable(), availability.getPayloadNotAvailable(), transformation);
142             }
143         } else {
144             String availabilityTopic = this.channelConfiguration.getAvailabilityTopic();
145             if (availabilityTopic != null) {
146                 String availabilityTemplate = this.channelConfiguration.getAvailabilityTemplate();
147                 ChannelTransformation transformation = null;
148                 if (availabilityTemplate != null) {
149                     transformation = new HomeAssistantChannelTransformation(getJinjava(), this, availabilityTemplate);
150                 }
151                 componentConfiguration.getTracker().addAvailabilityTopic(availabilityTopic,
152                         this.channelConfiguration.getPayloadAvailable(),
153                         this.channelConfiguration.getPayloadNotAvailable(), transformation);
154             }
155         }
156     }
157
158     protected ComponentChannel.Builder buildChannel(String channelID, ComponentChannelType channelType,
159             Value valueState, String label, ChannelStateUpdateListener channelStateUpdateListener) {
160         if (singleChannelComponent) {
161             channelID = groupId;
162         }
163         return new ComponentChannel.Builder(this, channelID, channelType.getChannelTypeUID(), valueState, label,
164                 channelStateUpdateListener);
165     }
166
167     public void setConfigSeen() {
168         this.configSeen = true;
169     }
170
171     /**
172      * Subscribes to all state channels of the component and adds all channels to the provided channel type provider.
173      *
174      * @param connection connection to the MQTT broker
175      * @param scheduler thing scheduler
176      * @param timeout channel subscription timeout
177      * @return A future that completes as soon as all subscriptions have been performed. Completes exceptionally on
178      *         errors.
179      */
180     public CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection, ScheduledExecutorService scheduler,
181             int timeout) {
182         return Stream.concat(channels.values().stream(), hiddenChannels.stream())
183                 .map(v -> v.start(connection, scheduler, timeout)) //
184                 .reduce(CompletableFuture.completedFuture(null), (f, v) -> f.thenCompose(b -> v));
185     }
186
187     /**
188      * Unsubscribes from all state channels of the component.
189      *
190      * @return A future that completes as soon as all subscriptions removals have been performed. Completes
191      *         exceptionally on errors.
192      */
193     public CompletableFuture<@Nullable Void> stop() {
194         return Stream.concat(channels.values().stream(), hiddenChannels.stream()) //
195                 .filter(Objects::nonNull) //
196                 .map(ComponentChannel::stop) //
197                 .reduce(CompletableFuture.completedFuture(null), (f, v) -> f.thenCompose(b -> v));
198     }
199
200     /**
201      * Add all state and command descriptions to the state description provider.
202      *
203      * @param stateDescriptionProvider The state description provider
204      */
205     public void addStateDescriptions(MqttChannelStateDescriptionProvider stateDescriptionProvider) {
206         channels.values().forEach(channel -> {
207             StateDescription stateDescription = channel.getStateDescription();
208             if (stateDescription != null) {
209                 stateDescriptionProvider.setDescription(channel.getChannel().getUID(), stateDescription);
210             }
211             CommandDescription commandDescription = channel.getCommandDescription();
212             if (commandDescription != null) {
213                 stateDescriptionProvider.setDescription(channel.getChannel().getUID(), commandDescription);
214             }
215         });
216     }
217
218     public ChannelUID buildChannelUID(String channelID) {
219         final ChannelGroupUID groupUID = channelGroupUID;
220         if (groupUID != null) {
221             return new ChannelUID(groupUID, channelID);
222         }
223         return new ChannelUID(componentConfiguration.getThingUID(), channelID);
224     }
225
226     public String getGroupId() {
227         return groupId;
228     }
229
230     /**
231      * Component (Channel Group) name.
232      */
233     public String getName() {
234         String result = channelConfiguration.getName();
235
236         Device device = channelConfiguration.getDevice();
237         if (result == null && device != null) {
238             result = device.getName();
239         }
240         if (result == null) {
241             result = haID.objectID;
242         }
243         return result;
244     }
245
246     /**
247      * Each component consists of multiple Channels.
248      */
249     public Map<String, ComponentChannel> getChannelMap() {
250         return channels;
251     }
252
253     /**
254      * Return a components channel. A HomeAssistant MQTT component consists of multiple functions
255      * and those are mapped to one or more channels. The channel IDs are constants within the
256      * derived Component, like the {@link Switch#SWITCH_CHANNEL_ID}.
257      *
258      * @param channelID The channel ID
259      * @return A components channel
260      */
261     public @Nullable ComponentChannel getChannel(String channelID) {
262         return channels.get(channelID);
263     }
264
265     /**
266      * @return Returns the configuration hash value for easy comparison.
267      */
268     public int getConfigHash() {
269         return configHash;
270     }
271
272     /**
273      * Return the channel group type.
274      */
275     public @Nullable ChannelGroupType getChannelGroupType(String prefix) {
276         if (channelGroupUID == null) {
277             return null;
278         }
279         return ChannelGroupTypeBuilder.instance(getChannelGroupTypeUID(prefix), getName())
280                 .withChannelDefinitions(getAllChannelDefinitions()).build();
281     }
282
283     public List<ChannelDefinition> getChannelDefinitions() {
284         if (channelGroupUID != null) {
285             return List.of();
286         }
287         return getAllChannelDefinitions();
288     }
289
290     private List<ChannelDefinition> getAllChannelDefinitions() {
291         return channels.values().stream().map(ComponentChannel::channelDefinition).toList();
292     }
293
294     public List<Channel> getChannels() {
295         return channels.values().stream().map(ComponentChannel::getChannel).toList();
296     }
297
298     /**
299      * Resets all channel states to state UNDEF. Call this method after the connection
300      * to the MQTT broker got lost.
301      */
302     public void resetState() {
303         channels.values().forEach(ComponentChannel::resetState);
304     }
305
306     /**
307      * Return the channel group definition for this component.
308      */
309     public @Nullable ChannelGroupDefinition getGroupDefinition(String prefix) {
310         if (channelGroupUID == null) {
311             return null;
312         }
313         return new ChannelGroupDefinition(channelGroupUID.getId(), getChannelGroupTypeUID(prefix), getName(), null);
314     }
315
316     public boolean hasGroup() {
317         return channelGroupUID != null;
318     }
319
320     public HaID getHaID() {
321         return haID;
322     }
323
324     public String getChannelConfigurationJson() {
325         return channelConfigurationJson;
326     }
327
328     public boolean isEnabledByDefault() {
329         return channelConfiguration.isEnabledByDefault();
330     }
331
332     public Gson getGson() {
333         return componentConfiguration.getGson();
334     }
335
336     public Jinjava getJinjava() {
337         return componentConfiguration.getJinjava();
338     }
339
340     public C getChannelConfiguration() {
341         return channelConfiguration;
342     }
343
344     private ChannelGroupTypeUID getChannelGroupTypeUID(String prefix) {
345         return new ChannelGroupTypeUID(MqttBindingConstants.BINDING_ID, prefix + "_" + uniqueId);
346     }
347 }