- [Climate](https://www.home-assistant.io/integrations/climate.mqtt/)
- [Cover](https://www.home-assistant.io/integrations/cover.mqtt/)
- [Device Trigger](https://www.home-assistant.io/integrations/device_trigger.mqtt/)
+- [Event](https://www.home-assistant.io/integrations/event.mqtt/)
- [Fan](https://www.home-assistant.io/integrations/fan.mqtt/)
- [Light](https://www.home-assistant.io/integrations/light.mqtt/)
- [Lock](https://www.home-assistant.io/integrations/lock.mqtt/)
return new Button(componentConfiguration, newStyleChannels);
case "camera":
return new Camera(componentConfiguration, newStyleChannels);
- case "cover":
- return new Cover(componentConfiguration, newStyleChannels);
- case "fan":
- return new Fan(componentConfiguration, newStyleChannels);
case "climate":
return new Climate(componentConfiguration, newStyleChannels);
+ case "cover":
+ return new Cover(componentConfiguration, newStyleChannels);
case "device_automation":
return new DeviceTrigger(componentConfiguration, newStyleChannels);
+ case "event":
+ return new Event(componentConfiguration, newStyleChannels);
+ case "fan":
+ return new Fan(componentConfiguration, newStyleChannels);
case "light":
return Light.create(componentConfiguration, newStyleChannels);
case "lock":
--- /dev/null
+/**
+ * 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)
+ }
+}
}
// Add component to the component map
- addComponent(discovered);
- // Start component / Subscribe to channel topics
- discovered.start(connection, scheduler, 0).exceptionally(e -> {
- logger.warn("Failed to start component {}", discovered.getHaID(), e);
- return null;
- });
+ if (addComponent(discovered)) {
+ // Start component / Subscribe to channel topics
+ discovered.start(connection, scheduler, 0).exceptionally(e -> {
+ logger.warn("Failed to start component {}", discovered.getHaID(), e);
+ return null;
+ });
+ }
if (discovered instanceof Update) {
updateComponent = (Update) discovered;
}
// should only be called when it's safe to access haComponents
- private void addComponent(AbstractComponent component) {
+ private boolean addComponent(AbstractComponent component) {
AbstractComponent existing = haComponents.get(component.getComponentId());
if (existing != null) {
// DeviceTriggers that are for the same subtype, topic, and value template
});
}
haComponentsByUniqueId.put(component.getUniqueId(), component);
- System.out.println("don't forget to add to the channel config");
- return;
+ return false;
}
}
}
haComponents.put(component.getComponentId(), component);
haComponentsByUniqueId.put(component.getUniqueId(), component);
+ return true;
}
/**
}
}
- protected void spyOnChannelUpdates(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
- String channelId) {
- // It's already thingHandler, but not the spy version
- component.getChannel(channelId).getState().setChannelStateUpdateListener(thingHandler);
- }
-
/**
* Assert a channel triggers
*/
protected void assertTriggered(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
String channelId, String trigger) {
- verify(thingHandler).triggerChannel(eq(component.getChannel(channelId).getChannel().getUID()), eq(trigger));
+ verify(callbackMock).channelTriggered(eq(haThing), eq(component.getChannel(channelId).getChannel().getUID()),
+ eq(trigger));
}
/**
*/
protected void assertNotTriggered(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
String channelId, String trigger) {
- verify(thingHandler, never()).triggerChannel(eq(component.getChannel(channelId).getChannel().getUID()),
- eq(trigger));
+ verify(callbackMock, never()).channelTriggered(eq(haThing),
+ eq(component.getChannel(channelId).getChannel().getUID()), eq(trigger));
}
/**
assertChannel(component, "on", "zigbee2mqtt/Charge Now Button/action", "", "MQTT Device Trigger",
TextValue.class);
- spyOnChannelUpdates(component, "on");
publishMessage("zigbee2mqtt/Charge Now Button/action", "on");
assertTriggered(component, "on", "on");
List<?> configList = (List<?>) config;
assertThat(configList.size(), is(2));
- spyOnChannelUpdates(component1, "turn_on");
publishMessage("zigbee2mqtt/Charge Now Button/action", "press");
assertTriggered(component1, "turn_on", "press");
--- /dev/null
+/**
+ * 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);
+ }
+}