[mqtt.homeassistant] implement iif and is_defined jinja function and filters (#17435)
Signed-off-by: Cody Cutrer <cody@cutrer.us>pull/17365/head
parent
4ae8bb5559
commit
5de73f923a
|
@ -20,6 +20,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
|
import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
|
||||||
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
|
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
|
||||||
|
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantJinjaFunctionLibrary;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler;
|
import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler;
|
||||||
import org.openhab.core.thing.Thing;
|
import org.openhab.core.thing.Thing;
|
||||||
import org.openhab.core.thing.ThingTypeUID;
|
import org.openhab.core.thing.ThingTypeUID;
|
||||||
|
@ -57,6 +58,8 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
|
||||||
this.typeProvider = typeProvider;
|
this.typeProvider = typeProvider;
|
||||||
this.stateDescriptionProvider = stateDescriptionProvider;
|
this.stateDescriptionProvider = stateDescriptionProvider;
|
||||||
this.channelTypeRegistry = channelTypeRegistry;
|
this.channelTypeRegistry = channelTypeRegistry;
|
||||||
|
|
||||||
|
HomeAssistantJinjaFunctionLibrary.register(jinjava.getGlobalContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -79,4 +82,8 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Jinjava getJinjava() {
|
||||||
|
return jinjava;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,8 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.hubspot.jinjava.Jinjava;
|
import com.hubspot.jinjava.Jinjava;
|
||||||
import com.hubspot.jinjava.interpret.FatalTemplateErrorsException;
|
import com.hubspot.jinjava.interpret.FatalTemplateErrorsException;
|
||||||
|
import com.hubspot.jinjava.interpret.InvalidInputException;
|
||||||
|
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a channel transformation for a Home Assistant channel with a
|
* Provides a channel transformation for a Home Assistant channel with a
|
||||||
|
@ -42,6 +44,12 @@ import com.hubspot.jinjava.interpret.FatalTemplateErrorsException;
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class HomeAssistantChannelTransformation extends ChannelTransformation {
|
public class HomeAssistantChannelTransformation extends ChannelTransformation {
|
||||||
|
public static class UndefinedException extends InvalidInputException {
|
||||||
|
public UndefinedException(JinjavaInterpreter interpreter) {
|
||||||
|
super(interpreter, "is_defined", "Value is undefined");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(HomeAssistantChannelTransformation.class);
|
private final Logger logger = LoggerFactory.getLogger(HomeAssistantChannelTransformation.class);
|
||||||
|
|
||||||
private final Jinjava jinjava;
|
private final Jinjava jinjava;
|
||||||
|
@ -89,8 +97,17 @@ public class HomeAssistantChannelTransformation extends ChannelTransformation {
|
||||||
try {
|
try {
|
||||||
transformationResult = jinjava.render(template, bindings);
|
transformationResult = jinjava.render(template, bindings);
|
||||||
} catch (FatalTemplateErrorsException e) {
|
} catch (FatalTemplateErrorsException e) {
|
||||||
logger.warn("Applying template {} for component {} failed: {}", template,
|
var error = e.getErrors().iterator();
|
||||||
component.getHaID().toShortTopic(), e.getMessage());
|
Exception exception = null;
|
||||||
|
if (error.hasNext()) {
|
||||||
|
exception = error.next().getException();
|
||||||
|
}
|
||||||
|
if (exception instanceof UndefinedException) {
|
||||||
|
// They used the is_defined filter; it's expected to return null, with no warning
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
logger.warn("Applying template {} for component {} failed: {} ({})", template,
|
||||||
|
component.getHaID().toShortTopic(), e.getMessage(), e.getClass());
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
|
||||||
|
import com.hubspot.jinjava.interpret.Context;
|
||||||
|
import com.hubspot.jinjava.interpret.InterpretException;
|
||||||
|
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
|
||||||
|
import com.hubspot.jinjava.lib.filter.Filter;
|
||||||
|
import com.hubspot.jinjava.lib.fn.ELFunctionDefinition;
|
||||||
|
import com.hubspot.jinjava.util.ObjectTruthValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains extensions methods exposed in Jinja transformations
|
||||||
|
*
|
||||||
|
* @author Cody Cutrer - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class HomeAssistantJinjaFunctionLibrary {
|
||||||
|
public static void register(Context context) {
|
||||||
|
context.registerFunction(
|
||||||
|
new ELFunctionDefinition("", "iif", Functions.class, "iif", Object.class, Object[].class));
|
||||||
|
context.registerFilter(new SimpleFilter("iif", Functions.class, "iif", Object.class, Object[].class));
|
||||||
|
context.registerFilter(new IsDefinedFilter());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNullByDefault({})
|
||||||
|
private static class SimpleFilter implements Filter {
|
||||||
|
private final String name;
|
||||||
|
private final Method method;
|
||||||
|
private final Class klass;
|
||||||
|
|
||||||
|
public SimpleFilter(String name, Class klass, String methodName, Class... args) {
|
||||||
|
this.name = name;
|
||||||
|
this.klass = klass;
|
||||||
|
try {
|
||||||
|
this.method = klass.getDeclaredMethod(methodName, args);
|
||||||
|
} catch (NoSuchMethodException e) {
|
||||||
|
throw new IllegalArgumentException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object filter(Object var, JinjavaInterpreter interpreter, Object[] args, Map<String, Object> kwargs) {
|
||||||
|
Object[] allArgs = Stream.of(Arrays.stream(args), kwargs.values().stream()).flatMap(s -> s)
|
||||||
|
.toArray(Object[]::new);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return method.invoke(klass, var, allArgs);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
// Not possible
|
||||||
|
return null;
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
throw new InterpretException(e.getMessage(), e, interpreter.getLineNumber(), interpreter.getPosition());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object filter(Object var, JinjavaInterpreter interpreter, String... args) {
|
||||||
|
// Object[] allArgs = Stream.concat(List.of(var).stream(), Arrays.stream(args)).toArray(Object[]::new);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return method.invoke(klass, var, args);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
// Not possible
|
||||||
|
return null;
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
throw new InterpretException(e.getMessage(), e, interpreter.getLineNumber(), interpreter.getPosition());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.home-assistant.io/docs/configuration/templating/#is-defined
|
||||||
|
@NonNullByDefault({})
|
||||||
|
private static class IsDefinedFilter implements Filter {
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "is_defined";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object filter(Object var, JinjavaInterpreter interpreter, String... args) {
|
||||||
|
if (var == null) {
|
||||||
|
throw new HomeAssistantChannelTransformation.UndefinedException(interpreter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return var;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Functions {
|
||||||
|
// https://www.home-assistant.io/docs/configuration/templating/#immediate-if-iif
|
||||||
|
public static Object iif(Object value, Object... results) {
|
||||||
|
if (results.length > 3) {
|
||||||
|
throw new IllegalArgumentException("Parameters for function 'iff' do not match");
|
||||||
|
}
|
||||||
|
if (value == null && results.length >= 3) {
|
||||||
|
return results[2];
|
||||||
|
}
|
||||||
|
if (ObjectTruthValue.evaluate(value)) {
|
||||||
|
if (results.length >= 1) {
|
||||||
|
return results[0];
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (results.length >= 2) {
|
||||||
|
return results[1];
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
|
import static org.hamcrest.CoreMatchers.nullValue;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.mockito.junit.jupiter.MockitoSettings;
|
||||||
|
import org.mockito.quality.Strictness;
|
||||||
|
import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
|
||||||
|
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
|
||||||
|
import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttThingHandlerFactory;
|
||||||
|
import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent;
|
||||||
|
import org.openhab.core.test.storage.VolatileStorageService;
|
||||||
|
import org.openhab.core.thing.type.ChannelTypeRegistry;
|
||||||
|
import org.openhab.core.thing.type.ThingTypeRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Jochen Klein - Initial contribution
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||||
|
@NonNullByDefault
|
||||||
|
public class HomeAssistantChannelTransformationTests {
|
||||||
|
protected @Mock @NonNullByDefault({}) ThingTypeRegistry thingTypeRegistry;
|
||||||
|
|
||||||
|
protected @NonNullByDefault({}) HomeAssistantChannelTransformation transformation;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void beforeEachChannelTransformationTest() {
|
||||||
|
MqttChannelTypeProvider channelTypeProvider = new MqttChannelTypeProvider(thingTypeRegistry,
|
||||||
|
new VolatileStorageService());
|
||||||
|
MqttChannelStateDescriptionProvider stateDescriptionProvider = new MqttChannelStateDescriptionProvider();
|
||||||
|
ChannelTypeRegistry channelTypeRegistry = new ChannelTypeRegistry();
|
||||||
|
MqttThingHandlerFactory thingHandlerFactory = new MqttThingHandlerFactory(channelTypeProvider,
|
||||||
|
stateDescriptionProvider, channelTypeRegistry);
|
||||||
|
|
||||||
|
AbstractComponent component = Mockito.mock(AbstractComponent.class);
|
||||||
|
HaID haID = new HaID("homeassistant/light/pool/light/config");
|
||||||
|
when(component.getHaID()).thenReturn(haID);
|
||||||
|
transformation = new HomeAssistantChannelTransformation(thingHandlerFactory.getJinjava(), component, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIif() {
|
||||||
|
assertThat(transform("{{ iif(True) }}", ""), is("true"));
|
||||||
|
assertThat(transform("{{ iif(False) }}", ""), is("false"));
|
||||||
|
assertThat(transform("{{ iif(Null) }}", ""), is("false"));
|
||||||
|
assertThat(transform("{{ iif(True, 'Yes') }}", ""), is("Yes"));
|
||||||
|
assertThat(transform("{{ iif(False, 'Yes') }}", ""), is("false"));
|
||||||
|
assertThat(transform("{{ iif(Null, 'Yes') }}", ""), is("false"));
|
||||||
|
assertThat(transform("{{ iif(True, 'Yes', 'No') }}", ""), is("Yes"));
|
||||||
|
assertThat(transform("{{ iif(False, 'Yes', 'No') }}", ""), is("No"));
|
||||||
|
assertThat(transform("{{ iif(Null, 'Yes', 'No') }}", ""), is("No"));
|
||||||
|
assertThat(transform("{{ iif(True, 'Yes', 'No', null) }}", ""), is("Yes"));
|
||||||
|
assertThat(transform("{{ iif(False, 'Yes', 'No', null) }}", ""), is("No"));
|
||||||
|
assertThat(transform("{{ iif(Null, 'Yes', 'No', 'NULL') }}", ""), is("NULL"));
|
||||||
|
assertThat(transform("{{ iif(Null, 'Yes', 'No', null) }}", ""), is(""));
|
||||||
|
assertThat(transform("{{ iif(True, 'Yes', 'No', null, null) }}", ""), is(nullValue()));
|
||||||
|
|
||||||
|
assertThat(transform("{{ True | iif('Yes') }}", ""), is("Yes"));
|
||||||
|
assertThat(transform("{{ False | iif('Yes') }}", ""), is("false"));
|
||||||
|
assertThat(transform("{{ Null | iif('Yes') }}", ""), is("false"));
|
||||||
|
assertThat(transform("{{ True | iif('Yes', 'No') }}", ""), is("Yes"));
|
||||||
|
assertThat(transform("{{ False | iif('Yes', 'No') }}", ""), is("No"));
|
||||||
|
assertThat(transform("{{ Null | iif('Yes', 'No') }}", ""), is("No"));
|
||||||
|
assertThat(transform("{{ True | iif('Yes', 'No', null) }}", ""), is("Yes"));
|
||||||
|
assertThat(transform("{{ False | iif('Yes', 'No', null) }}", ""), is("No"));
|
||||||
|
assertThat(transform("{{ Null | iif('Yes', 'No', 'NULL') }}", ""), is("NULL"));
|
||||||
|
assertThat(transform("{{ Null | iif('Yes', 'No', null) }}", ""), is(""));
|
||||||
|
assertThat(transform("{{ True | iif('Yes', 'No', null, null) }}", ""), is(nullValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIsDefined() {
|
||||||
|
assertThat(transform("{{ value_json.val | is_defined }}", "{}"), is(nullValue()));
|
||||||
|
assertThat(transform("{{ 'hi' | is_defined }}", "{}"), is("hi"));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected @Nullable String transform(String template, String value) {
|
||||||
|
return transformation.apply(template, value).orElse(null);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue