diff --git a/README.md b/README.md index fbc726fd..26833143 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 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: + +| 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..b77d5c85 --- /dev/null +++ b/src/main/java/no/seime/openhab/binding/esphome/events/ESPHomeEventFactory.java @@ -0,0 +1,172 @@ +/** + * 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 static final String SOURCE_PREFIX = "no.seime.openhab.binding.esphome$"; + + 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 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, + 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, SOURCE_PREFIX + deviceId, action, data, data_template, variables); + } + + /** + * Creates an {@link EventEvent} + * + * @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 event, Map data, + Map data_template, Map variables) { + 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, 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); + } + + /** + * 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..b991a7cc --- /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 event, Map data, + Map data_template, Map variables) { + super(topic, payload, deviceId, event, 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..ffaba61a --- /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 homeassistant.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..91c1733b --- /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 homeassistant.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..5b20f3ad --- /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 homeassistant.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..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 @@ -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,8 +54,9 @@ 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; + protected ESPHomeConfiguration deviceConfiguration; private ConnectionSelector selector; private @Mock Configuration configuration; private @Mock ESPChannelTypeProvider channelTypeProvider; @@ -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/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/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..30876e60 --- /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("no.seime.openhab.binding.esphome$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("no.seime.openhab.binding.esphome$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("no.seime.openhab.binding.esphome$testEventDevice", event.getSource()); + assertEquals("testEventAction", event.getEvent()); + assertEquals(data, event.getData()); + assertEquals(dataTemplate, event.getDataTemplate()); + assertEquals(variables, event.getVariables()); + } +} 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 diff --git a/triggers.png b/triggers.png new file mode 100644 index 00000000..5c31b485 Binary files /dev/null and b/triggers.png differ