mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[mqtt.homeassistant] Add support for Event component (#17599)
Signed-off-by: Cody Cutrer <cody@cutrer.us> Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
parent
356aa13fb7
commit
d135e24fe4
@ -21,6 +21,7 @@ You can also manually create a Thing, and provide the individual component topic
|
|||||||
- [Climate](https://www.home-assistant.io/integrations/climate.mqtt/)
|
- [Climate](https://www.home-assistant.io/integrations/climate.mqtt/)
|
||||||
- [Cover](https://www.home-assistant.io/integrations/cover.mqtt/)
|
- [Cover](https://www.home-assistant.io/integrations/cover.mqtt/)
|
||||||
- [Device Trigger](https://www.home-assistant.io/integrations/device_trigger.mqtt/)
|
- [Device Trigger](https://www.home-assistant.io/integrations/device_trigger.mqtt/)
|
||||||
|
- [Event](https://www.home-assistant.io/integrations/event.mqtt/)
|
||||||
- [Fan](https://www.home-assistant.io/integrations/fan.mqtt/)
|
- [Fan](https://www.home-assistant.io/integrations/fan.mqtt/)
|
||||||
- [Light](https://www.home-assistant.io/integrations/light.mqtt/)
|
- [Light](https://www.home-assistant.io/integrations/light.mqtt/)
|
||||||
- [Lock](https://www.home-assistant.io/integrations/lock.mqtt/)
|
- [Lock](https://www.home-assistant.io/integrations/lock.mqtt/)
|
||||||
|
@ -59,14 +59,16 @@ public class ComponentFactory {
|
|||||||
return new Button(componentConfiguration, newStyleChannels);
|
return new Button(componentConfiguration, newStyleChannels);
|
||||||
case "camera":
|
case "camera":
|
||||||
return new Camera(componentConfiguration, newStyleChannels);
|
return new Camera(componentConfiguration, newStyleChannels);
|
||||||
case "cover":
|
|
||||||
return new Cover(componentConfiguration, newStyleChannels);
|
|
||||||
case "fan":
|
|
||||||
return new Fan(componentConfiguration, newStyleChannels);
|
|
||||||
case "climate":
|
case "climate":
|
||||||
return new Climate(componentConfiguration, newStyleChannels);
|
return new Climate(componentConfiguration, newStyleChannels);
|
||||||
|
case "cover":
|
||||||
|
return new Cover(componentConfiguration, newStyleChannels);
|
||||||
case "device_automation":
|
case "device_automation":
|
||||||
return new DeviceTrigger(componentConfiguration, newStyleChannels);
|
return new DeviceTrigger(componentConfiguration, newStyleChannels);
|
||||||
|
case "event":
|
||||||
|
return new Event(componentConfiguration, newStyleChannels);
|
||||||
|
case "fan":
|
||||||
|
return new Fan(componentConfiguration, newStyleChannels);
|
||||||
case "light":
|
case "light":
|
||||||
return Light.create(componentConfiguration, newStyleChannels);
|
return Light.create(componentConfiguration, newStyleChannels);
|
||||||
case "lock":
|
case "lock":
|
||||||
|
@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.mqtt.homeassistant.internal.component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
|
||||||
|
import org.openhab.binding.mqtt.generic.values.TextValue;
|
||||||
|
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
|
||||||
|
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantChannelTransformation;
|
||||||
|
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
|
||||||
|
import org.openhab.core.thing.ChannelUID;
|
||||||
|
import org.openhab.core.types.Command;
|
||||||
|
import org.openhab.core.types.State;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A MQTT Event, following the https://www.home-assistant.io/integrations/event.mqttspecification.
|
||||||
|
*
|
||||||
|
* @author Cody Cutrer - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class Event extends AbstractComponent<Event.ChannelConfiguration> implements ChannelStateUpdateListener {
|
||||||
|
public static final String EVENT_TYPE_CHANNEL_ID = "event-type";
|
||||||
|
public static final String JSON_ATTRIBUTES_CHANNEL_ID = "json-attributes";
|
||||||
|
private static final String EVENT_TYPE_TRANFORMATION = "{{ value_json.event_type }}";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration class for MQTT component
|
||||||
|
*/
|
||||||
|
public static class ChannelConfiguration extends AbstractChannelConfiguration {
|
||||||
|
ChannelConfiguration() {
|
||||||
|
super("MQTT Event");
|
||||||
|
}
|
||||||
|
|
||||||
|
@SerializedName("state_topic")
|
||||||
|
protected String stateTopic = "";
|
||||||
|
|
||||||
|
@SerializedName("event_types")
|
||||||
|
protected List<String> eventTypes = new ArrayList();
|
||||||
|
|
||||||
|
@SerializedName("json_attributes_topic")
|
||||||
|
protected @Nullable String jsonAttributesTopic;
|
||||||
|
|
||||||
|
@SerializedName("json_attributes_template")
|
||||||
|
protected @Nullable String jsonAttributesTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final HomeAssistantChannelTransformation transformation;
|
||||||
|
|
||||||
|
public Event(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
|
||||||
|
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
|
||||||
|
|
||||||
|
transformation = new HomeAssistantChannelTransformation(getJinjava(), this, "");
|
||||||
|
|
||||||
|
buildChannel(EVENT_TYPE_CHANNEL_ID, ComponentChannelType.TRIGGER, new TextValue(), getName(), this)
|
||||||
|
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate()).trigger(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
if (channelConfiguration.jsonAttributesTopic != null) {
|
||||||
|
// It's unclear from the documentation if the JSON attributes value is expected
|
||||||
|
// to be the same as the main topic, and thus would always have an event_type
|
||||||
|
// attribute (and thus could possibly be shared with multiple components).
|
||||||
|
// If that were the case, we would need to intercept events, and check that they
|
||||||
|
// had an event_type that is in channelConfiguration.eventTypes. If/when that
|
||||||
|
// becomes an issue, change `channelStateUpdateListener` to `this`, and handle
|
||||||
|
// the filtering below.
|
||||||
|
buildChannel(JSON_ATTRIBUTES_CHANNEL_ID, ComponentChannelType.TRIGGER, new TextValue(), getName(),
|
||||||
|
componentConfiguration.getUpdateListener())
|
||||||
|
.stateTopic(channelConfiguration.jsonAttributesTopic, channelConfiguration.jsonAttributesTemplate)
|
||||||
|
.trigger(true).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
finalizeChannels();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void triggerChannel(ChannelUID channel, String event) {
|
||||||
|
String eventType = transformation.apply(EVENT_TYPE_TRANFORMATION, event).orElse(null);
|
||||||
|
if (eventType == null) {
|
||||||
|
// Warning logged from inside the transformation
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The TextValue allows anything, because it receives the full JSON, and
|
||||||
|
// we don't check the actual event_type against valid event_types until here
|
||||||
|
if (!channelConfiguration.eventTypes.contains(eventType)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentConfiguration.getUpdateListener().triggerChannel(channel, eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateChannelState(ChannelUID channel, State state) {
|
||||||
|
// N/A (only trigger channels)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postChannelCommand(ChannelUID channel, Command command) {
|
||||||
|
// N/A (only trigger channels)
|
||||||
|
}
|
||||||
|
}
|
@ -304,12 +304,13 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add component to the component map
|
// Add component to the component map
|
||||||
addComponent(discovered);
|
if (addComponent(discovered)) {
|
||||||
// Start component / Subscribe to channel topics
|
// Start component / Subscribe to channel topics
|
||||||
discovered.start(connection, scheduler, 0).exceptionally(e -> {
|
discovered.start(connection, scheduler, 0).exceptionally(e -> {
|
||||||
logger.warn("Failed to start component {}", discovered.getHaID(), e);
|
logger.warn("Failed to start component {}", discovered.getHaID(), e);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (discovered instanceof Update) {
|
if (discovered instanceof Update) {
|
||||||
updateComponent = (Update) discovered;
|
updateComponent = (Update) discovered;
|
||||||
@ -427,7 +428,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
// should only be called when it's safe to access haComponents
|
// should only be called when it's safe to access haComponents
|
||||||
private void addComponent(AbstractComponent component) {
|
private boolean addComponent(AbstractComponent component) {
|
||||||
AbstractComponent existing = haComponents.get(component.getComponentId());
|
AbstractComponent existing = haComponents.get(component.getComponentId());
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
// DeviceTriggers that are for the same subtype, topic, and value template
|
// DeviceTriggers that are for the same subtype, topic, and value template
|
||||||
@ -454,8 +455,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
haComponentsByUniqueId.put(component.getUniqueId(), component);
|
haComponentsByUniqueId.put(component.getUniqueId(), component);
|
||||||
System.out.println("don't forget to add to the channel config");
|
return false;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -467,6 +467,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
|||||||
}
|
}
|
||||||
haComponents.put(component.getComponentId(), component);
|
haComponents.put(component.getComponentId(), component);
|
||||||
haComponentsByUniqueId.put(component.getUniqueId(), component);
|
haComponentsByUniqueId.put(component.getUniqueId(), component);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -233,18 +233,13 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void spyOnChannelUpdates(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
|
|
||||||
String channelId) {
|
|
||||||
// It's already thingHandler, but not the spy version
|
|
||||||
component.getChannel(channelId).getState().setChannelStateUpdateListener(thingHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert a channel triggers
|
* Assert a channel triggers
|
||||||
*/
|
*/
|
||||||
protected void assertTriggered(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
|
protected void assertTriggered(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
|
||||||
String channelId, String trigger) {
|
String channelId, String trigger) {
|
||||||
verify(thingHandler).triggerChannel(eq(component.getChannel(channelId).getChannel().getUID()), eq(trigger));
|
verify(callbackMock).channelTriggered(eq(haThing), eq(component.getChannel(channelId).getChannel().getUID()),
|
||||||
|
eq(trigger));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -252,8 +247,8 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
|
|||||||
*/
|
*/
|
||||||
protected void assertNotTriggered(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
|
protected void assertNotTriggered(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
|
||||||
String channelId, String trigger) {
|
String channelId, String trigger) {
|
||||||
verify(thingHandler, never()).triggerChannel(eq(component.getChannel(channelId).getChannel().getUID()),
|
verify(callbackMock, never()).channelTriggered(eq(haThing),
|
||||||
eq(trigger));
|
eq(component.getChannel(channelId).getChannel().getUID()), eq(trigger));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -66,7 +66,6 @@ public class DeviceTriggerTests extends AbstractComponentTests {
|
|||||||
assertChannel(component, "on", "zigbee2mqtt/Charge Now Button/action", "", "MQTT Device Trigger",
|
assertChannel(component, "on", "zigbee2mqtt/Charge Now Button/action", "", "MQTT Device Trigger",
|
||||||
TextValue.class);
|
TextValue.class);
|
||||||
|
|
||||||
spyOnChannelUpdates(component, "on");
|
|
||||||
publishMessage("zigbee2mqtt/Charge Now Button/action", "on");
|
publishMessage("zigbee2mqtt/Charge Now Button/action", "on");
|
||||||
assertTriggered(component, "on", "on");
|
assertTriggered(component, "on", "on");
|
||||||
|
|
||||||
@ -132,7 +131,6 @@ public class DeviceTriggerTests extends AbstractComponentTests {
|
|||||||
List<?> configList = (List<?>) config;
|
List<?> configList = (List<?>) config;
|
||||||
assertThat(configList.size(), is(2));
|
assertThat(configList.size(), is(2));
|
||||||
|
|
||||||
spyOnChannelUpdates(component1, "turn_on");
|
|
||||||
publishMessage("zigbee2mqtt/Charge Now Button/action", "press");
|
publishMessage("zigbee2mqtt/Charge Now Button/action", "press");
|
||||||
assertTriggered(component1, "turn_on", "press");
|
assertTriggered(component1, "turn_on", "press");
|
||||||
|
|
||||||
|
@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.mqtt.homeassistant.internal.component;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.openhab.binding.mqtt.generic.values.TextValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link Event}
|
||||||
|
*
|
||||||
|
* @author Cody Cutrer - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class EventTests extends AbstractComponentTests {
|
||||||
|
public static final String CONFIG_TOPIC = "event/doorbell/action";
|
||||||
|
|
||||||
|
@SuppressWarnings("null")
|
||||||
|
@Test
|
||||||
|
public void test() throws InterruptedException {
|
||||||
|
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
|
||||||
|
{
|
||||||
|
"event_types": [
|
||||||
|
"press",
|
||||||
|
"release"
|
||||||
|
],
|
||||||
|
"state_topic": "zigbee2mqtt/doorbell/action"
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
assertThat(component.channels.size(), is(1));
|
||||||
|
assertThat(component.getName(), is("MQTT Event"));
|
||||||
|
|
||||||
|
assertChannel(component, "event-type", "zigbee2mqtt/doorbell/action", "", "MQTT Event", TextValue.class);
|
||||||
|
|
||||||
|
publishMessage("zigbee2mqtt/doorbell/action", "{ \"event_type\": \"press\" }");
|
||||||
|
assertTriggered(component, "event-type", "press");
|
||||||
|
|
||||||
|
publishMessage("zigbee2mqtt/doorbell/action", "{ \"event_type\": \"release\" }");
|
||||||
|
assertTriggered(component, "event-type", "release");
|
||||||
|
|
||||||
|
publishMessage("zigbee2mqtt/doorbell/action", "{ \"event_type\": \"else\" }");
|
||||||
|
assertNotTriggered(component, "event-type", "else");
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("null")
|
||||||
|
@Test
|
||||||
|
public void testJsonAttributes() throws InterruptedException {
|
||||||
|
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
|
||||||
|
{
|
||||||
|
"event_types": [
|
||||||
|
"press",
|
||||||
|
"release"
|
||||||
|
],
|
||||||
|
"state_topic": "zigbee2mqtt/doorbell/action",
|
||||||
|
"json_attributes_topic": "zigbee2mqtt/doorbell/action"
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
assertThat(component.channels.size(), is(2));
|
||||||
|
assertThat(component.getName(), is("MQTT Event"));
|
||||||
|
|
||||||
|
assertChannel(component, "event-type", "zigbee2mqtt/doorbell/action", "", "MQTT Event", TextValue.class);
|
||||||
|
assertChannel(component, "json-attributes", "zigbee2mqtt/doorbell/action", "", "MQTT Event", TextValue.class);
|
||||||
|
|
||||||
|
publishMessage("zigbee2mqtt/doorbell/action", "{ \"event_type\": \"press\" }");
|
||||||
|
assertTriggered(component, "json-attributes", "{ \"event_type\": \"press\" }");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Set<String> getConfigTopics() {
|
||||||
|
return Set.of(CONFIG_TOPIC);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user