Add support for short-form expire property in Item Yaml config (#5252)

* Add support for short-form expire property in Item Yaml config

Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
This commit is contained in:
jimtng
2026-01-23 19:55:47 +01:00
committed by GitHub
parent 4b7ad7a23e
commit e206397672
5 changed files with 257 additions and 9 deletions
@@ -49,6 +49,7 @@ public class YamlItemDTO implements YamlElement, Cloneable {
public String icon;
public String format;
public String unit;
public String expire;
public Boolean autoupdate;
public List<@NonNull String> groups;
public Set<@NonNull String> tags;
@@ -195,6 +196,12 @@ public class YamlItemDTO implements YamlElement, Cloneable {
"item \"%s\": \"format\" field is redundant with pattern in \"stateDescription\" metadata; \"%s\" will be considered"
.formatted(name, pattern));
}
md = metadata.get("expire");
if (md != null && expire != null) {
addToList(warnings,
"item \"%s\": \"expire\" field is redundant with \"expire\" metadata; value \"%s\" will be considered"
.formatted(name, md.getValue()));
}
}
return ok;
}
@@ -266,8 +273,8 @@ public class YamlItemDTO implements YamlElement, Cloneable {
@Override
public int hashCode() {
return Objects.hash(name, getType(), group, label, icon, format, unit, autoupdate, getGroups(), getTags(),
channel);
return Objects.hash(name, getType(), group, label, icon, format, unit, expire, autoupdate, getGroups(),
getTags(), channel);
}
@Override
@@ -281,10 +288,10 @@ public class YamlItemDTO implements YamlElement, Cloneable {
return Objects.equals(name, other.name) && Objects.equals(getType(), other.getType())
&& Objects.equals(group, other.group) && Objects.equals(label, other.label)
&& Objects.equals(icon, other.icon) && Objects.equals(format, other.format)
&& Objects.equals(unit, other.unit) && Objects.equals(autoupdate, other.autoupdate)
&& Objects.equals(getGroups(), other.getGroups()) && Objects.equals(getTags(), other.getTags())
&& Objects.equals(channel, other.channel) && equalsChannels(channels, other.channels)
&& equalsMetadata(metadata, other.metadata);
&& Objects.equals(unit, other.unit) && Objects.equals(expire, other.expire)
&& Objects.equals(autoupdate, other.autoupdate) && Objects.equals(getGroups(), other.getGroups())
&& Objects.equals(getTags(), other.getTags()) && Objects.equals(channel, other.channel)
&& equalsChannels(channels, other.channels) && equalsMetadata(metadata, other.metadata);
}
private boolean equalsChannels(@Nullable Map<@NonNull String, @NonNull Map<@NonNull String, @NonNull Object>> first,
@@ -264,6 +264,7 @@ public class YamlItemProvider extends AbstractProvider<Item> implements ItemProv
boolean hasAutoUpdateMetadata = false;
boolean hasUnitMetadata = false;
boolean hasStateDescriptionMetadata = false;
boolean hasExpireMetadata = false;
if (itemDTO.metadata != null) {
for (Map.Entry<String, YamlMetadataDTO> entry : itemDTO.metadata.entrySet()) {
if ("autoupdate".equals(entry.getKey())) {
@@ -272,6 +273,8 @@ public class YamlItemProvider extends AbstractProvider<Item> implements ItemProv
hasUnitMetadata = true;
} else if ("stateDescription".equals(entry.getKey())) {
hasStateDescriptionMetadata = true;
} else if ("expire".equals(entry.getKey())) {
hasExpireMetadata = true;
}
Map<String, Object> config = entry.getValue().config;
if (itemDTO.format != null && "stateDescription".equals(entry.getKey())
@@ -305,6 +308,11 @@ public class YamlItemProvider extends AbstractProvider<Item> implements ItemProv
mdDTO.config = Map.of("pattern", itemDTO.format);
metadata.put("stateDescription", mdDTO);
}
if (!hasExpireMetadata && itemDTO.expire != null) {
YamlMetadataDTO mdDTO = new YamlMetadataDTO();
mdDTO.value = itemDTO.expire;
metadata.put("expire", mdDTO);
}
}
metaDataProvider.updateMetadata(modelName, itemName, metadata);
}
@@ -184,13 +184,28 @@ public class YamlItemFileConverter extends AbstractItemFileGenerator implements
Map<String, YamlMetadataDTO> metadataDto = new LinkedHashMap<>();
for (Metadata md : metadata) {
String namespace = md.getUID().getNamespace();
String value = md.getValue();
if ("autoupdate".equals(namespace)) {
dto.autoupdate = Boolean.valueOf(md.getValue());
// When autoupdate value is an empty string, treat it as not set since dto.autoupdate only accepts
// true/false
if (value != null && !value.isEmpty()) {
dto.autoupdate = Boolean.valueOf(value);
}
} else if ("unit".equals(namespace)) {
dto.unit = md.getValue();
dto.unit = value; // When unit value is empty string, keep it as empty string
} else if ("expire".equals(namespace)) {
Map<String, Object> configuration = md.getConfiguration();
if (configuration.isEmpty()) {
dto.expire = value; // When expire value is empty string, keep it as empty string
} else {
YamlMetadataDTO mdDto = new YamlMetadataDTO();
mdDto.value = value.isEmpty() ? null : value;
mdDto.config = configuration;
metadataDto.put(namespace, mdDto);
}
} else {
YamlMetadataDTO mdDto = new YamlMetadataDTO();
mdDto.value = md.getValue().isEmpty() ? null : md.getValue();
mdDto.value = value.isEmpty() ? null : value;
Map<String, Object> configuration = new LinkedHashMap<>();
String statePattern = null;
for (ConfigParameter param : getConfigurationParameters(md)) {
@@ -15,6 +15,7 @@ package org.openhab.core.model.yaml.internal.items;
import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -376,6 +377,34 @@ public class YamlItemDTOTest {
assertEquals(item1.hashCode(), item2.hashCode());
}
@Test
public void testEqualsWithExpire() throws IOException {
YamlItemDTO item1 = new YamlItemDTO();
YamlItemDTO item2 = new YamlItemDTO();
item1.name = "item_name";
item2.name = "item_name";
item1.type = "Switch";
item2.type = "Switch";
item1.expire = null;
item2.expire = null;
assertTrue(item1.equals(item2));
assertEquals(item1.hashCode(), item2.hashCode());
item1.expire = "5m";
item2.expire = null;
assertFalse(item1.equals(item2));
item1.expire = null;
item2.expire = "5m";
assertFalse(item1.equals(item2));
item1.expire = "5m";
item2.expire = "1h";
assertFalse(item1.equals(item2));
item2.expire = "5m";
assertTrue(item1.equals(item2));
assertEquals(item1.hashCode(), item2.hashCode());
}
@Test
public void testEqualsWithGroups() throws IOException {
YamlItemDTO item1 = new YamlItemDTO();
@@ -551,4 +580,26 @@ public class YamlItemDTOTest {
item2.metadata = Map.of("namespace", md3, "namespace2", md2);
assertFalse(item1.equals(item2));
}
@Test
public void testExpireMetadataWarning() throws IOException {
YamlItemDTO item = new YamlItemDTO();
item.name = "item_name";
item.type = "String";
item.expire = "10m";
YamlMetadataDTO md = new YamlMetadataDTO();
md.value = "5m";
item.metadata = Map.of("expire", md);
List<String> errors = new ArrayList<>();
List<String> warnings = new ArrayList<>();
assertTrue(item.isValid(errors, warnings));
assertTrue(errors.isEmpty());
assertEquals(1, warnings.size());
assertEquals(
"item \"item_name\": \"expire\" field is redundant with \"expire\" metadata; value \"5m\" will be considered",
warnings.get(0));
}
}
@@ -0,0 +1,167 @@
/*
* Copyright (c) 2010-2026 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.model.yaml.internal.items.fileconverter;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.Test;
import org.openhab.core.config.core.ConfigDescriptionRegistry;
import org.openhab.core.items.Item;
import org.openhab.core.items.Metadata;
import org.openhab.core.items.MetadataKey;
import org.openhab.core.model.yaml.YamlElement;
import org.openhab.core.model.yaml.YamlModelRepository;
import org.openhab.core.model.yaml.internal.items.YamlChannelLinkProvider;
import org.openhab.core.model.yaml.internal.items.YamlItemDTO;
import org.openhab.core.model.yaml.internal.items.YamlItemProvider;
import org.openhab.core.model.yaml.internal.items.YamlMetadataProvider;
@NonNullByDefault
public class YamlItemFileConverterTest {
@Test
public void testExpireMetadataConvertedToShortForm() {
Metadata expireMetadata = new Metadata(new MetadataKey("expire", "item_name"), "10m", Map.of());
YamlItemDTO dto = convertWithMetadata(expireMetadata, "String");
assertEquals("10m", dto.expire);
assertNull(dto.metadata);
}
@Test
public void testExpireMetadataEmptyStringStaysInShortForm() {
Metadata expireMetadata = new Metadata(new MetadataKey("expire", "item_name"), "", Map.of());
YamlItemDTO dto = convertWithMetadata(expireMetadata, "String");
assertEquals("", dto.expire);
assertNull(dto.metadata);
}
@Test
public void testExpireMetadataWithConfigStaysMetadata() {
Metadata expireMetadata = new Metadata(new MetadataKey("expire", "item_name"), "10m", Map.of("command", "OFF"));
YamlItemDTO dto = convertWithMetadata(expireMetadata, "String");
assertNull(dto.expire);
assertNotNull(dto.metadata);
assertTrue(dto.metadata.containsKey("expire"));
assertEquals("10m", dto.metadata.get("expire").getValue());
assertEquals("OFF", dto.metadata.get("expire").config.get("command"));
}
@Test
public void testAutoupdateMetadataSetsField() {
Metadata autoupdate = new Metadata(new MetadataKey("autoupdate", "item_name"), "true", Map.of());
YamlItemDTO dto = convertWithMetadata(autoupdate, "String");
assertEquals(Boolean.TRUE, dto.autoupdate);
assertNull(dto.metadata);
}
@Test
public void testAutoupdateMetadataEmptyStringIsTreatedAsNotSet() {
Metadata autoupdate = new Metadata(new MetadataKey("autoupdate", "item_name"), "", Map.of());
YamlItemDTO dto = convertWithMetadata(autoupdate, "String");
assertNull(dto.autoupdate);
assertNull(dto.metadata);
}
@Test
public void testUnitMetadataSetsField() {
Metadata unit = new Metadata(new MetadataKey("unit", "item_name"), "kWh", Map.of());
YamlItemDTO dto = convertWithMetadata(unit, "Number");
assertEquals("kWh", dto.unit);
assertNull(dto.metadata);
}
@Test
public void testUnitMetadataEmptyStringStaysInShortForm() {
Metadata unit = new Metadata(new MetadataKey("unit", "item_name"), "", Map.of());
YamlItemDTO dto = convertWithMetadata(unit, "Number");
assertEquals("", dto.unit);
assertNull(dto.metadata);
}
private YamlItemDTO convertWithMetadata(Metadata metadata, String itemType) {
CapturingYamlModelRepository repository = new CapturingYamlModelRepository();
YamlItemFileConverter converter = new YamlItemFileConverter(repository, mock(YamlItemProvider.class),
mock(YamlMetadataProvider.class), mock(YamlChannelLinkProvider.class),
mock(ConfigDescriptionRegistry.class));
Item item = mock(Item.class);
when(item.getName()).thenReturn(metadata.getUID().getItemName());
when(item.getLabel()).thenReturn(null);
when(item.getType()).thenReturn(itemType);
when(item.getCategory()).thenReturn(null);
when(item.getGroupNames()).thenReturn(List.of());
when(item.getTags()).thenReturn(Set.of());
converter.setItemsToBeGenerated("id", List.of(item), List.of(metadata), Map.of(), false);
List<YamlElement> elements = repository.getElements();
assertEquals(1, elements.size());
assertInstanceOf(YamlItemDTO.class, elements.get(0));
return (YamlItemDTO) elements.get(0);
}
private static class CapturingYamlModelRepository implements YamlModelRepository {
private List<YamlElement> elements = new ArrayList<>();
@Override
public void addElementToModel(String modelName, YamlElement element) {
// no-op
}
@Override
public void removeElementFromModel(String modelName, YamlElement element) {
// no-op
}
@Override
public void updateElementInModel(String modelName, YamlElement element) {
// no-op
}
@Override
public void addElementsToBeGenerated(String id, List<YamlElement> elements) {
this.elements = elements;
}
@Override
public void generateFileFormat(String id, OutputStream out) {
// no-op
}
@Override
public @Nullable String createIsolatedModel(InputStream inputStream, List<String> errors,
List<String> warnings) {
return null;
}
@Override
public void removeIsolatedModel(String modelName) {
// no-op
}
public List<YamlElement> getElements() {
return elements;
}
}
}