]> git.basschouten.com Git - openhab-addons.git/blob
ca901bcba959018290c93acd04d155852b69f1d9
[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.Collectors;
23 import java.util.stream.Stream;
24
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;
48
49 import com.google.gson.Gson;
50
51 /**
52  * A HomeAssistant component is comparable to a channel group.
53  * It has a name and consists of multiple channels.
54  *
55  * @author David Graeff - Initial contribution
56  * @param <C> Config class derived from {@link AbstractChannelConfiguration}
57  */
58 @NonNullByDefault
59 public abstract class AbstractComponent<C extends AbstractChannelConfiguration> {
60     private static final String JINJA_PREFIX = "JINJA:";
61
62     // Component location fields
63     protected final ComponentConfiguration componentConfiguration;
64     protected final @Nullable ChannelGroupTypeUID channelGroupTypeUID;
65     protected final @Nullable ChannelGroupUID channelGroupUID;
66     protected final HaID haID;
67
68     // Channels and configuration
69     protected final Map<String, ComponentChannel> channels = new TreeMap<>();
70     protected final List<ComponentChannel> hiddenChannels = new ArrayList<>();
71
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;
77
78     protected boolean configSeen;
79
80     /**
81      * Creates component based on generic configuration and component configuration type.
82      *
83      * @param componentConfiguration generic componentConfiguration with not parsed JSON config
84      * @param clazz target configuration type
85      */
86     public AbstractComponent(ComponentFactory.ComponentConfiguration componentConfiguration, Class<C> clazz) {
87         this.componentConfiguration = componentConfiguration;
88
89         this.channelConfigurationJson = componentConfiguration.getConfigJSON();
90         this.channelConfiguration = componentConfiguration.getConfig(clazz);
91         this.configHash = channelConfigurationJson.hashCode();
92
93         this.haID = componentConfiguration.getHaID();
94
95         String name = channelConfiguration.getName();
96         if (name != null && !name.isEmpty()) {
97             String groupId = this.haID.getGroupId(channelConfiguration.getUniqueId());
98
99             this.channelGroupTypeUID = new ChannelGroupTypeUID(MqttBindingConstants.BINDING_ID, groupId);
100             this.channelGroupUID = new ChannelGroupUID(componentConfiguration.getThingUID(), groupId);
101         } else {
102             this.channelGroupTypeUID = null;
103             this.channelGroupUID = null;
104         }
105
106         this.configSeen = false;
107
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;
115             };
116             componentConfiguration.getTracker().setAvailabilityMode(availabilityTrackerMode);
117             for (Availability availability : availabilities) {
118                 String availabilityTemplate = availability.getValueTemplate();
119                 if (availabilityTemplate != null) {
120                     availabilityTemplate = JINJA_PREFIX + availabilityTemplate;
121                 }
122                 componentConfiguration.getTracker().addAvailabilityTopic(availability.getTopic(),
123                         availability.getPayloadAvailable(), availability.getPayloadNotAvailable(), availabilityTemplate,
124                         componentConfiguration.getTransformationServiceProvider());
125             }
126         } else {
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;
132                 }
133                 componentConfiguration.getTracker().addAvailabilityTopic(availabilityTopic,
134                         this.channelConfiguration.getPayloadAvailable(),
135                         this.channelConfiguration.getPayloadNotAvailable(), availabilityTemplate,
136                         componentConfiguration.getTransformationServiceProvider());
137             }
138         }
139     }
140
141     protected ComponentChannel.Builder buildChannel(String channelID, Value valueState, String label,
142             ChannelStateUpdateListener channelStateUpdateListener) {
143         return new ComponentChannel.Builder(this, channelID, valueState, label, channelStateUpdateListener);
144     }
145
146     public void setConfigSeen() {
147         this.configSeen = true;
148     }
149
150     /**
151      * Subscribes to all state channels of the component and adds all channels to the provided channel type provider.
152      *
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
157      *         errors.
158      */
159     public CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection, ScheduledExecutorService scheduler,
160             int timeout) {
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));
164     }
165
166     /**
167      * Unsubscribes from all state channels of the component.
168      *
169      * @return A future that completes as soon as all subscriptions removals have been performed. Completes
170      *         exceptionally on errors.
171      */
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));
177     }
178
179     /**
180      * Add all channel types to the channel type provider.
181      *
182      * @param channelTypeProvider The channel type provider
183      */
184     public void addChannelTypes(MqttChannelTypeProvider channelTypeProvider) {
185         ChannelGroupTypeUID groupTypeUID = channelGroupTypeUID;
186         if (groupTypeUID != null) {
187             channelTypeProvider.setChannelGroupType(groupTypeUID, Objects.requireNonNull(getType()));
188         }
189         channels.values().forEach(v -> v.addChannelTypes(channelTypeProvider));
190     }
191
192     /**
193      * Removes all channels from the channel type provider.
194      * Call this if the corresponding Thing handler gets disposed.
195      *
196      * @param channelTypeProvider The channel type provider
197      */
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);
203         }
204     }
205
206     public ChannelUID buildChannelUID(String channelID) {
207         final ChannelGroupUID groupUID = channelGroupUID;
208         if (groupUID != null) {
209             return new ChannelUID(groupUID, channelID);
210         }
211         return new ChannelUID(componentConfiguration.getThingUID(), channelID);
212     }
213
214     /**
215      * Each HomeAssistant component corresponds to a Channel Group Type.
216      */
217     public @Nullable ChannelGroupTypeUID getGroupTypeUID() {
218         return channelGroupTypeUID;
219     }
220
221     /**
222      * The unique id of this component.
223      */
224     public @Nullable ChannelGroupUID getGroupUID() {
225         return channelGroupUID;
226     }
227
228     /**
229      * Component (Channel Group) name.
230      */
231     public String getName() {
232         String result = channelConfiguration.getName();
233
234         Device device = channelConfiguration.getDevice();
235         if (result == null && device != null) {
236             result = device.getName();
237         }
238         if (result == null) {
239             result = haID.objectID;
240         }
241         return result;
242     }
243
244     /**
245      * Each component consists of multiple Channels.
246      */
247     public Map<String, ComponentChannel> getChannelMap() {
248         return channels;
249     }
250
251     /**
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}.
255      *
256      * @param channelID The channel ID
257      * @return A components channel
258      */
259     public @Nullable ComponentChannel getChannel(String channelID) {
260         return channels.get(channelID);
261     }
262
263     /**
264      * @return Returns the configuration hash value for easy comparison.
265      */
266     public int getConfigHash() {
267         return configHash;
268     }
269
270     /**
271      * Return the channel group type.
272      */
273     public @Nullable ChannelGroupType getType() {
274         ChannelGroupTypeUID groupTypeUID = channelGroupTypeUID;
275         if (groupTypeUID == null) {
276             return null;
277         }
278         final List<ChannelDefinition> channelDefinitions = channels.values().stream().map(ComponentChannel::type)
279                 .collect(Collectors.toList());
280         return ChannelGroupTypeBuilder.instance(groupTypeUID, getName()).withChannelDefinitions(channelDefinitions)
281                 .build();
282     }
283
284     public List<ChannelDefinition> getChannels() {
285         return channels.values().stream().map(ComponentChannel::type).collect(Collectors.toList());
286     }
287
288     /**
289      * Resets all channel states to state UNDEF. Call this method after the connection
290      * to the MQTT broker got lost.
291      */
292     public void resetState() {
293         channels.values().forEach(ComponentChannel::resetState);
294     }
295
296     /**
297      * Return the channel group definition for this component.
298      */
299     public @Nullable ChannelGroupDefinition getGroupDefinition() {
300         ChannelGroupTypeUID groupTypeUID = channelGroupTypeUID;
301         if (groupTypeUID == null) {
302             return null;
303         }
304         return new ChannelGroupDefinition(channelGroupUID.getId(), groupTypeUID, getName(), null);
305     }
306
307     public HaID getHaID() {
308         return haID;
309     }
310
311     public String getChannelConfigurationJson() {
312         return channelConfigurationJson;
313     }
314
315     @Nullable
316     public TransformationServiceProvider getTransformationServiceProvider() {
317         return componentConfiguration.getTransformationServiceProvider();
318     }
319
320     public boolean isEnabledByDefault() {
321         return channelConfiguration.isEnabledByDefault();
322     }
323
324     public Gson getGson() {
325         return componentConfiguration.getGson();
326     }
327 }