[shelly] Fix stopping sensor updates when using combo sensor connected to Shelly Addon (#20932)

* Fix externalTemp channel updates, improve test coverage

Signed-off-by: Markus Michels <markus7017@gmail.com>
This commit is contained in:
Markus Michels
2026-06-14 20:23:57 +02:00
committed by GitHub
parent fc68fffa12
commit 9c0c1295b8
9 changed files with 485 additions and 85 deletions
@@ -530,6 +530,7 @@ Depending on the device type and firmware release channels might be not availabl
| | temperature2 | Number | yes | Temperature value of external sensor #2 (if connected to temp/hum addon) |
| | temperature3 | Number | yes | Temperature value of external sensor #3 (if connected to temp/hum addon) |
| | humidity | Number | yes | Humidity in percent (if connected to temp/hum addon) |
| | lastUpdate | DateTime | yes | Timestamp of the last sensor update (heartbeat) |
| | input1 | Contact | yes | Status of the reed contact (OPEN/CLOSE), only with external switch add-on |
### Shelly 1L (thing-type: shelly1l)
@@ -555,6 +556,7 @@ Depending on the device type and firmware release channels might be not availabl
| | temperature2 | Number | yes | Temperature value of external sensor #2 (if connected to temp/hum addon) |
| | temperature3 | Number | yes | Temperature value of external sensor #3 (if connected to temp/hum addon) |
| | humidity | Number | yes | Humidity in percent (if connected to temp/hum addon) |
| | lastUpdate | DateTime | yes | Timestamp of the last sensor update (heartbeat) |
| | input1 | Contact | yes | Status of the reed contact (OPEN/CLOSE), only with external switch add-on |
`Note:`
@@ -580,6 +582,7 @@ In this case the is no real measurement based on power consumption, but the Shel
| | temperature2 | Number | yes | Temperature value of external sensor #2 (if connected to temp/hum addon) |
| | temperature3 | Number | yes | Temperature value of external sensor #3 (if connected to temp/hum addon) |
| | humidity | Number | yes | Humidity in percent (if connected to temp/hum addon) |
| | lastUpdate | DateTime | yes | Timestamp of the last sensor update (heartbeat) |
| | input1 | Contact | yes | Status of the reed contact (OPEN/CLOSE), only with external switch add-on |
### Shelly EM (thing-type: shellyem)
@@ -815,6 +818,7 @@ Channels lastEvent and eventCount are only available if input type is set to mom
| | temperature2 | Number | yes | Temperature value of external sensor #2 (if connected to temp/hum addon) |
| | temperature3 | Number | yes | Temperature value of external sensor #3 (if connected to temp/hum addon) |
| | humidity | Number | yes | Humidity in percent (if connected to temp/hum addon) |
| | lastUpdate | DateTime | yes | Timestamp of the last sensor update (heartbeat) |
| | voltage | Number | yes | ADCS voltage |
| status | input1 | Switch | yes | State of Input 1 |
| | input2 | Switch | yes | State of Input 2 |
@@ -1186,6 +1190,7 @@ If the Shelly Add-On is installed:
| | voltage | Number | yes | Measured voltage |
| | analogInput | Number | yes | Percentage of reference voltage (VREF) at analogous input |
| | digitalInput | Switch | yes | State of digital input (ON/OFF) |
| | lastUpdate | DateTime | yes | Timestamp of the last sensor update (heartbeat) |
### Shelly Plus 1PM (thing-type: shellyplus1pm)
@@ -1216,6 +1221,7 @@ If the Shelly Add-On is installed:
| | voltage | Number | yes | Measured voltage |
| | analogInput | Number | yes | Percentage of reference voltage (VREF) at analogous input |
| | digitalInput | Switch | yes | State of digital input (ON/OFF) |
| | lastUpdate | DateTime | yes | Timestamp of the last sensor update (heartbeat) |
### Shelly Plus 2PM - relay mode (thing-type: shellyplus2pm-relay)
@@ -663,12 +663,13 @@ public class Shelly1ApiJsonDTO {
@SerializedName("ext_switch")
public ShellyStatusSensor.ShellyExtSwitchSettings extSwitch;
@SerializedName("ext_temperature")
public ShellyStatusSensor.ShellyExtTemperature extTemperature; // Shelly 1/1PM: sensor values
public ShellyStatusSensor.@Nullable ShellyExtTemperature extTemperature; // Shelly 1/1PM: sensor values
@SerializedName("ext_humidity")
public ShellyStatusSensor.ShellyExtHumidity extHumidity; // Shelly 1/1PM: sensor values
public ShellyStatusSensor.ShellyExtVoltage extVoltage; // Shelly ´Plus 1/1PM: sensor values
public ShellyStatusSensor.ShellyExtAnalogInput extAnalogInput; // Shelly ´Plus 1/1PM: sensor values
public ShellyStatusSensor.ShellyExtDigitalInput extDigitalInput; // Shelly ´Plus 1/1PM: state of digital input
public ShellyStatusSensor.@Nullable ShellyExtHumidity extHumidity; // Shelly 1/1PM: sensor values
public ShellyStatusSensor.@Nullable ShellyExtVoltage extVoltage; // Shelly ´Plus 1/1PM: sensor values
public ShellyStatusSensor.@Nullable ShellyExtAnalogInput extAnalogInput; // Shelly ´Plus 1/1PM: sensor values
public ShellyStatusSensor.@Nullable ShellyExtDigitalInput extDigitalInput; // Shelly ´Plus 1/1PM: state of
// digital input
@SerializedName("temperature_units")
public String temperatureUnits = "C"; // Either'C'or'F'
@@ -792,12 +793,12 @@ public class Shelly1ApiJsonDTO {
public Double totalReturned;
@SerializedName("ext_temperature")
public ShellyStatusSensor.ShellyExtTemperature extTemperature; // Shelly 1/1PM: sensor values
public ShellyStatusSensor.@Nullable ShellyExtTemperature extTemperature; // Shelly 1/1PM: sensor values
@SerializedName("ext_humidity")
public ShellyStatusSensor.ShellyExtHumidity extHumidity; // Shelly 1/1PM: sensor values
public ShellyStatusSensor.ShellyExtVoltage extVoltage; // Shelly ´Plus 1/1PM: sensor values
public ShellyStatusSensor.ShellyExtAnalogInput extAnalogInput; // Shelly ´Plus 1/1PM: sensor values
public ShellyStatusSensor.ShellyExtDigitalInput extDigitalInput; // Shelly ´Plus 1/1PM: sensor values
public ShellyStatusSensor.@Nullable ShellyExtHumidity extHumidity; // Shelly 1/1PM: sensor values
public ShellyStatusSensor.@Nullable ShellyExtVoltage extVoltage; // Shelly ´Plus 1/1PM: sensor values
public ShellyStatusSensor.@Nullable ShellyExtAnalogInput extAnalogInput; // Shelly ´Plus 1/1PM: sensor values
public ShellyStatusSensor.@Nullable ShellyExtDigitalInput extDigitalInput; // Shelly ´Plus 1/1PM: sensor values
@SerializedName("ext_switch")
public ShellyStatusSensor.ShellyExtSwitchStatus extSwitch;
@@ -895,9 +896,9 @@ public class Shelly1ApiJsonDTO {
public ArrayList<ShellyInputState> inputs; // Firmware 1.5.6+
@SerializedName("ext_temperature")
public ShellyStatusSensor.ShellyExtTemperature extTemperature; // Shelly 1/1PM: sensor values
public ShellyStatusSensor.@Nullable ShellyExtTemperature extTemperature; // Shelly 1/1PM: sensor values
@SerializedName("ext_humidity")
public ShellyStatusSensor.ShellyExtHumidity extHumidity; // Shelly 1/1PM: sensor values
public ShellyStatusSensor.@Nullable ShellyExtHumidity extHumidity; // Shelly 1/1PM: sensor values
public Double temperature; // device temp acc. on the selected temp unit
public ShellySensorTmp tmp;
@@ -1046,91 +1047,95 @@ public class Shelly1ApiJsonDTO {
public static class ShellyExtTemperature {
public static class ShellyShortTemp {
public String hwID; // e.g. "2882379497020381",
public Double tC; // temperature in deg C
public Double tF; // temperature in deg F
public @Nullable String hwID; // e.g. "2882379497020381"
public @Nullable Double tC; // temperature in deg C
public @Nullable Double tF; // temperature in deg F
}
// Shelly 1/1PM have up to 3 sensors
// for whatever reasons it's not an array, but 3 independent elements
@SerializedName("0")
public ShellyShortTemp sensor1;
public @Nullable ShellyShortTemp sensor1;
@SerializedName("1")
public ShellyShortTemp sensor2;
public @Nullable ShellyShortTemp sensor2;
@SerializedName("2")
public ShellyShortTemp sensor3;
public @Nullable ShellyShortTemp sensor3;
@SerializedName("3")
public ShellyShortTemp sensor4;
public @Nullable ShellyShortTemp sensor4;
@SerializedName("4")
public ShellyShortTemp sensor5;
public @Nullable ShellyShortTemp sensor5;
}
public static class ShellyExtHumidity {
public static class ShellyShortHum {
public Double hum; // Humidity reading of sensor 0, percent
public @Nullable Double hum; // Humidity reading of sensor 0, percent
}
public ShellyExtHumidity() {
}
public ShellyExtHumidity(double hum) {
sensor1 = new ShellyShortHum();
sensor1.hum = hum;
ShellyShortHum s = new ShellyShortHum();
s.hum = hum;
sensor1 = s;
}
@SerializedName("0")
public ShellyShortHum sensor1;
public @Nullable ShellyShortHum sensor1;
}
public static class ShellyExtVoltage {
public static class ShellyShortVoltage {
public Double voltage;
public @Nullable Double voltage;
}
public ShellyExtVoltage() {
}
public ShellyExtVoltage(double voltage) {
sensor1 = new ShellyShortVoltage();
sensor1.voltage = voltage;
ShellyShortVoltage s = new ShellyShortVoltage();
s.voltage = voltage;
sensor1 = s;
}
@SerializedName("0")
public ShellyShortVoltage sensor1;
public @Nullable ShellyShortVoltage sensor1;
}
public static class ShellyExtDigitalInput {
public static class ShellyShortDigitalInput {
public Boolean state;
public @Nullable Boolean state;
}
public ShellyExtDigitalInput() {
}
public ShellyExtDigitalInput(boolean state) {
sensor1 = new ShellyShortDigitalInput();
sensor1.state = state;
ShellyShortDigitalInput s = new ShellyShortDigitalInput();
s.state = state;
sensor1 = s;
}
@SerializedName("0")
public ShellyShortDigitalInput sensor1;
public @Nullable ShellyShortDigitalInput sensor1;
}
public static class ShellyExtAnalogInput {
public static class ShellyShortAnalogInput {
public Double percent;
public @Nullable Double percent;
}
public ShellyExtAnalogInput() {
}
public ShellyExtAnalogInput(double percent) {
sensor1 = new ShellyShortAnalogInput();
sensor1.percent = percent;
ShellyShortAnalogInput s = new ShellyShortAnalogInput();
s.percent = percent;
sensor1 = s;
}
@SerializedName("0")
public ShellyShortAnalogInput sensor1;
public @Nullable ShellyShortAnalogInput sensor1;
}
public static class ShellyADC {
@@ -203,12 +203,17 @@ public class Shelly1CoIoTVersion2 extends Shelly1CoIoTProtocol implements Shelly
case "3201": // sensor_1: T, extTemp, C, -55/125; unknown 999
case "3301": // sensor_2: T, extTemp, C, -55/125; unknown 999
int idx = getExtTempId(sen.id);
if (idx >= 0 && value != SHELLY_API_INVTEMP) {
// H&T, Fllod, DW only have 1 channel, 1/1PM with Addon have up to to 3 sensors
if (idx >= 0) {
// H&T, Flood, DW only have 1 channel; 1/1PM with Addon have up to 3 sensors
String channel = profile.isSensor ? CHANNEL_SENSOR_TEMP : CHANNEL_SENSOR_TEMP + idx;
// Some devices report values = -999 or 99 during fw update
updateChannel(updates, CHANNEL_GROUP_SENSOR, channel,
toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
if (value == SHELLY_API_INVTEMP) {
// Sensor present but reading invalid: publish UNDEF so the cache doesn't
// block the next valid reading when the sensor recovers.
updateChannel(updates, CHANNEL_GROUP_SENSOR, channel, UnDefType.UNDEF);
} else {
updateChannel(updates, CHANNEL_GROUP_SENSOR, channel,
toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
}
} else {
logger.debug("{}: Unable to get extSensorId {} from {}/{}", thingName, sen.id, sen.type, sen.desc);
}
@@ -471,8 +471,12 @@ public class Shelly1CoapHandler implements Shelly1CoapListener {
if (!updates.isEmpty()) {
int updated = 0;
boolean sensorGroupUpdated = false;
for (Map.Entry<String, State> u : updates.entrySet()) {
String key = u.getKey();
if (key.startsWith(CHANNEL_GROUP_SENSOR + "#")) {
sensorGroupUpdated = true;
}
updated += thingHandler.updateChannel(key, u.getValue(), false) ? 1 : 0;
}
if (updated > 0) {
@@ -482,6 +486,11 @@ public class Shelly1CoapHandler implements Shelly1CoapListener {
thingHandler.updateChannel(profile.getControlGroup(0), CHANNEL_LAST_UPDATE, getTimestamp());
}
}
if (sensorGroupUpdated && !profile.isSensor && !profile.isRoller) {
// Relay device with addon sensors: always refresh sensors#lastUpdate so the channel acts
// as a heartbeat even when temperature values are unchanged (and thus cache-deduplicated)
thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_LAST_UPDATE, getTimestamp());
}
if (profile.isLight && profile.inColor && col.isRgbValid()) {
// Update color picker from single values
@@ -1140,20 +1140,28 @@ public class Shelly2ApiClient extends ShellyHttpClient implements ShellyDiscover
}
if (ds.temperature100 != null) {
if (status.extTemperature == null) {
status.extTemperature = new ShellyExtTemperature();
ShellyExtTemperature extTemp = status.extTemperature;
if (extTemp == null) {
extTemp = new ShellyExtTemperature();
status.extTemperature = extTemp;
}
status.extTemperature.sensor1 = updateExtTempSensor(ds.temperature100);
status.extTemperature.sensor2 = updateExtTempSensor(ds.temperature101);
status.extTemperature.sensor3 = updateExtTempSensor(ds.temperature102);
status.extTemperature.sensor4 = updateExtTempSensor(ds.temperature103);
status.extTemperature.sensor5 = updateExtTempSensor(ds.temperature104);
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) {
status.extHumidity = new ShellyExtHumidity(ds.humidity100.rh);
Double rh = ds.humidity100.rh;
if (rh != null) {
status.extHumidity = new ShellyExtHumidity(rh);
}
}
if (ds.voltmeter100 != null) {
status.extVoltage = new ShellyExtVoltage(ds.voltmeter100.voltage);
Double voltage = ds.voltmeter100.voltage;
if (voltage != null) {
status.extVoltage = new ShellyExtVoltage(voltage);
}
}
if (ds.input100 != null) {
if (ds.input100.state != null) {
@@ -1167,7 +1175,8 @@ public class Shelly2ApiClient extends ShellyHttpClient implements ShellyDiscover
private @Nullable ShellyShortTemp updateExtTempSensor(@Nullable Shelly2DeviceStatusTempId value) {
if (value != null) {
ShellyShortTemp temp = new ShellyShortTemp();
temp.hwID = value.id != null ? value.id.toString() : "999";
Integer idBox = value.id;
temp.hwID = idBox != null ? idBox.toString() : "999";
temp.tC = getDouble(value.tC);
temp.tF = getDouble(value.tF);
return temp;
@@ -653,8 +653,8 @@ public class Shelly2ApiJsonDTO {
}
public class Shelly2DeviceStatusHumidity {
public Integer id;
public Double rh;
public @Nullable Integer id;
public @Nullable Double rh;
}
public class Shelly2DeviceStatusIlluminance {
@@ -664,12 +664,12 @@ public class Shelly2ApiJsonDTO {
}
public class Shelly2DeviceStatusVoltage {
public Integer id;
public Double voltage;
public @Nullable Integer id;
public @Nullable Double voltage;
}
public class Shelly2DeviceStatusTempId extends Shelly2DeviceStatusTemp {
public Integer id;
public @Nullable Integer id;
}
public static class Shelly2DeviceStatusPower {
@@ -36,7 +36,16 @@ import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettings
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyADC;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtAnalogInput;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtAnalogInput.ShellyShortAnalogInput;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtDigitalInput;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtDigitalInput.ShellyShortDigitalInput;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtHumidity;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtHumidity.ShellyShortHum;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtTemperature;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtTemperature.ShellyShortTemp;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtVoltage;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtVoltage.ShellyShortVoltage;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyThermnostat;
import org.openhab.binding.shelly.internal.provider.ShellyChannelDefinitions;
import org.openhab.core.library.types.OnOffType;
@@ -577,31 +586,55 @@ public class ShellyComponents {
}
}
// Update Add-On channeös
if (status.extTemperature != null) {
// Update Add-On channels (Shelly 1/1PM and Plus 1/1PM with sensor addon)
boolean hasAddon = hasAddon(status);
ShellyExtTemperature extTemperature = status.extTemperature;
if (extTemperature != null) {
// Shelly 1/1PM support up to 3 external sensors
// for whatever reason those are not represented as an array, but 3 elements
updated |= updateTempChannel(status.extTemperature.sensor1, thingHandler, CHANNEL_ESENSOR_TEMP1);
updated |= updateTempChannel(status.extTemperature.sensor2, thingHandler, CHANNEL_ESENSOR_TEMP2);
updated |= updateTempChannel(status.extTemperature.sensor3, thingHandler, CHANNEL_ESENSOR_TEMP3);
updated |= updateTempChannel(status.extTemperature.sensor4, thingHandler, CHANNEL_ESENSOR_TEMP4);
updated |= updateTempChannel(status.extTemperature.sensor5, thingHandler, CHANNEL_ESENSOR_TEMP5);
updated |= updateTempChannel(extTemperature.sensor1, thingHandler, CHANNEL_ESENSOR_TEMP1);
updated |= updateTempChannel(extTemperature.sensor2, thingHandler, CHANNEL_ESENSOR_TEMP2);
updated |= updateTempChannel(extTemperature.sensor3, thingHandler, CHANNEL_ESENSOR_TEMP3);
updated |= updateTempChannel(extTemperature.sensor4, thingHandler, CHANNEL_ESENSOR_TEMP4);
updated |= updateTempChannel(extTemperature.sensor5, thingHandler, CHANNEL_ESENSOR_TEMP5);
}
if ((status.extHumidity != null) && (status.extHumidity.sensor1 != null)) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENSOR_HUMIDITY,
toQuantityType(getDouble(status.extHumidity.sensor1.hum), DIGITS_PERCENT, Units.PERCENT));
ShellyExtHumidity extHumidity = status.extHumidity;
if (extHumidity != null) {
ShellyShortHum humSensor = extHumidity.sensor1;
if (humSensor != null) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENSOR_HUMIDITY,
toQuantityType(getDouble(humSensor.hum), DIGITS_PERCENT, Units.PERCENT));
}
}
if ((status.extVoltage != null) && (status.extVoltage.sensor1 != null)) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENSOR_VOLTAGE,
toQuantityType(getDouble(status.extVoltage.sensor1.voltage), 4, Units.VOLT));
ShellyExtVoltage extVoltage = status.extVoltage;
if (extVoltage != null) {
ShellyShortVoltage voltSensor = extVoltage.sensor1;
if (voltSensor != null) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENSOR_VOLTAGE,
toQuantityType(getDouble(voltSensor.voltage), 4, Units.VOLT));
}
}
if ((status.extDigitalInput != null) && (status.extDigitalInput.sensor1 != null)) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENSOR_DIGITALINPUT,
getOnOff(status.extDigitalInput.sensor1.state));
ShellyExtDigitalInput extDigitalInput = status.extDigitalInput;
if (extDigitalInput != null) {
ShellyShortDigitalInput digSensor = extDigitalInput.sensor1;
if (digSensor != null) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENSOR_DIGITALINPUT,
getOnOff(digSensor.state));
}
}
if ((status.extAnalogInput != null) && (status.extAnalogInput.sensor1 != null)) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENSOR_ANALOGINPUT,
toQuantityType(getDouble(status.extAnalogInput.sensor1.percent), DIGITS_PERCENT, Units.PERCENT));
ShellyExtAnalogInput extAnalogInput = status.extAnalogInput;
if (extAnalogInput != null) {
ShellyShortAnalogInput anaSensor = extAnalogInput.sensor1;
if (anaSensor != null) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENSOR_ANALOGINPUT,
toQuantityType(getDouble(anaSensor.percent), DIGITS_PERCENT, Units.PERCENT));
}
}
if (hasAddon && !(profile.isSensor || profile.hasBattery)) {
// Relay devices with addon sensors: always refresh lastUpdate so the channel shows
// when sensor data was last received, even when temperature values are unchanged
thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_LAST_UPDATE, getTimestamp());
updated = true;
}
return updated;
@@ -687,16 +720,29 @@ public class ShellyComponents {
return updated;
}
public static boolean hasAddon(ShellySettingsStatus status) {
return status.extTemperature != null || status.extHumidity != null || status.extVoltage != null
|| status.extDigitalInput != null || status.extAnalogInput != null;
}
public static boolean updateTempChannel(@Nullable ShellyShortTemp sensor, ShellyThingInterface thingHandler,
String channel) {
return sensor != null ? updateTempChannel(thingHandler, CHANNEL_GROUP_SENSOR, channel, sensor.tC, "") : false;
if (sensor == null) {
return false;
}
return updateTempChannel(thingHandler, CHANNEL_GROUP_SENSOR, channel, sensor.tC, "");
}
public static boolean updateTempChannel(ShellyThingInterface thingHandler, String group, String channel,
@Nullable Double temp, @Nullable String unit) {
if (temp == null || temp == SHELLY_API_INVTEMP) {
if (temp == null) {
return false;
}
if (temp == SHELLY_API_INVTEMP) {
// Sensor present but reading invalid (e.g. disconnected wire): publish UNDEF so that
// the cache does not block the next valid reading when the sensor recovers.
return thingHandler.updateChannel(group, channel, UnDefType.UNDEF);
}
return thingHandler.updateChannel(group, channel,
toQuantityType(convertToC(temp, unit), DIGITS_TEMP, SIUnits.CELSIUS));
}
@@ -40,6 +40,7 @@ import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettings
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLightChannel;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
import org.openhab.binding.shelly.internal.handler.ShellyComponents;
import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
@@ -443,19 +444,22 @@ public class ShellyChannelDefinitions {
addChannel(thing, add, addon, CHGR_SENSOR,
CHANNEL_ESENSOR_INPUT + (profile.settings.extSwitch.input0.relayNum + 1));
}
if (profile.status.extTemperature != null) {
addChannel(thing, add, profile.status.extTemperature.sensor1 != null, CHGR_SENSOR, CHANNEL_ESENSOR_TEMP1);
addChannel(thing, add, profile.status.extTemperature.sensor2 != null, CHGR_SENSOR, CHANNEL_ESENSOR_TEMP2);
addChannel(thing, add, profile.status.extTemperature.sensor3 != null, CHGR_SENSOR, CHANNEL_ESENSOR_TEMP3);
addChannel(thing, add, profile.status.extTemperature.sensor4 != null, CHGR_SENSOR, CHANNEL_ESENSOR_TEMP4);
addChannel(thing, add, profile.status.extTemperature.sensor5 != null, CHGR_SENSOR, CHANNEL_ESENSOR_TEMP5);
ShellyStatusSensor.ShellyExtTemperature extTemp = profile.status.extTemperature;
if (extTemp != null) {
addChannel(thing, add, extTemp.sensor1 != null, CHGR_SENSOR, CHANNEL_ESENSOR_TEMP1);
addChannel(thing, add, extTemp.sensor2 != null, CHGR_SENSOR, CHANNEL_ESENSOR_TEMP2);
addChannel(thing, add, extTemp.sensor3 != null, CHGR_SENSOR, CHANNEL_ESENSOR_TEMP3);
addChannel(thing, add, extTemp.sensor4 != null, CHGR_SENSOR, CHANNEL_ESENSOR_TEMP4);
addChannel(thing, add, extTemp.sensor5 != null, CHGR_SENSOR, CHANNEL_ESENSOR_TEMP5);
}
addChannel(thing, add, profile.status.extHumidity != null && profile.status.extHumidity.sensor1 != null,
CHGR_SENSOR, CHANNEL_ESENSOR_HUMIDITY);
ShellyStatusSensor.ShellyExtHumidity extHum = profile.status.extHumidity;
addChannel(thing, add, extHum != null && extHum.sensor1 != null, CHGR_SENSOR, CHANNEL_ESENSOR_HUMIDITY);
addChannel(thing, add, profile.status.extVoltage != null, CHGR_SENSOR, CHANNEL_ESENSOR_VOLTAGE);
addChannel(thing, add, profile.status.extDigitalInput != null, CHGR_SENSOR, CHANNEL_ESENSOR_DIGITALINPUT);
addChannel(thing, add, profile.status.extAnalogInput != null, CHGR_SENSOR, CHANNEL_ESENSOR_ANALOGINPUT);
addChannel(thing, add, ShellyComponents.hasAddon(profile.status), CHGR_SENSOR, CHANNEL_LAST_UPDATE);
}
public static Map<String, Channel> createDimmerChannels(final Thing thing, final ShellyDeviceProfile profile,
@@ -0,0 +1,316 @@
/*
* 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.handler;
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.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.ShellyDevices.*;
import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.SHELLY_API_INVTEMP;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtAnalogInput;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtDigitalInput;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtHumidity;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtTemperature;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtTemperature.ShellyShortTemp;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtVoltage;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.types.UnDefType;
/**
* Tests for {@link ShellyComponents} — addon sensor update path.
*
* Covers:
* - {@link ShellyComponents#hasAddon} detection logic
* - {@link ShellyComponents#updateTempChannel} INVTEMP → UNDEF promotion
* - {@link ShellyComponents#updateSensors} lastUpdate heartbeat for relay+addon devices
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyComponentsTest {
// ── hasAddon ─────────────────────────────────────────────────────────────
@Test
void hasAddon_allNull_returnsFalse() {
assertThat(ShellyComponents.hasAddon(new ShellySettingsStatus()), is(false));
}
@Test
void hasAddon_extTemperature_returnsTrue() {
ShellySettingsStatus s = new ShellySettingsStatus();
s.extTemperature = new ShellyExtTemperature();
assertThat(ShellyComponents.hasAddon(s), is(true));
}
@Test
void hasAddon_extHumidity_returnsTrue() {
ShellySettingsStatus s = new ShellySettingsStatus();
s.extHumidity = new ShellyExtHumidity();
assertThat(ShellyComponents.hasAddon(s), is(true));
}
@Test
void hasAddon_extVoltage_returnsTrue() {
ShellySettingsStatus s = new ShellySettingsStatus();
s.extVoltage = new ShellyExtVoltage();
assertThat(ShellyComponents.hasAddon(s), is(true));
}
@Test
void hasAddon_extDigitalInput_returnsTrue() {
ShellySettingsStatus s = new ShellySettingsStatus();
s.extDigitalInput = new ShellyExtDigitalInput();
assertThat(ShellyComponents.hasAddon(s), is(true));
}
@Test
void hasAddon_extAnalogInput_returnsTrue() {
ShellySettingsStatus s = new ShellySettingsStatus();
s.extAnalogInput = new ShellyExtAnalogInput();
assertThat(ShellyComponents.hasAddon(s), is(true));
}
// ── updateTempChannel ─────────────────────────────────────────────────────
@Test
void updateTempChannel_nullSensor_returnsFalse() {
ShellyThingInterface handler = mock(ShellyThingInterface.class);
assertThat(ShellyComponents.updateTempChannel(null, handler, CHANNEL_ESENSOR_TEMP1), is(false));
verify(handler, never()).updateChannel(anyString(), anyString(), any());
}
@Test
void updateTempChannel_invTemp_publishesUndef() {
ShellyThingInterface handler = mock(ShellyThingInterface.class);
when(handler.updateChannel(anyString(), anyString(), any())).thenReturn(true);
ShellyShortTemp sensor = new ShellyShortTemp();
sensor.tC = SHELLY_API_INVTEMP;
assertThat(ShellyComponents.updateTempChannel(sensor, handler, CHANNEL_ESENSOR_TEMP1), is(true));
verify(handler).updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENSOR_TEMP1, UnDefType.UNDEF);
}
@Test
void updateTempChannel_validTemp_publishesQuantityType() {
ShellyThingInterface handler = mock(ShellyThingInterface.class);
when(handler.updateChannel(anyString(), anyString(), any())).thenReturn(true);
ShellyShortTemp sensor = new ShellyShortTemp();
sensor.tC = 22.5;
assertThat(ShellyComponents.updateTempChannel(sensor, handler, CHANNEL_ESENSOR_TEMP1), is(true));
verify(handler).updateChannel(eq(CHANNEL_GROUP_SENSOR), eq(CHANNEL_ESENSOR_TEMP1),
argThat(s -> s instanceof QuantityType<?>));
}
@Test
void updateTempChannel_nullTc_returnsFalse() {
ShellyThingInterface handler = mock(ShellyThingInterface.class);
ShellyShortTemp sensor = new ShellyShortTemp();
sensor.tC = null;
assertThat(ShellyComponents.updateTempChannel(sensor, handler, CHANNEL_ESENSOR_TEMP1), is(false));
verify(handler, never()).updateChannel(anyString(), anyString(), any());
}
// ── updateSensors — relay + addon ─────────────────────────────────────────
@Test
void updateSensors_relayWithAddonTemp_updatesLastUpdate() throws Exception {
ShellyThingInterface handler = relayHandlerWith(new ShellySettingsStatus());
ShellySettingsStatus status = new ShellySettingsStatus();
ShellyExtTemperature ext1 = new ShellyExtTemperature();
ext1.sensor1 = sensorAt(20.0);
status.extTemperature = ext1;
ShellyComponents.updateSensors(handler, status);
verify(handler).updateChannel(eq(CHANNEL_GROUP_SENSOR), eq(CHANNEL_LAST_UPDATE), any());
}
@Test
void updateSensors_relayWithoutAddon_noLastUpdate() throws Exception {
ShellyThingInterface handler = relayHandlerWith(new ShellySettingsStatus());
ShellySettingsStatus status = new ShellySettingsStatus();
// no extTemperature, extHumidity, etc.
ShellyComponents.updateSensors(handler, status);
verify(handler, never()).updateChannel(eq(CHANNEL_GROUP_SENSOR), eq(CHANNEL_LAST_UPDATE), any());
}
@Test
void updateSensors_relayWithInvTemp_publishesUndefAndUpdatesLastUpdate() throws Exception {
ShellyThingInterface handler = relayHandlerWith(new ShellySettingsStatus());
ShellySettingsStatus status = new ShellySettingsStatus();
ShellyExtTemperature ext2 = new ShellyExtTemperature();
ext2.sensor1 = sensorAt(SHELLY_API_INVTEMP);
status.extTemperature = ext2;
ShellyComponents.updateSensors(handler, status);
verify(handler).updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENSOR_TEMP1, UnDefType.UNDEF);
verify(handler).updateChannel(eq(CHANNEL_GROUP_SENSOR), eq(CHANNEL_LAST_UPDATE), any());
}
@Test
void updateSensors_pureRelayNoAddon_lastUpdateNeverWritten() throws Exception {
ShellyThingInterface handler = relayHandlerWith(new ShellySettingsStatus());
ShellyComponents.updateSensors(handler, new ShellySettingsStatus());
verify(handler, never()).updateChannel(eq(CHANNEL_GROUP_SENSOR), eq(CHANNEL_LAST_UPDATE), any());
}
@Test
void updateSensors_lastUpdateWrittenEvenWhenTempValueUnchanged() throws Exception {
// Critical heartbeat test: even when the temperature channel is cache-deduplicated
// (updateChannel returns false), sensors#lastUpdate must still be written so the
// channel acts as a "sensor alive" indicator.
ShellyThingInterface handler = relayHandlerWith(new ShellySettingsStatus());
when(handler.updateChannel(eq(CHANNEL_GROUP_SENSOR), eq(CHANNEL_ESENSOR_TEMP1), any())).thenReturn(false); // simulate
// cache
// dedup
ShellySettingsStatus status = new ShellySettingsStatus();
ShellyExtTemperature ext3 = new ShellyExtTemperature();
ext3.sensor1 = sensorAt(22.5);
status.extTemperature = ext3;
boolean result = ShellyComponents.updateSensors(handler, status);
assertThat("updated must be true even when temp is deduplicated", result, is(true));
verify(handler).updateChannel(eq(CHANNEL_GROUP_SENSOR), eq(CHANNEL_LAST_UPDATE), any());
}
@Test
void updateSensors_nullSensorSlot_skipsPublish() throws Exception {
// sensor1 valid, sensor2 null — only sensor1 published, sensor2 not touched
ShellyThingInterface handler = relayHandlerWith(new ShellySettingsStatus());
ShellySettingsStatus status = new ShellySettingsStatus();
ShellyExtTemperature ext4 = new ShellyExtTemperature();
ext4.sensor1 = sensorAt(21.0);
ext4.sensor2 = null;
status.extTemperature = ext4;
ShellyComponents.updateSensors(handler, status);
verify(handler).updateChannel(eq(CHANNEL_GROUP_SENSOR), eq(CHANNEL_ESENSOR_TEMP1), any());
verify(handler, never()).updateChannel(eq(CHANNEL_GROUP_SENSOR), eq(CHANNEL_ESENSOR_TEMP2), any());
}
@Test
void updateSensors_addonHumidity_updatesLastUpdate() throws Exception {
ShellyThingInterface handler = relayHandlerWith(new ShellySettingsStatus());
ShellySettingsStatus status = new ShellySettingsStatus();
status.extHumidity = new ShellyExtHumidity(55.0);
ShellyComponents.updateSensors(handler, status);
verify(handler).updateChannel(eq(CHANNEL_GROUP_SENSOR), eq(CHANNEL_LAST_UPDATE), any());
}
@Test
void hasAddon_multipleFieldsSet_returnsTrue() {
ShellySettingsStatus s = new ShellySettingsStatus();
s.extTemperature = new ShellyExtTemperature();
s.extHumidity = new ShellyExtHumidity();
assertThat(ShellyComponents.hasAddon(s), is(true));
}
@Test
void updateSensors_voltage_updatesVoltageChannel() throws Exception {
ShellyThingInterface handler = relayHandlerWith(new ShellySettingsStatus());
ShellySettingsStatus status = new ShellySettingsStatus();
status.extVoltage = new ShellyExtVoltage(3.3);
ShellyComponents.updateSensors(handler, status);
verify(handler).updateChannel(eq(CHANNEL_GROUP_SENSOR), eq(CHANNEL_ESENSOR_VOLTAGE),
argThat(s -> s instanceof QuantityType<?> && ((QuantityType<?>) s).doubleValue() == 3.3));
}
@Test
void updateSensors_digitalInput_updatesDigitalInputChannel() throws Exception {
ShellyThingInterface handler = relayHandlerWith(new ShellySettingsStatus());
ShellySettingsStatus status = new ShellySettingsStatus();
status.extDigitalInput = new ShellyExtDigitalInput(true);
ShellyComponents.updateSensors(handler, status);
verify(handler).updateChannel(eq(CHANNEL_GROUP_SENSOR), eq(CHANNEL_ESENSOR_DIGITALINPUT), eq(OnOffType.ON));
}
@Test
void updateSensors_analogInput_updatesAnalogInputChannel() throws Exception {
ShellyThingInterface handler = relayHandlerWith(new ShellySettingsStatus());
ShellySettingsStatus status = new ShellySettingsStatus();
status.extAnalogInput = new ShellyExtAnalogInput(75.0);
ShellyComponents.updateSensors(handler, status);
verify(handler).updateChannel(eq(CHANNEL_GROUP_SENSOR), eq(CHANNEL_ESENSOR_ANALOGINPUT),
argThat(s -> s instanceof QuantityType<?> && ((QuantityType<?>) s).doubleValue() == 75.0));
}
@Test
void updateSensors_multipleExtTempSlots_allPublished() throws Exception {
ShellyThingInterface handler = relayHandlerWith(new ShellySettingsStatus());
ShellySettingsStatus status = new ShellySettingsStatus();
ShellyExtTemperature ext = new ShellyExtTemperature();
ext.sensor1 = sensorAt(20.0);
ext.sensor2 = sensorAt(21.0);
status.extTemperature = ext;
ShellyComponents.updateSensors(handler, status);
verify(handler).updateChannel(eq(CHANNEL_GROUP_SENSOR), eq(CHANNEL_ESENSOR_TEMP1), any());
verify(handler).updateChannel(eq(CHANNEL_GROUP_SENSOR), eq(CHANNEL_ESENSOR_TEMP2), any());
verify(handler, never()).updateChannel(eq(CHANNEL_GROUP_SENSOR), eq(CHANNEL_ESENSOR_TEMP3), any());
}
// ── helpers ───────────────────────────────────────────────────────────────
private static ShellyThingInterface relayHandlerWith(ShellySettingsStatus profileStatus) {
ShellyDeviceProfile profile = new ShellyDeviceProfile(THING_TYPE_SHELLYPLUS1PM);
profile.isSensor = false;
profile.hasBattery = false;
profile.status = profileStatus;
ShellyThingInterface handler = mock(ShellyThingInterface.class);
when(handler.getProfile()).thenReturn(profile);
when(handler.areChannelsCreated()).thenReturn(true);
when(handler.updateChannel(anyString(), anyString(), any())).thenReturn(true);
return handler;
}
private static ShellyShortTemp sensorAt(double tC) {
ShellyShortTemp s = new ShellyShortTemp();
s.tC = tC;
return s;
}
}