]> git.basschouten.com Git - openhab-addons.git/commitdiff
[mqtt.homeassistant] fix newStyleChannels (#17491)
authorCody Cutrer <cody@cutrer.us>
Mon, 7 Oct 2024 21:28:50 +0000 (15:28 -0600)
committerGitHub <noreply@github.com>
Mon, 7 Oct 2024 21:28:50 +0000 (23:28 +0200)
* [mqtt.homeassistant] fix newStyleChannels
* further simplify channel IDs

Signed-off-by: Cody Cutrer <cody@cutrer.us>
27 files changed:
bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/ChannelState.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannel.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponent.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/AlarmControlPanel.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/BinarySensor.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Button.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Camera.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Climate.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Cover.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/DefaultSchemaLight.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/DeviceTrigger.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Fan.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/JSONSchemaLight.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Light.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Lock.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Number.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Scene.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Select.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Sensor.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Switch.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/TemplateSchemaLight.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Vacuum.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/config/homeassistant-channel-config.xml
bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/BinarySensorTests.java
bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/SensorTests.java
itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/homeassistant/HomeAssistantMQTTImplementationTest.java

index 62361348d26aa746313b8464715e5271b1ba808f..681e5073e9af0d86cbb521b0d405af7c7a123004 100644 (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
      *
index 0131823c4ccdbeec336e49b461ad1a9fa133325a..ac27112bff1ba71e37a6904518734ccc8ad0fb5b 100644 (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;
     }
index 111322a25392188b7cf84847ea1ef2859feb7858..877303c91f103eb8171ec42d756dbcfa1a442076 100644 (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() {
index 026ce5048556cb871f984a109b7191b481b17a3f..a915c11f43b7c3ba40c10eea585bba920b0940b2 100644 (file)
@@ -97,5 +97,6 @@ public class AlarmControlPanel extends AbstractComponent<AlarmControlPanel.Chann
                     componentConfiguration.getUpdateListener())
                     .commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos()).build();
         }
+        finalizeChannels();
     }
 }
index b6df93f18d6cf7a63be883a7d8ddc4cd84df02cf..9d4c7b5b84c1d35ef9984d9afac51a12c3fb035f 100644 (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,
index cff40ea8e4ade0c990b8d48532c5740981c6935d..7ac20139deb8ee4f2f2f1906574067b893788991 100644 (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();
     }
 }
index c1e934d43245905eda2df02aff743a7e0e5c2270..6bfe3ad98b2bbd2c837fa89bd8b35140d6bae03c 100644 (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();
     }
 }
index 1c7726bc52ced69347f3e7565d214df80bfb9332..1a21945962c48ebfe02e540f1e3400d2d8f52db0 100644 (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
index 077a7b94cd51ed42a253fc371ce7fd872e9e0de5..f365ec33ead360b7a8f0c830186f38dfad60f623 100644 (file)
@@ -150,5 +150,6 @@ public class Cover extends AbstractComponent<Cover.ChannelConfiguration> {
                     }
                     return true;
                 }).build();
+        finalizeChannels();
     }
 }
index 9d412c1a14ba39f1267a00cd62c921f17c0cd884..ef5e12786273ac5fc2ec132896759a42d1d0ae4d 100644 (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");
+        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);
                 }
-                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));
+
+                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 {
-                    HSBType xyColor = HSBType.fromXY(x, y);
-                    colorValue.update(new HSBType(xyColor.getHue(), xyColor.getSaturation(), brightness));
+                    colorValue.update(new HSBType(DecimalType.ZERO, PercentType.ZERO,
+                            (PercentType) brightnessValue.getChannelState()));
                 }
-                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;
+                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
     }
 }
index 91ea0456b65a4dd738e778a61f3f09e4f1def04b..bd6beb4075391161a3c159f183bc20bab61c1d0f 100644 (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();
     }
 }
index 24f9d8ee166080bbf82a06a31a9fd3842ff70374..3c8ed4139aba008a15e3a2d49b877bc3fc0fa688 100644 (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
index ac218c337a86d751493d9266fd0ba93426b9205e..006031478c0071c2724a59ae1b2edde0b7fa11f3 100644 (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());
         }
     }
 }
index ca47f7e3df3eecb204e468f006bd23b49f3fe044..46f26d530f7570d449f26de628adb14a0a438ed3 100644 (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();
index ce3317256a620a707d71ace09cfbc64651cbaf47..bce7cef22889b565e14547c4eed66cc271e6859b 100644 (file)
@@ -121,6 +121,7 @@ public class Lock extends AbstractComponent<Lock.ChannelConfiguration> {
                     }
                     return true;
                 }).build();
+        finalizeChannels();
     }
 
     private void autoUpdate(boolean locking) {
index 1d03661f880e757d7521cd10e5d24114bd4c28e9..2912ac9b7cc37ce7409f1f0ffe37bde11d722d2d 100644 (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();
     }
 }
index d6e66a2bcb2583ed64c4509c89e1162a38642dd4..0ca4f1ab209c9d205b208b871bc863373ca9ab53 100644 (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();
     }
 }
index b3ceccafe67c38dbd77363570b23856f172cca18..ad650c63117b02ee0dc388d69804cc3e3c43704d 100644 (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();
     }
 }
index 9472490ca68c0f6c940bb95e7a4db1f27f6729b2..2320019b871d8e6c7f56059cbea9c83dec3aef6c 100644 (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,
index b1288c151d374edf91095200141f4a153553a865..0027ffe1c77ef89cc01e3ca3589cde79ebda126a 100644 (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();
     }
 }
index 89e46bd6295a94290a700e5049b2cebd483ccaf7..3a056cb36f8a24c01ff15dc20cc001ea273d599b 100644 (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;
index 0c5185267230ac6cfd863c8b80587c0da9b95c34..5862255f5ceaa8f67b3c465ac61c715e67a4671e 100644 (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
index 1bfb1801214f08acdf591902222622dbf49a2acd..1143a8e669061cd3de002310aea951ed3640612a 100644 (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);
-        }
-        if (component == null) {
-            component = haComponents.get("");
-            if (component == null) {
-                return null;
-            }
-        }
-        ComponentChannel componentChannel = component.getChannel(channelUID.getIdWithoutGroup());
-        if (componentChannel == null) {
-            return null;
+            return channelStates.get(channelUID);
         }
-        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);
+    }
 }
index d84743bc7331d9bfda6420e33480f159c7b9f352..d3092a82b11af499d98890cc3fcc7308ca10600e 100644 (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>
                        <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>
index 6c875e74b0cb787e79543f86dba0645b8f826d20..d11f1f82f477584de21bb92c3178922ada0a0ca3 100644 (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);
index c2abf74b39c07c0fe8c2fd938daf1f9774eed5b4..d712ee20317b354188f5769f82f4c4544a7cb861 100644 (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);
index 2f273661d3be1f8f1012179fd50c6d4a021203df..64781741ea182d3bdf7520cef1fd32c9d0381c6c 100644 (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));
     }
 }