From 48e20d660a398b1831c59455ba653fa087c8ab8d Mon Sep 17 00:00:00 2001 From: maxx-ukoo Date: Mon, 9 Jun 2025 22:57:27 +0300 Subject: [PATCH] Fix calculate readOnly field in stateDescription for items with more than one linked channel (#4838) (#4845) Signed-off-by: Maksym Krasovskyi --- .../ChannelStateDescriptionProvider.java | 25 ++- .../ChannelStateDescriptionProviderTest.java | 159 ++++++++++++++++++ 2 files changed, 176 insertions(+), 8 deletions(-) create mode 100644 bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/ChannelStateDescriptionProviderTest.java diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ChannelStateDescriptionProvider.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ChannelStateDescriptionProvider.java index 5f8070bb15..754b5a6349 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ChannelStateDescriptionProvider.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ChannelStateDescriptionProvider.java @@ -91,24 +91,33 @@ public class ChannelStateDescriptionProvider implements StateDescriptionFragment private @Nullable StateDescription getStateDescription(String itemName, @Nullable Locale locale) { Set boundChannels = itemChannelLinkRegistry.getBoundChannels(itemName); - if (!boundChannels.isEmpty()) { - ChannelUID channelUID = boundChannels.iterator().next(); + StateDescription stateDescription = null; + for (ChannelUID channelUID : boundChannels) { Channel channel = thingRegistry.getChannel(channelUID); if (channel != null) { - StateDescription stateDescription = null; ChannelType channelType = thingTypeRegistry.getChannelType(channel, locale); + StateDescription nextStateDescription = null; if (channelType != null) { - stateDescription = channelType.getState(); + nextStateDescription = channelType.getState(); } - StateDescription dynamicStateDescription = getDynamicStateDescription(channel, stateDescription, + StateDescription dynamicStateDescription = getDynamicStateDescription(channel, nextStateDescription, locale); if (dynamicStateDescription != null) { - return dynamicStateDescription; + nextStateDescription = dynamicStateDescription; + } + if (nextStateDescription != null) { + if (stateDescription == null) { + stateDescription = nextStateDescription; + } else { + if (stateDescription.isReadOnly() && !nextStateDescription.isReadOnly()) { + stateDescription = nextStateDescription; + break; + } + } } - return stateDescription; } } - return null; + return stateDescription; } @SuppressWarnings("PMD.CompareObjectsWithEquals") diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/ChannelStateDescriptionProviderTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/ChannelStateDescriptionProviderTest.java new file mode 100644 index 0000000000..2bc16e8676 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/ChannelStateDescriptionProviderTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.internal; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingRegistry; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; +import org.openhab.core.thing.type.ChannelType; +import org.openhab.core.thing.type.DynamicStateDescriptionProvider; +import org.openhab.core.thing.type.ThingTypeRegistry; +import org.openhab.core.types.StateDescription; +import org.openhab.core.types.StateDescriptionFragment; +import org.openhab.core.types.StateDescriptionFragmentBuilder; + +/** + * @author Maksym Krasovskyi - Initial contribution + */ +@NonNullByDefault +@ExtendWith(MockitoExtension.class) +public class ChannelStateDescriptionProviderTest { + + private @NonNullByDefault({}) ChannelStateDescriptionProvider channelStateDescriptionProvider; + private @Mock @NonNullByDefault({}) DynamicStateDescriptionProvider dynamicStateDescriptionProvider; + private @Mock @NonNullByDefault({}) ItemChannelLinkRegistry itemChannelLinkRegistry; + private @Mock @NonNullByDefault({}) ThingTypeRegistry thingTypeRegistry; + private @Mock @NonNullByDefault({}) ThingRegistry thingRegistry; + + private static final ChannelUID CHANNEL_UID_1 = new ChannelUID("channel:f:g:1"); + private static final ChannelUID CHANNEL_UID_2 = new ChannelUID("channel:f:g:2"); + + private static final String ITEM_1 = "item1"; + + @BeforeEach + public void setup() { + channelStateDescriptionProvider = new ChannelStateDescriptionProvider(itemChannelLinkRegistry, + thingTypeRegistry, thingRegistry); + } + + @ParameterizedTest + @CsvSource({ "true, true, true", "true, false, false", "false, true, false", "false, false, false" }) + public void testStateDescriptionFromMultipleChannels(Boolean channel1State, Boolean channel2State, + Boolean expectedItemState) { + when(itemChannelLinkRegistry.getBoundChannels(ITEM_1)) + .thenReturn(new HashSet<>(Arrays.asList(CHANNEL_UID_1, CHANNEL_UID_2))); + // Setup channel 1 + Channel channel1 = ChannelBuilder.create(CHANNEL_UID_1).build(); + when(thingRegistry.getChannel(CHANNEL_UID_1)).thenReturn(channel1); + StateDescription stateDescription1 = StateDescriptionFragmentBuilder.create().withMinimum(BigDecimal.ZERO) + .withMaximum(new BigDecimal(100)).withStep(BigDecimal.ONE).withReadOnly(channel1State).withPattern("%s") + .build().toStateDescription(); + + ChannelType channelType1 = Mockito.mock(ChannelType.class); + + when(channelType1.getState()).thenReturn(stateDescription1); + when(thingTypeRegistry.getChannelType(channel1, Locale.ENGLISH)).thenReturn(channelType1); + when(dynamicStateDescriptionProvider.getStateDescription(channel1, stateDescription1, Locale.ENGLISH)) + .thenReturn(StateDescriptionFragmentBuilder.create(stateDescription1).build().toStateDescription()); + + // Setup channel 2 + Channel channel2 = ChannelBuilder.create(CHANNEL_UID_2).build(); + when(thingRegistry.getChannel(CHANNEL_UID_2)).thenReturn(channel2); + StateDescription stateDescription2 = StateDescriptionFragmentBuilder.create().withMinimum(BigDecimal.ZERO) + .withMaximum(new BigDecimal(100)).withStep(BigDecimal.ONE).withReadOnly(channel2State).withPattern("%s") + .build().toStateDescription(); + ChannelType channelType2 = Mockito.mock(ChannelType.class); + when(channelType2.getState()).thenReturn(stateDescription2); + when(thingTypeRegistry.getChannelType(channel2, Locale.ENGLISH)).thenReturn(channelType2); + when(dynamicStateDescriptionProvider.getStateDescription(channel2, stateDescription2, Locale.ENGLISH)) + .thenReturn(StateDescriptionFragmentBuilder.create(stateDescription2).build().toStateDescription()); + + channelStateDescriptionProvider.addDynamicStateDescriptionProvider(dynamicStateDescriptionProvider); + + @Nullable + StateDescriptionFragment stateDescriptionResult = channelStateDescriptionProvider + .getStateDescriptionFragment(ITEM_1, Locale.ENGLISH); + assertNotNull(stateDescriptionResult); + assertEquals(expectedItemState, stateDescriptionResult.isReadOnly()); + } + + @Test + public void testStateDescriptionWithSingleReadOnlyChannel() { + when(itemChannelLinkRegistry.getBoundChannels(ITEM_1)).thenReturn(new HashSet<>(Arrays.asList(CHANNEL_UID_1))); + // Setup channel 1 + Channel channel1 = ChannelBuilder.create(CHANNEL_UID_1).build(); + when(thingRegistry.getChannel(CHANNEL_UID_1)).thenReturn(channel1); + StateDescription stateDescription1 = StateDescriptionFragmentBuilder.create().withMinimum(BigDecimal.ZERO) + .withMaximum(new BigDecimal(100)).withStep(BigDecimal.ONE).withReadOnly(Boolean.TRUE).withPattern("%s") + .build().toStateDescription(); + ChannelType channelType = Mockito.mock(ChannelType.class); + when(channelType.getState()).thenReturn(stateDescription1); + when(thingTypeRegistry.getChannelType(channel1, Locale.ENGLISH)).thenReturn(channelType); + + when(dynamicStateDescriptionProvider.getStateDescription(channel1, stateDescription1, Locale.ENGLISH)) + .thenReturn(StateDescriptionFragmentBuilder.create(stateDescription1).build().toStateDescription()); + + channelStateDescriptionProvider.addDynamicStateDescriptionProvider(dynamicStateDescriptionProvider); + + @Nullable + StateDescriptionFragment stateDescriptionResult = channelStateDescriptionProvider + .getStateDescriptionFragment(ITEM_1, Locale.ENGLISH); + assertNotNull(stateDescriptionResult); + assertTrue(stateDescriptionResult.isReadOnly()); + } + + @Test + public void testStateDescriptionWithSingleWriteOnlyChannel() { + when(itemChannelLinkRegistry.getBoundChannels(ITEM_1)).thenReturn(new HashSet<>(Arrays.asList(CHANNEL_UID_1))); + // Setup channel 1 + Channel channel1 = ChannelBuilder.create(CHANNEL_UID_1).build(); + when(thingRegistry.getChannel(CHANNEL_UID_1)).thenReturn(channel1); + StateDescription stateDescription1 = StateDescriptionFragmentBuilder.create().withMinimum(BigDecimal.ZERO) + .withMaximum(new BigDecimal(100)).withStep(BigDecimal.ONE).withReadOnly(Boolean.FALSE).withPattern("%s") + .build().toStateDescription(); + ChannelType channelType = Mockito.mock(ChannelType.class); + when(channelType.getState()).thenReturn(stateDescription1); + when(thingTypeRegistry.getChannelType(channel1, Locale.ENGLISH)).thenReturn(channelType); + + when(dynamicStateDescriptionProvider.getStateDescription(channel1, stateDescription1, Locale.ENGLISH)) + .thenReturn(StateDescriptionFragmentBuilder.create(stateDescription1).build().toStateDescription()); + + channelStateDescriptionProvider.addDynamicStateDescriptionProvider(dynamicStateDescriptionProvider); + + @Nullable + StateDescriptionFragment stateDescriptionResult = channelStateDescriptionProvider + .getStateDescriptionFragment(ITEM_1, Locale.ENGLISH); + assertNotNull(stateDescriptionResult); + assertFalse(stateDescriptionResult.isReadOnly()); + } +}