]> git.basschouten.com Git - openhab-addons.git/commitdiff
[mqtt.homeassistant] Add support for Event component (#17599)
authorCody Cutrer <cody@cutrer.us>
Mon, 21 Oct 2024 20:48:01 +0000 (15:48 -0500)
committerGitHub <noreply@github.com>
Mon, 21 Oct 2024 20:48:01 +0000 (22:48 +0200)
Signed-off-by: Cody Cutrer <cody@cutrer.us>
bundles/org.openhab.binding.mqtt.homeassistant/README.md
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/ComponentFactory.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Event.java [new file with mode: 0644]
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java
bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java
bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/DeviceTriggerTests.java
bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/EventTests.java [new file with mode: 0644]

index ff5fb7cc4f889083dd97c3d4000aa3e33f72ab62..fb7d53ca01948872bfd9ebdc59a45fff41423740 100644 (file)
@@ -21,6 +21,7 @@ You can also manually create a Thing, and provide the individual component topic
 - [Climate](https://www.home-assistant.io/integrations/climate.mqtt/)
 - [Cover](https://www.home-assistant.io/integrations/cover.mqtt/)
 - [Device Trigger](https://www.home-assistant.io/integrations/device_trigger.mqtt/)
+- [Event](https://www.home-assistant.io/integrations/event.mqtt/)
 - [Fan](https://www.home-assistant.io/integrations/fan.mqtt/)
 - [Light](https://www.home-assistant.io/integrations/light.mqtt/)
 - [Lock](https://www.home-assistant.io/integrations/lock.mqtt/)
index 36e9bbd66bdf165217f0896db26120b97a45894d..4c2f989c8e982b5fb93e838990ff9c94e6232921 100644 (file)
@@ -59,14 +59,16 @@ public class ComponentFactory {
                 return new Button(componentConfiguration, newStyleChannels);
             case "camera":
                 return new Camera(componentConfiguration, newStyleChannels);
-            case "cover":
-                return new Cover(componentConfiguration, newStyleChannels);
-            case "fan":
-                return new Fan(componentConfiguration, newStyleChannels);
             case "climate":
                 return new Climate(componentConfiguration, newStyleChannels);
+            case "cover":
+                return new Cover(componentConfiguration, newStyleChannels);
             case "device_automation":
                 return new DeviceTrigger(componentConfiguration, newStyleChannels);
+            case "event":
+                return new Event(componentConfiguration, newStyleChannels);
+            case "fan":
+                return new Fan(componentConfiguration, newStyleChannels);
             case "light":
                 return Light.create(componentConfiguration, newStyleChannels);
             case "lock":
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Event.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Event.java
new file mode 100644 (file)
index 0000000..be11ca5
--- /dev/null
@@ -0,0 +1,116 @@
+/**
+ * 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.component;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
+import org.openhab.binding.mqtt.generic.values.TextValue;
+import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
+import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantChannelTransformation;
+import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * A MQTT Event, following the https://www.home-assistant.io/integrations/event.mqttspecification.
+ *
+ * @author Cody Cutrer - Initial contribution
+ */
+@NonNullByDefault
+public class Event extends AbstractComponent<Event.ChannelConfiguration> implements ChannelStateUpdateListener {
+    public static final String EVENT_TYPE_CHANNEL_ID = "event-type";
+    public static final String JSON_ATTRIBUTES_CHANNEL_ID = "json-attributes";
+    private static final String EVENT_TYPE_TRANFORMATION = "{{ value_json.event_type }}";
+
+    /**
+     * Configuration class for MQTT component
+     */
+    public static class ChannelConfiguration extends AbstractChannelConfiguration {
+        ChannelConfiguration() {
+            super("MQTT Event");
+        }
+
+        @SerializedName("state_topic")
+        protected String stateTopic = "";
+
+        @SerializedName("event_types")
+        protected List<String> eventTypes = new ArrayList();
+
+        @SerializedName("json_attributes_topic")
+        protected @Nullable String jsonAttributesTopic;
+
+        @SerializedName("json_attributes_template")
+        protected @Nullable String jsonAttributesTemplate;
+    }
+
+    private final HomeAssistantChannelTransformation transformation;
+
+    public Event(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
+        super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
+
+        transformation = new HomeAssistantChannelTransformation(getJinjava(), this, "");
+
+        buildChannel(EVENT_TYPE_CHANNEL_ID, ComponentChannelType.TRIGGER, new TextValue(), getName(), this)
+                .stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate()).trigger(true)
+                .build();
+
+        if (channelConfiguration.jsonAttributesTopic != null) {
+            // It's unclear from the documentation if the JSON attributes value is expected
+            // to be the same as the main topic, and thus would always have an event_type
+            // attribute (and thus could possibly be shared with multiple components).
+            // If that were the case, we would need to intercept events, and check that they
+            // had an event_type that is in channelConfiguration.eventTypes. If/when that
+            // becomes an issue, change `channelStateUpdateListener` to `this`, and handle
+            // the filtering below.
+            buildChannel(JSON_ATTRIBUTES_CHANNEL_ID, ComponentChannelType.TRIGGER, new TextValue(), getName(),
+                    componentConfiguration.getUpdateListener())
+                    .stateTopic(channelConfiguration.jsonAttributesTopic, channelConfiguration.jsonAttributesTemplate)
+                    .trigger(true).build();
+        }
+
+        finalizeChannels();
+    }
+
+    @Override
+    public void triggerChannel(ChannelUID channel, String event) {
+        String eventType = transformation.apply(EVENT_TYPE_TRANFORMATION, event).orElse(null);
+        if (eventType == null) {
+            // Warning logged from inside the transformation
+            return;
+        }
+        // The TextValue allows anything, because it receives the full JSON, and
+        // we don't check the actual event_type against valid event_types until here
+        if (!channelConfiguration.eventTypes.contains(eventType)) {
+            return;
+        }
+
+        componentConfiguration.getUpdateListener().triggerChannel(channel, eventType);
+    }
+
+    @Override
+    public void updateChannelState(ChannelUID channel, State state) {
+        // N/A (only trigger channels)
+    }
+
+    @Override
+    public void postChannelCommand(ChannelUID channel, Command command) {
+        // N/A (only trigger channels)
+    }
+}
index 7609fa7a44dd41964bbaa79e71bc7e2b967dfca7..11ce67c84f7aec87685d8b8122b0c0dae97e7be0 100644 (file)
@@ -304,12 +304,13 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
                 }
 
                 // Add component to the component map
-                addComponent(discovered);
-                // Start component / Subscribe to channel topics
-                discovered.start(connection, scheduler, 0).exceptionally(e -> {
-                    logger.warn("Failed to start component {}", discovered.getHaID(), e);
-                    return null;
-                });
+                if (addComponent(discovered)) {
+                    // Start component / Subscribe to channel topics
+                    discovered.start(connection, scheduler, 0).exceptionally(e -> {
+                        logger.warn("Failed to start component {}", discovered.getHaID(), e);
+                        return null;
+                    });
+                }
 
                 if (discovered instanceof Update) {
                     updateComponent = (Update) discovered;
@@ -427,7 +428,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
     }
 
     // should only be called when it's safe to access haComponents
-    private void addComponent(AbstractComponent component) {
+    private boolean addComponent(AbstractComponent component) {
         AbstractComponent existing = haComponents.get(component.getComponentId());
         if (existing != null) {
             // DeviceTriggers that are for the same subtype, topic, and value template
@@ -454,8 +455,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
                         });
                     }
                     haComponentsByUniqueId.put(component.getUniqueId(), component);
-                    System.out.println("don't forget to add to the channel config");
-                    return;
+                    return false;
                 }
             }
 
@@ -467,6 +467,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
         }
         haComponents.put(component.getComponentId(), component);
         haComponentsByUniqueId.put(component.getUniqueId(), component);
+        return true;
     }
 
     /**
index 2771859417bafd6fd909756f9c9de5b1b6d0d671..cb9c4fe5a306d2f93b032b9b46b92e3f2f6df90e 100644 (file)
@@ -233,18 +233,13 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
         }
     }
 
-    protected void spyOnChannelUpdates(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
-            String channelId) {
-        // It's already thingHandler, but not the spy version
-        component.getChannel(channelId).getState().setChannelStateUpdateListener(thingHandler);
-    }
-
     /**
      * Assert a channel triggers
      */
     protected void assertTriggered(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
             String channelId, String trigger) {
-        verify(thingHandler).triggerChannel(eq(component.getChannel(channelId).getChannel().getUID()), eq(trigger));
+        verify(callbackMock).channelTriggered(eq(haThing), eq(component.getChannel(channelId).getChannel().getUID()),
+                eq(trigger));
     }
 
     /**
@@ -252,8 +247,8 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
      */
     protected void assertNotTriggered(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
             String channelId, String trigger) {
-        verify(thingHandler, never()).triggerChannel(eq(component.getChannel(channelId).getChannel().getUID()),
-                eq(trigger));
+        verify(callbackMock, never()).channelTriggered(eq(haThing),
+                eq(component.getChannel(channelId).getChannel().getUID()), eq(trigger));
     }
 
     /**
index 96becd8e2193caa6f984b86e2e013be466a4117d..a45c1eac3479dafea451ee0884ce0abefd69dc3c 100644 (file)
@@ -66,7 +66,6 @@ public class DeviceTriggerTests extends AbstractComponentTests {
         assertChannel(component, "on", "zigbee2mqtt/Charge Now Button/action", "", "MQTT Device Trigger",
                 TextValue.class);
 
-        spyOnChannelUpdates(component, "on");
         publishMessage("zigbee2mqtt/Charge Now Button/action", "on");
         assertTriggered(component, "on", "on");
 
@@ -132,7 +131,6 @@ public class DeviceTriggerTests extends AbstractComponentTests {
         List<?> configList = (List<?>) config;
         assertThat(configList.size(), is(2));
 
-        spyOnChannelUpdates(component1, "turn_on");
         publishMessage("zigbee2mqtt/Charge Now Button/action", "press");
         assertTriggered(component1, "turn_on", "press");
 
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/EventTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/EventTests.java
new file mode 100644 (file)
index 0000000..6dc20ca
--- /dev/null
@@ -0,0 +1,89 @@
+/**
+ * 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.component;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mqtt.generic.values.TextValue;
+
+/**
+ * Tests for {@link Event}
+ *
+ * @author Cody Cutrer - Initial contribution
+ */
+@NonNullByDefault
+public class EventTests extends AbstractComponentTests {
+    public static final String CONFIG_TOPIC = "event/doorbell/action";
+
+    @SuppressWarnings("null")
+    @Test
+    public void test() throws InterruptedException {
+        var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
+                  {
+                      "event_types": [
+                        "press",
+                        "release"
+                      ],
+                      "state_topic": "zigbee2mqtt/doorbell/action"
+                  }
+                """);
+
+        assertThat(component.channels.size(), is(1));
+        assertThat(component.getName(), is("MQTT Event"));
+
+        assertChannel(component, "event-type", "zigbee2mqtt/doorbell/action", "", "MQTT Event", TextValue.class);
+
+        publishMessage("zigbee2mqtt/doorbell/action", "{ \"event_type\": \"press\" }");
+        assertTriggered(component, "event-type", "press");
+
+        publishMessage("zigbee2mqtt/doorbell/action", "{ \"event_type\": \"release\" }");
+        assertTriggered(component, "event-type", "release");
+
+        publishMessage("zigbee2mqtt/doorbell/action", "{ \"event_type\": \"else\" }");
+        assertNotTriggered(component, "event-type", "else");
+    }
+
+    @SuppressWarnings("null")
+    @Test
+    public void testJsonAttributes() throws InterruptedException {
+        var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
+                  {
+                      "event_types": [
+                        "press",
+                        "release"
+                      ],
+                      "state_topic": "zigbee2mqtt/doorbell/action",
+                      "json_attributes_topic": "zigbee2mqtt/doorbell/action"
+                  }
+                """);
+
+        assertThat(component.channels.size(), is(2));
+        assertThat(component.getName(), is("MQTT Event"));
+
+        assertChannel(component, "event-type", "zigbee2mqtt/doorbell/action", "", "MQTT Event", TextValue.class);
+        assertChannel(component, "json-attributes", "zigbee2mqtt/doorbell/action", "", "MQTT Event", TextValue.class);
+
+        publishMessage("zigbee2mqtt/doorbell/action", "{ \"event_type\": \"press\" }");
+        assertTriggered(component, "json-attributes", "{ \"event_type\": \"press\" }");
+    }
+
+    @Override
+    protected Set<String> getConfigTopics() {
+        return Set.of(CONFIG_TOPIC);
+    }
+}