[mqtt.homeassistant] support non-RGB lights (#13413)

* [mqtt.homeassistant] support non-RGB lights

dynamically decide which type of channel to expose. also send "down-typed"
commands to the proper topic. this also sets the groundwork for supporting
template and JSON schemas

Signed-off-by: Cody Cutrer <cody@cutrer.us>
This commit is contained in:
Cody Cutrer 2022-11-05 09:57:06 -06:00 committed by GitHub
parent 969fef8612
commit db0ca281ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 906 additions and 224 deletions

View File

@ -22,7 +22,8 @@ These can be installed under `Settings` &rarr; `Addon` &rarr; `Transformations`
* The HomeAssistant Fan Components only support ON/OFF.
* The HomeAssistant Cover Components only support OPEN/CLOSE/STOP.
* The HomeAssistant Light Component only supports RGB color changes.
* The HomeAssistant Light Component only support on/off, brightness, and RGB.
Other color spaces, color temperature, effects, and white channel may work, but are untested.
* The HomeAssistant Climate Components is not yet supported.
## Tasmota auto discovery

View File

@ -136,6 +136,8 @@ public class ComponentChannel {
private @Nullable String templateIn;
private @Nullable String templateOut;
private String format = "%s";
public Builder(AbstractComponent<?> component, String channelID, Value valueState, String label,
ChannelStateUpdateListener channelStateUpdateListener) {
this.component = component;
@ -206,6 +208,11 @@ public class ComponentChannel {
return this;
}
public Builder withFormat(String format) {
this.format = format;
return this;
}
public ComponentChannel build() {
return build(true);
}
@ -222,11 +229,10 @@ public class ComponentChannel {
channelUID.getGroupId() + "_" + channelID);
channelState = new HomeAssistantChannelState(
ChannelConfigBuilder.create().withRetain(retain).withQos(qos).withStateTopic(stateTopic)
.withCommandTopic(commandTopic).makeTrigger(trigger).build(),
.withCommandTopic(commandTopic).makeTrigger(trigger).withFormatter(format).build(),
channelUID, valueState, channelStateUpdateListener, commandFilter);
String localStateTopic = stateTopic;
if (localStateTopic == null || localStateTopic.isBlank() || this.trigger) {
if (this.trigger) {
type = ChannelTypeBuilder.trigger(channelTypeUID, label)
.withConfigDescriptionURI(URI.create(MqttBindingConstants.CONFIG_HA_CHANNEL))
.isAdvanced(isAdvanced).build();

View File

@ -56,9 +56,18 @@ public class HomeAssistantChannelState extends ChannelState {
@Override
public CompletableFuture<Boolean> publishValue(Command command) {
if (commandFilter != null && !commandFilter.test(command)) {
logger.trace("Channel {} updates are disabled by command filter, ignoring command {}", channelUID, command);
return CompletableFuture.completedFuture(false);
if (commandFilter != null) {
try {
if (!commandFilter.test(command)) {
logger.trace("Channel {} updates are disabled by command filter, ignoring command {}", channelUID,
command);
return CompletableFuture.completedFuture(false);
}
} catch (IllegalArgumentException e) {
CompletableFuture<Boolean> f = new CompletableFuture<>();
f.completeExceptionally(e);
return f;
}
}
return super.publishValue(command);
}

View File

@ -66,7 +66,7 @@ public class ComponentFactory {
case "climate":
return new Climate(componentConfiguration);
case "light":
return new Light(componentConfiguration);
return Light.create(componentConfiguration);
case "lock":
return new Lock(componentConfiguration);
case "sensor":

View File

@ -0,0 +1,350 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal.component;
import java.math.BigDecimal;
import java.util.Objects;
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.mapping.ColorMode;
import org.openhab.binding.mqtt.generic.values.ColorValue;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
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.thing.ChannelUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* A MQTT light, following the https://www.home-assistant.io/components/light.mqtt/ specification.
*
* Specifically, the default schema. This class will present a single channel for color, brightness,
* or on/off as appropriate. Additional attributes are still exposed as dedicated channels.
*
* @author Cody Cutrer - Initial contribution
*/
@NonNullByDefault
public class DefaultSchemaLight extends Light {
protected static final String HS_CHANNEL_ID = "hs";
protected static final String RGB_CHANNEL_ID = "rgb";
protected static final String RGBW_CHANNEL_ID = "rgbw";
protected static final String RGBWW_CHANNEL_ID = "rgbww";
protected static final String XY_CHANNEL_ID = "xy";
protected static final String WHITE_CHANNEL_ID = "white";
protected @Nullable ComponentChannel hsChannel;
protected @Nullable ComponentChannel rgbChannel;
protected @Nullable ComponentChannel xyChannel;
public DefaultSchemaLight(ComponentFactory.ComponentConfiguration builder) {
super(builder);
}
@Override
protected void buildChannels() {
ComponentChannel localOnOffChannel;
localOnOffChannel = onOffChannel = buildChannel(ON_OFF_CHANNEL_ID, onOffValue, "On/Off State", this)
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.stateValueTemplate)
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.commandFilter(this::handleRawOnOffCommand).build(false);
@Nullable
ComponentChannel localBrightnessChannel = null;
if (channelConfiguration.brightnessStateTopic != null || channelConfiguration.brightnessCommandTopic != null) {
localBrightnessChannel = brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, brightnessValue,
"Brightness", this)
.stateTopic(channelConfiguration.brightnessStateTopic,
channelConfiguration.brightnessValueTemplate)
.commandTopic(channelConfiguration.brightnessCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.withFormat("%.0f").commandFilter(this::handleBrightnessCommand).build(false);
}
if (channelConfiguration.whiteCommandTopic != null) {
buildChannel(WHITE_CHANNEL_ID, brightnessValue, "Go directly to white of a specific brightness", this)
.commandTopic(channelConfiguration.whiteCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.isAdvanced(true).build();
}
if (channelConfiguration.colorModeStateTopic != null) {
buildChannel(COLOR_MODE_CHANNEL_ID, new TextValue(), "Current color mode", this)
.stateTopic(channelConfiguration.colorModeStateTopic, channelConfiguration.colorModeValueTemplate)
.build();
}
if (channelConfiguration.colorTempStateTopic != null || channelConfiguration.colorTempCommandTopic != null) {
buildChannel(COLOR_TEMP_CHANNEL_ID, colorTempValue, "Color Temperature", this)
.stateTopic(channelConfiguration.colorTempStateTopic, channelConfiguration.colorTempValueTemplate)
.commandTopic(channelConfiguration.colorTempCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build();
}
if (channelConfiguration.effectStateTopic != null || channelConfiguration.effectCommandTopic != null) {
buildChannel(EFFECT_CHANNEL_ID, effectValue, "Lighting effect", this)
.stateTopic(channelConfiguration.effectStateTopic, channelConfiguration.effectValueTemplate)
.commandTopic(channelConfiguration.effectCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build();
}
if (channelConfiguration.rgbStateTopic != null || channelConfiguration.rgbCommandTopic != null) {
hasColorChannel = true;
hiddenChannels.add(rgbChannel = buildChannel(RGB_CHANNEL_ID, new ColorValue(ColorMode.RGB, null, null, 100),
"RGB state", this)
.stateTopic(channelConfiguration.rgbStateTopic, channelConfiguration.rgbValueTemplate)
.commandTopic(channelConfiguration.rgbCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build(false));
}
if (channelConfiguration.rgbwStateTopic != null || channelConfiguration.rgbwCommandTopic != null) {
hasColorChannel = true;
hiddenChannels.add(buildChannel(RGBW_CHANNEL_ID, new TextValue(), "RGBW state", this)
.stateTopic(channelConfiguration.rgbwStateTopic, channelConfiguration.rgbwValueTemplate)
.commandTopic(channelConfiguration.rgbwCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build(false));
}
if (channelConfiguration.rgbwwStateTopic != null || channelConfiguration.rgbwwCommandTopic != null) {
hasColorChannel = true;
hiddenChannels.add(buildChannel(RGBWW_CHANNEL_ID, new TextValue(), "RGBWW state", this)
.stateTopic(channelConfiguration.rgbwwStateTopic, channelConfiguration.rgbwwValueTemplate)
.commandTopic(channelConfiguration.rgbwwCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build(false));
}
if (channelConfiguration.xyStateTopic != null || channelConfiguration.xyCommandTopic != null) {
hasColorChannel = true;
hiddenChannels.add(
xyChannel = buildChannel(XY_CHANNEL_ID, new ColorValue(ColorMode.XYY, null, null, 100), "XY State",
this).stateTopic(channelConfiguration.xyStateTopic, channelConfiguration.xyValueTemplate)
.commandTopic(channelConfiguration.xyCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build(false));
}
if (channelConfiguration.hsStateTopic != null || channelConfiguration.hsCommandTopic != null) {
hasColorChannel = true;
hiddenChannels.add(this.hsChannel = buildChannel(HS_CHANNEL_ID, new TextValue(), "Hue and Saturation", this)
.stateTopic(channelConfiguration.hsStateTopic, channelConfiguration.hsValueTemplate)
.commandTopic(channelConfiguration.hsCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build(false));
}
if (hasColorChannel) {
hiddenChannels.add(localOnOffChannel);
if (localBrightnessChannel != null) {
hiddenChannels.add(localBrightnessChannel);
}
buildChannel(COLOR_CHANNEL_ID, colorValue, "Color", this)
.commandTopic(DUMMY_TOPIC, channelConfiguration.isRetain(), channelConfiguration.getQos())
.commandFilter(this::handleColorCommand).build();
} else if (localBrightnessChannel != null) {
hiddenChannels.add(localOnOffChannel);
channels.put(BRIGHTNESS_CHANNEL_ID, localBrightnessChannel);
} else {
channels.put(ON_OFF_CHANNEL_ID, localOnOffChannel);
}
}
// all handle*Command methods return false if they've been handled,
// or true if default handling should continue
// The commandFilter for onOffChannel
private boolean handleRawOnOffCommand(Command command) {
// on_command_type of brightness is not allowed to send an actual on command
if (command.equals(OnOffType.ON) && channelConfiguration.onCommandType.equals(ON_COMMAND_TYPE_BRIGHTNESS)) {
// No prior state (or explicit off); set to 100%
if (brightnessValue.getChannelState() instanceof UnDefType
|| brightnessValue.getChannelState().equals(PercentType.ZERO)) {
brightnessChannel.getState().publishValue(PercentType.HUNDRED);
} else {
brightnessChannel.getState().publishValue((Command) brightnessValue.getChannelState());
}
return false;
}
return true;
}
// The helper method the other commandFilters call
private boolean handleOnOffCommand(Command command) {
if (!handleRawOnOffCommand(command)) {
return false;
}
// OnOffType commands to go the regular command topic
if (command instanceof OnOffType) {
onOffChannel.getState().publishValue(command);
return false;
}
boolean needsOn = !onOffValue.getChannelState().equals(OnOffType.ON);
if (command.equals(PercentType.ZERO) || command.equals(HSBType.BLACK)) {
needsOn = false;
}
if (needsOn) {
if (channelConfiguration.onCommandType.equals(ON_COMMAND_TYPE_FIRST)) {
onOffChannel.getState().publishValue(OnOffType.ON);
} else if (channelConfiguration.onCommandType.equals(ON_COMMAND_TYPE_LAST)) {
// TODO: schedule the ON publish for after this is sent
}
}
return true;
}
private boolean handleBrightnessCommand(Command command) {
// if it's OnOffType, it'll get handled by this; otherwise it'll return
// true and PercentType will be handled as normal
return handleOnOffCommand(command);
}
private boolean handleColorCommand(Command command) {
if (!handleOnOffCommand(command)) {
return false;
} else if (command instanceof HSBType) {
HSBType color = (HSBType) command;
if (channelConfiguration.hsCommandTopic != null) {
// If we don't have a brightness channel, something is probably busted
// but don't choke
if (channelConfiguration.brightnessCommandTopic != null) {
brightnessChannel.getState().publishValue(color.getBrightness());
}
String hs = String.format("%d,%d", color.getHue().intValue(), color.getSaturation().intValue());
hsChannel.getState().publishValue(new StringType(hs));
} else if (channelConfiguration.rgbCommandTopic != null) {
rgbChannel.getState().publishValue(command);
// } else if (channelConfiguration.rgbwCommandTopic != null) {
// TODO
// } else if (channelConfiguration.rgbwwCommandTopic != null) {
// TODO
} else if (channelConfiguration.xyCommandTopic != null) {
PercentType[] xy = color.toXY();
// If we don't have a brightness channel, something is probably busted
// but don't choke
if (channelConfiguration.brightnessCommandTopic != null) {
brightnessChannel.getState().publishValue(color.getBrightness());
}
String xyString = String.format("%f,%f", xy[0].doubleValue(), xy[1].doubleValue());
xyChannel.getState().publishValue(new StringType(xyString));
}
} else if (command instanceof PercentType) {
if (channelConfiguration.brightnessCommandTopic != null) {
brightnessChannel.getState().publishValue(command);
} else {
// No brightness command topic?! must be RGB only
// so re-calculatate
State color = colorValue.getChannelState();
if (color instanceof UnDefType) {
color = HSBType.WHITE;
}
HSBType existingColor = (HSBType) color;
HSBType newCommand = new HSBType(existingColor.getHue(), existingColor.getSaturation(),
(PercentType) command);
// re-process
handleColorCommand(newCommand);
}
}
return false;
}
@Override
public void updateChannelState(ChannelUID channel, State state) {
ChannelStateUpdateListener listener = this.channelStateUpdateListener;
switch (channel.getIdWithoutGroup()) {
case ON_OFF_CHANNEL_ID:
if (hasColorChannel) {
HSBType newOnState = colorValue.getChannelState() instanceof HSBType
? (HSBType) colorValue.getChannelState()
: HSBType.WHITE;
if (state.equals(OnOffType.ON)) {
colorValue.update(newOnState);
}
listener.updateChannelState(new ChannelUID(getGroupUID(), COLOR_CHANNEL_ID),
state.equals(OnOffType.ON) ? newOnState : HSBType.BLACK);
} else if (brightnessChannel != null) {
listener.updateChannelState(new ChannelUID(channel.getThingUID(), BRIGHTNESS_CHANNEL_ID),
state.equals(OnOffType.ON) ? brightnessValue.getChannelState() : PercentType.ZERO);
} else {
listener.updateChannelState(channel, state);
}
return;
case BRIGHTNESS_CHANNEL_ID:
onOffValue.update(Objects.requireNonNull(state.as(OnOffType.class)));
if (hasColorChannel) {
if (colorValue.getChannelState() instanceof HSBType) {
HSBType hsb = (HSBType) (colorValue.getChannelState());
colorValue.update(new HSBType(hsb.getHue(), hsb.getSaturation(),
(PercentType) brightnessValue.getChannelState()));
} else {
colorValue.update(new HSBType(DecimalType.ZERO, PercentType.ZERO,
(PercentType) brightnessValue.getChannelState()));
}
listener.updateChannelState(new ChannelUID(getGroupUID(), COLOR_CHANNEL_ID),
colorValue.getChannelState());
} else {
listener.updateChannelState(channel, state);
}
return;
case COLOR_TEMP_CHANNEL_ID:
case EFFECT_CHANNEL_ID:
// Real channels; pass through
listener.updateChannelState(channel, state);
return;
case HS_CHANNEL_ID:
case XY_CHANNEL_ID:
if (brightnessValue.getChannelState() instanceof UnDefType) {
brightnessValue.update(PercentType.HUNDRED);
}
String[] split = state.toString().split(",");
if (split.length != 2) {
throw new IllegalArgumentException(state.toString() + " is not a valid string syntax");
}
float x = Float.parseFloat(split[0]);
float y = Float.parseFloat(split[1]);
PercentType brightness = (PercentType) brightnessValue.getChannelState();
if (channel.getIdWithoutGroup().equals(HS_CHANNEL_ID)) {
colorValue.update(new HSBType(new DecimalType(x), new PercentType(new BigDecimal(y)), brightness));
} else {
HSBType xyColor = HSBType.fromXY(x, y);
colorValue.update(new HSBType(xyColor.getHue(), xyColor.getSaturation(), brightness));
}
listener.updateChannelState(new ChannelUID(getGroupUID(), COLOR_CHANNEL_ID),
colorValue.getChannelState());
return;
case RGB_CHANNEL_ID:
colorValue.update((HSBType) state);
listener.updateChannelState(new ChannelUID(getGroupUID(), COLOR_CHANNEL_ID),
colorValue.getChannelState());
break;
case RGBW_CHANNEL_ID:
case RGBWW_CHANNEL_ID:
// TODO: update color value
break;
}
}
}

View File

@ -12,7 +12,10 @@
*/
package org.openhab.binding.mqtt.homeassistant.internal.component;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.stream.Stream;
@ -22,28 +25,56 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.mapping.ColorMode;
import org.openhab.binding.mqtt.generic.values.ColorValue;
import org.openhab.binding.mqtt.generic.values.NumberValue;
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.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.exception.UnsupportedComponentException;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import com.google.gson.annotations.SerializedName;
/**
* A MQTT light, following the https://www.home-assistant.io/components/light.mqtt/ specification.
* A MQTT light, following the
* https://www.home-assistant.io/components/light.mqtt/ specification.
*
* This class condenses the three state/command topics (for ON/OFF, Brightness, Color) to one
* color channel.
* Individual concrete classes implement the differing semantics of the
* three different schemas.
*
* As of now, only on/off, brightness, and RGB are fully implemented and tested.
* HS and XY are implemented, but not tested. Color temp and effect are only
* implemented (but not tested) for the default schema.
*
* @author David Graeff - Initial contribution
* @author Cody Cutrer - Re-write for (nearly) full support
*/
@NonNullByDefault
public class Light extends AbstractComponent<Light.ChannelConfiguration> implements ChannelStateUpdateListener {
public static final String SWITCH_CHANNEL_ID = "light"; // Randomly chosen channel "ID"
public static final String BRIGHTNESS_CHANNEL_ID = "brightness"; // Randomly chosen channel "ID"
public static final String COLOR_CHANNEL_ID = "color"; // Randomly chosen channel "ID"
public abstract class Light extends AbstractComponent<Light.ChannelConfiguration>
implements ChannelStateUpdateListener {
protected static final String DEFAULT_SCHEMA = "default";
protected static final String JSON_SCHEMA = "json";
protected static final String TEMPLATE_SCHEMA = "template";
protected static final String STATE_CHANNEL_ID = "state";
protected static final String ON_OFF_CHANNEL_ID = "on_off";
protected static final String BRIGHTNESS_CHANNEL_ID = "brightness";
protected static final String COLOR_MODE_CHANNEL_ID = "color_mode";
protected static final String COLOR_TEMP_CHANNEL_ID = "color_temp";
protected static final String EFFECT_CHANNEL_ID = "effect";
// This channel is a synthetic channel that may send to other channels
// underneath
protected static final String COLOR_CHANNEL_ID = "color";
protected static final String DUMMY_TOPIC = "dummy";
protected static final String ON_COMMAND_TYPE_FIRST = "first";
protected static final String ON_COMMAND_TYPE_BRIGHTNESS = "brightness";
protected static final String ON_COMMAND_TYPE_LAST = "last";
/**
* Configuration class for MQTT component
@ -53,155 +84,238 @@ public class Light extends AbstractComponent<Light.ChannelConfiguration> impleme
super("MQTT Light");
}
@SerializedName("brightness_scale")
protected int brightnessScale = 255;
protected boolean optimistic = false;
@SerializedName("effect_list")
protected @Nullable List<String> effectList;
/* Attributes that control the basic structure of the light */
// Defines when on the payload_on is sent. Using last (the default) will send any style (brightness, color, etc)
// topics first and then a payload_on to the command_topic. Using first will send the payload_on and then any
// style topics. Using brightness will only send brightness commands instead of the payload_on to turn the light
protected String schema = DEFAULT_SCHEMA;
protected @Nullable Boolean optimistic; // All schemas
protected boolean brightness = false; // JSON schema only
@SerializedName("color_mode")
protected boolean colorMode = false; // JSON schema only
@SerializedName("supported_color_modes")
protected @Nullable List<String> supportedColorModes; // JSON schema only
// Defines when on the payload_on is sent. Using last (the default) will send
// any style (brightness, color, etc)
// topics first and then a payload_on to the command_topic. Using first will
// send the payload_on and then any
// style topics. Using brightness will only send brightness commands instead of
// the payload_on to turn the light
// on.
@SerializedName("on_command_type")
protected String onCommandType = "last";
protected String onCommandType = ON_COMMAND_TYPE_LAST; // Default schema only
/* Basic control attributes */
@SerializedName("state_topic")
protected @Nullable String stateTopic;
@SerializedName("command_topic")
protected @Nullable String commandTopic;
protected @Nullable String stateTopic; // All Schemas
@SerializedName("state_value_template")
protected @Nullable String stateValueTemplate;
@SerializedName("brightness_state_topic")
protected @Nullable String brightnessStateTopic;
@SerializedName("brightness_command_topic")
protected @Nullable String brightnessCommandTopic;
@SerializedName("brightness_value_template")
protected @Nullable String brightnessValueTemplate;
@SerializedName("color_temp_state_topic")
protected @Nullable String colorTempStateTopic;
@SerializedName("color_temp_command_topic")
protected @Nullable String colorTempCommandTopic;
@SerializedName("color_temp_value_template")
protected @Nullable String colorTempValueTemplate;
@SerializedName("effect_command_topic")
protected @Nullable String effectCommandTopic;
@SerializedName("effect_state_topic")
protected @Nullable String effectStateTopic;
@SerializedName("effect_value_template")
protected @Nullable String effectValueTemplate;
@SerializedName("rgb_command_topic")
protected @Nullable String rgbCommandTopic;
@SerializedName("rgb_state_topic")
protected @Nullable String rgbStateTopic;
@SerializedName("rgb_value_template")
protected @Nullable String rgbValueTemplate;
@SerializedName("rgb_command_template")
protected @Nullable String rgbCommandTemplate;
@SerializedName("white_value_command_topic")
protected @Nullable String whiteValueCommandTopic;
@SerializedName("white_value_state_topic")
protected @Nullable String whiteValueStateTopic;
@SerializedName("white_value_template")
protected @Nullable String whiteValueTemplate;
@SerializedName("xy_command_topic")
protected @Nullable String xyCommandTopic;
@SerializedName("xy_state_topic")
protected @Nullable String xyStateTopic;
@SerializedName("xy_value_template")
protected @Nullable String xyValueTemplate;
protected @Nullable String stateValueTemplate; // Default schema only
@SerializedName("state_template")
protected @Nullable String stateTemplate; // Template schema only
@SerializedName("payload_on")
protected String payloadOn = "ON";
protected String payloadOn = "ON"; // Default schema only
@SerializedName("payload_off")
protected String payloadOff = "OFF";
protected String payloadOff = "OFF"; // Default schema only
@SerializedName("command_topic")
protected @Nullable String commandTopic; // All schemas
@SerializedName("command_on_template")
protected @Nullable String commandOnTemplate; // Template schema only; required
@SerializedName("command_off_template")
protected @Nullable String commandOffTemplate; // Template schema only; required
/* Brightness attributes */
@SerializedName("brightness_scale")
protected int brightnessScale = 255; // Default, JSON schemas only
@SerializedName("brightness_state_topic")
protected @Nullable String brightnessStateTopic; // Default schema only
@SerializedName("brightness_value_template")
protected @Nullable String brightnessValueTemplate; // Default schema only
@SerializedName("brightness_template")
protected @Nullable String brightnessTemplate; // Template schema only
@SerializedName("brightness_command_topic")
protected @Nullable String brightnessCommandTopic; // Default schema only
@SerializedName("brightness_command_template")
protected @Nullable String brightnessCommandTemplate; // Default schema only
/* White value attributes */
@SerializedName("white_scale")
protected int whiteScale = 255; // Default, JSON schemas only
@SerializedName("white_command_topic")
protected @Nullable String whiteCommandTopic; // Default schema only
/* Color mode attributes */
@SerializedName("color_mode_state_topic")
protected @Nullable String colorModeStateTopic; // Default schema only
@SerializedName("color_mode_value_template")
protected @Nullable String colorModeValueTemplate; // Default schema only
/* Color temp attributes */
@SerializedName("min_mireds")
protected @Nullable Integer minMireds; // All schemas
@SerializedName("max_mireds")
protected @Nullable Integer maxMireds; // All schemas
@SerializedName("color_temp_state_topic")
protected @Nullable String colorTempStateTopic; // Default schema only
@SerializedName("color_temp_value_template")
protected @Nullable String colorTempValueTemplate; // Default schema only
@SerializedName("color_temp_template")
protected @Nullable String colorTempTemplate; // Template schema only
@SerializedName("color_temp_command_topic")
protected @Nullable String colorTempCommandTopic; // Default schema only
@SerializedName("color_temp_command_template")
protected @Nullable String colorTempCommandTemplate; // Default schema only
/* Effect attributes */
@SerializedName("effect_list")
protected @Nullable List<String> effectList; // All schemas
@SerializedName("effect_state_topic")
protected @Nullable String effectStateTopic; // Default schema only
@SerializedName("effect_value_template")
protected @Nullable String effectValueTemplate; // Default schema only
@SerializedName("effect_template")
protected @Nullable String effectTemplate; // Template schema only
@SerializedName("effect_command_topic")
protected @Nullable String effectCommandTopic; // Default schema only
@SerializedName("effect_command_template")
protected @Nullable String effectCommandTemplate; // Default schema only
/* HS attributes */
@SerializedName("hs_state_topic")
protected @Nullable String hsStateTopic; // Default schema only
@SerializedName("hs_value_template")
protected @Nullable String hsValueTemplate; // Default schema only
@SerializedName("hs_command_topic")
protected @Nullable String hsCommandTopic; // Default schema only
/* RGB attributes */
@SerializedName("rgb_state_topic")
protected @Nullable String rgbStateTopic; // Default schema only
@SerializedName("rgb_value_template")
protected @Nullable String rgbValueTemplate; // Default schema only
@SerializedName("red_template")
protected @Nullable String redTemplate; // Template schema only
@SerializedName("green_template")
protected @Nullable String greenTemplate; // Template schema only
@SerializedName("blue_template")
protected @Nullable String blueTemplate; // Template schema only
@SerializedName("rgb_command_topic")
protected @Nullable String rgbCommandTopic; // Default schema only
@SerializedName("rgb_command_template")
protected @Nullable String rgbCommandTemplate; // Default schema only
/* RGBW attributes */
@SerializedName("rgbw_state_topic")
protected @Nullable String rgbwStateTopic; // Default schema only
@SerializedName("rgbw_value_template")
protected @Nullable String rgbwValueTemplate; // Default schema only
@SerializedName("rgbw_command_topic")
protected @Nullable String rgbwCommandTopic; // Default schema only
@SerializedName("rgbw_command_template")
protected @Nullable String rgbwCommandTemplate; // Default schema only
/* RGBWW attributes */
@SerializedName("rgbww_state_topic")
protected @Nullable String rgbwwStateTopic; // Default schema only
@SerializedName("rgbww_value_template")
protected @Nullable String rgbwwValueTemplate; // Default schema only
@SerializedName("rgbww_command_topic")
protected @Nullable String rgbwwCommandTopic; // Default schema only
@SerializedName("rgbww_command_template")
protected @Nullable String rgbwwCommandTemplate; // Default schema only
/* XY attributes */
@SerializedName("xy_command_topic")
protected @Nullable String xyCommandTopic; // Default schema only
@SerializedName("xy_state_topic")
protected @Nullable String xyStateTopic; // Default schema only
@SerializedName("xy_value_template")
protected @Nullable String xyValueTemplate; // Default schema only
}
protected ComponentChannel colorChannel;
protected ComponentChannel switchChannel;
protected ComponentChannel brightnessChannel;
private final @Nullable ChannelStateUpdateListener channelStateUpdateListener;
protected final boolean optimistic;
protected boolean hasColorChannel = false;
public Light(ComponentFactory.ComponentConfiguration builder) {
protected @Nullable ComponentChannel onOffChannel;
protected @Nullable ComponentChannel brightnessChannel;
// State has to be stored here, in order to mux multiple
// MQTT sources into single OpenHAB channels
protected OnOffValue onOffValue;
protected PercentageValue brightnessValue;
protected final NumberValue colorTempValue;
protected final TextValue effectValue = new TextValue();
protected final ColorValue colorValue = new ColorValue(ColorMode.HSB, null, null, 100);
protected final List<ComponentChannel> hiddenChannels = new ArrayList<>();
protected final ChannelStateUpdateListener channelStateUpdateListener;
public static Light create(ComponentFactory.ComponentConfiguration builder) throws UnsupportedComponentException {
String schema = builder.getConfig(ChannelConfiguration.class).schema;
switch (schema) {
case DEFAULT_SCHEMA:
return new DefaultSchemaLight(builder);
default:
throw new UnsupportedComponentException(
"Component '" + builder.getHaID() + "' of schema '" + schema + "' is not supported!");
}
}
protected Light(ComponentFactory.ComponentConfiguration builder) {
super(builder, ChannelConfiguration.class);
this.channelStateUpdateListener = builder.getUpdateListener();
ColorValue value = new ColorValue(ColorMode.RGB, channelConfiguration.payloadOn,
channelConfiguration.payloadOff, 100);
// Create three MQTT subscriptions and use this class object as update listener
switchChannel = buildChannel(SWITCH_CHANNEL_ID, value, channelConfiguration.getName(), this)
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.stateValueTemplate,
channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build(false);
@Nullable
Boolean optimistic = channelConfiguration.optimistic;
if (optimistic != null) {
this.optimistic = optimistic;
} else {
this.optimistic = (channelConfiguration.stateTopic == null);
}
colorChannel = buildChannel(COLOR_CHANNEL_ID, value, channelConfiguration.getName(), this)
.stateTopic(channelConfiguration.rgbStateTopic, channelConfiguration.rgbValueTemplate)
.commandTopic(channelConfiguration.rgbCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build(false);
onOffValue = new OnOffValue(channelConfiguration.payloadOn, channelConfiguration.payloadOff);
brightnessValue = new PercentageValue(null, new BigDecimal(channelConfiguration.brightnessScale), null, null,
null);
@Nullable
BigDecimal min = null, max = null;
if (channelConfiguration.minMireds != null) {
min = new BigDecimal(channelConfiguration.minMireds);
}
if (channelConfiguration.maxMireds != null) {
max = new BigDecimal(channelConfiguration.maxMireds);
}
colorTempValue = new NumberValue(min, max, BigDecimal.ONE, Units.MIRED);
brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, value, channelConfiguration.getName(), this)
.stateTopic(channelConfiguration.brightnessStateTopic, channelConfiguration.brightnessValueTemplate)
.commandTopic(channelConfiguration.brightnessCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build(false);
channels.put(COLOR_CHANNEL_ID, colorChannel);
buildChannels();
}
protected abstract void buildChannels();
@Override
public CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection, ScheduledExecutorService scheduler,
int timeout) {
return Stream.of(switchChannel, brightnessChannel, colorChannel) //
return Stream.concat(channels.values().stream(), hiddenChannels.stream()) //
.map(v -> v.start(connection, scheduler, timeout)) //
.reduce(CompletableFuture.completedFuture(null), (f, v) -> f.thenCompose(b -> v));
}
@Override
public CompletableFuture<@Nullable Void> stop() {
return Stream.of(switchChannel, brightnessChannel, colorChannel) //
.map(v -> v.stop()) //
return Stream.concat(channels.values().stream(), hiddenChannels.stream()) //
.filter(Objects::nonNull) //
.map(ComponentChannel::stop) //
.reduce(CompletableFuture.completedFuture(null), (f, v) -> f.thenCompose(b -> v));
}
/**
* Proxy method to condense all three MQTT subscriptions to one channel
*/
@Override
public void updateChannelState(ChannelUID channelUID, State value) {
ChannelStateUpdateListener listener = channelStateUpdateListener;
if (listener != null) {
listener.updateChannelState(colorChannel.getChannelUID(), value);
}
}
/**
* Proxy method to condense all three MQTT subscriptions to one channel
*/
@Override
public void postChannelCommand(ChannelUID channelUID, Command value) {
ChannelStateUpdateListener listener = channelStateUpdateListener;
if (listener != null) {
listener.postChannelCommand(colorChannel.getChannelUID(), value);
}
throw new UnsupportedOperationException();
}
/**
* Proxy method to condense all three MQTT subscriptions to one channel
*/
@Override
public void triggerChannel(ChannelUID channelUID, String eventPayload) {
ChannelStateUpdateListener listener = channelStateUpdateListener;
if (listener != null) {
listener.triggerChannel(colorChannel.getChannelUID(), eventPayload);
}
throw new UnsupportedOperationException();
}
}

View File

@ -51,6 +51,7 @@ import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThin
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
/**
@ -244,6 +245,19 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
return false;
}
/**
* Send command to a thing's channel
*
* @param component component
* @param channelId channel
* @param command command to send
*/
protected void sendCommand(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
String channelId, Command command) {
var channel = Objects.requireNonNull(component.getChannel(channelId));
thingHandler.handleCommand(channel.getChannelUID(), command);
}
protected static class LatchThingHandler extends HomeAssistantThingHandler {
private @Nullable CountDownLatch latch;
private @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoveredComponent;

View File

@ -0,0 +1,282 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal.component;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import java.math.BigDecimal;
import java.math.MathContext;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.Test;
import org.openhab.binding.mqtt.generic.values.ColorValue;
import org.openhab.binding.mqtt.generic.values.OnOffValue;
import org.openhab.binding.mqtt.generic.values.PercentageValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
/**
* Tests for {@link Light} confirming to the default schema
*
* @author Anton Kharuzhy - Initial contribution
*/
@NonNullByDefault
public class DefaultSchemaLightTests extends AbstractComponentTests {
public static final String CONFIG_TOPIC = "light/0x0000000000000000_light_zigbee2mqtt";
@Test
public void testRgb() throws InterruptedException {
// @formatter:off
var component = (Light) discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
"{ " +
" \"availability\": [ " +
" { " +
" \"topic\": \"zigbee2mqtt/bridge/state\" " +
" } " +
" ], " +
" \"device\": { " +
" \"identifiers\": [ " +
" \"zigbee2mqtt_0x0000000000000000\" " +
" ], " +
" \"manufacturer\": \"Lights inc\", " +
" \"model\": \"light v1\", " +
" \"name\": \"Light\", " +
" \"sw_version\": \"Zigbee2MQTT 1.18.2\" " +
" }, " +
" \"name\": \"light\", " +
" \"state_topic\": \"zigbee2mqtt/light/state\", " +
" \"command_topic\": \"zigbee2mqtt/light/set/state\", " +
" \"state_value_template\": \"{{ value_json.power }}\", " +
" \"payload_on\": \"ON_\", " +
" \"payload_off\": \"OFF_\", " +
" \"rgb_state_topic\": \"zigbee2mqtt/light/rgb\", " +
" \"rgb_command_topic\": \"zigbee2mqtt/light/set/rgb\", " +
" \"rgb_value_template\": \"{{ value_json.rgb }}\", " +
" \"brightness_state_topic\": \"zigbee2mqtt/light/brightness\", " +
" \"brightness_command_topic\": \"zigbee2mqtt/light/set/brightness\", " +
" \"brightness_value_template\": \"{{ value_json.br }}\" " +
"}");
// @formatter:on
assertThat(component.channels.size(), is(1));
assertThat(component.getName(), is("light"));
assertChannel(component, Light.COLOR_CHANNEL_ID, "", "dummy", "Color", ColorValue.class);
@Nullable
ComponentChannel onOffChannel = component.onOffChannel;
assertThat(onOffChannel, is(notNullValue()));
if (onOffChannel != null) {
assertChannel(onOffChannel, "zigbee2mqtt/light/state", "zigbee2mqtt/light/set/state", "On/Off State",
OnOffValue.class);
}
@Nullable
ComponentChannel brightnessChannel = component.brightnessChannel;
assertThat(brightnessChannel, is(notNullValue()));
if (brightnessChannel != null) {
assertChannel(brightnessChannel, "zigbee2mqtt/light/brightness", "zigbee2mqtt/light/set/brightness",
"Brightness", PercentageValue.class);
}
publishMessage("zigbee2mqtt/light/state", "{\"power\": \"ON_\"}");
assertState(component, Light.COLOR_CHANNEL_ID, HSBType.WHITE);
publishMessage("zigbee2mqtt/light/rgb", "{\"rgb\": \"10,20,30\"}");
assertState(component, Light.COLOR_CHANNEL_ID, HSBType.fromRGB(10, 20, 30));
publishMessage("zigbee2mqtt/light/rgb", "{\"rgb\": \"255,255,255\"}");
assertState(component, Light.COLOR_CHANNEL_ID, HSBType.WHITE);
sendCommand(component, Light.COLOR_CHANNEL_ID, HSBType.BLUE);
assertPublished("zigbee2mqtt/light/set/rgb", "0,0,255");
// Brightness commands should route to the correct topic
sendCommand(component, Light.COLOR_CHANNEL_ID, new PercentType(50));
assertPublished("zigbee2mqtt/light/set/brightness", "128");
// OnOff commands should route to the correct topic
sendCommand(component, Light.COLOR_CHANNEL_ID, OnOffType.OFF);
assertPublished("zigbee2mqtt/light/set/state", "OFF_");
}
@Test
public void testRgbWithoutBrightness() throws InterruptedException {
// @formatter:off
var component = (Light) discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
"{ " +
" \"name\": \"light\", " +
" \"state_topic\": \"zigbee2mqtt/light/state\", " +
" \"command_topic\": \"zigbee2mqtt/light/set/state\", " +
" \"state_value_template\": \"{{ value_json.power }}\", " +
" \"payload_on\": \"ON_\", " +
" \"payload_off\": \"OFF_\", " +
" \"rgb_state_topic\": \"zigbee2mqtt/light/rgb\", " +
" \"rgb_command_topic\": \"zigbee2mqtt/light/set/rgb\", " +
" \"rgb_value_template\": \"{{ value_json.rgb }}\"" +
"}");
// @formatter:on
publishMessage("zigbee2mqtt/light/rgb", "{\"rgb\": \"255,255,255\"}");
assertState(component, Light.COLOR_CHANNEL_ID, HSBType.WHITE);
// Brightness commands should route to the correct topic, converted to RGB
sendCommand(component, Light.COLOR_CHANNEL_ID, new PercentType(50));
assertPublished("zigbee2mqtt/light/set/rgb", "127,127,127");
// OnOff commands should route to the correct topic
sendCommand(component, Light.COLOR_CHANNEL_ID, OnOffType.OFF);
assertPublished("zigbee2mqtt/light/set/state", "OFF_");
}
@Test
public void testHsb() throws InterruptedException {
// @formatter:off
var component = (Light) discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
"{ " +
" \"name\": \"light\", " +
" \"state_topic\": \"zigbee2mqtt/light/state\", " +
" \"command_topic\": \"zigbee2mqtt/light/set/state\", " +
" \"state_value_template\": \"{{ value_json.power }}\", " +
" \"payload_on\": \"ON_\", " +
" \"payload_off\": \"OFF_\", " +
" \"hs_state_topic\": \"zigbee2mqtt/light/hs\", " +
" \"hs_command_topic\": \"zigbee2mqtt/light/set/hs\", " +
" \"hs_value_template\": \"{{ value_json.hs }}\", " +
" \"brightness_state_topic\": \"zigbee2mqtt/light/brightness\", " +
" \"brightness_command_topic\": \"zigbee2mqtt/light/set/brightness\", " +
" \"brightness_value_template\": \"{{ value_json.br }}\" " +
"}");
// @formatter:on
assertThat(component.channels.size(), is(1));
assertThat(component.getName(), is("light"));
assertChannel(component, Light.COLOR_CHANNEL_ID, "", "dummy", "Color", ColorValue.class);
@Nullable
ComponentChannel onOffChannel = component.onOffChannel;
assertThat(onOffChannel, is(notNullValue()));
if (onOffChannel != null) {
assertChannel(onOffChannel, "zigbee2mqtt/light/state", "zigbee2mqtt/light/set/state", "On/Off State",
OnOffValue.class);
}
@Nullable
ComponentChannel brightnessChannel = component.brightnessChannel;
assertThat(brightnessChannel, is(notNullValue()));
if (brightnessChannel != null) {
assertChannel(brightnessChannel, "zigbee2mqtt/light/brightness", "zigbee2mqtt/light/set/brightness",
"Brightness", PercentageValue.class);
}
publishMessage("zigbee2mqtt/light/hs", "{\"hs\": \"180,50\"}");
publishMessage("zigbee2mqtt/light/brightness", "{\"br\": \"128\"}");
assertState(component, Light.COLOR_CHANNEL_ID, new HSBType(new DecimalType(180), new PercentType(50),
new PercentType(new BigDecimal(128 * 100).divide(new BigDecimal(255), MathContext.DECIMAL128))));
sendCommand(component, Light.COLOR_CHANNEL_ID, HSBType.BLUE);
assertPublished("zigbee2mqtt/light/set/brightness", "255");
assertPublished("zigbee2mqtt/light/set/hs", "240,100");
// Brightness commands should route to the correct topic
sendCommand(component, Light.COLOR_CHANNEL_ID, new PercentType(50));
assertPublished("zigbee2mqtt/light/set/brightness", "128");
// OnOff commands should route to the correct topic
sendCommand(component, Light.COLOR_CHANNEL_ID, OnOffType.OFF);
assertPublished("zigbee2mqtt/light/set/state", "OFF_");
}
@Test
public void testBrightnessAndOnOff() throws InterruptedException {
// @formatter:off
var component = (Light) discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
"{ " +
" \"name\": \"light\", " +
" \"state_topic\": \"zigbee2mqtt/light/state\", " +
" \"command_topic\": \"zigbee2mqtt/light/set/state\", " +
" \"state_value_template\": \"{{ value_json.power }}\", " +
" \"brightness_state_topic\": \"zigbee2mqtt/light/brightness\", " +
" \"brightness_command_topic\": \"zigbee2mqtt/light/set/brightness\", " +
" \"payload_on\": \"ON_\", " +
" \"payload_off\": \"OFF_\" " +
"}");
// @formatter:on
assertThat(component.channels.size(), is(1));
assertThat(component.getName(), is("light"));
assertChannel(component, Light.BRIGHTNESS_CHANNEL_ID, "zigbee2mqtt/light/brightness",
"zigbee2mqtt/light/set/brightness", "Brightness", PercentageValue.class);
@Nullable
ComponentChannel onOffChannel = component.onOffChannel;
assertThat(onOffChannel, is(notNullValue()));
if (onOffChannel != null) {
assertChannel(onOffChannel, "zigbee2mqtt/light/state", "zigbee2mqtt/light/set/state", "On/Off State",
OnOffValue.class);
}
publishMessage("zigbee2mqtt/light/brightness", "128");
assertState(component, Light.BRIGHTNESS_CHANNEL_ID,
new PercentType(new BigDecimal(128 * 100).divide(new BigDecimal(255), MathContext.DECIMAL128)));
publishMessage("zigbee2mqtt/light/brightness", "64");
assertState(component, Light.BRIGHTNESS_CHANNEL_ID,
new PercentType(new BigDecimal(64 * 100).divide(new BigDecimal(255), MathContext.DECIMAL128)));
sendCommand(component, Light.BRIGHTNESS_CHANNEL_ID, OnOffType.OFF);
assertPublished("zigbee2mqtt/light/set/state", "OFF_");
sendCommand(component, Light.BRIGHTNESS_CHANNEL_ID, OnOffType.ON);
assertPublished("zigbee2mqtt/light/set/state", "ON_");
}
@Test
public void testOnOffOnly() throws InterruptedException {
// @formatter:off
var component = (Light) discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
"{ " +
" \"name\": \"light\", " +
" \"state_topic\": \"zigbee2mqtt/light/state\", " +
" \"command_topic\": \"zigbee2mqtt/light/set/state\", " +
" \"state_value_template\": \"{{ value_json.power }}\", " +
" \"payload_on\": \"ON_\", " +
" \"payload_off\": \"OFF_\" " +
"}");
// @formatter:on
assertThat(component.channels.size(), is(1));
assertThat(component.getName(), is("light"));
assertChannel(component, Light.ON_OFF_CHANNEL_ID, "zigbee2mqtt/light/state", "zigbee2mqtt/light/set/state",
"On/Off State", OnOffValue.class);
assertThat(component.brightnessChannel, is(nullValue()));
publishMessage("zigbee2mqtt/light/state", "{\"power\": \"ON_\"}");
assertState(component, Light.ON_OFF_CHANNEL_ID, OnOffType.ON);
publishMessage("zigbee2mqtt/light/state", "{\"power\": \"OFF_\"}");
assertState(component, Light.ON_OFF_CHANNEL_ID, OnOffType.OFF);
sendCommand(component, Light.ON_OFF_CHANNEL_ID, OnOffType.OFF);
assertPublished("zigbee2mqtt/light/set/state", "OFF_");
}
@Override
protected Set<String> getConfigTopics() {
return Set.of(CONFIG_TOPIC);
}
}

View File

@ -1,94 +0,0 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal.component;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.mqtt.generic.values.ColorValue;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
/**
* Tests for {@link Light}
* The current {@link Light} is non-compliant with the Specification and must be rewritten from scratch.
*
* @author Anton Kharuzhy - Initial contribution
*/
@NonNullByDefault
public class LightTests extends AbstractComponentTests {
public static final String CONFIG_TOPIC = "light/0x0000000000000000_light_zigbee2mqtt";
@Test
public void test() throws InterruptedException {
// @formatter:off
var component = (Light) discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
"{ " +
" \"availability\": [ " +
" { " +
" \"topic\": \"zigbee2mqtt/bridge/state\" " +
" } " +
" ], " +
" \"device\": { " +
" \"identifiers\": [ " +
" \"zigbee2mqtt_0x0000000000000000\" " +
" ], " +
" \"manufacturer\": \"Lights inc\", " +
" \"model\": \"light v1\", " +
" \"name\": \"Light\", " +
" \"sw_version\": \"Zigbee2MQTT 1.18.2\" " +
" }, " +
" \"name\": \"light\", " +
" \"state_topic\": \"zigbee2mqtt/light/state\", " +
" \"command_topic\": \"zigbee2mqtt/light/set/state\", " +
" \"state_value_template\": \"{{ value_json.power }}\", " +
" \"payload_on\": \"ON_\", " +
" \"payload_off\": \"OFF_\", " +
" \"rgb_state_topic\": \"zigbee2mqtt/light/rgb\", " +
" \"rgb_command_topic\": \"zigbee2mqtt/light/set/rgb\", " +
" \"rgb_value_template\": \"{{ value_json.rgb }}\", " +
" \"brightness_state_topic\": \"zigbee2mqtt/light/brightness\", " +
" \"brightness_command_topic\": \"zigbee2mqtt/light/set/brightness\", " +
" \"brightness_value_template\": \"{{ value_json.br }}\" " +
"}");
// @formatter:on
assertThat(component.channels.size(), is(1));
assertThat(component.getName(), is("light"));
assertChannel(component, Light.COLOR_CHANNEL_ID, "zigbee2mqtt/light/rgb", "zigbee2mqtt/light/set/rgb", "light",
ColorValue.class);
assertChannel(component.switchChannel, "zigbee2mqtt/light/state", "zigbee2mqtt/light/set/state", "light",
ColorValue.class);
assertChannel(component.brightnessChannel, "zigbee2mqtt/light/brightness", "zigbee2mqtt/light/set/brightness",
"light", ColorValue.class);
publishMessage("zigbee2mqtt/light/rgb", "{\"rgb\": \"255,255,255\"}");
assertState(component, Light.COLOR_CHANNEL_ID, HSBType.fromRGB(255, 255, 255));
publishMessage("zigbee2mqtt/light/rgb", "{\"rgb\": \"10,20,30\"}");
assertState(component, Light.COLOR_CHANNEL_ID, HSBType.fromRGB(10, 20, 30));
component.switchChannel.getState().publishValue(OnOffType.OFF);
assertPublished("zigbee2mqtt/light/set/state", "0,0,0");
}
@Override
protected Set<String> getConfigTopics() {
return Set.of(CONFIG_TOPIC);
}
}