Skip to content

Commit

Permalink
[mqtt.homeassistant] Implement template schema lights (openhab#17399)
Browse files Browse the repository at this point in the history
* [mqtt.homeassistant] implement template schema lights

Signed-off-by: Cody Cutrer <[email protected]>
  • Loading branch information
ccutrer authored and digitaldan committed Sep 21, 2024
1 parent b4f79c5 commit 21a010e
Show file tree
Hide file tree
Showing 6 changed files with 528 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ public boolean isEmpty() {

@Override
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<>();

logger.debug("about to transform '{}' by the function '{}'", value, template);
Expand All @@ -77,6 +80,12 @@ public Optional<String> apply(String value) {
// ok, then value_json is null...
}

return apply(template, bindings);
}

public Optional<String> apply(String template, Map<String, @Nullable Object> bindings) {
String transformationResult;

try {
transformationResult = jinjava.render(template, bindings);
} catch (FatalTemplateErrorsException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ abstract class AbstractRawSchemaLight extends Light {
protected static final String RAW_CHANNEL_ID = "raw";

protected ComponentChannel rawChannel;
protected TextValue colorModeValue;

public AbstractRawSchemaLight(ComponentFactory.ComponentConfiguration builder, boolean newStyleChannels) {
super(builder, newStyleChannels);
Expand All @@ -39,6 +40,7 @@ public AbstractRawSchemaLight(ComponentFactory.ComponentConfiguration builder, b
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build(false));
colorModeValue = new TextValue();
}

protected boolean handleCommand(Command command) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,8 @@ protected static class Color {
protected @Nullable Integer transition;
}

TextValue colorModeValue;

public JSONSchemaLight(ComponentFactory.ComponentConfiguration builder, boolean newStyleChannels) {
super(builder, newStyleChannels);
colorModeValue = new TextValue();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ public static Light create(ComponentFactory.ComponentConfiguration builder, bool
return new DefaultSchemaLight(builder, newStyleChannels);
case JSON_SCHEMA:
return new JSONSchemaLight(builder, newStyleChannels);
case TEMPLATE_SCHEMA:
return new TemplateSchemaLight(builder, newStyleChannels);
default:
throw new UnsupportedComponentException(
"Component '" + builder.getHaID() + "' of schema '" + schema + "' is not supported!");
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit 21a010e

Please sign in to comment.