From 43873cf0f3178162f26bbafc705c0c85508c6d16 Mon Sep 17 00:00:00 2001 From: Markus Michels Date: Sun, 21 Jun 2026 15:04:47 +0200 Subject: [PATCH] =?UTF-8?q?[shelly]=20Fix=20addon=20sensor=20read=20errors?= =?UTF-8?q?=20causing=20spurious=200=20=C2=B0C=20spike=20(#20995)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [shelly] Fix addon sensor read errors causing spurious 0 °C spike Signed-off-by: Markus Michels --- .../internal/api2/Shelly2ApiClient.java | 148 ++++++---- .../internal/api2/Shelly2ApiJsonDTO.java | 35 +-- .../internal/api2/Shelly2AddonStatusTest.java | 271 ++++++++++++++++++ .../internal/api2/ShellyBluApiTest.java | 1 + .../handler/ShellyComponentsTest.java | 1 + .../handler/ShellyThingTableTest.java | 1 + 6 files changed, 389 insertions(+), 68 deletions(-) create mode 100644 bundles/org.openhab.binding.shelly/src/test/java/org/openhab/binding/shelly/internal/api2/Shelly2AddonStatusTest.java diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiClient.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiClient.java index 6424a7fcf9..c28acc0637 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiClient.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiClient.java @@ -525,11 +525,12 @@ public class Shelly2ApiClient extends ShellyHttpClient implements ShellyDiscover boolean channelUpdate) throws ShellyApiException { boolean updated = false; - if (result.temperature0 != null && result.temperature0.tC != null && !getProfile().isSensor) { + Shelly2DeviceStatusTempId temperature0 = result.temperature0; + if (temperature0 != null && temperature0.tC != null && !getProfile().isSensor) { if (status.tmp == null) { status.tmp = new ShellySensorTmp(); } - status.temperature = status.tmp.tC = result.temperature0.tC; + status.temperature = status.tmp.tC = temperature0.tC; } updated |= updateInputStatus(status, result, channelUpdate); @@ -597,17 +598,20 @@ public class Shelly2ApiClient extends ShellyHttpClient implements ShellyDiscover if (rs.timerStartetAt != null && rs.timerStartetAt > 0) { sr.timerRemaining = (int) (now() - rs.timerStartetAt); } - if (rs.temperature != null && rs.temperature.tC != null) { - if (status.tmp == null) { - status.tmp = new ShellySensorTmp(); - } - status.tmp.isValid = true; - status.tmp.tC = rs.temperature.tC; - status.tmp.tF = rs.temperature.tF; - status.tmp.units = "C"; - sr.temperature = rs.temperature.tC; - if (status.temperature == null || rs.temperature.tC > status.temperature) { - status.temperature = sr.temperature; + if (rs.temperature != null) { + Double tC = rs.temperature.tC; + if (tC != null) { + if (status.tmp == null) { + status.tmp = new ShellySensorTmp(); + } + status.tmp.isValid = true; + status.tmp.tC = tC; + status.tmp.tF = rs.temperature.tF; + status.tmp.units = "C"; + sr.temperature = tC; + if (status.temperature == null || tC > status.temperature) { + status.temperature = sr.temperature; + } } } @@ -686,17 +690,20 @@ public class Shelly2ApiClient extends ShellyHttpClient implements ShellyDiscover if (bs.output != null) { sr.ison = rstatus.ison = getBool(bs.output); } - if (bs.temperature != null && bs.temperature.tC != null) { - if (status.tmp == null) { - status.tmp = new ShellySensorTmp(); - } - status.tmp.isValid = true; - status.tmp.tC = bs.temperature.tC; - status.tmp.tF = bs.temperature.tF; - status.tmp.units = "C"; - sr.temperature = getDouble(bs.temperature.tC); - if (status.temperature == null || getDouble(bs.temperature.tC) > status.temperature) { - status.temperature = sr.temperature; + if (bs.temperature != null) { + Double tC = bs.temperature.tC; + if (tC != null) { + if (status.tmp == null) { + status.tmp = new ShellySensorTmp(); + } + status.tmp.isValid = true; + status.tmp.tC = tC; + status.tmp.tF = bs.temperature.tF; + status.tmp.units = "C"; + sr.temperature = getDouble(tC); + if (status.temperature == null || getDouble(tC) > status.temperature) { + status.temperature = sr.temperature; + } } } @@ -1140,31 +1147,58 @@ public class Shelly2ApiClient extends ShellyHttpClient implements ShellyDiscover } if (ds.temperature100 != null) { - ShellyExtTemperature extTemp = status.extTemperature; - if (extTemp == null) { - extTemp = new ShellyExtTemperature(); - status.extTemperature = extTemp; - } - extTemp.sensor1 = updateExtTempSensor(ds.temperature100); - extTemp.sensor2 = updateExtTempSensor(ds.temperature101); - extTemp.sensor3 = updateExtTempSensor(ds.temperature102); - extTemp.sensor4 = updateExtTempSensor(ds.temperature103); - extTemp.sensor5 = updateExtTempSensor(ds.temperature104); - } - if (ds.humidity100 != null) { - Double rh = ds.humidity100.rh; - if (rh != null) { - status.extHumidity = new ShellyExtHumidity(rh); + ShellyShortTemp s1 = updateExtTempSensor(ds.temperature100); + ShellyShortTemp s2 = updateExtTempSensor(ds.temperature101); + ShellyShortTemp s3 = updateExtTempSensor(ds.temperature102); + ShellyShortTemp s4 = updateExtTempSensor(ds.temperature103); + ShellyShortTemp s5 = updateExtTempSensor(ds.temperature104); + if (s1 != null || s2 != null || s3 != null || s4 != null || s5 != null) { + ShellyExtTemperature extTemp = status.extTemperature; + if (extTemp == null) { + extTemp = new ShellyExtTemperature(); + status.extTemperature = extTemp; + } + extTemp.sensor1 = s1; + extTemp.sensor2 = s2; + extTemp.sensor3 = s3; + extTemp.sensor4 = s4; + extTemp.sensor5 = s5; + } else { + // all sensors in this notification reported read errors — clear so + // hasAddon() returns false and sensors#lastUpdate is not written + status.extTemperature = null; } } - if (ds.voltmeter100 != null) { - Double voltage = ds.voltmeter100.voltage; - if (voltage != null) { - status.extVoltage = new ShellyExtVoltage(voltage); + Shelly2DeviceStatusHumidity humidity100 = ds.humidity100; + if (humidity100 != null) { + if (hasReadError(humidity100.errors)) { + logger.debug("{}: Addon humidity:100 sensor read error, skipping update", thingName); + status.extHumidity = null; + } else { + Double rh = humidity100.rh; + if (rh != null) { + status.extHumidity = new ShellyExtHumidity(rh); + } + } + } + Shelly2DeviceStatusVoltage voltmeter100 = ds.voltmeter100; + if (voltmeter100 != null) { + if (hasReadError(voltmeter100.errors)) { + logger.debug("{}: Addon voltmeter:100 sensor read error, skipping update", thingName); + status.extVoltage = null; + } else { + Double voltage = voltmeter100.voltage; + if (voltage != null) { + status.extVoltage = new ShellyExtVoltage(voltage); + } } } if (ds.input100 != null) { - if (ds.input100.state != null) { + if (hasReadError(ds.input100.errors)) { + logger.debug("{}: Addon input:100 sensor read error, skipping update", thingName); + status.extDigitalInput = null; + status.extAnalogInput = null; + } else if (ds.input100.state != null) { status.extDigitalInput = new ShellyExtDigitalInput(getBool(ds.input100.state)); } else if (ds.input100.percent != null) { status.extAnalogInput = new ShellyExtAnalogInput(getDouble(ds.input100.percent)); @@ -1173,15 +1207,25 @@ public class Shelly2ApiClient extends ShellyHttpClient implements ShellyDiscover } private @Nullable ShellyShortTemp updateExtTempSensor(@Nullable Shelly2DeviceStatusTempId value) { - if (value != null) { - ShellyShortTemp temp = new ShellyShortTemp(); - Integer idBox = value.id; - temp.hwID = idBox != null ? idBox.toString() : "999"; - temp.tC = getDouble(value.tC); - temp.tF = getDouble(value.tF); - return temp; + if (value == null) { + return null; } - return null; + if (hasReadError(value.errors)) { + Integer idBox = value.id; + logger.debug("{}: Addon temperature:{} sensor read error, skipping update", thingName, + idBox != null ? idBox : "?"); + return null; + } + ShellyShortTemp temp = new ShellyShortTemp(); + Integer idBox = value.id; + temp.hwID = idBox != null ? idBox.toString() : "999"; + temp.tC = getDouble(value.tC); + temp.tF = getDouble(value.tF); + return temp; + } + + private static boolean hasReadError(@Nullable ArrayList errors) { + return errors != null && errors.contains("read"); } protected void updateHumidityStatus(ShellyStatusSensor sdata, @Nullable Shelly2DeviceStatusHumidity value) { diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiJsonDTO.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiJsonDTO.java index c1ff2e3994..a4208949a2 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiJsonDTO.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiJsonDTO.java @@ -598,7 +598,7 @@ public class Shelly2ApiJsonDTO { public Integer id; public Boolean state; public Double percent; // analog input only - public ArrayList errors;// shown only if at least one error is present. + public @Nullable ArrayList errors; public Double xpercent; public Shelly2InputCounts counts; public Double freq; @@ -655,6 +655,7 @@ public class Shelly2ApiJsonDTO { public class Shelly2DeviceStatusHumidity { public @Nullable Integer id; public @Nullable Double rh; + public @Nullable ArrayList errors; } public class Shelly2DeviceStatusIlluminance { @@ -666,6 +667,7 @@ public class Shelly2ApiJsonDTO { public class Shelly2DeviceStatusVoltage { public @Nullable Integer id; public @Nullable Double voltage; + public @Nullable ArrayList errors; } public class Shelly2DeviceStatusTempId extends Shelly2DeviceStatusTemp { @@ -834,22 +836,22 @@ public class Shelly2ApiJsonDTO { public Shelly2DeviceStatusLight light1; @SerializedName("temperature:0") - public Shelly2DeviceStatusTempId temperature0; + public @Nullable Shelly2DeviceStatusTempId temperature0; @SerializedName("temperature:100") - public Shelly2DeviceStatusTempId temperature100; + public @Nullable Shelly2DeviceStatusTempId temperature100; @SerializedName("temperature:101") - public Shelly2DeviceStatusTempId temperature101; + public @Nullable Shelly2DeviceStatusTempId temperature101; @SerializedName("temperature:102") - public Shelly2DeviceStatusTempId temperature102; + public @Nullable Shelly2DeviceStatusTempId temperature102; @SerializedName("temperature:103") - public Shelly2DeviceStatusTempId temperature103; + public @Nullable Shelly2DeviceStatusTempId temperature103; @SerializedName("temperature:104") - public Shelly2DeviceStatusTempId temperature104; + public @Nullable Shelly2DeviceStatusTempId temperature104; @SerializedName("humidity:0") - public Shelly2DeviceStatusHumidity humidity0; + public @Nullable Shelly2DeviceStatusHumidity humidity0; @SerializedName("humidity:100") - public Shelly2DeviceStatusHumidity humidity100; + public @Nullable Shelly2DeviceStatusHumidity humidity100; @SerializedName("illuminance:0") Shelly2DeviceStatusIlluminance illuminance0; @@ -858,15 +860,15 @@ public class Shelly2ApiJsonDTO { public Shelly2DeviceStatusSmoke smoke0; @SerializedName("voltmeter:0") - public Shelly2DeviceStatusVoltage voltmeter0; + public @Nullable Shelly2DeviceStatusVoltage voltmeter0; @SerializedName("voltmeter:1") - public Shelly2DeviceStatusVoltage voltmeter1; + public @Nullable Shelly2DeviceStatusVoltage voltmeter1; @SerializedName("voltmeter:2") - public Shelly2DeviceStatusVoltage voltmeter2; + public @Nullable Shelly2DeviceStatusVoltage voltmeter2; @SerializedName("voltmeter:3") - public Shelly2DeviceStatusVoltage voltmeter3; + public @Nullable Shelly2DeviceStatusVoltage voltmeter3; @SerializedName("voltmeter:100") - public Shelly2DeviceStatusVoltage voltmeter100; + public @Nullable Shelly2DeviceStatusVoltage voltmeter100; @SerializedName("devicepower:0") public Shelly2DeviceStatusPower devicepower0; @@ -988,8 +990,9 @@ public class Shelly2ApiJsonDTO { } public static class Shelly2DeviceStatusTemp { - public Double tC; - public Double tF; + public @Nullable Double tC; + public @Nullable Double tF; + public @Nullable ArrayList errors; } public static class Shelly2Energy { diff --git a/bundles/org.openhab.binding.shelly/src/test/java/org/openhab/binding/shelly/internal/api2/Shelly2AddonStatusTest.java b/bundles/org.openhab.binding.shelly/src/test/java/org/openhab/binding/shelly/internal/api2/Shelly2AddonStatusTest.java new file mode 100644 index 0000000000..2224254890 --- /dev/null +++ b/bundles/org.openhab.binding.shelly/src/test/java/org/openhab/binding/shelly/internal/api2/Shelly2AddonStatusTest.java @@ -0,0 +1,271 @@ +/* + * 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.binding.shelly.internal.api2; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; +import static org.openhab.binding.shelly.internal.ShellyDevices.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.openhab.binding.shelly.internal.api.ShellyApiException; +import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile; +import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus; +import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult; +import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatusTemp; +import org.openhab.binding.shelly.internal.config.ShellyApiConfiguration; +import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration; +import org.openhab.binding.shelly.internal.config.ShellyBindingRuntimeConfig; +import org.openhab.binding.shelly.internal.handler.ShellyThingInterface; +import org.openhab.core.net.NetworkAddressChangeListener; +import org.openhab.core.net.NetworkAddressService; + +import com.google.gson.Gson; + +/** + * Tests for the Gen2 addon sensor read-error path in {@link Shelly2ApiClient}. + * + *

+ * Verifies that when a DS18B20 or AM2301 addon sensor reports {@code "errors":["read"]} (firmware + * sends {@code "tC":null} alongside the error), the binding: + *

    + *
  • correctly deserialises the {@code errors} array from the DTO,
  • + *
  • leaves {@link ShellySettingsStatus#extTemperature} / {@code extHumidity} / {@code extVoltage} + * null so that no channel update is emitted and {@code sensors#lastUpdate} is not advanced.
  • + *
+ * + * @author Markus Michels - Initial contribution + */ +@NonNullByDefault +@SuppressWarnings({ "null" }) +public class Shelly2AddonStatusTest { + + private static final String LOCAL_IP = "192.168.1.50"; + private static final String DEVICE_IP = "192.168.1.100"; + + // ── helpers ────────────────────────────────────────────────────────────────── + + private ShellyApiConfiguration testConfig() { + ShellyBindingConfiguration raw = ShellyBindingConfiguration + .fromProperties(Map.of(ShellyBindingConfiguration.CONFIG_LOCAL_IP, LOCAL_IP)); + ShellyBindingRuntimeConfig bindingConfig = new ShellyBindingRuntimeConfig(raw, 8080, nullNas()); + return new ShellyApiConfiguration(bindingConfig, "test-realm", DEVICE_IP); + } + + private static NetworkAddressService nullNas() { + return new NetworkAddressService() { + @Override + public @Nullable String getPrimaryIpv4HostAddress() { + return null; + } + + @Override + public @Nullable String getConfiguredBroadcastAddress() { + return null; + } + + @Override + public boolean isUseOnlyOneAddress() { + return false; + } + + @Override + public boolean isUseIPv6() { + return false; + } + + @Override + public void addNetworkAddressChangeListener(NetworkAddressChangeListener listener) { + } + + @Override + public void removeNetworkAddressChangeListener(NetworkAddressChangeListener listener) { + } + }; + } + + private ShellyThingInterface mockRelayThing() { + ShellyDeviceProfile profile = new ShellyDeviceProfile(THING_TYPE_SHELLYPLUS1PM); + profile.isSensor = false; + profile.hasBattery = false; + ShellyThingInterface handler = mock(ShellyThingInterface.class); + when(handler.getProfile()).thenReturn(profile); + when(handler.areChannelsCreated()).thenReturn(false); + when(handler.updateChannel(anyString(), anyString(), any())).thenReturn(false); + return handler; + } + + /** + * Testable subclass of {@link Shelly2ApiClient} that injects a mock {@link ShellyThingInterface} + * and exposes the protected {@link #fillDeviceStatus} for unit testing. + */ + private static class TestableApiClient extends Shelly2ApiClient { + TestableApiClient(ShellyApiConfiguration config, ShellyThingInterface thing) { + super("test-thing", config, Mockito.mock(HttpClient.class)); + this.thing = thing; + } + + @Override + public T apiRequest(String method, @Nullable Object params, Class classOfT) throws ShellyApiException { + throw new ShellyApiException("Not expected in addon test: " + method); + } + + public boolean testFillDeviceStatus(ShellySettingsStatus status, Shelly2DeviceStatusResult result) + throws ShellyApiException { + return fillDeviceStatus(status, result, false); + } + } + + private TestableApiClient newClient() { + return new TestableApiClient(testConfig(), mockRelayThing()); + } + + // ── DTO parsing ─────────────────────────────────────────────────────────────── + + @Test + void temperatureStatusTemp_readError_parsesErrorsFieldAndNullTc() { + Gson gson = new Gson(); + Shelly2DeviceStatusTemp t = Objects + .requireNonNull(gson.fromJson("{\"tC\":null,\"errors\":[\"read\"]}", Shelly2DeviceStatusTemp.class)); + assertThat(t.tC, is(nullValue())); + assertThat(t.errors, is(not(nullValue()))); + assertThat(t.errors.contains("read"), is(true)); + } + + @Test + void temperatureStatusTemp_normalReading_noErrorsField() { + Gson gson = new Gson(); + Shelly2DeviceStatusTemp t = Objects + .requireNonNull(gson.fromJson("{\"tC\":22.5,\"tF\":72.5}", Shelly2DeviceStatusTemp.class)); + assertThat(t.tC, is(22.5)); + assertThat(t.errors, is(nullValue())); + } + + // ── fillDeviceStatus — temperature ──────────────────────────────────────────── + + @Test + void fillDeviceStatus_tempReadError_extTemperatureNull() throws ShellyApiException { + Shelly2DeviceStatusResult result = new Shelly2DeviceStatusResult(); + result.temperature100 = result.new Shelly2DeviceStatusTempId(); + result.temperature100.id = 100; + result.temperature100.errors = new ArrayList<>(List.of("read")); + + ShellySettingsStatus status = new ShellySettingsStatus(); + newClient().testFillDeviceStatus(status, result); + + assertThat("sensor read error must leave extTemperature null", status.extTemperature, is(nullValue())); + } + + @Test + void fillDeviceStatus_tempValid_extTemperaturePopulated() throws ShellyApiException { + Shelly2DeviceStatusResult result = new Shelly2DeviceStatusResult(); + result.temperature100 = result.new Shelly2DeviceStatusTempId(); + result.temperature100.id = 100; + result.temperature100.tC = 22.5; + + ShellySettingsStatus status = new ShellySettingsStatus(); + newClient().testFillDeviceStatus(status, result); + + assertThat("valid temp must set extTemperature", status.extTemperature, is(not(nullValue()))); + assertThat(status.extTemperature.sensor1, is(not(nullValue()))); + assertThat(status.extTemperature.sensor1.tC, is(22.5)); + } + + @Test + void fillDeviceStatus_mixedSensors_errorSlotNullValidSlotSet() throws ShellyApiException { + Shelly2DeviceStatusResult result = new Shelly2DeviceStatusResult(); + // sensor 100 errors — sensor 101 is valid + result.temperature100 = result.new Shelly2DeviceStatusTempId(); + result.temperature100.id = 100; + result.temperature100.errors = new ArrayList<>(List.of("read")); + result.temperature101 = result.new Shelly2DeviceStatusTempId(); + result.temperature101.id = 101; + result.temperature101.tC = 18.0; + + ShellySettingsStatus status = new ShellySettingsStatus(); + newClient().testFillDeviceStatus(status, result); + + // extTemperature must be set because sensor101 is valid + assertThat("at least one valid sensor must set extTemperature", status.extTemperature, is(not(nullValue()))); + assertThat("errored slot must be null", status.extTemperature.sensor1, is(nullValue())); + assertThat("valid slot must be set", status.extTemperature.sensor2, is(not(nullValue()))); + assertThat(status.extTemperature.sensor2.tC, is(18.0)); + } + + // ── fillDeviceStatus — humidity ─────────────────────────────────────────────── + + @Test + void fillDeviceStatus_humidityReadError_extHumidityNull() throws ShellyApiException { + Shelly2DeviceStatusResult result = new Shelly2DeviceStatusResult(); + result.humidity100 = result.new Shelly2DeviceStatusHumidity(); + result.humidity100.id = 100; + result.humidity100.errors = new ArrayList<>(List.of("read")); + + ShellySettingsStatus status = new ShellySettingsStatus(); + newClient().testFillDeviceStatus(status, result); + + assertThat("humidity read error must leave extHumidity null", status.extHumidity, is(nullValue())); + } + + @Test + void fillDeviceStatus_humidityValid_extHumidityPopulated() throws ShellyApiException { + Shelly2DeviceStatusResult result = new Shelly2DeviceStatusResult(); + result.humidity100 = result.new Shelly2DeviceStatusHumidity(); + result.humidity100.id = 100; + result.humidity100.rh = 55.0; + + ShellySettingsStatus status = new ShellySettingsStatus(); + newClient().testFillDeviceStatus(status, result); + + assertThat("valid humidity must set extHumidity", status.extHumidity, is(not(nullValue()))); + } + + // ── fillDeviceStatus — voltage ──────────────────────────────────────────────── + + @Test + void fillDeviceStatus_voltageReadError_extVoltageNull() throws ShellyApiException { + Shelly2DeviceStatusResult result = new Shelly2DeviceStatusResult(); + result.voltmeter100 = result.new Shelly2DeviceStatusVoltage(); + result.voltmeter100.id = 100; + result.voltmeter100.errors = new ArrayList<>(List.of("read")); + + ShellySettingsStatus status = new ShellySettingsStatus(); + newClient().testFillDeviceStatus(status, result); + + assertThat("voltage read error must leave extVoltage null", status.extVoltage, is(nullValue())); + } + + @Test + void fillDeviceStatus_voltageValid_extVoltagePopulated() throws ShellyApiException { + Shelly2DeviceStatusResult result = new Shelly2DeviceStatusResult(); + result.voltmeter100 = result.new Shelly2DeviceStatusVoltage(); + result.voltmeter100.id = 100; + result.voltmeter100.voltage = 3.3; + + ShellySettingsStatus status = new ShellySettingsStatus(); + newClient().testFillDeviceStatus(status, result); + + assertThat("valid voltage must set extVoltage", status.extVoltage, is(not(nullValue()))); + } +} diff --git a/bundles/org.openhab.binding.shelly/src/test/java/org/openhab/binding/shelly/internal/api2/ShellyBluApiTest.java b/bundles/org.openhab.binding.shelly/src/test/java/org/openhab/binding/shelly/internal/api2/ShellyBluApiTest.java index d9448ae36b..264868c4f4 100644 --- a/bundles/org.openhab.binding.shelly/src/test/java/org/openhab/binding/shelly/internal/api2/ShellyBluApiTest.java +++ b/bundles/org.openhab.binding.shelly/src/test/java/org/openhab/binding/shelly/internal/api2/ShellyBluApiTest.java @@ -45,6 +45,7 @@ import org.openhab.core.thing.ThingTypeUID; * @author Markus Michels - Initial contribution */ @NonNullByDefault +@SuppressWarnings({ "null" }) public class ShellyBluApiTest { // ── getStatus() connection guard ───────────────────────────────────────── diff --git a/bundles/org.openhab.binding.shelly/src/test/java/org/openhab/binding/shelly/internal/handler/ShellyComponentsTest.java b/bundles/org.openhab.binding.shelly/src/test/java/org/openhab/binding/shelly/internal/handler/ShellyComponentsTest.java index 4e21842c69..0dee5f958a 100644 --- a/bundles/org.openhab.binding.shelly/src/test/java/org/openhab/binding/shelly/internal/handler/ShellyComponentsTest.java +++ b/bundles/org.openhab.binding.shelly/src/test/java/org/openhab/binding/shelly/internal/handler/ShellyComponentsTest.java @@ -48,6 +48,7 @@ import org.openhab.core.types.UnDefType; * @author Markus Michels - Initial contribution */ @NonNullByDefault +@SuppressWarnings({ "null" }) public class ShellyComponentsTest { // ── hasAddon ───────────────────────────────────────────────────────────── diff --git a/bundles/org.openhab.binding.shelly/src/test/java/org/openhab/binding/shelly/internal/handler/ShellyThingTableTest.java b/bundles/org.openhab.binding.shelly/src/test/java/org/openhab/binding/shelly/internal/handler/ShellyThingTableTest.java index fe32f02e50..fc5a85a59c 100644 --- a/bundles/org.openhab.binding.shelly/src/test/java/org/openhab/binding/shelly/internal/handler/ShellyThingTableTest.java +++ b/bundles/org.openhab.binding.shelly/src/test/java/org/openhab/binding/shelly/internal/handler/ShellyThingTableTest.java @@ -35,6 +35,7 @@ import org.junit.jupiter.api.Test; * @author Markus Michels - Initial contribution */ @NonNullByDefault +@SuppressWarnings({ "null" }) public class ShellyThingTableTest { private static final String GATEWAY_IP = "192.168.1.100";