[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
protected final boolean readOnly;
protected final ChannelUID channelUID;
protected final ChannelConfig config;
/** Channel value **/
protected final Value cachedValue;
// Runtime variables
protected ChannelUID channelUID;
private @Nullable MqttBrokerConnection connection;
protected final ChannelTransformation incomingTransformation;
protected final ChannelTransformation outgoingTransformation;
@ -132,6 +132,11 @@ public class ChannelState implements MqttMessageSubscriber {
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
*

View File

@ -58,7 +58,7 @@ import org.openhab.core.types.StateDescription;
@NonNullByDefault
public class ComponentChannel {
private final ChannelState channelState;
private final Channel channel;
private Channel channel;
private final @Nullable StateDescription stateDescription;
private final @Nullable CommandDescription commandDescription;
private final ChannelStateUpdateListener channelStateUpdateListener;
@ -77,6 +77,18 @@ public class ComponentChannel {
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() {
return channelState;
}

View File

@ -24,6 +24,7 @@ import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
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.MqttChannelStateDescriptionProvider;
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.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelGroupUID;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.binding.generic.ChannelTransformation;
import org.openhab.core.thing.type.ChannelDefinition;
@ -65,7 +65,6 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
// Component location fields
protected final ComponentConfiguration componentConfiguration;
protected final @Nullable ChannelGroupUID channelGroupUID;
protected final HaID haID;
// Channels and configuration
@ -79,14 +78,10 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
protected final C channelConfiguration;
protected boolean configSeen;
protected final boolean singleChannelComponent;
protected final String groupId;
protected final boolean newStyleChannels;
protected final String uniqueId;
public AbstractComponent(ComponentFactory.ComponentConfiguration componentConfiguration, Class<C> clazz,
boolean newStyleChannels) {
this(componentConfiguration, clazz, newStyleChannels, false);
}
protected @Nullable String groupId;
protected String componentId;
/**
* 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)
*/
public AbstractComponent(ComponentFactory.ComponentConfiguration componentConfiguration, Class<C> clazz,
boolean newStyleChannels, boolean singleChannelComponent) {
boolean newStyleChannels) {
this.componentConfiguration = componentConfiguration;
this.singleChannelComponent = newStyleChannels && singleChannelComponent;
this.newStyleChannels = newStyleChannels;
this.channelConfigurationJson = componentConfiguration.getConfigJSON();
this.channelConfiguration = componentConfiguration.getConfig(clazz);
@ -109,14 +104,16 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
this.haID = componentConfiguration.getHaID();
String name = channelConfiguration.getName();
if (name != null && !name.isEmpty()) {
groupId = this.haID.getGroupId(channelConfiguration.getUniqueId(), newStyleChannels);
this.channelGroupUID = this.singleChannelComponent ? null
: new ChannelGroupUID(componentConfiguration.getThingUID(), groupId);
if (newStyleChannels) {
// try for a simple component/group ID first; if there are conflicts
// (components of different types, but the same object id)
// we'll resolve them later
groupId = componentId = haID.objectID.replace('-', '_');
} else if (name != null && !name.isEmpty()) {
groupId = componentId = this.haID.getGroupId(channelConfiguration.getUniqueId(), false);
} else {
this.groupId = this.singleChannelComponent ? haID.component : "";
this.channelGroupUID = null;
groupId = null;
componentId = "";
}
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,
Value valueState, String label, ChannelStateUpdateListener channelStateUpdateListener) {
if (singleChannelComponent) {
channelID = groupId;
if (groupId == null) {
channelID = componentId;
}
return new ComponentChannel.Builder(this, channelID, channelType.getChannelTypeUID(), valueState, label,
channelStateUpdateListener);
@ -216,15 +233,19 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
}
public ChannelUID buildChannelUID(String channelID) {
final ChannelGroupUID groupUID = channelGroupUID;
if (groupUID != null) {
return new ChannelUID(groupUID, channelID);
final String localGroupID = groupId;
if (localGroupID != null) {
return new ChannelUID(componentConfiguration.getThingUID(), localGroupID, channelID);
}
return new ChannelUID(componentConfiguration.getThingUID(), channelID);
}
public String getGroupId() {
return groupId;
public String getComponentId() {
return componentId;
}
public String getUniqueId() {
return uniqueId;
}
/**
@ -273,7 +294,7 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
* Return the channel group type.
*/
public @Nullable ChannelGroupType getChannelGroupType(String prefix) {
if (channelGroupUID == null) {
if (groupId == null) {
return null;
}
return ChannelGroupTypeBuilder.instance(getChannelGroupTypeUID(prefix), getName())
@ -281,7 +302,7 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
}
public List<ChannelDefinition> getChannelDefinitions() {
if (channelGroupUID != null) {
if (groupId != null) {
return List.of();
}
return getAllChannelDefinitions();
@ -295,6 +316,10 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
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
* 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.
*/
public @Nullable ChannelGroupDefinition getGroupDefinition(String prefix) {
if (channelGroupUID == null) {
String localGroupId = groupId;
if (localGroupId == null) {
return null;
}
return new ChannelGroupDefinition(channelGroupUID.getId(), getChannelGroupTypeUID(prefix), getName(), null);
return new ChannelGroupDefinition(localGroupId, getChannelGroupTypeUID(prefix), getName(), null);
}
public boolean hasGroup() {
return channelGroupUID != null;
return groupId != null;
}
public HaID getHaID() {

View File

@ -97,5 +97,6 @@ public class AlarmControlPanel extends AbstractComponent<AlarmControlPanel.Chann
componentConfiguration.getUpdateListener())
.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) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true);
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
OnOffValue value = new OnOffValue(channelConfiguration.payloadOn, channelConfiguration.payloadOff);
@ -77,6 +77,7 @@ public class BinarySensor extends AbstractComponent<BinarySensor.ChannelConfigur
getListener(componentConfiguration, value))
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
.withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build();
finalizeChannels();
}
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) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true);
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
TextValue value = new TextValue(new String[] { channelConfiguration.payloadPress });
@ -57,5 +57,6 @@ public class Button extends AbstractComponent<Button.ChannelConfiguration> {
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.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(),
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,
channelConfiguration.powerCommandTopic, null, null, null);
finalizeChannels();
}
@Nullable

View File

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

View File

@ -112,6 +112,7 @@ public class DefaultSchemaLight extends Light {
.build();
}
boolean hasColorChannel = false;
if (channelConfiguration.rgbStateTopic != null || channelConfiguration.rgbCommandTopic != null) {
hasColorChannel = true;
hiddenChannels.add(rgbChannel = buildChannel(RGB_CHANNEL_ID, ComponentChannelType.COLOR,
@ -167,7 +168,7 @@ public class DefaultSchemaLight extends Light {
if (localBrightnessChannel != null) {
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())
.commandFilter(this::handleColorCommand).build();
} else if (localBrightnessChannel != null) {
@ -280,74 +281,74 @@ public class DefaultSchemaLight extends Light {
@Override
public void updateChannelState(ChannelUID channel, State state) {
ChannelStateUpdateListener listener = this.channelStateUpdateListener;
switch (channel.getIdWithoutGroup()) {
case ON_OFF_CHANNEL_ID:
if (hasColorChannel) {
HSBType newOnState = colorValue.getChannelState() instanceof HSBType
? (HSBType) colorValue.getChannelState()
: HSBType.WHITE;
if (state.equals(OnOffType.ON)) {
colorValue.update(newOnState);
}
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;
String id = channel.getIdWithoutGroup();
ComponentChannel localBrightnessChannel = brightnessChannel;
ComponentChannel localColorChannel = colorChannel;
ChannelUID primaryChannelUID;
if (localColorChannel != null) {
primaryChannelUID = localColorChannel.getChannel().getUID();
} else if (localBrightnessChannel != null) {
primaryChannelUID = localBrightnessChannel.getChannel().getUID();
} else {
primaryChannelUID = onOffChannel.getChannel().getUID();
}
// 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) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true);
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
if (!"trigger".equals(channelConfiguration.automationType)) {
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(),
componentConfiguration.getUpdateListener())
.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.util.List;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@ -118,6 +119,8 @@ public class Fan extends AbstractComponent<Fan.ChannelConfiguration> implements
private final PercentageValue speedValue;
private State rawSpeedState;
private final ComponentChannel onOffChannel;
private final @Nullable ComponentChannel speedChannel;
private final ComponentChannel primaryChannel;
private final ChannelStateUpdateListener channelStateUpdateListener;
public Fan(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
@ -144,11 +147,15 @@ public class Fan extends AbstractComponent<Fan.ChannelConfiguration> implements
if (channelConfiguration.percentageCommandTopic != null) {
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)
.commandTopic(channelConfiguration.percentageCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos(), channelConfiguration.percentageCommandTemplate)
.commandFilter(this::handlePercentageCommand).build();
} else {
primaryChannel = onOffChannel;
speedChannel = null;
}
List<String> presetModes = channelConfiguration.presetModes;
@ -184,6 +191,7 @@ public class Fan extends AbstractComponent<Fan.ChannelConfiguration> implements
channelConfiguration.getQos(), channelConfiguration.directionCommandTemplate)
.build();
}
finalizeChannels();
}
private boolean handlePercentageCommand(Command command) {
@ -197,7 +205,7 @@ public class Fan extends AbstractComponent<Fan.ChannelConfiguration> implements
@Override
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)) {
// Assume full on if we don't yet know the actual speed
state = PercentType.HUNDRED;
@ -206,7 +214,7 @@ public class Fan extends AbstractComponent<Fan.ChannelConfiguration> implements
} else {
state = rawSpeedState;
}
} else if (channel.getIdWithoutGroup().equals(SPEED_CHANNEL_ID)) {
} else if (Objects.requireNonNull(speedChannel).getChannel().getUID().equals(channel)) {
rawSpeedState = state;
if (onOffValue.getChannelState().equals(OnOffType.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);
channelStateUpdateListener.updateChannelState(buildChannelUID(SPEED_CHANNEL_ID), state);
channelStateUpdateListener.updateChannelState(primaryChannel.getChannel().getUID(), state);
}
@Override

View File

@ -21,6 +21,7 @@ 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.ComponentChannel;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
@ -77,6 +78,7 @@ public class JSONSchemaLight extends AbstractRawSchemaLight {
@Override
protected void buildChannels() {
boolean hasColorChannel = false;
List<LightColorMode> supportedColorModes = channelConfiguration.supportedColorModes;
if (supportedColorModes != null) {
if (LightColorMode.hasColorChannel(supportedColorModes)) {
@ -99,7 +101,7 @@ public class JSONSchemaLight extends AbstractRawSchemaLight {
}
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();
} else if (channelConfiguration.brightness) {
brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, ComponentChannelType.DIMMER, brightnessValue,
@ -144,7 +146,7 @@ public class JSONSchemaLight extends AbstractRawSchemaLight {
.divide(new BigDecimal(100), MathContext.DECIMAL128).intValue();
}
if (hasColorChannel) {
if (colorChannel != null) {
json.color = new JSONState.Color();
if (channelConfiguration.supportedColorModes.contains(LightColorMode.COLOR_MODE_HS)) {
json.color.h = state.getHue().toBigDecimal();
@ -318,12 +320,15 @@ public class JSONSchemaLight extends AbstractRawSchemaLight {
listener.updateChannelState(buildChannelUID(COLOR_MODE_CHANNEL_ID), colorModeValue.getChannelState());
if (hasColorChannel) {
listener.updateChannelState(buildChannelUID(COLOR_CHANNEL_ID), colorValue.getChannelState());
} else if (brightnessChannel != null) {
listener.updateChannelState(buildChannelUID(BRIGHTNESS_CHANNEL_ID), brightnessValue.getChannelState());
ComponentChannel localBrightnessChannel = brightnessChannel;
ComponentChannel localColorChannel = colorChannel;
if (localColorChannel != null) {
listener.updateChannelState(localColorChannel.getChannel().getUID(), colorValue.getChannelState());
} else if (localBrightnessChannel != null) {
listener.updateChannelState(localBrightnessChannel.getChannel().getUID(),
brightnessValue.getChannelState());
} 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 boolean hasColorChannel = false;
protected @Nullable ComponentChannel onOffChannel;
protected @Nullable ComponentChannel brightnessChannel;
protected @Nullable ComponentChannel colorChannel;
// State has to be stored here, in order to mux multiple
// 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);
buildChannels();
finalizeChannels();
}
protected abstract void buildChannels();

View File

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

View File

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

View File

@ -69,7 +69,7 @@ public class Sensor extends AbstractComponent<Sensor.ChannelConfiguration> {
}
public Sensor(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true);
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
Value value;
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))
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())//
.trigger(trigger).build();
finalizeChannels();
}
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) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true);
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic
: channelConfiguration.stateTopic.isBlank();
@ -79,5 +79,6 @@ public class Switch extends AbstractComponent<Switch.ChannelConfiguration> {
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.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.PercentageValue;
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.HomeAssistantChannelTransformation;
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
&& channelConfiguration.blueTemplate != null) {
hasColorChannel = true;
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(command -> handleCommand(command)).build();
} else if (channelConfiguration.brightnessTemplate != null) {
brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, ComponentChannelType.DIMMER, brightnessValue,
@ -127,7 +127,7 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
binding.put(TemplateVariables.BRIGHTNESS,
state.getBrightness().toBigDecimal().multiply(factor).intValue());
}
if (hasColorChannel) {
if (colorChannel != null) {
int[] rgb = ColorUtil.hsbToRgb(state);
binding.put(TemplateVariables.RED, rgb[0]);
binding.put(TemplateVariables.GREEN, rgb[1]);
@ -249,13 +249,15 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
colorValue.update(HSBType.fromRGB(red, green, blue));
}
}
if (hasColorChannel) {
listener.updateChannelState(buildChannelUID(COLOR_CHANNEL_ID), colorValue.getChannelState());
} else if (brightnessChannel != null) {
listener.updateChannelState(buildChannelUID(BRIGHTNESS_CHANNEL_ID), brightnessValue.getChannelState());
ComponentChannel localBrightnessChannel = brightnessChannel;
ComponentChannel localColorChannel = colorChannel;
if (localColorChannel != null) {
listener.updateChannelState(localColorChannel.getChannel().getUID(), colorValue.getChannelState());
} else if (localBrightnessChannel != null) {
listener.updateChannelState(localBrightnessChannel.getChannel().getUID(),
brightnessValue.getChannelState());
} else {
listener.updateChannelState(buildChannelUID(ON_OFF_CHANNEL_ID), onOffValue.getChannelState());
listener.updateChannelState(onOffChannel.getChannel().getUID(), onOffValue.getChannelState());
}
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,
null, channelConfiguration.jsonAttributesTemplate, channelConfiguration.jsonAttributesTopic);
finalizeChannels();
}
@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.utils.FutureCollector;
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.ComponentDiscovered;
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.config.ChannelConfigurationTypeAdapterFactory;
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.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.Channel;
@ -98,6 +98,8 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
private final Gson gson;
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();
private Set<HaID> discoveryHomeAssistantIDs = new HashSet<>();
@ -147,13 +149,21 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
ThingTypeUID typeID = getThing().getThingTypeUID();
for (Channel channel : thing.getChannels()) {
final String groupID = channel.getUID().getGroupId();
// Already restored component?
@Nullable
AbstractComponent<?> component = haComponents.get(groupID);
if (component != null) {
if (groupID != null) {
// Already restored component via another channel in the component?
AbstractComponent<?> component = haComponents.get(groupID);
if (component != null) {
continue;
}
}
Configuration channelConfig = channel.getConfiguration();
if (!channelConfig.containsKey("component")
|| !channelConfig.containsKey("objectid") | !channelConfig.containsKey("config")) {
// Must be a secondary channel
continue;
}
HaID haID = HaID.fromConfig(config.basetopic, channel.getConfiguration());
HaID haID = HaID.fromConfig(config.basetopic, channelConfig);
if (!config.topics.contains(haID.getTopic())) {
// 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);
ThingUID thingUID = channel.getUID().getThingUID();
String channelConfigurationJSON = (String) channel.getConfiguration().get("config");
if (channelConfigurationJSON == null) {
logger.warn("Provided channel does not have a 'config' configuration key!");
} else {
try {
component = ComponentFactory.createComponent(thingUID, haID, channelConfigurationJSON, this, this,
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());
String channelConfigurationJSON = (String) channelConfig.get("config");
try {
AbstractComponent<?> component = ComponentFactory.createComponent(thingUID, haID,
channelConfigurationJSON, this, this, scheduler, gson, jinjava, newStyleChannels);
if (typeID.equals(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)) {
typeID = calculateThingTypeUID(component);
}
addComponent(component);
} catch (ConfigurationException e) {
logger.warn("Cannot restore component {}: {}", thing, e.getMessage());
}
}
if (updateThingType(typeID)) {
@ -241,27 +247,9 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
@Override
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
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)) {
typeID = calculateThingTypeUID(discovered);
}
String id = discovered.getGroupId();
AbstractComponent<?> known = haComponents.get(id);
AbstractComponent<?> known = haComponentsByUniqueId.get(discovered.getUniqueId());
// Is component already known?
if (known != null) {
if (discovered.getConfigHash() != known.getConfigHash()) {
// Don't wait for the future to complete. We are also not interested in failures.
// The component will be replaced in a moment.
known.stop();
haComponentsByUniqueId.remove(discovered.getUniqueId());
haComponents.remove(known.getComponentId());
if (!known.getComponentId().equals(discovered.getComponentId())) {
discovered.resolveConflict();
}
} else {
known.setConfigSeen();
continue;
@ -304,7 +296,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
}
// Add component to the component map
haComponents.put(id, discovered);
addComponent(discovered);
// Start component / Subscribe to channel topics
discovered.start(connection, scheduler, 0).exceptionally(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)
.forEach(c -> thingBuilder.withChannel(c));
channelStates.clear();
sortedComponents.forEach(c -> c.getChannelStates(channelStates));
updateThing(thingBuilder.build());
}
}
@ -422,4 +417,18 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
properties = state.appendToProperties(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">
<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>
<description>Home Assistant component type (e.g. binary_sensor, switch, light)</description>
<default></default>
@ -15,12 +15,12 @@
<description>Optional node name of the component</description>
<default></default>
</parameter>
<parameter name="objectid" type="text" readOnly="true" required="true">
<parameter name="objectid" type="text" readOnly="true">
<label>Object ID</label>
<description>Object ID of the component</description>
<default></default>
</parameter>
<parameter name="config" type="text" readOnly="true" required="true">
<parameter name="config" type="text" readOnly="true">
<label>JSON Configuration</label>
<description>The JSON configuration string received by the component via MQTT.</description>
<default></default>

View File

@ -65,7 +65,7 @@ public class BinarySensorTests extends AbstractComponentTests {
assertThat(component.channels.size(), is(1));
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",
OnOffValue.class);

View File

@ -65,7 +65,7 @@ public class SensorTests extends AbstractComponentTests {
assertThat(component.channels.size(), is(1));
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",
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.
final CountDownLatch latch = new CountDownLatch(1);
ComponentDiscovered cd = (haID, c) -> {
haComponents.put(c.getGroupId(), c);
haComponents.put(c.getComponentId(), c);
latch.countDown();
};
@ -174,11 +174,10 @@ public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest {
assertNull(failure);
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;
State value = haComponents.get(channelGroupId).getChannel(channelGroupId).getState().getCache()
.getChannelState();
State value = haComponents.get(componentId).getChannel(channelId).getState().getCache().getChannelState();
assertThat(value, is(UnDefType.UNDEF));
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());
// 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));
}
}