From 39272a6802bb71cd9decb424f967f2d3045bcc34 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Fri, 3 Oct 2025 12:35:36 -0600 Subject: [PATCH 1/5] Add support for actions and events They get sent through the openHAB event bus, and can be used in rules via custom triggers. Signed-off-by: Cody Cutrer --- README.md | 24 +++ .../esphome/events/AbstractESPHomeEvent.java | 52 ++++++ .../binding/esphome/events/ActionEvent.java | 46 +++++ .../esphome/events/ESPHomeEventFactory.java | 160 ++++++++++++++++++ .../binding/esphome/events/EventEvent.java | 46 +++++ .../esphome/events/TagScannedEvent.java | 44 +++++ .../internal/ESPHomeConfiguration.java | 2 + .../internal/handler/ESPHomeHandler.java | 40 ++++- .../handler/ESPHomeHandlerFactory.java | 9 +- .../AbstractESPHomeTriggerHandler.java | 82 +++++++++ .../module/handler/ActionTriggerHandler.java | 66 ++++++++ .../handler/ESPHomeModuleHandlerFactory.java | 83 +++++++++ .../module/handler/EventTriggerHandler.java | 66 ++++++++ .../handler/TagScannedTriggerHandler.java | 104 ++++++++++++ .../automation/moduletypes/ActionTrigger.json | 26 +++ .../automation/moduletypes/EventTrigger.json | 26 +++ .../moduletypes/TagScannedTrigger.json | 26 +++ .../resources/OH-INF/thing/thing-esphome.xml | 11 ++ .../devicetest/AbstractESPHomeDeviceTest.java | 4 +- .../events/ESPHomeEventFactoryTest.java | 64 +++++++ triggers.png | Bin 0 -> 95726 bytes 21 files changed, 976 insertions(+), 5 deletions(-) create mode 100644 src/main/java/no/seime/openhab/binding/esphome/events/AbstractESPHomeEvent.java create mode 100644 src/main/java/no/seime/openhab/binding/esphome/events/ActionEvent.java create mode 100644 src/main/java/no/seime/openhab/binding/esphome/events/ESPHomeEventFactory.java create mode 100644 src/main/java/no/seime/openhab/binding/esphome/events/EventEvent.java create mode 100644 src/main/java/no/seime/openhab/binding/esphome/events/TagScannedEvent.java create mode 100644 src/main/java/no/seime/openhab/binding/esphome/internal/module/handler/AbstractESPHomeTriggerHandler.java create mode 100644 src/main/java/no/seime/openhab/binding/esphome/internal/module/handler/ActionTriggerHandler.java create mode 100644 src/main/java/no/seime/openhab/binding/esphome/internal/module/handler/ESPHomeModuleHandlerFactory.java create mode 100644 src/main/java/no/seime/openhab/binding/esphome/internal/module/handler/EventTriggerHandler.java create mode 100644 src/main/java/no/seime/openhab/binding/esphome/internal/module/handler/TagScannedTriggerHandler.java create mode 100644 src/main/resources/OH-INF/automation/moduletypes/ActionTrigger.json create mode 100644 src/main/resources/OH-INF/automation/moduletypes/EventTrigger.json create mode 100644 src/main/resources/OH-INF/automation/moduletypes/TagScannedTrigger.json create mode 100644 src/test/java/no/seime/openhab/binding/esphome/events/ESPHomeEventFactoryTest.java create mode 100644 triggers.png diff --git a/README.md b/README.md index fbc726fd..16ba8ef3 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ to `$OH_CONFDIR/services/runtime.cfg` . | `hostname` | `text` | Hostname or IP address of the device. Typically something like `myboard.local` or `192.168.0.123`. *It is recommended to configure your ESP with a static IP address and use that here, it will allow for quicker reconnects* | | yes | no | | `port` | `integer` | IP Port of the device | 6053 | no | no | | `encryptionKey` | `text` | Encryption key as defined in `api: encryption: key: `. See https://esphome.io/components/api#configuration-variables. *Can also be set on the binding level if your ESPs all use the same key.* | | yes or via binding configuration | no | +| `allowActions` | `boolean` | Allow the device to send actions and events. | false | no | no | | `pingInterval` | `integer` | Seconds between sending ping requests to device to check if alive | 10 | no | yes | | `maxPingTimeouts` | `integer` | Number of missed ping requests before deeming device unresponsive. | 4 | no | yes | | `reconnectInterval` | `integer` | Seconds between reconnect attempts when connection is lost or the device restarts. | 10 | no | yes | @@ -320,6 +321,29 @@ time: id: openhab_time ``` +## Actions and Events + +To process actions and events sent via the [Native API Component's actions](https://esphome.io/components/api/#api-actions), the binding adds three new trigger types accessible via UI rules: + +![New Triggers](triggers.png) + +Be sure to enable `allowActions` in the Thing configuration so that openHAB will request the device to send events. +The event object has `getData`, `getDataTemplate`, and `getVariables` methods to access the appropriate information for action and event events. +For tag scanned events, the event's payload is the tag ID. +The event's source is the ESPHome device ID that sent the event. + +Unfortunately, these triggers are not available from Rules DSL, but some other openHAB automation languages may support setting triggers based on any event sent through the [event bus](https://www.openhab.org/docs/developer/utils/events.html). +To listen for these events, they look like this: + +| ESPHome Action | Topic | Event Type | Payload | +|-----------------------------|-----------------------------------|---------------------------|------------------------------------------------------------------------| +| `homeassistant.action` | `openhab/esphome/action/` | `esphome.ActionEvent` | JSON object with "data", "data_template", and "variables" sub-objects. | +| `homeassistant.event` | `openhab/esphome/event/` | `esphome.EventEvent` | JSON object with "data", "data_template", and "variables" sub-objects. | +| `homeassistant.tag_scanned` | `openhab/esphome/tag_scanned` | `esphome.TagScannedEvent` | The tag id. | + +For JRuby, use the [`event` trigger](https://openhab.github.io/openhab-jruby/main/OpenHAB/DSL/Rules/BuilderDSL.html#event-instance_method). +For Python Scripting, use [`GenericEventTrigger`](https://www.openhab.org/addons/automation/pythonscripting/#module-openhab-triggers). + ## Limitations Most entity types and functions are now supported. However, there are some limitations: diff --git a/src/main/java/no/seime/openhab/binding/esphome/events/AbstractESPHomeEvent.java b/src/main/java/no/seime/openhab/binding/esphome/events/AbstractESPHomeEvent.java new file mode 100644 index 00000000..7f3e2746 --- /dev/null +++ b/src/main/java/no/seime/openhab/binding/esphome/events/AbstractESPHomeEvent.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2023 Contributors to the Seime Openhab Addons 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 no.seime.openhab.binding.esphome.events; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.events.AbstractEvent; + +/** + * Abstract base class for both action and event events. + * + * @author Cody Cutrer - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractESPHomeEvent extends AbstractEvent { + protected final String action; + private final Map data; + private final Map data_template; + private final Map variables; + + public AbstractESPHomeEvent(String topic, String payload, String deviceId, String action, Map data, + Map data_template, Map variables) { + super(topic, payload, deviceId); + this.action = action; + this.data = data; + this.data_template = data_template; + this.variables = variables; + } + + public Map getData() { + return data; + } + + public Map getDataTemplate() { + return data_template; + } + + public Map getVariables() { + return variables; + } +} diff --git a/src/main/java/no/seime/openhab/binding/esphome/events/ActionEvent.java b/src/main/java/no/seime/openhab/binding/esphome/events/ActionEvent.java new file mode 100644 index 00000000..14f84a6c --- /dev/null +++ b/src/main/java/no/seime/openhab/binding/esphome/events/ActionEvent.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2023 Contributors to the Seime Openhab Addons 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 no.seime.openhab.binding.esphome.events; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents an action request sent from an ESPHome device. + * + * @author Cody Cutrer - Initial contribution + */ +@NonNullByDefault +public class ActionEvent extends AbstractESPHomeEvent { + public static final String TYPE = "esphome.ActionEvent"; + + public ActionEvent(String topic, String payload, String deviceId, String action, Map data, + Map data_template, Map variables) { + super(topic, payload, deviceId, action, data, data_template, variables); + } + + @Override + public String getType() { + return TYPE; + } + + public String getAction() { + return action; + } + + @Override + public String toString() { + return String.format("Device '%s' requested action '%s'", getSource(), action); + } +} diff --git a/src/main/java/no/seime/openhab/binding/esphome/events/ESPHomeEventFactory.java b/src/main/java/no/seime/openhab/binding/esphome/events/ESPHomeEventFactory.java new file mode 100644 index 00000000..1a429b84 --- /dev/null +++ b/src/main/java/no/seime/openhab/binding/esphome/events/ESPHomeEventFactory.java @@ -0,0 +1,160 @@ +/** + * Copyright (c) 2023 Contributors to the Seime Openhab Addons 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 no.seime.openhab.binding.esphome.events; + +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.events.AbstractEventFactory; +import org.openhab.core.events.Event; +import org.openhab.core.events.EventFactory; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class represents an action request sent from an ESPHome device. + * + * @author Cody Cutrer - Initial contribution + */ +@NonNullByDefault +@Component(service = EventFactory.class, immediate = true) +public class ESPHomeEventFactory extends AbstractEventFactory { + private static final String ACTION_IDENTIFIER = "{action}"; + private static final String ACTION_EVENT_TOPIC = "openhab/esphome/action/" + ACTION_IDENTIFIER; + private static final String EVENT_EVENT_TOPIC = "openhab/esphome/event/" + ACTION_IDENTIFIER; + private static final String TAG_SCANNED_EVENT_TOPIC = "openhab/esphome/tag_scanned"; + + private final Logger logger = LoggerFactory.getLogger(ESPHomeEventFactory.class); + + private static final Set SUPPORTED_TYPES = Set.of(ActionEvent.TYPE, EventEvent.TYPE, TagScannedEvent.TYPE); + + public ESPHomeEventFactory() { + super(SUPPORTED_TYPES); + } + + @Override + protected Event createEventByType(String eventType, String topic, String payload, @Nullable String source) + throws Exception { + logger.trace("creating ruleEvent of type: {}", eventType); + if (source == null) { + throw new IllegalArgumentException("'source' must not be null for ESPHome events"); + } + if (ActionEvent.TYPE.equals(eventType)) { + return createActionEvent(topic, payload, source); + } else if (EventEvent.TYPE.equals(eventType)) { + return createEventEvent(topic, payload, source); + } else if (TagScannedEvent.TYPE.equals(eventType)) { + return new TagScannedEvent(topic, payload, source); + } + throw new IllegalArgumentException("The event type '" + eventType + "' is not supported by this factory."); + } + + private Event createActionEvent(String topic, String payload, String source) { + String action = getAction(topic); + ActionEventPayloadBean bean = deserializePayload(payload, ActionEventPayloadBean.class); + + return new ActionEvent(topic, payload, source, action, bean.getData(), bean.getDataTemplate(), + bean.getVariables()); + } + + private Event createEventEvent(String topic, String payload, String source) { + String action = getAction(topic); + ActionEventPayloadBean bean = deserializePayload(payload, ActionEventPayloadBean.class); + + return new EventEvent(topic, payload, source, action, bean.getData(), bean.getDataTemplate(), + bean.getVariables()); + } + + private String getAction(String topic) { + String[] topicElements = getTopicElements(topic); + if (topicElements.length < 4) { + throw new IllegalArgumentException("Event creation failed, invalid topic: " + topic); + } + + return topicElements[3]; + } + + /** + * Creates an {@link ActionEvent} + * + * @param moduleId the module type id of this event + * @param label The label (or id) of this object + * @param configuration the configuration of the trigger + * @return the created event + */ + public static ActionEvent createActionEvent(String deviceId, String action, Map data, + Map data_template, Map variables) { + String topic = ACTION_EVENT_TOPIC.replace(ACTION_IDENTIFIER, action); + ActionEventPayloadBean bean = new ActionEventPayloadBean(data, data_template, variables); + String payload = serializePayload(bean); + return new ActionEvent(topic, payload, deviceId, action, data, data_template, variables); + } + + /** + * Creates an {@link ActionEvent} + * + * @param moduleId the module type id of this event + * @param label The label (or id) of this object + * @param configuration the configuration of the trigger + * @return the created event + */ + public static EventEvent createEventEvent(String deviceId, String action, Map data, + Map data_template, Map variables) { + String topic = EVENT_EVENT_TOPIC.replace(ACTION_IDENTIFIER, action); + ActionEventPayloadBean bean = new ActionEventPayloadBean(data, data_template, variables); + String payload = serializePayload(bean); + return new EventEvent(topic, payload, deviceId, action, data, data_template, variables); + } + + public static TagScannedEvent createTagScannedEvent(String deviceId, String tagId) { + return new TagScannedEvent(TAG_SCANNED_EVENT_TOPIC, tagId, deviceId); + } + + /** + * This is a java bean that is used to serialize/deserialize action event payload. + */ + private static class ActionEventPayloadBean { + private @NonNullByDefault({}) Map data; + private @NonNullByDefault({}) Map data_template; + private @NonNullByDefault({}) Map variables; + + /** + * Default constructor for deserialization e.g. by Gson. + */ + @SuppressWarnings("unused") + protected ActionEventPayloadBean() { + } + + public ActionEventPayloadBean(Map data, Map data_template, + Map variables) { + this.data = data; + this.data_template = data_template; + this.variables = variables; + } + + public Map getData() { + return data; + } + + public Map getDataTemplate() { + return data_template; + } + + public Map getVariables() { + return variables; + } + } +} diff --git a/src/main/java/no/seime/openhab/binding/esphome/events/EventEvent.java b/src/main/java/no/seime/openhab/binding/esphome/events/EventEvent.java new file mode 100644 index 00000000..b06be690 --- /dev/null +++ b/src/main/java/no/seime/openhab/binding/esphome/events/EventEvent.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2023 Contributors to the Seime Openhab Addons 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 no.seime.openhab.binding.esphome.events; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents an event sent from an ESPHome device. + * + * @author Cody Cutrer - Initial contribution + */ +@NonNullByDefault +public class EventEvent extends AbstractESPHomeEvent { + public static final String TYPE = "esphome.EventEvent"; + + public EventEvent(String topic, String payload, String deviceId, String action, Map data, + Map data_template, Map variables) { + super(topic, payload, deviceId, action, data, data_template, variables); + } + + @Override + public String getType() { + return TYPE; + } + + public String getEvent() { + return action; + } + + @Override + public String toString() { + return String.format("Device '%s' sent event '%s'", getSource(), action); + } +} diff --git a/src/main/java/no/seime/openhab/binding/esphome/events/TagScannedEvent.java b/src/main/java/no/seime/openhab/binding/esphome/events/TagScannedEvent.java new file mode 100644 index 00000000..8447601a --- /dev/null +++ b/src/main/java/no/seime/openhab/binding/esphome/events/TagScannedEvent.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2023 Contributors to the Seime Openhab Addons 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 no.seime.openhab.binding.esphome.events; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.events.AbstractEvent; + +/** + * This class represents a tag scan event sent from an ESPHome device. + * + * @author Cody Cutrer - Initial contribution + */ +@NonNullByDefault +public class TagScannedEvent extends AbstractEvent { + public static final String TYPE = "esphome.TagScannedEvent"; + + public TagScannedEvent(String topic, String tag_id, String source) { + super(topic, tag_id, source); + } + + @Override + public String getType() { + return TYPE; + } + + public String getTagId() { + return getPayload(); + } + + @Override + public String toString() { + return String.format("Device '%s' scanned tag '%s'", getSource(), getTagId()); + } +} diff --git a/src/main/java/no/seime/openhab/binding/esphome/internal/ESPHomeConfiguration.java b/src/main/java/no/seime/openhab/binding/esphome/internal/ESPHomeConfiguration.java index 0228c757..f41b98d2 100644 --- a/src/main/java/no/seime/openhab/binding/esphome/internal/ESPHomeConfiguration.java +++ b/src/main/java/no/seime/openhab/binding/esphome/internal/ESPHomeConfiguration.java @@ -38,6 +38,8 @@ public class ESPHomeConfiguration { public String deviceId; + public boolean allowActions = false; + @Nullable public String logPrefix; diff --git a/src/main/java/no/seime/openhab/binding/esphome/internal/handler/ESPHomeHandler.java b/src/main/java/no/seime/openhab/binding/esphome/internal/handler/ESPHomeHandler.java index c0479b13..c68a2f1e 100644 --- a/src/main/java/no/seime/openhab/binding/esphome/internal/handler/ESPHomeHandler.java +++ b/src/main/java/no/seime/openhab/binding/esphome/internal/handler/ESPHomeHandler.java @@ -22,6 +22,8 @@ import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.events.AbstractEvent; +import org.openhab.core.events.EventPublisher; import org.openhab.core.thing.*; import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.thing.type.ChannelType; @@ -33,6 +35,7 @@ import com.jano7.executor.KeySequentialExecutor; import io.esphome.api.*; +import no.seime.openhab.binding.esphome.events.ESPHomeEventFactory; import no.seime.openhab.binding.esphome.internal.*; import no.seime.openhab.binding.esphome.internal.LogLevel; import no.seime.openhab.binding.esphome.internal.bluetooth.ESPHomeBluetoothProxyHandler; @@ -53,6 +56,7 @@ public class ESPHomeHandler extends BaseThingHandler implements CommunicationLis private static final int API_VERSION_MAJOR = 1; private static final int API_VERSION_MINOR = 9; private static final String DEVICE_LOGGER_NAME = "ESPHOMEDEVICE"; + private static final String ACTION_TAG_SCANNED = "esphome.tag_scanned"; private final Logger logger = LoggerFactory.getLogger(ESPHomeHandler.class); private final Logger deviceLogger = LoggerFactory.getLogger(DEVICE_LOGGER_NAME); @@ -66,6 +70,7 @@ public class ESPHomeHandler extends BaseThingHandler implements CommunicationLis private final ESPHomeEventSubscriber eventSubscriber; private final MonitoredScheduledThreadPoolExecutor executorService; private final KeySequentialExecutor packetProcessor; + private final EventPublisher eventPublisher; @Nullable private final String defaultEncryptionKey; private @Nullable ESPHomeConfiguration config; @@ -90,7 +95,8 @@ public class ESPHomeHandler extends BaseThingHandler implements CommunicationLis public ESPHomeHandler(Thing thing, ConnectionSelector connectionSelector, ESPChannelTypeProvider dynamicChannelTypeProvider, ESPStateDescriptionProvider stateDescriptionProvider, ESPHomeEventSubscriber eventSubscriber, MonitoredScheduledThreadPoolExecutor executorService, - KeySequentialExecutor packetProcessor, @Nullable String defaultEncryptionKey) { + KeySequentialExecutor packetProcessor, EventPublisher eventPublisher, + @Nullable String defaultEncryptionKey) { super(thing); this.connectionSelector = connectionSelector; this.dynamicChannelTypeProvider = dynamicChannelTypeProvider; @@ -99,6 +105,7 @@ public ESPHomeHandler(Thing thing, ConnectionSelector connectionSelector, this.eventSubscriber = eventSubscriber; this.executorService = executorService; this.packetProcessor = packetProcessor; + this.eventPublisher = eventPublisher; this.defaultEncryptionKey = defaultEncryptionKey; // Register message handlers for each type of message pairs @@ -435,6 +442,25 @@ private void handleConnected(GeneratedMessage message) throws ProtocolAPIError { } } else if (message instanceof SubscribeLogsResponse subscribeLogsResponse) { deviceLogger.info("[{}] {}", logPrefix, subscribeLogsResponse.getMessage().toStringUtf8()); + } else if (message instanceof HomeassistantServiceResponse serviceResponse) { + Map data = convertPbListToMap(serviceResponse.getDataList()); + Map dataTemplate = convertPbListToMap(serviceResponse.getDataTemplateList()); + Map variables = convertPbListToMap(serviceResponse.getVariablesList()); + AbstractEvent event; + if (serviceResponse.getIsEvent()) { + String tagId; + if (serviceResponse.getService().equals(ACTION_TAG_SCANNED) && dataTemplate.isEmpty() + && variables.isEmpty() && data.size() == 1 && (tagId = data.get("tag_id")) != null) { + event = ESPHomeEventFactory.createTagScannedEvent(config.deviceId, tagId); + } else { + event = ESPHomeEventFactory.createEventEvent(config.deviceId, serviceResponse.getService(), data, + dataTemplate, variables); + } + } else { + event = ESPHomeEventFactory.createActionEvent(config.deviceId, serviceResponse.getService(), data, + dataTemplate, variables); + } + eventPublisher.post(event); } else if (message instanceof SubscribeHomeAssistantStateResponse subscribeHomeAssistantStateResponse) { initializeStateSubscription(subscribeHomeAssistantStateResponse); } else if (message instanceof GetTimeRequest) { @@ -536,6 +562,10 @@ private void handleLoginResponse(GeneratedMessage message) throws ProtocolAPIErr } connectionState = ConnectionState.CONNECTED; + if (config.allowActions) { + logger.debug("[{}] Requesting device to send actions and events", logPrefix); + frameHelper.send(SubscribeHomeassistantServicesRequest.getDefaultInstance()); + } if (config.deviceLogLevel != LogLevel.NONE) { logger.info("[{}] Starting to stream logs to logger " + DEVICE_LOGGER_NAME, logPrefix); @@ -691,6 +721,14 @@ public String getLogPrefix() { return logPrefix; } + private Map convertPbListToMap(List list) { + Map map = new HashMap<>(); + for (HomeassistantServiceMap kv : list) { + map.put(kv.getKey(), kv.getValue()); + } + return Collections.unmodifiableMap(map); + } + private enum ConnectionState { // Initial state, no connection UNINITIALIZED, diff --git a/src/main/java/no/seime/openhab/binding/esphome/internal/handler/ESPHomeHandlerFactory.java b/src/main/java/no/seime/openhab/binding/esphome/internal/handler/ESPHomeHandlerFactory.java index ff426a3b..32243457 100644 --- a/src/main/java/no/seime/openhab/binding/esphome/internal/handler/ESPHomeHandlerFactory.java +++ b/src/main/java/no/seime/openhab/binding/esphome/internal/handler/ESPHomeHandlerFactory.java @@ -20,6 +20,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.bluetooth.BluetoothAdapter; +import org.openhab.core.events.EventPublisher; import org.openhab.core.thing.*; import org.openhab.core.thing.binding.BaseThingHandlerFactory; import org.openhab.core.thing.binding.ThingHandler; @@ -68,6 +69,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { private final ESPHomeEventSubscriber eventSubscriber; private final ThingRegistry thingRegistry; + private final EventPublisher eventPublisher; private final MonitoredScheduledThreadPoolExecutor scheduler; private final KeySequentialExecutor packetExecutor; private final ConnectionSelector connectionSelector; @@ -75,8 +77,8 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { @Activate public ESPHomeHandlerFactory(@Reference ESPChannelTypeProvider dynamicChannelTypeProvider, @Reference ESPStateDescriptionProvider stateDescriptionProvider, - @Reference ESPHomeEventSubscriber eventSubscriber, @Reference ThingRegistry thingRegistry) - throws IOException { + @Reference ESPHomeEventSubscriber eventSubscriber, @Reference ThingRegistry thingRegistry, + @Reference EventPublisher eventPublisher) throws IOException { scheduler = new MonitoredScheduledThreadPoolExecutor(4, r -> { long currentCount = threadCounter.incrementAndGet(); logger.debug("Creating new worker thread {} for scheduler", currentCount); @@ -92,6 +94,7 @@ public ESPHomeHandlerFactory(@Reference ESPChannelTypeProvider dynamicChannelTyp this.stateDescriptionProvider = stateDescriptionProvider; this.eventSubscriber = eventSubscriber; this.thingRegistry = thingRegistry; + this.eventPublisher = eventPublisher; connectionSelector = new ConnectionSelector(); } @@ -102,7 +105,7 @@ public ESPHomeHandlerFactory(@Reference ESPChannelTypeProvider dynamicChannelTyp if (BindingConstants.THING_TYPE_DEVICE.equals(thingTypeUID)) { return new ESPHomeHandler(thing, connectionSelector, dynamicChannelTypeProvider, stateDescriptionProvider, - eventSubscriber, scheduler, packetExecutor, defaultEncryptionKey); + eventSubscriber, scheduler, packetExecutor, eventPublisher, defaultEncryptionKey); } else if (BindingConstants.THING_TYPE_BLE_PROXY.equals(thingTypeUID)) { ESPHomeBluetoothProxyHandler handler = new ESPHomeBluetoothProxyHandler((Bridge) thing, thingRegistry, scheduler); diff --git a/src/main/java/no/seime/openhab/binding/esphome/internal/module/handler/AbstractESPHomeTriggerHandler.java b/src/main/java/no/seime/openhab/binding/esphome/internal/module/handler/AbstractESPHomeTriggerHandler.java new file mode 100644 index 00000000..c59ecfba --- /dev/null +++ b/src/main/java/no/seime/openhab/binding/esphome/internal/module/handler/AbstractESPHomeTriggerHandler.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2023 Contributors to the Seime Openhab Addons 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 no.seime.openhab.binding.esphome.internal.module.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.automation.ModuleHandlerCallback; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.handler.BaseTriggerModuleHandler; +import org.openhab.core.config.core.ConfigParser; +import org.openhab.core.events.EventFilter; +import org.openhab.core.events.EventSubscriber; +import org.openhab.core.events.TopicPrefixEventFilter; +import org.openhab.core.scheduler.ScheduledCompletableFuture; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is a ModuleHandler implementation for Triggers which triggers rules + * based on events sent from ESPHome devices + * + * @author Cody Cutrer - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractESPHomeTriggerHandler extends BaseTriggerModuleHandler implements EventSubscriber { + public static final String CONFIG_DEVICE_ID = "deviceId"; + + private final Logger logger = LoggerFactory.getLogger(AbstractESPHomeTriggerHandler.class); + + private final @Nullable EventFilter eventFilter; + protected final String action; + protected @Nullable String deviceId = null; + + private @Nullable ScheduledCompletableFuture schedule; + private @Nullable ServiceRegistration eventSubscriberRegistration; + + public AbstractESPHomeTriggerHandler(Trigger module, String baseTopic, String configAction, + BundleContext bundleContext) { + super(module); + this.action = ConfigParser.valueAsOrElse(module.getConfiguration().get(configAction), String.class, ""); + if (this.action.isBlank()) { + logger.warn("action is blank in module '{}', trigger will not work", module.getId()); + eventFilter = null; + return; + } + this.eventFilter = new TopicPrefixEventFilter(baseTopic + this.action); + this.deviceId = ConfigParser.valueAs(module.getConfiguration().get(CONFIG_DEVICE_ID), String.class); + eventSubscriberRegistration = bundleContext.registerService(EventSubscriber.class.getName(), this, null); + } + + @Override + public void dispose() { + ServiceRegistration eventSubscriberRegistration = this.eventSubscriberRegistration; + if (eventSubscriberRegistration != null) { + eventSubscriberRegistration.unregister(); + this.eventSubscriberRegistration = null; + } + super.dispose(); + } + + @Override + public synchronized void setCallback(ModuleHandlerCallback callback) { + super.setCallback(callback); + } + + @Override + public @Nullable EventFilter getEventFilter() { + return eventFilter; + } +} diff --git a/src/main/java/no/seime/openhab/binding/esphome/internal/module/handler/ActionTriggerHandler.java b/src/main/java/no/seime/openhab/binding/esphome/internal/module/handler/ActionTriggerHandler.java new file mode 100644 index 00000000..aaba8f09 --- /dev/null +++ b/src/main/java/no/seime/openhab/binding/esphome/internal/module/handler/ActionTriggerHandler.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2023 Contributors to the Seime Openhab Addons 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 no.seime.openhab.binding.esphome.internal.module.handler; + +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.automation.ModuleHandlerCallback; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.handler.TriggerHandlerCallback; +import org.openhab.core.events.Event; +import org.osgi.framework.BundleContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import no.seime.openhab.binding.esphome.events.ActionEvent; + +/** + * This is a ModuleHandler implementation for Triggers which triggers rules + * based on events sent from ESPHome devices + * + * @author Cody Cutrer - Initial contribution + */ +@NonNullByDefault +public class ActionTriggerHandler extends AbstractESPHomeTriggerHandler { + public static final String MODULE_TYPE_ID = "esphome.ActionTrigger"; + public static final String CONFIG_ACTION = "action"; + + private final Logger logger = LoggerFactory.getLogger(ActionTriggerHandler.class); + + public ActionTriggerHandler(Trigger module, BundleContext bundleContext) { + super(module, "openhab/esphome/action/", CONFIG_ACTION, bundleContext); + } + + @Override + public Set getSubscribedEventTypes() { + return Set.of(ActionEvent.TYPE); + } + + @Override + public void receive(Event event) { + if (event instanceof ActionEvent actionEvent && (actionEvent.getAction().equals(action))) { + if (deviceId != null && !actionEvent.getSource().equals(deviceId)) { + // device is configured on the trigger, but doesn't match the event, so skip it + return; + } + ModuleHandlerCallback callback = this.callback; + if (callback instanceof TriggerHandlerCallback triggerHandlerCallback) { + triggerHandlerCallback.triggered(module, Map.of("event", event)); + } else { + logger.debug("Tried to trigger, but callback isn't available!"); + } + } + } +} diff --git a/src/main/java/no/seime/openhab/binding/esphome/internal/module/handler/ESPHomeModuleHandlerFactory.java b/src/main/java/no/seime/openhab/binding/esphome/internal/module/handler/ESPHomeModuleHandlerFactory.java new file mode 100644 index 00000000..c53979f9 --- /dev/null +++ b/src/main/java/no/seime/openhab/binding/esphome/internal/module/handler/ESPHomeModuleHandlerFactory.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2023 Contributors to the Seime Openhab Addons 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 no.seime.openhab.binding.esphome.internal.module.handler; + +import java.util.Arrays; +import java.util.Collection; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.automation.Module; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.handler.BaseModuleHandlerFactory; +import org.openhab.core.automation.handler.ModuleHandler; +import org.openhab.core.automation.handler.ModuleHandlerFactory; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This HandlerFactory creates TimerTriggerHandlers to control items within the + * RuleManager. + * + * @author Christoph Knauf - Initial contribution + * @author Kai Kreuzer - added new module types + */ +@NonNullByDefault +@Component(immediate = true, service = ModuleHandlerFactory.class) +public class ESPHomeModuleHandlerFactory extends BaseModuleHandlerFactory { + + private final Logger logger = LoggerFactory.getLogger(ESPHomeModuleHandlerFactory.class); + + private static final Collection TYPES = Arrays.asList(ActionTriggerHandler.MODULE_TYPE_ID, + EventTriggerHandler.MODULE_TYPE_ID, TagScannedTriggerHandler.MODULE_TYPE_ID); + + private final BundleContext bundleContext; + + @Activate + public ESPHomeModuleHandlerFactory(final BundleContext bundleContext) { + this.bundleContext = bundleContext; + } + + @Override + @Deactivate + public void deactivate() { + super.deactivate(); + } + + @Override + public Collection getTypes() { + return TYPES; + } + + @Override + protected @Nullable ModuleHandler internalCreate(Module module, String ruleUID) { + logger.trace("create {} -> {}", module.getId(), module.getTypeUID()); + String moduleTypeUID = module.getTypeUID(); + if (module instanceof Trigger trigger) { + switch (moduleTypeUID) { + case ActionTriggerHandler.MODULE_TYPE_ID: + return new ActionTriggerHandler(trigger, bundleContext); + case EventTriggerHandler.MODULE_TYPE_ID: + return new EventTriggerHandler(trigger, bundleContext); + case TagScannedTriggerHandler.MODULE_TYPE_ID: + return new TagScannedTriggerHandler(trigger, bundleContext); + } + } + logger.error("The module handler type '{}' is not supported.", moduleTypeUID); + return null; + } +} diff --git a/src/main/java/no/seime/openhab/binding/esphome/internal/module/handler/EventTriggerHandler.java b/src/main/java/no/seime/openhab/binding/esphome/internal/module/handler/EventTriggerHandler.java new file mode 100644 index 00000000..72dbd9db --- /dev/null +++ b/src/main/java/no/seime/openhab/binding/esphome/internal/module/handler/EventTriggerHandler.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2023 Contributors to the Seime Openhab Addons 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 no.seime.openhab.binding.esphome.internal.module.handler; + +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.automation.ModuleHandlerCallback; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.handler.TriggerHandlerCallback; +import org.openhab.core.events.Event; +import org.osgi.framework.BundleContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import no.seime.openhab.binding.esphome.events.EventEvent; + +/** + * This is a ModuleHandler implementation for Triggers which triggers rules + * based on events sent from ESPHome devices + * + * @author Cody Cutrer - Initial contribution + */ +@NonNullByDefault +public class EventTriggerHandler extends AbstractESPHomeTriggerHandler { + public static final String MODULE_TYPE_ID = "esphome.EventTrigger"; + public static final String CONFIG_EVENT = "event"; + + private final Logger logger = LoggerFactory.getLogger(EventTriggerHandler.class); + + public EventTriggerHandler(Trigger module, BundleContext bundleContext) { + super(module, "openhab/esphome/event/", CONFIG_EVENT, bundleContext); + } + + @Override + public Set getSubscribedEventTypes() { + return Set.of(EventEvent.TYPE); + } + + @Override + public void receive(Event event) { + if (event instanceof EventEvent eventEvent && (eventEvent.getEvent().equals(action))) { + if (deviceId != null && !eventEvent.getSource().equals(deviceId)) { + // device is configured on the trigger, but doesn't match the event, so skip it + return; + } + ModuleHandlerCallback callback = this.callback; + if (callback instanceof TriggerHandlerCallback triggerHandlerCallback) { + triggerHandlerCallback.triggered(module, Map.of("event", event)); + } else { + logger.debug("Tried to trigger, but callback isn't available!"); + } + } + } +} diff --git a/src/main/java/no/seime/openhab/binding/esphome/internal/module/handler/TagScannedTriggerHandler.java b/src/main/java/no/seime/openhab/binding/esphome/internal/module/handler/TagScannedTriggerHandler.java new file mode 100644 index 00000000..de9f563c --- /dev/null +++ b/src/main/java/no/seime/openhab/binding/esphome/internal/module/handler/TagScannedTriggerHandler.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2023 Contributors to the Seime Openhab Addons 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 no.seime.openhab.binding.esphome.internal.module.handler; + +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.automation.ModuleHandlerCallback; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.handler.BaseTriggerModuleHandler; +import org.openhab.core.automation.handler.TriggerHandlerCallback; +import org.openhab.core.config.core.ConfigParser; +import org.openhab.core.events.Event; +import org.openhab.core.events.EventFilter; +import org.openhab.core.events.EventSubscriber; +import org.openhab.core.events.TopicPrefixEventFilter; +import org.openhab.core.scheduler.ScheduledCompletableFuture; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import no.seime.openhab.binding.esphome.events.TagScannedEvent; + +/** + * This is a ModuleHandler implementation for Triggers which triggers rules + * based on events sent from ESPHome devices + * + * @author Cody Cutrer - Initial contribution + */ +@NonNullByDefault +public class TagScannedTriggerHandler extends BaseTriggerModuleHandler implements EventSubscriber { + public static final String CONFIG_DEVICE_ID = "deviceId"; + + public static final String MODULE_TYPE_ID = "esphome.TagScannedTrigger"; + + private final Logger logger = LoggerFactory.getLogger(TagScannedTriggerHandler.class); + + private final @Nullable EventFilter eventFilter; + private @Nullable String deviceId = null; + + private @Nullable ScheduledCompletableFuture schedule; + private @Nullable ServiceRegistration eventSubscriberRegistration; + + public TagScannedTriggerHandler(Trigger module, BundleContext bundleContext) { + super(module); + this.eventFilter = new TopicPrefixEventFilter("openhab/esphome/tag_scanned"); + this.deviceId = ConfigParser.valueAs(module.getConfiguration().get(CONFIG_DEVICE_ID), String.class); + eventSubscriberRegistration = bundleContext.registerService(EventSubscriber.class.getName(), this, null); + } + + @Override + public void dispose() { + ServiceRegistration eventSubscriberRegistration = this.eventSubscriberRegistration; + if (eventSubscriberRegistration != null) { + eventSubscriberRegistration.unregister(); + this.eventSubscriberRegistration = null; + } + super.dispose(); + } + + @Override + public synchronized void setCallback(ModuleHandlerCallback callback) { + super.setCallback(callback); + } + + @Override + public Set getSubscribedEventTypes() { + return Set.of(TagScannedEvent.TYPE); + } + + @Override + public @Nullable EventFilter getEventFilter() { + return eventFilter; + } + + @Override + public void receive(Event event) { + if (event instanceof TagScannedEvent tagScannedEvent) { + if (deviceId != null && !tagScannedEvent.getSource().equals(deviceId)) { + // device is configured on the trigger, but doesn't match the event, so skip it + return; + } + ModuleHandlerCallback callback = this.callback; + if (callback instanceof TriggerHandlerCallback triggerHandlerCallback) { + triggerHandlerCallback.triggered(module, Map.of("event", event)); + } else { + logger.debug("Tried to trigger, but callback isn't available!"); + } + } + } +} diff --git a/src/main/resources/OH-INF/automation/moduletypes/ActionTrigger.json b/src/main/resources/OH-INF/automation/moduletypes/ActionTrigger.json new file mode 100644 index 00000000..f68b505e --- /dev/null +++ b/src/main/resources/OH-INF/automation/moduletypes/ActionTrigger.json @@ -0,0 +1,26 @@ +{ + "triggers": [ + { + "uid": "esphome.ActionTrigger", + "label": "an action is requested from an ESPHome device", + "description": "Triggers when the homeassistent.action action is called on a device", + "visibility": "PUBLIC", + "configDescriptions": [ + { + "name": "action", + "type": "TEXT", + "label": "Action", + "description": "the action to perform", + "required": true + }, + { + "name": "deviceId", + "type": "TEXT", + "label": "Device ID", + "description": "the device that requested the action", + "required": false + } + ] + } + ] +} diff --git a/src/main/resources/OH-INF/automation/moduletypes/EventTrigger.json b/src/main/resources/OH-INF/automation/moduletypes/EventTrigger.json new file mode 100644 index 00000000..7e6f9d8b --- /dev/null +++ b/src/main/resources/OH-INF/automation/moduletypes/EventTrigger.json @@ -0,0 +1,26 @@ +{ + "triggers": [ + { + "uid": "esphome.EventTrigger", + "label": "an event is triggered from an ESPHome device", + "description": "Triggers when the homeassistent.event action is called on a device", + "visibility": "PUBLIC", + "configDescriptions": [ + { + "name": "event", + "type": "TEXT", + "label": "Event", + "description": "the event", + "required": true + }, + { + "name": "deviceId", + "type": "TEXT", + "label": "Device ID", + "description": "the device that sent the event", + "required": false + } + ] + } + ] +} diff --git a/src/main/resources/OH-INF/automation/moduletypes/TagScannedTrigger.json b/src/main/resources/OH-INF/automation/moduletypes/TagScannedTrigger.json new file mode 100644 index 00000000..f4b6083b --- /dev/null +++ b/src/main/resources/OH-INF/automation/moduletypes/TagScannedTrigger.json @@ -0,0 +1,26 @@ +{ + "triggers": [ + { + "uid": "esphome.TagScannedTrigger", + "label": "a tag is scanned from an ESPHome device", + "description": "Triggers when the homeassistent.tag_scanned action is called on a device", + "visibility": "PUBLIC", + "configDescriptions": [ + { + "name": "tagId", + "type": "TEXT", + "label": "Tag", + "description": "the tag that was scanned", + "required": false + }, + { + "name": "deviceId", + "type": "TEXT", + "label": "Device ID", + "description": "the device that scanned the tag", + "required": false + } + ] + } + ] +} diff --git a/src/main/resources/OH-INF/thing/thing-esphome.xml b/src/main/resources/OH-INF/thing/thing-esphome.xml index 5fae426c..25f39a34 100644 --- a/src/main/resources/OH-INF/thing/thing-esphome.xml +++ b/src/main/resources/OH-INF/thing/thing-esphome.xml @@ -14,6 +14,9 @@ network-address + + + @@ -47,6 +50,14 @@ https://esphome.io/components/api#configuration-variables + + + Actions includes requesting actions be executed, sending events, and scanning tags. See + https://esphome.io/components/api/#api-actions + false + false + + 10 diff --git a/src/test/java/no/seime/openhab/binding/esphome/devicetest/AbstractESPHomeDeviceTest.java b/src/test/java/no/seime/openhab/binding/esphome/devicetest/AbstractESPHomeDeviceTest.java index 6226350b..22081930 100644 --- a/src/test/java/no/seime/openhab/binding/esphome/devicetest/AbstractESPHomeDeviceTest.java +++ b/src/test/java/no/seime/openhab/binding/esphome/devicetest/AbstractESPHomeDeviceTest.java @@ -17,6 +17,7 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.openhab.core.config.core.Configuration; +import org.openhab.core.events.EventPublisher; import org.openhab.core.items.GenericItem; import org.openhab.core.items.Item; import org.openhab.core.items.ItemNotFoundException; @@ -53,6 +54,7 @@ public abstract class AbstractESPHomeDeviceTest { protected ThingImpl thing; protected @Mock ItemRegistry itemRegistry; protected @Mock ThingRegistry thingRegistry; + protected @Mock EventPublisher eventPublisher; protected ESPHomeEventSubscriber eventSubscriber; private ESPHomeConfiguration deviceConfiguration; private ConnectionSelector selector; @@ -86,7 +88,7 @@ public void setUp() throws Exception { eventSubscriber = new ESPHomeEventSubscriber(thingRegistry, itemRegistry); thingHandler = new ESPHomeHandler(thing, selector, channelTypeProvider, stateDescriptionProvider, - eventSubscriber, executor, new KeySequentialExecutor(executor), null); + eventSubscriber, executor, new KeySequentialExecutor(executor), eventPublisher, null); thingHandlerCallback = Mockito.mock(ThingHandlerCallback.class); thingHandler.setCallback(thingHandlerCallback); } diff --git a/src/test/java/no/seime/openhab/binding/esphome/events/ESPHomeEventFactoryTest.java b/src/test/java/no/seime/openhab/binding/esphome/events/ESPHomeEventFactoryTest.java new file mode 100644 index 00000000..7a1a84cc --- /dev/null +++ b/src/test/java/no/seime/openhab/binding/esphome/events/ESPHomeEventFactoryTest.java @@ -0,0 +1,64 @@ +package no.seime.openhab.binding.esphome.events; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +public class ESPHomeEventFactoryTest { + + @Test + void createTagScannedEvent() { + TagScannedEvent event = ESPHomeEventFactory.createTagScannedEvent("device123", "tag456"); + assertEquals("openhab/esphome/tag_scanned", event.getTopic()); + assertEquals("tag456", event.getTagId()); + assertEquals("device123", event.getSource()); + } + + @Test + void createActionEvent() { + Map data = new HashMap<>(); + data.put("key1", "value1"); + data.put("key2", "value2"); + + Map dataTemplate = new HashMap<>(); + dataTemplate.put("template1", "templateValue1"); + + Map variables = new HashMap<>(); + variables.put("var1", "varValue1"); + + ActionEvent event = ESPHomeEventFactory.createActionEvent("testDevice", "testAction", data, dataTemplate, + variables); + + assertEquals("openhab/esphome/action/testAction", event.getTopic()); + assertEquals("testDevice", event.getSource()); + assertEquals("testAction", event.getAction()); + assertEquals(data, event.getData()); + assertEquals(dataTemplate, event.getDataTemplate()); + assertEquals(variables, event.getVariables()); + } + + @Test + void createEventEvent() { + Map data = new HashMap<>(); + data.put("eventKey", "eventValue"); + + Map dataTemplate = new HashMap<>(); + dataTemplate.put("eventTemplate", "eventTemplateValue"); + + Map variables = new HashMap<>(); + variables.put("eventVar", "eventVarValue"); + + EventEvent event = ESPHomeEventFactory.createEventEvent("testEventDevice", "testEventAction", data, + dataTemplate, variables); + + assertEquals("openhab/esphome/event/testEventAction", event.getTopic()); + assertEquals("testEventDevice", event.getSource()); + assertEquals("testEventAction", event.getEvent()); + assertEquals(data, event.getData()); + assertEquals(dataTemplate, event.getDataTemplate()); + assertEquals(variables, event.getVariables()); + } +} diff --git a/triggers.png b/triggers.png new file mode 100644 index 0000000000000000000000000000000000000000..5c31b4854228fc710d14f252ce17be311a6a0632 GIT binary patch literal 95726 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

`M(I1tN1HXdjJr{mI%1CxZ>rh!mHHEor=r1oc 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 literal 0 HcmV?d00001 From 38a2012982aa5ab214dbe8c1081c1b7732828da2 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Sun, 5 Oct 2025 18:54:52 -0600 Subject: [PATCH 2/5] Add actions device test Signed-off-by: Cody Cutrer --- .../devicetest/AbstractESPHomeDeviceTest.java | 2 +- .../devicetest/ActionsEsphomeDeviceTest.java | 63 +++++++++++++++++++ .../device_configurations/actions.yaml | 30 +++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 src/test/java/no/seime/openhab/binding/esphome/devicetest/ActionsEsphomeDeviceTest.java create mode 100644 src/test/resources/device_configurations/actions.yaml diff --git a/src/test/java/no/seime/openhab/binding/esphome/devicetest/AbstractESPHomeDeviceTest.java b/src/test/java/no/seime/openhab/binding/esphome/devicetest/AbstractESPHomeDeviceTest.java index 22081930..b0be3cd4 100644 --- a/src/test/java/no/seime/openhab/binding/esphome/devicetest/AbstractESPHomeDeviceTest.java +++ b/src/test/java/no/seime/openhab/binding/esphome/devicetest/AbstractESPHomeDeviceTest.java @@ -56,7 +56,7 @@ public abstract class AbstractESPHomeDeviceTest { protected @Mock ThingRegistry thingRegistry; protected @Mock EventPublisher eventPublisher; protected ESPHomeEventSubscriber eventSubscriber; - private ESPHomeConfiguration deviceConfiguration; + protected ESPHomeConfiguration deviceConfiguration; private ConnectionSelector selector; private @Mock Configuration configuration; private @Mock ESPChannelTypeProvider channelTypeProvider; diff --git a/src/test/java/no/seime/openhab/binding/esphome/devicetest/ActionsEsphomeDeviceTest.java b/src/test/java/no/seime/openhab/binding/esphome/devicetest/ActionsEsphomeDeviceTest.java new file mode 100644 index 00000000..64fc34f1 --- /dev/null +++ b/src/test/java/no/seime/openhab/binding/esphome/devicetest/ActionsEsphomeDeviceTest.java @@ -0,0 +1,63 @@ +package no.seime.openhab.binding.esphome.devicetest; + +import static org.awaitility.Awaitility.await; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +import java.io.File; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.openhab.core.items.ItemNotFoundException; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.*; + +import no.seime.openhab.binding.esphome.events.ESPHomeEventFactory; + +public class ActionsEsphomeDeviceTest extends AbstractESPHomeDeviceTest { + + protected File getEspDeviceConfigurationYamlFileName() { + return new File("src/test/resources/device_configurations/actions.yaml"); + } + + @Test + public void testAction() throws ItemNotFoundException { + deviceConfiguration.allowActions = true; + thingHandler.initialize(); + await().until(() -> thingHandler.isInterrogated()); + + thingHandler.handleCommand(new ChannelUID(thing.getUID(), "trigger_action"), OnOffType.ON); + + verify(eventPublisher, timeout(2000)).post(ESPHomeEventFactory.createActionEvent("virtual", "some.action", + Map.of("entity_id", "Something"), Map.of(), Map.of())); + + thingHandler.dispose(); + } + + @Test + public void testEvent() throws ItemNotFoundException { + deviceConfiguration.allowActions = true; + thingHandler.initialize(); + await().until(() -> thingHandler.isInterrogated()); + + thingHandler.handleCommand(new ChannelUID(thing.getUID(), "trigger_event"), OnOffType.ON); + + verify(eventPublisher, timeout(2000)).post( + ESPHomeEventFactory.createEventEvent("virtual", "esphome.something", Map.of(), Map.of(), Map.of())); + + thingHandler.dispose(); + } + + @Test + public void testTag() throws ItemNotFoundException { + deviceConfiguration.allowActions = true; + thingHandler.initialize(); + await().until(() -> thingHandler.isInterrogated()); + + thingHandler.handleCommand(new ChannelUID(thing.getUID(), "trigger_tag"), OnOffType.ON); + + verify(eventPublisher, timeout(2000)).post(ESPHomeEventFactory.createTagScannedEvent("virtual", "mytag")); + + thingHandler.dispose(); + } +} diff --git a/src/test/resources/device_configurations/actions.yaml b/src/test/resources/device_configurations/actions.yaml new file mode 100644 index 00000000..d207d764 --- /dev/null +++ b/src/test/resources/device_configurations/actions.yaml @@ -0,0 +1,30 @@ +esphome: + name: virtual + +host: + mac_address: "06:35:69:ab:f6:79" + +logger: + level: DEBUG + +api: + encryption: + key: !secret emulator_encryption_key + +button: + - platform: template + name: "Trigger Action" + on_press: + - homeassistant.action: + action: some.action + data: + entity_id: Something + - platform: template + name: "Trigger Event" + on_press: + - homeassistant.event: + event: esphome.something + - platform: template + name: "Trigger Tag" + on_press: + - homeassistant.tag_scanned: mytag From 1d8392d34928abb1e13239bddbf696e645e6b301 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Sun, 5 Oct 2025 18:58:26 -0600 Subject: [PATCH 3/5] adjust source to prefix it with the package per discussion in openhab-core Signed-off-by: Cody Cutrer --- README.md | 2 +- .../binding/esphome/events/ESPHomeEventFactory.java | 7 ++++--- .../binding/esphome/events/ESPHomeEventFactoryTest.java | 6 +++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 16ba8ef3..26833143 100644 --- a/README.md +++ b/README.md @@ -330,7 +330,7 @@ To process actions and events sent via the [Native API Component's actions](http Be sure to enable `allowActions` in the Thing configuration so that openHAB will request the device to send events. The event object has `getData`, `getDataTemplate`, and `getVariables` methods to access the appropriate information for action and event events. For tag scanned events, the event's payload is the tag ID. -The event's source is the ESPHome device ID that sent the event. +The event's source will be of the form `no.seime.openhab.binding.openhab$`, where device_id is the ESPHome device ID that sent the event. Unfortunately, these triggers are not available from Rules DSL, but some other openHAB automation languages may support setting triggers based on any event sent through the [event bus](https://www.openhab.org/docs/developer/utils/events.html). To listen for these events, they look like this: diff --git a/src/main/java/no/seime/openhab/binding/esphome/events/ESPHomeEventFactory.java b/src/main/java/no/seime/openhab/binding/esphome/events/ESPHomeEventFactory.java index 1a429b84..ad3ea85e 100644 --- a/src/main/java/no/seime/openhab/binding/esphome/events/ESPHomeEventFactory.java +++ b/src/main/java/no/seime/openhab/binding/esphome/events/ESPHomeEventFactory.java @@ -36,6 +36,7 @@ public class ESPHomeEventFactory extends AbstractEventFactory { private static final String ACTION_EVENT_TOPIC = "openhab/esphome/action/" + ACTION_IDENTIFIER; private static final String EVENT_EVENT_TOPIC = "openhab/esphome/event/" + ACTION_IDENTIFIER; private static final String TAG_SCANNED_EVENT_TOPIC = "openhab/esphome/tag_scanned"; + private static final String SOURCE_PREFIX = "no.seime.openhab.binding.esphome$"; private final Logger logger = LoggerFactory.getLogger(ESPHomeEventFactory.class); @@ -100,7 +101,7 @@ public static ActionEvent createActionEvent(String deviceId, String action, Map< String topic = ACTION_EVENT_TOPIC.replace(ACTION_IDENTIFIER, action); ActionEventPayloadBean bean = new ActionEventPayloadBean(data, data_template, variables); String payload = serializePayload(bean); - return new ActionEvent(topic, payload, deviceId, action, data, data_template, variables); + return new ActionEvent(topic, payload, SOURCE_PREFIX + deviceId, action, data, data_template, variables); } /** @@ -116,11 +117,11 @@ public static EventEvent createEventEvent(String deviceId, String action, Map Date: Sun, 5 Oct 2025 19:30:29 -0600 Subject: [PATCH 4/5] correct spelling of homeassistant in trigger docs Signed-off-by: Cody Cutrer --- .../resources/OH-INF/automation/moduletypes/ActionTrigger.json | 2 +- .../resources/OH-INF/automation/moduletypes/EventTrigger.json | 2 +- .../OH-INF/automation/moduletypes/TagScannedTrigger.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/resources/OH-INF/automation/moduletypes/ActionTrigger.json b/src/main/resources/OH-INF/automation/moduletypes/ActionTrigger.json index f68b505e..ffaba61a 100644 --- a/src/main/resources/OH-INF/automation/moduletypes/ActionTrigger.json +++ b/src/main/resources/OH-INF/automation/moduletypes/ActionTrigger.json @@ -3,7 +3,7 @@ { "uid": "esphome.ActionTrigger", "label": "an action is requested from an ESPHome device", - "description": "Triggers when the homeassistent.action action is called on a device", + "description": "Triggers when the homeassistant.action action is called on a device", "visibility": "PUBLIC", "configDescriptions": [ { diff --git a/src/main/resources/OH-INF/automation/moduletypes/EventTrigger.json b/src/main/resources/OH-INF/automation/moduletypes/EventTrigger.json index 7e6f9d8b..91c1733b 100644 --- a/src/main/resources/OH-INF/automation/moduletypes/EventTrigger.json +++ b/src/main/resources/OH-INF/automation/moduletypes/EventTrigger.json @@ -3,7 +3,7 @@ { "uid": "esphome.EventTrigger", "label": "an event is triggered from an ESPHome device", - "description": "Triggers when the homeassistent.event action is called on a device", + "description": "Triggers when the homeassistant.event action is called on a device", "visibility": "PUBLIC", "configDescriptions": [ { diff --git a/src/main/resources/OH-INF/automation/moduletypes/TagScannedTrigger.json b/src/main/resources/OH-INF/automation/moduletypes/TagScannedTrigger.json index f4b6083b..5b20f3ad 100644 --- a/src/main/resources/OH-INF/automation/moduletypes/TagScannedTrigger.json +++ b/src/main/resources/OH-INF/automation/moduletypes/TagScannedTrigger.json @@ -3,7 +3,7 @@ { "uid": "esphome.TagScannedTrigger", "label": "a tag is scanned from an ESPHome device", - "description": "Triggers when the homeassistent.tag_scanned action is called on a device", + "description": "Triggers when the homeassistant.tag_scanned action is called on a device", "visibility": "PUBLIC", "configDescriptions": [ { From 9936abcf0be81ddc961c71373784fc7e87aadb7d Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Sun, 5 Oct 2025 19:33:54 -0600 Subject: [PATCH 5/5] correct javadocs in EventFactory Signed-off-by: Cody Cutrer --- .../esphome/events/ESPHomeEventFactory.java | 31 +++++++++++++------ .../binding/esphome/events/EventEvent.java | 4 +-- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/main/java/no/seime/openhab/binding/esphome/events/ESPHomeEventFactory.java b/src/main/java/no/seime/openhab/binding/esphome/events/ESPHomeEventFactory.java index ad3ea85e..b77d5c85 100644 --- a/src/main/java/no/seime/openhab/binding/esphome/events/ESPHomeEventFactory.java +++ b/src/main/java/no/seime/openhab/binding/esphome/events/ESPHomeEventFactory.java @@ -91,9 +91,11 @@ private String getAction(String topic) { /** * Creates an {@link ActionEvent} * - * @param moduleId the module type id of this event - * @param label The label (or id) of this object - * @param configuration the configuration of the trigger + * @param deviceId the device requesting the action + * @param action the action to perform + * @param data the data for the action + * @param data_template the data template for the action + * @param variables variables for the use in the templates * @return the created event */ public static ActionEvent createActionEvent(String deviceId, String action, Map data, @@ -105,21 +107,30 @@ public static ActionEvent createActionEvent(String deviceId, String action, Map< } /** - * Creates an {@link ActionEvent} + * Creates an {@link EventEvent} * - * @param moduleId the module type id of this event - * @param label The label (or id) of this object - * @param configuration the configuration of the trigger + * @param deviceId the device emitting the event + * @param event the event identifier + * @param data the data for the action + * @param data_template the data template for the action + * @param variables variables for the use in the templates * @return the created event */ - public static EventEvent createEventEvent(String deviceId, String action, Map data, + public static EventEvent createEventEvent(String deviceId, String event, Map data, Map data_template, Map variables) { - String topic = EVENT_EVENT_TOPIC.replace(ACTION_IDENTIFIER, action); + String topic = EVENT_EVENT_TOPIC.replace(ACTION_IDENTIFIER, event); ActionEventPayloadBean bean = new ActionEventPayloadBean(data, data_template, variables); String payload = serializePayload(bean); - return new EventEvent(topic, payload, SOURCE_PREFIX + deviceId, action, data, data_template, variables); + return new EventEvent(topic, payload, SOURCE_PREFIX + deviceId, event, data, data_template, variables); } + /** + * Creates a {@link TagScannedEvent} + * + * @param deviceId the device emitting the event + * @param tagId the tag identifier + * @return the created event + */ public static TagScannedEvent createTagScannedEvent(String deviceId, String tagId) { return new TagScannedEvent(TAG_SCANNED_EVENT_TOPIC, tagId, SOURCE_PREFIX + deviceId); } diff --git a/src/main/java/no/seime/openhab/binding/esphome/events/EventEvent.java b/src/main/java/no/seime/openhab/binding/esphome/events/EventEvent.java index b06be690..b991a7cc 100644 --- a/src/main/java/no/seime/openhab/binding/esphome/events/EventEvent.java +++ b/src/main/java/no/seime/openhab/binding/esphome/events/EventEvent.java @@ -25,9 +25,9 @@ public class EventEvent extends AbstractESPHomeEvent { public static final String TYPE = "esphome.EventEvent"; - public EventEvent(String topic, String payload, String deviceId, String action, Map data, + public EventEvent(String topic, String payload, String deviceId, String event, Map data, Map data_template, Map variables) { - super(topic, payload, deviceId, action, data, data_template, variables); + super(topic, payload, deviceId, event, data, data_template, variables); } @Override