Add support for env var resolution in Thing configuration (#5568)

* [config] ConfigUtil: Implement methods to resolve env variable syntax to environment variable values in Configuration (values)

Signed-off-by: Florian Hotze <dev@florianhotze.com>

* [things] BaseThingHandler: Automatically resolve variables in configuration

Signed-off-by: Florian Hotze <dev@florianhotze.com>

* [config] ConfigUtil: Make envProvider protected to subclasses in tests can access it from outside the package

Signed-off-by: Florian Hotze <dev@florianhotze.com>

* [config] ConfigUtilTest: Add a resource lock on ConfigUtil.class

Signed-off-by: Florian Hotze <dev@florianhotze.com>

* [thing] BaseThingHandler: Add unit tests for config variable resolving

Signed-off-by: Florian Hotze <dev@florianhotze.com>

* Address Copilot review

Signed-off-by: Florian Hotze <dev@florianhotze.com>

* Address Copilot review

Signed-off-by: Florian Hotze <dev@florianhotze.com>

* Address Copilot review

Signed-off-by: Florian Hotze <dev@florianhotze.com>

* ConfigUtil: Throw IEA when a variable fails to resolve

Signed-off-by: Florian Hotze <dev@florianhotze.com>

* Address Copilot review

Signed-off-by: Florian Hotze <dev@florianhotze.com>

* Address Copilot review

Signed-off-by: Florian Hotze <dev@florianhotze.com>

* Address Copilot review

Signed-off-by: Florian Hotze <dev@florianhotze.com>

* ConfigUtil: Add method to resolve variables & normalize config afterward

Signed-off-by: Florian Hotze <dev@florianhotze.com>

---------

Signed-off-by: Florian Hotze <dev@florianhotze.com>
This commit is contained in:
Florian Hotze
2026-05-17 19:55:04 +02:00
committed by GitHub
parent 85c7c3c469
commit 50b360fa26
4 changed files with 496 additions and 10 deletions
@@ -23,6 +23,7 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
@@ -48,11 +49,28 @@ import org.slf4j.LoggerFactory;
*
* @author Kai Kreuzer - Initial contribution
* @author Thomas Höfer - Minor changes for type normalization based on config description
* @author Florian Hotze - Add support for environment variable substitution in config values
*/
@NonNullByDefault
public class ConfigUtil {
private static final Pattern DEFAULT_LIST_SPLITTER = Pattern.compile("(?<!\\\\),");
private static final Pattern ENV_PATTERN = Pattern.compile("\\$\\{ENV:([^}]+)}");
private static EnvProvider envProvider = System::getenv;
/**
* Setter for envProvider to allow overwriting it in tests.
*
* <p>
* This <strong>MUST NOT</strong> be called in production environments as it can break environment variable
* resolving.
*
* @param provider the env provider to use for resolving environment variables
*/
protected static void setEnvProvider(EnvProvider provider) {
envProvider = Objects.requireNonNull(provider, "provider must not be null");
}
/**
* Maps the provided (default) value of the given {@link ConfigDescriptionParameter} to the corresponding Java type.
@@ -286,4 +304,132 @@ public class ConfigUtil {
return Constants.OBJECTCLASS.equals(name) || ComponentConstants.COMPONENT_NAME.equals(name)
|| ComponentConstants.COMPONENT_ID.equals(name);
}
/**
* Checks a string value for the variable patterns and resolves referenced variables.
*
* <p>
* Note: At the moment, only environment variables are supported.
* If no variable is referenced, the string value is returned as-is.
* If a referenced variable fails to resolve, a {@link IllegalArgumentException} is thrown.
*
* @param value the value to resolve
* @return the resolved value
* @throws IllegalArgumentException if a variable fails to resolve
*/
private static String resolveVariables(String value) throws IllegalArgumentException {
final Matcher matcher = ENV_PATTERN.matcher(value);
return matcher.replaceAll(matchResult -> {
final String envVarName = matchResult.group(1);
final @Nullable String envVarValue = envProvider.get(envVarName);
if (envVarValue == null) {
throw new IllegalArgumentException(
"Could not resolve environment variable '%s'!".formatted(envVarName));
}
// Safely escape the replacement string so '$' and '\' are treated as literals
return Matcher.quoteReplacement(envVarValue);
});
}
/**
* Resolves variables in the given value by replacing the variable patterns through the variable values.
*
* <p>
* The following rules are applied:
* <ol>
* <li>If the given value is a string, it is checked for variable patterns and referenced variables are
* resolved.</li>
* <li>If a variable fails to resolve, a {@link IllegalArgumentException} is thrown.</li>
* <li>If the value is a collection, this method is called for each element.</li>
* <li>If the value is neither a string nor a collection, it is returned as-is.</li>
* </ol>
*
* @param value the value to resolve
* @return the resolved value
* @throws IllegalArgumentException if a variable fails to resolve
*/
public static Object resolveVariables(Object value) throws IllegalArgumentException {
if (value instanceof String stringValue) {
return resolveVariables(stringValue);
} else if (value instanceof Collection<?> collectionValue) {
final List<Object> entry = new ArrayList<>(collectionValue.size());
for (final Object it : collectionValue) {
final Object resolved = resolveVariables(it);
entry.add(resolved);
}
return entry;
}
return value;
}
/**
* Resolve variables in the given configuration.
*
* <p>
* Note that when substituting variables in non-TEXT values such as BOOLEAN, DECIMAL, etc., the config needs to be
* normalized.
*
* @param configuration the configuration to resolve variables in
* @return the resolved configuration
* @throws IllegalArgumentException if a variable fails to resolve
*/
public static Map<String, @Nullable Object> resolveVariables(Map<String, @Nullable Object> configuration)
throws IllegalArgumentException {
final Map<String, @Nullable Object> resolvedProperties = new HashMap<>();
for (final Entry<String, @Nullable Object> entry : configuration.entrySet()) {
final @Nullable Object value = entry.getValue();
if (value != null) {
final Object resolved = resolveVariables(value);
resolvedProperties.put(entry.getKey(), resolved);
} else {
resolvedProperties.put(entry.getKey(), null);
}
}
return resolvedProperties;
}
/**
* Resolve variables in the given {@link Configuration}.
*
* <p>
* Note that when substituting variables in non-TEXT values such as BOOLEAN, DECIMAL, etc., the config needs to be
* normalized.
*
* @param configuration the configuration to resolve variables in
* @return the resolved configuration
* @throws IllegalArgumentException if a variable fails to resolve
*/
public static Configuration resolveVariables(Configuration configuration) throws IllegalArgumentException {
return new Configuration(resolveVariables(configuration.getProperties()));
}
/**
* Resolve variables and normalize the results in the given {@link Configuration}.
*
* <p>
* Normalizing after the resolve step allows to use variable substitution for non-TEXT values such as BOOLEAN,
* DECIMAL, etc.
*
* @param configuration the configuration to resolve variables in and normalize afterward
* @param configDescriptions the configuration descriptions that should be applied (must not be empty).
* @return the normalized configuration
* @throws IllegalArgumentException if a variable fails to refresh or the given config description is null
*/
public static Configuration resolveVariablesAndNormalizeTypes(Configuration configuration,
List<ConfigDescription> configDescriptions) throws IllegalArgumentException {
final Map<String, @Nullable Object> resolvedConfiguration = resolveVariables(configuration.getProperties());
return new Configuration(normalizeTypes(resolvedConfiguration, configDescriptions));
}
/**
* A provider for environment variables.
*/
@FunctionalInterface
protected interface EnvProvider {
@Nullable
String get(String name);
}
}
@@ -14,6 +14,11 @@ package org.openhab.core.config.core;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.when;
import static org.openhab.core.config.core.ConfigDescriptionParameter.Type.*;
import java.math.BigDecimal;
@@ -22,21 +27,46 @@ import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.jupiter.api.parallel.ResourceAccessMode;
import org.junit.jupiter.api.parallel.ResourceLock;
/**
* @author Simon Kaufmann - Initial contribution
* @author Florian Hotze - Add tests for env variable substitution
*/
@Execution(ExecutionMode.SAME_THREAD) // Force sequential test execution to avoid multi-threaded access to
// ConfigUtil::setEnvProvider
@ResourceLock(value = "org.openhab.core.config.core.ConfigUtil", mode = ResourceAccessMode.READ_WRITE)
@NonNullByDefault
public class ConfigUtilTest {
private final URI configUri = URI.create("system:ephemeris");
private final ConfigUtil.EnvProvider mockEnv = mock(ConfigUtil.EnvProvider.class);
private final ConfigDescriptionParameterBuilder configDescriptionParameterBuilder1 = ConfigDescriptionParameterBuilder
.create("p1", DECIMAL).withMultiple(true).withMultipleLimit(7);
private final ConfigDescriptionParameterBuilder configDescriptionParameterBuilder2 = ConfigDescriptionParameterBuilder
.create("p2", TEXT).withMultiple(true).withMultipleLimit(2);
@BeforeEach
public void setup() {
reset(mockEnv);
when(mockEnv.get("HOSTNAME")).thenReturn("openhab-host");
when(mockEnv.get("PATH")).thenReturn("openhab-path");
ConfigUtil.setEnvProvider(mockEnv);
}
@AfterEach
public void tearDown() {
ConfigUtil.setEnvProvider(System::getenv);
}
@Test
public void verifyNormalizeDefaultTypeForTextReturnsString() {
assertThat(ConfigUtil.getDefaultValueAsCorrectType(
@@ -173,7 +203,7 @@ public class ConfigUtilTest {
}
@Test
public void firstDesciptionWinsForNormalization() {
public void firstDescriptionWinsForNormalization() {
ConfigDescription configDescriptionInteger = ConfigDescriptionBuilder.create(URI.create("thing:fooThing"))
.withParameter(ConfigDescriptionParameterBuilder.create("foo", INTEGER).build()).build();
@@ -191,4 +221,93 @@ public class ConfigUtilTest {
.normalizeTypes(Map.of("foo", "1"), List.of(configDescriptionString, configDescriptionInteger))
.get("foo"), is(instanceOf(String.class)));
}
@Test
public void resolveVariablesResolvesSingleEnvVariable() {
String hostname = mockEnv.get("HOSTNAME");
assertEquals(hostname, ConfigUtil.resolveVariables("${ENV:HOSTNAME}"));
assertEquals("prefix-" + hostname + "-suffix", ConfigUtil.resolveVariables("prefix-${ENV:HOSTNAME}-suffix"));
}
@Test
public void resolveVariablesResolvesMultipleEnvVariables() {
String hostname = mockEnv.get("HOSTNAME");
String path = mockEnv.get("PATH");
assertEquals(hostname + ":" + path, ConfigUtil.resolveVariables("${ENV:HOSTNAME}:${ENV:PATH}"));
}
@Test
public void resolveVariablesThrowsIAEForUnknownEnvVariable() {
assertThrows(IllegalArgumentException.class, () -> ConfigUtil.resolveVariables("${ENV:UNKNOWN_VAR}"));
}
@Test
public void resolveVariablesResolvesList() {
String hostname = mockEnv.get("HOSTNAME");
List<Object> input = List.of("plain", "${ENV:HOSTNAME}", true, 42, 3.14159);
List<Object> expected = List.of("plain", hostname, true, 42, 3.14159);
assertEquals(expected, ConfigUtil.resolveVariables(input));
}
@Test
public void resolveVariablesPassesThroughPrimitivesNotString() {
assertEquals(42, ConfigUtil.resolveVariables(42));
assertEquals(3.14159, ConfigUtil.resolveVariables(3.14159));
assertEquals(true, ConfigUtil.resolveVariables(true));
assertEquals(false, ConfigUtil.resolveVariables(false));
}
@Test
public void resolveVariablesPassesThroughIncorrectPatterns() {
String noEnv = "${HOSTNAME}";
String noBraces = "$ENV:HOSTNAME";
String unclosedBraces = "${ENV:HOSTNAME";
assertEquals(noEnv, ConfigUtil.resolveVariables(noEnv));
assertEquals(noBraces, ConfigUtil.resolveVariables(noBraces));
assertEquals(unclosedBraces, ConfigUtil.resolveVariables(unclosedBraces));
}
@Test
public void resolveVariablesResolvesEnvVariablesInConfiguration() {
String hostname = mockEnv.get("HOSTNAME");
Map<String, @Nullable Object> config = Map.of("p1", "plain", "p2", "${ENV:HOSTNAME}", "p3", true, "p4", 42,
"p5", 3.14159);
Map<String, @Nullable Object> resolvedConfig = ConfigUtil.resolveVariables(config);
assertEquals("plain", resolvedConfig.get("p1"));
assertEquals(hostname, resolvedConfig.get("p2"));
assertEquals(true, resolvedConfig.get("p3"));
assertEquals(42, resolvedConfig.get("p4"));
assertEquals(3.14159, resolvedConfig.get("p5"));
}
@Test
public void resolveVariablesAndNormalizeTypesResolvesThenNormalizedConfiguration() {
String hostname = mockEnv.get("HOSTNAME");
when(mockEnv.get("BOOLEAN")).thenReturn("true");
when(mockEnv.get("INTEGER")).thenReturn("42");
when(mockEnv.get("DECIMAL")).thenReturn("3.14159");
Map<String, @Nullable Object> config = Map.of("p1", "plain", "p2", "${ENV:HOSTNAME}", "p3", "${ENV:BOOLEAN}",
"p4", "${ENV:INTEGER}", "p5", "${ENV:DECIMAL}");
ConfigDescription configDescription = ConfigDescriptionBuilder.create(URI.create("thingType:fooThing"))
.withParameter(ConfigDescriptionParameterBuilder.create("p1", TEXT).build())
.withParameter(ConfigDescriptionParameterBuilder.create("p2", TEXT).build())
.withParameter(ConfigDescriptionParameterBuilder.create("p3", BOOLEAN).build())
.withParameter(ConfigDescriptionParameterBuilder.create("p4", INTEGER).build())
.withParameter(ConfigDescriptionParameterBuilder.create("p5", DECIMAL).build()).build();
Configuration normalizedAndResolvedConfig = ConfigUtil
.resolveVariablesAndNormalizeTypes(new Configuration(config), List.of(configDescription));
assertEquals("plain", normalizedAndResolvedConfig.get("p1"));
assertEquals(hostname, normalizedAndResolvedConfig.get("p2"));
assertEquals(true, normalizedAndResolvedConfig.get("p3"));
assertEquals(new BigDecimal("42"), normalizedAndResolvedConfig.get("p4"));
assertEquals(new BigDecimal("3.14159"), normalizedAndResolvedConfig.get("p5"));
}
}
@@ -23,6 +23,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.ThreadPoolManager;
import org.openhab.core.config.core.ConfigDescription;
import org.openhab.core.config.core.ConfigUtil;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.config.core.validation.ConfigValidationException;
import org.openhab.core.thing.Bridge;
@@ -75,6 +76,8 @@ public abstract class BaseThingHandler implements ThingHandler {
protected Thing thing;
private volatile @Nullable Configuration resolvedConfig;
private @Nullable ThingHandlerCallback callback;
/**
@@ -132,7 +135,7 @@ public abstract class BaseThingHandler implements ThingHandler {
* @return true if the parameters would result in a modified configuration, false otherwise
*/
protected boolean isModifyingCurrentConfig(Map<String, Object> configurationParameters) {
Configuration currentConfig = getConfig();
Configuration currentConfig = getRawConfig();
for (Entry<String, Object> entry : configurationParameters.entrySet()) {
if (!Objects.equals(currentConfig.get(entry.getKey()), entry.getValue())) {
return true;
@@ -154,7 +157,18 @@ public abstract class BaseThingHandler implements ThingHandler {
@Override
public void thingUpdated(Thing thing) {
dispose();
this.thing = thing;
Configuration resolvedConfiguration;
try {
resolvedConfiguration = ConfigUtil.resolveVariables(thing.getConfiguration());
} catch (IllegalArgumentException e) {
logger.warn("Updated thing '{}' with a thing containing invalid configuration '{}': {}", thing.getUID(),
thing.getConfiguration(), e.getMessage());
resolvedConfiguration = thing.getConfiguration();
}
synchronized (this) {
this.thing = thing;
this.resolvedConfig = resolvedConfiguration;
}
initialize();
}
@@ -238,13 +252,43 @@ public abstract class BaseThingHandler implements ThingHandler {
return null;
}
/**
* Returns the raw configuration of the thing.
* Variable patterns are not resolved.
*
* @return raw configuration of the thing
*/
private Configuration getRawConfig() {
return getThing().getConfiguration();
}
/**
* Returns the configuration of the thing.
* Variable patterns are automatically resolved to the variable value.
*
* <p>
* To modify the configuration, retrieve an editable configuration through {@link #editConfiguration()} and persist
* the changed configuration with {@link #updateConfiguration(Configuration)}.
*
* @return configuration of the thing
*/
protected Configuration getConfig() {
return getThing().getConfiguration();
Configuration resolvedConfig = this.resolvedConfig;
if (resolvedConfig == null) {
synchronized (this) {
resolvedConfig = this.resolvedConfig;
if (resolvedConfig == null) {
try {
this.resolvedConfig = resolvedConfig = ConfigUtil.resolveVariables(getRawConfig());
} catch (IllegalArgumentException e) {
logger.warn("Failed to resolve variables for configuration '{}' of thing '{}': {}",
getRawConfig(), getThing().getUID(), e.getMessage());
return new Configuration(getRawConfig());
}
}
}
}
return new Configuration(resolvedConfig);
}
/**
@@ -476,8 +520,16 @@ public abstract class BaseThingHandler implements ThingHandler {
this.getClass().getSimpleName(), thing.getUID());
return;
}
Configuration resolvedConfiguration;
try {
callback.validateConfigurationParameters(thing, thing.getConfiguration().getProperties());
resolvedConfiguration = ConfigUtil.resolveVariables(thing.getConfiguration());
} catch (IllegalArgumentException e) {
logger.warn("Attempt to update thing '{}' with a thing containing invalid configuration '{}' blocked: {}",
thing.getUID(), thing.getConfiguration(), e.getMessage());
return;
}
try {
callback.validateConfigurationParameters(thing, resolvedConfiguration.getProperties());
thing.getChannels().forEach(channel -> callback.validateConfigurationParameters(channel,
channel.getConfiguration().getProperties()));
} catch (ConfigValidationException e) {
@@ -487,6 +539,7 @@ public abstract class BaseThingHandler implements ThingHandler {
return;
}
synchronized (this) {
this.resolvedConfig = resolvedConfiguration;
this.thing = thing;
callback.thingUpdated(thing);
}
@@ -499,7 +552,7 @@ public abstract class BaseThingHandler implements ThingHandler {
* @return copy of the thing configuration (not null)
*/
protected Configuration editConfiguration() {
Map<String, Object> properties = this.thing.getConfiguration().getProperties();
Map<String, Object> properties = getRawConfig().getProperties();
return new Configuration(new HashMap<>(properties));
}
@@ -512,15 +565,24 @@ public abstract class BaseThingHandler implements ThingHandler {
* @param configuration configuration, that was updated and should be persisted
*/
protected void updateConfiguration(Configuration configuration) {
Map<String, Object> old = this.thing.getConfiguration().getProperties();
Map<String, Object> old = getRawConfig().getProperties();
Configuration oldResolved = getConfig();
ThingHandlerCallback callback = this.callback;
if (callback == null) {
logger.warn("Handler {} tried updating its configuration although the handler was already disposed.",
this.getClass().getSimpleName());
return;
}
Configuration resolvedConfiguration;
try {
callback.validateConfigurationParameters(this.thing, configuration.getProperties());
resolvedConfiguration = ConfigUtil.resolveVariables(configuration);
} catch (IllegalArgumentException e) {
logger.warn("Attempt to apply configuration '{}' on thing '{}' blocked: {}", configuration, thing.getUID(),
e.getMessage());
return;
}
try {
callback.validateConfigurationParameters(this.thing, resolvedConfiguration.getProperties());
} catch (ConfigValidationException e) {
logger.warn(
"Attempt to apply invalid configuration '{}' on thing '{}' blocked. This is most likely a bug: {}",
@@ -528,15 +590,19 @@ public abstract class BaseThingHandler implements ThingHandler {
return;
}
try {
this.thing.getConfiguration().setProperties(configuration.getProperties());
synchronized (this) {
this.resolvedConfig = resolvedConfiguration;
this.thing.getConfiguration().setProperties(configuration.getProperties());
callback.thingUpdated(thing);
}
} catch (RuntimeException e) {
logger.warn(
"Error while applying configuration changes: '{}: {}' - reverting configuration changes on thing '{}'.",
e.getClass().getSimpleName(), e.getMessage(), this.thing.getUID().getAsString());
this.thing.getConfiguration().setProperties(old);
synchronized (this) {
this.thing.getConfiguration().setProperties(old);
this.resolvedConfig = oldResolved;
}
throw e;
}
}
@@ -0,0 +1,155 @@
/*
* 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.thing.binding;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.jupiter.api.parallel.ResourceAccessMode;
import org.junit.jupiter.api.parallel.ResourceLock;
import org.openhab.core.config.core.ConfigUtil;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.types.Command;
/**
* Tests for {@link BaseThingHandler}.
*
* @author Florian Hotze - Initial contribution
*/
@Execution(ExecutionMode.SAME_THREAD)
@ResourceLock(value = "org.openhab.core.config.core.ConfigUtil", mode = ResourceAccessMode.READ_WRITE)
@NonNullByDefault
class BaseThingHandlerTest {
private static final ThingTypeUID THING_TYPE_UID = new ThingTypeUID("test:type");
private static final ThingUID THING_UID = new ThingUID(THING_TYPE_UID, "thing");
private @NonNullByDefault({}) ThingHandlerCallback callback;
private @NonNullByDefault({}) TestThingHandler handler;
private static class TestThingHandler extends BaseThingHandler {
public @Nullable Configuration configInInitialize = null;
public TestThingHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
@Override
public void initialize() {
this.configInInitialize = getConfig();
}
}
private static class ConfigUtilAccessor extends ConfigUtil {
static void setEnv(Map<String, String> values) {
setEnvProvider(values::get);
}
static void resetEnv() {
setEnvProvider(System::getenv);
}
}
@BeforeEach
public void setup() throws Exception {
callback = mock(ThingHandlerCallback.class);
Thing thing = ThingBuilder.create(THING_TYPE_UID, THING_UID.getId())
.withConfiguration(new Configuration(Map.of("p1", "${ENV:VAR}", "p2", "${ENV:FOO}"))).build();
handler = new TestThingHandler(thing);
handler.setCallback(callback);
ConfigUtilAccessor.setEnv(Map.of("VAR", "resolved-var", "FOO", "resolved-foo", "BAR", "resolved-bar"));
}
@AfterEach
public void tearDown() throws Exception {
ConfigUtilAccessor.resetEnv();
}
@Test
public void testGetConfig() {
Configuration config = handler.getConfig();
assertEquals("resolved-var", config.get("p1"));
assertEquals("resolved-foo", config.get("p2"));
}
public static class ConfigClass {
public String p1 = "";
public String p2 = "";
}
@Test
public void testGetConfigAs() {
ConfigClass config = handler.getConfig().as(ConfigClass.class);
assertEquals("resolved-var", config.p1);
assertEquals("resolved-foo", config.p2);
}
@Test
public void testResolvedConfigAfterThingUpdate() {
// Notify the handler: Thing updated with new configuration
Thing thing = handler.editThing()
.withConfiguration(new Configuration(Map.of("p1", "${ENV:FOO}", "p2", "${ENV:BAR}"))).build();
handler.thingUpdated(thing);
assertEquals("resolved-foo", handler.getConfig().get("p1"));
assertEquals("resolved-bar", handler.getConfig().get("p2"));
}
@Test
public void testResolvedConfigAfterUpdateThing() {
// Action: Update the Thing with new configuration
Thing newThing = ThingBuilder.create(THING_TYPE_UID, THING_UID.getId())
.withConfiguration(new Configuration(Map.of("p1", "${ENV:FOO}", "p2", "${ENV:BAR}"))).build();
handler.updateThing(newThing);
assertEquals("resolved-foo", handler.getConfig().get("p1"));
assertEquals("resolved-bar", handler.getConfig().get("p2"));
}
@Test
public void testResolvedConfigAfterConfigurationUpdate() {
// Action: Update the configuration
Configuration newConfig = new Configuration(Map.of("p1", "${ENV:FOO}", "p2", "${ENV:BAR}"));
handler.updateConfiguration(newConfig);
assertEquals("resolved-foo", handler.getConfig().get("p1"));
assertEquals("resolved-bar", handler.getConfig().get("p2"));
}
@Test
public void testHandleConfigurationUpdate() {
handler.handleConfigurationUpdate(Map.of("p1", "${ENV:VAR}_suffix"));
assertEquals("resolved-var_suffix", handler.getConfig().get("p1"));
assertEquals("resolved-foo", handler.getConfig().get("p2"));
}
}