Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <BASE64ENCODEDKEY>`. 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 |
Expand Down Expand Up @@ -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$<device_id>`, 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/<action>` | `esphome.ActionEvent` | JSON object with "data", "data_template", and "variables" sub-objects. |
| `homeassistant.event` | `openhab/esphome/event/<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:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Copyright (c) 2023 Contributors to the Seime Openhab Addons project
* <p>
* See the NOTICE file(s) distributed with this work for additional
* information.
* <p>
* 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
* <p>
* 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<String, String> data;
private final Map<String, String> data_template;
private final Map<String, String> variables;

public AbstractESPHomeEvent(String topic, String payload, String deviceId, String action, Map<String, String> data,
Map<String, String> data_template, Map<String, String> variables) {
super(topic, payload, deviceId);
this.action = action;
this.data = data;
this.data_template = data_template;
this.variables = variables;
}

public Map<String, String> getData() {
return data;
}

public Map<String, String> getDataTemplate() {
return data_template;
}

public Map<String, String> getVariables() {
return variables;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Copyright (c) 2023 Contributors to the Seime Openhab Addons project
* <p>
* See the NOTICE file(s) distributed with this work for additional
* information.
* <p>
* 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
* <p>
* 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<String, String> data,
Map<String, String> data_template, Map<String, String> 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/**
* Copyright (c) 2023 Contributors to the Seime Openhab Addons project
* <p>
* See the NOTICE file(s) distributed with this work for additional
* information.
* <p>
* 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
* <p>
* 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<String> 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<String, String> data,
Map<String, String> data_template, Map<String, String> 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<String, String> data,
Map<String, String> data_template, Map<String, String> 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<String, String> data;
private @NonNullByDefault({}) Map<String, String> data_template;
private @NonNullByDefault({}) Map<String, String> variables;

/**
* Default constructor for deserialization e.g. by Gson.
*/
@SuppressWarnings("unused")
protected ActionEventPayloadBean() {
}

public ActionEventPayloadBean(Map<String, String> data, Map<String, String> data_template,
Map<String, String> variables) {
this.data = data;
this.data_template = data_template;
this.variables = variables;
}

public Map<String, String> getData() {
return data;
}

public Map<String, String> getDataTemplate() {
return data_template;
}

public Map<String, String> getVariables() {
return variables;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Copyright (c) 2023 Contributors to the Seime Openhab Addons project
* <p>
* See the NOTICE file(s) distributed with this work for additional
* information.
* <p>
* 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
* <p>
* 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<String, String> data,
Map<String, String> data_template, Map<String, String> 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);
}
}
Loading