mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-25 14:55:55 +01:00
[mqtt.homeassistant] Implement template schema lights (#17399)
* [mqtt.homeassistant] implement template schema lights Signed-off-by: Cody Cutrer <cody@cutrer.us>
This commit is contained in:
parent
63efac653c
commit
436dea619d
@ -63,7 +63,10 @@ public class HomeAssistantChannelTransformation extends ChannelTransformation {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<String> apply(String value) {
|
public Optional<String> apply(String value) {
|
||||||
String transformationResult;
|
return apply(template, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> apply(String template, String value) {
|
||||||
Map<String, @Nullable Object> bindings = new HashMap<>();
|
Map<String, @Nullable Object> bindings = new HashMap<>();
|
||||||
|
|
||||||
logger.debug("about to transform '{}' by the function '{}'", value, template);
|
logger.debug("about to transform '{}' by the function '{}'", value, template);
|
||||||
@ -77,6 +80,12 @@ public class HomeAssistantChannelTransformation extends ChannelTransformation {
|
|||||||
// ok, then value_json is null...
|
// ok, then value_json is null...
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return apply(template, bindings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> apply(String template, Map<String, @Nullable Object> bindings) {
|
||||||
|
String transformationResult;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
transformationResult = jinjava.render(template, bindings);
|
transformationResult = jinjava.render(template, bindings);
|
||||||
} catch (FatalTemplateErrorsException e) {
|
} catch (FatalTemplateErrorsException e) {
|
||||||
|
@ -31,6 +31,7 @@ abstract class AbstractRawSchemaLight extends Light {
|
|||||||
protected static final String RAW_CHANNEL_ID = "raw";
|
protected static final String RAW_CHANNEL_ID = "raw";
|
||||||
|
|
||||||
protected ComponentChannel rawChannel;
|
protected ComponentChannel rawChannel;
|
||||||
|
protected TextValue colorModeValue;
|
||||||
|
|
||||||
public AbstractRawSchemaLight(ComponentFactory.ComponentConfiguration builder, boolean newStyleChannels) {
|
public AbstractRawSchemaLight(ComponentFactory.ComponentConfiguration builder, boolean newStyleChannels) {
|
||||||
super(builder, newStyleChannels);
|
super(builder, newStyleChannels);
|
||||||
@ -39,6 +40,7 @@ abstract class AbstractRawSchemaLight extends Light {
|
|||||||
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
|
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
|
||||||
channelConfiguration.getQos())
|
channelConfiguration.getQos())
|
||||||
.build(false));
|
.build(false));
|
||||||
|
colorModeValue = new TextValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean handleCommand(Command command) {
|
protected boolean handleCommand(Command command) {
|
||||||
|
@ -71,11 +71,8 @@ public class JSONSchemaLight extends AbstractRawSchemaLight {
|
|||||||
protected @Nullable Integer transition;
|
protected @Nullable Integer transition;
|
||||||
}
|
}
|
||||||
|
|
||||||
TextValue colorModeValue;
|
|
||||||
|
|
||||||
public JSONSchemaLight(ComponentFactory.ComponentConfiguration builder, boolean newStyleChannels) {
|
public JSONSchemaLight(ComponentFactory.ComponentConfiguration builder, boolean newStyleChannels) {
|
||||||
super(builder, newStyleChannels);
|
super(builder, newStyleChannels);
|
||||||
colorModeValue = new TextValue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -251,6 +251,8 @@ public abstract class Light extends AbstractComponent<Light.ChannelConfiguration
|
|||||||
return new DefaultSchemaLight(builder, newStyleChannels);
|
return new DefaultSchemaLight(builder, newStyleChannels);
|
||||||
case JSON_SCHEMA:
|
case JSON_SCHEMA:
|
||||||
return new JSONSchemaLight(builder, newStyleChannels);
|
return new JSONSchemaLight(builder, newStyleChannels);
|
||||||
|
case TEMPLATE_SCHEMA:
|
||||||
|
return new TemplateSchemaLight(builder, newStyleChannels);
|
||||||
default:
|
default:
|
||||||
throw new UnsupportedComponentException(
|
throw new UnsupportedComponentException(
|
||||||
"Component '" + builder.getHaID() + "' of schema '" + schema + "' is not supported!");
|
"Component '" + builder.getHaID() + "' of schema '" + schema + "' is not supported!");
|
||||||
|
@ -0,0 +1,310 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2024 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.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
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.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.ComponentChannelType;
|
||||||
|
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantChannelTransformation;
|
||||||
|
import org.openhab.binding.mqtt.homeassistant.internal.exception.UnsupportedComponentException;
|
||||||
|
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.QuantityType;
|
||||||
|
import org.openhab.core.library.types.StringType;
|
||||||
|
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 org.openhab.core.types.UnDefType;
|
||||||
|
import org.openhab.core.util.ColorUtil;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A MQTT light, following the https://www.home-assistant.io/components/light.mqtt/ specification.
|
||||||
|
*
|
||||||
|
* Specifically, the template schema. All channels are synthetic, and wrap the single internal raw
|
||||||
|
* state.
|
||||||
|
*
|
||||||
|
* @author Cody Cutrer - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(TemplateSchemaLight.class);
|
||||||
|
private final HomeAssistantChannelTransformation transformation;
|
||||||
|
|
||||||
|
private static class TemplateVariables {
|
||||||
|
public static final String STATE = "state";
|
||||||
|
public static final String TRANSITION = "transition";
|
||||||
|
public static final String BRIGHTNESS = "brightness";
|
||||||
|
public static final String COLOR_TEMP = "color_temp";
|
||||||
|
public static final String RED = "red";
|
||||||
|
public static final String GREEN = "green";
|
||||||
|
public static final String BLUE = "blue";
|
||||||
|
public static final String HUE = "hue";
|
||||||
|
public static final String SAT = "sat";
|
||||||
|
public static final String FLASH = "flash";
|
||||||
|
public static final String EFFECT = "effect";
|
||||||
|
}
|
||||||
|
|
||||||
|
public TemplateSchemaLight(ComponentFactory.ComponentConfiguration builder, boolean newStyleChannels) {
|
||||||
|
super(builder, newStyleChannels);
|
||||||
|
transformation = new HomeAssistantChannelTransformation(getJinjava(), this, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void buildChannels() {
|
||||||
|
if (channelConfiguration.commandOnTemplate == null || channelConfiguration.commandOffTemplate == null) {
|
||||||
|
throw new UnsupportedComponentException("Template schema light component '" + getHaID()
|
||||||
|
+ "' does not define command_on_template or command_off_template!");
|
||||||
|
}
|
||||||
|
|
||||||
|
onOffValue = new OnOffValue("on", "off");
|
||||||
|
brightnessValue = new PercentageValue(null, new BigDecimal(255), null, null, null);
|
||||||
|
|
||||||
|
if (channelConfiguration.redTemplate != null && channelConfiguration.greenTemplate != null
|
||||||
|
&& channelConfiguration.blueTemplate != null) {
|
||||||
|
hasColorChannel = true;
|
||||||
|
buildChannel(COLOR_CHANNEL_ID, ComponentChannelType.COLOR, colorValue, "Color", this)
|
||||||
|
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleCommand(command)).build();
|
||||||
|
} else if (channelConfiguration.brightnessTemplate != null) {
|
||||||
|
brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, ComponentChannelType.DIMMER, brightnessValue,
|
||||||
|
"Brightness", this).commandTopic(DUMMY_TOPIC, true, 1)
|
||||||
|
.commandFilter(command -> handleCommand(command)).build();
|
||||||
|
} else {
|
||||||
|
onOffChannel = buildChannel(ON_OFF_CHANNEL_ID, ComponentChannelType.SWITCH, onOffValue, "On/Off State",
|
||||||
|
this).commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleCommand(command)).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channelConfiguration.colorTempTemplate != null) {
|
||||||
|
buildChannel(COLOR_TEMP_CHANNEL_ID, ComponentChannelType.NUMBER, colorTempValue, "Color Temperature", this)
|
||||||
|
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleColorTempCommand(command))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
TextValue localEffectValue = effectValue;
|
||||||
|
if (channelConfiguration.effectTemplate != null && localEffectValue != null) {
|
||||||
|
buildChannel(EFFECT_CHANNEL_ID, ComponentChannelType.STRING, localEffectValue, "Effect", this)
|
||||||
|
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleEffectCommand(command)).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BigDecimal factor = new BigDecimal("2.55"); // string to not lose precision
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void publishState(HSBType state) {
|
||||||
|
Map<String, @Nullable Object> binding = new HashMap<>();
|
||||||
|
String template;
|
||||||
|
|
||||||
|
logger.trace("Publishing new state {} of light {} to MQTT.", state, getName());
|
||||||
|
if (state.getBrightness().equals(PercentType.ZERO)) {
|
||||||
|
template = Objects.requireNonNull(channelConfiguration.commandOffTemplate);
|
||||||
|
binding.put(TemplateVariables.STATE, "off");
|
||||||
|
} else {
|
||||||
|
template = Objects.requireNonNull(channelConfiguration.commandOnTemplate);
|
||||||
|
binding.put(TemplateVariables.STATE, "on");
|
||||||
|
if (channelConfiguration.brightnessTemplate != null) {
|
||||||
|
binding.put(TemplateVariables.BRIGHTNESS,
|
||||||
|
state.getBrightness().toBigDecimal().multiply(factor).intValue());
|
||||||
|
}
|
||||||
|
if (hasColorChannel) {
|
||||||
|
int[] rgb = ColorUtil.hsbToRgb(state);
|
||||||
|
binding.put(TemplateVariables.RED, rgb[0]);
|
||||||
|
binding.put(TemplateVariables.GREEN, rgb[1]);
|
||||||
|
binding.put(TemplateVariables.BLUE, rgb[2]);
|
||||||
|
binding.put(TemplateVariables.HUE, state.getHue().toBigDecimal());
|
||||||
|
binding.put(TemplateVariables.SAT, state.getSaturation().toBigDecimal());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
publishState(binding, template);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean handleColorTempCommand(Command command) {
|
||||||
|
if (command instanceof DecimalType) {
|
||||||
|
command = new QuantityType<>(((DecimalType) command).toBigDecimal(), Units.MIRED);
|
||||||
|
}
|
||||||
|
if (command instanceof QuantityType quantity) {
|
||||||
|
QuantityType<?> mireds = quantity.toInvertibleUnit(Units.MIRED);
|
||||||
|
if (mireds == null) {
|
||||||
|
logger.warn("Unable to convert {} to mireds", command);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, @Nullable Object> binding = new HashMap<>();
|
||||||
|
|
||||||
|
binding.put(TemplateVariables.STATE, "on");
|
||||||
|
binding.put(TemplateVariables.COLOR_TEMP, mireds.toBigDecimal().intValue());
|
||||||
|
|
||||||
|
publishState(binding, Objects.requireNonNull(channelConfiguration.commandOnTemplate));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean handleEffectCommand(Command command) {
|
||||||
|
if (!(command instanceof StringType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, @Nullable Object> binding = new HashMap<>();
|
||||||
|
|
||||||
|
binding.put(TemplateVariables.STATE, "on");
|
||||||
|
binding.put(TemplateVariables.EFFECT, command.toString());
|
||||||
|
|
||||||
|
publishState(binding, Objects.requireNonNull(channelConfiguration.commandOnTemplate));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void publishState(Map<String, @Nullable Object> binding, String template) {
|
||||||
|
String command;
|
||||||
|
|
||||||
|
command = transform(template, binding);
|
||||||
|
if (command == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Publishing new state '{}' of light {} to MQTT.", command, getHaID().toShortTopic());
|
||||||
|
rawChannel.getState().publishValue(new StringType(command));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateChannelState(ChannelUID channel, State state) {
|
||||||
|
ChannelStateUpdateListener listener = this.channelStateUpdateListener;
|
||||||
|
|
||||||
|
String value;
|
||||||
|
|
||||||
|
String template = channelConfiguration.stateTemplate;
|
||||||
|
if (template != null) {
|
||||||
|
value = transform(template, state.toString());
|
||||||
|
if (value == null || value.isEmpty()) {
|
||||||
|
onOffValue.update(UnDefType.NULL);
|
||||||
|
} else if ("on".equals(value)) {
|
||||||
|
onOffValue.update(OnOffType.ON);
|
||||||
|
} else if ("off".equals(value)) {
|
||||||
|
onOffValue.update(OnOffType.OFF);
|
||||||
|
} else {
|
||||||
|
logger.warn("Invalid state value '{}' for component {}; expected 'on' or 'off'.", value,
|
||||||
|
getHaID().toShortTopic());
|
||||||
|
onOffValue.update(UnDefType.UNDEF);
|
||||||
|
}
|
||||||
|
if (brightnessValue.getChannelState() instanceof UnDefType
|
||||||
|
&& !(onOffValue.getChannelState() instanceof UnDefType)) {
|
||||||
|
brightnessValue.update(
|
||||||
|
(PercentType) Objects.requireNonNull(onOffValue.getChannelState().as(PercentType.class)));
|
||||||
|
}
|
||||||
|
if (colorValue.getChannelState() instanceof UnDefType) {
|
||||||
|
colorValue.update((OnOffType) onOffValue.getChannelState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template = channelConfiguration.brightnessTemplate;
|
||||||
|
if (template != null) {
|
||||||
|
Integer brightness = getColorChannelValue(template, state.toString());
|
||||||
|
if (brightness == null) {
|
||||||
|
brightnessValue.update(UnDefType.NULL);
|
||||||
|
colorValue.update(UnDefType.NULL);
|
||||||
|
} else {
|
||||||
|
brightnessValue.update((PercentType) brightnessValue.parseMessage(new DecimalType(brightness)));
|
||||||
|
if (colorValue.getChannelState() instanceof HSBType color) {
|
||||||
|
colorValue.update(new HSBType(color.getHue(), color.getSaturation(),
|
||||||
|
(PercentType) brightnessValue.getChannelState()));
|
||||||
|
} else {
|
||||||
|
colorValue.update(new HSBType(DecimalType.ZERO, PercentType.ZERO,
|
||||||
|
(PercentType) brightnessValue.getChannelState()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
String redTemplate, greenTemplate, blueTemplate;
|
||||||
|
if ((redTemplate = channelConfiguration.redTemplate) != null
|
||||||
|
&& (greenTemplate = channelConfiguration.greenTemplate) != null
|
||||||
|
&& (blueTemplate = channelConfiguration.blueTemplate) != null) {
|
||||||
|
Integer red = getColorChannelValue(redTemplate, state.toString());
|
||||||
|
Integer green = getColorChannelValue(greenTemplate, state.toString());
|
||||||
|
Integer blue = getColorChannelValue(blueTemplate, state.toString());
|
||||||
|
if (red == null || green == null || blue == null) {
|
||||||
|
colorValue.update(UnDefType.NULL);
|
||||||
|
} else {
|
||||||
|
colorValue.update(HSBType.fromRGB(red, green, blue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasColorChannel) {
|
||||||
|
listener.updateChannelState(buildChannelUID(COLOR_CHANNEL_ID), colorValue.getChannelState());
|
||||||
|
} else if (brightnessChannel != null) {
|
||||||
|
listener.updateChannelState(buildChannelUID(BRIGHTNESS_CHANNEL_ID), brightnessValue.getChannelState());
|
||||||
|
} else {
|
||||||
|
listener.updateChannelState(buildChannelUID(ON_OFF_CHANNEL_ID), onOffValue.getChannelState());
|
||||||
|
}
|
||||||
|
|
||||||
|
template = channelConfiguration.effectTemplate;
|
||||||
|
if (template != null) {
|
||||||
|
value = transform(template, state.toString());
|
||||||
|
if (value == null || value.isEmpty()) {
|
||||||
|
effectValue.update(UnDefType.NULL);
|
||||||
|
} else {
|
||||||
|
effectValue.update(new StringType(value));
|
||||||
|
}
|
||||||
|
listener.updateChannelState(buildChannelUID(EFFECT_CHANNEL_ID), effectValue.getChannelState());
|
||||||
|
}
|
||||||
|
|
||||||
|
template = channelConfiguration.colorTempTemplate;
|
||||||
|
if (template != null) {
|
||||||
|
Integer mireds = getColorChannelValue(template, state.toString());
|
||||||
|
if (mireds == null) {
|
||||||
|
colorTempValue.update(UnDefType.NULL);
|
||||||
|
} else {
|
||||||
|
colorTempValue.update(new QuantityType(mireds, Units.MIRED));
|
||||||
|
}
|
||||||
|
listener.updateChannelState(buildChannelUID(COLOR_TEMP_CHANNEL_ID), colorTempValue.getChannelState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable Integer getColorChannelValue(String template, String value) {
|
||||||
|
Object result = transform(template, value);
|
||||||
|
if (result == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String string = result.toString();
|
||||||
|
if (string.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(result.toString());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
logger.warn("Applying template {} for component {} failed: {}", template, getHaID().toShortTopic(),
|
||||||
|
e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable String transform(String template, Map<String, @Nullable Object> binding) {
|
||||||
|
return transformation.apply(template, binding).orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable String transform(String template, String value) {
|
||||||
|
return transformation.apply(template, value).orElse(null);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,204 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2024 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.math.BigDecimal;
|
||||||
|
import java.math.MathContext;
|
||||||
|
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.binding.mqtt.generic.values.NumberValue;
|
||||||
|
import org.openhab.binding.mqtt.generic.values.OnOffValue;
|
||||||
|
import org.openhab.binding.mqtt.generic.values.PercentageValue;
|
||||||
|
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.QuantityType;
|
||||||
|
import org.openhab.core.library.unit.Units;
|
||||||
|
import org.openhab.core.types.UnDefType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link Light} conforming to the template schema
|
||||||
|
*
|
||||||
|
* @author Cody Cutrer - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class TemplateSchemaLightTests extends AbstractComponentTests {
|
||||||
|
public static final String CONFIG_TOPIC = "light/0x0000000000000000_light_zigbee2mqtt";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRgb() throws InterruptedException {
|
||||||
|
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",
|
||||||
|
"schema": "template",
|
||||||
|
"state_topic": "zigbee2mqtt/light/state",
|
||||||
|
"command_topic": "zigbee2mqtt/light/set/state",
|
||||||
|
"command_on_template": "{{state}},{{red}},{{green}},{{blue}}",
|
||||||
|
"command_off_template": "off",
|
||||||
|
"state_template": "{{value_json.state}}",
|
||||||
|
"red_template": "{{value_json.r}}",
|
||||||
|
"green_template": "{{value_json.g}}",
|
||||||
|
"blue_template": "{{value_json.b}}",
|
||||||
|
"brightness_template": "{{value_json.brightness}}"
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
assertThat(component.channels.size(), is(1));
|
||||||
|
assertThat(component.getName(), is("light"));
|
||||||
|
|
||||||
|
assertChannel(component, Light.COLOR_CHANNEL_ID, "", "dummy", "Color", ColorValue.class);
|
||||||
|
|
||||||
|
publishMessage("zigbee2mqtt/light/state", """
|
||||||
|
{ "state": "on", "r": 255, "g": 255, "b": 255, "brightness": 255 }
|
||||||
|
""");
|
||||||
|
assertState(component, Light.COLOR_CHANNEL_ID, HSBType.WHITE);
|
||||||
|
|
||||||
|
sendCommand(component, Light.COLOR_CHANNEL_ID, HSBType.BLUE);
|
||||||
|
assertPublished("zigbee2mqtt/light/set/state", "on,0,0,255");
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
var component = (Light) discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
|
||||||
|
{
|
||||||
|
"name": "light",
|
||||||
|
"schema": "template",
|
||||||
|
"state_topic": "zigbee2mqtt/light/state",
|
||||||
|
"command_topic": "zigbee2mqtt/light/set/state",
|
||||||
|
"command_on_template": "{{state}},{{brightness}}",
|
||||||
|
"command_off_template": "off",
|
||||||
|
"state_template": "{{value_json.state}}",
|
||||||
|
"brightness_template": "{{value_json.brightness}}"
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
assertThat(component.channels.size(), is(1));
|
||||||
|
assertThat(component.getName(), is("light"));
|
||||||
|
|
||||||
|
assertChannel(component, Light.BRIGHTNESS_CHANNEL_ID, "", "dummy", "Brightness", PercentageValue.class);
|
||||||
|
|
||||||
|
publishMessage("zigbee2mqtt/light/state", "{ \"state\": \"on\", \"brightness\": 128 }");
|
||||||
|
assertState(component, Light.BRIGHTNESS_CHANNEL_ID,
|
||||||
|
new PercentType(new BigDecimal(128 * 100).divide(new BigDecimal(255), MathContext.DECIMAL128)));
|
||||||
|
|
||||||
|
sendCommand(component, Light.BRIGHTNESS_CHANNEL_ID, PercentType.HUNDRED);
|
||||||
|
assertPublished("zigbee2mqtt/light/set/state", "on,255");
|
||||||
|
|
||||||
|
sendCommand(component, Light.BRIGHTNESS_CHANNEL_ID, OnOffType.OFF);
|
||||||
|
assertPublished("zigbee2mqtt/light/set/state", "off");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBrightnessAndCCT() throws InterruptedException {
|
||||||
|
var component = (Light) discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"schema": "template",
|
||||||
|
"name": "Bulb-white",
|
||||||
|
"command_topic": "shellies/bulb/color/0/set",
|
||||||
|
"state_topic": "shellies/bulb/color/0/status",
|
||||||
|
"availability_topic": "shellies/bulb/online",
|
||||||
|
"command_on_template": "{\\"turn\\": \\"on\\", \\"mode\\": \\"white\\"{%- if brightness is defined -%}, \\"brightness\\": {{brightness | float | multiply(0.39215686) | round(0)}}{%- endif -%}{%- if color_temp is defined -%}, \\"temp\\": {{ (1000000 / color_temp | float) | round(0) }}{%- endif -%}}",
|
||||||
|
"command_off_template": "{\\"turn\\":\\"off\\", \\"mode\\": \\"white\\"}",
|
||||||
|
"state_template": "{% if value_json.ison and value_json.mode == 'white' %}on{% else %}off{% endif %}",
|
||||||
|
"brightness_template": "{{ value_json.brightness | float | multiply(2.55) | round(0) }}",
|
||||||
|
"color_temp_template": "{{ (1000000 / value_json.temp | float) | round(0) }}",
|
||||||
|
"payload_available": "true",
|
||||||
|
"payload_not_available": "false",
|
||||||
|
"max_mireds": 334,
|
||||||
|
"min_mireds": 153,
|
||||||
|
"qos": 1,
|
||||||
|
"retain": false,
|
||||||
|
"optimistic": false
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
assertThat(component.channels.size(), is(2));
|
||||||
|
assertThat(component.getName(), is("Bulb-white"));
|
||||||
|
|
||||||
|
assertChannel(component, Light.BRIGHTNESS_CHANNEL_ID, "", "dummy", "Brightness", PercentageValue.class);
|
||||||
|
assertChannel(component, Light.COLOR_TEMP_CHANNEL_ID, "", "dummy", "Color Temperature", NumberValue.class);
|
||||||
|
|
||||||
|
publishMessage("shellies/bulb/color/0/status", "{ \"state\": \"on\", \"brightness\": 100 }");
|
||||||
|
assertState(component, Light.BRIGHTNESS_CHANNEL_ID, PercentType.HUNDRED);
|
||||||
|
assertState(component, Light.COLOR_TEMP_CHANNEL_ID, UnDefType.NULL);
|
||||||
|
|
||||||
|
sendCommand(component, Light.BRIGHTNESS_CHANNEL_ID, PercentType.HUNDRED);
|
||||||
|
assertPublished("shellies/bulb/color/0/set", "{\"turn\": \"on\", \"mode\": \"white\", \"brightness\": 100}");
|
||||||
|
|
||||||
|
sendCommand(component, Light.BRIGHTNESS_CHANNEL_ID, OnOffType.OFF);
|
||||||
|
assertPublished("shellies/bulb/color/0/set", "{\"turn\":\"off\", \"mode\": \"white\"}");
|
||||||
|
|
||||||
|
sendCommand(component, Light.COLOR_TEMP_CHANNEL_ID, new QuantityType(200, Units.MIRED));
|
||||||
|
assertPublished("shellies/bulb/color/0/set", "{\"turn\": \"on\", \"mode\": \"white\", \"temp\": 5000}");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOnOffOnly() throws InterruptedException {
|
||||||
|
var component = (Light) discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
|
||||||
|
{
|
||||||
|
"name": "light",
|
||||||
|
"schema": "template",
|
||||||
|
"state_topic": "zigbee2mqtt/light/state",
|
||||||
|
"command_topic": "zigbee2mqtt/light/set/state",
|
||||||
|
"state_template": "{{ value_json.power }}",
|
||||||
|
"command_on_template": "{{state}}",
|
||||||
|
"command_off_template": "off"
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
assertThat(component.channels.size(), is(1));
|
||||||
|
assertThat(component.getName(), is("light"));
|
||||||
|
|
||||||
|
assertChannel(component, Light.ON_OFF_CHANNEL_ID, "", "dummy", "On/Off State", OnOffValue.class);
|
||||||
|
|
||||||
|
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");
|
||||||
|
sendCommand(component, Light.ON_OFF_CHANNEL_ID, OnOffType.ON);
|
||||||
|
assertPublished("zigbee2mqtt/light/set/state", "on");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Set<String> getConfigTopics() {
|
||||||
|
return Set.of(CONFIG_TOPIC);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user