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