2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.mqtt.homeassistant.internal;
16 import java.util.concurrent.CompletableFuture;
17 import java.util.concurrent.ScheduledExecutorService;
18 import java.util.function.Predicate;
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.binding.mqtt.generic.ChannelConfigBuilder;
23 import org.openhab.binding.mqtt.generic.ChannelState;
24 import org.openhab.binding.mqtt.generic.ChannelStateTransformation;
25 import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
26 import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
27 import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
28 import org.openhab.binding.mqtt.generic.values.Value;
29 import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
30 import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent;
31 import org.openhab.core.config.core.Configuration;
32 import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
33 import org.openhab.core.thing.Channel;
34 import org.openhab.core.thing.ChannelUID;
35 import org.openhab.core.thing.binding.builder.ChannelBuilder;
36 import org.openhab.core.thing.type.AutoUpdatePolicy;
37 import org.openhab.core.thing.type.ChannelDefinition;
38 import org.openhab.core.thing.type.ChannelDefinitionBuilder;
39 import org.openhab.core.thing.type.ChannelType;
40 import org.openhab.core.thing.type.ChannelTypeBuilder;
41 import org.openhab.core.thing.type.ChannelTypeUID;
42 import org.openhab.core.types.Command;
43 import org.openhab.core.types.CommandDescription;
44 import org.openhab.core.types.StateDescriptionFragment;
47 * An {@link AbstractComponent}s derived class consists of one or multiple channels.
48 * Each component channel consists of the determined channel type, channel type UID and the
49 * channel description itself as well as the the channels state.
51 * After the discovery process has completed and the tree of components and component channels
52 * have been built up, the channel types are registered to a custom channel type provider
53 * before adding the channel descriptions to the Thing themselves.
56 * An object of this class creates the required {@link ChannelType} and {@link ChannelTypeUID} as well
57 * as keeps the {@link ChannelState} and {@link Channel} in one place.
59 * @author David Graeff - Initial contribution
62 public class ComponentChannel {
63 private static final String JINJA = "JINJA";
65 private final ChannelUID channelUID;
66 private final ChannelState channelState;
67 private final Channel channel;
68 private final ChannelType type;
69 private final ChannelTypeUID channelTypeUID;
70 private final ChannelStateUpdateListener channelStateUpdateListener;
72 private ComponentChannel(ChannelUID channelUID, ChannelState channelState, Channel channel, ChannelType type,
73 ChannelTypeUID channelTypeUID, ChannelStateUpdateListener channelStateUpdateListener) {
75 this.channelUID = channelUID;
76 this.channelState = channelState;
77 this.channel = channel;
79 this.channelTypeUID = channelTypeUID;
80 this.channelStateUpdateListener = channelStateUpdateListener;
83 public ChannelUID getChannelUID() {
87 public Channel getChannel() {
91 public ChannelState getState() {
95 public CompletableFuture<@Nullable Void> stop() {
96 return channelState.stop();
99 public CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection, ScheduledExecutorService scheduler,
101 // Make sure we set the callback again which might have been nulled during a stop
102 channelState.setChannelStateUpdateListener(this.channelStateUpdateListener);
104 return channelState.start(connection, scheduler, timeout);
107 public void addChannelTypes(MqttChannelTypeProvider channelTypeProvider) {
108 channelTypeProvider.setChannelType(channelTypeUID, type);
111 public void removeChannelTypes(MqttChannelTypeProvider channelTypeProvider) {
112 channelTypeProvider.removeChannelType(channelTypeUID);
115 public ChannelDefinition type() {
116 return new ChannelDefinitionBuilder(channelUID.getId(), channelTypeUID).build();
119 public void resetState() {
120 channelState.getCache().resetState();
123 public static class Builder {
124 private final AbstractComponent<?> component;
125 private final String channelID;
126 private final Value valueState;
127 private final String label;
128 private final ChannelStateUpdateListener channelStateUpdateListener;
130 private @Nullable String stateTopic;
131 private @Nullable String commandTopic;
132 private boolean retain;
133 private boolean trigger;
134 private boolean isAdvanced;
135 private @Nullable AutoUpdatePolicy autoUpdatePolicy;
136 private @Nullable Integer qos;
137 private @Nullable Predicate<Command> commandFilter;
139 private @Nullable String templateIn;
140 private @Nullable String templateOut;
142 private String format = "%s";
144 public Builder(AbstractComponent<?> component, String channelID, Value valueState, String label,
145 ChannelStateUpdateListener channelStateUpdateListener) {
146 this.component = component;
147 this.channelID = channelID;
148 this.valueState = valueState;
150 this.isAdvanced = false;
151 this.channelStateUpdateListener = channelStateUpdateListener;
154 public Builder stateTopic(@Nullable String stateTopic) {
155 this.stateTopic = stateTopic;
159 public Builder stateTopic(@Nullable String stateTopic, @Nullable String... templates) {
160 this.stateTopic = stateTopic;
161 if (stateTopic != null && !stateTopic.isBlank()) {
162 for (String template : templates) {
163 if (template != null && !template.isBlank()) {
164 this.templateIn = template;
173 * @deprecated use commandTopic(String, boolean, int)
174 * @param commandTopic topic
175 * @param retain retain
179 public Builder commandTopic(@Nullable String commandTopic, boolean retain) {
180 this.commandTopic = commandTopic;
181 this.retain = retain;
185 public Builder commandTopic(@Nullable String commandTopic, boolean retain, int qos) {
186 return commandTopic(commandTopic, retain, qos, null);
189 public Builder commandTopic(@Nullable String commandTopic, boolean retain, int qos, @Nullable String template) {
190 this.commandTopic = commandTopic;
191 this.retain = retain;
193 if (commandTopic != null && !commandTopic.isBlank()) {
194 this.templateOut = template;
199 public Builder trigger(boolean trigger) {
200 this.trigger = trigger;
204 public Builder isAdvanced(boolean advanced) {
205 this.isAdvanced = advanced;
209 public Builder withAutoUpdatePolicy(@Nullable AutoUpdatePolicy autoUpdatePolicy) {
210 this.autoUpdatePolicy = autoUpdatePolicy;
214 public Builder commandFilter(@Nullable Predicate<Command> commandFilter) {
215 this.commandFilter = commandFilter;
219 public Builder withFormat(String format) {
220 this.format = format;
224 public ComponentChannel build() {
228 public ComponentChannel build(boolean addToComponent) {
229 ChannelUID channelUID;
230 ChannelState channelState;
233 ChannelTypeUID channelTypeUID;
235 channelUID = component.buildChannelUID(channelID);
236 channelTypeUID = new ChannelTypeUID(MqttBindingConstants.BINDING_ID,
237 channelUID.getGroupId() + "_" + channelID);
238 channelState = new HomeAssistantChannelState(
239 ChannelConfigBuilder.create().withRetain(retain).withQos(qos).withStateTopic(stateTopic)
240 .withCommandTopic(commandTopic).makeTrigger(trigger).withFormatter(format).build(),
241 channelUID, valueState, channelStateUpdateListener, commandFilter);
243 // disabled by default components should always show up as advanced
244 if (!component.isEnabledByDefault()) {
248 ChannelTypeBuilder typeBuilder;
250 typeBuilder = ChannelTypeBuilder.trigger(channelTypeUID, label);
252 StateDescriptionFragment stateDescription = valueState.createStateDescription(commandTopic == null)
254 CommandDescription commandDescription = valueState.createCommandDescription().build();
255 typeBuilder = ChannelTypeBuilder.state(channelTypeUID, label, channelState.getItemType())
256 .withStateDescriptionFragment(stateDescription).withCommandDescription(commandDescription);
258 type = typeBuilder.withConfigDescriptionURI(URI.create(MqttBindingConstants.CONFIG_HA_CHANNEL))
259 .isAdvanced(isAdvanced).build();
261 Configuration configuration = new Configuration();
262 configuration.put("config", component.getChannelConfigurationJson());
263 component.getHaID().toConfig(configuration);
265 channel = ChannelBuilder.create(channelUID, channelState.getItemType()).withType(channelTypeUID)
266 .withKind(type.getKind()).withLabel(label).withConfiguration(configuration)
267 .withAutoUpdatePolicy(autoUpdatePolicy).build();
269 ComponentChannel result = new ComponentChannel(channelUID, channelState, channel, type, channelTypeUID,
270 channelStateUpdateListener);
272 TransformationServiceProvider transformationProvider = component.getTransformationServiceProvider();
274 final String templateIn = this.templateIn;
275 if (templateIn != null && transformationProvider != null) {
277 .addTransformation(new ChannelStateTransformation(JINJA, templateIn, transformationProvider));
279 final String templateOut = this.templateOut;
280 if (templateOut != null && transformationProvider != null) {
281 channelState.addTransformationOut(
282 new ChannelStateTransformation(JINJA, templateOut, transformationProvider));
284 if (addToComponent) {
285 component.getChannelMap().put(channelID, result);