diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/.gitignore b/bundles/org.openhab.binding.mqtt.homeassistant/.gitignore
deleted file mode 100644
index 11bcd382c29..00000000000
--- a/bundles/org.openhab.binding.mqtt.homeassistant/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/src/main/python/**/__pycache__/
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/DEVELOPMENT.md b/bundles/org.openhab.binding.mqtt.homeassistant/DEVELOPMENT.md
deleted file mode 100644
index b73513dfca7..00000000000
--- a/bundles/org.openhab.binding.mqtt.homeassistant/DEVELOPMENT.md
+++ /dev/null
@@ -1,12 +0,0 @@
-src/main/python is forked from [Home Assistant core](https://github.com/home-assistant/core), in order to have near-perfect compatibility with for the Jinja templates.
-It was forked from the dev branch as of 2025-04-23, corresponding to the 2025.4.3 release of Home Assistant.
-
-The following alterations have been made:
-- Code not specifically used by this binding has been stripped out.
-- Generics and some type checks have been removed, being incompatible with GraalPy 24.2.0, which roughly corresponds with Python 3.11.
-- The standard json library is used, instead of orjson, since orjson requires a Rust compiler and would pre-compile native extensions for the architecture of the build environment, and embed them in the JAR, thus making it incompatible with other runtime architectures.
- AFAICT this should still be fully compatible, since Home Assistant explicitly sets multiple options in order to disable features that are orjson specific.
-- ciso8601 is not included, since it has a native extension. Instead, the stdlib parser is used.
-- All asynchronous processing has been removed; the Java side threading model dominates.
-- The `hass` variable has been removed from templates; Limited templates (which are what MQTT integrations use) set it to `None` anyway.
-- Limited and strict template options have been removed; it's assumed that templates are limited and not strict.
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/NOTICE b/bundles/org.openhab.binding.mqtt.homeassistant/NOTICE
index 54479cdbefd..38d625e3492 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/NOTICE
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/NOTICE
@@ -11,10 +11,3 @@ https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons
-
-== Third-party Content
-
-Parts of this code (src/main/python/) have been forked.
-* License: Apache License 2.0
-* Project: https://www.home-assistant.io/
-* Source: https://github.com/home-assistant/core
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/bnd.bnd b/bundles/org.openhab.binding.mqtt.homeassistant/bnd.bnd
deleted file mode 100644
index 9b692df1603..00000000000
--- a/bundles/org.openhab.binding.mqtt.homeassistant/bnd.bnd
+++ /dev/null
@@ -1,21 +0,0 @@
-Bundle-SymbolicName: ${project.artifactId}
-DynamicImport-Package: *
-Import-Package: org.openhab.core.automation.module.script,org.openhab.core.items,org.openhab.core.library.types,javax.management,javax.script,javax.xml.datatype,javax.xml.stream;version="[1.0,2)",org.osgi.framework;version="[1.8,2)",org.slf4j;version="[1.7,2)"
-Require-Capability:
- osgi.extender:=
- filter:="(osgi.extender=osgi.serviceloader.processor)",
- osgi.serviceloader:=
- filter:="(osgi.serviceloader=org.graalvm.polyglot.impl.AbstractPolyglotImpl)";
- cardinality:=multiple
-Require-Bundle: org.graalvm.sdk.collections;bundle-version="24.2.0",\
- org.graalvm.sdk.jniutils;bundle-version="24.2.0",\
- org.graalvm.sdk.nativeimage;bundle-version="24.2.0",\
- org.graalvm.sdk.word;bundle-version="24.2.0",\
- org.graalvm.shadowed.icu4j;bundle-version="24.2.0",\
- org.graalvm.truffle.truffle-compiler;bundle-version="24.2.0",\
- org.graalvm.truffle.truffle-runtime;bundle-version="24.2.0"
-
-SPI-Provider: *
-SPI-Consumer: *
-
--fixupmessages "Classes found in the wrong directory"; restrict:=error; is:=warning
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/noEmbedDependencies.profile b/bundles/org.openhab.binding.mqtt.homeassistant/noEmbedDependencies.profile
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/pom.xml b/bundles/org.openhab.binding.mqtt.homeassistant/pom.xml
index 3829b6f2dd0..df22ab61e78 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/pom.xml
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/pom.xml
@@ -14,12 +14,6 @@
openHAB Add-ons :: Bundles :: MQTT HomeAssistant Convention
-
- 24.2.0
-
- bin/python3
-
-
org.openhab.addons.bundles
@@ -33,182 +27,42 @@
${project.version}
provided
+
+ com.google.guava
+ guava
+ 33.3.1-jre
+ test
+
-
- org.graalvm.polyglot
- polyglot
- ${graalpy.version}
-
-
-
- org.graalvm.regex
- regex
- ${graalpy.version}
-
-
-
- org.graalvm.polyglot
- python-community
- ${graalpy.version}
- pom
+ org.openhab.osgiify
+ com.hubspot.jinjava.jinjava
+ 2.7.4
+ compile
- org.graalvm.python
- python-embedding
- ${graalpy.version}
+ org.openhab.osgiify
+ com.google.re2j.re2j
+ 1.2
+ compile
+
+
+ ch.obermuhlner
+ big-math
+ 2.3.2
+ compile
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jdk8
+ ${jackson.version}
+ compile
+
+
+ org.openhab.osgiify
+ com.hubspot.immutables.immutables-exceptions
+ 1.9
+ compile
-
-
-
-
- org.apache.maven.plugins
- maven-dependency-plugin
-
-
- embed-dependencies
-
- unpack-dependencies
-
-
-
-
-
- org.apache.maven.plugins
- maven-surefire-plugin
- 3.5.2
-
- -noverify
-
-
-
- maven-resources-plugin
- 3.3.1
-
-
- copy-homeassistant-python
-
- copy-resources
-
- generate-resources
-
- ${project.build.directory}/classes/GRAALPY-VFS/${project.groupId}/${project.artifactId}/src
-
-
- src/main/python
-
-
-
-
-
-
-
- org.graalvm.python
- graalpy-maven-plugin
- ${graalpy.version}
-
-
- install-python-packages
-
- process-graalpy-resources
-
- generate-resources
-
- GRAALPY-VFS/${project.groupId}/${project.artifactId}
-
- awesomeversion==24.6.0
- Jinja2==3.1.6
- python-slugify==8.0.4
-
-
-
-
-
- generate-python-filelist
-
- process-graalpy-resources
-
- process-resources
-
- GRAALPY-VFS/${project.groupId}/${project.artifactId}
-
- awesomeversion==24.6.0
- Jinja2==3.1.6
- python-slugify==8.0.4
-
-
-
-
-
-
- org.codehaus.mojo
- exec-maven-plugin
- 3.1.0
-
-
- compile-python
-
- exec
-
- generate-resources
-
- ${project.build.directory}/classes/GRAALPY-VFS/${project.groupId}/${project.artifactId}/venv/${graalpy.executable}
-
- -m
- compileall
- ${project.build.directory}/classes/GRAALPY-VFS/${project.groupId}/${project.artifactId}/src
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-shade-plugin
- 3.6.0
-
-
-
- shade
-
- package
-
-
-
- org.graalvm.llvm:llvm-api
- org.graalvm.polyglot:polyglot
- org.graalvm.python:python-language
- org.graalvm.python:python-resources
- org.graalvm.regex:regex
- org.graalvm.tools:profiler-tool
- org.graalvm.truffle:truffle-api
- org.graalvm.truffle:truffle-nfi
- org.graalvm.truffle:truffle-nfi-libffi
-
-
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- windows
-
-
-
- Scripts/python
-
-
-
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/feature/feature.xml b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/feature/feature.xml
index 7fdf408c336..845c8065384 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/feature/feature.xml
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/feature/feature.xml
@@ -7,14 +7,11 @@
openhab-runtime-base
openhab-transport-mqtt
openhab.tp-commons-net
- mvn:org.openhab.osgiify/org.graalvm.sdk.collections/24.2.0
- mvn:org.openhab.osgiify/org.graalvm.sdk.jniutils/24.2.0
- mvn:org.openhab.osgiify/org.graalvm.sdk.nativeimage/24.2.0
- mvn:org.openhab.osgiify/org.graalvm.sdk.word/24.2.0
- mvn:org.openhab.osgiify/org.graalvm.shadowed.icu4j/24.2.0
- mvn:org.openhab.osgiify/org.graalvm.shadowed.xz/24.2.0
- mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-compiler/24.2.0
- mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-runtime/24.2.0
+ mvn:org.openhab.osgiify/com.hubspot.jinjava.jinjava/2.7.4
+ mvn:org.openhab.osgiify/com.google.re2j.re2j/1.2
+ mvn:ch.obermuhlner/big-math/2.3.2
+ mvn:com.fasterxml.jackson.datatype/jackson-datatype-jdk8/${jackson.version}
+ mvn:org.openhab.osgiify/com.hubspot.immutables.immutables-exceptions/1.9
mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt/${project.version}
mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt.generic/${project.version}
mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt.homeassistant/${project.version}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/generic/internal/MqttThingHandlerFactory.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/generic/internal/MqttThingHandlerFactory.java
index f7604d0172a..808f272c3a5 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/generic/internal/MqttThingHandlerFactory.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/generic/internal/MqttThingHandlerFactory.java
@@ -20,7 +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.HomeAssistantPythonBridge;
+import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantJinjaFunctionLibrary;
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantStateDescriptionProvider;
import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler;
import org.openhab.core.i18n.UnitProvider;
@@ -34,6 +34,8 @@ import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
+import com.hubspot.jinjava.Jinjava;
+
/**
* The {@link MqttThingHandlerFactory} is responsible for creating things and thing
* handlers.
@@ -46,8 +48,8 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
private final MqttChannelTypeProvider typeProvider;
private final MqttChannelStateDescriptionProvider stateDescriptionProvider;
private final ChannelTypeRegistry channelTypeRegistry;
+ private final Jinjava jinjava = new Jinjava();
private final UnitProvider unitProvider;
- private final HomeAssistantPythonBridge python;
private static final Set SUPPORTED_THING_TYPES_UIDS = Stream
.of(MqttBindingConstants.HOMEASSISTANT_MQTT_THING).collect(Collectors.toSet());
@@ -60,7 +62,8 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
this.stateDescriptionProvider = stateDescriptionProvider;
this.channelTypeRegistry = channelTypeRegistry;
this.unitProvider = unitProvider;
- this.python = new HomeAssistantPythonBridge();
+
+ HomeAssistantJinjaFunctionLibrary.register(jinjava.getGlobalContext());
}
@Override
@@ -79,12 +82,12 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
if (supportsThingType(thingTypeUID)) {
return new HomeAssistantThingHandler(thing, this, typeProvider, stateDescriptionProvider,
- channelTypeRegistry, python, unitProvider, 10000, 2000);
+ channelTypeRegistry, jinjava, unitProvider, 10000, 2000);
}
return null;
}
- public HomeAssistantPythonBridge getPython() {
- return python;
+ public Jinjava getJinjava() {
+ return jinjava;
}
}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannel.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannel.java
index cbec34753d9..24a9c2529e9 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannel.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannel.java
@@ -136,7 +136,6 @@ public class ComponentChannel {
private @Nullable String stateTopic;
private @Nullable String commandTopic;
- private boolean parseCommandValueAsInteger;
private boolean retain;
private boolean trigger;
private boolean isAdvanced;
@@ -207,11 +206,6 @@ public class ComponentChannel {
return this;
}
- public Builder parseCommandValueAsInteger(boolean parseCommandValueAsInteger) {
- this.parseCommandValueAsInteger = parseCommandValueAsInteger;
- return this;
- }
-
public Builder trigger(boolean trigger) {
this.trigger = trigger;
return this;
@@ -271,13 +265,13 @@ public class ComponentChannel {
String localTemplateIn = templateIn;
if (localTemplateIn != null) {
- incomingTransformation = new HomeAssistantChannelTransformation(component.getPython(), component,
- localTemplateIn, false);
+ incomingTransformation = new HomeAssistantChannelTransformation(component.getJinjava(), component,
+ localTemplateIn);
}
String localTemplateOut = templateOut;
if (localTemplateOut != null) {
- outgoingTransformation = new HomeAssistantChannelTransformation(component.getPython(), component,
- localTemplateOut, true, parseCommandValueAsInteger);
+ outgoingTransformation = new HomeAssistantChannelTransformation(component.getJinjava(), component,
+ localTemplateOut);
}
channelState = new HomeAssistantChannelState(channelConfigBuilder.build(), channelUID, valueState,
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/DiscoverComponents.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/DiscoverComponents.java
index 3c187f3e7f2..9663e2d2243 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/DiscoverComponents.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/DiscoverComponents.java
@@ -38,6 +38,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
+import com.hubspot.jinjava.Jinjava;
/**
* Responsible for subscribing to the HomeAssistant MQTT components wildcard topic, either
@@ -56,7 +57,7 @@ public class DiscoverComponents implements MqttMessageSubscriber {
protected final CompletableFuture<@Nullable Void> discoverFinishedFuture = new CompletableFuture<>();
private final Gson gson;
- private final HomeAssistantPythonBridge python;
+ private final Jinjava jinjava;
private final UnitProvider unitProvider;
private @Nullable ScheduledFuture> stopDiscoveryFuture;
@@ -83,13 +84,13 @@ public class DiscoverComponents implements MqttMessageSubscriber {
*/
public DiscoverComponents(ThingUID thingUID, ScheduledExecutorService scheduler,
ChannelStateUpdateListener channelStateUpdateListener, HomeAssistantChannelLinkageChecker linkageChecker,
- AvailabilityTracker tracker, Gson gson, HomeAssistantPythonBridge python, UnitProvider unitProvider) {
+ AvailabilityTracker tracker, Gson gson, Jinjava jinjava, UnitProvider unitProvider) {
this.thingUID = thingUID;
this.scheduler = scheduler;
this.updateListener = channelStateUpdateListener;
this.linkageChecker = linkageChecker;
this.gson = gson;
- this.python = python;
+ this.jinjava = jinjava;
this.unitProvider = unitProvider;
this.tracker = tracker;
}
@@ -107,7 +108,7 @@ public class DiscoverComponents implements MqttMessageSubscriber {
if (config.length() > 0) {
try {
component = ComponentFactory.createComponent(thingUID, haID, config, updateListener, linkageChecker,
- tracker, scheduler, gson, python, unitProvider);
+ tracker, scheduler, gson, jinjava, unitProvider);
component.setConfigSeen();
logger.trace("Found HomeAssistant component {}", haID);
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelTransformation.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelTransformation.java
index 484df982c3b..f29b05622b2 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelTransformation.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelTransformation.java
@@ -12,109 +12,137 @@
*/
package org.openhab.binding.mqtt.homeassistant.internal;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
-import org.graalvm.polyglot.PolyglotException;
-import org.graalvm.polyglot.Value;
import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent;
import org.openhab.core.thing.binding.generic.ChannelTransformation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+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
* Jinja2 template, providing the additional context and extensions required by Home Assistant
+ * Based in part on the JinjaTransformationService
*
* @author Cody Cutrer - Initial contribution
*/
@NonNullByDefault
public class HomeAssistantChannelTransformation extends ChannelTransformation {
- // These map to PayloadSentinen.NONE and PayloadSentinel.DEFAULT in mqtt/models.py
- // NONE is used to indicate that errors should be ignored, and if any happen the original
- // payload should be returned directly
- public static final String PAYLOAD_SENTINEL_NONE = "none";
- public static final String PAYLOAD_SENTINEL_DEFAULT = "default";
+ 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 HomeAssistantPythonBridge python;
- private final AbstractComponent component;
- private final Value template;
- private final boolean command;
- private final String defaultValue;
- private final boolean parseValueAsInteger;
+ private final Jinjava jinjava;
+ private final AbstractComponent> component;
+ private final String template;
+ private final ObjectMapper objectMapper = new ObjectMapper();
- public HomeAssistantChannelTransformation(HomeAssistantPythonBridge python, AbstractComponent component,
- String template, boolean command) {
- this(python, component, template, command, PAYLOAD_SENTINEL_NONE, false);
- }
-
- public HomeAssistantChannelTransformation(HomeAssistantPythonBridge python, AbstractComponent component,
- String template, boolean command, boolean parseValueAsInteger) {
- this(python, component, template, command, PAYLOAD_SENTINEL_NONE, parseValueAsInteger);
- }
-
- public HomeAssistantChannelTransformation(HomeAssistantPythonBridge python, AbstractComponent component,
- String template, String defaultValue) {
- this(python, component, template, false, defaultValue, false);
- }
-
- private HomeAssistantChannelTransformation(HomeAssistantPythonBridge python, AbstractComponent component,
- String template, boolean command, String defaultValue, boolean parseValueAsInteger) {
+ public HomeAssistantChannelTransformation(Jinjava jinjava, AbstractComponent> component, String template) {
super((String) null);
- this.python = python;
+ this.jinjava = jinjava;
this.component = component;
- this.command = command;
- this.template = command ? python.newCommandTemplate(template) : python.newValueTemplate(template);
- this.defaultValue = defaultValue;
- this.parseValueAsInteger = parseValueAsInteger;
+ this.template = template;
}
@Override
public boolean isEmpty() {
- return false;
+ return template.isEmpty();
}
@Override
public Optional apply(String value) {
- Object objValue = value;
- if (parseValueAsInteger) {
- try {
- objValue = (int) Float.parseFloat(value);
- } catch (NumberFormatException e) {
- logger.warn("Failed to parse value {} as integer: {}", value, e.getMessage());
+ return apply(template, value);
+ }
+
+ public Optional apply(String template, String value) {
+ Map bindings = new HashMap<>();
+
+ logger.debug("about to transform '{}' by the function '{}'", value, template);
+
+ bindings.put("value", value);
+
+ try {
+ JsonNode tree = objectMapper.readTree(value);
+ bindings.put("value_json", toObject(tree));
+ } catch (IOException e) {
+ // ok, then value_json is null...
+ }
+
+ return apply(template, bindings);
+ }
+
+ public Optional apply(String template, Map bindings) {
+ String transformationResult;
+
+ try {
+ transformationResult = jinjava.render(template, bindings);
+ } catch (FatalTemplateErrorsException e) {
+ 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();
}
- }
- Object result = transform(objValue);
- if (result == null) {
+ logger.warn("Applying template {} for component {} failed: {} ({})", template,
+ component.getHaID().toShortTopic(), e.getMessage(), e.getClass());
return Optional.empty();
}
- return Optional.of(result.toString());
+
+ logger.debug("transformation resulted in '{}'", transformationResult);
+
+ return Optional.of(transformationResult);
}
- public @Nullable String transform(Object value) {
- try {
- return command ? python.renderCommandTemplate(template, value)
- : python.renderValueTemplate(template, value, defaultValue);
- } catch (PolyglotException e) {
- logger.warn("Applying template for component {} failed: {}", component.getHaID().toShortTopic(),
- e.getMessage(), e);
- return null;
- }
- }
-
- public @Nullable String transform(Object value, Map variables) {
- try {
- return command ? python.renderCommandTemplate(template, value, variables)
- : python.renderValueTemplate(template, value, defaultValue, variables);
- } catch (PolyglotException e) {
- logger.warn("Applying template for component {} failed: {}", component.getHaID().toShortTopic(),
- e.getMessage(), e);
- return null;
+ private static @Nullable Object toObject(JsonNode node) {
+ switch (node.getNodeType()) {
+ case ARRAY: {
+ List<@Nullable Object> result = new ArrayList<>();
+ for (JsonNode el : node) {
+ result.add(toObject(el));
+ }
+ return result;
+ }
+ case NUMBER:
+ return node.decimalValue();
+ case OBJECT: {
+ Map result = new HashMap<>();
+ Iterator> it = node.fields();
+ while (it.hasNext()) {
+ Entry field = it.next();
+ result.put(field.getKey(), toObject(field.getValue()));
+ }
+ return result;
+ }
+ case STRING:
+ return node.asText();
+ case BOOLEAN:
+ return node.asBoolean();
+ case NULL:
+ default:
+ return null;
}
}
}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantJinjaFunctionLibrary.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantJinjaFunctionLibrary.java
new file mode 100644
index 00000000000..de4a822a22c
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantJinjaFunctionLibrary.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (c) 2010-2025 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.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.re2j.Matcher;
+import com.google.re2j.Pattern;
+import com.google.re2j.PatternSyntaxException;
+import com.hubspot.jinjava.interpret.Context;
+import com.hubspot.jinjava.interpret.InterpretException;
+import com.hubspot.jinjava.interpret.InvalidArgumentException;
+import com.hubspot.jinjava.interpret.InvalidReason;
+import com.hubspot.jinjava.interpret.JinjavaInterpreter;
+import com.hubspot.jinjava.interpret.TemplateSyntaxException;
+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());
+ context.registerFilter(new RegexFindAllFilter());
+ context.registerFilter(new RegexFindAllIndexFilter());
+ }
+
+ @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 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;
+ }
+ }
+
+ // https://www.home-assistant.io/docs/configuration/templating/#regular-expressions
+ // https://github.com/home-assistant/core/blob/2024.12.2/homeassistant/helpers/template.py#L2453
+ @NonNullByDefault({})
+ private static class RegexFindAllFilter implements Filter {
+ @Override
+ public String getName() {
+ return "regex_findall";
+ }
+
+ @Override
+ public Object filter(Object var, JinjavaInterpreter interpreter, String... args) {
+ if (args.length > 2) {
+ throw new TemplateSyntaxException(interpreter, getName(),
+ "requires at most 2 arguments (regex string, ignore case)");
+ }
+
+ String find = null;
+ if (args.length >= 1) {
+ find = args[0];
+ }
+ String ignoreCase = null;
+ if (args.length == 2) {
+ ignoreCase = args[1];
+ }
+
+ Matcher m = regexFindAll(var, interpreter, find, ignoreCase);
+
+ List