Skip to content

Commit

Permalink
[mqtt.homeassistant] Use Jinjava directly (openhab#17378)
Browse files Browse the repository at this point in the history
* [mqtt.homeassistant] Use Jinjava directly

Signed-off-by: Cody Cutrer <cody@cutrer.us>
  • Loading branch information
ccutrer authored and joni1993 committed Oct 15, 2024
1 parent 03f0209 commit 3d462c9
Show file tree
Hide file tree
Showing 20 changed files with 242 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,35 @@ public class ChannelState implements MqttMessageSubscriber {
*/
public ChannelState(ChannelConfig config, ChannelUID channelUID, Value cachedValue,
@Nullable ChannelStateUpdateListener channelStateUpdateListener) {
this(config, channelUID, cachedValue, channelStateUpdateListener,
new ChannelTransformation(config.transformationPattern),
new ChannelTransformation(config.transformationPatternOut));
}

/**
* Creates a new channel state.
*
* @param config The channel configuration
* @param channelUID The channelUID is used for the {@link ChannelStateUpdateListener} to notify about value changes
* @param cachedValue MQTT only notifies us once about a value, during the subscribe. The channel state therefore
* needs a cache for the current value.
* @param channelStateUpdateListener A channel state update listener
* @param incomingTransformation A transformation to apply to incoming values
* @param outgoingTransformation A transformation to apply to outgoing values
*/
public ChannelState(ChannelConfig config, ChannelUID channelUID, Value cachedValue,
@Nullable ChannelStateUpdateListener channelStateUpdateListener,
@Nullable ChannelTransformation incomingTransformation,
@Nullable ChannelTransformation outgoingTransformation) {
this.config = config;
this.channelStateUpdateListener = channelStateUpdateListener;
this.channelUID = channelUID;
this.cachedValue = cachedValue;
this.readOnly = config.commandTopic.isBlank();
this.incomingTransformation = new ChannelTransformation(config.transformationPattern);
this.outgoingTransformation = new ChannelTransformation(config.transformationPatternOut);
this.incomingTransformation = incomingTransformation == null ? new ChannelTransformation((String) null)
: incomingTransformation;
this.outgoingTransformation = outgoingTransformation == null ? new ChannelTransformation((String) null)
: outgoingTransformation;
}

public boolean isReadOnly() {
Expand Down
9 changes: 0 additions & 9 deletions bundles/org.openhab.binding.mqtt.homeassistant/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,6 @@ Devices that use [Home Assistant MQTT Discovery](https://www.home-assistant.io/i
Components that share a common `device.identifiers` will automatically be grouped together as a single Thing.
Each component will be represented as a Channel Group, with the attributes of that component being individual channels.

## Requirements

The Home Assistant MQTT binding requires two transformations to be installed:

- JINJA-Transformations
- JSONPath-Transformations

These can be installed under `Settings` &rarr; `Addon` &rarr; `Transformations`

## Discovery

Any device that publishes the component configuration under the `homeassistant` prefix in MQTT will have their components automatically discovered and added to the Inbox.
Expand Down
Empty file.
23 changes: 12 additions & 11 deletions bundles/org.openhab.binding.mqtt.homeassistant/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,22 @@
</dependency>

<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.transform.jinja</artifactId>
<version>${project.version}</version>
<scope>test</scope>
<groupId>org.openhab.osgiify</groupId>
<artifactId>com.hubspot.jinjava.jinjava</artifactId>
<version>2.7.2_0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.hubspot.jinjava</groupId>
<artifactId>jinjava</artifactId>
<version>2.7.2</version>
<scope>test</scope>
<groupId>org.openhab.osgiify</groupId>
<artifactId>com.google.re2j.re2j</artifactId>
<version>1.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.google.re2j</groupId>
<artifactId>re2j</artifactId>
<version>1.2</version>
<groupId>ch.obermuhlner</groupId>
<artifactId>big-math</artifactId>
<version>2.3.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
<feature name="openhab-binding-mqtt-homeassistant" description="MQTT Binding Homeassistant" version="${project.version}">
<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/com.hubspot.jinjava.jinjava/2.7.2_0</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 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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
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.
Expand All @@ -43,6 +45,7 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
private final MqttChannelTypeProvider typeProvider;
private final MqttChannelStateDescriptionProvider stateDescriptionProvider;
private final ChannelTypeRegistry channelTypeRegistry;
private final Jinjava jinjava = new Jinjava();

private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.of(MqttBindingConstants.HOMEASSISTANT_MQTT_THING).collect(Collectors.toSet());
Expand Down Expand Up @@ -72,7 +75,7 @@ private boolean isHomeassistantDynamicType(ThingTypeUID thingTypeUID) {

if (supportsThingType(thingTypeUID)) {
return new HomeAssistantThingHandler(thing, typeProvider, stateDescriptionProvider, channelTypeRegistry,
10000, 2000);
jinjava, 10000, 2000);
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
*/
package org.openhab.binding.mqtt.homeassistant.internal;

import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
Expand All @@ -30,6 +29,7 @@
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.generic.ChannelTransformation;
import org.openhab.core.thing.type.AutoUpdatePolicy;
import org.openhab.core.thing.type.ChannelDefinition;
import org.openhab.core.thing.type.ChannelDefinitionBuilder;
Expand Down Expand Up @@ -223,21 +223,26 @@ public ComponentChannel build(boolean addToComponent) {
ChannelUID channelUID;
ChannelState channelState;
Channel channel;
ChannelTransformation incomingTransformation = null, outgoingTransformation = null;

channelUID = component.buildChannelUID(channelID);
ChannelConfigBuilder channelConfigBuilder = ChannelConfigBuilder.create().withRetain(retain).withQos(qos)
.withStateTopic(stateTopic).withCommandTopic(commandTopic).makeTrigger(trigger)
.withFormatter(format);

if (templateIn != null) {
channelConfigBuilder.withTransformationPattern(List.of(JINJA + ":" + templateIn));
String localTemplateIn = templateIn;
if (localTemplateIn != null) {
incomingTransformation = new HomeAssistantChannelTransformation(component.getJinjava(), component,
localTemplateIn);
}
if (templateOut != null) {
channelConfigBuilder.withTransformationPatternOut(List.of(JINJA + ":" + templateOut));
String localTemplateOut = templateOut;
if (localTemplateOut != null) {
outgoingTransformation = new HomeAssistantChannelTransformation(component.getJinjava(), component,
localTemplateOut);
}

channelState = new HomeAssistantChannelState(channelConfigBuilder.build(), channelUID, valueState,
channelStateUpdateListener, commandFilter);
channelStateUpdateListener, commandFilter, incomingTransformation, outgoingTransformation);

// disabled by default components should always show up as advanced
if (!component.isEnabledByDefault()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
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
Expand All @@ -55,6 +56,7 @@ public class DiscoverComponents implements MqttMessageSubscriber {

protected final CompletableFuture<@Nullable Void> discoverFinishedFuture = new CompletableFuture<>();
private final Gson gson;
private final Jinjava jinjava;

private @Nullable ScheduledFuture<?> stopDiscoveryFuture;
private WeakReference<@Nullable MqttBrokerConnection> connectionRef = new WeakReference<>(null);
Expand All @@ -78,11 +80,12 @@ public static interface ComponentDiscovered {
*/
public DiscoverComponents(ThingUID thingUID, ScheduledExecutorService scheduler,
ChannelStateUpdateListener channelStateUpdateListener, AvailabilityTracker tracker, Gson gson,
boolean newStyleChannels) {
Jinjava jinjava, boolean newStyleChannels) {
this.thingUID = thingUID;
this.scheduler = scheduler;
this.updateListener = channelStateUpdateListener;
this.gson = gson;
this.jinjava = jinjava;
this.tracker = tracker;
this.newStyleChannels = newStyleChannels;
}
Expand All @@ -100,7 +103,7 @@ public void processMessage(String topic, byte[] payload) {
if (config.length() > 0) {
try {
component = ComponentFactory.createComponent(thingUID, haID, config, updateListener, tracker, scheduler,
gson, newStyleChannels);
gson, jinjava, newStyleChannels);
component.setConfigSeen();

logger.trace("Found HomeAssistant component {}", haID);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.values.Value;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.binding.generic.ChannelTransformation;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -48,9 +49,11 @@ public class HomeAssistantChannelState extends ChannelState {
* <code>false</code> ignored. Can be <code>null</code> to publish all commands.
*/
public HomeAssistantChannelState(ChannelConfig config, ChannelUID channelUID, Value cachedValue,
@Nullable ChannelStateUpdateListener channelStateUpdateListener,
@Nullable Predicate<Command> commandFilter) {
super(config, channelUID, cachedValue, channelStateUpdateListener);
@Nullable ChannelStateUpdateListener channelStateUpdateListener, @Nullable Predicate<Command> commandFilter,
@Nullable ChannelTransformation incomingTransformation,
@Nullable ChannelTransformation outgoingTransformation) {
super(config, channelUID, cachedValue, channelStateUpdateListener, incomingTransformation,
outgoingTransformation);
this.commandFilter = commandFilter;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal;

import java.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.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;

/**
* 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 {
private final Logger logger = LoggerFactory.getLogger(HomeAssistantChannelTransformation.class);

private final Jinjava jinjava;
private final AbstractComponent component;
private final String template;
private final ObjectMapper objectMapper = new ObjectMapper();

public HomeAssistantChannelTransformation(Jinjava jinjava, AbstractComponent component, String template) {
super((String) null);
this.jinjava = jinjava;
this.component = component;
this.template = template;
}

@Override
public boolean isEmpty() {
return template.isEmpty();
}

@Override
public Optional<String> apply(String value) {
String transformationResult;
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...
}

try {
transformationResult = jinjava.render(template, bindings);
} catch (FatalTemplateErrorsException e) {
logger.warn("Applying template {} for component {} failed: {}", template,
component.getHaID().toShortTopic(), e.getMessage());
return Optional.empty();
}

logger.debug("transformation resulted in '{}'", transformationResult);

return Optional.of(transformationResult);
}

private static @Nullable Object toObject(JsonNode node) {
switch (node.getNodeType()) {
case ARRAY: {
List<@Nullable Object> result = new ArrayList<>();
for (JsonNode el : node) {
result.add(toObject(el));
}
return result;
}
case NUMBER:
return node.decimalValue();
case OBJECT: {
Map<String, @Nullable Object> result = new HashMap<>();
Iterator<Entry<String, JsonNode>> it = node.fields();
while (it.hasNext()) {
Entry<String, JsonNode> field = it.next();
result.put(field.getKey(), toObject(field.getValue()));
}
return result;
}
case STRING:
return node.asText();
case BOOLEAN:
return node.asBoolean();
case NULL:
default:
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import org.openhab.core.types.StateDescription;

import com.google.gson.Gson;
import com.hubspot.jinjava.Jinjava;

/**
* A HomeAssistant component is comparable to a channel group.
Expand Down Expand Up @@ -334,6 +335,10 @@ public Gson getGson() {
return componentConfiguration.getGson();
}

public Jinjava getJinjava() {
return componentConfiguration.getJinjava();
}

public C getChannelConfiguration() {
return channelConfiguration;
}
Expand Down
Loading

0 comments on commit 3d462c9

Please sign in to comment.