Skip to content
Open
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
18 changes: 17 additions & 1 deletion bundles/org.openhab.binding.mqtt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ You can add the following channels:
- **image**: This channel handles binary images in common java supported formats (bmp,jpg,png).
- **datetime**: This channel handles date/time values.
- **rollershutter**: This channel is for rollershutters.
- **trigger**: This channel emits the received MQTT payload as a channel event without updating an Item state.

## Channel Configuration

Expand All @@ -168,7 +169,22 @@ You can add the following channels:
You usually need this to be `true` if your item is also linked to another channel, say a KNX actor, and you want a received MQTT payload to command that KNX actor.
- **retained**: The value will be published to the command topic as retained message. A retained value stays on the broker and can even be seen by MQTT clients that are subscribing at a later point in time.
- **qos**: QoS of this channel. Overrides the connection QoS (defined in broker connection).
- **trigger**: If `true`, the state topic will not update a state, but trigger a channel instead.
- **trigger**: If `true`, a received MQTT value that is valid for the selected channel type triggers a channel event instead of updating a state.
This typed trigger behavior remains supported, but for untyped trigger events the dedicated `trigger` channel type is preferred.
If a `commandTopic` is also configured, the channel retains state-channel metadata so linked Item commands can continue to be published to MQTT.

### Trigger Channels

The trigger channel emits every successfully transformed payload received on `stateTopic` as a channel event.
It does not validate the payload as one of the state channel types and does not update an Item state.

In a `.things` file, use the MQTT trigger channel type:

```java
Type trigger : alarm [ stateTopic="sensors/alarm" ]
```

The generic `Trigger String` and `Trigger Switch` forms do not select an MQTT channel type and are not supported by this binding.

### Channel Type "string"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.binding.generic.ChannelTransformation;
import org.openhab.core.thing.type.ChannelKind;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.StateDescription;
import org.openhab.core.types.util.UnitUtils;
Expand Down Expand Up @@ -150,6 +151,7 @@
continue;
}
final ChannelConfig channelConfig = channel.getConfiguration().as(ChannelConfig.class);
ChannelBuilder channelBuilder = null;

if (channelTypeUID
.equals(new ChannelTypeUID(MqttBindingConstants.BINDING_ID, MqttBindingConstants.NUMBER))) {
Expand All @@ -159,7 +161,7 @@
// Number
String actualItemType = channel.getAcceptedItemType();
if (!expectedItemType.equals(actualItemType)) {
ChannelBuilder channelBuilder = callback.createChannelBuilder(channel.getUID(), channelTypeUID)
channelBuilder = callback.createChannelBuilder(channel.getUID(), channelTypeUID)
.withAcceptedItemType(expectedItemType).withConfiguration(channel.getConfiguration());
String label = channel.getLabel();
if (label != null) {
Expand All @@ -169,12 +171,23 @@
if (description != null) {
channelBuilder.withDescription(description);
}
thingBuilder.withoutChannel(channel.getUID());
thingBuilder.withChannel(channelBuilder.build());
modified = true;
}
}

if (channelConfig.trigger && channelConfig.commandTopic.isBlank()
&& channel.getKind() != ChannelKind.TRIGGER) {
if (channelBuilder == null) {
channelBuilder = ChannelBuilder.create(channel);
}
channelBuilder.withKind(ChannelKind.TRIGGER);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trigger=true should probably not change the channel kind for image channels. ImageValue is binary, and ChannelState.processMessage() handles binary values before the config.trigger branch, so it still calls updateChannelState() rather than triggerChannel(). With this change an image channel can therefore be exposed as a trigger while its runtime behavior remains a state update. Could this either exclude binary channels or make their trigger behavior consistent?

}

if (channelBuilder != null) {
thingBuilder.withoutChannel(channel.getUID());
thingBuilder.withChannel(channelBuilder.build());
modified = true;
}

try {
Value value = ValueFactory.createValueState(channelConfig, channelTypeUID.getId());
ChannelState channelState = createChannelState(channelConfig, channel.getUID(), value);
Expand Down Expand Up @@ -206,7 +219,7 @@

@Override
protected void updateThingStatus(boolean messageReceived, Optional<Boolean> availibilityTopicsSeen) {
if (availibilityTopicsSeen.orElse(true)) {

Check warning on line 222 in bundles/org.openhab.binding.mqtt/src/main/java/org/openhab/binding/mqtt/generic/internal/handler/GenericMQTTThingHandler.java

View workflow job for this annotation

GitHub Actions / Build (Java 21, ubuntu-24.04)

Potential null pointer access: This expression of type java.lang.Boolean may be null but requires auto-unboxing
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import org.openhab.binding.mqtt.generic.values.ImageValue;
import org.openhab.binding.mqtt.generic.values.LocationValue;
import org.openhab.binding.mqtt.generic.values.NumberValue;
import org.openhab.binding.mqtt.generic.values.OnOffValue;
import org.openhab.binding.mqtt.generic.values.PercentageValue;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
Expand Down Expand Up @@ -203,6 +204,20 @@ public void receiveStringTest() throws Exception {
verify(channelStateUpdateListenerMock).updateChannelState(eq(channelUIDMock), any());
}

@Test
public void typedTriggerOnlyEmitsEventsForValidValues() {
ChannelConfig triggerConfig = ChannelConfigBuilder.create("state", "").makeTrigger(true).build();
ChannelState channelState = new ChannelState(triggerConfig, channelUIDMock, new OnOffValue("ON", "OFF"),
channelStateUpdateListenerMock);

channelState.processMessage("state", "ON".getBytes());
channelState.processMessage("state", "INVALID".getBytes());

verify(channelStateUpdateListenerMock).triggerChannel(channelUIDMock, "ON");
verify(channelStateUpdateListenerMock, times(1)).triggerChannel(any(), any());
verify(channelStateUpdateListenerMock, never()).updateChannelState(any(), any());
}

@Test
public void receiveDecimalTest() {
NumberValue value = new NumberValue(null, null, new BigDecimal(10), null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,19 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.openhab.binding.mqtt.generic.internal.MqttBindingConstants.GENERIC_MQTT_THING;
import static org.openhab.binding.mqtt.generic.internal.handler.ThingChannelConstants.*;

import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
Expand All @@ -42,12 +46,14 @@
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.type.ChannelKind;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.UnDefType;

Expand All @@ -73,6 +79,7 @@ public void setUp() {
ThingStatusInfo thingStatus = new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);

// Mock the thing: We need the thingUID and the bridgeUID
when(thingMock.getThingTypeUID()).thenReturn(GENERIC_MQTT_THING);
when(thingMock.getUID()).thenReturn(TEST_GENERIC_THING);
when(thingMock.getChannels()).thenReturn(THING_CHANNEL_LIST);
when(thingMock.getStatusInfo()).thenReturn(thingStatus);
Expand Down Expand Up @@ -125,6 +132,35 @@ public void initialize() {
&& ThingStatusDetail.NONE.equals(arg.getStatusDetail())));
}

@Test
public void initializeMarksReadOnlyTypedTriggerAsTriggerChannel() {
Configuration configuration = new Configuration(Map.of("stateTopic", "test/state", "trigger", true));
Channel triggerChannel = cb("onoff", "Switch", configuration, ON_OFF_CHANNEL);
when(thingMock.getChannels()).thenReturn(List.of(triggerChannel));

thingHandler.initialize();

ArgumentCaptor<Thing> thingCaptor = ArgumentCaptor.forClass(Thing.class);
verify(callbackMock).thingUpdated(thingCaptor.capture());
Channel updatedChannel = thingCaptor.getValue().getChannel(triggerChannel.getUID());
assertThat(updatedChannel.getKind(), is(ChannelKind.TRIGGER));
assertThat(updatedChannel.getChannelTypeUID(), is(ON_OFF_CHANNEL));
assertThat(updatedChannel.getAcceptedItemType(), is("Switch"));
}

@Test
public void initializeKeepsCommandCapableTypedTriggerAsStateChannel() {
Configuration configuration = new Configuration(
Map.of("stateTopic", "test/state", "commandTopic", "test/command", "trigger", true));
Channel triggerChannel = cb("onoff", "Switch", configuration, ON_OFF_CHANNEL);
when(thingMock.getChannels()).thenReturn(List.of(triggerChannel));

thingHandler.initialize();

verify(callbackMock, never()).thingUpdated(any());
assertThat(triggerChannel.getKind(), is(ChannelKind.STATE));
}

@Test
public void handleCommandRefresh() {
TextValue value = spy(new TextValue());
Expand Down
Loading