Skip to content

Commit 436dea6

Browse files
authored
[mqtt.homeassistant] Implement template schema lights (openhab#17399)
* [mqtt.homeassistant] implement template schema lights Signed-off-by: Cody Cutrer <[email protected]>
1 parent 63efac6 commit 436dea6

File tree

6 files changed

+528
-4
lines changed

6 files changed

+528
-4
lines changed

bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelTransformation.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@ public boolean isEmpty() {
6363

6464
@Override
6565
public Optional<String> apply(String value) {
66-
String transformationResult;
66+
return apply(template, value);
67+
}
68+
69+
public Optional<String> apply(String template, String value) {
6770
Map<String, @Nullable Object> bindings = new HashMap<>();
6871

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

83+
return apply(template, bindings);
84+
}
85+
86+
public Optional<String> apply(String template, Map<String, @Nullable Object> bindings) {
87+
String transformationResult;
88+
8089
try {
8190
transformationResult = jinjava.render(template, bindings);
8291
} catch (FatalTemplateErrorsException e) {

bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractRawSchemaLight.java

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ abstract class AbstractRawSchemaLight extends Light {
3131
protected static final String RAW_CHANNEL_ID = "raw";
3232

3333
protected ComponentChannel rawChannel;
34+
protected TextValue colorModeValue;
3435

3536
public AbstractRawSchemaLight(ComponentFactory.ComponentConfiguration builder, boolean newStyleChannels) {
3637
super(builder, newStyleChannels);
@@ -39,6 +40,7 @@ public AbstractRawSchemaLight(ComponentFactory.ComponentConfiguration builder, b
3940
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
4041
channelConfiguration.getQos())
4142
.build(false));
43+
colorModeValue = new TextValue();
4244
}
4345

4446
protected boolean handleCommand(Command command) {

bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/JSONSchemaLight.java

-3
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,8 @@ protected static class Color {
7171
protected @Nullable Integer transition;
7272
}
7373

74-
TextValue colorModeValue;
75-
7674
public JSONSchemaLight(ComponentFactory.ComponentConfiguration builder, boolean newStyleChannels) {
7775
super(builder, newStyleChannels);
78-
colorModeValue = new TextValue();
7976
}
8077

8178
@Override

bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Light.java

+2
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,8 @@ public static Light create(ComponentFactory.ComponentConfiguration builder, bool
251251
return new DefaultSchemaLight(builder, newStyleChannels);
252252
case JSON_SCHEMA:
253253
return new JSONSchemaLight(builder, newStyleChannels);
254+
case TEMPLATE_SCHEMA:
255+
return new TemplateSchemaLight(builder, newStyleChannels);
254256
default:
255257
throw new UnsupportedComponentException(
256258
"Component '" + builder.getHaID() + "' of schema '" + schema + "' is not supported!");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
/**
2+
* Copyright (c) 2010-2024 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.mqtt.homeassistant.internal.component;
14+
15+
import java.math.BigDecimal;
16+
import java.util.HashMap;
17+
import java.util.Map;
18+
import java.util.Objects;
19+
20+
import org.eclipse.jdt.annotation.NonNullByDefault;
21+
import org.eclipse.jdt.annotation.Nullable;
22+
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
23+
import org.openhab.binding.mqtt.generic.values.OnOffValue;
24+
import org.openhab.binding.mqtt.generic.values.PercentageValue;
25+
import org.openhab.binding.mqtt.generic.values.TextValue;
26+
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
27+
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantChannelTransformation;
28+
import org.openhab.binding.mqtt.homeassistant.internal.exception.UnsupportedComponentException;
29+
import org.openhab.core.library.types.DecimalType;
30+
import org.openhab.core.library.types.HSBType;
31+
import org.openhab.core.library.types.OnOffType;
32+
import org.openhab.core.library.types.PercentType;
33+
import org.openhab.core.library.types.QuantityType;
34+
import org.openhab.core.library.types.StringType;
35+
import org.openhab.core.library.unit.Units;
36+
import org.openhab.core.thing.ChannelUID;
37+
import org.openhab.core.types.Command;
38+
import org.openhab.core.types.State;
39+
import org.openhab.core.types.UnDefType;
40+
import org.openhab.core.util.ColorUtil;
41+
import org.slf4j.Logger;
42+
import org.slf4j.LoggerFactory;
43+
44+
/**
45+
* A MQTT light, following the https://www.home-assistant.io/components/light.mqtt/ specification.
46+
*
47+
* Specifically, the template schema. All channels are synthetic, and wrap the single internal raw
48+
* state.
49+
*
50+
* @author Cody Cutrer - Initial contribution
51+
*/
52+
@NonNullByDefault
53+
public class TemplateSchemaLight extends AbstractRawSchemaLight {
54+
private final Logger logger = LoggerFactory.getLogger(TemplateSchemaLight.class);
55+
private final HomeAssistantChannelTransformation transformation;
56+
57+
private static class TemplateVariables {
58+
public static final String STATE = "state";
59+
public static final String TRANSITION = "transition";
60+
public static final String BRIGHTNESS = "brightness";
61+
public static final String COLOR_TEMP = "color_temp";
62+
public static final String RED = "red";
63+
public static final String GREEN = "green";
64+
public static final String BLUE = "blue";
65+
public static final String HUE = "hue";
66+
public static final String SAT = "sat";
67+
public static final String FLASH = "flash";
68+
public static final String EFFECT = "effect";
69+
}
70+
71+
public TemplateSchemaLight(ComponentFactory.ComponentConfiguration builder, boolean newStyleChannels) {
72+
super(builder, newStyleChannels);
73+
transformation = new HomeAssistantChannelTransformation(getJinjava(), this, "");
74+
}
75+
76+
@Override
77+
protected void buildChannels() {
78+
if (channelConfiguration.commandOnTemplate == null || channelConfiguration.commandOffTemplate == null) {
79+
throw new UnsupportedComponentException("Template schema light component '" + getHaID()
80+
+ "' does not define command_on_template or command_off_template!");
81+
}
82+
83+
onOffValue = new OnOffValue("on", "off");
84+
brightnessValue = new PercentageValue(null, new BigDecimal(255), null, null, null);
85+
86+
if (channelConfiguration.redTemplate != null && channelConfiguration.greenTemplate != null
87+
&& channelConfiguration.blueTemplate != null) {
88+
hasColorChannel = true;
89+
buildChannel(COLOR_CHANNEL_ID, ComponentChannelType.COLOR, colorValue, "Color", this)
90+
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleCommand(command)).build();
91+
} else if (channelConfiguration.brightnessTemplate != null) {
92+
brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, ComponentChannelType.DIMMER, brightnessValue,
93+
"Brightness", this).commandTopic(DUMMY_TOPIC, true, 1)
94+
.commandFilter(command -> handleCommand(command)).build();
95+
} else {
96+
onOffChannel = buildChannel(ON_OFF_CHANNEL_ID, ComponentChannelType.SWITCH, onOffValue, "On/Off State",
97+
this).commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleCommand(command)).build();
98+
}
99+
100+
if (channelConfiguration.colorTempTemplate != null) {
101+
buildChannel(COLOR_TEMP_CHANNEL_ID, ComponentChannelType.NUMBER, colorTempValue, "Color Temperature", this)
102+
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleColorTempCommand(command))
103+
.build();
104+
}
105+
TextValue localEffectValue = effectValue;
106+
if (channelConfiguration.effectTemplate != null && localEffectValue != null) {
107+
buildChannel(EFFECT_CHANNEL_ID, ComponentChannelType.STRING, localEffectValue, "Effect", this)
108+
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleEffectCommand(command)).build();
109+
}
110+
}
111+
112+
private static BigDecimal factor = new BigDecimal("2.55"); // string to not lose precision
113+
114+
@Override
115+
protected void publishState(HSBType state) {
116+
Map<String, @Nullable Object> binding = new HashMap<>();
117+
String template;
118+
119+
logger.trace("Publishing new state {} of light {} to MQTT.", state, getName());
120+
if (state.getBrightness().equals(PercentType.ZERO)) {
121+
template = Objects.requireNonNull(channelConfiguration.commandOffTemplate);
122+
binding.put(TemplateVariables.STATE, "off");
123+
} else {
124+
template = Objects.requireNonNull(channelConfiguration.commandOnTemplate);
125+
binding.put(TemplateVariables.STATE, "on");
126+
if (channelConfiguration.brightnessTemplate != null) {
127+
binding.put(TemplateVariables.BRIGHTNESS,
128+
state.getBrightness().toBigDecimal().multiply(factor).intValue());
129+
}
130+
if (hasColorChannel) {
131+
int[] rgb = ColorUtil.hsbToRgb(state);
132+
binding.put(TemplateVariables.RED, rgb[0]);
133+
binding.put(TemplateVariables.GREEN, rgb[1]);
134+
binding.put(TemplateVariables.BLUE, rgb[2]);
135+
binding.put(TemplateVariables.HUE, state.getHue().toBigDecimal());
136+
binding.put(TemplateVariables.SAT, state.getSaturation().toBigDecimal());
137+
}
138+
}
139+
140+
publishState(binding, template);
141+
}
142+
143+
private boolean handleColorTempCommand(Command command) {
144+
if (command instanceof DecimalType) {
145+
command = new QuantityType<>(((DecimalType) command).toBigDecimal(), Units.MIRED);
146+
}
147+
if (command instanceof QuantityType quantity) {
148+
QuantityType<?> mireds = quantity.toInvertibleUnit(Units.MIRED);
149+
if (mireds == null) {
150+
logger.warn("Unable to convert {} to mireds", command);
151+
return false;
152+
}
153+
154+
Map<String, @Nullable Object> binding = new HashMap<>();
155+
156+
binding.put(TemplateVariables.STATE, "on");
157+
binding.put(TemplateVariables.COLOR_TEMP, mireds.toBigDecimal().intValue());
158+
159+
publishState(binding, Objects.requireNonNull(channelConfiguration.commandOnTemplate));
160+
}
161+
return false;
162+
}
163+
164+
private boolean handleEffectCommand(Command command) {
165+
if (!(command instanceof StringType)) {
166+
return false;
167+
}
168+
169+
Map<String, @Nullable Object> binding = new HashMap<>();
170+
171+
binding.put(TemplateVariables.STATE, "on");
172+
binding.put(TemplateVariables.EFFECT, command.toString());
173+
174+
publishState(binding, Objects.requireNonNull(channelConfiguration.commandOnTemplate));
175+
return false;
176+
}
177+
178+
private void publishState(Map<String, @Nullable Object> binding, String template) {
179+
String command;
180+
181+
command = transform(template, binding);
182+
if (command == null) {
183+
return;
184+
}
185+
186+
logger.debug("Publishing new state '{}' of light {} to MQTT.", command, getHaID().toShortTopic());
187+
rawChannel.getState().publishValue(new StringType(command));
188+
}
189+
190+
@Override
191+
public void updateChannelState(ChannelUID channel, State state) {
192+
ChannelStateUpdateListener listener = this.channelStateUpdateListener;
193+
194+
String value;
195+
196+
String template = channelConfiguration.stateTemplate;
197+
if (template != null) {
198+
value = transform(template, state.toString());
199+
if (value == null || value.isEmpty()) {
200+
onOffValue.update(UnDefType.NULL);
201+
} else if ("on".equals(value)) {
202+
onOffValue.update(OnOffType.ON);
203+
} else if ("off".equals(value)) {
204+
onOffValue.update(OnOffType.OFF);
205+
} else {
206+
logger.warn("Invalid state value '{}' for component {}; expected 'on' or 'off'.", value,
207+
getHaID().toShortTopic());
208+
onOffValue.update(UnDefType.UNDEF);
209+
}
210+
if (brightnessValue.getChannelState() instanceof UnDefType
211+
&& !(onOffValue.getChannelState() instanceof UnDefType)) {
212+
brightnessValue.update(
213+
(PercentType) Objects.requireNonNull(onOffValue.getChannelState().as(PercentType.class)));
214+
}
215+
if (colorValue.getChannelState() instanceof UnDefType) {
216+
colorValue.update((OnOffType) onOffValue.getChannelState());
217+
}
218+
}
219+
220+
template = channelConfiguration.brightnessTemplate;
221+
if (template != null) {
222+
Integer brightness = getColorChannelValue(template, state.toString());
223+
if (brightness == null) {
224+
brightnessValue.update(UnDefType.NULL);
225+
colorValue.update(UnDefType.NULL);
226+
} else {
227+
brightnessValue.update((PercentType) brightnessValue.parseMessage(new DecimalType(brightness)));
228+
if (colorValue.getChannelState() instanceof HSBType color) {
229+
colorValue.update(new HSBType(color.getHue(), color.getSaturation(),
230+
(PercentType) brightnessValue.getChannelState()));
231+
} else {
232+
colorValue.update(new HSBType(DecimalType.ZERO, PercentType.ZERO,
233+
(PercentType) brightnessValue.getChannelState()));
234+
}
235+
}
236+
}
237+
238+
@Nullable
239+
String redTemplate, greenTemplate, blueTemplate;
240+
if ((redTemplate = channelConfiguration.redTemplate) != null
241+
&& (greenTemplate = channelConfiguration.greenTemplate) != null
242+
&& (blueTemplate = channelConfiguration.blueTemplate) != null) {
243+
Integer red = getColorChannelValue(redTemplate, state.toString());
244+
Integer green = getColorChannelValue(greenTemplate, state.toString());
245+
Integer blue = getColorChannelValue(blueTemplate, state.toString());
246+
if (red == null || green == null || blue == null) {
247+
colorValue.update(UnDefType.NULL);
248+
} else {
249+
colorValue.update(HSBType.fromRGB(red, green, blue));
250+
}
251+
}
252+
253+
if (hasColorChannel) {
254+
listener.updateChannelState(buildChannelUID(COLOR_CHANNEL_ID), colorValue.getChannelState());
255+
} else if (brightnessChannel != null) {
256+
listener.updateChannelState(buildChannelUID(BRIGHTNESS_CHANNEL_ID), brightnessValue.getChannelState());
257+
} else {
258+
listener.updateChannelState(buildChannelUID(ON_OFF_CHANNEL_ID), onOffValue.getChannelState());
259+
}
260+
261+
template = channelConfiguration.effectTemplate;
262+
if (template != null) {
263+
value = transform(template, state.toString());
264+
if (value == null || value.isEmpty()) {
265+
effectValue.update(UnDefType.NULL);
266+
} else {
267+
effectValue.update(new StringType(value));
268+
}
269+
listener.updateChannelState(buildChannelUID(EFFECT_CHANNEL_ID), effectValue.getChannelState());
270+
}
271+
272+
template = channelConfiguration.colorTempTemplate;
273+
if (template != null) {
274+
Integer mireds = getColorChannelValue(template, state.toString());
275+
if (mireds == null) {
276+
colorTempValue.update(UnDefType.NULL);
277+
} else {
278+
colorTempValue.update(new QuantityType(mireds, Units.MIRED));
279+
}
280+
listener.updateChannelState(buildChannelUID(COLOR_TEMP_CHANNEL_ID), colorTempValue.getChannelState());
281+
}
282+
}
283+
284+
private @Nullable Integer getColorChannelValue(String template, String value) {
285+
Object result = transform(template, value);
286+
if (result == null) {
287+
return null;
288+
}
289+
290+
String string = result.toString();
291+
if (string.isEmpty()) {
292+
return null;
293+
}
294+
try {
295+
return Integer.parseInt(result.toString());
296+
} catch (NumberFormatException e) {
297+
logger.warn("Applying template {} for component {} failed: {}", template, getHaID().toShortTopic(),
298+
e.getMessage());
299+
return null;
300+
}
301+
}
302+
303+
private @Nullable String transform(String template, Map<String, @Nullable Object> binding) {
304+
return transformation.apply(template, binding).orElse(null);
305+
}
306+
307+
private @Nullable String transform(String template, String value) {
308+
return transformation.apply(template, value).orElse(null);
309+
}
310+
}

0 commit comments

Comments
 (0)