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
*/
@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;
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();
}
--- /dev/null
+/**
+ * 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;
+ }
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}