]> git.basschouten.com Git - openhab-addons.git/blob
befc19a5c7d87d4b47c02445e5bec59b84beac7f
[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.TransformationServiceProvider;
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.ComponentChannelType;
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.Channel;
42 import org.openhab.core.thing.ChannelGroupUID;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.thing.type.ChannelDefinition;
45 import org.openhab.core.thing.type.ChannelGroupDefinition;
46 import org.openhab.core.thing.type.ChannelGroupType;
47 import org.openhab.core.thing.type.ChannelGroupTypeBuilder;
48 import org.openhab.core.thing.type.ChannelGroupTypeUID;
49 import org.openhab.core.types.CommandDescription;
50 import org.openhab.core.types.StateDescription;
51
52 import com.google.gson.Gson;
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                 if (availabilityTemplate != null) {
136                     availabilityTemplate = JINJA_PREFIX + availabilityTemplate;
137                 }
138                 componentConfiguration.getTracker().addAvailabilityTopic(availability.getTopic(),
139                         availability.getPayloadAvailable(), availability.getPayloadNotAvailable(), availabilityTemplate,
140                         componentConfiguration.getTransformationServiceProvider());
141             }
142         } else {
143             String availabilityTopic = this.channelConfiguration.getAvailabilityTopic();
144             if (availabilityTopic != null) {
145                 String availabilityTemplate = this.channelConfiguration.getAvailabilityTemplate();
146                 if (availabilityTemplate != null) {
147                     availabilityTemplate = JINJA_PREFIX + availabilityTemplate;
148                 }
149                 componentConfiguration.getTracker().addAvailabilityTopic(availabilityTopic,
150                         this.channelConfiguration.getPayloadAvailable(),
151                         this.channelConfiguration.getPayloadNotAvailable(), availabilityTemplate,
152                         componentConfiguration.getTransformationServiceProvider());
153             }
154         }
155     }
156
157     protected ComponentChannel.Builder buildChannel(String channelID, ComponentChannelType channelType,
158             Value valueState, String label, ChannelStateUpdateListener channelStateUpdateListener) {
159         if (singleChannelComponent) {
160             channelID = groupId;
161         }
162         return new ComponentChannel.Builder(this, channelID, channelType.getChannelTypeUID(), valueState, label,
163                 channelStateUpdateListener);
164     }
165
166     public void setConfigSeen() {
167         this.configSeen = true;
168     }
169
170     /**
171      * Subscribes to all state channels of the component and adds all channels to the provided channel type provider.
172      *
173      * @param connection connection to the MQTT broker
174      * @param scheduler thing scheduler
175      * @param timeout channel subscription timeout
176      * @return A future that completes as soon as all subscriptions have been performed. Completes exceptionally on
177      *         errors.
178      */
179     public CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection, ScheduledExecutorService scheduler,
180             int timeout) {
181         return Stream.concat(channels.values().stream(), hiddenChannels.stream())
182                 .map(v -> v.start(connection, scheduler, timeout)) //
183                 .reduce(CompletableFuture.completedFuture(null), (f, v) -> f.thenCompose(b -> v));
184     }
185
186     /**
187      * Unsubscribes from all state channels of the component.
188      *
189      * @return A future that completes as soon as all subscriptions removals have been performed. Completes
190      *         exceptionally on errors.
191      */
192     public CompletableFuture<@Nullable Void> stop() {
193         return Stream.concat(channels.values().stream(), hiddenChannels.stream()) //
194                 .filter(Objects::nonNull) //
195                 .map(ComponentChannel::stop) //
196                 .reduce(CompletableFuture.completedFuture(null), (f, v) -> f.thenCompose(b -> v));
197     }
198
199     /**
200      * Add all state and command descriptions to the state description provider.
201      *
202      * @param stateDescriptionProvider The state description provider
203      */
204     public void addStateDescriptions(MqttChannelStateDescriptionProvider stateDescriptionProvider) {
205         channels.values().forEach(channel -> {
206             StateDescription stateDescription = channel.getStateDescription();
207             if (stateDescription != null) {
208                 stateDescriptionProvider.setDescription(channel.getChannel().getUID(), stateDescription);
209             }
210             CommandDescription commandDescription = channel.getCommandDescription();
211             if (commandDescription != null) {
212                 stateDescriptionProvider.setDescription(channel.getChannel().getUID(), commandDescription);
213             }
214         });
215     }
216
217     public ChannelUID buildChannelUID(String channelID) {
218         final ChannelGroupUID groupUID = channelGroupUID;
219         if (groupUID != null) {
220             return new ChannelUID(groupUID, channelID);
221         }
222         return new ChannelUID(componentConfiguration.getThingUID(), channelID);
223     }
224
225     public String getGroupId() {
226         return groupId;
227     }
228
229     /**
230      * Component (Channel Group) name.
231      */
232     public String getName() {
233         String result = channelConfiguration.getName();
234
235         Device device = channelConfiguration.getDevice();
236         if (result == null && device != null) {
237             result = device.getName();
238         }
239         if (result == null) {
240             result = haID.objectID;
241         }
242         return result;
243     }
244
245     /**
246      * Each component consists of multiple Channels.
247      */
248     public Map<String, ComponentChannel> getChannelMap() {
249         return channels;
250     }
251
252     /**
253      * Return a components channel. A HomeAssistant MQTT component consists of multiple functions
254      * and those are mapped to one or more channels. The channel IDs are constants within the
255      * derived Component, like the {@link Switch#SWITCH_CHANNEL_ID}.
256      *
257      * @param channelID The channel ID
258      * @return A components channel
259      */
260     public @Nullable ComponentChannel getChannel(String channelID) {
261         return channels.get(channelID);
262     }
263
264     /**
265      * @return Returns the configuration hash value for easy comparison.
266      */
267     public int getConfigHash() {
268         return configHash;
269     }
270
271     /**
272      * Return the channel group type.
273      */
274     public @Nullable ChannelGroupType getChannelGroupType(String prefix) {
275         if (channelGroupUID == null) {
276             return null;
277         }
278         return ChannelGroupTypeBuilder.instance(getChannelGroupTypeUID(prefix), getName())
279                 .withChannelDefinitions(getAllChannelDefinitions()).build();
280     }
281
282     public List<ChannelDefinition> getChannelDefinitions() {
283         if (channelGroupUID != null) {
284             return List.of();
285         }
286         return getAllChannelDefinitions();
287     }
288
289     private List<ChannelDefinition> getAllChannelDefinitions() {
290         return channels.values().stream().map(ComponentChannel::channelDefinition).toList();
291     }
292
293     public List<Channel> getChannels() {
294         return channels.values().stream().map(ComponentChannel::getChannel).toList();
295     }
296
297     /**
298      * Resets all channel states to state UNDEF. Call this method after the connection
299      * to the MQTT broker got lost.
300      */
301     public void resetState() {
302         channels.values().forEach(ComponentChannel::resetState);
303     }
304
305     /**
306      * Return the channel group definition for this component.
307      */
308     public @Nullable ChannelGroupDefinition getGroupDefinition(String prefix) {
309         if (channelGroupUID == null) {
310             return null;
311         }
312         return new ChannelGroupDefinition(channelGroupUID.getId(), getChannelGroupTypeUID(prefix), getName(), null);
313     }
314
315     public boolean hasGroup() {
316         return channelGroupUID != null;
317     }
318
319     public HaID getHaID() {
320         return haID;
321     }
322
323     public String getChannelConfigurationJson() {
324         return channelConfigurationJson;
325     }
326
327     @Nullable
328     public TransformationServiceProvider getTransformationServiceProvider() {
329         return componentConfiguration.getTransformationServiceProvider();
330     }
331
332     public boolean isEnabledByDefault() {
333         return channelConfiguration.isEnabledByDefault();
334     }
335
336     public Gson getGson() {
337         return componentConfiguration.getGson();
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 }