mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[mqtt.homeassistant] implement iif and is_defined jinja function and filters (#17435)
Signed-off-by: Cody Cutrer <cody@cutrer.us> Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
parent
7902c1c29d
commit
53937d0c08
@ -20,6 +20,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
|
||||
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.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
@ -57,6 +58,8 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
|
||||
this.typeProvider = typeProvider;
|
||||
this.stateDescriptionProvider = stateDescriptionProvider;
|
||||
this.channelTypeRegistry = channelTypeRegistry;
|
||||
|
||||
HomeAssistantJinjaFunctionLibrary.register(jinjava.getGlobalContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -79,4 +82,8 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
|
||||
}
|
||||
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.hubspot.jinjava.Jinjava;
|
||||
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
|
||||
@ -42,6 +44,12 @@ import com.hubspot.jinjava.interpret.FatalTemplateErrorsException;
|
||||
*/
|
||||
@NonNullByDefault
|
||||
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 Jinjava jinjava;
|
||||
@ -89,8 +97,17 @@ public class HomeAssistantChannelTransformation extends ChannelTransformation {
|
||||
try {
|
||||
transformationResult = jinjava.render(template, bindings);
|
||||
} catch (FatalTemplateErrorsException e) {
|
||||
logger.warn("Applying template {} for component {} failed: {}", template,
|
||||
component.getHaID().toShortTopic(), e.getMessage());
|
||||
var error = e.getErrors().iterator();
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -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
Block a user