[mqtt.homeassistant] Revert "Use GraalPy and import actual Home Assistant templating code (#18601)" (#18788)
This reverts commit 5a2179106e
.
Signed-off-by: Cody Cutrer <cody@cutrer.us>
pull/18793/head
parent
fd1236081d
commit
f7713a54f4
|
@ -1 +0,0 @@
|
||||||
/src/main/python/**/__pycache__/
|
|
|
@ -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.
|
|
|
@ -11,10 +11,3 @@ https://www.eclipse.org/legal/epl-2.0/.
|
||||||
== Source Code
|
== Source Code
|
||||||
|
|
||||||
https://github.com/openhab/openhab-addons
|
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
|
|
||||||
|
|
|
@ -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
|
|
|
@ -14,12 +14,6 @@
|
||||||
|
|
||||||
<name>openHAB Add-ons :: Bundles :: MQTT HomeAssistant Convention</name>
|
<name>openHAB Add-ons :: Bundles :: MQTT HomeAssistant Convention</name>
|
||||||
|
|
||||||
<properties>
|
|
||||||
<graalpy.version>24.2.0</graalpy.version>
|
|
||||||
<!-- define a property to overwrite it on Windows, as venv has a different structure -->
|
|
||||||
<graalpy.executable>bin/python3</graalpy.executable>
|
|
||||||
</properties>
|
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openhab.addons.bundles</groupId>
|
<groupId>org.openhab.addons.bundles</groupId>
|
||||||
|
@ -33,182 +27,42 @@
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.guava</groupId>
|
||||||
|
<artifactId>guava</artifactId>
|
||||||
|
<version>33.3.1-jre</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Graal Polyglot Framework -->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.graalvm.polyglot</groupId>
|
<groupId>org.openhab.osgiify</groupId>
|
||||||
<artifactId>polyglot</artifactId>
|
<artifactId>com.hubspot.jinjava.jinjava</artifactId>
|
||||||
<version>${graalpy.version}</version>
|
<version>2.7.4</version>
|
||||||
</dependency>
|
<scope>compile</scope>
|
||||||
<!-- Graal TRegex engine (internally used by Graal Python engine) -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.graalvm.regex</groupId>
|
|
||||||
<artifactId>regex</artifactId>
|
|
||||||
<version>${graalpy.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<!-- Graal Python engine (depends on Graal TRegex engine, must be added after it) -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.graalvm.polyglot</groupId>
|
|
||||||
<artifactId>python-community</artifactId>
|
|
||||||
<version>${graalpy.version}</version>
|
|
||||||
<type>pom</type>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.graalvm.python</groupId>
|
<groupId>org.openhab.osgiify</groupId>
|
||||||
<artifactId>python-embedding</artifactId>
|
<artifactId>com.google.re2j.re2j</artifactId>
|
||||||
<version>${graalpy.version}</version>
|
<version>1.2</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>ch.obermuhlner</groupId>
|
||||||
|
<artifactId>big-math</artifactId>
|
||||||
|
<version>2.3.2</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||||
|
<artifactId>jackson-datatype-jdk8</artifactId>
|
||||||
|
<version>${jackson.version}</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openhab.osgiify</groupId>
|
||||||
|
<artifactId>com.hubspot.immutables.immutables-exceptions</artifactId>
|
||||||
|
<version>1.9</version>
|
||||||
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-dependency-plugin</artifactId>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<id>embed-dependencies</id>
|
|
||||||
<goals>
|
|
||||||
<goal>unpack-dependencies</goal>
|
|
||||||
</goals>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-surefire-plugin</artifactId>
|
|
||||||
<version>3.5.2</version>
|
|
||||||
<configuration>
|
|
||||||
<argLine>-noverify</argLine>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<artifactId>maven-resources-plugin</artifactId>
|
|
||||||
<version>3.3.1</version>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<id>copy-homeassistant-python</id>
|
|
||||||
<goals>
|
|
||||||
<goal>copy-resources</goal>
|
|
||||||
</goals>
|
|
||||||
<phase>generate-resources</phase>
|
|
||||||
<configuration>
|
|
||||||
<outputDirectory>${project.build.directory}/classes/GRAALPY-VFS/${project.groupId}/${project.artifactId}/src</outputDirectory>
|
|
||||||
<resources>
|
|
||||||
<resource>
|
|
||||||
<directory>src/main/python</directory>
|
|
||||||
</resource>
|
|
||||||
</resources>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.graalvm.python</groupId>
|
|
||||||
<artifactId>graalpy-maven-plugin</artifactId>
|
|
||||||
<version>${graalpy.version}</version>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<id>install-python-packages</id>
|
|
||||||
<goals>
|
|
||||||
<goal>process-graalpy-resources</goal>
|
|
||||||
</goals>
|
|
||||||
<phase>generate-resources</phase>
|
|
||||||
<configuration>
|
|
||||||
<resourceDirectory>GRAALPY-VFS/${project.groupId}/${project.artifactId}</resourceDirectory>
|
|
||||||
<packages>
|
|
||||||
<package>awesomeversion==24.6.0</package>
|
|
||||||
<package>Jinja2==3.1.6</package>
|
|
||||||
<package>python-slugify==8.0.4</package>
|
|
||||||
</packages>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
<!-- yes, this is the same as above, but it needs run again to regenerate the filelist with our .pyc files -->
|
|
||||||
<execution>
|
|
||||||
<id>generate-python-filelist</id>
|
|
||||||
<goals>
|
|
||||||
<goal>process-graalpy-resources</goal>
|
|
||||||
</goals>
|
|
||||||
<phase>process-resources</phase>
|
|
||||||
<configuration>
|
|
||||||
<resourceDirectory>GRAALPY-VFS/${project.groupId}/${project.artifactId}</resourceDirectory>
|
|
||||||
<packages>
|
|
||||||
<package>awesomeversion==24.6.0</package>
|
|
||||||
<package>Jinja2==3.1.6</package>
|
|
||||||
<package>python-slugify==8.0.4</package>
|
|
||||||
</packages>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.codehaus.mojo</groupId>
|
|
||||||
<artifactId>exec-maven-plugin</artifactId>
|
|
||||||
<version>3.1.0</version>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<id>compile-python</id>
|
|
||||||
<goals>
|
|
||||||
<goal>exec</goal>
|
|
||||||
</goals>
|
|
||||||
<phase>generate-resources</phase>
|
|
||||||
<configuration>
|
|
||||||
<executable>${project.build.directory}/classes/GRAALPY-VFS/${project.groupId}/${project.artifactId}/venv/${graalpy.executable}</executable>
|
|
||||||
<arguments>
|
|
||||||
<argument>-m</argument>
|
|
||||||
<argument>compileall</argument>
|
|
||||||
<argument>${project.build.directory}/classes/GRAALPY-VFS/${project.groupId}/${project.artifactId}/src</argument>
|
|
||||||
</arguments>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-shade-plugin</artifactId>
|
|
||||||
<version>3.6.0</version>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<goals>
|
|
||||||
<goal>shade</goal>
|
|
||||||
</goals>
|
|
||||||
<phase>package</phase>
|
|
||||||
<configuration>
|
|
||||||
<artifactSet>
|
|
||||||
<includes>
|
|
||||||
<include>org.graalvm.llvm:llvm-api</include>
|
|
||||||
<include>org.graalvm.polyglot:polyglot</include>
|
|
||||||
<include>org.graalvm.python:python-language</include>
|
|
||||||
<include>org.graalvm.python:python-resources</include>
|
|
||||||
<include>org.graalvm.regex:regex</include>
|
|
||||||
<include>org.graalvm.tools:profiler-tool</include>
|
|
||||||
<include>org.graalvm.truffle:truffle-api</include>
|
|
||||||
<include>org.graalvm.truffle:truffle-nfi</include>
|
|
||||||
<include>org.graalvm.truffle:truffle-nfi-libffi</include>
|
|
||||||
</includes>
|
|
||||||
</artifactSet>
|
|
||||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
|
||||||
<transformers>
|
|
||||||
<!-- Transformer to merge module-info.class
|
|
||||||
files, if needed -->
|
|
||||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
|
|
||||||
</transformers>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
<profiles>
|
|
||||||
<profile>
|
|
||||||
<activation>
|
|
||||||
<os>
|
|
||||||
<family>windows</family>
|
|
||||||
</os>
|
|
||||||
</activation>
|
|
||||||
<properties>
|
|
||||||
<graalpy.executable>Scripts/python</graalpy.executable>
|
|
||||||
</properties>
|
|
||||||
</profile>
|
|
||||||
</profiles>
|
|
||||||
</project>
|
</project>
|
||||||
|
|
|
@ -7,14 +7,11 @@
|
||||||
<feature>openhab-runtime-base</feature>
|
<feature>openhab-runtime-base</feature>
|
||||||
<feature>openhab-transport-mqtt</feature>
|
<feature>openhab-transport-mqtt</feature>
|
||||||
<feature dependency="true">openhab.tp-commons-net</feature>
|
<feature dependency="true">openhab.tp-commons-net</feature>
|
||||||
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.sdk.collections/24.2.0</bundle>
|
<bundle dependency="true">mvn:org.openhab.osgiify/com.hubspot.jinjava.jinjava/2.7.4</bundle>
|
||||||
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.sdk.jniutils/24.2.0</bundle>
|
<bundle dependency="true">mvn:org.openhab.osgiify/com.google.re2j.re2j/1.2</bundle>
|
||||||
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.sdk.nativeimage/24.2.0</bundle>
|
<bundle dependency="true">mvn:ch.obermuhlner/big-math/2.3.2</bundle>
|
||||||
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.sdk.word/24.2.0</bundle>
|
<bundle dependency="true">mvn:com.fasterxml.jackson.datatype/jackson-datatype-jdk8/${jackson.version}</bundle>
|
||||||
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.shadowed.icu4j/24.2.0</bundle>
|
<bundle dependency="true">mvn:org.openhab.osgiify/com.hubspot.immutables.immutables-exceptions/1.9</bundle>
|
||||||
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.shadowed.xz/24.2.0</bundle>
|
|
||||||
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-compiler/24.2.0</bundle>
|
|
||||||
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-runtime/24.2.0</bundle>
|
|
||||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt/${project.version}</bundle>
|
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt/${project.version}</bundle>
|
||||||
<bundle start-level="81">mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt.generic/${project.version}</bundle>
|
<bundle start-level="81">mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt.generic/${project.version}</bundle>
|
||||||
<bundle start-level="82">mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt.homeassistant/${project.version}</bundle>
|
<bundle start-level="82">mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt.homeassistant/${project.version}</bundle>
|
||||||
|
|
|
@ -20,7 +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.HomeAssistantPythonBridge;
|
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantJinjaFunctionLibrary;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantStateDescriptionProvider;
|
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantStateDescriptionProvider;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler;
|
import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler;
|
||||||
import org.openhab.core.i18n.UnitProvider;
|
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.Component;
|
||||||
import org.osgi.service.component.annotations.Reference;
|
import org.osgi.service.component.annotations.Reference;
|
||||||
|
|
||||||
|
import com.hubspot.jinjava.Jinjava;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link MqttThingHandlerFactory} is responsible for creating things and thing
|
* The {@link MqttThingHandlerFactory} is responsible for creating things and thing
|
||||||
* handlers.
|
* handlers.
|
||||||
|
@ -46,8 +48,8 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
|
||||||
private final MqttChannelTypeProvider typeProvider;
|
private final MqttChannelTypeProvider typeProvider;
|
||||||
private final MqttChannelStateDescriptionProvider stateDescriptionProvider;
|
private final MqttChannelStateDescriptionProvider stateDescriptionProvider;
|
||||||
private final ChannelTypeRegistry channelTypeRegistry;
|
private final ChannelTypeRegistry channelTypeRegistry;
|
||||||
|
private final Jinjava jinjava = new Jinjava();
|
||||||
private final UnitProvider unitProvider;
|
private final UnitProvider unitProvider;
|
||||||
private final HomeAssistantPythonBridge python;
|
|
||||||
|
|
||||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
|
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
|
||||||
.of(MqttBindingConstants.HOMEASSISTANT_MQTT_THING).collect(Collectors.toSet());
|
.of(MqttBindingConstants.HOMEASSISTANT_MQTT_THING).collect(Collectors.toSet());
|
||||||
|
@ -60,7 +62,8 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
|
||||||
this.stateDescriptionProvider = stateDescriptionProvider;
|
this.stateDescriptionProvider = stateDescriptionProvider;
|
||||||
this.channelTypeRegistry = channelTypeRegistry;
|
this.channelTypeRegistry = channelTypeRegistry;
|
||||||
this.unitProvider = unitProvider;
|
this.unitProvider = unitProvider;
|
||||||
this.python = new HomeAssistantPythonBridge();
|
|
||||||
|
HomeAssistantJinjaFunctionLibrary.register(jinjava.getGlobalContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -79,12 +82,12 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
|
||||||
|
|
||||||
if (supportsThingType(thingTypeUID)) {
|
if (supportsThingType(thingTypeUID)) {
|
||||||
return new HomeAssistantThingHandler(thing, this, typeProvider, stateDescriptionProvider,
|
return new HomeAssistantThingHandler(thing, this, typeProvider, stateDescriptionProvider,
|
||||||
channelTypeRegistry, python, unitProvider, 10000, 2000);
|
channelTypeRegistry, jinjava, unitProvider, 10000, 2000);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public HomeAssistantPythonBridge getPython() {
|
public Jinjava getJinjava() {
|
||||||
return python;
|
return jinjava;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -136,7 +136,6 @@ public class ComponentChannel {
|
||||||
|
|
||||||
private @Nullable String stateTopic;
|
private @Nullable String stateTopic;
|
||||||
private @Nullable String commandTopic;
|
private @Nullable String commandTopic;
|
||||||
private boolean parseCommandValueAsInteger;
|
|
||||||
private boolean retain;
|
private boolean retain;
|
||||||
private boolean trigger;
|
private boolean trigger;
|
||||||
private boolean isAdvanced;
|
private boolean isAdvanced;
|
||||||
|
@ -207,11 +206,6 @@ public class ComponentChannel {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder parseCommandValueAsInteger(boolean parseCommandValueAsInteger) {
|
|
||||||
this.parseCommandValueAsInteger = parseCommandValueAsInteger;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder trigger(boolean trigger) {
|
public Builder trigger(boolean trigger) {
|
||||||
this.trigger = trigger;
|
this.trigger = trigger;
|
||||||
return this;
|
return this;
|
||||||
|
@ -271,13 +265,13 @@ public class ComponentChannel {
|
||||||
|
|
||||||
String localTemplateIn = templateIn;
|
String localTemplateIn = templateIn;
|
||||||
if (localTemplateIn != null) {
|
if (localTemplateIn != null) {
|
||||||
incomingTransformation = new HomeAssistantChannelTransformation(component.getPython(), component,
|
incomingTransformation = new HomeAssistantChannelTransformation(component.getJinjava(), component,
|
||||||
localTemplateIn, false);
|
localTemplateIn);
|
||||||
}
|
}
|
||||||
String localTemplateOut = templateOut;
|
String localTemplateOut = templateOut;
|
||||||
if (localTemplateOut != null) {
|
if (localTemplateOut != null) {
|
||||||
outgoingTransformation = new HomeAssistantChannelTransformation(component.getPython(), component,
|
outgoingTransformation = new HomeAssistantChannelTransformation(component.getJinjava(), component,
|
||||||
localTemplateOut, true, parseCommandValueAsInteger);
|
localTemplateOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
channelState = new HomeAssistantChannelState(channelConfigBuilder.build(), channelUID, valueState,
|
channelState = new HomeAssistantChannelState(channelConfigBuilder.build(), channelUID, valueState,
|
||||||
|
|
|
@ -38,6 +38,7 @@ import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
import com.hubspot.jinjava.Jinjava;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Responsible for subscribing to the HomeAssistant MQTT components wildcard topic, either
|
* 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<>();
|
protected final CompletableFuture<@Nullable Void> discoverFinishedFuture = new CompletableFuture<>();
|
||||||
private final Gson gson;
|
private final Gson gson;
|
||||||
private final HomeAssistantPythonBridge python;
|
private final Jinjava jinjava;
|
||||||
private final UnitProvider unitProvider;
|
private final UnitProvider unitProvider;
|
||||||
|
|
||||||
private @Nullable ScheduledFuture<?> stopDiscoveryFuture;
|
private @Nullable ScheduledFuture<?> stopDiscoveryFuture;
|
||||||
|
@ -83,13 +84,13 @@ public class DiscoverComponents implements MqttMessageSubscriber {
|
||||||
*/
|
*/
|
||||||
public DiscoverComponents(ThingUID thingUID, ScheduledExecutorService scheduler,
|
public DiscoverComponents(ThingUID thingUID, ScheduledExecutorService scheduler,
|
||||||
ChannelStateUpdateListener channelStateUpdateListener, HomeAssistantChannelLinkageChecker linkageChecker,
|
ChannelStateUpdateListener channelStateUpdateListener, HomeAssistantChannelLinkageChecker linkageChecker,
|
||||||
AvailabilityTracker tracker, Gson gson, HomeAssistantPythonBridge python, UnitProvider unitProvider) {
|
AvailabilityTracker tracker, Gson gson, Jinjava jinjava, UnitProvider unitProvider) {
|
||||||
this.thingUID = thingUID;
|
this.thingUID = thingUID;
|
||||||
this.scheduler = scheduler;
|
this.scheduler = scheduler;
|
||||||
this.updateListener = channelStateUpdateListener;
|
this.updateListener = channelStateUpdateListener;
|
||||||
this.linkageChecker = linkageChecker;
|
this.linkageChecker = linkageChecker;
|
||||||
this.gson = gson;
|
this.gson = gson;
|
||||||
this.python = python;
|
this.jinjava = jinjava;
|
||||||
this.unitProvider = unitProvider;
|
this.unitProvider = unitProvider;
|
||||||
this.tracker = tracker;
|
this.tracker = tracker;
|
||||||
}
|
}
|
||||||
|
@ -107,7 +108,7 @@ public class DiscoverComponents implements MqttMessageSubscriber {
|
||||||
if (config.length() > 0) {
|
if (config.length() > 0) {
|
||||||
try {
|
try {
|
||||||
component = ComponentFactory.createComponent(thingUID, haID, config, updateListener, linkageChecker,
|
component = ComponentFactory.createComponent(thingUID, haID, config, updateListener, linkageChecker,
|
||||||
tracker, scheduler, gson, python, unitProvider);
|
tracker, scheduler, gson, jinjava, unitProvider);
|
||||||
component.setConfigSeen();
|
component.setConfigSeen();
|
||||||
|
|
||||||
logger.trace("Found HomeAssistant component {}", haID);
|
logger.trace("Found HomeAssistant component {}", haID);
|
||||||
|
|
|
@ -12,108 +12,136 @@
|
||||||
*/
|
*/
|
||||||
package org.openhab.binding.mqtt.homeassistant.internal;
|
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;
|
||||||
|
import java.util.Map.Entry;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
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.binding.mqtt.homeassistant.internal.component.AbstractComponent;
|
||||||
import org.openhab.core.thing.binding.generic.ChannelTransformation;
|
import org.openhab.core.thing.binding.generic.ChannelTransformation;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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
|
* Provides a channel transformation for a Home Assistant channel with a
|
||||||
* Jinja2 template, providing the additional context and extensions required by Home Assistant
|
* Jinja2 template, providing the additional context and extensions required by Home Assistant
|
||||||
|
* Based in part on the JinjaTransformationService
|
||||||
*
|
*
|
||||||
* @author Cody Cutrer - Initial contribution
|
* @author Cody Cutrer - Initial contribution
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class HomeAssistantChannelTransformation extends ChannelTransformation {
|
public class HomeAssistantChannelTransformation extends ChannelTransformation {
|
||||||
// These map to PayloadSentinen.NONE and PayloadSentinel.DEFAULT in mqtt/models.py
|
public static class UndefinedException extends InvalidInputException {
|
||||||
// NONE is used to indicate that errors should be ignored, and if any happen the original
|
public UndefinedException(JinjavaInterpreter interpreter) {
|
||||||
// payload should be returned directly
|
super(interpreter, "is_defined", "Value is undefined");
|
||||||
public static final String PAYLOAD_SENTINEL_NONE = "none";
|
}
|
||||||
public static final String PAYLOAD_SENTINEL_DEFAULT = "default";
|
}
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(HomeAssistantChannelTransformation.class);
|
private final Logger logger = LoggerFactory.getLogger(HomeAssistantChannelTransformation.class);
|
||||||
|
|
||||||
private final HomeAssistantPythonBridge python;
|
private final Jinjava jinjava;
|
||||||
private final AbstractComponent component;
|
private final AbstractComponent<?> component;
|
||||||
private final Value template;
|
private final String template;
|
||||||
private final boolean command;
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
private final String defaultValue;
|
|
||||||
private final boolean parseValueAsInteger;
|
|
||||||
|
|
||||||
public HomeAssistantChannelTransformation(HomeAssistantPythonBridge python, AbstractComponent component,
|
public HomeAssistantChannelTransformation(Jinjava jinjava, AbstractComponent<?> component, String template) {
|
||||||
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) {
|
|
||||||
super((String) null);
|
super((String) null);
|
||||||
this.python = python;
|
this.jinjava = jinjava;
|
||||||
this.component = component;
|
this.component = component;
|
||||||
this.command = command;
|
this.template = template;
|
||||||
this.template = command ? python.newCommandTemplate(template) : python.newValueTemplate(template);
|
|
||||||
this.defaultValue = defaultValue;
|
|
||||||
this.parseValueAsInteger = parseValueAsInteger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEmpty() {
|
public boolean isEmpty() {
|
||||||
return false;
|
return template.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<String> apply(String value) {
|
public Optional<String> apply(String value) {
|
||||||
Object objValue = value;
|
return apply(template, value);
|
||||||
if (parseValueAsInteger) {
|
|
||||||
try {
|
|
||||||
objValue = (int) Float.parseFloat(value);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
logger.warn("Failed to parse value {} as integer: {}", value, e.getMessage());
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Object result = transform(objValue);
|
|
||||||
if (result == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return Optional.of(result.toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable String transform(Object value) {
|
public Optional<String> apply(String template, String value) {
|
||||||
|
Map<String, @Nullable Object> bindings = new HashMap<>();
|
||||||
|
|
||||||
|
logger.debug("about to transform '{}' by the function '{}'", value, template);
|
||||||
|
|
||||||
|
bindings.put("value", value);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return command ? python.renderCommandTemplate(template, value)
|
JsonNode tree = objectMapper.readTree(value);
|
||||||
: python.renderValueTemplate(template, value, defaultValue);
|
bindings.put("value_json", toObject(tree));
|
||||||
} catch (PolyglotException e) {
|
} catch (IOException e) {
|
||||||
logger.warn("Applying template for component {} failed: {}", component.getHaID().toShortTopic(),
|
// ok, then value_json is null...
|
||||||
e.getMessage(), e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable String transform(Object value, Map<String, @Nullable Object> variables) {
|
return apply(template, bindings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> apply(String template, Map<String, @Nullable Object> bindings) {
|
||||||
|
String transformationResult;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return command ? python.renderCommandTemplate(template, value, variables)
|
transformationResult = jinjava.render(template, bindings);
|
||||||
: python.renderValueTemplate(template, value, defaultValue, variables);
|
} catch (FatalTemplateErrorsException e) {
|
||||||
} catch (PolyglotException e) {
|
var error = e.getErrors().iterator();
|
||||||
logger.warn("Applying template for component {} failed: {}", component.getHaID().toShortTopic(),
|
Exception exception = null;
|
||||||
e.getMessage(), e);
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("transformation resulted in '{}'", transformationResult);
|
||||||
|
|
||||||
|
return Optional.of(transformationResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String, @Nullable Object> result = new HashMap<>();
|
||||||
|
Iterator<Entry<String, JsonNode>> it = node.fields();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
Entry<String, JsonNode> 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;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<Object> result = new ArrayList<>();
|
||||||
|
while (m.find()) {
|
||||||
|
result.add(resultForMatcher(m));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Object resultForMatcher(Matcher m) {
|
||||||
|
if (m.groupCount() == 0) {
|
||||||
|
return m.group();
|
||||||
|
} else if (m.groupCount() == 1) {
|
||||||
|
return m.group(1);
|
||||||
|
} else {
|
||||||
|
List<String> groups = new ArrayList<>(m.groupCount());
|
||||||
|
for (int i = 1; i <= m.groupCount(); ++i) {
|
||||||
|
groups.add(m.group(i));
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Matcher regexFindAll(Object var, JinjavaInterpreter interpreter, String find, String ignoreCaseStr) {
|
||||||
|
String s;
|
||||||
|
if (var == null) {
|
||||||
|
s = "None";
|
||||||
|
} else {
|
||||||
|
s = var.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean ignoreCase = ObjectTruthValue.evaluate(ignoreCaseStr);
|
||||||
|
int flags = 0;
|
||||||
|
if (ignoreCase) {
|
||||||
|
flags = Pattern.CASE_INSENSITIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
Pattern p;
|
||||||
|
try {
|
||||||
|
if (find instanceof String findString) {
|
||||||
|
p = Pattern.compile(findString, flags);
|
||||||
|
} else if (find == null) {
|
||||||
|
p = Pattern.compile("", flags);
|
||||||
|
} else {
|
||||||
|
throw new InvalidArgumentException(interpreter, this, InvalidReason.REGEX, 0, find);
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.matcher(s);
|
||||||
|
} catch (PatternSyntaxException e) {
|
||||||
|
throw new InvalidArgumentException(interpreter, this, InvalidReason.REGEX, 0, find);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.home-assistant.io/docs/configuration/templating/#regular-expressions
|
||||||
|
// https://github.com/home-assistant/core/blob/2024.12.2/homeassistant/helpers/template.py#L2448
|
||||||
|
@NonNullByDefault({})
|
||||||
|
private static class RegexFindAllIndexFilter extends RegexFindAllFilter {
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "regex_findall_index";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object filter(Object var, JinjavaInterpreter interpreter, String... args) {
|
||||||
|
if (args.length > 3) {
|
||||||
|
throw new TemplateSyntaxException(interpreter, getName(),
|
||||||
|
"requires at most 3 arguments (regex string, index, ignore case)");
|
||||||
|
}
|
||||||
|
|
||||||
|
String find = null;
|
||||||
|
if (args.length >= 1) {
|
||||||
|
find = args[0];
|
||||||
|
}
|
||||||
|
int index = 0;
|
||||||
|
if (args.length >= 2) {
|
||||||
|
index = Integer.valueOf(args[1]);
|
||||||
|
if (index < 0) {
|
||||||
|
throw new InvalidArgumentException(interpreter, this, InvalidReason.POSITIVE_NUMBER, 1, args[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String ignoreCase = null;
|
||||||
|
if (args.length == 3) {
|
||||||
|
ignoreCase = args[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher m = regexFindAll(var, interpreter, find, ignoreCase);
|
||||||
|
int i = 0;
|
||||||
|
while (i <= index) {
|
||||||
|
if (!m.find()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultForMatcher(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Functions {
|
||||||
|
// https://www.home-assistant.io/docs/configuration/templating/#immediate-if-iif
|
||||||
|
public static @Nullable Object iif(@Nullable Object value, @Nullable 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,101 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.util.Map;
|
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
|
||||||
import org.graalvm.polyglot.Context;
|
|
||||||
import org.graalvm.polyglot.Value;
|
|
||||||
import org.graalvm.python.embedding.GraalPyResources;
|
|
||||||
import org.graalvm.python.embedding.VirtualFileSystem;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Centralizes all calls into python to ensure thread safety and a single cached context
|
|
||||||
*
|
|
||||||
* @author Cody Cutrer - Initial contribution
|
|
||||||
*/
|
|
||||||
@NonNullByDefault
|
|
||||||
public class HomeAssistantPythonBridge {
|
|
||||||
private static final String PYTHON = "python";
|
|
||||||
private final Context context;
|
|
||||||
private final Value newCommandTemplateMeth, newValueTemplateMeth, renderCommandTemplateMeth,
|
|
||||||
renderValueTemplateMeth, renderCommandTemplateWithVariablesMeth, renderValueTemplateWithVariablesMeth;
|
|
||||||
|
|
||||||
public HomeAssistantPythonBridge() {
|
|
||||||
VirtualFileSystem vfs = VirtualFileSystem.newBuilder()
|
|
||||||
.resourceDirectory("GRAALPY-VFS/org.openhab.addons.bundles/org.openhab.binding.mqtt.homeassistant")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
context = GraalPyResources.contextBuilder(vfs).build();
|
|
||||||
|
|
||||||
Value bindings = context.getBindings(PYTHON);
|
|
||||||
|
|
||||||
context.eval(PYTHON,
|
|
||||||
"""
|
|
||||||
from homeassistant.helpers.template import Template
|
|
||||||
from homeassistant.components.mqtt.models import MqttCommandTemplate, MqttValueTemplate
|
|
||||||
|
|
||||||
def new_command_template(template_string):
|
|
||||||
return MqttCommandTemplate(Template(template_string))
|
|
||||||
|
|
||||||
def render_command_template(template, value):
|
|
||||||
return template.render(value=value)
|
|
||||||
|
|
||||||
def render_command_template_with_variables(template, value, variables):
|
|
||||||
return template.render(value=value, variables=variables)
|
|
||||||
|
|
||||||
def new_value_template(template_string):
|
|
||||||
return MqttValueTemplate(Template(template_string))
|
|
||||||
|
|
||||||
def render_value_template(template, payload, default):
|
|
||||||
return template.render_with_possible_json_value(payload=payload, default=default)
|
|
||||||
|
|
||||||
def render_value_template_with_variables(template, payload, default, variables):
|
|
||||||
return template.render_with_possible_json_value(payload=payload, default=default, variables=variables)
|
|
||||||
""");
|
|
||||||
|
|
||||||
newCommandTemplateMeth = bindings.getMember("new_command_template");
|
|
||||||
renderCommandTemplateMeth = bindings.getMember("render_command_template");
|
|
||||||
renderCommandTemplateWithVariablesMeth = bindings.getMember("render_command_template_with_variables");
|
|
||||||
newValueTemplateMeth = bindings.getMember("new_value_template");
|
|
||||||
renderValueTemplateMeth = bindings.getMember("render_value_template");
|
|
||||||
renderValueTemplateWithVariablesMeth = bindings.getMember("render_value_template_with_variables");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Value newCommandTemplate(String template) {
|
|
||||||
return newCommandTemplateMeth.execute(template);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String renderCommandTemplate(Value template, Object value) {
|
|
||||||
return renderCommandTemplateMeth.execute(template, value).asString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String renderCommandTemplate(Value template, Object value, Map<String, @Nullable Object> variables) {
|
|
||||||
return renderCommandTemplateWithVariablesMeth.execute(template, value, variables).asString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Value newValueTemplate(String template) {
|
|
||||||
return newValueTemplateMeth.execute(template);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String renderValueTemplate(Value template, Object payload, String defaultValue) {
|
|
||||||
return renderValueTemplateMeth.execute(template, payload, defaultValue).asString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String renderValueTemplate(Value template, Object payload, String defaultValue,
|
|
||||||
Map<String, @Nullable Object> variables) {
|
|
||||||
return renderValueTemplateWithVariablesMeth.execute(template, payload, defaultValue, variables).asString();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -38,7 +38,6 @@ import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
|
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.HaID;
|
import org.openhab.binding.mqtt.homeassistant.internal.HaID;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantChannelTransformation;
|
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantChannelTransformation;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantPythonBridge;
|
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory.ComponentConfiguration;
|
import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory.ComponentConfiguration;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
|
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Availability;
|
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Availability;
|
||||||
|
@ -63,6 +62,7 @@ import org.openhab.core.types.StateDescription;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.annotations.SerializedName;
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
import com.hubspot.jinjava.Jinjava;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A HomeAssistant component is comparable to a channel group.
|
* A HomeAssistant component is comparable to a channel group.
|
||||||
|
@ -155,8 +155,7 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
|
||||||
String availabilityTemplate = availability.getValueTemplate();
|
String availabilityTemplate = availability.getValueTemplate();
|
||||||
ChannelTransformation transformation = null;
|
ChannelTransformation transformation = null;
|
||||||
if (availabilityTemplate != null) {
|
if (availabilityTemplate != null) {
|
||||||
transformation = new HomeAssistantChannelTransformation(getPython(), this, availabilityTemplate,
|
transformation = new HomeAssistantChannelTransformation(getJinjava(), this, availabilityTemplate);
|
||||||
false);
|
|
||||||
}
|
}
|
||||||
componentConfiguration.getTracker().addAvailabilityTopic(availability.getTopic(),
|
componentConfiguration.getTracker().addAvailabilityTopic(availability.getTopic(),
|
||||||
availability.getPayloadAvailable(), availability.getPayloadNotAvailable(), transformation);
|
availability.getPayloadAvailable(), availability.getPayloadNotAvailable(), transformation);
|
||||||
|
@ -167,8 +166,7 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
|
||||||
String availabilityTemplate = this.channelConfiguration.getAvailabilityTemplate();
|
String availabilityTemplate = this.channelConfiguration.getAvailabilityTemplate();
|
||||||
ChannelTransformation transformation = null;
|
ChannelTransformation transformation = null;
|
||||||
if (availabilityTemplate != null) {
|
if (availabilityTemplate != null) {
|
||||||
transformation = new HomeAssistantChannelTransformation(getPython(), this, availabilityTemplate,
|
transformation = new HomeAssistantChannelTransformation(getJinjava(), this, availabilityTemplate);
|
||||||
false);
|
|
||||||
}
|
}
|
||||||
componentConfiguration.getTracker().addAvailabilityTopic(availabilityTopic,
|
componentConfiguration.getTracker().addAvailabilityTopic(availabilityTopic,
|
||||||
this.channelConfiguration.getPayloadAvailable(),
|
this.channelConfiguration.getPayloadAvailable(),
|
||||||
|
@ -408,8 +406,8 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
|
||||||
return componentConfiguration.getGson();
|
return componentConfiguration.getGson();
|
||||||
}
|
}
|
||||||
|
|
||||||
public HomeAssistantPythonBridge getPython() {
|
public Jinjava getJinjava() {
|
||||||
return componentConfiguration.getPython();
|
return componentConfiguration.getJinjava();
|
||||||
}
|
}
|
||||||
|
|
||||||
public C getChannelConfiguration() {
|
public C getChannelConfiguration() {
|
||||||
|
|
|
@ -19,7 +19,6 @@ import org.openhab.binding.mqtt.generic.AvailabilityTracker;
|
||||||
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
|
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.HaID;
|
import org.openhab.binding.mqtt.homeassistant.internal.HaID;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantChannelLinkageChecker;
|
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantChannelLinkageChecker;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantPythonBridge;
|
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
|
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
|
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.exception.UnsupportedComponentException;
|
import org.openhab.binding.mqtt.homeassistant.internal.exception.UnsupportedComponentException;
|
||||||
|
@ -27,6 +26,7 @@ import org.openhab.core.i18n.UnitProvider;
|
||||||
import org.openhab.core.thing.ThingUID;
|
import org.openhab.core.thing.ThingUID;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
import com.hubspot.jinjava.Jinjava;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A factory to create HomeAssistant MQTT components. Those components are specified at:
|
* A factory to create HomeAssistant MQTT components. Those components are specified at:
|
||||||
|
@ -49,10 +49,10 @@ public class ComponentFactory {
|
||||||
*/
|
*/
|
||||||
public static AbstractComponent<?> createComponent(ThingUID thingUID, HaID haID, String channelConfigurationJSON,
|
public static AbstractComponent<?> createComponent(ThingUID thingUID, HaID haID, String channelConfigurationJSON,
|
||||||
ChannelStateUpdateListener updateListener, HomeAssistantChannelLinkageChecker linkageChecker,
|
ChannelStateUpdateListener updateListener, HomeAssistantChannelLinkageChecker linkageChecker,
|
||||||
AvailabilityTracker tracker, ScheduledExecutorService scheduler, Gson gson,
|
AvailabilityTracker tracker, ScheduledExecutorService scheduler, Gson gson, Jinjava jinjava,
|
||||||
HomeAssistantPythonBridge python, UnitProvider unitProvider) throws ConfigurationException {
|
UnitProvider unitProvider) throws ConfigurationException {
|
||||||
ComponentConfiguration componentConfiguration = new ComponentConfiguration(thingUID, haID,
|
ComponentConfiguration componentConfiguration = new ComponentConfiguration(thingUID, haID,
|
||||||
channelConfigurationJSON, gson, python, updateListener, linkageChecker, tracker, scheduler,
|
channelConfigurationJSON, gson, jinjava, updateListener, linkageChecker, tracker, scheduler,
|
||||||
unitProvider);
|
unitProvider);
|
||||||
switch (haID.component) {
|
switch (haID.component) {
|
||||||
case "alarm_control_panel":
|
case "alarm_control_panel":
|
||||||
|
@ -116,7 +116,7 @@ public class ComponentFactory {
|
||||||
private final HomeAssistantChannelLinkageChecker linkageChecker;
|
private final HomeAssistantChannelLinkageChecker linkageChecker;
|
||||||
private final AvailabilityTracker tracker;
|
private final AvailabilityTracker tracker;
|
||||||
private final Gson gson;
|
private final Gson gson;
|
||||||
private final HomeAssistantPythonBridge python;
|
private final Jinjava jinjava;
|
||||||
private final ScheduledExecutorService scheduler;
|
private final ScheduledExecutorService scheduler;
|
||||||
private final UnitProvider unitProvider;
|
private final UnitProvider unitProvider;
|
||||||
|
|
||||||
|
@ -128,15 +128,14 @@ public class ComponentFactory {
|
||||||
* @param configJSON The configuration string
|
* @param configJSON The configuration string
|
||||||
* @param gson A Gson instance
|
* @param gson A Gson instance
|
||||||
*/
|
*/
|
||||||
protected ComponentConfiguration(ThingUID thingUID, HaID haID, String configJSON, Gson gson,
|
protected ComponentConfiguration(ThingUID thingUID, HaID haID, String configJSON, Gson gson, Jinjava jinjava,
|
||||||
HomeAssistantPythonBridge python, ChannelStateUpdateListener updateListener,
|
ChannelStateUpdateListener updateListener, HomeAssistantChannelLinkageChecker linkageChecker,
|
||||||
HomeAssistantChannelLinkageChecker linkageChecker, AvailabilityTracker tracker,
|
AvailabilityTracker tracker, ScheduledExecutorService scheduler, UnitProvider unitProvider) {
|
||||||
ScheduledExecutorService scheduler, UnitProvider unitProvider) {
|
|
||||||
this.thingUID = thingUID;
|
this.thingUID = thingUID;
|
||||||
this.haID = haID;
|
this.haID = haID;
|
||||||
this.configJSON = configJSON;
|
this.configJSON = configJSON;
|
||||||
this.gson = gson;
|
this.gson = gson;
|
||||||
this.python = python;
|
this.jinjava = jinjava;
|
||||||
this.updateListener = updateListener;
|
this.updateListener = updateListener;
|
||||||
this.linkageChecker = linkageChecker;
|
this.linkageChecker = linkageChecker;
|
||||||
this.tracker = tracker;
|
this.tracker = tracker;
|
||||||
|
@ -168,8 +167,8 @@ public class ComponentFactory {
|
||||||
return gson;
|
return gson;
|
||||||
}
|
}
|
||||||
|
|
||||||
public HomeAssistantPythonBridge getPython() {
|
public Jinjava getJinjava() {
|
||||||
return python;
|
return jinjava;
|
||||||
}
|
}
|
||||||
|
|
||||||
public UnitProvider getUnitProvider() {
|
public UnitProvider getUnitProvider() {
|
||||||
|
|
|
@ -57,7 +57,7 @@ public class Event extends AbstractComponent<Event.ChannelConfiguration> impleme
|
||||||
public Event(ComponentFactory.ComponentConfiguration componentConfiguration) {
|
public Event(ComponentFactory.ComponentConfiguration componentConfiguration) {
|
||||||
super(componentConfiguration, ChannelConfiguration.class);
|
super(componentConfiguration, ChannelConfiguration.class);
|
||||||
|
|
||||||
transformation = new HomeAssistantChannelTransformation(getPython(), this, EVENT_TYPE_TRANFORMATION, false);
|
transformation = new HomeAssistantChannelTransformation(getJinjava(), this, "");
|
||||||
|
|
||||||
buildChannel(EVENT_TYPE_CHANNEL_ID, ComponentChannelType.TRIGGER, new TextValue(), getName(), this)
|
buildChannel(EVENT_TYPE_CHANNEL_ID, ComponentChannelType.TRIGGER, new TextValue(), getName(), this)
|
||||||
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate()).trigger(true)
|
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate()).trigger(true)
|
||||||
|
@ -87,7 +87,7 @@ public class Event extends AbstractComponent<Event.ChannelConfiguration> impleme
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void triggerChannel(ChannelUID channel, String event) {
|
public void triggerChannel(ChannelUID channel, String event) {
|
||||||
String eventType = transformation.apply(event).orElse(null);
|
String eventType = transformation.apply(EVENT_TYPE_TRANFORMATION, event).orElse(null);
|
||||||
if (eventType == null) {
|
if (eventType == null) {
|
||||||
// Warning logged from inside the transformation
|
// Warning logged from inside the transformation
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -159,8 +159,8 @@ public class Fan extends AbstractComponent<Fan.ChannelConfiguration> implements
|
||||||
.stateTopic(channelConfiguration.percentageStateTopic, channelConfiguration.percentageValueTemplate)
|
.stateTopic(channelConfiguration.percentageStateTopic, channelConfiguration.percentageValueTemplate)
|
||||||
.commandTopic(channelConfiguration.percentageCommandTopic, channelConfiguration.isRetain(),
|
.commandTopic(channelConfiguration.percentageCommandTopic, channelConfiguration.isRetain(),
|
||||||
channelConfiguration.getQos(), channelConfiguration.percentageCommandTemplate)
|
channelConfiguration.getQos(), channelConfiguration.percentageCommandTemplate)
|
||||||
.parseCommandValueAsInteger(true).inferOptimistic(channelConfiguration.optimistic)
|
.inferOptimistic(channelConfiguration.optimistic).commandFilter(this::handlePercentageCommand)
|
||||||
.commandFilter(this::handlePercentageCommand).build();
|
.build();
|
||||||
} else {
|
} else {
|
||||||
primaryChannel = onOffChannel;
|
primaryChannel = onOffChannel;
|
||||||
speedChannel = null;
|
speedChannel = null;
|
||||||
|
|
|
@ -54,9 +54,7 @@ import org.slf4j.LoggerFactory;
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
||||||
private final Logger logger = LoggerFactory.getLogger(TemplateSchemaLight.class);
|
private final Logger logger = LoggerFactory.getLogger(TemplateSchemaLight.class);
|
||||||
private @Nullable HomeAssistantChannelTransformation commandOnTransformation, commandOffTransformation,
|
private final HomeAssistantChannelTransformation transformation;
|
||||||
stateTransformation, brightnessTransformation, redTransformation, greenTransformation, blueTransformation,
|
|
||||||
effectTransformation, colorTempTransformation;
|
|
||||||
|
|
||||||
private static class TemplateVariables {
|
private static class TemplateVariables {
|
||||||
public static final String STATE = "state";
|
public static final String STATE = "state";
|
||||||
|
@ -74,36 +72,26 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
||||||
|
|
||||||
public TemplateSchemaLight(ComponentFactory.ComponentConfiguration builder) {
|
public TemplateSchemaLight(ComponentFactory.ComponentConfiguration builder) {
|
||||||
super(builder);
|
super(builder);
|
||||||
|
transformation = new HomeAssistantChannelTransformation(getJinjava(), this, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void buildChannels() {
|
protected void buildChannels() {
|
||||||
AutoUpdatePolicy autoUpdatePolicy = optimistic ? AutoUpdatePolicy.RECOMMEND : null;
|
AutoUpdatePolicy autoUpdatePolicy = optimistic ? AutoUpdatePolicy.RECOMMEND : null;
|
||||||
String commandOnTemplate = channelConfiguration.commandOnTemplate,
|
if (channelConfiguration.commandOnTemplate == null || channelConfiguration.commandOffTemplate == null) {
|
||||||
commandOffTemplate = channelConfiguration.commandOffTemplate;
|
|
||||||
if (commandOnTemplate == null || commandOffTemplate == null) {
|
|
||||||
throw new UnsupportedComponentException("Template schema light component '" + getHaID()
|
throw new UnsupportedComponentException("Template schema light component '" + getHaID()
|
||||||
+ "' does not define command_on_template or command_off_template!");
|
+ "' does not define command_on_template or command_off_template!");
|
||||||
}
|
}
|
||||||
|
|
||||||
onOffValue = new OnOffValue("on", "off");
|
onOffValue = new OnOffValue("on", "off");
|
||||||
brightnessValue = new PercentageValue(null, new BigDecimal(255), null, null, null, FORMAT_INTEGER);
|
brightnessValue = new PercentageValue(null, new BigDecimal(255), null, null, null, FORMAT_INTEGER);
|
||||||
commandOnTransformation = new HomeAssistantChannelTransformation(getPython(), this, commandOnTemplate, true);
|
|
||||||
commandOffTransformation = new HomeAssistantChannelTransformation(getPython(), this, commandOffTemplate, true);
|
|
||||||
|
|
||||||
String redTemplate = channelConfiguration.redTemplate, greenTemplate = channelConfiguration.greenTemplate,
|
if (channelConfiguration.redTemplate != null && channelConfiguration.greenTemplate != null
|
||||||
blueTemplate = channelConfiguration.blueTemplate,
|
&& channelConfiguration.blueTemplate != null) {
|
||||||
brightnessTemplate = channelConfiguration.brightnessTemplate;
|
|
||||||
if (redTemplate != null && greenTemplate != null && blueTemplate != null) {
|
|
||||||
redTransformation = new HomeAssistantChannelTransformation(getPython(), this, redTemplate, false);
|
|
||||||
greenTransformation = new HomeAssistantChannelTransformation(getPython(), this, greenTemplate, false);
|
|
||||||
blueTransformation = new HomeAssistantChannelTransformation(getPython(), this, blueTemplate, false);
|
|
||||||
colorChannel = buildChannel(COLOR_CHANNEL_ID, ComponentChannelType.COLOR, colorValue, "Color", this)
|
colorChannel = buildChannel(COLOR_CHANNEL_ID, ComponentChannelType.COLOR, colorValue, "Color", this)
|
||||||
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleCommand(command))
|
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleCommand(command))
|
||||||
.withAutoUpdatePolicy(autoUpdatePolicy).build();
|
.withAutoUpdatePolicy(autoUpdatePolicy).build();
|
||||||
} else if (brightnessTemplate != null) {
|
} else if (channelConfiguration.brightnessTemplate != null) {
|
||||||
brightnessTransformation = new HomeAssistantChannelTransformation(getPython(), this, brightnessTemplate,
|
|
||||||
false);
|
|
||||||
brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, ComponentChannelType.DIMMER, brightnessValue,
|
brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, ComponentChannelType.DIMMER, brightnessValue,
|
||||||
"Brightness", this).commandTopic(DUMMY_TOPIC, true, 1)
|
"Brightness", this).commandTopic(DUMMY_TOPIC, true, 1)
|
||||||
.commandFilter(command -> handleCommand(command)).withAutoUpdatePolicy(autoUpdatePolicy).build();
|
.commandFilter(command -> handleCommand(command)).withAutoUpdatePolicy(autoUpdatePolicy).build();
|
||||||
|
@ -113,29 +101,17 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
||||||
.withAutoUpdatePolicy(autoUpdatePolicy).build();
|
.withAutoUpdatePolicy(autoUpdatePolicy).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
String colorTempTemplate = channelConfiguration.colorTempTemplate;
|
if (channelConfiguration.colorTempTemplate != null) {
|
||||||
if (colorTempTemplate != null) {
|
|
||||||
colorTempTransformation = new HomeAssistantChannelTransformation(getPython(), this, colorTempTemplate,
|
|
||||||
false);
|
|
||||||
|
|
||||||
buildChannel(COLOR_TEMP_CHANNEL_ID, ComponentChannelType.NUMBER, colorTempValue, "Color Temperature", this)
|
buildChannel(COLOR_TEMP_CHANNEL_ID, ComponentChannelType.NUMBER, colorTempValue, "Color Temperature", this)
|
||||||
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleColorTempCommand(command))
|
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleColorTempCommand(command))
|
||||||
.withAutoUpdatePolicy(autoUpdatePolicy).build();
|
.withAutoUpdatePolicy(autoUpdatePolicy).build();
|
||||||
}
|
}
|
||||||
TextValue localEffectValue = effectValue;
|
TextValue localEffectValue = effectValue;
|
||||||
String effectTemplate = channelConfiguration.effectTemplate;
|
if (channelConfiguration.effectTemplate != null && localEffectValue != null) {
|
||||||
if (effectTemplate != null && localEffectValue != null) {
|
|
||||||
effectTransformation = new HomeAssistantChannelTransformation(getPython(), this, effectTemplate, false);
|
|
||||||
|
|
||||||
buildChannel(EFFECT_CHANNEL_ID, ComponentChannelType.STRING, localEffectValue, "Effect", this)
|
buildChannel(EFFECT_CHANNEL_ID, ComponentChannelType.STRING, localEffectValue, "Effect", this)
|
||||||
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleEffectCommand(command))
|
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleEffectCommand(command))
|
||||||
.withAutoUpdatePolicy(autoUpdatePolicy).build();
|
.withAutoUpdatePolicy(autoUpdatePolicy).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
String stateTemplate = channelConfiguration.stateTemplate;
|
|
||||||
if (stateTemplate != null) {
|
|
||||||
stateTransformation = new HomeAssistantChannelTransformation(getPython(), this, stateTemplate, false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BigDecimal factor = new BigDecimal("2.55"); // string to not lose precision
|
private static BigDecimal factor = new BigDecimal("2.55"); // string to not lose precision
|
||||||
|
@ -143,14 +119,14 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
||||||
@Override
|
@Override
|
||||||
protected void publishState(HSBType state) {
|
protected void publishState(HSBType state) {
|
||||||
Map<String, @Nullable Object> binding = new HashMap<>();
|
Map<String, @Nullable Object> binding = new HashMap<>();
|
||||||
HomeAssistantChannelTransformation transformation;
|
String template;
|
||||||
|
|
||||||
logger.trace("Publishing new state {} of light {} to MQTT.", state, getName());
|
logger.trace("Publishing new state {} of light {} to MQTT.", state, getName());
|
||||||
if (state.getBrightness().equals(PercentType.ZERO)) {
|
if (state.getBrightness().equals(PercentType.ZERO)) {
|
||||||
transformation = Objects.requireNonNull(commandOffTransformation);
|
template = Objects.requireNonNull(channelConfiguration.commandOffTemplate);
|
||||||
binding.put(TemplateVariables.STATE, "off");
|
binding.put(TemplateVariables.STATE, "off");
|
||||||
} else {
|
} else {
|
||||||
transformation = Objects.requireNonNull(commandOnTransformation);
|
template = Objects.requireNonNull(channelConfiguration.commandOnTemplate);
|
||||||
binding.put(TemplateVariables.STATE, "on");
|
binding.put(TemplateVariables.STATE, "on");
|
||||||
if (channelConfiguration.brightnessTemplate != null) {
|
if (channelConfiguration.brightnessTemplate != null) {
|
||||||
binding.put(TemplateVariables.BRIGHTNESS,
|
binding.put(TemplateVariables.BRIGHTNESS,
|
||||||
|
@ -166,7 +142,7 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
publishState(binding, transformation);
|
publishState(binding, template);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean handleColorTempCommand(Command command) {
|
private boolean handleColorTempCommand(Command command) {
|
||||||
|
@ -185,7 +161,7 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
||||||
binding.put(TemplateVariables.STATE, "on");
|
binding.put(TemplateVariables.STATE, "on");
|
||||||
binding.put(TemplateVariables.COLOR_TEMP, mireds.toBigDecimal().intValue());
|
binding.put(TemplateVariables.COLOR_TEMP, mireds.toBigDecimal().intValue());
|
||||||
|
|
||||||
publishState(binding, Objects.requireNonNull(commandOnTransformation));
|
publishState(binding, Objects.requireNonNull(channelConfiguration.commandOnTemplate));
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -200,15 +176,14 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
||||||
binding.put(TemplateVariables.STATE, "on");
|
binding.put(TemplateVariables.STATE, "on");
|
||||||
binding.put(TemplateVariables.EFFECT, command.toString());
|
binding.put(TemplateVariables.EFFECT, command.toString());
|
||||||
|
|
||||||
publishState(binding, Objects.requireNonNull(commandOnTransformation));
|
publishState(binding, Objects.requireNonNull(channelConfiguration.commandOnTemplate));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void publishState(Map<String, @Nullable Object> binding,
|
private void publishState(Map<String, @Nullable Object> binding, String template) {
|
||||||
HomeAssistantChannelTransformation transformation) {
|
|
||||||
String command;
|
String command;
|
||||||
|
|
||||||
command = transform(transformation, binding);
|
command = transform(template, binding);
|
||||||
if (command == null) {
|
if (command == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -223,9 +198,9 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
||||||
|
|
||||||
String value;
|
String value;
|
||||||
|
|
||||||
HomeAssistantChannelTransformation stateTransformation = this.stateTransformation;
|
String template = channelConfiguration.stateTemplate;
|
||||||
if (stateTransformation != null) {
|
if (template != null) {
|
||||||
value = transform(stateTransformation, state.toString());
|
value = transform(template, state.toString());
|
||||||
if (value == null || value.isEmpty()) {
|
if (value == null || value.isEmpty()) {
|
||||||
onOffValue.update(UnDefType.NULL);
|
onOffValue.update(UnDefType.NULL);
|
||||||
} else if ("on".equals(value)) {
|
} else if ("on".equals(value)) {
|
||||||
|
@ -242,15 +217,14 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
||||||
brightnessValue.update(
|
brightnessValue.update(
|
||||||
(PercentType) Objects.requireNonNull(onOffValue.getChannelState().as(PercentType.class)));
|
(PercentType) Objects.requireNonNull(onOffValue.getChannelState().as(PercentType.class)));
|
||||||
}
|
}
|
||||||
if (colorValue.getChannelState() instanceof UnDefType
|
if (colorValue.getChannelState() instanceof UnDefType) {
|
||||||
&& onOffValue.getChannelState() instanceof OnOffType onOffValue) {
|
colorValue.update((OnOffType) onOffValue.getChannelState());
|
||||||
colorValue.update(onOffValue);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HomeAssistantChannelTransformation brightnessTransformation = this.brightnessTransformation;
|
template = channelConfiguration.brightnessTemplate;
|
||||||
if (brightnessTransformation != null) {
|
if (template != null) {
|
||||||
Integer brightness = getColorChannelValue(brightnessTransformation, state.toString());
|
Integer brightness = getColorChannelValue(template, state.toString());
|
||||||
if (brightness == null) {
|
if (brightness == null) {
|
||||||
brightnessValue.update(UnDefType.NULL);
|
brightnessValue.update(UnDefType.NULL);
|
||||||
colorValue.update(UnDefType.NULL);
|
colorValue.update(UnDefType.NULL);
|
||||||
|
@ -267,13 +241,13 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
HomeAssistantChannelTransformation redTransformation, greenTransformation, blueTransformation;
|
String redTemplate, greenTemplate, blueTemplate;
|
||||||
if ((redTransformation = this.redTransformation) != null
|
if ((redTemplate = channelConfiguration.redTemplate) != null
|
||||||
&& (greenTransformation = this.greenTransformation) != null
|
&& (greenTemplate = channelConfiguration.greenTemplate) != null
|
||||||
&& (blueTransformation = this.blueTransformation) != null) {
|
&& (blueTemplate = channelConfiguration.blueTemplate) != null) {
|
||||||
Integer red = getColorChannelValue(redTransformation, state.toString());
|
Integer red = getColorChannelValue(redTemplate, state.toString());
|
||||||
Integer green = getColorChannelValue(greenTransformation, state.toString());
|
Integer green = getColorChannelValue(greenTemplate, state.toString());
|
||||||
Integer blue = getColorChannelValue(blueTransformation, state.toString());
|
Integer blue = getColorChannelValue(blueTemplate, state.toString());
|
||||||
if (red == null || green == null || blue == null) {
|
if (red == null || green == null || blue == null) {
|
||||||
colorValue.update(UnDefType.NULL);
|
colorValue.update(UnDefType.NULL);
|
||||||
} else {
|
} else {
|
||||||
|
@ -291,9 +265,9 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
||||||
listener.updateChannelState(onOffChannel.getChannel().getUID(), onOffValue.getChannelState());
|
listener.updateChannelState(onOffChannel.getChannel().getUID(), onOffValue.getChannelState());
|
||||||
}
|
}
|
||||||
|
|
||||||
HomeAssistantChannelTransformation effectTransformation = this.effectTransformation;
|
template = channelConfiguration.effectTemplate;
|
||||||
if (effectTransformation != null) {
|
if (template != null) {
|
||||||
value = transform(effectTransformation, state.toString());
|
value = transform(template, state.toString());
|
||||||
if (value == null || value.isEmpty()) {
|
if (value == null || value.isEmpty()) {
|
||||||
effectValue.update(UnDefType.NULL);
|
effectValue.update(UnDefType.NULL);
|
||||||
} else {
|
} else {
|
||||||
|
@ -302,9 +276,9 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
||||||
listener.updateChannelState(buildChannelUID(EFFECT_CHANNEL_ID), effectValue.getChannelState());
|
listener.updateChannelState(buildChannelUID(EFFECT_CHANNEL_ID), effectValue.getChannelState());
|
||||||
}
|
}
|
||||||
|
|
||||||
HomeAssistantChannelTransformation colorTempTransformation = this.colorTempTransformation;
|
template = channelConfiguration.colorTempTemplate;
|
||||||
if (colorTempTransformation != null) {
|
if (template != null) {
|
||||||
Integer mireds = getColorChannelValue(colorTempTransformation, state.toString());
|
Integer mireds = getColorChannelValue(template, state.toString());
|
||||||
if (mireds == null) {
|
if (mireds == null) {
|
||||||
colorTempValue.update(UnDefType.NULL);
|
colorTempValue.update(UnDefType.NULL);
|
||||||
} else {
|
} else {
|
||||||
|
@ -314,8 +288,8 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private @Nullable Integer getColorChannelValue(HomeAssistantChannelTransformation transformation, String value) {
|
private @Nullable Integer getColorChannelValue(String template, String value) {
|
||||||
Object result = transform(transformation, value);
|
Object result = transform(template, value);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -327,21 +301,17 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
||||||
try {
|
try {
|
||||||
return Integer.parseInt(result.toString());
|
return Integer.parseInt(result.toString());
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
logger.warn("Applying template for component {} failed: {}", getHaID().toShortTopic(), e.getMessage());
|
logger.warn("Applying template {} for component {} failed: {}", template, getHaID().toShortTopic(),
|
||||||
|
e.getMessage());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private @Nullable String transform(HomeAssistantChannelTransformation transformation,
|
private @Nullable String transform(String template, Map<String, @Nullable Object> binding) {
|
||||||
Map<String, @Nullable Object> variables) {
|
return transformation.apply(template, binding).orElse(null);
|
||||||
Object result = transformation.transform("", variables);
|
|
||||||
if (result == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return result.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private @Nullable String transform(HomeAssistantChannelTransformation transformation, String value) {
|
private @Nullable String transform(String template, String value) {
|
||||||
return transformation.apply(value).orElse(null);
|
return transformation.apply(template, value).orElse(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,6 @@ import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents.Compon
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.HaID;
|
import org.openhab.binding.mqtt.homeassistant.internal.HaID;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
|
import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantChannelLinkageChecker;
|
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantChannelLinkageChecker;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantPythonBridge;
|
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.actions.HomeAssistantUpdateThingActions;
|
import org.openhab.binding.mqtt.homeassistant.internal.actions.HomeAssistantUpdateThingActions;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent;
|
import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory;
|
import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory;
|
||||||
|
@ -64,6 +63,7 @@ import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.hubspot.jinjava.Jinjava;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles HomeAssistant MQTT object things. Such an HA Object can have multiple HA Components with different instances
|
* Handles HomeAssistant MQTT object things. Such an HA Object can have multiple HA Components with different instances
|
||||||
|
@ -95,7 +95,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
||||||
protected final MqttChannelTypeProvider channelTypeProvider;
|
protected final MqttChannelTypeProvider channelTypeProvider;
|
||||||
protected final MqttChannelStateDescriptionProvider stateDescriptionProvider;
|
protected final MqttChannelStateDescriptionProvider stateDescriptionProvider;
|
||||||
protected final ChannelTypeRegistry channelTypeRegistry;
|
protected final ChannelTypeRegistry channelTypeRegistry;
|
||||||
protected final HomeAssistantPythonBridge python;
|
protected final Jinjava jinjava;
|
||||||
protected final UnitProvider unitProvider;
|
protected final UnitProvider unitProvider;
|
||||||
public final int attributeReceiveTimeout;
|
public final int attributeReceiveTimeout;
|
||||||
protected final DelayedBatchProcessing<Object> delayedProcessing;
|
protected final DelayedBatchProcessing<Object> delayedProcessing;
|
||||||
|
@ -124,19 +124,19 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
||||||
*/
|
*/
|
||||||
public HomeAssistantThingHandler(Thing thing, BaseThingHandlerFactory thingHandlerFactory,
|
public HomeAssistantThingHandler(Thing thing, BaseThingHandlerFactory thingHandlerFactory,
|
||||||
MqttChannelTypeProvider channelTypeProvider, MqttChannelStateDescriptionProvider stateDescriptionProvider,
|
MqttChannelTypeProvider channelTypeProvider, MqttChannelStateDescriptionProvider stateDescriptionProvider,
|
||||||
ChannelTypeRegistry channelTypeRegistry, HomeAssistantPythonBridge python, UnitProvider unitProvider,
|
ChannelTypeRegistry channelTypeRegistry, Jinjava jinjava, UnitProvider unitProvider, int subscribeTimeout,
|
||||||
int subscribeTimeout, int attributeReceiveTimeout) {
|
int attributeReceiveTimeout) {
|
||||||
super(thing, subscribeTimeout);
|
super(thing, subscribeTimeout);
|
||||||
this.gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
|
this.gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
|
||||||
this.thingHandlerFactory = thingHandlerFactory;
|
this.thingHandlerFactory = thingHandlerFactory;
|
||||||
this.channelTypeProvider = channelTypeProvider;
|
this.channelTypeProvider = channelTypeProvider;
|
||||||
this.stateDescriptionProvider = stateDescriptionProvider;
|
this.stateDescriptionProvider = stateDescriptionProvider;
|
||||||
this.channelTypeRegistry = channelTypeRegistry;
|
this.channelTypeRegistry = channelTypeRegistry;
|
||||||
this.python = python;
|
this.jinjava = jinjava;
|
||||||
this.unitProvider = unitProvider;
|
this.unitProvider = unitProvider;
|
||||||
this.attributeReceiveTimeout = attributeReceiveTimeout;
|
this.attributeReceiveTimeout = attributeReceiveTimeout;
|
||||||
this.delayedProcessing = new DelayedBatchProcessing<>(attributeReceiveTimeout, this, scheduler);
|
this.delayedProcessing = new DelayedBatchProcessing<>(attributeReceiveTimeout, this, scheduler);
|
||||||
this.discoverComponents = new DiscoverComponents(thing.getUID(), scheduler, this, this, this, gson, python,
|
this.discoverComponents = new DiscoverComponents(thing.getUID(), scheduler, this, this, this, gson, jinjava,
|
||||||
unitProvider);
|
unitProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,7 +185,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
||||||
String channelConfigurationJSON = (String) channelConfig.get("config");
|
String channelConfigurationJSON = (String) channelConfig.get("config");
|
||||||
try {
|
try {
|
||||||
AbstractComponent<?> component = ComponentFactory.createComponent(thingUID, haID,
|
AbstractComponent<?> component = ComponentFactory.createComponent(thingUID, haID,
|
||||||
channelConfigurationJSON, this, this, this, scheduler, gson, python, unitProvider);
|
channelConfigurationJSON, this, this, this, scheduler, gson, jinjava, unitProvider);
|
||||||
if (typeID.equals(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)) {
|
if (typeID.equals(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)) {
|
||||||
typeID = calculateThingTypeUID(component);
|
typeID = calculateThingTypeUID(component);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
"""Constants used by multiple MQTT modules."""
|
|
||||||
|
|
||||||
import jinja2
|
|
||||||
|
|
||||||
from homeassistant.exceptions import TemplateError
|
|
||||||
|
|
||||||
TEMPLATE_ERRORS = (jinja2.TemplateError, TemplateError, TypeError, ValueError)
|
|
|
@ -1,210 +0,0 @@
|
||||||
"""Models used by multiple MQTT modules."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from ast import literal_eval
|
|
||||||
from collections.abc import Mapping
|
|
||||||
from enum import StrEnum
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from homeassistant.exceptions import ServiceValidationError, TemplateError
|
|
||||||
from homeassistant.helpers import template
|
|
||||||
|
|
||||||
from .const import TEMPLATE_ERRORS
|
|
||||||
|
|
||||||
class PayloadSentinel(StrEnum):
|
|
||||||
"""Sentinel for `render_with_possible_json_value`."""
|
|
||||||
|
|
||||||
NONE = "none"
|
|
||||||
DEFAULT = "default"
|
|
||||||
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def convert_outgoing_mqtt_payload(
|
|
||||||
payload: str | bytes | int | float | None,
|
|
||||||
) -> str | bytes | int | float | None:
|
|
||||||
"""Ensure correct raw MQTT payload is passed as bytes for publishing."""
|
|
||||||
if isinstance(payload, str) and payload.startswith(("b'", 'b"')):
|
|
||||||
try:
|
|
||||||
native_object = literal_eval(payload)
|
|
||||||
except (ValueError, TypeError, SyntaxError, MemoryError):
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if isinstance(native_object, bytes):
|
|
||||||
return native_object
|
|
||||||
|
|
||||||
return payload
|
|
||||||
|
|
||||||
|
|
||||||
class MqttCommandTemplateException(ServiceValidationError):
|
|
||||||
"""Handle MqttCommandTemplate exceptions."""
|
|
||||||
|
|
||||||
_message: str
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*args: object,
|
|
||||||
base_exception: Exception,
|
|
||||||
command_template: str,
|
|
||||||
value: str | bytes | int | float | None,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize exception."""
|
|
||||||
super().__init__(base_exception, *args)
|
|
||||||
value_log = str(value)
|
|
||||||
self._message = (
|
|
||||||
f"{type(base_exception).__name__}: {base_exception} rendering template"
|
|
||||||
f", template: '{command_template}' and payload: {value_log}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
"""Return exception message string."""
|
|
||||||
return self._message
|
|
||||||
|
|
||||||
|
|
||||||
class MqttCommandTemplate:
|
|
||||||
"""Class for rendering MQTT payload with command templates."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
command_template: template.Template | None,
|
|
||||||
) -> None:
|
|
||||||
"""Instantiate a command template."""
|
|
||||||
self._template_state: template.TemplateStateFromEntityId | None = None
|
|
||||||
self._command_template = command_template
|
|
||||||
|
|
||||||
def render(
|
|
||||||
self,
|
|
||||||
value: str | bytes | int | float | None = None,
|
|
||||||
variables: Mapping[str, Any] | None = None,
|
|
||||||
) -> str | bytes | int | float | None:
|
|
||||||
"""Render or convert the command template with given value or variables."""
|
|
||||||
if self._command_template is None:
|
|
||||||
return value
|
|
||||||
|
|
||||||
values: dict[str, Any] = {"value": value}
|
|
||||||
|
|
||||||
if variables is not None:
|
|
||||||
values.update(variables)
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Rendering outgoing payload with variables %s and %s",
|
|
||||||
values,
|
|
||||||
self._command_template,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
return convert_outgoing_mqtt_payload(
|
|
||||||
self._command_template.render(values, parse_result=False)
|
|
||||||
)
|
|
||||||
except TemplateError as exc:
|
|
||||||
raise MqttCommandTemplateException(
|
|
||||||
base_exception=exc,
|
|
||||||
command_template=self._command_template.template,
|
|
||||||
value=value,
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
|
|
||||||
class MqttValueTemplateException(TemplateError):
|
|
||||||
"""Handle MqttValueTemplate exceptions."""
|
|
||||||
|
|
||||||
_message: str
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*args: object,
|
|
||||||
base_exception: Exception,
|
|
||||||
value_template: str,
|
|
||||||
default: str | bytes | bytearray | PayloadSentinel,
|
|
||||||
payload: str | bytes | bytearray,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize exception."""
|
|
||||||
super().__init__(base_exception, *args)
|
|
||||||
default_log = str(default)
|
|
||||||
default_payload_log = (
|
|
||||||
"" if default is PayloadSentinel.NONE else f", default value: {default_log}"
|
|
||||||
)
|
|
||||||
payload_log = str(payload)
|
|
||||||
self._message = (
|
|
||||||
f"{type(base_exception).__name__}: {base_exception} rendering template"
|
|
||||||
f", template: '{value_template}'{default_payload_log} and payload: {payload_log}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
"""Return exception message string."""
|
|
||||||
return self._message
|
|
||||||
|
|
||||||
|
|
||||||
class MqttValueTemplate:
|
|
||||||
"""Class for rendering MQTT value template with possible json values."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
value_template: template.Template | None,
|
|
||||||
) -> None:
|
|
||||||
"""Instantiate a value template."""
|
|
||||||
self._value_template = value_template
|
|
||||||
|
|
||||||
def render_with_possible_json_value(
|
|
||||||
self,
|
|
||||||
payload: str | bytes | bytearray,
|
|
||||||
default: str | bytes | bytearray | PayloadSentinel = PayloadSentinel.NONE,
|
|
||||||
variables: Mapping[str, Any] | None = None,
|
|
||||||
) -> str | bytes | bytearray:
|
|
||||||
"""Render with possible json value or pass-though a received MQTT value."""
|
|
||||||
rendered_payload: str | bytes | bytearray
|
|
||||||
|
|
||||||
if self._value_template is None:
|
|
||||||
return payload
|
|
||||||
|
|
||||||
values: dict[str, Any] = {}
|
|
||||||
|
|
||||||
if variables is not None:
|
|
||||||
values.update(variables)
|
|
||||||
|
|
||||||
if default is PayloadSentinel.NONE:
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Rendering incoming payload '%s' with variables %s and %s",
|
|
||||||
payload,
|
|
||||||
values,
|
|
||||||
self._value_template,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
rendered_payload = (
|
|
||||||
self._value_template.render_with_possible_json_value(
|
|
||||||
payload, variables=values
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except TEMPLATE_ERRORS as exc:
|
|
||||||
raise MqttValueTemplateException(
|
|
||||||
base_exception=exc,
|
|
||||||
value_template=self._value_template.template,
|
|
||||||
default=default,
|
|
||||||
payload=payload,
|
|
||||||
) from exc
|
|
||||||
return rendered_payload
|
|
||||||
|
|
||||||
_LOGGER.debug(
|
|
||||||
(
|
|
||||||
"Rendering incoming payload '%s' with variables %s with default value"
|
|
||||||
" '%s' and %s"
|
|
||||||
),
|
|
||||||
payload,
|
|
||||||
values,
|
|
||||||
default,
|
|
||||||
self._value_template,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
rendered_payload = (
|
|
||||||
self._value_template.render_with_possible_json_value(
|
|
||||||
payload, default, variables=values
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except TEMPLATE_ERRORS as exc:
|
|
||||||
raise MqttValueTemplateException(
|
|
||||||
base_exception=exc,
|
|
||||||
value_template=self._value_template.template,
|
|
||||||
default=default,
|
|
||||||
payload=payload,
|
|
||||||
) from exc
|
|
||||||
return rendered_payload
|
|
|
@ -1,26 +0,0 @@
|
||||||
"""The exceptions used by Home Assistant."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
# this is from Voluptuous
|
|
||||||
class Invalid(Exception):
|
|
||||||
"""The data was invalid."""
|
|
||||||
|
|
||||||
|
|
||||||
class HomeAssistantError(Exception):
|
|
||||||
"""General Home Assistant exception occurred."""
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceValidationError(HomeAssistantError):
|
|
||||||
"""A validation exception occurred when calling a service."""
|
|
||||||
|
|
||||||
|
|
||||||
class TemplateError(HomeAssistantError):
|
|
||||||
"""Error during template rendering."""
|
|
||||||
|
|
||||||
def __init__(self, exception: Exception | str) -> None:
|
|
||||||
"""Init the error."""
|
|
||||||
if isinstance(exception, str):
|
|
||||||
super().__init__(exception)
|
|
||||||
else:
|
|
||||||
super().__init__(f"{exception.__class__.__name__}: {exception}")
|
|
|
@ -1,23 +0,0 @@
|
||||||
"""Helpers for config validation using voluptuous."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from numbers import Number
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from homeassistant.exceptions import Invalid
|
|
||||||
|
|
||||||
def boolean(value: Any) -> bool:
|
|
||||||
"""Validate and coerce a boolean value."""
|
|
||||||
if isinstance(value, bool):
|
|
||||||
return value
|
|
||||||
if isinstance(value, str):
|
|
||||||
value = value.lower().strip()
|
|
||||||
if value in ("1", "true", "yes", "on", "enable"):
|
|
||||||
return True
|
|
||||||
if value in ("0", "false", "no", "off", "disable"):
|
|
||||||
return False
|
|
||||||
elif isinstance(value, Number):
|
|
||||||
# type ignore: https://github.com/python/mypy/issues/3186
|
|
||||||
return value != 0 # type: ignore[comparison-overlap]
|
|
||||||
raise Invalid(f"invalid boolean value {value}")
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,12 +0,0 @@
|
||||||
"""Helper methods for various modules."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import slugify as unicode_slug
|
|
||||||
|
|
||||||
def slugify(text: str | None, *, separator: str = "_") -> str:
|
|
||||||
"""Slugify a given text."""
|
|
||||||
if text == "" or text is None:
|
|
||||||
return ""
|
|
||||||
slug = unicode_slug.slugify(text, separator=separator)
|
|
||||||
return "unknown" if slug == "" else slug
|
|
|
@ -1,321 +0,0 @@
|
||||||
"""Helper methods to handle the time in Home Assistant."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from contextlib import suppress
|
|
||||||
import datetime as dt
|
|
||||||
from functools import partial
|
|
||||||
import re
|
|
||||||
from typing import Any, Literal, overload
|
|
||||||
import zoneinfo
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
UTC = dt.UTC
|
|
||||||
DEFAULT_TIME_ZONE: dt.tzinfo = dt.UTC
|
|
||||||
|
|
||||||
# EPOCHORDINAL is not exposed as a constant
|
|
||||||
# https://github.com/python/cpython/blob/3.10/Lib/zoneinfo/_zoneinfo.py#L12
|
|
||||||
EPOCHORDINAL = dt.datetime(1970, 1, 1).toordinal()
|
|
||||||
|
|
||||||
# Copyright (c) Django Software Foundation and individual contributors.
|
|
||||||
# All rights reserved.
|
|
||||||
# https://github.com/django/django/blob/main/LICENSE
|
|
||||||
DATETIME_RE = re.compile(
|
|
||||||
r"(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})"
|
|
||||||
r"[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})"
|
|
||||||
r"(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?"
|
|
||||||
r"(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?$"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Copyright (c) Django Software Foundation and individual contributors.
|
|
||||||
# All rights reserved.
|
|
||||||
# https://github.com/django/django/blob/main/LICENSE
|
|
||||||
STANDARD_DURATION_RE = re.compile(
|
|
||||||
r"^"
|
|
||||||
r"(?:(?P<days>-?\d+) (days?, )?)?"
|
|
||||||
r"(?P<sign>-?)"
|
|
||||||
r"((?:(?P<hours>\d+):)(?=\d+:\d+))?"
|
|
||||||
r"(?:(?P<minutes>\d+):)?"
|
|
||||||
r"(?P<seconds>\d+)"
|
|
||||||
r"(?:[\.,](?P<microseconds>\d{1,6})\d{0,6})?"
|
|
||||||
r"$"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Copyright (c) Django Software Foundation and individual contributors.
|
|
||||||
# All rights reserved.
|
|
||||||
# https://github.com/django/django/blob/main/LICENSE
|
|
||||||
ISO8601_DURATION_RE = re.compile(
|
|
||||||
r"^(?P<sign>[-+]?)"
|
|
||||||
r"P"
|
|
||||||
r"(?:(?P<days>\d+([\.,]\d+)?)D)?"
|
|
||||||
r"(?:T"
|
|
||||||
r"(?:(?P<hours>\d+([\.,]\d+)?)H)?"
|
|
||||||
r"(?:(?P<minutes>\d+([\.,]\d+)?)M)?"
|
|
||||||
r"(?:(?P<seconds>\d+([\.,]\d+)?)S)?"
|
|
||||||
r")?"
|
|
||||||
r"$"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Copyright (c) Django Software Foundation and individual contributors.
|
|
||||||
# All rights reserved.
|
|
||||||
# https://github.com/django/django/blob/main/LICENSE
|
|
||||||
POSTGRES_INTERVAL_RE = re.compile(
|
|
||||||
r"^"
|
|
||||||
r"(?:(?P<days>-?\d+) (days? ?))?"
|
|
||||||
r"(?:(?P<sign>[-+])?"
|
|
||||||
r"(?P<hours>\d+):"
|
|
||||||
r"(?P<minutes>\d\d):"
|
|
||||||
r"(?P<seconds>\d\d)"
|
|
||||||
r"(?:\.(?P<microseconds>\d{1,6}))?"
|
|
||||||
r")?$"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def set_default_time_zone(time_zone: dt.tzinfo) -> None:
|
|
||||||
"""Set a default time zone to be used when none is specified.
|
|
||||||
|
|
||||||
Async friendly.
|
|
||||||
"""
|
|
||||||
# pylint: disable-next=global-statement
|
|
||||||
global DEFAULT_TIME_ZONE # noqa: PLW0603
|
|
||||||
|
|
||||||
assert isinstance(time_zone, dt.tzinfo)
|
|
||||||
|
|
||||||
DEFAULT_TIME_ZONE = time_zone
|
|
||||||
|
|
||||||
|
|
||||||
def get_time_zone(time_zone_str: str) -> zoneinfo.ZoneInfo | None:
|
|
||||||
"""Get time zone from string. Return None if unable to determine."""
|
|
||||||
try:
|
|
||||||
return zoneinfo.ZoneInfo(time_zone_str)
|
|
||||||
except zoneinfo.ZoneInfoNotFoundError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# We use a partial here since it is implemented in native code
|
|
||||||
# and avoids the global lookup of UTC
|
|
||||||
utcnow = partial(dt.datetime.now, UTC)
|
|
||||||
utcnow.__doc__ = "Get now in UTC time."
|
|
||||||
|
|
||||||
|
|
||||||
def now(time_zone: dt.tzinfo | None = None) -> dt.datetime:
|
|
||||||
"""Get now in specified time zone."""
|
|
||||||
return dt.datetime.now(time_zone or DEFAULT_TIME_ZONE)
|
|
||||||
|
|
||||||
|
|
||||||
def as_timestamp(dt_value: dt.datetime | str) -> float:
|
|
||||||
"""Convert a date/time into a unix time (seconds since 1970)."""
|
|
||||||
parsed_dt: dt.datetime | None
|
|
||||||
if isinstance(dt_value, dt.datetime):
|
|
||||||
parsed_dt = dt_value
|
|
||||||
else:
|
|
||||||
parsed_dt = parse_datetime(str(dt_value))
|
|
||||||
if parsed_dt is None:
|
|
||||||
raise ValueError("not a valid date/time.")
|
|
||||||
return parsed_dt.timestamp()
|
|
||||||
|
|
||||||
|
|
||||||
def as_local(dattim: dt.datetime) -> dt.datetime:
|
|
||||||
"""Convert a UTC datetime object to local time zone."""
|
|
||||||
if dattim.tzinfo == DEFAULT_TIME_ZONE:
|
|
||||||
return dattim
|
|
||||||
if dattim.tzinfo is None:
|
|
||||||
dattim = dattim.replace(tzinfo=DEFAULT_TIME_ZONE)
|
|
||||||
|
|
||||||
return dattim.astimezone(DEFAULT_TIME_ZONE)
|
|
||||||
|
|
||||||
|
|
||||||
# We use a partial here to improve performance by avoiding the global lookup
|
|
||||||
# of UTC and the function call overhead.
|
|
||||||
utc_from_timestamp = partial(dt.datetime.fromtimestamp, tz=UTC)
|
|
||||||
"""Return a UTC time from a timestamp."""
|
|
||||||
|
|
||||||
|
|
||||||
def start_of_local_day(dt_or_d: dt.date | dt.datetime | None = None) -> dt.datetime:
|
|
||||||
"""Return local datetime object of start of day from date or datetime."""
|
|
||||||
if dt_or_d is None:
|
|
||||||
date: dt.date = now().date()
|
|
||||||
elif isinstance(dt_or_d, dt.datetime):
|
|
||||||
date = dt_or_d.date()
|
|
||||||
else:
|
|
||||||
date = dt_or_d
|
|
||||||
|
|
||||||
return dt.datetime.combine(date, dt.time(), tzinfo=DEFAULT_TIME_ZONE)
|
|
||||||
|
|
||||||
|
|
||||||
# Copyright (c) Django Software Foundation and individual contributors.
|
|
||||||
# All rights reserved.
|
|
||||||
# https://github.com/django/django/blob/main/LICENSE
|
|
||||||
@overload
|
|
||||||
def parse_datetime(dt_str: str) -> dt.datetime | None: ...
|
|
||||||
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def parse_datetime(dt_str: str, *, raise_on_error: Literal[True]) -> dt.datetime: ...
|
|
||||||
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def parse_datetime(
|
|
||||||
dt_str: str, *, raise_on_error: Literal[False]
|
|
||||||
) -> dt.datetime | None: ...
|
|
||||||
|
|
||||||
|
|
||||||
def parse_datetime(dt_str: str, *, raise_on_error: bool = False) -> dt.datetime | None:
|
|
||||||
"""Parse a string and return a datetime.datetime.
|
|
||||||
|
|
||||||
This function supports time zone offsets. When the input contains one,
|
|
||||||
the output uses a timezone with a fixed offset from UTC.
|
|
||||||
Raises ValueError if the input is well formatted but not a valid datetime.
|
|
||||||
|
|
||||||
If the input isn't well formatted, returns None if raise_on_error is False
|
|
||||||
or raises ValueError if it's True.
|
|
||||||
"""
|
|
||||||
# First try if the string can be parsed by the stdlib
|
|
||||||
with suppress(ValueError, IndexError):
|
|
||||||
return datetime.fromisoformat(dt_str)
|
|
||||||
|
|
||||||
# stdlib failed to parse the string, fall back to regex
|
|
||||||
if not (match := DATETIME_RE.match(dt_str)):
|
|
||||||
if raise_on_error:
|
|
||||||
raise ValueError
|
|
||||||
return None
|
|
||||||
kws: dict[str, Any] = match.groupdict()
|
|
||||||
if kws["microsecond"]:
|
|
||||||
kws["microsecond"] = kws["microsecond"].ljust(6, "0")
|
|
||||||
tzinfo_str = kws.pop("tzinfo")
|
|
||||||
|
|
||||||
tzinfo: dt.tzinfo | None = None
|
|
||||||
if tzinfo_str == "Z":
|
|
||||||
tzinfo = UTC
|
|
||||||
elif tzinfo_str is not None:
|
|
||||||
offset_mins = int(tzinfo_str[-2:]) if len(tzinfo_str) > 3 else 0
|
|
||||||
offset_hours = int(tzinfo_str[1:3])
|
|
||||||
offset = dt.timedelta(hours=offset_hours, minutes=offset_mins)
|
|
||||||
if tzinfo_str[0] == "-":
|
|
||||||
offset = -offset
|
|
||||||
tzinfo = dt.timezone(offset)
|
|
||||||
kws = {k: int(v) for k, v in kws.items() if v is not None}
|
|
||||||
kws["tzinfo"] = tzinfo
|
|
||||||
return dt.datetime(**kws)
|
|
||||||
|
|
||||||
|
|
||||||
# Copyright (c) Django Software Foundation and individual contributors.
|
|
||||||
# All rights reserved.
|
|
||||||
# https://github.com/django/django/blob/master/LICENSE
|
|
||||||
def parse_duration(value: str) -> dt.timedelta | None:
|
|
||||||
"""Parse a duration string and return a datetime.timedelta.
|
|
||||||
|
|
||||||
Also supports ISO 8601 representation and PostgreSQL's day-time interval
|
|
||||||
format.
|
|
||||||
"""
|
|
||||||
match = (
|
|
||||||
STANDARD_DURATION_RE.match(value)
|
|
||||||
or ISO8601_DURATION_RE.match(value)
|
|
||||||
or POSTGRES_INTERVAL_RE.match(value)
|
|
||||||
)
|
|
||||||
if match:
|
|
||||||
kws = match.groupdict()
|
|
||||||
sign = -1 if kws.pop("sign", "+") == "-" else 1
|
|
||||||
if kws.get("microseconds"):
|
|
||||||
kws["microseconds"] = kws["microseconds"].ljust(6, "0")
|
|
||||||
time_delta_args: dict[str, float] = {
|
|
||||||
k: float(v.replace(",", ".")) for k, v in kws.items() if v is not None
|
|
||||||
}
|
|
||||||
days = dt.timedelta(float(time_delta_args.pop("days", 0.0) or 0.0))
|
|
||||||
if match.re == ISO8601_DURATION_RE:
|
|
||||||
days *= sign
|
|
||||||
return days + sign * dt.timedelta(**time_delta_args)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def parse_time(time_str: str) -> dt.time | None:
|
|
||||||
"""Parse a time string (00:20:00) into Time object.
|
|
||||||
|
|
||||||
Return None if invalid.
|
|
||||||
"""
|
|
||||||
parts = str(time_str).split(":")
|
|
||||||
if len(parts) < 2:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
hour = int(parts[0])
|
|
||||||
minute = int(parts[1])
|
|
||||||
second = int(parts[2]) if len(parts) > 2 else 0
|
|
||||||
return dt.time(hour, minute, second)
|
|
||||||
except ValueError:
|
|
||||||
# ValueError if value cannot be converted to an int or not in range
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _get_timestring(timediff: float, precision: int = 1) -> str:
|
|
||||||
"""Return a string representation of a time diff."""
|
|
||||||
|
|
||||||
def formatn(number: int, unit: str) -> str:
|
|
||||||
"""Add "unit" if it's plural."""
|
|
||||||
if number == 1:
|
|
||||||
return f"1 {unit} "
|
|
||||||
return f"{number:d} {unit}s "
|
|
||||||
|
|
||||||
if timediff == 0.0:
|
|
||||||
return "0 seconds"
|
|
||||||
|
|
||||||
units = ("year", "month", "day", "hour", "minute", "second")
|
|
||||||
|
|
||||||
factors = (365 * 24 * 60 * 60, 30 * 24 * 60 * 60, 24 * 60 * 60, 60 * 60, 60, 1)
|
|
||||||
|
|
||||||
result_string: str = ""
|
|
||||||
current_precision = 0
|
|
||||||
|
|
||||||
for i, current_factor in enumerate(factors):
|
|
||||||
selected_unit = units[i]
|
|
||||||
if timediff < current_factor:
|
|
||||||
continue
|
|
||||||
current_precision = current_precision + 1
|
|
||||||
if current_precision == precision:
|
|
||||||
return (
|
|
||||||
result_string + formatn(round(timediff / current_factor), selected_unit)
|
|
||||||
).rstrip()
|
|
||||||
curr_diff = int(timediff // current_factor)
|
|
||||||
result_string += formatn(curr_diff, selected_unit)
|
|
||||||
timediff -= (curr_diff) * current_factor
|
|
||||||
|
|
||||||
return result_string.rstrip()
|
|
||||||
|
|
||||||
|
|
||||||
def get_age(date: dt.datetime, precision: int = 1) -> str:
|
|
||||||
"""Take a datetime and return its "age" as a string.
|
|
||||||
|
|
||||||
The age can be in second, minute, hour, day, month and year.
|
|
||||||
|
|
||||||
depth number of units will be returned, with the last unit rounded
|
|
||||||
|
|
||||||
The date must be in the past or a ValueException will be raised.
|
|
||||||
"""
|
|
||||||
|
|
||||||
delta = (now() - date).total_seconds()
|
|
||||||
|
|
||||||
rounded_delta = round(delta)
|
|
||||||
|
|
||||||
if rounded_delta < 0:
|
|
||||||
raise ValueError("Time value is in the future")
|
|
||||||
return _get_timestring(rounded_delta, precision)
|
|
||||||
|
|
||||||
|
|
||||||
def get_time_remaining(date: dt.datetime, precision: int = 1) -> str:
|
|
||||||
"""Take a datetime and return its "age" as a string.
|
|
||||||
|
|
||||||
The age can be in second, minute, hour, day, month and year.
|
|
||||||
|
|
||||||
depth number of units will be returned, with the last unit rounded
|
|
||||||
|
|
||||||
The date must be in the future or a ValueException will be raised.
|
|
||||||
"""
|
|
||||||
|
|
||||||
delta = (date - now()).total_seconds()
|
|
||||||
|
|
||||||
rounded_delta = round(delta)
|
|
||||||
|
|
||||||
if rounded_delta < 0:
|
|
||||||
raise ValueError("Time value is in the past")
|
|
||||||
|
|
||||||
return _get_timestring(rounded_delta, precision)
|
|
|
@ -57,7 +57,11 @@ import org.openhab.core.thing.type.ChannelTypeRegistry;
|
||||||
import org.openhab.core.thing.type.ThingType;
|
import org.openhab.core.thing.type.ThingType;
|
||||||
import org.openhab.core.thing.type.ThingTypeBuilder;
|
import org.openhab.core.thing.type.ThingTypeBuilder;
|
||||||
import org.openhab.core.thing.type.ThingTypeRegistry;
|
import org.openhab.core.thing.type.ThingTypeRegistry;
|
||||||
|
import org.openhab.core.transform.TransformationHelper;
|
||||||
|
import org.openhab.core.transform.TransformationService;
|
||||||
import org.openhab.core.util.BundleResolver;
|
import org.openhab.core.util.BundleResolver;
|
||||||
|
import org.osgi.framework.BundleContext;
|
||||||
|
import org.osgi.framework.ServiceReference;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract class for HomeAssistant unit tests.
|
* Abstract class for HomeAssistant unit tests.
|
||||||
|
@ -82,7 +86,6 @@ public abstract class AbstractHomeAssistantTests extends JavaTest {
|
||||||
public static final ThingUID HA_UID = new ThingUID(MqttBindingConstants.HOMEASSISTANT_MQTT_THING, HA_ID);
|
public static final ThingUID HA_UID = new ThingUID(MqttBindingConstants.HOMEASSISTANT_MQTT_THING, HA_ID);
|
||||||
public static final ThingType HA_THING_TYPE = ThingTypeBuilder
|
public static final ThingType HA_THING_TYPE = ThingTypeBuilder
|
||||||
.instance(MqttBindingConstants.HOMEASSISTANT_MQTT_THING, HA_TYPE_LABEL).build();
|
.instance(MqttBindingConstants.HOMEASSISTANT_MQTT_THING, HA_TYPE_LABEL).build();
|
||||||
protected static final HomeAssistantPythonBridge python = new HomeAssistantPythonBridge();
|
|
||||||
|
|
||||||
protected @Mock @NonNullByDefault({}) MqttBrokerConnection bridgeConnection;
|
protected @Mock @NonNullByDefault({}) MqttBrokerConnection bridgeConnection;
|
||||||
protected @Mock @NonNullByDefault({}) ThingTypeRegistry thingTypeRegistry;
|
protected @Mock @NonNullByDefault({}) ThingTypeRegistry thingTypeRegistry;
|
||||||
|
@ -96,11 +99,20 @@ public abstract class AbstractHomeAssistantTests extends JavaTest {
|
||||||
protected Thing haThing = ThingBuilder.create(HA_TYPE_UID, HA_UID).withBridge(BRIDGE_UID).build();
|
protected Thing haThing = ThingBuilder.create(HA_TYPE_UID, HA_UID).withBridge(BRIDGE_UID).build();
|
||||||
protected final ConcurrentMap<String, Set<MqttMessageSubscriber>> subscriptions = new ConcurrentHashMap<>();
|
protected final ConcurrentMap<String, Set<MqttMessageSubscriber>> subscriptions = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private @Mock @NonNullByDefault({}) TransformationService transformationService1Mock;
|
||||||
|
|
||||||
|
private @Mock @NonNullByDefault({}) BundleContext bundleContextMock;
|
||||||
private @Mock @NonNullByDefault({}) TranslationProvider translationProvider;
|
private @Mock @NonNullByDefault({}) TranslationProvider translationProvider;
|
||||||
private @Mock @NonNullByDefault({}) BundleResolver bundleResolver;
|
private @Mock @NonNullByDefault({}) BundleResolver bundleResolver;
|
||||||
|
private @Mock @NonNullByDefault({}) ServiceReference<TransformationService> serviceRefMock;
|
||||||
|
|
||||||
|
private @NonNullByDefault({}) TransformationHelper transformationHelper;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void beforeEachAbstractHomeAssistantTests() {
|
public void beforeEachAbstractHomeAssistantTests() {
|
||||||
|
transformationHelper = new TransformationHelper(bundleContextMock);
|
||||||
|
transformationHelper.setTransformationService(serviceRefMock);
|
||||||
|
|
||||||
when(thingTypeRegistry.getThingType(BRIDGE_TYPE_UID))
|
when(thingTypeRegistry.getThingType(BRIDGE_TYPE_UID))
|
||||||
.thenReturn(ThingTypeBuilder.instance(BRIDGE_TYPE_UID, BRIDGE_TYPE_LABEL).build());
|
.thenReturn(ThingTypeBuilder.instance(BRIDGE_TYPE_UID, BRIDGE_TYPE_LABEL).build());
|
||||||
when(thingTypeRegistry.getThingType(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)).thenReturn(HA_THING_TYPE);
|
when(thingTypeRegistry.getThingType(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)).thenReturn(HA_THING_TYPE);
|
||||||
|
|
|
@ -24,10 +24,19 @@ import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.Mockito;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.mockito.junit.jupiter.MockitoSettings;
|
import org.mockito.junit.jupiter.MockitoSettings;
|
||||||
import org.mockito.quality.Strictness;
|
import org.mockito.quality.Strictness;
|
||||||
|
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.binding.mqtt.homeassistant.internal.component.AbstractComponent;
|
||||||
|
import org.openhab.core.i18n.TranslationProvider;
|
||||||
|
import org.openhab.core.i18n.UnitProvider;
|
||||||
|
import org.openhab.core.test.storage.VolatileStorageService;
|
||||||
|
import org.openhab.core.thing.type.ChannelTypeRegistry;
|
||||||
|
import org.openhab.core.thing.type.ThingTypeRegistry;
|
||||||
|
import org.openhab.core.util.BundleResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Jochen Klein - Initial contribution
|
* @author Jochen Klein - Initial contribution
|
||||||
|
@ -35,62 +44,72 @@ import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractCompone
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class HomeAssistantChannelTransformationTests extends AbstractHomeAssistantTests {
|
public class HomeAssistantChannelTransformationTests {
|
||||||
|
protected @Mock @NonNullByDefault({}) ThingTypeRegistry thingTypeRegistry;
|
||||||
|
protected @Mock @NonNullByDefault({}) UnitProvider unitProvider;
|
||||||
|
|
||||||
private @Mock @NonNullByDefault({}) AbstractComponent component;
|
protected @NonNullByDefault({}) HomeAssistantChannelTransformation transformation;
|
||||||
|
private @Mock @NonNullByDefault({}) BundleResolver bundleResolver;
|
||||||
|
private @Mock @NonNullByDefault({}) TranslationProvider translationProvider;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void beforeEachChannelTransformationTest() {
|
public void beforeEachChannelTransformationTest() {
|
||||||
|
MqttChannelTypeProvider channelTypeProvider = new MqttChannelTypeProvider(thingTypeRegistry,
|
||||||
|
new VolatileStorageService());
|
||||||
|
HomeAssistantStateDescriptionProvider stateDescriptionProvider = new HomeAssistantStateDescriptionProvider(
|
||||||
|
translationProvider, bundleResolver);
|
||||||
|
ChannelTypeRegistry channelTypeRegistry = new ChannelTypeRegistry();
|
||||||
|
MqttThingHandlerFactory thingHandlerFactory = new MqttThingHandlerFactory(channelTypeProvider,
|
||||||
|
stateDescriptionProvider, channelTypeRegistry, unitProvider);
|
||||||
|
|
||||||
|
AbstractComponent component = Mockito.mock(AbstractComponent.class);
|
||||||
HaID haID = new HaID("homeassistant/light/pool/light/config");
|
HaID haID = new HaID("homeassistant/light/pool/light/config");
|
||||||
when(component.getHaID()).thenReturn(haID);
|
when(component.getHaID()).thenReturn(haID);
|
||||||
}
|
transformation = new HomeAssistantChannelTransformation(thingHandlerFactory.getJinjava(), component, "");
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testInvalidTemplate() {
|
|
||||||
assertThat(transform("{{}}", ""), is(nullValue()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testIif() {
|
public void testIif() {
|
||||||
assertThat(transform("{{ iif(true) }}", ""), is("True"));
|
assertThat(transform("{{ iif(True) }}", ""), is("true"));
|
||||||
assertThat(transform("{{ iif(false) }}", ""), is("False"));
|
assertThat(transform("{{ iif(False) }}", ""), is("false"));
|
||||||
assertThat(transform("{{ iif(none) }}", ""), is("False"));
|
assertThat(transform("{{ iif(Null) }}", ""), is("false"));
|
||||||
assertThat(transform("{{ iif(true, 'Yes') }}", ""), is("Yes"));
|
assertThat(transform("{{ iif(True, 'Yes') }}", ""), is("Yes"));
|
||||||
assertThat(transform("{{ iif(false, 'Yes') }}", ""), is("False"));
|
assertThat(transform("{{ iif(False, 'Yes') }}", ""), is("false"));
|
||||||
assertThat(transform("{{ iif(none, 'Yes') }}", ""), is("False"));
|
assertThat(transform("{{ iif(Null, 'Yes') }}", ""), is("false"));
|
||||||
assertThat(transform("{{ iif(true, 'Yes', 'No') }}", ""), is("Yes"));
|
assertThat(transform("{{ iif(True, 'Yes', 'No') }}", ""), is("Yes"));
|
||||||
assertThat(transform("{{ iif(false, 'Yes', 'No') }}", ""), is("No"));
|
assertThat(transform("{{ iif(False, 'Yes', 'No') }}", ""), is("No"));
|
||||||
assertThat(transform("{{ iif(none, 'Yes', 'No') }}", ""), is("No"));
|
assertThat(transform("{{ iif(Null, 'Yes', 'No') }}", ""), is("No"));
|
||||||
assertThat(transform("{{ iif(true, 'Yes', 'No', none) }}", ""), is("Yes"));
|
assertThat(transform("{{ iif(True, 'Yes', 'No', null) }}", ""), is("Yes"));
|
||||||
assertThat(transform("{{ iif(false, 'Yes', 'No', none) }}", ""), is("No"));
|
assertThat(transform("{{ iif(False, 'Yes', 'No', null) }}", ""), is("No"));
|
||||||
assertThat(transform("{{ iif(none, 'Yes', 'No', 'NULL') }}", ""), is("NULL"));
|
assertThat(transform("{{ iif(Null, 'Yes', 'No', 'NULL') }}", ""), is("NULL"));
|
||||||
assertThat(transform("{{ iif(none, 'Yes', 'No', none) }}", ""), is("None"));
|
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("{{ True | iif('Yes') }}", ""), is("Yes"));
|
||||||
assertThat(transform("{{ false | iif('Yes') }}", ""), is("False"));
|
assertThat(transform("{{ False | iif('Yes') }}", ""), is("false"));
|
||||||
assertThat(transform("{{ none | iif('Yes') }}", ""), is("False"));
|
assertThat(transform("{{ Null | iif('Yes') }}", ""), is("false"));
|
||||||
assertThat(transform("{{ true | iif('Yes', 'No') }}", ""), is("Yes"));
|
assertThat(transform("{{ True | iif('Yes', 'No') }}", ""), is("Yes"));
|
||||||
assertThat(transform("{{ false | iif('Yes', 'No') }}", ""), is("No"));
|
assertThat(transform("{{ False | iif('Yes', 'No') }}", ""), is("No"));
|
||||||
assertThat(transform("{{ none | iif('Yes', 'No') }}", ""), is("No"));
|
assertThat(transform("{{ Null | iif('Yes', 'No') }}", ""), is("No"));
|
||||||
assertThat(transform("{{ true | iif('Yes', 'No', none) }}", ""), is("Yes"));
|
assertThat(transform("{{ True | iif('Yes', 'No', null) }}", ""), is("Yes"));
|
||||||
assertThat(transform("{{ false | iif('Yes', 'No', none) }}", ""), is("No"));
|
assertThat(transform("{{ False | iif('Yes', 'No', null) }}", ""), is("No"));
|
||||||
assertThat(transform("{{ none | iif('Yes', 'No', 'NULL') }}", ""), is("NULL"));
|
assertThat(transform("{{ Null | iif('Yes', 'No', 'NULL') }}", ""), is("NULL"));
|
||||||
assertThat(transform("{{ none | iif('Yes', 'No', none) }}", ""), is("None"));
|
assertThat(transform("{{ Null | iif('Yes', 'No', null) }}", ""), is(""));
|
||||||
|
assertThat(transform("{{ True | iif('Yes', 'No', null, null) }}", ""), is(nullValue()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testIsDefined() {
|
public void testIsDefined() {
|
||||||
assertThat(transform("{{ value_json.val }}", "{ \"val\": \"abc\" }", "default"), is("abc"));
|
assertThat(transform("{{ value_json.val | is_defined }}", "{}"), is(nullValue()));
|
||||||
assertThat(transform("{{ value_json.val }}", "{ \"val\": null }", "default"), is("None"));
|
assertThat(transform("{{ 'hi' | is_defined }}", "{}"), is("hi"));
|
||||||
assertThat(transform("{{ value_json.something | is_defined }}", "{ \"val\": null }", "default"), is("default"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRegexFindall() {
|
public void testRegexFindall() {
|
||||||
assertThat(transform("{{ 'Flight from JFK to LHR' | regex_findall('([A-Z]{3})') }}", ""), is("['JFK', 'LHR']"));
|
assertThat(transform("{{ 'Flight from JFK to LHR' | regex_findall('([A-Z]{3})') }}", ""), is("[JFK, LHR]"));
|
||||||
assertThat(transform(
|
assertThat(transform(
|
||||||
"{{ 'button_up_press' | regex_findall('^(?P<button>(?:button_)?[a-z0-9]+)_(?P<action>(?:press|hold)(?:_release)?)$') }}",
|
"{{ 'button_up_press' | regex_findall('^(?P<button>(?:button_)?[a-z0-9]+)_(?P<action>(?:press|hold)(?:_release)?)$') }}",
|
||||||
""), is("[('button_up', 'press')]"));
|
""), is("[[button_up, press]]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -100,24 +119,10 @@ public class HomeAssistantChannelTransformationTests extends AbstractHomeAssista
|
||||||
assertThat(transform("{{ ['JFK', 'LHR'] | regex_findall_index('([A-Z]{3})', 1) }}", ""), is("LHR"));
|
assertThat(transform("{{ ['JFK', 'LHR'] | regex_findall_index('([A-Z]{3})', 1) }}", ""), is("LHR"));
|
||||||
assertThat(transform(
|
assertThat(transform(
|
||||||
"{{ 'button_up_press' | regex_findall_index('^(?P<button>(?:button_)?[a-z0-9]+)_(?P<action>(?:press|hold)(?:_release)?)$') }}",
|
"{{ 'button_up_press' | regex_findall_index('^(?P<button>(?:button_)?[a-z0-9]+)_(?P<action>(?:press|hold)(?:_release)?)$') }}",
|
||||||
""), is("('button_up', 'press')"));
|
""), is("[button_up, press]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
protected @Nullable String transform(String template, String value) {
|
||||||
public void testIntegerDictLookup() {
|
return transformation.apply(template, value).orElse(null);
|
||||||
assertThat(transform("{{ {0:'off', 1:'low', 2:'medium', 3:'high'}[value] | default('') }}", 0, true),
|
|
||||||
is("off"));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected @Nullable Object transform(String template, Object value) {
|
|
||||||
return new HomeAssistantChannelTransformation(python, component, template, false).transform(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected @Nullable Object transform(String template, Object value, boolean command) {
|
|
||||||
return new HomeAssistantChannelTransformation(python, component, template, command).transform(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected @Nullable Object transform(String template, Object value, String defaultValue) {
|
|
||||||
return new HomeAssistantChannelTransformation(python, component, template, defaultValue).transform(value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,6 @@ import org.openhab.binding.mqtt.homeassistant.internal.AbstractHomeAssistantTest
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
|
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.HaID;
|
import org.openhab.binding.mqtt.homeassistant.internal.HaID;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
|
import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantPythonBridge;
|
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
|
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler;
|
import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler;
|
||||||
import org.openhab.core.i18n.UnitProvider;
|
import org.openhab.core.i18n.UnitProvider;
|
||||||
|
@ -56,6 +55,8 @@ import org.openhab.core.thing.type.ChannelTypeRegistry;
|
||||||
import org.openhab.core.types.Command;
|
import org.openhab.core.types.Command;
|
||||||
import org.openhab.core.types.State;
|
import org.openhab.core.types.State;
|
||||||
|
|
||||||
|
import com.hubspot.jinjava.Jinjava;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract class for components tests.
|
* Abstract class for components tests.
|
||||||
*
|
*
|
||||||
|
@ -87,7 +88,7 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
|
||||||
when(callbackMock.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing);
|
when(callbackMock.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing);
|
||||||
|
|
||||||
thingHandler = new LatchThingHandler(haThing, thingHandlerFactory, channelTypeProvider,
|
thingHandler = new LatchThingHandler(haThing, thingHandlerFactory, channelTypeProvider,
|
||||||
stateDescriptionProvider, channelTypeRegistry, python, unitProvider, SUBSCRIBE_TIMEOUT,
|
stateDescriptionProvider, channelTypeRegistry, unitProvider, SUBSCRIBE_TIMEOUT,
|
||||||
ATTRIBUTE_RECEIVE_TIMEOUT);
|
ATTRIBUTE_RECEIVE_TIMEOUT);
|
||||||
thingHandler.setConnection(bridgeConnection);
|
thingHandler.setConnection(bridgeConnection);
|
||||||
thingHandler.setCallback(callbackMock);
|
thingHandler.setCallback(callbackMock);
|
||||||
|
@ -366,10 +367,9 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
|
||||||
public LatchThingHandler(Thing thing, BaseThingHandlerFactory thingHandlerFactory,
|
public LatchThingHandler(Thing thing, BaseThingHandlerFactory thingHandlerFactory,
|
||||||
MqttChannelTypeProvider channelTypeProvider,
|
MqttChannelTypeProvider channelTypeProvider,
|
||||||
MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry,
|
MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry,
|
||||||
HomeAssistantPythonBridge python, UnitProvider unitProvider, int subscribeTimeout,
|
UnitProvider unitProvider, int subscribeTimeout, int attributeReceiveTimeout) {
|
||||||
int attributeReceiveTimeout) {
|
|
||||||
super(thing, thingHandlerFactory, channelTypeProvider, stateDescriptionProvider, channelTypeRegistry,
|
super(thing, thingHandlerFactory, channelTypeProvider, stateDescriptionProvider, channelTypeRegistry,
|
||||||
python, unitProvider, subscribeTimeout, attributeReceiveTimeout);
|
new Jinjava(), unitProvider, subscribeTimeout, attributeReceiveTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -48,6 +48,8 @@ import org.openhab.core.thing.binding.builder.ChannelBuilder;
|
||||||
import org.openhab.core.thing.binding.builder.ThingBuilder;
|
import org.openhab.core.thing.binding.builder.ThingBuilder;
|
||||||
import org.openhab.core.types.StateDescription;
|
import org.openhab.core.types.StateDescription;
|
||||||
|
|
||||||
|
import com.hubspot.jinjava.Jinjava;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link HomeAssistantThingHandler}
|
* Tests for {@link HomeAssistantThingHandler}
|
||||||
*
|
*
|
||||||
|
@ -89,7 +91,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
|
||||||
|
|
||||||
protected void setupThingHandler() {
|
protected void setupThingHandler() {
|
||||||
thingHandler = new HomeAssistantThingHandler(haThing, thingHandlerFactory, channelTypeProvider,
|
thingHandler = new HomeAssistantThingHandler(haThing, thingHandlerFactory, channelTypeProvider,
|
||||||
stateDescriptionProvider, channelTypeRegistry, python, unitProvider, SUBSCRIBE_TIMEOUT,
|
stateDescriptionProvider, channelTypeRegistry, new Jinjava(), unitProvider, SUBSCRIBE_TIMEOUT,
|
||||||
ATTRIBUTE_RECEIVE_TIMEOUT);
|
ATTRIBUTE_RECEIVE_TIMEOUT);
|
||||||
thingHandler.setConnection(bridgeConnection);
|
thingHandler.setConnection(bridgeConnection);
|
||||||
thingHandler.setCallback(callbackMock);
|
thingHandler.setCallback(callbackMock);
|
||||||
|
@ -359,7 +361,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
|
||||||
@Test
|
@Test
|
||||||
public void testDuplicateChannelId() {
|
public void testDuplicateChannelId() {
|
||||||
thingHandler = new HomeAssistantThingHandler(haThing, thingHandlerFactory, channelTypeProvider,
|
thingHandler = new HomeAssistantThingHandler(haThing, thingHandlerFactory, channelTypeProvider,
|
||||||
stateDescriptionProvider, channelTypeRegistry, python, unitProvider, SUBSCRIBE_TIMEOUT,
|
stateDescriptionProvider, channelTypeRegistry, new Jinjava(), unitProvider, SUBSCRIBE_TIMEOUT,
|
||||||
ATTRIBUTE_RECEIVE_TIMEOUT);
|
ATTRIBUTE_RECEIVE_TIMEOUT);
|
||||||
thingHandler.setConnection(bridgeConnection);
|
thingHandler.setConnection(bridgeConnection);
|
||||||
thingHandler.setCallback(callbackMock);
|
thingHandler.setCallback(callbackMock);
|
||||||
|
@ -416,7 +418,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
|
||||||
@Test
|
@Test
|
||||||
public void testDuplicateChannelIdComplex() {
|
public void testDuplicateChannelIdComplex() {
|
||||||
thingHandler = new HomeAssistantThingHandler(haThing, thingHandlerFactory, channelTypeProvider,
|
thingHandler = new HomeAssistantThingHandler(haThing, thingHandlerFactory, channelTypeProvider,
|
||||||
stateDescriptionProvider, channelTypeRegistry, python, unitProvider, SUBSCRIBE_TIMEOUT,
|
stateDescriptionProvider, channelTypeRegistry, new Jinjava(), unitProvider, SUBSCRIBE_TIMEOUT,
|
||||||
ATTRIBUTE_RECEIVE_TIMEOUT);
|
ATTRIBUTE_RECEIVE_TIMEOUT);
|
||||||
thingHandler.setConnection(bridgeConnection);
|
thingHandler.setConnection(bridgeConnection);
|
||||||
thingHandler.setCallback(callbackMock);
|
thingHandler.setCallback(callbackMock);
|
||||||
|
|
|
@ -23,14 +23,11 @@
|
||||||
<feature>openhab-runtime-base</feature>
|
<feature>openhab-runtime-base</feature>
|
||||||
<feature>openhab-transport-mqtt</feature>
|
<feature>openhab-transport-mqtt</feature>
|
||||||
<feature dependency="true">openhab.tp-commons-net</feature>
|
<feature dependency="true">openhab.tp-commons-net</feature>
|
||||||
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.sdk.collections/24.2.0</bundle>
|
<bundle dependency="true">mvn:org.openhab.osgiify/com.hubspot.jinjava.jinjava/2.7.4</bundle>
|
||||||
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.sdk.jniutils/24.2.0</bundle>
|
<bundle dependency="true">mvn:org.openhab.osgiify/com.google.re2j.re2j/1.2</bundle>
|
||||||
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.sdk.nativeimage/24.2.0</bundle>
|
<bundle dependency="true">mvn:ch.obermuhlner/big-math/2.3.2</bundle>
|
||||||
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.sdk.word/24.2.0</bundle>
|
<bundle dependency="true">mvn:com.fasterxml.jackson.datatype/jackson-datatype-jdk8/${jackson.version}</bundle>
|
||||||
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.shadowed.icu4j/24.2.0</bundle>
|
<bundle dependency="true">mvn:org.openhab.osgiify/com.hubspot.immutables.immutables-exceptions/1.9</bundle>
|
||||||
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.shadowed.xz/24.2.0</bundle>
|
|
||||||
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-compiler/24.2.0</bundle>
|
|
||||||
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-runtime/24.2.0</bundle>
|
|
||||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt/${project.version}</bundle>
|
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt/${project.version}</bundle>
|
||||||
<bundle start-level="81">mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt.awtrixlight/${project.version}</bundle>
|
<bundle start-level="81">mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt.awtrixlight/${project.version}</bundle>
|
||||||
<bundle start-level="81">mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt.espmilighthub/${project.version}</bundle>
|
<bundle start-level="81">mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt.espmilighthub/${project.version}</bundle>
|
||||||
|
|
|
@ -110,35 +110,29 @@ Import-Package: \
|
||||||
org.openhab.binding.mqtt.homeassistant.tests;version='[5.0.0,5.0.1)',\
|
org.openhab.binding.mqtt.homeassistant.tests;version='[5.0.0,5.0.1)',\
|
||||||
org.openhab.core;version='[5.0.0,5.0.1)',\
|
org.openhab.core;version='[5.0.0,5.0.1)',\
|
||||||
org.openhab.core.addon;version='[5.0.0,5.0.1)',\
|
org.openhab.core.addon;version='[5.0.0,5.0.1)',\
|
||||||
org.openhab.core.automation;version='[5.0.0,5.0.1)',\
|
|
||||||
org.openhab.core.automation.module.script;version='[5.0.0,5.0.1)',\
|
|
||||||
org.openhab.core.config.core;version='[5.0.0,5.0.1)',\
|
org.openhab.core.config.core;version='[5.0.0,5.0.1)',\
|
||||||
org.openhab.core.config.discovery;version='[5.0.0,5.0.1)',\
|
org.openhab.core.config.discovery;version='[5.0.0,5.0.1)',\
|
||||||
org.openhab.core.ephemeris;version='[5.0.0,5.0.1)',\
|
|
||||||
org.openhab.core.io.console;version='[5.0.0,5.0.1)',\
|
org.openhab.core.io.console;version='[5.0.0,5.0.1)',\
|
||||||
org.openhab.core.io.transport.mqtt;version='[5.0.0,5.0.1)',\
|
org.openhab.core.io.transport.mqtt;version='[5.0.0,5.0.1)',\
|
||||||
org.openhab.core.semantics;version='[5.0.0,5.0.1)',\
|
org.openhab.core.semantics;version='[5.0.0,5.0.1)',\
|
||||||
org.openhab.core.test;version='[5.0.0,5.0.1)',\
|
org.openhab.core.test;version='[5.0.0,5.0.1)',\
|
||||||
org.openhab.core.thing;version='[5.0.0,5.0.1)',\
|
org.openhab.core.thing;version='[5.0.0,5.0.1)',\
|
||||||
org.openhab.core.transform;version='[5.0.0,5.0.1)',\
|
org.openhab.core.transform;version='[5.0.0,5.0.1)',\
|
||||||
|
ch.obermuhlner.math.big;version='[2.3.2,2.3.3)',\
|
||||||
com.fasterxml.jackson.core.jackson-annotations;version='[2.18.2,2.18.3)',\
|
com.fasterxml.jackson.core.jackson-annotations;version='[2.18.2,2.18.3)',\
|
||||||
com.fasterxml.jackson.core.jackson-core;version='[2.18.2,2.18.3)',\
|
com.fasterxml.jackson.core.jackson-core;version='[2.18.2,2.18.3)',\
|
||||||
com.fasterxml.jackson.core.jackson-databind;version='[2.18.2,2.18.3)',\
|
com.fasterxml.jackson.core.jackson-databind;version='[2.18.2,2.18.3)',\
|
||||||
|
com.google.guava.failureaccess;version='[1.0.2,1.0.3)',\
|
||||||
|
com.google.re2j.re2j;version='[1.2.0,1.2.1)',\
|
||||||
|
com.hubspot.jinjava.jinjava;version='[2.7.4,2.7.5)',\
|
||||||
|
javassist;version='[3.30.2,3.30.3)',\
|
||||||
|
org.apache.commons.commons-net;version='[3.11.1,3.11.2)',\
|
||||||
|
org.apache.commons.lang3;version='[3.17.0,3.17.1)',\
|
||||||
org.yaml.snakeyaml;version='[2.3.0,2.3.1)',\
|
org.yaml.snakeyaml;version='[2.3.0,2.3.1)',\
|
||||||
|
com.fasterxml.jackson.datatype.jackson-datatype-jdk8;version='[2.18.2,2.18.3)',\
|
||||||
|
com.google.guava;version='[33.3.1,33.3.2)',\
|
||||||
|
com.hubspot.immutables.immutables-exceptions;version='[1.9.0,1.9.1)',\
|
||||||
biz.aQute.tester.junit-platform;version='[7.1.0,7.1.1)',\
|
biz.aQute.tester.junit-platform;version='[7.1.0,7.1.1)',\
|
||||||
org.osgi.service.cm;version='[1.6.0,1.6.1)',\
|
|
||||||
com.fasterxml.jackson.dataformat.jackson-dataformat-yaml;version='[2.18.2,2.18.3)',\
|
com.fasterxml.jackson.dataformat.jackson-dataformat-yaml;version='[2.18.2,2.18.3)',\
|
||||||
com.fasterxml.jackson.dataformat.jackson-dataformat-xml;version='[2.18.2,2.18.3)',\
|
|
||||||
jakarta.annotation-api;version='[2.1.1,2.1.2)',\
|
jakarta.annotation-api;version='[2.1.1,2.1.2)',\
|
||||||
jakarta.inject.jakarta.inject-api;version='[1.0.5,1.0.6)',\
|
jakarta.inject.jakarta.inject-api;version='[1.0.5,1.0.6)'
|
||||||
de.focus_shift.jollyday-core;version='[1.5.0,1.5.1)',\
|
|
||||||
de.focus_shift.jollyday-jackson;version='[1.5.0,1.5.1)',\
|
|
||||||
org.threeten.extra;version='[1.8.0,1.8.1)',\
|
|
||||||
org.graalvm.sdk.collections;version='[24.2.0,24.2.1)',\
|
|
||||||
org.graalvm.sdk.jniutils;version='[24.2.0,24.2.1)',\
|
|
||||||
org.graalvm.sdk.nativeimage;version='[24.2.0,24.2.1)',\
|
|
||||||
org.graalvm.sdk.word;version='[24.2.0,24.2.1)',\
|
|
||||||
org.graalvm.shadowed.icu4j;version='[24.2.0,24.2.1)',\
|
|
||||||
org.graalvm.shadowed.xz;version='[24.2.0,24.2.1)',\
|
|
||||||
org.graalvm.truffle.truffle-compiler;version='[24.2.0,24.2.1)',\
|
|
||||||
org.graalvm.truffle.truffle-runtime;version='[24.2.0,24.2.1)'
|
|
||||||
|
|
|
@ -16,59 +16,9 @@
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<mqttbroker.port>1883</mqttbroker.port>
|
<mqttbroker.port>1883</mqttbroker.port>
|
||||||
<graalpy.version>24.2.0</graalpy.version>
|
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
|
||||||
<groupId>org.openhab.osgiify</groupId>
|
|
||||||
<artifactId>org.graalvm.sdk.collections</artifactId>
|
|
||||||
<version>${graalpy.version}</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.openhab.osgiify</groupId>
|
|
||||||
<artifactId>org.graalvm.sdk.jniutils</artifactId>
|
|
||||||
<version>${graalpy.version}</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.openhab.osgiify</groupId>
|
|
||||||
<artifactId>org.graalvm.sdk.nativeimage</artifactId>
|
|
||||||
<version>${graalpy.version}</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.openhab.osgiify</groupId>
|
|
||||||
<artifactId>org.graalvm.sdk.word</artifactId>
|
|
||||||
<version>${graalpy.version}</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.openhab.osgiify</groupId>
|
|
||||||
<artifactId>org.graalvm.shadowed.icu4j</artifactId>
|
|
||||||
<version>${graalpy.version}</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.openhab.osgiify</groupId>
|
|
||||||
<artifactId>org.graalvm.shadowed.xz</artifactId>
|
|
||||||
<version>${graalpy.version}</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.openhab.osgiify</groupId>
|
|
||||||
<artifactId>org.graalvm.truffle.truffle-compiler</artifactId>
|
|
||||||
<version>${graalpy.version}</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.openhab.osgiify</groupId>
|
|
||||||
<artifactId>org.graalvm.truffle.truffle-runtime</artifactId>
|
|
||||||
<version>${graalpy.version}</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openhab.addons.bundles</groupId>
|
<groupId>org.openhab.addons.bundles</groupId>
|
||||||
<artifactId>org.openhab.binding.mqtt</artifactId>
|
<artifactId>org.openhab.binding.mqtt</artifactId>
|
||||||
|
|
|
@ -41,7 +41,6 @@ import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents.Compon
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.HaID;
|
import org.openhab.binding.mqtt.homeassistant.internal.HaID;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
|
import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantChannelLinkageChecker;
|
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantChannelLinkageChecker;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantPythonBridge;
|
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory;
|
import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory;
|
||||||
import org.openhab.core.i18n.UnitProvider;
|
import org.openhab.core.i18n.UnitProvider;
|
||||||
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
|
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
|
||||||
|
@ -49,6 +48,7 @@ import org.openhab.core.test.java.JavaOSGiTest;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.hubspot.jinjava.Jinjava;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests the {@link DiscoverComponents} class.
|
* Tests the {@link DiscoverComponents} class.
|
||||||
|
@ -66,8 +66,6 @@ public class DiscoverComponentsTest extends JavaOSGiTest {
|
||||||
private @Mock @NonNullByDefault({}) HomeAssistantChannelLinkageChecker linkageChecker;
|
private @Mock @NonNullByDefault({}) HomeAssistantChannelLinkageChecker linkageChecker;
|
||||||
private @Mock @NonNullByDefault({}) AvailabilityTracker availabilityTracker;
|
private @Mock @NonNullByDefault({}) AvailabilityTracker availabilityTracker;
|
||||||
|
|
||||||
private static final HomeAssistantPythonBridge python = new HomeAssistantPythonBridge();
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void beforeEach() {
|
public void beforeEach() {
|
||||||
CompletableFuture<@Nullable Void> voidFutureComplete = new CompletableFuture<>();
|
CompletableFuture<@Nullable Void> voidFutureComplete = new CompletableFuture<>();
|
||||||
|
@ -85,11 +83,12 @@ public class DiscoverComponentsTest extends JavaOSGiTest {
|
||||||
ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1);
|
ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1);
|
||||||
|
|
||||||
Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
|
Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
|
||||||
|
Jinjava jinjava = new Jinjava();
|
||||||
UnitProvider unitProvider = mock(UnitProvider.class);
|
UnitProvider unitProvider = mock(UnitProvider.class);
|
||||||
|
|
||||||
DiscoverComponents discover = spy(
|
DiscoverComponents discover = spy(
|
||||||
new DiscoverComponents(ThingChannelConstants.TEST_HOME_ASSISTANT_THING, scheduler,
|
new DiscoverComponents(ThingChannelConstants.TEST_HOME_ASSISTANT_THING, scheduler,
|
||||||
channelStateUpdateListener, linkageChecker, availabilityTracker, gson, python, unitProvider));
|
channelStateUpdateListener, linkageChecker, availabilityTracker, gson, jinjava, unitProvider));
|
||||||
|
|
||||||
HandlerConfiguration config = new HandlerConfiguration("homeassistant", List.of("switch/object"));
|
HandlerConfiguration config = new HandlerConfiguration("homeassistant", List.of("switch/object"));
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,6 @@ import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents.ComponentDiscovered;
|
import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents.ComponentDiscovered;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.HaID;
|
import org.openhab.binding.mqtt.homeassistant.internal.HaID;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantChannelLinkageChecker;
|
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantChannelLinkageChecker;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantPythonBridge;
|
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent;
|
import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.component.Switch;
|
import org.openhab.binding.mqtt.homeassistant.internal.component.Switch;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory;
|
import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory;
|
||||||
|
@ -60,6 +59,7 @@ import org.openhab.core.types.UnDefType;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.hubspot.jinjava.Jinjava;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A full implementation test, that starts the embedded MQTT broker and publishes a homeassistant MQTT discovery device
|
* A full implementation test, that starts the embedded MQTT broker and publishes a homeassistant MQTT discovery device
|
||||||
|
@ -80,8 +80,6 @@ public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest {
|
||||||
private @Mock @NonNullByDefault({}) HomeAssistantChannelLinkageChecker linkageChecker;
|
private @Mock @NonNullByDefault({}) HomeAssistantChannelLinkageChecker linkageChecker;
|
||||||
private @Mock @NonNullByDefault({}) AvailabilityTracker availabilityTracker;
|
private @Mock @NonNullByDefault({}) AvailabilityTracker availabilityTracker;
|
||||||
|
|
||||||
private static final HomeAssistantPythonBridge python = new HomeAssistantPythonBridge();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an observer that fails the test as soon as the broker client connection changes its connection state
|
* Create an observer that fails the test as soon as the broker client connection changes its connection state
|
||||||
* to something else then CONNECTED.
|
* to something else then CONNECTED.
|
||||||
|
@ -170,11 +168,12 @@ public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest {
|
||||||
|
|
||||||
final Map<String, AbstractComponent<?>> haComponents = new HashMap<>();
|
final Map<String, AbstractComponent<?>> haComponents = new HashMap<>();
|
||||||
Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
|
Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
|
||||||
|
Jinjava jinjava = new Jinjava();
|
||||||
|
|
||||||
ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(4);
|
ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(4);
|
||||||
DiscoverComponents discover = spy(
|
DiscoverComponents discover = spy(
|
||||||
new DiscoverComponents(ThingChannelConstants.TEST_HOME_ASSISTANT_THING, scheduler,
|
new DiscoverComponents(ThingChannelConstants.TEST_HOME_ASSISTANT_THING, scheduler,
|
||||||
channelStateUpdateListener, linkageChecker, availabilityTracker, gson, python, unitProvider));
|
channelStateUpdateListener, linkageChecker, availabilityTracker, gson, jinjava, unitProvider));
|
||||||
|
|
||||||
when(linkageChecker.isChannelLinked(any())).thenReturn(true);
|
when(linkageChecker.isChannelLinked(any())).thenReturn(true);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue