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