Fix calculate readOnly field in stateDescription for items with more than one linked channel (#4838) (#4845)

Signed-off-by: Maksym Krasovskyi <maxx@maxx.net.ua>
pull/4381/merge
maxx-ukoo 2025-06-09 22:57:27 +03:00 committed by GitHub
parent 95cb442524
commit 48e20d660a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 176 additions and 8 deletions

View File

@ -91,24 +91,33 @@ public class ChannelStateDescriptionProvider implements StateDescriptionFragment
private @Nullable StateDescription getStateDescription(String itemName, @Nullable Locale locale) {
Set<ChannelUID> 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")

View File

@ -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());
}
}