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";
53 * Configuration class for MQTT component
55 static class ChannelConfiguration extends AbstractChannelConfiguration {
56 ChannelConfiguration() {
60 protected @Nullable Boolean optimistic;
62 @SerializedName("state_topic")
63 protected @Nullable String stateTopic;
64 @SerializedName("command_template")
65 protected @Nullable String commandTemplate;
66 @SerializedName("command_topic")
67 protected String commandTopic = "";
68 @SerializedName("direction_command_template")
69 protected @Nullable String directionCommandTemplate;
70 @SerializedName("direction_command_topic")
71 protected @Nullable String directionCommandTopic;
72 @SerializedName("direction_state_topic")
73 protected @Nullable String directionStateTopic;
74 @SerializedName("direction_value_template")
75 protected @Nullable String directionValueTemplate;
76 @SerializedName("oscillation_command_template")
77 protected @Nullable String oscillationCommandTemplate;
78 @SerializedName("oscillation_command_topic")
79 protected @Nullable String oscillationCommandTopic;
80 @SerializedName("oscillation_state_topic")
81 protected @Nullable String oscillationStateTopic;
82 @SerializedName("oscillation_value_template")
83 protected @Nullable String oscillationValueTemplate;
84 @SerializedName("payload_oscillation_off")
85 protected String payloadOscillationOff = "oscillate_off";
86 @SerializedName("payload_oscillation_on")
87 protected String payloadOscillationOn = "oscillate_on";
88 @SerializedName("payload_off")
89 protected String payloadOff = "OFF";
90 @SerializedName("payload_on")
91 protected String payloadOn = "ON";
92 @SerializedName("payload_reset_percentage")
93 protected String payloadResetPercentage = "None";
94 @SerializedName("payload_reset_preset_mode")
95 protected String payloadResetPresetMode = "None";
96 @SerializedName("percentage_command_template")
97 protected @Nullable String percentageCommandTemplate;
98 @SerializedName("percentage_command_topic")
99 protected @Nullable String percentageCommandTopic;
100 @SerializedName("percentage_state_topic")
101 protected @Nullable String percentageStateTopic;
102 @SerializedName("percentage_value_template")
103 protected @Nullable String percentageValueTemplate;
104 @SerializedName("preset_mode_command_template")
105 protected @Nullable String presetModeCommandTemplate;
106 @SerializedName("preset_mode_command_topic")
107 protected @Nullable String presetModeCommandTopic;
108 @SerializedName("preset_mode_state_topic")
109 protected @Nullable String presetModeStateTopic;
110 @SerializedName("preset_mode_value_template")
111 protected @Nullable String presetModeValueTemplate;
112 @SerializedName("preset_modes")
113 protected @Nullable List<String> presetModes;
114 @SerializedName("speed_range_max")
115 protected int speedRangeMax = 100;
116 @SerializedName("speed_range_min")
117 protected int speedRangeMin = 1;
120 private final OnOffValue onOffValue;
121 private final PercentageValue speedValue;
122 private State rawSpeedState;
123 private final ComponentChannel onOffChannel;
124 private final @Nullable ComponentChannel speedChannel;
125 private final ComponentChannel primaryChannel;
126 private final ChannelStateUpdateListener channelStateUpdateListener;
128 public Fan(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
129 super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
130 this.channelStateUpdateListener = componentConfiguration.getUpdateListener();
132 onOffValue = new OnOffValue(channelConfiguration.payloadOn, channelConfiguration.payloadOff);
133 ChannelStateUpdateListener onOffListener = channelConfiguration.percentageCommandTopic == null
134 ? componentConfiguration.getUpdateListener()
136 onOffChannel = buildChannel(SWITCH_CHANNEL_ID, ComponentChannelType.SWITCH, onOffValue, "On/Off State",
138 .stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
139 .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
140 channelConfiguration.getQos(), channelConfiguration.commandTemplate)
141 .inferOptimistic(channelConfiguration.optimistic)
142 .build(channelConfiguration.percentageCommandTopic == null);
144 rawSpeedState = UnDefType.NULL;
146 int speeds = Math.min(channelConfiguration.speedRangeMax, 100) - Math.max(channelConfiguration.speedRangeMin, 1)
148 speedValue = new PercentageValue(BigDecimal.ZERO, BigDecimal.valueOf(100), BigDecimal.valueOf(100.0d / speeds),
149 channelConfiguration.payloadOn, channelConfiguration.payloadOff);
151 if (channelConfiguration.percentageCommandTopic != null) {
152 hiddenChannels.add(onOffChannel);
153 primaryChannel = speedChannel = buildChannel(SPEED_CHANNEL_ID, ComponentChannelType.DIMMER, speedValue,
155 .stateTopic(channelConfiguration.percentageStateTopic, channelConfiguration.percentageValueTemplate)
156 .commandTopic(channelConfiguration.percentageCommandTopic, channelConfiguration.isRetain(),
157 channelConfiguration.getQos(), channelConfiguration.percentageCommandTemplate)
158 .inferOptimistic(channelConfiguration.optimistic).commandFilter(this::handlePercentageCommand)
161 primaryChannel = onOffChannel;
165 List<String> presetModes = channelConfiguration.presetModes;
166 if (presetModes != null) {
167 TextValue presetModeValue = new TextValue(presetModes.toArray(new String[0]));
168 presetModeValue.setNullValue(channelConfiguration.payloadResetPresetMode);
169 buildChannel(PRESET_MODE_CHANNEL_ID, ComponentChannelType.STRING, presetModeValue, "Preset Mode",
170 componentConfiguration.getUpdateListener())
171 .stateTopic(channelConfiguration.presetModeStateTopic, channelConfiguration.presetModeValueTemplate)
172 .commandTopic(channelConfiguration.presetModeCommandTopic, channelConfiguration.isRetain(),
173 channelConfiguration.getQos(), channelConfiguration.presetModeCommandTemplate)
174 .inferOptimistic(channelConfiguration.optimistic).build();
177 if (channelConfiguration.oscillationCommandTopic != null) {
178 OnOffValue oscillationValue = new OnOffValue(channelConfiguration.payloadOscillationOn,
179 channelConfiguration.payloadOscillationOff);
180 buildChannel(OSCILLATION_CHANNEL_ID, ComponentChannelType.SWITCH, oscillationValue, "Oscillation",
181 componentConfiguration.getUpdateListener())
182 .stateTopic(channelConfiguration.oscillationStateTopic,
183 channelConfiguration.oscillationValueTemplate)
184 .commandTopic(channelConfiguration.oscillationCommandTopic, channelConfiguration.isRetain(),
185 channelConfiguration.getQos(), channelConfiguration.oscillationCommandTemplate)
186 .inferOptimistic(channelConfiguration.optimistic).build();
189 if (channelConfiguration.directionCommandTopic != null) {
190 TextValue directionValue = new TextValue(new String[] { "forward", "backward" });
191 buildChannel(DIRECTION_CHANNEL_ID, ComponentChannelType.STRING, directionValue, "Direction",
192 componentConfiguration.getUpdateListener())
193 .stateTopic(channelConfiguration.directionStateTopic, channelConfiguration.directionValueTemplate)
194 .commandTopic(channelConfiguration.directionCommandTopic, channelConfiguration.isRetain(),
195 channelConfiguration.getQos(), channelConfiguration.directionCommandTemplate)
196 .inferOptimistic(channelConfiguration.optimistic).build();
201 private boolean handlePercentageCommand(Command command) {
202 // ON/OFF go to the regular command topic, not the percentage topic
203 if (command.equals(OnOffType.ON) || command.equals(OnOffType.OFF)) {
204 onOffChannel.getState().publishValue(command);
211 public void updateChannelState(ChannelUID channel, State state) {
212 if (onOffChannel.getChannel().getUID().equals(channel)) {
213 if (rawSpeedState instanceof UnDefType && state.equals(OnOffType.ON)) {
214 // Assume full on if we don't yet know the actual speed
215 state = PercentType.HUNDRED;
216 } else if (state.equals(OnOffType.OFF)) {
217 state = PercentType.ZERO;
219 state = rawSpeedState;
221 } else if (Objects.requireNonNull(speedChannel).getChannel().getUID().equals(channel)) {
222 rawSpeedState = state;
223 if (onOffValue.getChannelState().equals(OnOffType.OFF)) {
224 // Don't pass on percentage values while the fan is off
225 state = PercentType.ZERO;
228 speedValue.update(state);
229 channelStateUpdateListener.updateChannelState(primaryChannel.getChannel().getUID(), state);
233 public void postChannelCommand(ChannelUID channelUID, Command value) {
234 throw new UnsupportedOperationException();
238 public void triggerChannel(ChannelUID channelUID, String eventPayload) {
239 throw new UnsupportedOperationException();