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.component;
15 import java.math.BigDecimal;
16 import java.util.List;
17 import java.util.Objects;
19 import org.eclipse.jdt.annotation.NonNullByDefault;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
22 import org.openhab.binding.mqtt.generic.values.OnOffValue;
23 import org.openhab.binding.mqtt.generic.values.PercentageValue;
24 import org.openhab.binding.mqtt.generic.values.TextValue;
25 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
26 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
27 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
28 import org.openhab.core.library.types.OnOffType;
29 import org.openhab.core.library.types.PercentType;
30 import org.openhab.core.thing.ChannelUID;
31 import org.openhab.core.types.Command;
32 import org.openhab.core.types.State;
33 import org.openhab.core.types.UnDefType;
35 import com.google.gson.annotations.SerializedName;
38 * A MQTT Fan component, following the https://www.home-assistant.io/components/fan.mqtt/ specification.
40 * Only ON/OFF is supported so far.
42 * @author David Graeff - Initial contribution
45 public class Fan extends AbstractComponent<Fan.ChannelConfiguration> implements ChannelStateUpdateListener {
46 public static final String SWITCH_CHANNEL_ID = "fan";
47 public static final String SPEED_CHANNEL_ID = "speed";
48 public static final String PRESET_MODE_CHANNEL_ID = "preset-mode";
49 public static final String OSCILLATION_CHANNEL_ID = "oscillation";
50 public static final String DIRECTION_CHANNEL_ID = "direction";
51 public static final String JSON_ATTRIBUTES_CHANNEL_ID = "json-attributes";
54 * Configuration class for MQTT component
56 static class ChannelConfiguration extends AbstractChannelConfiguration {
57 ChannelConfiguration() {
61 protected @Nullable Boolean optimistic;
63 @SerializedName("state_topic")
64 protected @Nullable String stateTopic;
65 @SerializedName("command_template")
66 protected @Nullable String commandTemplate;
67 @SerializedName("command_topic")
68 protected String commandTopic = "";
69 @SerializedName("direction_command_template")
70 protected @Nullable String directionCommandTemplate;
71 @SerializedName("direction_command_topic")
72 protected @Nullable String directionCommandTopic;
73 @SerializedName("direction_state_topic")
74 protected @Nullable String directionStateTopic;
75 @SerializedName("direction_value_template")
76 protected @Nullable String directionValueTemplate;
77 @SerializedName("oscillation_command_template")
78 protected @Nullable String oscillationCommandTemplate;
79 @SerializedName("oscillation_command_topic")
80 protected @Nullable String oscillationCommandTopic;
81 @SerializedName("oscillation_state_topic")
82 protected @Nullable String oscillationStateTopic;
83 @SerializedName("oscillation_value_template")
84 protected @Nullable String oscillationValueTemplate;
85 @SerializedName("payload_oscillation_off")
86 protected String payloadOscillationOff = "oscillate_off";
87 @SerializedName("payload_oscillation_on")
88 protected String payloadOscillationOn = "oscillate_on";
89 @SerializedName("payload_off")
90 protected String payloadOff = "OFF";
91 @SerializedName("payload_on")
92 protected String payloadOn = "ON";
93 @SerializedName("payload_reset_percentage")
94 protected String payloadResetPercentage = "None";
95 @SerializedName("payload_reset_preset_mode")
96 protected String payloadResetPresetMode = "None";
97 @SerializedName("percentage_command_template")
98 protected @Nullable String percentageCommandTemplate;
99 @SerializedName("percentage_command_topic")
100 protected @Nullable String percentageCommandTopic;
101 @SerializedName("percentage_state_topic")
102 protected @Nullable String percentageStateTopic;
103 @SerializedName("percentage_value_template")
104 protected @Nullable String percentageValueTemplate;
105 @SerializedName("preset_mode_command_template")
106 protected @Nullable String presetModeCommandTemplate;
107 @SerializedName("preset_mode_command_topic")
108 protected @Nullable String presetModeCommandTopic;
109 @SerializedName("preset_mode_state_topic")
110 protected @Nullable String presetModeStateTopic;
111 @SerializedName("preset_mode_value_template")
112 protected @Nullable String presetModeValueTemplate;
113 @SerializedName("preset_modes")
114 protected @Nullable List<String> presetModes;
115 @SerializedName("speed_range_max")
116 protected int speedRangeMax = 100;
117 @SerializedName("speed_range_min")
118 protected int speedRangeMin = 1;
119 @SerializedName("json_attributes_template")
120 protected @Nullable String jsonAttributesTemplate;
121 @SerializedName("json_attributes_topic")
122 protected @Nullable String jsonAttributesTopic;
125 private final OnOffValue onOffValue;
126 private final PercentageValue speedValue;
127 private State rawSpeedState;
128 private final ComponentChannel onOffChannel;
129 private final @Nullable ComponentChannel speedChannel;
130 private final ComponentChannel primaryChannel;
131 private final ChannelStateUpdateListener channelStateUpdateListener;
133 public Fan(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
134 super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
135 this.channelStateUpdateListener = componentConfiguration.getUpdateListener();
137 onOffValue = new OnOffValue(channelConfiguration.payloadOn, channelConfiguration.payloadOff);
138 ChannelStateUpdateListener onOffListener = channelConfiguration.percentageCommandTopic == null
139 ? componentConfiguration.getUpdateListener()
141 onOffChannel = buildChannel(SWITCH_CHANNEL_ID, ComponentChannelType.SWITCH, onOffValue, "On/Off State",
143 .stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
144 .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
145 channelConfiguration.getQos(), channelConfiguration.commandTemplate)
146 .inferOptimistic(channelConfiguration.optimistic)
147 .build(channelConfiguration.percentageCommandTopic == null);
149 rawSpeedState = UnDefType.NULL;
151 int speeds = Math.min(channelConfiguration.speedRangeMax, 100) - Math.max(channelConfiguration.speedRangeMin, 1)
153 speedValue = new PercentageValue(BigDecimal.ZERO, BigDecimal.valueOf(100), BigDecimal.valueOf(100.0d / speeds),
154 channelConfiguration.payloadOn, channelConfiguration.payloadOff);
156 if (channelConfiguration.percentageCommandTopic != null) {
157 hiddenChannels.add(onOffChannel);
158 primaryChannel = speedChannel = buildChannel(SPEED_CHANNEL_ID, ComponentChannelType.DIMMER, speedValue,
160 .stateTopic(channelConfiguration.percentageStateTopic, channelConfiguration.percentageValueTemplate)
161 .commandTopic(channelConfiguration.percentageCommandTopic, channelConfiguration.isRetain(),
162 channelConfiguration.getQos(), channelConfiguration.percentageCommandTemplate)
163 .inferOptimistic(channelConfiguration.optimistic).commandFilter(this::handlePercentageCommand)
166 primaryChannel = onOffChannel;
170 List<String> presetModes = channelConfiguration.presetModes;
171 if (presetModes != null) {
172 TextValue presetModeValue = new TextValue(presetModes.toArray(new String[0]));
173 presetModeValue.setNullValue(channelConfiguration.payloadResetPresetMode);
174 buildChannel(PRESET_MODE_CHANNEL_ID, ComponentChannelType.STRING, presetModeValue, "Preset Mode",
175 componentConfiguration.getUpdateListener())
176 .stateTopic(channelConfiguration.presetModeStateTopic, channelConfiguration.presetModeValueTemplate)
177 .commandTopic(channelConfiguration.presetModeCommandTopic, channelConfiguration.isRetain(),
178 channelConfiguration.getQos(), channelConfiguration.presetModeCommandTemplate)
179 .inferOptimistic(channelConfiguration.optimistic).build();
182 if (channelConfiguration.oscillationCommandTopic != null) {
183 OnOffValue oscillationValue = new OnOffValue(channelConfiguration.payloadOscillationOn,
184 channelConfiguration.payloadOscillationOff);
185 buildChannel(OSCILLATION_CHANNEL_ID, ComponentChannelType.SWITCH, oscillationValue, "Oscillation",
186 componentConfiguration.getUpdateListener())
187 .stateTopic(channelConfiguration.oscillationStateTopic,
188 channelConfiguration.oscillationValueTemplate)
189 .commandTopic(channelConfiguration.oscillationCommandTopic, channelConfiguration.isRetain(),
190 channelConfiguration.getQos(), channelConfiguration.oscillationCommandTemplate)
191 .inferOptimistic(channelConfiguration.optimistic).build();
194 if (channelConfiguration.directionCommandTopic != null) {
195 TextValue directionValue = new TextValue(new String[] { "forward", "backward" });
196 buildChannel(DIRECTION_CHANNEL_ID, ComponentChannelType.STRING, directionValue, "Direction",
197 componentConfiguration.getUpdateListener())
198 .stateTopic(channelConfiguration.directionStateTopic, channelConfiguration.directionValueTemplate)
199 .commandTopic(channelConfiguration.directionCommandTopic, channelConfiguration.isRetain(),
200 channelConfiguration.getQos(), channelConfiguration.directionCommandTemplate)
201 .inferOptimistic(channelConfiguration.optimistic).build();
204 if (channelConfiguration.jsonAttributesTemplate != null) {
205 buildChannel(JSON_ATTRIBUTES_CHANNEL_ID, ComponentChannelType.STRING, new TextValue(), "JSON Attributes",
206 componentConfiguration.getUpdateListener())
207 .stateTopic(channelConfiguration.jsonAttributesTopic, channelConfiguration.jsonAttributesTemplate)
214 private boolean handlePercentageCommand(Command command) {
215 // ON/OFF go to the regular command topic, not the percentage topic
216 if (command.equals(OnOffType.ON) || command.equals(OnOffType.OFF)) {
217 onOffChannel.getState().publishValue(command);
224 public void updateChannelState(ChannelUID channel, State state) {
225 if (onOffChannel.getChannel().getUID().equals(channel)) {
226 if (rawSpeedState instanceof UnDefType && state.equals(OnOffType.ON)) {
227 // Assume full on if we don't yet know the actual speed
228 state = PercentType.HUNDRED;
229 } else if (state.equals(OnOffType.OFF)) {
230 state = PercentType.ZERO;
232 state = rawSpeedState;
234 } else if (Objects.requireNonNull(speedChannel).getChannel().getUID().equals(channel)) {
235 rawSpeedState = state;
236 if (onOffValue.getChannelState().equals(OnOffType.OFF)) {
237 // Don't pass on percentage values while the fan is off
238 state = PercentType.ZERO;
241 speedValue.update(state);
242 channelStateUpdateListener.updateChannelState(primaryChannel.getChannel().getUID(), state);
246 public void postChannelCommand(ChannelUID channelUID, Command value) {
247 throw new UnsupportedOperationException();
251 public void triggerChannel(ChannelUID channelUID, String eventPayload) {
252 throw new UnsupportedOperationException();