mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[mqtt.homeassistant] Fully implement Fan component (#17402)
Signed-off-by: Cody Cutrer <cody@cutrer.us> Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
parent
c35aa6a147
commit
a4d083281a
@ -22,14 +22,14 @@ You can also manually create a Thing, and provide the individual component topic
|
||||
- [Cover](https://www.home-assistant.io/integrations/cover.mqtt/)
|
||||
- [Device Trigger](https://www.home-assistant.io/integrations/device_trigger.mqtt/)
|
||||
- [Fan](https://www.home-assistant.io/integrations/fan.mqtt/)<br>
|
||||
Only ON/OFF is supported. JSON attributes are not supported.
|
||||
- [Light](https://www.home-assistant.io/integrations/light.mqtt/)<br>
|
||||
Template schema is not supported. Command templates only have access to the `value` variable.
|
||||
JSON attributes are not supported.
|
||||
- [Light](https://www.home-assistant.io/integrations/light.mqtt/)
|
||||
- [Lock](https://www.home-assistant.io/integrations/lock.mqtt/)
|
||||
- [Number](https://www.home-assistant.io/integrations/number.mqtt/)
|
||||
- [Scene](https://www.home-assistant.io/integrations/scene.mqtt/)
|
||||
- [Select](https://www.home-assistant.io/integrations/select.mqtt/)
|
||||
- [Sensor](https://www.home-assistant.io/integrations/sensor.mqtt/)
|
||||
- [Sensor](https://www.home-assistant.io/integrations/sensor.mqtt/)<br>
|
||||
JSON attributes are not supported.
|
||||
- [Switch](https://www.home-assistant.io/integrations/switch.mqtt/)
|
||||
- [Update](https://www.home-assistant.io/integrations/update.mqtt/)<br>
|
||||
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.
|
||||
|
@ -12,11 +12,24 @@
|
||||
*/
|
||||
package org.openhab.binding.mqtt.homeassistant.internal.component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
|
||||
import org.openhab.binding.mqtt.generic.values.OnOffValue;
|
||||
import org.openhab.binding.mqtt.generic.values.PercentageValue;
|
||||
import org.openhab.binding.mqtt.generic.values.TextValue;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
@ -28,8 +41,12 @@ import com.google.gson.annotations.SerializedName;
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Fan extends AbstractComponent<Fan.ChannelConfiguration> {
|
||||
public static final String SWITCH_CHANNEL_ID = "fan"; // Randomly chosen channel "ID"
|
||||
public class Fan extends AbstractComponent<Fan.ChannelConfiguration> implements ChannelStateUpdateListener {
|
||||
public static final String SWITCH_CHANNEL_ID = "fan";
|
||||
public static final String SPEED_CHANNEL_ID = "speed";
|
||||
public static final String PRESET_MODE_CHANNEL_ID = "preset_mode";
|
||||
public static final String OSCILLATION_CHANNEL_ID = "oscillation";
|
||||
public static final String DIRECTION_CHANNEL_ID = "direction";
|
||||
|
||||
/**
|
||||
* Configuration class for MQTT component
|
||||
@ -45,21 +62,168 @@ public class Fan extends AbstractComponent<Fan.ChannelConfiguration> {
|
||||
protected @Nullable String commandTemplate;
|
||||
@SerializedName("command_topic")
|
||||
protected String commandTopic = "";
|
||||
@SerializedName("payload_on")
|
||||
protected String payloadOn = "ON";
|
||||
@SerializedName("direction_command_template")
|
||||
protected @Nullable String directionCommandTemplate;
|
||||
@SerializedName("direction_command_topic")
|
||||
protected @Nullable String directionCommandTopic;
|
||||
@SerializedName("direction_state_topic")
|
||||
protected @Nullable String directionStateTopic;
|
||||
@SerializedName("direction_value_template")
|
||||
protected @Nullable String directionValueTemplate;
|
||||
@SerializedName("oscillation_command_template")
|
||||
protected @Nullable String oscillationCommandTemplate;
|
||||
@SerializedName("oscillation_command_topic")
|
||||
protected @Nullable String oscillationCommandTopic;
|
||||
@SerializedName("oscillation_state_topic")
|
||||
protected @Nullable String oscillationStateTopic;
|
||||
@SerializedName("oscillation_value_template")
|
||||
protected @Nullable String oscillationValueTemplate;
|
||||
@SerializedName("payload_oscillation_off")
|
||||
protected String payloadOscillationOff = "oscillate_off";
|
||||
@SerializedName("payload_oscillation_on")
|
||||
protected String payloadOscillationOn = "oscillate_on";
|
||||
@SerializedName("payload_off")
|
||||
protected String payloadOff = "OFF";
|
||||
@SerializedName("payload_on")
|
||||
protected String payloadOn = "ON";
|
||||
@SerializedName("payload_reset_percentage")
|
||||
protected String payloadResetPercentage = "None";
|
||||
@SerializedName("payload_reset_preset_mode")
|
||||
protected String payloadResetPresetMode = "None";
|
||||
@SerializedName("percentage_command_template")
|
||||
protected @Nullable String percentageCommandTemplate;
|
||||
@SerializedName("percentage_command_topic")
|
||||
protected @Nullable String percentageCommandTopic;
|
||||
@SerializedName("percentage_state_topic")
|
||||
protected @Nullable String percentageStateTopic;
|
||||
@SerializedName("percentage_value_template")
|
||||
protected @Nullable String percentageValueTemplate;
|
||||
@SerializedName("preset_mode_command_template")
|
||||
protected @Nullable String presetModeCommandTemplate;
|
||||
@SerializedName("preset_mode_command_topic")
|
||||
protected @Nullable String presetModeCommandTopic;
|
||||
@SerializedName("preset_mode_state_topic")
|
||||
protected @Nullable String presetModeStateTopic;
|
||||
@SerializedName("preset_mode_value_template")
|
||||
protected @Nullable String presetModeValueTemplate;
|
||||
@SerializedName("preset_modes")
|
||||
protected @Nullable List<String> presetModes;
|
||||
@SerializedName("speed_range_max")
|
||||
protected int speedRangeMax = 100;
|
||||
@SerializedName("speed_range_min")
|
||||
protected int speedRangeMin = 1;
|
||||
}
|
||||
|
||||
private final OnOffValue onOffValue;
|
||||
private final PercentageValue speedValue;
|
||||
private State rawSpeedState;
|
||||
private final ComponentChannel onOffChannel;
|
||||
private final ChannelStateUpdateListener channelStateUpdateListener;
|
||||
|
||||
public Fan(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
|
||||
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
|
||||
this.channelStateUpdateListener = componentConfiguration.getUpdateListener();
|
||||
|
||||
OnOffValue value = new OnOffValue(channelConfiguration.payloadOn, channelConfiguration.payloadOff);
|
||||
buildChannel(SWITCH_CHANNEL_ID, ComponentChannelType.SWITCH, value, getName(),
|
||||
componentConfiguration.getUpdateListener())
|
||||
onOffValue = new OnOffValue(channelConfiguration.payloadOn, channelConfiguration.payloadOff);
|
||||
ChannelStateUpdateListener onOffListener = channelConfiguration.percentageCommandTopic == null
|
||||
? componentConfiguration.getUpdateListener()
|
||||
: this;
|
||||
onOffChannel = buildChannel(SWITCH_CHANNEL_ID, ComponentChannelType.SWITCH, onOffValue, "On/Off State",
|
||||
onOffListener)
|
||||
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
|
||||
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
|
||||
channelConfiguration.getQos(), channelConfiguration.commandTemplate)
|
||||
.build();
|
||||
.build(channelConfiguration.percentageCommandTopic == null);
|
||||
|
||||
rawSpeedState = UnDefType.NULL;
|
||||
|
||||
int speeds = Math.min(channelConfiguration.speedRangeMax, 100) - Math.max(channelConfiguration.speedRangeMin, 1)
|
||||
+ 1;
|
||||
speedValue = new PercentageValue(BigDecimal.ZERO, BigDecimal.valueOf(100), BigDecimal.valueOf(100.0d / speeds),
|
||||
channelConfiguration.payloadOn, channelConfiguration.payloadOff);
|
||||
|
||||
if (channelConfiguration.percentageCommandTopic != null) {
|
||||
hiddenChannels.add(onOffChannel);
|
||||
buildChannel(SPEED_CHANNEL_ID, ComponentChannelType.DIMMER, speedValue, "Speed", this)
|
||||
.stateTopic(channelConfiguration.percentageStateTopic, channelConfiguration.percentageValueTemplate)
|
||||
.commandTopic(channelConfiguration.percentageCommandTopic, channelConfiguration.isRetain(),
|
||||
channelConfiguration.getQos(), channelConfiguration.percentageCommandTemplate)
|
||||
.commandFilter(this::handlePercentageCommand).build();
|
||||
}
|
||||
|
||||
List<String> presetModes = channelConfiguration.presetModes;
|
||||
if (presetModes != null) {
|
||||
TextValue presetModeValue = new TextValue(presetModes.toArray(new String[0]));
|
||||
presetModeValue.setNullValue(channelConfiguration.payloadResetPresetMode);
|
||||
buildChannel(PRESET_MODE_CHANNEL_ID, ComponentChannelType.STRING, presetModeValue, "Preset Mode",
|
||||
componentConfiguration.getUpdateListener())
|
||||
.stateTopic(channelConfiguration.presetModeStateTopic, channelConfiguration.presetModeValueTemplate)
|
||||
.commandTopic(channelConfiguration.presetModeCommandTopic, channelConfiguration.isRetain(),
|
||||
channelConfiguration.getQos(), channelConfiguration.presetModeCommandTemplate)
|
||||
.build();
|
||||
}
|
||||
|
||||
if (channelConfiguration.oscillationCommandTopic != null) {
|
||||
OnOffValue oscillationValue = new OnOffValue(channelConfiguration.payloadOscillationOn,
|
||||
channelConfiguration.payloadOscillationOff);
|
||||
buildChannel(OSCILLATION_CHANNEL_ID, ComponentChannelType.SWITCH, oscillationValue, "Oscillation",
|
||||
componentConfiguration.getUpdateListener())
|
||||
.stateTopic(channelConfiguration.oscillationStateTopic,
|
||||
channelConfiguration.oscillationValueTemplate)
|
||||
.commandTopic(channelConfiguration.oscillationCommandTopic, channelConfiguration.isRetain(),
|
||||
channelConfiguration.getQos(), channelConfiguration.oscillationCommandTemplate)
|
||||
.build();
|
||||
}
|
||||
|
||||
if (channelConfiguration.directionCommandTopic != null) {
|
||||
TextValue directionValue = new TextValue(new String[] { "forward", "backward" });
|
||||
buildChannel(DIRECTION_CHANNEL_ID, ComponentChannelType.STRING, directionValue, "Direction",
|
||||
componentConfiguration.getUpdateListener())
|
||||
.stateTopic(channelConfiguration.directionStateTopic, channelConfiguration.directionValueTemplate)
|
||||
.commandTopic(channelConfiguration.directionCommandTopic, channelConfiguration.isRetain(),
|
||||
channelConfiguration.getQos(), channelConfiguration.directionCommandTemplate)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean handlePercentageCommand(Command command) {
|
||||
// ON/OFF go to the regular command topic, not the percentage topic
|
||||
if (command.equals(OnOffType.ON) || command.equals(OnOffType.OFF)) {
|
||||
onOffChannel.getState().publishValue(command);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateChannelState(ChannelUID channel, State state) {
|
||||
if (channel.getIdWithoutGroup().equals(SWITCH_CHANNEL_ID)) {
|
||||
if (rawSpeedState instanceof UnDefType && state.equals(OnOffType.ON)) {
|
||||
// Assume full on if we don't yet know the actual speed
|
||||
state = PercentType.HUNDRED;
|
||||
} else if (state.equals(OnOffType.OFF)) {
|
||||
state = PercentType.ZERO;
|
||||
} else {
|
||||
state = rawSpeedState;
|
||||
}
|
||||
} else if (channel.getIdWithoutGroup().equals(SPEED_CHANNEL_ID)) {
|
||||
rawSpeedState = state;
|
||||
if (onOffValue.getChannelState().equals(OnOffType.OFF)) {
|
||||
// Don't pass on percentage values while the fan is off
|
||||
state = PercentType.ZERO;
|
||||
}
|
||||
}
|
||||
speedValue.update(state);
|
||||
channelStateUpdateListener.updateChannelState(buildChannelUID(SPEED_CHANNEL_ID), state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postChannelCommand(ChannelUID channelUID, Command value) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void triggerChannel(ChannelUID channelUID, String eventPayload) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
@ -15,12 +15,19 @@ package org.openhab.binding.mqtt.homeassistant.internal.component;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openhab.binding.mqtt.generic.values.OnOffValue;
|
||||
import org.openhab.binding.mqtt.generic.values.PercentageValue;
|
||||
import org.openhab.binding.mqtt.generic.values.TextValue;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
/**
|
||||
* Tests for {@link Fan}
|
||||
@ -64,8 +71,8 @@ public class FanTests extends AbstractComponentTests {
|
||||
assertThat(component.channels.size(), is(1));
|
||||
assertThat(component.getName(), is("fan"));
|
||||
|
||||
assertChannel(component, Fan.SWITCH_CHANNEL_ID, "zigbee2mqtt/fan/state", "zigbee2mqtt/fan/set/state", "fan",
|
||||
OnOffValue.class);
|
||||
assertChannel(component, Fan.SWITCH_CHANNEL_ID, "zigbee2mqtt/fan/state", "zigbee2mqtt/fan/set/state",
|
||||
"On/Off State", OnOffValue.class);
|
||||
|
||||
publishMessage("zigbee2mqtt/fan/state", "ON_");
|
||||
assertState(component, Fan.SWITCH_CHANNEL_ID, OnOffType.ON);
|
||||
@ -116,6 +123,116 @@ public class FanTests extends AbstractComponentTests {
|
||||
assertPublished("zigbee2mqtt/fan/set/state", "set to OFF_");
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@Test
|
||||
public void testComplex() throws InterruptedException {
|
||||
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
|
||||
{
|
||||
"availability": [
|
||||
{
|
||||
"topic": "zigbee2mqtt/bridge/state"
|
||||
}
|
||||
],
|
||||
"device": {
|
||||
"identifiers": [
|
||||
"zigbee2mqtt_0x0000000000000000"
|
||||
],
|
||||
"manufacturer": "Fans inc",
|
||||
"model": "Fan",
|
||||
"name": "FanBlower",
|
||||
"sw_version": "Zigbee2MQTT 1.18.2"
|
||||
},
|
||||
"name": "Bedroom Fan",
|
||||
"payload_off": "false",
|
||||
"payload_on": "true",
|
||||
"state_topic": "bedroom_fan/on/state",
|
||||
"command_topic": "bedroom_fan/on/set",
|
||||
"direction_state_topic": "bedroom_fan/direction/state",
|
||||
"direction_command_topic": "bedroom_fan/direction/set",
|
||||
"oscillation_state_topic": "bedroom_fan/oscillation/state",
|
||||
"oscillation_command_topic": "bedroom_fan/oscillation/set",
|
||||
"percentage_state_topic": "bedroom_fan/speed/percentage_state",
|
||||
"percentage_command_topic": "bedroom_fan/speed/percentage",
|
||||
"preset_mode_state_topic": "bedroom_fan/preset/preset_mode_state",
|
||||
"preset_mode_command_topic": "bedroom_fan/preset/preset_mode",
|
||||
"preset_modes": [
|
||||
"auto",
|
||||
"smart",
|
||||
"whoosh",
|
||||
"eco",
|
||||
"breeze"
|
||||
],
|
||||
"payload_oscillation_on": "true",
|
||||
"payload_oscillation_off": "false",
|
||||
"speed_range_min": 1,
|
||||
"speed_range_max": 10
|
||||
}
|
||||
""");
|
||||
|
||||
assertThat(component.channels.size(), is(4));
|
||||
assertThat(component.getName(), is("Bedroom Fan"));
|
||||
|
||||
assertChannel(component, Fan.SPEED_CHANNEL_ID, "bedroom_fan/speed/percentage_state",
|
||||
"bedroom_fan/speed/percentage", "Speed", PercentageValue.class);
|
||||
var channel = Objects.requireNonNull(component.getChannel(Fan.SPEED_CHANNEL_ID));
|
||||
assertThat(channel.getStateDescription().getStep(), is(BigDecimal.valueOf(10.0d)));
|
||||
assertChannel(component, Fan.OSCILLATION_CHANNEL_ID, "bedroom_fan/oscillation/state",
|
||||
"bedroom_fan/oscillation/set", "Oscillation", OnOffValue.class);
|
||||
assertChannel(component, Fan.DIRECTION_CHANNEL_ID, "bedroom_fan/direction/state", "bedroom_fan/direction/set",
|
||||
"Direction", TextValue.class);
|
||||
assertChannel(component, Fan.PRESET_MODE_CHANNEL_ID, "bedroom_fan/preset/preset_mode_state",
|
||||
"bedroom_fan/preset/preset_mode", "Preset Mode", TextValue.class);
|
||||
|
||||
publishMessage("bedroom_fan/on/state", "true");
|
||||
assertState(component, Fan.SPEED_CHANNEL_ID, PercentType.HUNDRED);
|
||||
publishMessage("bedroom_fan/on/state", "false");
|
||||
assertState(component, Fan.SPEED_CHANNEL_ID, PercentType.ZERO);
|
||||
publishMessage("bedroom_fan/on/state", "true");
|
||||
publishMessage("bedroom_fan/speed/percentage_state", "50");
|
||||
assertState(component, Fan.SPEED_CHANNEL_ID, new PercentType(50));
|
||||
publishMessage("bedroom_fan/on/state", "false");
|
||||
// Off, even though we got an updated speed
|
||||
assertState(component, Fan.SPEED_CHANNEL_ID, PercentType.ZERO);
|
||||
publishMessage("bedroom_fan/speed/percentage_state", "25");
|
||||
assertState(component, Fan.SPEED_CHANNEL_ID, PercentType.ZERO);
|
||||
publishMessage("bedroom_fan/on/state", "true");
|
||||
// Now that it's on, the channel reflects the proper speed
|
||||
assertState(component, Fan.SPEED_CHANNEL_ID, new PercentType(25));
|
||||
|
||||
publishMessage("bedroom_fan/oscillation/state", "true");
|
||||
assertState(component, Fan.OSCILLATION_CHANNEL_ID, OnOffType.ON);
|
||||
publishMessage("bedroom_fan/oscillation/state", "false");
|
||||
assertState(component, Fan.OSCILLATION_CHANNEL_ID, OnOffType.OFF);
|
||||
|
||||
publishMessage("bedroom_fan/direction/state", "forward");
|
||||
assertState(component, Fan.DIRECTION_CHANNEL_ID, new StringType("forward"));
|
||||
publishMessage("bedroom_fan/direction/state", "backward");
|
||||
assertState(component, Fan.DIRECTION_CHANNEL_ID, new StringType("backward"));
|
||||
|
||||
publishMessage("bedroom_fan/preset/preset_mode_state", "auto");
|
||||
assertState(component, Fan.PRESET_MODE_CHANNEL_ID, new StringType("auto"));
|
||||
publishMessage("bedroom_fan/preset/preset_mode_state", "None");
|
||||
assertState(component, Fan.PRESET_MODE_CHANNEL_ID, UnDefType.NULL);
|
||||
|
||||
component.getChannel(Fan.SPEED_CHANNEL_ID).getState().publishValue(OnOffType.OFF);
|
||||
assertPublished("bedroom_fan/on/set", "false");
|
||||
component.getChannel(Fan.SPEED_CHANNEL_ID).getState().publishValue(OnOffType.ON);
|
||||
assertPublished("bedroom_fan/on/set", "true");
|
||||
// Setting to a specific speed turns it on first
|
||||
component.getChannel(Fan.SPEED_CHANNEL_ID).getState().publishValue(PercentType.HUNDRED);
|
||||
assertPublished("bedroom_fan/on/set", "true");
|
||||
assertPublished("bedroom_fan/speed/percentage", "100");
|
||||
|
||||
component.getChannel(Fan.OSCILLATION_CHANNEL_ID).getState().publishValue(OnOffType.ON);
|
||||
assertPublished("bedroom_fan/oscillation/set", "true");
|
||||
|
||||
component.getChannel(Fan.DIRECTION_CHANNEL_ID).getState().publishValue(new StringType("forward"));
|
||||
assertPublished("bedroom_fan/direction/set", "forward");
|
||||
|
||||
component.getChannel(Fan.PRESET_MODE_CHANNEL_ID).getState().publishValue(new StringType("eco"));
|
||||
assertPublished("bedroom_fan/preset/preset_mode", "eco");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Set<String> getConfigTopics() {
|
||||
return Set.of(CONFIG_TOPIC);
|
||||
|
Loading…
Reference in New Issue
Block a user