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;
18 import org.eclipse.jdt.annotation.NonNullByDefault;
19 import org.eclipse.jdt.annotation.Nullable;
20 import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
21 import org.openhab.binding.mqtt.generic.values.OnOffValue;
22 import org.openhab.binding.mqtt.generic.values.PercentageValue;
23 import org.openhab.binding.mqtt.generic.values.TextValue;
24 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
25 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
26 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
27 import org.openhab.core.library.types.OnOffType;
28 import org.openhab.core.library.types.PercentType;
29 import org.openhab.core.thing.ChannelUID;
30 import org.openhab.core.types.Command;
31 import org.openhab.core.types.State;
32 import org.openhab.core.types.UnDefType;
34 import com.google.gson.annotations.SerializedName;
37 * A MQTT Fan component, following the https://www.home-assistant.io/components/fan.mqtt/ specification.
39 * Only ON/OFF is supported so far.
41 * @author David Graeff - Initial contribution
44 public class Fan extends AbstractComponent<Fan.ChannelConfiguration> implements ChannelStateUpdateListener {
45 public static final String SWITCH_CHANNEL_ID = "fan";
46 public static final String SPEED_CHANNEL_ID = "speed";
47 public static final String PRESET_MODE_CHANNEL_ID = "preset_mode";
48 public static final String OSCILLATION_CHANNEL_ID = "oscillation";
49 public static final String DIRECTION_CHANNEL_ID = "direction";
52 * Configuration class for MQTT component
54 static class ChannelConfiguration extends AbstractChannelConfiguration {
55 ChannelConfiguration() {
59 @SerializedName("state_topic")
60 protected @Nullable String stateTopic;
61 @SerializedName("command_template")
62 protected @Nullable String commandTemplate;
63 @SerializedName("command_topic")
64 protected String commandTopic = "";
65 @SerializedName("direction_command_template")
66 protected @Nullable String directionCommandTemplate;
67 @SerializedName("direction_command_topic")
68 protected @Nullable String directionCommandTopic;
69 @SerializedName("direction_state_topic")
70 protected @Nullable String directionStateTopic;
71 @SerializedName("direction_value_template")
72 protected @Nullable String directionValueTemplate;
73 @SerializedName("oscillation_command_template")
74 protected @Nullable String oscillationCommandTemplate;
75 @SerializedName("oscillation_command_topic")
76 protected @Nullable String oscillationCommandTopic;
77 @SerializedName("oscillation_state_topic")
78 protected @Nullable String oscillationStateTopic;
79 @SerializedName("oscillation_value_template")
80 protected @Nullable String oscillationValueTemplate;
81 @SerializedName("payload_oscillation_off")
82 protected String payloadOscillationOff = "oscillate_off";
83 @SerializedName("payload_oscillation_on")
84 protected String payloadOscillationOn = "oscillate_on";
85 @SerializedName("payload_off")
86 protected String payloadOff = "OFF";
87 @SerializedName("payload_on")
88 protected String payloadOn = "ON";
89 @SerializedName("payload_reset_percentage")
90 protected String payloadResetPercentage = "None";
91 @SerializedName("payload_reset_preset_mode")
92 protected String payloadResetPresetMode = "None";
93 @SerializedName("percentage_command_template")
94 protected @Nullable String percentageCommandTemplate;
95 @SerializedName("percentage_command_topic")
96 protected @Nullable String percentageCommandTopic;
97 @SerializedName("percentage_state_topic")
98 protected @Nullable String percentageStateTopic;
99 @SerializedName("percentage_value_template")
100 protected @Nullable String percentageValueTemplate;
101 @SerializedName("preset_mode_command_template")
102 protected @Nullable String presetModeCommandTemplate;
103 @SerializedName("preset_mode_command_topic")
104 protected @Nullable String presetModeCommandTopic;
105 @SerializedName("preset_mode_state_topic")
106 protected @Nullable String presetModeStateTopic;
107 @SerializedName("preset_mode_value_template")
108 protected @Nullable String presetModeValueTemplate;
109 @SerializedName("preset_modes")
110 protected @Nullable List<String> presetModes;
111 @SerializedName("speed_range_max")
112 protected int speedRangeMax = 100;
113 @SerializedName("speed_range_min")
114 protected int speedRangeMin = 1;
117 private final OnOffValue onOffValue;
118 private final PercentageValue speedValue;
119 private State rawSpeedState;
120 private final ComponentChannel onOffChannel;
121 private final ChannelStateUpdateListener channelStateUpdateListener;
123 public Fan(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
124 super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
125 this.channelStateUpdateListener = componentConfiguration.getUpdateListener();
127 onOffValue = new OnOffValue(channelConfiguration.payloadOn, channelConfiguration.payloadOff);
128 ChannelStateUpdateListener onOffListener = channelConfiguration.percentageCommandTopic == null
129 ? componentConfiguration.getUpdateListener()
131 onOffChannel = buildChannel(SWITCH_CHANNEL_ID, ComponentChannelType.SWITCH, onOffValue, "On/Off State",
133 .stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
134 .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
135 channelConfiguration.getQos(), channelConfiguration.commandTemplate)
136 .build(channelConfiguration.percentageCommandTopic == null);
138 rawSpeedState = UnDefType.NULL;
140 int speeds = Math.min(channelConfiguration.speedRangeMax, 100) - Math.max(channelConfiguration.speedRangeMin, 1)
142 speedValue = new PercentageValue(BigDecimal.ZERO, BigDecimal.valueOf(100), BigDecimal.valueOf(100.0d / speeds),
143 channelConfiguration.payloadOn, channelConfiguration.payloadOff);
145 if (channelConfiguration.percentageCommandTopic != null) {
146 hiddenChannels.add(onOffChannel);
147 buildChannel(SPEED_CHANNEL_ID, ComponentChannelType.DIMMER, speedValue, "Speed", this)
148 .stateTopic(channelConfiguration.percentageStateTopic, channelConfiguration.percentageValueTemplate)
149 .commandTopic(channelConfiguration.percentageCommandTopic, channelConfiguration.isRetain(),
150 channelConfiguration.getQos(), channelConfiguration.percentageCommandTemplate)
151 .commandFilter(this::handlePercentageCommand).build();
154 List<String> presetModes = channelConfiguration.presetModes;
155 if (presetModes != null) {
156 TextValue presetModeValue = new TextValue(presetModes.toArray(new String[0]));
157 presetModeValue.setNullValue(channelConfiguration.payloadResetPresetMode);
158 buildChannel(PRESET_MODE_CHANNEL_ID, ComponentChannelType.STRING, presetModeValue, "Preset Mode",
159 componentConfiguration.getUpdateListener())
160 .stateTopic(channelConfiguration.presetModeStateTopic, channelConfiguration.presetModeValueTemplate)
161 .commandTopic(channelConfiguration.presetModeCommandTopic, channelConfiguration.isRetain(),
162 channelConfiguration.getQos(), channelConfiguration.presetModeCommandTemplate)
166 if (channelConfiguration.oscillationCommandTopic != null) {
167 OnOffValue oscillationValue = new OnOffValue(channelConfiguration.payloadOscillationOn,
168 channelConfiguration.payloadOscillationOff);
169 buildChannel(OSCILLATION_CHANNEL_ID, ComponentChannelType.SWITCH, oscillationValue, "Oscillation",
170 componentConfiguration.getUpdateListener())
171 .stateTopic(channelConfiguration.oscillationStateTopic,
172 channelConfiguration.oscillationValueTemplate)
173 .commandTopic(channelConfiguration.oscillationCommandTopic, channelConfiguration.isRetain(),
174 channelConfiguration.getQos(), channelConfiguration.oscillationCommandTemplate)
178 if (channelConfiguration.directionCommandTopic != null) {
179 TextValue directionValue = new TextValue(new String[] { "forward", "backward" });
180 buildChannel(DIRECTION_CHANNEL_ID, ComponentChannelType.STRING, directionValue, "Direction",
181 componentConfiguration.getUpdateListener())
182 .stateTopic(channelConfiguration.directionStateTopic, channelConfiguration.directionValueTemplate)
183 .commandTopic(channelConfiguration.directionCommandTopic, channelConfiguration.isRetain(),
184 channelConfiguration.getQos(), channelConfiguration.directionCommandTemplate)
189 private boolean handlePercentageCommand(Command command) {
190 // ON/OFF go to the regular command topic, not the percentage topic
191 if (command.equals(OnOffType.ON) || command.equals(OnOffType.OFF)) {
192 onOffChannel.getState().publishValue(command);
199 public void updateChannelState(ChannelUID channel, State state) {
200 if (channel.getIdWithoutGroup().equals(SWITCH_CHANNEL_ID)) {
201 if (rawSpeedState instanceof UnDefType && state.equals(OnOffType.ON)) {
202 // Assume full on if we don't yet know the actual speed
203 state = PercentType.HUNDRED;
204 } else if (state.equals(OnOffType.OFF)) {
205 state = PercentType.ZERO;
207 state = rawSpeedState;
209 } else if (channel.getIdWithoutGroup().equals(SPEED_CHANNEL_ID)) {
210 rawSpeedState = state;
211 if (onOffValue.getChannelState().equals(OnOffType.OFF)) {
212 // Don't pass on percentage values while the fan is off
213 state = PercentType.ZERO;
216 speedValue.update(state);
217 channelStateUpdateListener.updateChannelState(buildChannelUID(SPEED_CHANNEL_ID), state);
221 public void postChannelCommand(ChannelUID channelUID, Command value) {
222 throw new UnsupportedOperationException();
226 public void triggerChannel(ChannelUID channelUID, String eventPayload) {
227 throw new UnsupportedOperationException();