[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
|
||||
|
||||
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>
|
||||
|
||||
<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>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
|
@ -33,182 +27,42 @@
|
|||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>33.3.1-jre</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Graal Polyglot Framework -->
|
||||
<dependency>
|
||||
<groupId>org.graalvm.polyglot</groupId>
|
||||
<artifactId>polyglot</artifactId>
|
||||
<version>${graalpy.version}</version>
|
||||
</dependency>
|
||||
<!-- 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>
|
||||
<groupId>org.openhab.osgiify</groupId>
|
||||
<artifactId>com.hubspot.jinjava.jinjava</artifactId>
|
||||
<version>2.7.4</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.graalvm.python</groupId>
|
||||
<artifactId>python-embedding</artifactId>
|
||||
<version>${graalpy.version}</version>
|
||||
<groupId>org.openhab.osgiify</groupId>
|
||||
<artifactId>com.google.re2j.re2j</artifactId>
|
||||
<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>
|
||||
</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>
|
||||
|
|
|
@ -7,14 +7,11 @@
|
|||
<feature>openhab-runtime-base</feature>
|
||||
<feature>openhab-transport-mqtt</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/org.graalvm.sdk.jniutils/24.2.0</bundle>
|
||||
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.sdk.nativeimage/24.2.0</bundle>
|
||||
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.sdk.word/24.2.0</bundle>
|
||||
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.shadowed.icu4j/24.2.0</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 dependency="true">mvn:org.openhab.osgiify/com.hubspot.jinjava.jinjava/2.7.4</bundle>
|
||||
<bundle dependency="true">mvn:org.openhab.osgiify/com.google.re2j.re2j/1.2</bundle>
|
||||
<bundle dependency="true">mvn:ch.obermuhlner/big-math/2.3.2</bundle>
|
||||
<bundle dependency="true">mvn:com.fasterxml.jackson.datatype/jackson-datatype-jdk8/${jackson.version}</bundle>
|
||||
<bundle dependency="true">mvn:org.openhab.osgiify/com.hubspot.immutables.immutables-exceptions/1.9</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="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.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
|
||||
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantPythonBridge;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantJinjaFunctionLibrary;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantStateDescriptionProvider;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler;
|
||||
import org.openhab.core.i18n.UnitProvider;
|
||||
|
@ -34,6 +34,8 @@ import org.osgi.service.component.annotations.Activate;
|
|||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
import com.hubspot.jinjava.Jinjava;
|
||||
|
||||
/**
|
||||
* The {@link MqttThingHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
|
@ -46,8 +48,8 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
|
|||
private final MqttChannelTypeProvider typeProvider;
|
||||
private final MqttChannelStateDescriptionProvider stateDescriptionProvider;
|
||||
private final ChannelTypeRegistry channelTypeRegistry;
|
||||
private final Jinjava jinjava = new Jinjava();
|
||||
private final UnitProvider unitProvider;
|
||||
private final HomeAssistantPythonBridge python;
|
||||
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
|
||||
.of(MqttBindingConstants.HOMEASSISTANT_MQTT_THING).collect(Collectors.toSet());
|
||||
|
@ -60,7 +62,8 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
|
|||
this.stateDescriptionProvider = stateDescriptionProvider;
|
||||
this.channelTypeRegistry = channelTypeRegistry;
|
||||
this.unitProvider = unitProvider;
|
||||
this.python = new HomeAssistantPythonBridge();
|
||||
|
||||
HomeAssistantJinjaFunctionLibrary.register(jinjava.getGlobalContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -79,12 +82,12 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
|
|||
|
||||
if (supportsThingType(thingTypeUID)) {
|
||||
return new HomeAssistantThingHandler(thing, this, typeProvider, stateDescriptionProvider,
|
||||
channelTypeRegistry, python, unitProvider, 10000, 2000);
|
||||
channelTypeRegistry, jinjava, unitProvider, 10000, 2000);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public HomeAssistantPythonBridge getPython() {
|
||||
return python;
|
||||
public Jinjava getJinjava() {
|
||||
return jinjava;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -136,7 +136,6 @@ public class ComponentChannel {
|
|||
|
||||
private @Nullable String stateTopic;
|
||||
private @Nullable String commandTopic;
|
||||
private boolean parseCommandValueAsInteger;
|
||||
private boolean retain;
|
||||
private boolean trigger;
|
||||
private boolean isAdvanced;
|
||||
|
@ -207,11 +206,6 @@ public class ComponentChannel {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder parseCommandValueAsInteger(boolean parseCommandValueAsInteger) {
|
||||
this.parseCommandValueAsInteger = parseCommandValueAsInteger;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder trigger(boolean trigger) {
|
||||
this.trigger = trigger;
|
||||
return this;
|
||||
|
@ -271,13 +265,13 @@ public class ComponentChannel {
|
|||
|
||||
String localTemplateIn = templateIn;
|
||||
if (localTemplateIn != null) {
|
||||
incomingTransformation = new HomeAssistantChannelTransformation(component.getPython(), component,
|
||||
localTemplateIn, false);
|
||||
incomingTransformation = new HomeAssistantChannelTransformation(component.getJinjava(), component,
|
||||
localTemplateIn);
|
||||
}
|
||||
String localTemplateOut = templateOut;
|
||||
if (localTemplateOut != null) {
|
||||
outgoingTransformation = new HomeAssistantChannelTransformation(component.getPython(), component,
|
||||
localTemplateOut, true, parseCommandValueAsInteger);
|
||||
outgoingTransformation = new HomeAssistantChannelTransformation(component.getJinjava(), component,
|
||||
localTemplateOut);
|
||||
}
|
||||
|
||||
channelState = new HomeAssistantChannelState(channelConfigBuilder.build(), channelUID, valueState,
|
||||
|
|
|
@ -38,6 +38,7 @@ import org.slf4j.Logger;
|
|||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.hubspot.jinjava.Jinjava;
|
||||
|
||||
/**
|
||||
* Responsible for subscribing to the HomeAssistant MQTT components wildcard topic, either
|
||||
|
@ -56,7 +57,7 @@ public class DiscoverComponents implements MqttMessageSubscriber {
|
|||
|
||||
protected final CompletableFuture<@Nullable Void> discoverFinishedFuture = new CompletableFuture<>();
|
||||
private final Gson gson;
|
||||
private final HomeAssistantPythonBridge python;
|
||||
private final Jinjava jinjava;
|
||||
private final UnitProvider unitProvider;
|
||||
|
||||
private @Nullable ScheduledFuture<?> stopDiscoveryFuture;
|
||||
|
@ -83,13 +84,13 @@ public class DiscoverComponents implements MqttMessageSubscriber {
|
|||
*/
|
||||
public DiscoverComponents(ThingUID thingUID, ScheduledExecutorService scheduler,
|
||||
ChannelStateUpdateListener channelStateUpdateListener, HomeAssistantChannelLinkageChecker linkageChecker,
|
||||
AvailabilityTracker tracker, Gson gson, HomeAssistantPythonBridge python, UnitProvider unitProvider) {
|
||||
AvailabilityTracker tracker, Gson gson, Jinjava jinjava, UnitProvider unitProvider) {
|
||||
this.thingUID = thingUID;
|
||||
this.scheduler = scheduler;
|
||||
this.updateListener = channelStateUpdateListener;
|
||||
this.linkageChecker = linkageChecker;
|
||||
this.gson = gson;
|
||||
this.python = python;
|
||||
this.jinjava = jinjava;
|
||||
this.unitProvider = unitProvider;
|
||||
this.tracker = tracker;
|
||||
}
|
||||
|
@ -107,7 +108,7 @@ public class DiscoverComponents implements MqttMessageSubscriber {
|
|||
if (config.length() > 0) {
|
||||
try {
|
||||
component = ComponentFactory.createComponent(thingUID, haID, config, updateListener, linkageChecker,
|
||||
tracker, scheduler, gson, python, unitProvider);
|
||||
tracker, scheduler, gson, jinjava, unitProvider);
|
||||
component.setConfigSeen();
|
||||
|
||||
logger.trace("Found HomeAssistant component {}", haID);
|
||||
|
|
|
@ -12,109 +12,137 @@
|
|||
*/
|
||||
package org.openhab.binding.mqtt.homeassistant.internal;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.graalvm.polyglot.PolyglotException;
|
||||
import org.graalvm.polyglot.Value;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent;
|
||||
import org.openhab.core.thing.binding.generic.ChannelTransformation;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.hubspot.jinjava.Jinjava;
|
||||
import com.hubspot.jinjava.interpret.FatalTemplateErrorsException;
|
||||
import com.hubspot.jinjava.interpret.InvalidInputException;
|
||||
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
|
||||
|
||||
/**
|
||||
* Provides a channel transformation for a Home Assistant channel with a
|
||||
* Jinja2 template, providing the additional context and extensions required by Home Assistant
|
||||
* Based in part on the JinjaTransformationService
|
||||
*
|
||||
* @author Cody Cutrer - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HomeAssistantChannelTransformation extends ChannelTransformation {
|
||||
// These map to PayloadSentinen.NONE and PayloadSentinel.DEFAULT in mqtt/models.py
|
||||
// NONE is used to indicate that errors should be ignored, and if any happen the original
|
||||
// payload should be returned directly
|
||||
public static final String PAYLOAD_SENTINEL_NONE = "none";
|
||||
public static final String PAYLOAD_SENTINEL_DEFAULT = "default";
|
||||
public static class UndefinedException extends InvalidInputException {
|
||||
public UndefinedException(JinjavaInterpreter interpreter) {
|
||||
super(interpreter, "is_defined", "Value is undefined");
|
||||
}
|
||||
}
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(HomeAssistantChannelTransformation.class);
|
||||
|
||||
private final HomeAssistantPythonBridge python;
|
||||
private final AbstractComponent component;
|
||||
private final Value template;
|
||||
private final boolean command;
|
||||
private final String defaultValue;
|
||||
private final boolean parseValueAsInteger;
|
||||
private final Jinjava jinjava;
|
||||
private final AbstractComponent<?> component;
|
||||
private final String template;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
public HomeAssistantChannelTransformation(HomeAssistantPythonBridge python, AbstractComponent component,
|
||||
String template, boolean command) {
|
||||
this(python, component, template, command, PAYLOAD_SENTINEL_NONE, false);
|
||||
}
|
||||
|
||||
public HomeAssistantChannelTransformation(HomeAssistantPythonBridge python, AbstractComponent component,
|
||||
String template, boolean command, boolean parseValueAsInteger) {
|
||||
this(python, component, template, command, PAYLOAD_SENTINEL_NONE, parseValueAsInteger);
|
||||
}
|
||||
|
||||
public HomeAssistantChannelTransformation(HomeAssistantPythonBridge python, AbstractComponent component,
|
||||
String template, String defaultValue) {
|
||||
this(python, component, template, false, defaultValue, false);
|
||||
}
|
||||
|
||||
private HomeAssistantChannelTransformation(HomeAssistantPythonBridge python, AbstractComponent component,
|
||||
String template, boolean command, String defaultValue, boolean parseValueAsInteger) {
|
||||
public HomeAssistantChannelTransformation(Jinjava jinjava, AbstractComponent<?> component, String template) {
|
||||
super((String) null);
|
||||
this.python = python;
|
||||
this.jinjava = jinjava;
|
||||
this.component = component;
|
||||
this.command = command;
|
||||
this.template = command ? python.newCommandTemplate(template) : python.newValueTemplate(template);
|
||||
this.defaultValue = defaultValue;
|
||||
this.parseValueAsInteger = parseValueAsInteger;
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return false;
|
||||
return template.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> apply(String value) {
|
||||
Object objValue = value;
|
||||
if (parseValueAsInteger) {
|
||||
try {
|
||||
objValue = (int) Float.parseFloat(value);
|
||||
} catch (NumberFormatException e) {
|
||||
logger.warn("Failed to parse value {} as integer: {}", value, e.getMessage());
|
||||
return apply(template, value);
|
||||
}
|
||||
|
||||
public Optional<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 {
|
||||
JsonNode tree = objectMapper.readTree(value);
|
||||
bindings.put("value_json", toObject(tree));
|
||||
} catch (IOException e) {
|
||||
// ok, then value_json is null...
|
||||
}
|
||||
|
||||
return apply(template, bindings);
|
||||
}
|
||||
|
||||
public Optional<String> apply(String template, Map<String, @Nullable Object> bindings) {
|
||||
String transformationResult;
|
||||
|
||||
try {
|
||||
transformationResult = jinjava.render(template, bindings);
|
||||
} catch (FatalTemplateErrorsException e) {
|
||||
var error = e.getErrors().iterator();
|
||||
Exception exception = null;
|
||||
if (error.hasNext()) {
|
||||
exception = error.next().getException();
|
||||
}
|
||||
if (exception instanceof UndefinedException) {
|
||||
// They used the is_defined filter; it's expected to return null, with no warning
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
Object result = transform(objValue);
|
||||
if (result == null) {
|
||||
logger.warn("Applying template {} for component {} failed: {} ({})", template,
|
||||
component.getHaID().toShortTopic(), e.getMessage(), e.getClass());
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(result.toString());
|
||||
|
||||
logger.debug("transformation resulted in '{}'", transformationResult);
|
||||
|
||||
return Optional.of(transformationResult);
|
||||
}
|
||||
|
||||
public @Nullable String transform(Object value) {
|
||||
try {
|
||||
return command ? python.renderCommandTemplate(template, value)
|
||||
: python.renderValueTemplate(template, value, defaultValue);
|
||||
} catch (PolyglotException e) {
|
||||
logger.warn("Applying template for component {} failed: {}", component.getHaID().toShortTopic(),
|
||||
e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable String transform(Object value, Map<String, @Nullable Object> variables) {
|
||||
try {
|
||||
return command ? python.renderCommandTemplate(template, value, variables)
|
||||
: python.renderValueTemplate(template, value, defaultValue, variables);
|
||||
} catch (PolyglotException e) {
|
||||
logger.warn("Applying template for component {} failed: {}", component.getHaID().toShortTopic(),
|
||||
e.getMessage(), e);
|
||||
return null;
|
||||
private static @Nullable Object toObject(JsonNode node) {
|
||||
switch (node.getNodeType()) {
|
||||
case ARRAY: {
|
||||
List<@Nullable Object> result = new ArrayList<>();
|
||||
for (JsonNode el : node) {
|
||||
result.add(toObject(el));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
case NUMBER:
|
||||
return node.decimalValue();
|
||||
case OBJECT: {
|
||||
Map<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.HaID;
|
||||
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.config.dto.AbstractChannelConfiguration;
|
||||
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.annotations.SerializedName;
|
||||
import com.hubspot.jinjava.Jinjava;
|
||||
|
||||
/**
|
||||
* A HomeAssistant component is comparable to a channel group.
|
||||
|
@ -155,8 +155,7 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
|
|||
String availabilityTemplate = availability.getValueTemplate();
|
||||
ChannelTransformation transformation = null;
|
||||
if (availabilityTemplate != null) {
|
||||
transformation = new HomeAssistantChannelTransformation(getPython(), this, availabilityTemplate,
|
||||
false);
|
||||
transformation = new HomeAssistantChannelTransformation(getJinjava(), this, availabilityTemplate);
|
||||
}
|
||||
componentConfiguration.getTracker().addAvailabilityTopic(availability.getTopic(),
|
||||
availability.getPayloadAvailable(), availability.getPayloadNotAvailable(), transformation);
|
||||
|
@ -167,8 +166,7 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
|
|||
String availabilityTemplate = this.channelConfiguration.getAvailabilityTemplate();
|
||||
ChannelTransformation transformation = null;
|
||||
if (availabilityTemplate != null) {
|
||||
transformation = new HomeAssistantChannelTransformation(getPython(), this, availabilityTemplate,
|
||||
false);
|
||||
transformation = new HomeAssistantChannelTransformation(getJinjava(), this, availabilityTemplate);
|
||||
}
|
||||
componentConfiguration.getTracker().addAvailabilityTopic(availabilityTopic,
|
||||
this.channelConfiguration.getPayloadAvailable(),
|
||||
|
@ -408,8 +406,8 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
|
|||
return componentConfiguration.getGson();
|
||||
}
|
||||
|
||||
public HomeAssistantPythonBridge getPython() {
|
||||
return componentConfiguration.getPython();
|
||||
public Jinjava getJinjava() {
|
||||
return componentConfiguration.getJinjava();
|
||||
}
|
||||
|
||||
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.homeassistant.internal.HaID;
|
||||
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.exception.ConfigurationException;
|
||||
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 com.google.gson.Gson;
|
||||
import com.hubspot.jinjava.Jinjava;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
ChannelStateUpdateListener updateListener, HomeAssistantChannelLinkageChecker linkageChecker,
|
||||
AvailabilityTracker tracker, ScheduledExecutorService scheduler, Gson gson,
|
||||
HomeAssistantPythonBridge python, UnitProvider unitProvider) throws ConfigurationException {
|
||||
AvailabilityTracker tracker, ScheduledExecutorService scheduler, Gson gson, Jinjava jinjava,
|
||||
UnitProvider unitProvider) throws ConfigurationException {
|
||||
ComponentConfiguration componentConfiguration = new ComponentConfiguration(thingUID, haID,
|
||||
channelConfigurationJSON, gson, python, updateListener, linkageChecker, tracker, scheduler,
|
||||
channelConfigurationJSON, gson, jinjava, updateListener, linkageChecker, tracker, scheduler,
|
||||
unitProvider);
|
||||
switch (haID.component) {
|
||||
case "alarm_control_panel":
|
||||
|
@ -116,7 +116,7 @@ public class ComponentFactory {
|
|||
private final HomeAssistantChannelLinkageChecker linkageChecker;
|
||||
private final AvailabilityTracker tracker;
|
||||
private final Gson gson;
|
||||
private final HomeAssistantPythonBridge python;
|
||||
private final Jinjava jinjava;
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private final UnitProvider unitProvider;
|
||||
|
||||
|
@ -128,15 +128,14 @@ public class ComponentFactory {
|
|||
* @param configJSON The configuration string
|
||||
* @param gson A Gson instance
|
||||
*/
|
||||
protected ComponentConfiguration(ThingUID thingUID, HaID haID, String configJSON, Gson gson,
|
||||
HomeAssistantPythonBridge python, ChannelStateUpdateListener updateListener,
|
||||
HomeAssistantChannelLinkageChecker linkageChecker, AvailabilityTracker tracker,
|
||||
ScheduledExecutorService scheduler, UnitProvider unitProvider) {
|
||||
protected ComponentConfiguration(ThingUID thingUID, HaID haID, String configJSON, Gson gson, Jinjava jinjava,
|
||||
ChannelStateUpdateListener updateListener, HomeAssistantChannelLinkageChecker linkageChecker,
|
||||
AvailabilityTracker tracker, ScheduledExecutorService scheduler, UnitProvider unitProvider) {
|
||||
this.thingUID = thingUID;
|
||||
this.haID = haID;
|
||||
this.configJSON = configJSON;
|
||||
this.gson = gson;
|
||||
this.python = python;
|
||||
this.jinjava = jinjava;
|
||||
this.updateListener = updateListener;
|
||||
this.linkageChecker = linkageChecker;
|
||||
this.tracker = tracker;
|
||||
|
@ -168,8 +167,8 @@ public class ComponentFactory {
|
|||
return gson;
|
||||
}
|
||||
|
||||
public HomeAssistantPythonBridge getPython() {
|
||||
return python;
|
||||
public Jinjava getJinjava() {
|
||||
return jinjava;
|
||||
}
|
||||
|
||||
public UnitProvider getUnitProvider() {
|
||||
|
|
|
@ -57,7 +57,7 @@ public class Event extends AbstractComponent<Event.ChannelConfiguration> impleme
|
|||
public Event(ComponentFactory.ComponentConfiguration componentConfiguration) {
|
||||
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)
|
||||
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate()).trigger(true)
|
||||
|
@ -87,7 +87,7 @@ public class Event extends AbstractComponent<Event.ChannelConfiguration> impleme
|
|||
|
||||
@Override
|
||||
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) {
|
||||
// Warning logged from inside the transformation
|
||||
return;
|
||||
|
|
|
@ -159,8 +159,8 @@ public class Fan extends AbstractComponent<Fan.ChannelConfiguration> implements
|
|||
.stateTopic(channelConfiguration.percentageStateTopic, channelConfiguration.percentageValueTemplate)
|
||||
.commandTopic(channelConfiguration.percentageCommandTopic, channelConfiguration.isRetain(),
|
||||
channelConfiguration.getQos(), channelConfiguration.percentageCommandTemplate)
|
||||
.parseCommandValueAsInteger(true).inferOptimistic(channelConfiguration.optimistic)
|
||||
.commandFilter(this::handlePercentageCommand).build();
|
||||
.inferOptimistic(channelConfiguration.optimistic).commandFilter(this::handlePercentageCommand)
|
||||
.build();
|
||||
} else {
|
||||
primaryChannel = onOffChannel;
|
||||
speedChannel = null;
|
||||
|
|
|
@ -54,9 +54,7 @@ import org.slf4j.LoggerFactory;
|
|||
@NonNullByDefault
|
||||
public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
||||
private final Logger logger = LoggerFactory.getLogger(TemplateSchemaLight.class);
|
||||
private @Nullable HomeAssistantChannelTransformation commandOnTransformation, commandOffTransformation,
|
||||
stateTransformation, brightnessTransformation, redTransformation, greenTransformation, blueTransformation,
|
||||
effectTransformation, colorTempTransformation;
|
||||
private final HomeAssistantChannelTransformation transformation;
|
||||
|
||||
private static class TemplateVariables {
|
||||
public static final String STATE = "state";
|
||||
|
@ -74,36 +72,26 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
|||
|
||||
public TemplateSchemaLight(ComponentFactory.ComponentConfiguration builder) {
|
||||
super(builder);
|
||||
transformation = new HomeAssistantChannelTransformation(getJinjava(), this, "");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void buildChannels() {
|
||||
AutoUpdatePolicy autoUpdatePolicy = optimistic ? AutoUpdatePolicy.RECOMMEND : null;
|
||||
String commandOnTemplate = channelConfiguration.commandOnTemplate,
|
||||
commandOffTemplate = channelConfiguration.commandOffTemplate;
|
||||
if (commandOnTemplate == null || commandOffTemplate == null) {
|
||||
if (channelConfiguration.commandOnTemplate == null || channelConfiguration.commandOffTemplate == null) {
|
||||
throw new UnsupportedComponentException("Template schema light component '" + getHaID()
|
||||
+ "' does not define command_on_template or command_off_template!");
|
||||
}
|
||||
|
||||
onOffValue = new OnOffValue("on", "off");
|
||||
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,
|
||||
blueTemplate = channelConfiguration.blueTemplate,
|
||||
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);
|
||||
if (channelConfiguration.redTemplate != null && channelConfiguration.greenTemplate != null
|
||||
&& channelConfiguration.blueTemplate != null) {
|
||||
colorChannel = buildChannel(COLOR_CHANNEL_ID, ComponentChannelType.COLOR, colorValue, "Color", this)
|
||||
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleCommand(command))
|
||||
.withAutoUpdatePolicy(autoUpdatePolicy).build();
|
||||
} else if (brightnessTemplate != null) {
|
||||
brightnessTransformation = new HomeAssistantChannelTransformation(getPython(), this, brightnessTemplate,
|
||||
false);
|
||||
} else if (channelConfiguration.brightnessTemplate != null) {
|
||||
brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, ComponentChannelType.DIMMER, brightnessValue,
|
||||
"Brightness", this).commandTopic(DUMMY_TOPIC, true, 1)
|
||||
.commandFilter(command -> handleCommand(command)).withAutoUpdatePolicy(autoUpdatePolicy).build();
|
||||
|
@ -113,29 +101,17 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
|||
.withAutoUpdatePolicy(autoUpdatePolicy).build();
|
||||
}
|
||||
|
||||
String colorTempTemplate = channelConfiguration.colorTempTemplate;
|
||||
if (colorTempTemplate != null) {
|
||||
colorTempTransformation = new HomeAssistantChannelTransformation(getPython(), this, colorTempTemplate,
|
||||
false);
|
||||
|
||||
if (channelConfiguration.colorTempTemplate != null) {
|
||||
buildChannel(COLOR_TEMP_CHANNEL_ID, ComponentChannelType.NUMBER, colorTempValue, "Color Temperature", this)
|
||||
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleColorTempCommand(command))
|
||||
.withAutoUpdatePolicy(autoUpdatePolicy).build();
|
||||
}
|
||||
TextValue localEffectValue = effectValue;
|
||||
String effectTemplate = channelConfiguration.effectTemplate;
|
||||
if (effectTemplate != null && localEffectValue != null) {
|
||||
effectTransformation = new HomeAssistantChannelTransformation(getPython(), this, effectTemplate, false);
|
||||
|
||||
if (channelConfiguration.effectTemplate != null && localEffectValue != null) {
|
||||
buildChannel(EFFECT_CHANNEL_ID, ComponentChannelType.STRING, localEffectValue, "Effect", this)
|
||||
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleEffectCommand(command))
|
||||
.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
|
||||
|
@ -143,14 +119,14 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
|||
@Override
|
||||
protected void publishState(HSBType state) {
|
||||
Map<String, @Nullable Object> binding = new HashMap<>();
|
||||
HomeAssistantChannelTransformation transformation;
|
||||
String template;
|
||||
|
||||
logger.trace("Publishing new state {} of light {} to MQTT.", state, getName());
|
||||
if (state.getBrightness().equals(PercentType.ZERO)) {
|
||||
transformation = Objects.requireNonNull(commandOffTransformation);
|
||||
template = Objects.requireNonNull(channelConfiguration.commandOffTemplate);
|
||||
binding.put(TemplateVariables.STATE, "off");
|
||||
} else {
|
||||
transformation = Objects.requireNonNull(commandOnTransformation);
|
||||
template = Objects.requireNonNull(channelConfiguration.commandOnTemplate);
|
||||
binding.put(TemplateVariables.STATE, "on");
|
||||
if (channelConfiguration.brightnessTemplate != null) {
|
||||
binding.put(TemplateVariables.BRIGHTNESS,
|
||||
|
@ -166,7 +142,7 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
|||
}
|
||||
}
|
||||
|
||||
publishState(binding, transformation);
|
||||
publishState(binding, template);
|
||||
}
|
||||
|
||||
private boolean handleColorTempCommand(Command command) {
|
||||
|
@ -185,7 +161,7 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
|||
binding.put(TemplateVariables.STATE, "on");
|
||||
binding.put(TemplateVariables.COLOR_TEMP, mireds.toBigDecimal().intValue());
|
||||
|
||||
publishState(binding, Objects.requireNonNull(commandOnTransformation));
|
||||
publishState(binding, Objects.requireNonNull(channelConfiguration.commandOnTemplate));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -200,15 +176,14 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
|||
binding.put(TemplateVariables.STATE, "on");
|
||||
binding.put(TemplateVariables.EFFECT, command.toString());
|
||||
|
||||
publishState(binding, Objects.requireNonNull(commandOnTransformation));
|
||||
publishState(binding, Objects.requireNonNull(channelConfiguration.commandOnTemplate));
|
||||
return false;
|
||||
}
|
||||
|
||||
private void publishState(Map<String, @Nullable Object> binding,
|
||||
HomeAssistantChannelTransformation transformation) {
|
||||
private void publishState(Map<String, @Nullable Object> binding, String template) {
|
||||
String command;
|
||||
|
||||
command = transform(transformation, binding);
|
||||
command = transform(template, binding);
|
||||
if (command == null) {
|
||||
return;
|
||||
}
|
||||
|
@ -223,9 +198,9 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
|||
|
||||
String value;
|
||||
|
||||
HomeAssistantChannelTransformation stateTransformation = this.stateTransformation;
|
||||
if (stateTransformation != null) {
|
||||
value = transform(stateTransformation, state.toString());
|
||||
String template = channelConfiguration.stateTemplate;
|
||||
if (template != null) {
|
||||
value = transform(template, state.toString());
|
||||
if (value == null || value.isEmpty()) {
|
||||
onOffValue.update(UnDefType.NULL);
|
||||
} else if ("on".equals(value)) {
|
||||
|
@ -242,15 +217,14 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
|||
brightnessValue.update(
|
||||
(PercentType) Objects.requireNonNull(onOffValue.getChannelState().as(PercentType.class)));
|
||||
}
|
||||
if (colorValue.getChannelState() instanceof UnDefType
|
||||
&& onOffValue.getChannelState() instanceof OnOffType onOffValue) {
|
||||
colorValue.update(onOffValue);
|
||||
if (colorValue.getChannelState() instanceof UnDefType) {
|
||||
colorValue.update((OnOffType) onOffValue.getChannelState());
|
||||
}
|
||||
}
|
||||
|
||||
HomeAssistantChannelTransformation brightnessTransformation = this.brightnessTransformation;
|
||||
if (brightnessTransformation != null) {
|
||||
Integer brightness = getColorChannelValue(brightnessTransformation, state.toString());
|
||||
template = channelConfiguration.brightnessTemplate;
|
||||
if (template != null) {
|
||||
Integer brightness = getColorChannelValue(template, state.toString());
|
||||
if (brightness == null) {
|
||||
brightnessValue.update(UnDefType.NULL);
|
||||
colorValue.update(UnDefType.NULL);
|
||||
|
@ -267,13 +241,13 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
|||
}
|
||||
|
||||
@Nullable
|
||||
HomeAssistantChannelTransformation redTransformation, greenTransformation, blueTransformation;
|
||||
if ((redTransformation = this.redTransformation) != null
|
||||
&& (greenTransformation = this.greenTransformation) != null
|
||||
&& (blueTransformation = this.blueTransformation) != null) {
|
||||
Integer red = getColorChannelValue(redTransformation, state.toString());
|
||||
Integer green = getColorChannelValue(greenTransformation, state.toString());
|
||||
Integer blue = getColorChannelValue(blueTransformation, state.toString());
|
||||
String redTemplate, greenTemplate, blueTemplate;
|
||||
if ((redTemplate = channelConfiguration.redTemplate) != null
|
||||
&& (greenTemplate = channelConfiguration.greenTemplate) != null
|
||||
&& (blueTemplate = channelConfiguration.blueTemplate) != null) {
|
||||
Integer red = getColorChannelValue(redTemplate, state.toString());
|
||||
Integer green = getColorChannelValue(greenTemplate, state.toString());
|
||||
Integer blue = getColorChannelValue(blueTemplate, state.toString());
|
||||
if (red == null || green == null || blue == null) {
|
||||
colorValue.update(UnDefType.NULL);
|
||||
} else {
|
||||
|
@ -291,9 +265,9 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
|||
listener.updateChannelState(onOffChannel.getChannel().getUID(), onOffValue.getChannelState());
|
||||
}
|
||||
|
||||
HomeAssistantChannelTransformation effectTransformation = this.effectTransformation;
|
||||
if (effectTransformation != null) {
|
||||
value = transform(effectTransformation, state.toString());
|
||||
template = channelConfiguration.effectTemplate;
|
||||
if (template != null) {
|
||||
value = transform(template, state.toString());
|
||||
if (value == null || value.isEmpty()) {
|
||||
effectValue.update(UnDefType.NULL);
|
||||
} else {
|
||||
|
@ -302,9 +276,9 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
|||
listener.updateChannelState(buildChannelUID(EFFECT_CHANNEL_ID), effectValue.getChannelState());
|
||||
}
|
||||
|
||||
HomeAssistantChannelTransformation colorTempTransformation = this.colorTempTransformation;
|
||||
if (colorTempTransformation != null) {
|
||||
Integer mireds = getColorChannelValue(colorTempTransformation, state.toString());
|
||||
template = channelConfiguration.colorTempTemplate;
|
||||
if (template != null) {
|
||||
Integer mireds = getColorChannelValue(template, state.toString());
|
||||
if (mireds == null) {
|
||||
colorTempValue.update(UnDefType.NULL);
|
||||
} else {
|
||||
|
@ -314,8 +288,8 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
|||
}
|
||||
}
|
||||
|
||||
private @Nullable Integer getColorChannelValue(HomeAssistantChannelTransformation transformation, String value) {
|
||||
Object result = transform(transformation, value);
|
||||
private @Nullable Integer getColorChannelValue(String template, String value) {
|
||||
Object result = transform(template, value);
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
|
@ -327,21 +301,17 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
|
|||
try {
|
||||
return Integer.parseInt(result.toString());
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable String transform(HomeAssistantChannelTransformation transformation,
|
||||
Map<String, @Nullable Object> variables) {
|
||||
Object result = transformation.transform("", variables);
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
return result.toString();
|
||||
private @Nullable String transform(String template, Map<String, @Nullable Object> binding) {
|
||||
return transformation.apply(template, binding).orElse(null);
|
||||
}
|
||||
|
||||
private @Nullable String transform(HomeAssistantChannelTransformation transformation, String value) {
|
||||
return transformation.apply(value).orElse(null);
|
||||
private @Nullable String transform(String template, String value) {
|
||||
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.HandlerConfiguration;
|
||||
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.component.AbstractComponent;
|
||||
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.GsonBuilder;
|
||||
import com.hubspot.jinjava.Jinjava;
|
||||
|
||||
/**
|
||||
* 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 MqttChannelStateDescriptionProvider stateDescriptionProvider;
|
||||
protected final ChannelTypeRegistry channelTypeRegistry;
|
||||
protected final HomeAssistantPythonBridge python;
|
||||
protected final Jinjava jinjava;
|
||||
protected final UnitProvider unitProvider;
|
||||
public final int attributeReceiveTimeout;
|
||||
protected final DelayedBatchProcessing<Object> delayedProcessing;
|
||||
|
@ -124,19 +124,19 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
|||
*/
|
||||
public HomeAssistantThingHandler(Thing thing, BaseThingHandlerFactory thingHandlerFactory,
|
||||
MqttChannelTypeProvider channelTypeProvider, MqttChannelStateDescriptionProvider stateDescriptionProvider,
|
||||
ChannelTypeRegistry channelTypeRegistry, HomeAssistantPythonBridge python, UnitProvider unitProvider,
|
||||
int subscribeTimeout, int attributeReceiveTimeout) {
|
||||
ChannelTypeRegistry channelTypeRegistry, Jinjava jinjava, UnitProvider unitProvider, int subscribeTimeout,
|
||||
int attributeReceiveTimeout) {
|
||||
super(thing, subscribeTimeout);
|
||||
this.gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
|
||||
this.thingHandlerFactory = thingHandlerFactory;
|
||||
this.channelTypeProvider = channelTypeProvider;
|
||||
this.stateDescriptionProvider = stateDescriptionProvider;
|
||||
this.channelTypeRegistry = channelTypeRegistry;
|
||||
this.python = python;
|
||||
this.jinjava = jinjava;
|
||||
this.unitProvider = unitProvider;
|
||||
this.attributeReceiveTimeout = attributeReceiveTimeout;
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -185,7 +185,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
|||
String channelConfigurationJSON = (String) channelConfig.get("config");
|
||||
try {
|
||||
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)) {
|
||||
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.ThingTypeBuilder;
|
||||
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.osgi.framework.BundleContext;
|
||||
import org.osgi.framework.ServiceReference;
|
||||
|
||||
/**
|
||||
* 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 ThingType HA_THING_TYPE = ThingTypeBuilder
|
||||
.instance(MqttBindingConstants.HOMEASSISTANT_MQTT_THING, HA_TYPE_LABEL).build();
|
||||
protected static final HomeAssistantPythonBridge python = new HomeAssistantPythonBridge();
|
||||
|
||||
protected @Mock @NonNullByDefault({}) MqttBrokerConnection bridgeConnection;
|
||||
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 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({}) BundleResolver bundleResolver;
|
||||
private @Mock @NonNullByDefault({}) ServiceReference<TransformationService> serviceRefMock;
|
||||
|
||||
private @NonNullByDefault({}) TransformationHelper transformationHelper;
|
||||
|
||||
@BeforeEach
|
||||
public void beforeEachAbstractHomeAssistantTests() {
|
||||
transformationHelper = new TransformationHelper(bundleContextMock);
|
||||
transformationHelper.setTransformationService(serviceRefMock);
|
||||
|
||||
when(thingTypeRegistry.getThingType(BRIDGE_TYPE_UID))
|
||||
.thenReturn(ThingTypeBuilder.instance(BRIDGE_TYPE_UID, BRIDGE_TYPE_LABEL).build());
|
||||
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.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
|
||||
import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttThingHandlerFactory;
|
||||
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
|
||||
|
@ -35,62 +44,72 @@ import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractCompone
|
|||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
@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
|
||||
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");
|
||||
when(component.getHaID()).thenReturn(haID);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidTemplate() {
|
||||
assertThat(transform("{{}}", ""), is(nullValue()));
|
||||
transformation = new HomeAssistantChannelTransformation(thingHandlerFactory.getJinjava(), component, "");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIif() {
|
||||
assertThat(transform("{{ iif(true) }}", ""), is("True"));
|
||||
assertThat(transform("{{ iif(false) }}", ""), is("False"));
|
||||
assertThat(transform("{{ iif(none) }}", ""), is("False"));
|
||||
assertThat(transform("{{ iif(true, 'Yes') }}", ""), is("Yes"));
|
||||
assertThat(transform("{{ iif(false, 'Yes') }}", ""), is("False"));
|
||||
assertThat(transform("{{ iif(none, 'Yes') }}", ""), is("False"));
|
||||
assertThat(transform("{{ iif(true, 'Yes', 'No') }}", ""), is("Yes"));
|
||||
assertThat(transform("{{ iif(false, 'Yes', 'No') }}", ""), is("No"));
|
||||
assertThat(transform("{{ iif(none, 'Yes', 'No') }}", ""), is("No"));
|
||||
assertThat(transform("{{ iif(true, 'Yes', 'No', none) }}", ""), is("Yes"));
|
||||
assertThat(transform("{{ iif(false, 'Yes', 'No', none) }}", ""), is("No"));
|
||||
assertThat(transform("{{ iif(none, 'Yes', 'No', 'NULL') }}", ""), is("NULL"));
|
||||
assertThat(transform("{{ iif(none, 'Yes', 'No', none) }}", ""), is("None"));
|
||||
assertThat(transform("{{ iif(True) }}", ""), is("true"));
|
||||
assertThat(transform("{{ iif(False) }}", ""), is("false"));
|
||||
assertThat(transform("{{ iif(Null) }}", ""), is("false"));
|
||||
assertThat(transform("{{ iif(True, 'Yes') }}", ""), is("Yes"));
|
||||
assertThat(transform("{{ iif(False, 'Yes') }}", ""), is("false"));
|
||||
assertThat(transform("{{ iif(Null, 'Yes') }}", ""), is("false"));
|
||||
assertThat(transform("{{ iif(True, 'Yes', 'No') }}", ""), is("Yes"));
|
||||
assertThat(transform("{{ iif(False, 'Yes', 'No') }}", ""), is("No"));
|
||||
assertThat(transform("{{ iif(Null, 'Yes', 'No') }}", ""), is("No"));
|
||||
assertThat(transform("{{ iif(True, 'Yes', 'No', null) }}", ""), is("Yes"));
|
||||
assertThat(transform("{{ iif(False, 'Yes', 'No', null) }}", ""), is("No"));
|
||||
assertThat(transform("{{ iif(Null, 'Yes', 'No', 'NULL') }}", ""), is("NULL"));
|
||||
assertThat(transform("{{ iif(Null, 'Yes', 'No', null) }}", ""), is(""));
|
||||
assertThat(transform("{{ iif(True, 'Yes', 'No', null, null) }}", ""), is(nullValue()));
|
||||
|
||||
assertThat(transform("{{ true | iif('Yes') }}", ""), is("Yes"));
|
||||
assertThat(transform("{{ false | iif('Yes') }}", ""), is("False"));
|
||||
assertThat(transform("{{ none | iif('Yes') }}", ""), is("False"));
|
||||
assertThat(transform("{{ true | iif('Yes', 'No') }}", ""), is("Yes"));
|
||||
assertThat(transform("{{ false | iif('Yes', 'No') }}", ""), is("No"));
|
||||
assertThat(transform("{{ none | iif('Yes', 'No') }}", ""), is("No"));
|
||||
assertThat(transform("{{ true | iif('Yes', 'No', none) }}", ""), is("Yes"));
|
||||
assertThat(transform("{{ false | iif('Yes', 'No', none) }}", ""), is("No"));
|
||||
assertThat(transform("{{ none | iif('Yes', 'No', 'NULL') }}", ""), is("NULL"));
|
||||
assertThat(transform("{{ none | iif('Yes', 'No', none) }}", ""), is("None"));
|
||||
assertThat(transform("{{ True | iif('Yes') }}", ""), is("Yes"));
|
||||
assertThat(transform("{{ False | iif('Yes') }}", ""), is("false"));
|
||||
assertThat(transform("{{ Null | iif('Yes') }}", ""), is("false"));
|
||||
assertThat(transform("{{ True | iif('Yes', 'No') }}", ""), is("Yes"));
|
||||
assertThat(transform("{{ False | iif('Yes', 'No') }}", ""), is("No"));
|
||||
assertThat(transform("{{ Null | iif('Yes', 'No') }}", ""), is("No"));
|
||||
assertThat(transform("{{ True | iif('Yes', 'No', null) }}", ""), is("Yes"));
|
||||
assertThat(transform("{{ False | iif('Yes', 'No', null) }}", ""), is("No"));
|
||||
assertThat(transform("{{ Null | iif('Yes', 'No', 'NULL') }}", ""), is("NULL"));
|
||||
assertThat(transform("{{ Null | iif('Yes', 'No', null) }}", ""), is(""));
|
||||
assertThat(transform("{{ True | iif('Yes', 'No', null, null) }}", ""), is(nullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsDefined() {
|
||||
assertThat(transform("{{ value_json.val }}", "{ \"val\": \"abc\" }", "default"), is("abc"));
|
||||
assertThat(transform("{{ value_json.val }}", "{ \"val\": null }", "default"), is("None"));
|
||||
assertThat(transform("{{ value_json.something | is_defined }}", "{ \"val\": null }", "default"), is("default"));
|
||||
assertThat(transform("{{ value_json.val | is_defined }}", "{}"), is(nullValue()));
|
||||
assertThat(transform("{{ 'hi' | is_defined }}", "{}"), is("hi"));
|
||||
}
|
||||
|
||||
@Test
|
||||
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(
|
||||
"{{ '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
|
||||
|
@ -100,24 +119,10 @@ public class HomeAssistantChannelTransformationTests extends AbstractHomeAssista
|
|||
assertThat(transform("{{ ['JFK', 'LHR'] | regex_findall_index('([A-Z]{3})', 1) }}", ""), is("LHR"));
|
||||
assertThat(transform(
|
||||
"{{ '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
|
||||
public void testIntegerDictLookup() {
|
||||
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);
|
||||
protected @Nullable String transform(String template, String value) {
|
||||
return transformation.apply(template, value).orElse(null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.HaID;
|
||||
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.handler.HomeAssistantThingHandler;
|
||||
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.State;
|
||||
|
||||
import com.hubspot.jinjava.Jinjava;
|
||||
|
||||
/**
|
||||
* Abstract class for components tests.
|
||||
*
|
||||
|
@ -87,7 +88,7 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
|
|||
when(callbackMock.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing);
|
||||
|
||||
thingHandler = new LatchThingHandler(haThing, thingHandlerFactory, channelTypeProvider,
|
||||
stateDescriptionProvider, channelTypeRegistry, python, unitProvider, SUBSCRIBE_TIMEOUT,
|
||||
stateDescriptionProvider, channelTypeRegistry, unitProvider, SUBSCRIBE_TIMEOUT,
|
||||
ATTRIBUTE_RECEIVE_TIMEOUT);
|
||||
thingHandler.setConnection(bridgeConnection);
|
||||
thingHandler.setCallback(callbackMock);
|
||||
|
@ -366,10 +367,9 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
|
|||
public LatchThingHandler(Thing thing, BaseThingHandlerFactory thingHandlerFactory,
|
||||
MqttChannelTypeProvider channelTypeProvider,
|
||||
MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry,
|
||||
HomeAssistantPythonBridge python, UnitProvider unitProvider, int subscribeTimeout,
|
||||
int attributeReceiveTimeout) {
|
||||
UnitProvider unitProvider, int subscribeTimeout, int attributeReceiveTimeout) {
|
||||
super(thing, thingHandlerFactory, channelTypeProvider, stateDescriptionProvider, channelTypeRegistry,
|
||||
python, unitProvider, subscribeTimeout, attributeReceiveTimeout);
|
||||
new Jinjava(), unitProvider, subscribeTimeout, attributeReceiveTimeout);
|
||||
}
|
||||
|
||||
@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.types.StateDescription;
|
||||
|
||||
import com.hubspot.jinjava.Jinjava;
|
||||
|
||||
/**
|
||||
* Tests for {@link HomeAssistantThingHandler}
|
||||
*
|
||||
|
@ -89,7 +91,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
|
|||
|
||||
protected void setupThingHandler() {
|
||||
thingHandler = new HomeAssistantThingHandler(haThing, thingHandlerFactory, channelTypeProvider,
|
||||
stateDescriptionProvider, channelTypeRegistry, python, unitProvider, SUBSCRIBE_TIMEOUT,
|
||||
stateDescriptionProvider, channelTypeRegistry, new Jinjava(), unitProvider, SUBSCRIBE_TIMEOUT,
|
||||
ATTRIBUTE_RECEIVE_TIMEOUT);
|
||||
thingHandler.setConnection(bridgeConnection);
|
||||
thingHandler.setCallback(callbackMock);
|
||||
|
@ -359,7 +361,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
|
|||
@Test
|
||||
public void testDuplicateChannelId() {
|
||||
thingHandler = new HomeAssistantThingHandler(haThing, thingHandlerFactory, channelTypeProvider,
|
||||
stateDescriptionProvider, channelTypeRegistry, python, unitProvider, SUBSCRIBE_TIMEOUT,
|
||||
stateDescriptionProvider, channelTypeRegistry, new Jinjava(), unitProvider, SUBSCRIBE_TIMEOUT,
|
||||
ATTRIBUTE_RECEIVE_TIMEOUT);
|
||||
thingHandler.setConnection(bridgeConnection);
|
||||
thingHandler.setCallback(callbackMock);
|
||||
|
@ -416,7 +418,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
|
|||
@Test
|
||||
public void testDuplicateChannelIdComplex() {
|
||||
thingHandler = new HomeAssistantThingHandler(haThing, thingHandlerFactory, channelTypeProvider,
|
||||
stateDescriptionProvider, channelTypeRegistry, python, unitProvider, SUBSCRIBE_TIMEOUT,
|
||||
stateDescriptionProvider, channelTypeRegistry, new Jinjava(), unitProvider, SUBSCRIBE_TIMEOUT,
|
||||
ATTRIBUTE_RECEIVE_TIMEOUT);
|
||||
thingHandler.setConnection(bridgeConnection);
|
||||
thingHandler.setCallback(callbackMock);
|
||||
|
|
|
@ -23,14 +23,11 @@
|
|||
<feature>openhab-runtime-base</feature>
|
||||
<feature>openhab-transport-mqtt</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/org.graalvm.sdk.jniutils/24.2.0</bundle>
|
||||
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.sdk.nativeimage/24.2.0</bundle>
|
||||
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.sdk.word/24.2.0</bundle>
|
||||
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.shadowed.icu4j/24.2.0</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 dependency="true">mvn:org.openhab.osgiify/com.hubspot.jinjava.jinjava/2.7.4</bundle>
|
||||
<bundle dependency="true">mvn:org.openhab.osgiify/com.google.re2j.re2j/1.2</bundle>
|
||||
<bundle dependency="true">mvn:ch.obermuhlner/big-math/2.3.2</bundle>
|
||||
<bundle dependency="true">mvn:com.fasterxml.jackson.datatype/jackson-datatype-jdk8/${jackson.version}</bundle>
|
||||
<bundle dependency="true">mvn:org.openhab.osgiify/com.hubspot.immutables.immutables-exceptions/1.9</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.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.core;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.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.transport.mqtt;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.thing;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-core;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)',\
|
||||
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)',\
|
||||
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-xml;version='[2.18.2,2.18.3)',\
|
||||
jakarta.annotation-api;version='[2.1.1,2.1.2)',\
|
||||
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)'
|
||||
jakarta.inject.jakarta.inject-api;version='[1.0.5,1.0.6)'
|
||||
|
|
|
@ -16,59 +16,9 @@
|
|||
|
||||
<properties>
|
||||
<mqttbroker.port>1883</mqttbroker.port>
|
||||
<graalpy.version>24.2.0</graalpy.version>
|
||||
</properties>
|
||||
|
||||
<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>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<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.HandlerConfiguration;
|
||||
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.core.i18n.UnitProvider;
|
||||
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.GsonBuilder;
|
||||
import com.hubspot.jinjava.Jinjava;
|
||||
|
||||
/**
|
||||
* Tests the {@link DiscoverComponents} class.
|
||||
|
@ -66,8 +66,6 @@ public class DiscoverComponentsTest extends JavaOSGiTest {
|
|||
private @Mock @NonNullByDefault({}) HomeAssistantChannelLinkageChecker linkageChecker;
|
||||
private @Mock @NonNullByDefault({}) AvailabilityTracker availabilityTracker;
|
||||
|
||||
private static final HomeAssistantPythonBridge python = new HomeAssistantPythonBridge();
|
||||
|
||||
@BeforeEach
|
||||
public void beforeEach() {
|
||||
CompletableFuture<@Nullable Void> voidFutureComplete = new CompletableFuture<>();
|
||||
|
@ -85,11 +83,12 @@ public class DiscoverComponentsTest extends JavaOSGiTest {
|
|||
ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1);
|
||||
|
||||
Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
|
||||
Jinjava jinjava = new Jinjava();
|
||||
UnitProvider unitProvider = mock(UnitProvider.class);
|
||||
|
||||
DiscoverComponents discover = spy(
|
||||
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"));
|
||||
|
||||
|
|
|
@ -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.HaID;
|
||||
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.Switch;
|
||||
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.GsonBuilder;
|
||||
import com.hubspot.jinjava.Jinjava;
|
||||
|
||||
/**
|
||||
* 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({}) 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
|
||||
* to something else then CONNECTED.
|
||||
|
@ -170,11 +168,12 @@ public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest {
|
|||
|
||||
final Map<String, AbstractComponent<?>> haComponents = new HashMap<>();
|
||||
Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
|
||||
Jinjava jinjava = new Jinjava();
|
||||
|
||||
ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(4);
|
||||
DiscoverComponents discover = spy(
|
||||
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);
|
||||
|
||||
|
|
Loading…
Reference in New Issue