-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[mqtt.homeassistant] implement template schema lights
Signed-off-by: Cody Cutrer <[email protected]>
- Loading branch information
Showing
6 changed files
with
531 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
313 changes: 313 additions & 0 deletions
313
...n/java/org/openhab/binding/mqtt/homeassistant/internal/component/TemplateSchemaLight.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,313 @@ | ||
/** | ||
* Copyright (c) 2010-2022 Contributors to the openHAB project | ||
Check failure on line 2 in bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/TemplateSchemaLight.java GitHub Actions / Build (Java 17, ubuntu-22.04)
|
||
* | ||
* 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 java.util.Optional; | ||
|
||
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.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) { | ||
PercentType[] rgb = state.toRGB(); | ||
binding.put(TemplateVariables.RED, rgb[0].toBigDecimal().multiply(factor).intValue()); | ||
binding.put(TemplateVariables.GREEN, rgb[1].toBigDecimal().multiply(factor).intValue()); | ||
binding.put(TemplateVariables.BLUE, rgb[2].toBigDecimal().multiply(factor).intValue()); | ||
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) { | ||
QuantityType<?> mireds = ((QuantityType<?>) command).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 (value.equals("on")) { | ||
onOffValue.update(OnOffType.ON); | ||
} else if (value.equals("off")) { | ||
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) { | ||
HSBType color = (HSBType) colorValue.getChannelState(); | ||
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) { | ||
Optional<String> result = transformation.apply(template, binding); | ||
return result.orElse(null); | ||
} | ||
|
||
private @Nullable String transform(String template, String value) { | ||
Optional<String> result = transformation.apply(template, value); | ||
return result.orElse(null); | ||
} | ||
} |
Oops, something went wrong.