Skip to content

Commit 6953c8b

Browse files
authored
[mqtt.homeassistant] Fully implement Fan component (openhab#17402)
Signed-off-by: Cody Cutrer <[email protected]>
1 parent fd70c5d commit 6953c8b

File tree

3 files changed

+295
-14
lines changed
  • bundles/org.openhab.binding.mqtt.homeassistant
    • src
      • main/java/org/openhab/binding/mqtt/homeassistant/internal/component
      • test/java/org/openhab/binding/mqtt/homeassistant/internal/component

3 files changed

+295
-14
lines changed

bundles/org.openhab.binding.mqtt.homeassistant/README.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ You can also manually create a Thing, and provide the individual component topic
2222
- [Cover](https://www.home-assistant.io/integrations/cover.mqtt/)
2323
- [Device Trigger](https://www.home-assistant.io/integrations/device_trigger.mqtt/)
2424
- [Fan](https://www.home-assistant.io/integrations/fan.mqtt/)<br>
25-
Only ON/OFF is supported. JSON attributes are not supported.
26-
- [Light](https://www.home-assistant.io/integrations/light.mqtt/)<br>
27-
Template schema is not supported. Command templates only have access to the `value` variable.
25+
JSON attributes are not supported.
26+
- [Light](https://www.home-assistant.io/integrations/light.mqtt/)
2827
- [Lock](https://www.home-assistant.io/integrations/lock.mqtt/)
2928
- [Number](https://www.home-assistant.io/integrations/number.mqtt/)
3029
- [Scene](https://www.home-assistant.io/integrations/scene.mqtt/)
3130
- [Select](https://www.home-assistant.io/integrations/select.mqtt/)
32-
- [Sensor](https://www.home-assistant.io/integrations/sensor.mqtt/)
31+
- [Sensor](https://www.home-assistant.io/integrations/sensor.mqtt/)<br>
32+
JSON attributes are not supported.
3333
- [Switch](https://www.home-assistant.io/integrations/switch.mqtt/)
3434
- [Update](https://www.home-assistant.io/integrations/update.mqtt/)<br>
3535
This is a special component, that will show up as additional properties on the Thing, and add a button on the Thing to initiate an OTA update.

bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Fan.java

+172-8
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,24 @@
1212
*/
1313
package org.openhab.binding.mqtt.homeassistant.internal.component;
1414

15+
import java.math.BigDecimal;
16+
import java.util.List;
17+
1518
import org.eclipse.jdt.annotation.NonNullByDefault;
1619
import org.eclipse.jdt.annotation.Nullable;
20+
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
1721
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;
1825
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
1926
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;
2033

2134
import com.google.gson.annotations.SerializedName;
2235

@@ -28,8 +41,12 @@
2841
* @author David Graeff - Initial contribution
2942
*/
3043
@NonNullByDefault
31-
public class Fan extends AbstractComponent<Fan.ChannelConfiguration> {
32-
public static final String SWITCH_CHANNEL_ID = "fan"; // Randomly chosen channel "ID"
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";
3350

3451
/**
3552
* Configuration class for MQTT component
@@ -45,21 +62,168 @@ static class ChannelConfiguration extends AbstractChannelConfiguration {
4562
protected @Nullable String commandTemplate;
4663
@SerializedName("command_topic")
4764
protected String commandTopic = "";
48-
@SerializedName("payload_on")
49-
protected String payloadOn = "ON";
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";
5085
@SerializedName("payload_off")
5186
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;
52115
}
53116

117+
private final OnOffValue onOffValue;
118+
private final PercentageValue speedValue;
119+
private State rawSpeedState;
120+
private final ComponentChannel onOffChannel;
121+
private final ChannelStateUpdateListener channelStateUpdateListener;
122+
54123
public Fan(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
55124
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
125+
this.channelStateUpdateListener = componentConfiguration.getUpdateListener();
56126

57-
OnOffValue value = new OnOffValue(channelConfiguration.payloadOn, channelConfiguration.payloadOff);
58-
buildChannel(SWITCH_CHANNEL_ID, ComponentChannelType.SWITCH, value, getName(),
59-
componentConfiguration.getUpdateListener())
127+
onOffValue = new OnOffValue(channelConfiguration.payloadOn, channelConfiguration.payloadOff);
128+
ChannelStateUpdateListener onOffListener = channelConfiguration.percentageCommandTopic == null
129+
? componentConfiguration.getUpdateListener()
130+
: this;
131+
onOffChannel = buildChannel(SWITCH_CHANNEL_ID, ComponentChannelType.SWITCH, onOffValue, "On/Off State",
132+
onOffListener)
60133
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
61134
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
62135
channelConfiguration.getQos(), channelConfiguration.commandTemplate)
63-
.build();
136+
.build(channelConfiguration.percentageCommandTopic == null);
137+
138+
rawSpeedState = UnDefType.NULL;
139+
140+
int speeds = Math.min(channelConfiguration.speedRangeMax, 100) - Math.max(channelConfiguration.speedRangeMin, 1)
141+
+ 1;
142+
speedValue = new PercentageValue(BigDecimal.ZERO, BigDecimal.valueOf(100), BigDecimal.valueOf(100.0d / speeds),
143+
channelConfiguration.payloadOn, channelConfiguration.payloadOff);
144+
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();
152+
}
153+
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)
163+
.build();
164+
}
165+
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)
175+
.build();
176+
}
177+
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)
185+
.build();
186+
}
187+
}
188+
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);
193+
return false;
194+
}
195+
return true;
196+
}
197+
198+
@Override
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;
206+
} else {
207+
state = rawSpeedState;
208+
}
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;
214+
}
215+
}
216+
speedValue.update(state);
217+
channelStateUpdateListener.updateChannelState(buildChannelUID(SPEED_CHANNEL_ID), state);
218+
}
219+
220+
@Override
221+
public void postChannelCommand(ChannelUID channelUID, Command value) {
222+
throw new UnsupportedOperationException();
223+
}
224+
225+
@Override
226+
public void triggerChannel(ChannelUID channelUID, String eventPayload) {
227+
throw new UnsupportedOperationException();
64228
}
65229
}

0 commit comments

Comments
 (0)