[mqtt.homeassistant] fix newStyleChannels (#17491)

* [mqtt.homeassistant] fix newStyleChannels
* further simplify channel IDs

Signed-off-by: Cody Cutrer <cody@cutrer.us>
This commit is contained in:
Cody Cutrer 2024-10-07 15:28:50 -06:00 committed by GitHub
parent 90442a3864
commit 31f6cda174
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 264 additions and 182 deletions

View File

@ -51,13 +51,13 @@ public class ChannelState implements MqttMessageSubscriber {
// Immutable channel configuration // Immutable channel configuration
protected final boolean readOnly; protected final boolean readOnly;
protected final ChannelUID channelUID;
protected final ChannelConfig config; protected final ChannelConfig config;
/** Channel value **/ /** Channel value **/
protected final Value cachedValue; protected final Value cachedValue;
// Runtime variables // Runtime variables
protected ChannelUID channelUID;
private @Nullable MqttBrokerConnection connection; private @Nullable MqttBrokerConnection connection;
protected final ChannelTransformation incomingTransformation; protected final ChannelTransformation incomingTransformation;
protected final ChannelTransformation outgoingTransformation; protected final ChannelTransformation outgoingTransformation;
@ -132,6 +132,11 @@ public class ChannelState implements MqttMessageSubscriber {
return channelUID; return channelUID;
} }
// If the UID of the channel changed after it was initially created
public void setChannelUID(ChannelUID channelUID) {
this.channelUID = channelUID;
}
/** /**
* Incoming message from the MqttBrokerConnection * Incoming message from the MqttBrokerConnection
* *

View File

@ -58,7 +58,7 @@ import org.openhab.core.types.StateDescription;
@NonNullByDefault @NonNullByDefault
public class ComponentChannel { public class ComponentChannel {
private final ChannelState channelState; private final ChannelState channelState;
private final Channel channel; private Channel channel;
private final @Nullable StateDescription stateDescription; private final @Nullable StateDescription stateDescription;
private final @Nullable CommandDescription commandDescription; private final @Nullable CommandDescription commandDescription;
private final ChannelStateUpdateListener channelStateUpdateListener; private final ChannelStateUpdateListener channelStateUpdateListener;
@ -77,6 +77,18 @@ public class ComponentChannel {
return channel; return channel;
} }
public void resetUID(ChannelUID channelUID) {
channel = ChannelBuilder.create(channelUID, channel.getAcceptedItemType()).withType(channel.getChannelTypeUID())
.withKind(channel.getKind()).withLabel(Objects.requireNonNull(channel.getLabel()))
.withConfiguration(channel.getConfiguration()).withAutoUpdatePolicy(channel.getAutoUpdatePolicy())
.build();
channelState.setChannelUID(channelUID);
}
public void clearConfiguration() {
channel = ChannelBuilder.create(channel).withConfiguration(new Configuration()).build();
}
public ChannelState getState() { public ChannelState getState() {
return channelState; return channelState;
} }

View File

@ -24,6 +24,7 @@ import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.AvailabilityTracker; import org.openhab.binding.mqtt.generic.AvailabilityTracker;
import org.openhab.binding.mqtt.generic.ChannelState;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener; import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider; import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
import org.openhab.binding.mqtt.generic.values.Value; import org.openhab.binding.mqtt.generic.values.Value;
@ -39,7 +40,6 @@ import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AvailabilityMo
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Device; import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Device;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.Channel; import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelGroupUID;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.binding.generic.ChannelTransformation; import org.openhab.core.thing.binding.generic.ChannelTransformation;
import org.openhab.core.thing.type.ChannelDefinition; import org.openhab.core.thing.type.ChannelDefinition;
@ -65,7 +65,6 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
// Component location fields // Component location fields
protected final ComponentConfiguration componentConfiguration; protected final ComponentConfiguration componentConfiguration;
protected final @Nullable ChannelGroupUID channelGroupUID;
protected final HaID haID; protected final HaID haID;
// Channels and configuration // Channels and configuration
@ -79,14 +78,10 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
protected final C channelConfiguration; protected final C channelConfiguration;
protected boolean configSeen; protected boolean configSeen;
protected final boolean singleChannelComponent; protected final boolean newStyleChannels;
protected final String groupId;
protected final String uniqueId; protected final String uniqueId;
protected @Nullable String groupId;
public AbstractComponent(ComponentFactory.ComponentConfiguration componentConfiguration, Class<C> clazz, protected String componentId;
boolean newStyleChannels) {
this(componentConfiguration, clazz, newStyleChannels, false);
}
/** /**
* Creates component based on generic configuration and component configuration type. * Creates component based on generic configuration and component configuration type.
@ -98,9 +93,9 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
* (only if newStyleChannels is true) * (only if newStyleChannels is true)
*/ */
public AbstractComponent(ComponentFactory.ComponentConfiguration componentConfiguration, Class<C> clazz, public AbstractComponent(ComponentFactory.ComponentConfiguration componentConfiguration, Class<C> clazz,
boolean newStyleChannels, boolean singleChannelComponent) { boolean newStyleChannels) {
this.componentConfiguration = componentConfiguration; this.componentConfiguration = componentConfiguration;
this.singleChannelComponent = newStyleChannels && singleChannelComponent; this.newStyleChannels = newStyleChannels;
this.channelConfigurationJson = componentConfiguration.getConfigJSON(); this.channelConfigurationJson = componentConfiguration.getConfigJSON();
this.channelConfiguration = componentConfiguration.getConfig(clazz); this.channelConfiguration = componentConfiguration.getConfig(clazz);
@ -109,14 +104,16 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
this.haID = componentConfiguration.getHaID(); this.haID = componentConfiguration.getHaID();
String name = channelConfiguration.getName(); String name = channelConfiguration.getName();
if (name != null && !name.isEmpty()) { if (newStyleChannels) {
groupId = this.haID.getGroupId(channelConfiguration.getUniqueId(), newStyleChannels); // try for a simple component/group ID first; if there are conflicts
// (components of different types, but the same object id)
this.channelGroupUID = this.singleChannelComponent ? null // we'll resolve them later
: new ChannelGroupUID(componentConfiguration.getThingUID(), groupId); groupId = componentId = haID.objectID.replace('-', '_');
} else if (name != null && !name.isEmpty()) {
groupId = componentId = this.haID.getGroupId(channelConfiguration.getUniqueId(), false);
} else { } else {
this.groupId = this.singleChannelComponent ? haID.component : ""; groupId = null;
this.channelGroupUID = null; componentId = "";
} }
uniqueId = this.haID.getGroupId(channelConfiguration.getUniqueId(), false); uniqueId = this.haID.getGroupId(channelConfiguration.getUniqueId(), false);
@ -155,10 +152,30 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
} }
} }
protected void finalizeChannels() {
if (!newStyleChannels) {
return;
}
if (channels.size() == 1) {
groupId = null;
channels.values().forEach(c -> c.resetUID(buildChannelUID(componentId)));
} else {
// only the first channel needs to persist the configuration
channels.values().stream().skip(1).forEach(c -> {
c.clearConfiguration();
});
}
}
public void resolveConflict() {
componentId = this.haID.getGroupId(channelConfiguration.getUniqueId(), newStyleChannels);
channels.values().forEach(c -> c.resetUID(buildChannelUID(c.getChannel().getUID().getIdWithoutGroup())));
}
protected ComponentChannel.Builder buildChannel(String channelID, ComponentChannelType channelType, protected ComponentChannel.Builder buildChannel(String channelID, ComponentChannelType channelType,
Value valueState, String label, ChannelStateUpdateListener channelStateUpdateListener) { Value valueState, String label, ChannelStateUpdateListener channelStateUpdateListener) {
if (singleChannelComponent) { if (groupId == null) {
channelID = groupId; channelID = componentId;
} }
return new ComponentChannel.Builder(this, channelID, channelType.getChannelTypeUID(), valueState, label, return new ComponentChannel.Builder(this, channelID, channelType.getChannelTypeUID(), valueState, label,
channelStateUpdateListener); channelStateUpdateListener);
@ -216,15 +233,19 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
} }
public ChannelUID buildChannelUID(String channelID) { public ChannelUID buildChannelUID(String channelID) {
final ChannelGroupUID groupUID = channelGroupUID; final String localGroupID = groupId;
if (groupUID != null) { if (localGroupID != null) {
return new ChannelUID(groupUID, channelID); return new ChannelUID(componentConfiguration.getThingUID(), localGroupID, channelID);
} }
return new ChannelUID(componentConfiguration.getThingUID(), channelID); return new ChannelUID(componentConfiguration.getThingUID(), channelID);
} }
public String getGroupId() { public String getComponentId() {
return groupId; return componentId;
}
public String getUniqueId() {
return uniqueId;
} }
/** /**
@ -273,7 +294,7 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
* Return the channel group type. * Return the channel group type.
*/ */
public @Nullable ChannelGroupType getChannelGroupType(String prefix) { public @Nullable ChannelGroupType getChannelGroupType(String prefix) {
if (channelGroupUID == null) { if (groupId == null) {
return null; return null;
} }
return ChannelGroupTypeBuilder.instance(getChannelGroupTypeUID(prefix), getName()) return ChannelGroupTypeBuilder.instance(getChannelGroupTypeUID(prefix), getName())
@ -281,7 +302,7 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
} }
public List<ChannelDefinition> getChannelDefinitions() { public List<ChannelDefinition> getChannelDefinitions() {
if (channelGroupUID != null) { if (groupId != null) {
return List.of(); return List.of();
} }
return getAllChannelDefinitions(); return getAllChannelDefinitions();
@ -295,6 +316,10 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
return channels.values().stream().map(ComponentChannel::getChannel).toList(); return channels.values().stream().map(ComponentChannel::getChannel).toList();
} }
public void getChannelStates(Map<ChannelUID, ChannelState> states) {
channels.values().forEach(c -> states.put(c.getChannel().getUID(), c.getState()));
}
/** /**
* Resets all channel states to state UNDEF. Call this method after the connection * Resets all channel states to state UNDEF. Call this method after the connection
* to the MQTT broker got lost. * to the MQTT broker got lost.
@ -307,14 +332,15 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
* Return the channel group definition for this component. * Return the channel group definition for this component.
*/ */
public @Nullable ChannelGroupDefinition getGroupDefinition(String prefix) { public @Nullable ChannelGroupDefinition getGroupDefinition(String prefix) {
if (channelGroupUID == null) { String localGroupId = groupId;
if (localGroupId == null) {
return null; return null;
} }
return new ChannelGroupDefinition(channelGroupUID.getId(), getChannelGroupTypeUID(prefix), getName(), null); return new ChannelGroupDefinition(localGroupId, getChannelGroupTypeUID(prefix), getName(), null);
} }
public boolean hasGroup() { public boolean hasGroup() {
return channelGroupUID != null; return groupId != null;
} }
public HaID getHaID() { public HaID getHaID() {

View File

@ -97,5 +97,6 @@ public class AlarmControlPanel extends AbstractComponent<AlarmControlPanel.Chann
componentConfiguration.getUpdateListener()) componentConfiguration.getUpdateListener())
.commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos()).build(); .commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos()).build();
} }
finalizeChannels();
} }
} }

View File

@ -69,7 +69,7 @@ public class BinarySensor extends AbstractComponent<BinarySensor.ChannelConfigur
} }
public BinarySensor(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) { public BinarySensor(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
OnOffValue value = new OnOffValue(channelConfiguration.payloadOn, channelConfiguration.payloadOff); OnOffValue value = new OnOffValue(channelConfiguration.payloadOn, channelConfiguration.payloadOff);
@ -77,6 +77,7 @@ public class BinarySensor extends AbstractComponent<BinarySensor.ChannelConfigur
getListener(componentConfiguration, value)) getListener(componentConfiguration, value))
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate()) .stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
.withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build(); .withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build();
finalizeChannels();
} }
private ChannelStateUpdateListener getListener(ComponentFactory.ComponentConfiguration componentConfiguration, private ChannelStateUpdateListener getListener(ComponentFactory.ComponentConfiguration componentConfiguration,

View File

@ -48,7 +48,7 @@ public class Button extends AbstractComponent<Button.ChannelConfiguration> {
} }
public Button(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) { public Button(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
TextValue value = new TextValue(new String[] { channelConfiguration.payloadPress }); TextValue value = new TextValue(new String[] { channelConfiguration.payloadPress });
@ -57,5 +57,6 @@ public class Button extends AbstractComponent<Button.ChannelConfiguration> {
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(), .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos()) channelConfiguration.getQos())
.withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build(); .withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build();
finalizeChannels();
} }
} }

View File

@ -46,5 +46,6 @@ public class Camera extends AbstractComponent<Camera.ChannelConfiguration> {
buildChannel(CAMERA_CHANNEL_ID, ComponentChannelType.IMAGE, value, getName(), buildChannel(CAMERA_CHANNEL_ID, ComponentChannelType.IMAGE, value, getName(),
componentConfiguration.getUpdateListener()).stateTopic(channelConfiguration.topic).build(); componentConfiguration.getUpdateListener()).stateTopic(channelConfiguration.topic).build();
finalizeChannels();
} }
} }

View File

@ -284,6 +284,7 @@ public class Climate extends AbstractComponent<Climate.ChannelConfiguration> {
buildOptionalChannel(POWER_CH_ID, ComponentChannelType.SWITCH, new OnOffValue(), updateListener, null, buildOptionalChannel(POWER_CH_ID, ComponentChannelType.SWITCH, new OnOffValue(), updateListener, null,
channelConfiguration.powerCommandTopic, null, null, null); channelConfiguration.powerCommandTopic, null, null, null);
finalizeChannels();
} }
@Nullable @Nullable

View File

@ -150,5 +150,6 @@ public class Cover extends AbstractComponent<Cover.ChannelConfiguration> {
} }
return true; return true;
}).build(); }).build();
finalizeChannels();
} }
} }

View File

@ -112,6 +112,7 @@ public class DefaultSchemaLight extends Light {
.build(); .build();
} }
boolean hasColorChannel = false;
if (channelConfiguration.rgbStateTopic != null || channelConfiguration.rgbCommandTopic != null) { if (channelConfiguration.rgbStateTopic != null || channelConfiguration.rgbCommandTopic != null) {
hasColorChannel = true; hasColorChannel = true;
hiddenChannels.add(rgbChannel = buildChannel(RGB_CHANNEL_ID, ComponentChannelType.COLOR, hiddenChannels.add(rgbChannel = buildChannel(RGB_CHANNEL_ID, ComponentChannelType.COLOR,
@ -167,7 +168,7 @@ public class DefaultSchemaLight extends Light {
if (localBrightnessChannel != null) { if (localBrightnessChannel != null) {
hiddenChannels.add(localBrightnessChannel); hiddenChannels.add(localBrightnessChannel);
} }
buildChannel(COLOR_CHANNEL_ID, ComponentChannelType.COLOR, colorValue, "Color", this) colorChannel = buildChannel(COLOR_CHANNEL_ID, ComponentChannelType.COLOR, colorValue, "Color", this)
.commandTopic(DUMMY_TOPIC, channelConfiguration.isRetain(), channelConfiguration.getQos()) .commandTopic(DUMMY_TOPIC, channelConfiguration.isRetain(), channelConfiguration.getQos())
.commandFilter(this::handleColorCommand).build(); .commandFilter(this::handleColorCommand).build();
} else if (localBrightnessChannel != null) { } else if (localBrightnessChannel != null) {
@ -280,74 +281,74 @@ public class DefaultSchemaLight extends Light {
@Override @Override
public void updateChannelState(ChannelUID channel, State state) { public void updateChannelState(ChannelUID channel, State state) {
ChannelStateUpdateListener listener = this.channelStateUpdateListener; ChannelStateUpdateListener listener = this.channelStateUpdateListener;
switch (channel.getIdWithoutGroup()) { String id = channel.getIdWithoutGroup();
case ON_OFF_CHANNEL_ID: ComponentChannel localBrightnessChannel = brightnessChannel;
if (hasColorChannel) { ComponentChannel localColorChannel = colorChannel;
HSBType newOnState = colorValue.getChannelState() instanceof HSBType ChannelUID primaryChannelUID;
? (HSBType) colorValue.getChannelState() if (localColorChannel != null) {
: HSBType.WHITE; primaryChannelUID = localColorChannel.getChannel().getUID();
if (state.equals(OnOffType.ON)) { } else if (localBrightnessChannel != null) {
colorValue.update(newOnState); primaryChannelUID = localBrightnessChannel.getChannel().getUID();
} } else {
primaryChannelUID = onOffChannel.getChannel().getUID();
listener.updateChannelState(buildChannelUID(COLOR_CHANNEL_ID),
state.equals(OnOffType.ON) ? newOnState : HSBType.BLACK);
} else if (brightnessChannel != null) {
listener.updateChannelState(new ChannelUID(channel.getThingUID(), BRIGHTNESS_CHANNEL_ID),
state.equals(OnOffType.ON) ? brightnessValue.getChannelState() : PercentType.ZERO);
} else {
listener.updateChannelState(channel, state);
}
return;
case BRIGHTNESS_CHANNEL_ID:
onOffValue.update(Objects.requireNonNull(state.as(OnOffType.class)));
if (hasColorChannel) {
if (colorValue.getChannelState() instanceof HSBType) {
HSBType hsb = (HSBType) (colorValue.getChannelState());
colorValue.update(new HSBType(hsb.getHue(), hsb.getSaturation(),
(PercentType) brightnessValue.getChannelState()));
} else {
colorValue.update(new HSBType(DecimalType.ZERO, PercentType.ZERO,
(PercentType) brightnessValue.getChannelState()));
}
listener.updateChannelState(buildChannelUID(COLOR_CHANNEL_ID), colorValue.getChannelState());
} else {
listener.updateChannelState(channel, state);
}
return;
case COLOR_TEMP_CHANNEL_ID:
case EFFECT_CHANNEL_ID:
// Real channels; pass through
listener.updateChannelState(channel, state);
return;
case HS_CHANNEL_ID:
case XY_CHANNEL_ID:
if (brightnessValue.getChannelState() instanceof UnDefType) {
brightnessValue.update(PercentType.HUNDRED);
}
String[] split = state.toString().split(",");
if (split.length != 2) {
throw new IllegalArgumentException(state.toString() + " is not a valid string syntax");
}
float x = Float.parseFloat(split[0]);
float y = Float.parseFloat(split[1]);
PercentType brightness = (PercentType) brightnessValue.getChannelState();
if (channel.getIdWithoutGroup().equals(HS_CHANNEL_ID)) {
colorValue.update(new HSBType(new DecimalType(x), new PercentType(new BigDecimal(y)), brightness));
} else {
HSBType xyColor = HSBType.fromXY(x, y);
colorValue.update(new HSBType(xyColor.getHue(), xyColor.getSaturation(), brightness));
}
listener.updateChannelState(buildChannelUID(COLOR_CHANNEL_ID), colorValue.getChannelState());
return;
case RGB_CHANNEL_ID:
colorValue.update((HSBType) state);
listener.updateChannelState(buildChannelUID(COLOR_CHANNEL_ID), colorValue.getChannelState());
break;
case RGBW_CHANNEL_ID:
case RGBWW_CHANNEL_ID:
// TODO: update color value
break;
} }
// on_off, brightness, and color might exist as a sole channel, which means
// they got renamed. they need to be compared against the actual UID of the
// channel. all the rest we can just check against the basic ID
if (channel.equals(onOffChannel.getChannel().getUID())) {
if (localColorChannel != null) {
HSBType newOnState = colorValue.getChannelState() instanceof HSBType newOnStateTmp ? newOnStateTmp
: HSBType.WHITE;
if (state.equals(OnOffType.ON)) {
colorValue.update(newOnState);
}
listener.updateChannelState(primaryChannelUID, state.equals(OnOffType.ON) ? newOnState : HSBType.BLACK);
} else if (brightnessChannel != null) {
listener.updateChannelState(primaryChannelUID,
state.equals(OnOffType.ON) ? brightnessValue.getChannelState() : PercentType.ZERO);
} else {
listener.updateChannelState(primaryChannelUID, state);
}
} else if (localBrightnessChannel != null && localBrightnessChannel.getChannel().getUID().equals(channel)) {
onOffValue.update(Objects.requireNonNull(state.as(OnOffType.class)));
if (localColorChannel != null) {
if (colorValue.getChannelState() instanceof HSBType hsb) {
colorValue.update(new HSBType(hsb.getHue(), hsb.getSaturation(),
(PercentType) brightnessValue.getChannelState()));
} else {
colorValue.update(new HSBType(DecimalType.ZERO, PercentType.ZERO,
(PercentType) brightnessValue.getChannelState()));
}
listener.updateChannelState(primaryChannelUID, colorValue.getChannelState());
} else {
listener.updateChannelState(primaryChannelUID, state);
}
} else if (id.equals(COLOR_TEMP_CHANNEL_ID) || channel.getIdWithoutGroup().equals(EFFECT_CHANNEL_ID)) {
// Real channels; pass through
listener.updateChannelState(channel, state);
} else if (id.equals(HS_CHANNEL_ID) || id.equals(XY_CHANNEL_ID)) {
if (brightnessValue.getChannelState() instanceof UnDefType) {
brightnessValue.update(PercentType.HUNDRED);
}
String[] split = state.toString().split(",");
if (split.length != 2) {
throw new IllegalArgumentException(state.toString() + " is not a valid string syntax");
}
float x = Float.parseFloat(split[0]);
float y = Float.parseFloat(split[1]);
PercentType brightness = (PercentType) brightnessValue.getChannelState();
if (channel.getIdWithoutGroup().equals(HS_CHANNEL_ID)) {
colorValue.update(new HSBType(new DecimalType(x), new PercentType(new BigDecimal(y)), brightness));
} else {
HSBType xyColor = HSBType.fromXY(x, y);
colorValue.update(new HSBType(xyColor.getHue(), xyColor.getSaturation(), brightness));
}
listener.updateChannelState(primaryChannelUID, colorValue.getChannelState());
} else if (id.equals(RGB_CHANNEL_ID)) {
colorValue.update((HSBType) state);
listener.updateChannelState(primaryChannelUID, colorValue.getChannelState());
}
// else rgbw channel, rgbww channel
} }
} }

View File

@ -46,7 +46,7 @@ public class DeviceTrigger extends AbstractComponent<DeviceTrigger.ChannelConfig
} }
public DeviceTrigger(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) { public DeviceTrigger(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
if (!"trigger".equals(channelConfiguration.automationType)) { if (!"trigger".equals(channelConfiguration.automationType)) {
throw new ConfigurationException("Component:DeviceTrigger must have automation_type 'trigger'"); throw new ConfigurationException("Component:DeviceTrigger must have automation_type 'trigger'");
@ -69,5 +69,6 @@ public class DeviceTrigger extends AbstractComponent<DeviceTrigger.ChannelConfig
buildChannel(channelConfiguration.type, ComponentChannelType.TRIGGER, value, getName(), buildChannel(channelConfiguration.type, ComponentChannelType.TRIGGER, value, getName(),
componentConfiguration.getUpdateListener()) componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.topic, channelConfiguration.getValueTemplate()).trigger(true).build(); .stateTopic(channelConfiguration.topic, channelConfiguration.getValueTemplate()).trigger(true).build();
finalizeChannels();
} }
} }

View File

@ -14,6 +14,7 @@ package org.openhab.binding.mqtt.homeassistant.internal.component;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
@ -118,6 +119,8 @@ public class Fan extends AbstractComponent<Fan.ChannelConfiguration> implements
private final PercentageValue speedValue; private final PercentageValue speedValue;
private State rawSpeedState; private State rawSpeedState;
private final ComponentChannel onOffChannel; private final ComponentChannel onOffChannel;
private final @Nullable ComponentChannel speedChannel;
private final ComponentChannel primaryChannel;
private final ChannelStateUpdateListener channelStateUpdateListener; private final ChannelStateUpdateListener channelStateUpdateListener;
public Fan(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) { public Fan(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
@ -144,11 +147,15 @@ public class Fan extends AbstractComponent<Fan.ChannelConfiguration> implements
if (channelConfiguration.percentageCommandTopic != null) { if (channelConfiguration.percentageCommandTopic != null) {
hiddenChannels.add(onOffChannel); hiddenChannels.add(onOffChannel);
buildChannel(SPEED_CHANNEL_ID, ComponentChannelType.DIMMER, speedValue, "Speed", this) primaryChannel = speedChannel = buildChannel(SPEED_CHANNEL_ID, ComponentChannelType.DIMMER, speedValue,
"Speed", this)
.stateTopic(channelConfiguration.percentageStateTopic, channelConfiguration.percentageValueTemplate) .stateTopic(channelConfiguration.percentageStateTopic, channelConfiguration.percentageValueTemplate)
.commandTopic(channelConfiguration.percentageCommandTopic, channelConfiguration.isRetain(), .commandTopic(channelConfiguration.percentageCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos(), channelConfiguration.percentageCommandTemplate) channelConfiguration.getQos(), channelConfiguration.percentageCommandTemplate)
.commandFilter(this::handlePercentageCommand).build(); .commandFilter(this::handlePercentageCommand).build();
} else {
primaryChannel = onOffChannel;
speedChannel = null;
} }
List<String> presetModes = channelConfiguration.presetModes; List<String> presetModes = channelConfiguration.presetModes;
@ -184,6 +191,7 @@ public class Fan extends AbstractComponent<Fan.ChannelConfiguration> implements
channelConfiguration.getQos(), channelConfiguration.directionCommandTemplate) channelConfiguration.getQos(), channelConfiguration.directionCommandTemplate)
.build(); .build();
} }
finalizeChannels();
} }
private boolean handlePercentageCommand(Command command) { private boolean handlePercentageCommand(Command command) {
@ -197,7 +205,7 @@ public class Fan extends AbstractComponent<Fan.ChannelConfiguration> implements
@Override @Override
public void updateChannelState(ChannelUID channel, State state) { public void updateChannelState(ChannelUID channel, State state) {
if (channel.getIdWithoutGroup().equals(SWITCH_CHANNEL_ID)) { if (onOffChannel.getChannel().getUID().equals(channel)) {
if (rawSpeedState instanceof UnDefType && state.equals(OnOffType.ON)) { if (rawSpeedState instanceof UnDefType && state.equals(OnOffType.ON)) {
// Assume full on if we don't yet know the actual speed // Assume full on if we don't yet know the actual speed
state = PercentType.HUNDRED; state = PercentType.HUNDRED;
@ -206,7 +214,7 @@ public class Fan extends AbstractComponent<Fan.ChannelConfiguration> implements
} else { } else {
state = rawSpeedState; state = rawSpeedState;
} }
} else if (channel.getIdWithoutGroup().equals(SPEED_CHANNEL_ID)) { } else if (Objects.requireNonNull(speedChannel).getChannel().getUID().equals(channel)) {
rawSpeedState = state; rawSpeedState = state;
if (onOffValue.getChannelState().equals(OnOffType.OFF)) { if (onOffValue.getChannelState().equals(OnOffType.OFF)) {
// Don't pass on percentage values while the fan is off // Don't pass on percentage values while the fan is off
@ -214,7 +222,7 @@ public class Fan extends AbstractComponent<Fan.ChannelConfiguration> implements
} }
} }
speedValue.update(state); speedValue.update(state);
channelStateUpdateListener.updateChannelState(buildChannelUID(SPEED_CHANNEL_ID), state); channelStateUpdateListener.updateChannelState(primaryChannel.getChannel().getUID(), state);
} }
@Override @Override

View File

@ -21,6 +21,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener; import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.values.TextValue; import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType; import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.HSBType;
@ -77,6 +78,7 @@ public class JSONSchemaLight extends AbstractRawSchemaLight {
@Override @Override
protected void buildChannels() { protected void buildChannels() {
boolean hasColorChannel = false;
List<LightColorMode> supportedColorModes = channelConfiguration.supportedColorModes; List<LightColorMode> supportedColorModes = channelConfiguration.supportedColorModes;
if (supportedColorModes != null) { if (supportedColorModes != null) {
if (LightColorMode.hasColorChannel(supportedColorModes)) { if (LightColorMode.hasColorChannel(supportedColorModes)) {
@ -99,7 +101,7 @@ public class JSONSchemaLight extends AbstractRawSchemaLight {
} }
if (hasColorChannel) { if (hasColorChannel) {
buildChannel(COLOR_CHANNEL_ID, ComponentChannelType.COLOR, colorValue, "Color", this) colorChannel = buildChannel(COLOR_CHANNEL_ID, ComponentChannelType.COLOR, colorValue, "Color", this)
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build(); .commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
} else if (channelConfiguration.brightness) { } else if (channelConfiguration.brightness) {
brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, ComponentChannelType.DIMMER, brightnessValue, brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, ComponentChannelType.DIMMER, brightnessValue,
@ -144,7 +146,7 @@ public class JSONSchemaLight extends AbstractRawSchemaLight {
.divide(new BigDecimal(100), MathContext.DECIMAL128).intValue(); .divide(new BigDecimal(100), MathContext.DECIMAL128).intValue();
} }
if (hasColorChannel) { if (colorChannel != null) {
json.color = new JSONState.Color(); json.color = new JSONState.Color();
if (channelConfiguration.supportedColorModes.contains(LightColorMode.COLOR_MODE_HS)) { if (channelConfiguration.supportedColorModes.contains(LightColorMode.COLOR_MODE_HS)) {
json.color.h = state.getHue().toBigDecimal(); json.color.h = state.getHue().toBigDecimal();
@ -318,12 +320,15 @@ public class JSONSchemaLight extends AbstractRawSchemaLight {
listener.updateChannelState(buildChannelUID(COLOR_MODE_CHANNEL_ID), colorModeValue.getChannelState()); listener.updateChannelState(buildChannelUID(COLOR_MODE_CHANNEL_ID), colorModeValue.getChannelState());
if (hasColorChannel) { ComponentChannel localBrightnessChannel = brightnessChannel;
listener.updateChannelState(buildChannelUID(COLOR_CHANNEL_ID), colorValue.getChannelState()); ComponentChannel localColorChannel = colorChannel;
} else if (brightnessChannel != null) { if (localColorChannel != null) {
listener.updateChannelState(buildChannelUID(BRIGHTNESS_CHANNEL_ID), brightnessValue.getChannelState()); listener.updateChannelState(localColorChannel.getChannel().getUID(), colorValue.getChannelState());
} else if (localBrightnessChannel != null) {
listener.updateChannelState(localBrightnessChannel.getChannel().getUID(),
brightnessValue.getChannelState());
} else { } else {
listener.updateChannelState(buildChannelUID(ON_OFF_CHANNEL_ID), onOffValue.getChannelState()); listener.updateChannelState(onOffChannel.getChannel().getUID(), onOffValue.getChannelState());
} }
} }
} }

View File

@ -228,10 +228,10 @@ public abstract class Light extends AbstractComponent<Light.ChannelConfiguration
} }
protected final boolean optimistic; protected final boolean optimistic;
protected boolean hasColorChannel = false;
protected @Nullable ComponentChannel onOffChannel; protected @Nullable ComponentChannel onOffChannel;
protected @Nullable ComponentChannel brightnessChannel; protected @Nullable ComponentChannel brightnessChannel;
protected @Nullable ComponentChannel colorChannel;
// State has to be stored here, in order to mux multiple // State has to be stored here, in order to mux multiple
// MQTT sources into single OpenHAB channels // MQTT sources into single OpenHAB channels
@ -292,6 +292,7 @@ public abstract class Light extends AbstractComponent<Light.ChannelConfiguration
colorTempValue = new NumberValue(min, max, BigDecimal.ONE, Units.MIRED); colorTempValue = new NumberValue(min, max, BigDecimal.ONE, Units.MIRED);
buildChannels(); buildChannels();
finalizeChannels();
} }
protected abstract void buildChannels(); protected abstract void buildChannels();

View File

@ -121,6 +121,7 @@ public class Lock extends AbstractComponent<Lock.ChannelConfiguration> {
} }
return true; return true;
}).build(); }).build();
finalizeChannels();
} }
private void autoUpdate(boolean locking) { private void autoUpdate(boolean locking) {

View File

@ -71,7 +71,7 @@ public class Number extends AbstractComponent<Number.ChannelConfiguration> {
} }
public Number(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) { public Number(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic
: channelConfiguration.stateTopic.isBlank(); : channelConfiguration.stateTopic.isBlank();
@ -89,5 +89,6 @@ public class Number extends AbstractComponent<Number.ChannelConfiguration> {
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(), .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos(), channelConfiguration.commandTemplate) channelConfiguration.getQos(), channelConfiguration.commandTemplate)
.build(); .build();
finalizeChannels();
} }
} }

View File

@ -46,7 +46,7 @@ public class Scene extends AbstractComponent<Scene.ChannelConfiguration> {
} }
public Scene(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) { public Scene(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
TextValue value = new TextValue(new String[] { channelConfiguration.payloadOn }); TextValue value = new TextValue(new String[] { channelConfiguration.payloadOn });
@ -55,5 +55,6 @@ public class Scene extends AbstractComponent<Scene.ChannelConfiguration> {
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(), .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos()) channelConfiguration.getQos())
.withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build(); .withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build();
finalizeChannels();
} }
} }

View File

@ -56,7 +56,7 @@ public class Select extends AbstractComponent<Select.ChannelConfiguration> {
} }
public Select(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) { public Select(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic
: channelConfiguration.stateTopic.isBlank(); : channelConfiguration.stateTopic.isBlank();
@ -73,5 +73,6 @@ public class Select extends AbstractComponent<Select.ChannelConfiguration> {
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(), .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos(), channelConfiguration.commandTemplate) channelConfiguration.getQos(), channelConfiguration.commandTemplate)
.build(); .build();
finalizeChannels();
} }
} }

View File

@ -69,7 +69,7 @@ public class Sensor extends AbstractComponent<Sensor.ChannelConfiguration> {
} }
public Sensor(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) { public Sensor(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
Value value; Value value;
String uom = channelConfiguration.unitOfMeasurement; String uom = channelConfiguration.unitOfMeasurement;
@ -96,6 +96,7 @@ public class Sensor extends AbstractComponent<Sensor.ChannelConfiguration> {
buildChannel(SENSOR_CHANNEL_ID, type, value, getName(), getListener(componentConfiguration, value)) buildChannel(SENSOR_CHANNEL_ID, type, value, getName(), getListener(componentConfiguration, value))
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())// .stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())//
.trigger(trigger).build(); .trigger(trigger).build();
finalizeChannels();
} }
private ChannelStateUpdateListener getListener(ComponentFactory.ComponentConfiguration componentConfiguration, private ChannelStateUpdateListener getListener(ComponentFactory.ComponentConfiguration componentConfiguration,

View File

@ -61,7 +61,7 @@ public class Switch extends AbstractComponent<Switch.ChannelConfiguration> {
} }
public Switch(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) { public Switch(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic
: channelConfiguration.stateTopic.isBlank(); : channelConfiguration.stateTopic.isBlank();
@ -79,5 +79,6 @@ public class Switch extends AbstractComponent<Switch.ChannelConfiguration> {
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(), .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos()) channelConfiguration.getQos())
.build(); .build();
finalizeChannels();
} }
} }

View File

@ -23,6 +23,7 @@ import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.values.OnOffValue; import org.openhab.binding.mqtt.generic.values.OnOffValue;
import org.openhab.binding.mqtt.generic.values.PercentageValue; import org.openhab.binding.mqtt.generic.values.PercentageValue;
import org.openhab.binding.mqtt.generic.values.TextValue; import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType; import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantChannelTransformation; import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantChannelTransformation;
import org.openhab.binding.mqtt.homeassistant.internal.exception.UnsupportedComponentException; import org.openhab.binding.mqtt.homeassistant.internal.exception.UnsupportedComponentException;
@ -85,8 +86,7 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
if (channelConfiguration.redTemplate != null && channelConfiguration.greenTemplate != null if (channelConfiguration.redTemplate != null && channelConfiguration.greenTemplate != null
&& channelConfiguration.blueTemplate != null) { && channelConfiguration.blueTemplate != null) {
hasColorChannel = true; colorChannel = buildChannel(COLOR_CHANNEL_ID, ComponentChannelType.COLOR, colorValue, "Color", this)
buildChannel(COLOR_CHANNEL_ID, ComponentChannelType.COLOR, colorValue, "Color", this)
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleCommand(command)).build(); .commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleCommand(command)).build();
} else if (channelConfiguration.brightnessTemplate != null) { } else if (channelConfiguration.brightnessTemplate != null) {
brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, ComponentChannelType.DIMMER, brightnessValue, brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, ComponentChannelType.DIMMER, brightnessValue,
@ -127,7 +127,7 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
binding.put(TemplateVariables.BRIGHTNESS, binding.put(TemplateVariables.BRIGHTNESS,
state.getBrightness().toBigDecimal().multiply(factor).intValue()); state.getBrightness().toBigDecimal().multiply(factor).intValue());
} }
if (hasColorChannel) { if (colorChannel != null) {
int[] rgb = ColorUtil.hsbToRgb(state); int[] rgb = ColorUtil.hsbToRgb(state);
binding.put(TemplateVariables.RED, rgb[0]); binding.put(TemplateVariables.RED, rgb[0]);
binding.put(TemplateVariables.GREEN, rgb[1]); binding.put(TemplateVariables.GREEN, rgb[1]);
@ -249,13 +249,15 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
colorValue.update(HSBType.fromRGB(red, green, blue)); colorValue.update(HSBType.fromRGB(red, green, blue));
} }
} }
ComponentChannel localBrightnessChannel = brightnessChannel;
if (hasColorChannel) { ComponentChannel localColorChannel = colorChannel;
listener.updateChannelState(buildChannelUID(COLOR_CHANNEL_ID), colorValue.getChannelState()); if (localColorChannel != null) {
} else if (brightnessChannel != null) { listener.updateChannelState(localColorChannel.getChannel().getUID(), colorValue.getChannelState());
listener.updateChannelState(buildChannelUID(BRIGHTNESS_CHANNEL_ID), brightnessValue.getChannelState()); } else if (localBrightnessChannel != null) {
listener.updateChannelState(localBrightnessChannel.getChannel().getUID(),
brightnessValue.getChannelState());
} else { } else {
listener.updateChannelState(buildChannelUID(ON_OFF_CHANNEL_ID), onOffValue.getChannelState()); listener.updateChannelState(onOffChannel.getChannel().getUID(), onOffValue.getChannelState());
} }
template = channelConfiguration.effectTemplate; template = channelConfiguration.effectTemplate;

View File

@ -290,6 +290,7 @@ public class Vacuum extends AbstractComponent<Vacuum.ChannelConfiguration> {
buildOptionalChannel(JSON_ATTRIBUTES_CH_ID, ComponentChannelType.STRING, new TextValue(), updateListener, null, buildOptionalChannel(JSON_ATTRIBUTES_CH_ID, ComponentChannelType.STRING, new TextValue(), updateListener, null,
null, channelConfiguration.jsonAttributesTemplate, channelConfiguration.jsonAttributesTopic); null, channelConfiguration.jsonAttributesTemplate, channelConfiguration.jsonAttributesTopic);
finalizeChannels();
} }
@Nullable @Nullable

View File

@ -33,7 +33,6 @@ import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.generic.tools.DelayedBatchProcessing; import org.openhab.binding.mqtt.generic.tools.DelayedBatchProcessing;
import org.openhab.binding.mqtt.generic.utils.FutureCollector; import org.openhab.binding.mqtt.generic.utils.FutureCollector;
import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants; import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents; import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents;
import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents.ComponentDiscovered; import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents.ComponentDiscovered;
import org.openhab.binding.mqtt.homeassistant.internal.HaID; import org.openhab.binding.mqtt.homeassistant.internal.HaID;
@ -43,6 +42,7 @@ import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactor
import org.openhab.binding.mqtt.homeassistant.internal.component.Update; import org.openhab.binding.mqtt.homeassistant.internal.component.Update;
import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory; import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory;
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException; import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.config.core.validation.ConfigValidationException; import org.openhab.core.config.core.validation.ConfigValidationException;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.Channel; import org.openhab.core.thing.Channel;
@ -98,6 +98,8 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
private final Gson gson; private final Gson gson;
protected final Map<@Nullable String, AbstractComponent<?>> haComponents = new HashMap<>(); protected final Map<@Nullable String, AbstractComponent<?>> haComponents = new HashMap<>();
protected final Map<@Nullable String, AbstractComponent<?>> haComponentsByUniqueId = new HashMap<>();
protected final Map<ChannelUID, ChannelState> channelStates = new HashMap<>();
protected HandlerConfiguration config = new HandlerConfiguration(); protected HandlerConfiguration config = new HandlerConfiguration();
private Set<HaID> discoveryHomeAssistantIDs = new HashSet<>(); private Set<HaID> discoveryHomeAssistantIDs = new HashSet<>();
@ -147,13 +149,21 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
ThingTypeUID typeID = getThing().getThingTypeUID(); ThingTypeUID typeID = getThing().getThingTypeUID();
for (Channel channel : thing.getChannels()) { for (Channel channel : thing.getChannels()) {
final String groupID = channel.getUID().getGroupId(); final String groupID = channel.getUID().getGroupId();
// Already restored component? if (groupID != null) {
@Nullable // Already restored component via another channel in the component?
AbstractComponent<?> component = haComponents.get(groupID); AbstractComponent<?> component = haComponents.get(groupID);
if (component != null) { if (component != null) {
continue;
}
}
Configuration channelConfig = channel.getConfiguration();
if (!channelConfig.containsKey("component")
|| !channelConfig.containsKey("objectid") | !channelConfig.containsKey("config")) {
// Must be a secondary channel
continue; continue;
} }
HaID haID = HaID.fromConfig(config.basetopic, channel.getConfiguration());
HaID haID = HaID.fromConfig(config.basetopic, channelConfig);
if (!config.topics.contains(haID.getTopic())) { if (!config.topics.contains(haID.getTopic())) {
// don't add a component for this channel that isn't configured on the thing // don't add a component for this channel that isn't configured on the thing
@ -164,21 +174,17 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
discoveryHomeAssistantIDs.add(haID); discoveryHomeAssistantIDs.add(haID);
ThingUID thingUID = channel.getUID().getThingUID(); ThingUID thingUID = channel.getUID().getThingUID();
String channelConfigurationJSON = (String) channel.getConfiguration().get("config"); String channelConfigurationJSON = (String) channelConfig.get("config");
if (channelConfigurationJSON == null) { try {
logger.warn("Provided channel does not have a 'config' configuration key!"); AbstractComponent<?> component = ComponentFactory.createComponent(thingUID, haID,
} else { channelConfigurationJSON, this, this, scheduler, gson, jinjava, newStyleChannels);
try { if (typeID.equals(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)) {
component = ComponentFactory.createComponent(thingUID, haID, channelConfigurationJSON, this, this, typeID = calculateThingTypeUID(component);
scheduler, gson, jinjava, newStyleChannels);
if (typeID.equals(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)) {
typeID = calculateThingTypeUID(component);
}
haComponents.put(component.getGroupId(), component);
} catch (ConfigurationException e) {
logger.error("Cannot restore component {}: {}", thing, e.getMessage());
} }
addComponent(component);
} catch (ConfigurationException e) {
logger.warn("Cannot restore component {}: {}", thing, e.getMessage());
} }
} }
if (updateThingType(typeID)) { if (updateThingType(typeID)) {
@ -241,27 +247,9 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
@Override @Override
public @Nullable ChannelState getChannelState(ChannelUID channelUID) { public @Nullable ChannelState getChannelState(ChannelUID channelUID) {
String componentId;
if (channelUID.isInGroup()) {
componentId = channelUID.getGroupId();
} else {
componentId = channelUID.getId();
}
AbstractComponent<?> component;
synchronized (haComponents) { // sync whenever discoverComponents is started synchronized (haComponents) { // sync whenever discoverComponents is started
component = haComponents.get(componentId); return channelStates.get(channelUID);
} }
if (component == null) {
component = haComponents.get("");
if (component == null) {
return null;
}
}
ComponentChannel componentChannel = component.getChannel(channelUID.getIdWithoutGroup());
if (componentChannel == null) {
return null;
}
return componentChannel.getState();
} }
/** /**
@ -289,14 +277,18 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
if (typeID.equals(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)) { if (typeID.equals(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)) {
typeID = calculateThingTypeUID(discovered); typeID = calculateThingTypeUID(discovered);
} }
String id = discovered.getGroupId(); AbstractComponent<?> known = haComponentsByUniqueId.get(discovered.getUniqueId());
AbstractComponent<?> known = haComponents.get(id);
// Is component already known? // Is component already known?
if (known != null) { if (known != null) {
if (discovered.getConfigHash() != known.getConfigHash()) { if (discovered.getConfigHash() != known.getConfigHash()) {
// Don't wait for the future to complete. We are also not interested in failures. // Don't wait for the future to complete. We are also not interested in failures.
// The component will be replaced in a moment. // The component will be replaced in a moment.
known.stop(); known.stop();
haComponentsByUniqueId.remove(discovered.getUniqueId());
haComponents.remove(known.getComponentId());
if (!known.getComponentId().equals(discovered.getComponentId())) {
discovered.resolveConflict();
}
} else { } else {
known.setConfigSeen(); known.setConfigSeen();
continue; continue;
@ -304,7 +296,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
} }
// Add component to the component map // Add component to the component map
haComponents.put(id, discovered); 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);
@ -392,6 +384,9 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
sortedComponents.stream().map(AbstractComponent::getChannels).flatMap(List::stream) sortedComponents.stream().map(AbstractComponent::getChannels).flatMap(List::stream)
.forEach(c -> thingBuilder.withChannel(c)); .forEach(c -> thingBuilder.withChannel(c));
channelStates.clear();
sortedComponents.forEach(c -> c.getChannelStates(channelStates));
updateThing(thingBuilder.build()); updateThing(thingBuilder.build());
} }
} }
@ -422,4 +417,18 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
properties = state.appendToProperties(properties); properties = state.appendToProperties(properties);
updateProperties(properties); updateProperties(properties);
} }
// should only be called when it's safe to access haComponents
private void addComponent(AbstractComponent component) {
AbstractComponent existing = haComponents.get(component.getComponentId());
if (existing != null) {
// rename the conflict
haComponents.remove(existing.getComponentId());
existing.resolveConflict();
component.resolveConflict();
haComponents.put(existing.getComponentId(), existing);
}
haComponents.put(component.getComponentId(), component);
haComponentsByUniqueId.put(component.getUniqueId(), component);
}
} }

View File

@ -5,7 +5,7 @@
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="channel-type:mqtt:ha-channel"> <config-description uri="channel-type:mqtt:ha-channel">
<parameter name="component" type="text" readOnly="true" required="true"> <parameter name="component" type="text" readOnly="true">
<label>Component</label> <label>Component</label>
<description>Home Assistant component type (e.g. binary_sensor, switch, light)</description> <description>Home Assistant component type (e.g. binary_sensor, switch, light)</description>
<default></default> <default></default>
@ -15,12 +15,12 @@
<description>Optional node name of the component</description> <description>Optional node name of the component</description>
<default></default> <default></default>
</parameter> </parameter>
<parameter name="objectid" type="text" readOnly="true" required="true"> <parameter name="objectid" type="text" readOnly="true">
<label>Object ID</label> <label>Object ID</label>
<description>Object ID of the component</description> <description>Object ID of the component</description>
<default></default> <default></default>
</parameter> </parameter>
<parameter name="config" type="text" readOnly="true" required="true"> <parameter name="config" type="text" readOnly="true">
<label>JSON Configuration</label> <label>JSON Configuration</label>
<description>The JSON configuration string received by the component via MQTT.</description> <description>The JSON configuration string received by the component via MQTT.</description>
<default></default> <default></default>

View File

@ -65,7 +65,7 @@ public class BinarySensorTests extends AbstractComponentTests {
assertThat(component.channels.size(), is(1)); assertThat(component.channels.size(), is(1));
assertThat(component.getName(), is("onoffsensor")); assertThat(component.getName(), is("onoffsensor"));
assertThat(component.getGroupId(), is("sn1")); assertThat(component.getComponentId(), is("sn1"));
assertChannel(component, BinarySensor.SENSOR_CHANNEL_ID, "zigbee2mqtt/sensor/state", "", "onoffsensor", assertChannel(component, BinarySensor.SENSOR_CHANNEL_ID, "zigbee2mqtt/sensor/state", "", "onoffsensor",
OnOffValue.class); OnOffValue.class);

View File

@ -65,7 +65,7 @@ public class SensorTests extends AbstractComponentTests {
assertThat(component.channels.size(), is(1)); assertThat(component.channels.size(), is(1));
assertThat(component.getName(), is("sensor1")); assertThat(component.getName(), is("sensor1"));
assertThat(component.getGroupId(), is("sn1")); assertThat(component.getComponentId(), is("sn1"));
assertChannel(component, Sensor.SENSOR_CHANNEL_ID, "zigbee2mqtt/sensor/state", "", "sensor1", assertChannel(component, Sensor.SENSOR_CHANNEL_ID, "zigbee2mqtt/sensor/state", "", "sensor1",
NumberValue.class); NumberValue.class);

View File

@ -155,7 +155,7 @@ public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest {
// and add the types to the channelTypeProvider, like in the real Thing handler. // and add the types to the channelTypeProvider, like in the real Thing handler.
final CountDownLatch latch = new CountDownLatch(1); final CountDownLatch latch = new CountDownLatch(1);
ComponentDiscovered cd = (haID, c) -> { ComponentDiscovered cd = (haID, c) -> {
haComponents.put(c.getGroupId(), c); haComponents.put(c.getComponentId(), c);
latch.countDown(); latch.countDown();
}; };
@ -174,11 +174,10 @@ public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest {
assertNull(failure); assertNull(failure);
assertThat(haComponents.size(), is(1)); assertThat(haComponents.size(), is(1));
String channelGroupId = "switch_" + ThingChannelConstants.TEST_HOME_ASSISTANT_THING.getId(); String componentId = ThingChannelConstants.TEST_HOME_ASSISTANT_THING.getId();
String channelId = Switch.SWITCH_CHANNEL_ID; String channelId = Switch.SWITCH_CHANNEL_ID;
State value = haComponents.get(channelGroupId).getChannel(channelGroupId).getState().getCache() State value = haComponents.get(componentId).getChannel(channelId).getState().getCache().getChannelState();
.getChannelState();
assertThat(value, is(UnDefType.UNDEF)); assertThat(value, is(UnDefType.UNDEF));
haComponents.values().stream().map(e -> e.start(haConnection, scheduler, 100)) haComponents.values().stream().map(e -> e.start(haConnection, scheduler, 100))
@ -191,7 +190,7 @@ public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest {
verify(channelStateUpdateListener, timeout(4000).times(1)).updateChannelState(any(), any()); verify(channelStateUpdateListener, timeout(4000).times(1)).updateChannelState(any(), any());
// Value should be ON now. // Value should be ON now.
value = haComponents.get(channelGroupId).getChannel(channelGroupId).getState().getCache().getChannelState(); value = haComponents.get(componentId).getChannel(channelId).getState().getCache().getChannelState();
assertThat(value, is(OnOffType.ON)); assertThat(value, is(OnOffType.ON));
} }
} }