| carbonmonoxide | Switch | R | `ON` = carbon monoxide detected | carbonmonoxide |
| color | Color | R | Color set by remote | colorcontrol |
| windowopen | Contact | R | `windowopen` status is reported by some thermostats | thermostat |
-| externalwindowopen | Contact | R/W | forward a status to a theromastat (some devices) | thermostat |
-| locked | Switch | R/W | reports/sets the childlock on some thermostats | thermostat |
+| externalwindowopen | Contact | R/W | forward a status to a thermostat (some devices) | thermostat |
+| on | Switch | R | some thermostats report their output state as switch | thermostat |
+| locked | Switch | R/W | reports/sets the child lock on some thermostats | thermostat |
| airquality | String | R | Airquality as string | airqualitysensor |
| airqualityppb | Number:Dimensionless | R | Airquality (in parts-per-billion) | airqualitysensor |
| moisture | Number:Dimensionless | R | Moisture | moisturesensor |
public static final String CHANNEL_THERMOSTAT_MODE = "mode";
public static final String CHANNEL_THERMOSTAT_LOCKED = "locked";
public static final String CHANNEL_TEMPERATURE_OFFSET = "offset";
+ public static final String CHANNEL_THERMOSTAT_ON = "on";
public static final String CHANNEL_VALVE_POSITION = "valve";
public static final String CHANNEL_WINDOW_OPEN = "windowopen";
public static final String CHANNEL_EXTERNAL_WINDOW_OPEN = "externalwindowopen";
public @Nullable Integer gesture;
/** Thermostat may provide this value. */
public @Nullable Integer valve;
+ public @Nullable Boolean on;
/** air quality sensors provide this value */
public @Nullable String airquality;
public @Nullable Integer airqualityppb;
import static org.openhab.core.library.unit.Units.PERCENT;
import java.math.BigDecimal;
-import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
import org.openhab.binding.deconz.internal.dto.SensorConfig;
-import org.openhab.binding.deconz.internal.dto.SensorMessage;
import org.openhab.binding.deconz.internal.dto.SensorState;
import org.openhab.binding.deconz.internal.dto.ThermostatUpdateConfig;
import org.openhab.binding.deconz.internal.types.ThermostatMode;
public class SensorThermostatThingHandler extends SensorBaseThingHandler {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_THERMOSTAT);
- private static final List<String> CONFIG_CHANNELS = Arrays.asList(CHANNEL_BATTERY_LEVEL, CHANNEL_BATTERY_LOW,
- CHANNEL_HEATSETPOINT, CHANNEL_TEMPERATURE_OFFSET, CHANNEL_THERMOSTAT_MODE, CHANNEL_THERMOSTAT_LOCKED);
+ private static final List<String> CONFIG_CHANNELS = List.of(CHANNEL_EXTERNAL_WINDOW_OPEN, CHANNEL_BATTERY_LEVEL,
+ CHANNEL_BATTERY_LOW, CHANNEL_HEATSETPOINT, CHANNEL_TEMPERATURE_OFFSET, CHANNEL_THERMOSTAT_MODE,
+ CHANNEL_THERMOSTAT_LOCKED);
private final Logger logger = LoggerFactory.getLogger(SensorThermostatThingHandler.class);
updateState(channelUID, "Closed".equals(open) ? OpenClosedType.CLOSED : OpenClosedType.OPEN);
}
}
+ case CHANNEL_THERMOSTAT_ON -> updateSwitchChannel(channelUID, newState.on);
}
}
if (sensorConfig.locked != null && createChannel(thingBuilder, CHANNEL_THERMOSTAT_LOCKED, ChannelKind.STATE)) {
thingEdited = true;
}
+ if (sensorState.valve != null && createChannel(thingBuilder, CHANNEL_VALVE_POSITION, ChannelKind.STATE)) {
+ thingEdited = true;
+ }
+ if (sensorState.on != null && createChannel(thingBuilder, CHANNEL_THERMOSTAT_ON, ChannelKind.STATE)) {
+ thingEdited = true;
+ }
+ if (sensorState.windowopen != null && createChannel(thingBuilder, CHANNEL_WINDOW_OPEN, ChannelKind.STATE)) {
+ thingEdited = true;
+ }
+ if (sensorConfig.externalwindowopen != null
+ && createChannel(thingBuilder, CHANNEL_EXTERNAL_WINDOW_OPEN, ChannelKind.STATE)) {
+ thingEdited = true;
+ }
+
return thingEdited;
}
}
return newTemperature.scaleByPowerOfTen(2).intValue();
}
-
- @Override
- protected void processStateResponse(DeconzBaseMessage stateResponse) {
- if (!(stateResponse instanceof SensorMessage sensorMessage)) {
- return;
- }
-
- SensorState sensorState = sensorMessage.state;
- SensorConfig sensorConfig = sensorMessage.config;
-
- boolean changed = false;
- ThingBuilder thingBuilder = editThing();
-
- if (sensorState != null && sensorState.windowopen != null) {
- if (createChannel(thingBuilder, CHANNEL_WINDOW_OPEN, ChannelKind.STATE)) {
- changed = true;
- }
- }
-
- if (sensorConfig != null && sensorConfig.externalwindowopen != null) {
- if (createChannel(thingBuilder, CHANNEL_EXTERNAL_WINDOW_OPEN, ChannelKind.STATE)) {
- changed = true;
- }
- }
-
- if (changed) {
- updateThing(thingBuilder.build());
- }
-
- super.processStateResponse(stateResponse);
- }
}
# thing types
-thing-type.deconz.airqualitysensor.label = Carbon-monoxide Sensor
+thing-type.deconz.airqualitysensor.label = Air Quality Sensor
thing-type.deconz.alarmsensor.label = Alarm Sensor
thing-type.deconz.batterysensor.label = Battery Sensor
thing-type.deconz.carbonmonoxidesensor.label = Carbon-monoxide Sensor
channel-type.deconz.gesture.state.option.8 = Rotate Counter Clockwise
channel-type.deconz.gestureevent.label = Gesture Trigger
channel-type.deconz.gestureevent.description = This channel is triggered on a gesture event. The trigger payload consists of the gesture event number.
-channel-type.deconz.heatsetpoint.label = Target temperature
+channel-type.deconz.heatsetpoint.label = Target Temperature
channel-type.deconz.heatsetpoint.description = Target temperature
channel-type.deconz.humidity.label = Humidity
channel-type.deconz.humidity.description = Current humidity
channel-type.deconz.moisture.description = Current moisture
channel-type.deconz.offset.label = Offset
channel-type.deconz.offset.description = Temperature offset
+channel-type.deconz.on.label = Heater State
channel-type.deconz.ontime.label = On Time
channel-type.deconz.ontime.description = Time that the light stays on before switched off automatically (0=forever)
channel-type.deconz.open.label = Open/Close
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
- <label>Carbon-monoxide Sensor</label>
+ <label>Air Quality Sensor</label>
<channels>
<channel typeId="airquality" id="airquality"/>
<channel typeId="airqualityppb" id="airqualityppb"/>
<channel typeId="heatsetpoint" id="heatsetpoint"/>
<channel typeId="mode" id="mode"/>
<channel typeId="offset" id="offset"/>
- <channel typeId="valve" id="valve"/>
<channel typeId="last_updated" id="last_updated"/>
</channels>
<representation-property>uid</representation-property>
<description>Current valve position</description>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
+ <channel-type id="on">
+ <item-type>Switch</item-type>
+ <label>Heater State</label>
+ <state readOnly="true"/>
+ </channel-type>
</thing:thing-descriptions>
* @author Jan N. Klug - Initial contribution
*/
@ExtendWith(MockitoExtension.class)
-@MockitoSettings(strictness = Strictness.WARN)
+@MockitoSettings(strictness = Strictness.LENIENT)
@NonNullByDefault
public class DeconzTest {
private @NonNullByDefault({}) Gson gson;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.openhab.binding.deconz.internal.dto.SensorMessage;
-import org.openhab.binding.deconz.internal.handler.SensorThermostatThingHandler;
import org.openhab.binding.deconz.internal.handler.SensorThingHandler;
import org.openhab.binding.deconz.internal.types.LightType;
import org.openhab.binding.deconz.internal.types.LightTypeDeserializer;
-import org.openhab.binding.deconz.internal.types.ThermostatMode;
-import org.openhab.binding.deconz.internal.types.ThermostatModeGsonTypeAdapter;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
-import org.openhab.core.library.unit.SIUnits;
-import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
-import org.openhab.core.types.UnDefType;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
public void initialize() {
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(LightType.class, new LightTypeDeserializer());
- gsonBuilder.registerTypeAdapter(ThermostatMode.class, new ThermostatModeGsonTypeAdapter());
gson = gsonBuilder.create();
}
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID), eq(new QuantityType<>("129 ppb")));
}
- @Test
- public void thermostatSensorUpdateTest() throws IOException {
- SensorMessage sensorMessage = DeconzTest.getObjectFromJson("thermostat.json", SensorMessage.class, gson);
- assertNotNull(sensorMessage);
-
- ThingUID thingUID = new ThingUID("deconz", "sensor");
- ChannelUID channelValveUID = new ChannelUID(thingUID, "valve");
- ChannelUID channelHeatSetPointUID = new ChannelUID(thingUID, "heatsetpoint");
- ChannelUID channelModeUID = new ChannelUID(thingUID, "mode");
- ChannelUID channelTemperatureUID = new ChannelUID(thingUID, "temperature");
- Thing sensor = ThingBuilder.create(THING_TYPE_THERMOSTAT, thingUID)
- .withChannel(ChannelBuilder.create(channelValveUID, "Number").build())
- .withChannel(ChannelBuilder.create(channelHeatSetPointUID, "Number").build())
- .withChannel(ChannelBuilder.create(channelModeUID, "String").build())
- .withChannel(ChannelBuilder.create(channelTemperatureUID, "Number").build()).build();
- SensorThermostatThingHandler sensorThingHandler = new SensorThermostatThingHandler(sensor, gson);
- sensorThingHandler.setCallback(thingHandlerCallback);
-
- sensorMessage = DeconzTest.getObjectFromJson("thermostat-undef.json", SensorMessage.class, gson);
- assertNotNull(sensorMessage);
- sensorThingHandler.messageReceived(sensorMessage);
-
- Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelValveUID), eq(UnDefType.UNDEF));
- Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelHeatSetPointUID),
- eq(new QuantityType<>(25, SIUnits.CELSIUS)));
- Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelModeUID),
- eq(new StringType(ThermostatMode.AUTO.name())));
- Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelTemperatureUID),
- eq(new QuantityType<>(16.5, SIUnits.CELSIUS)));
-
- sensorMessage = DeconzTest.getObjectFromJson("thermostat.json", SensorMessage.class, gson);
- assertNotNull(sensorMessage);
- sensorThingHandler.messageReceived(sensorMessage);
- Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelValveUID),
- eq(new QuantityType<>(99, Units.PERCENT)));
- }
-
@Test
public void fireSensorUpdateTest() throws IOException {
SensorMessage sensorMessage = DeconzTest.getObjectFromJson("fire.json", SensorMessage.class, gson);
--- /dev/null
+/**
+ * Copyright (c) 2010-2023 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.deconz.internal.handler;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.openhab.binding.deconz.internal.BindingConstants.*;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.openhab.binding.deconz.DeconzTest;
+import org.openhab.binding.deconz.internal.Util;
+import org.openhab.binding.deconz.internal.dto.BridgeFullState;
+import org.openhab.binding.deconz.internal.dto.SensorMessage;
+import org.openhab.binding.deconz.internal.netutils.WebSocketConnection;
+import org.openhab.binding.deconz.internal.types.LightType;
+import org.openhab.binding.deconz.internal.types.LightTypeDeserializer;
+import org.openhab.binding.deconz.internal.types.ResourceType;
+import org.openhab.binding.deconz.internal.types.ThermostatMode;
+import org.openhab.binding.deconz.internal.types.ThermostatModeGsonTypeAdapter;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.OpenClosedType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.test.java.JavaTest;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandlerCallback;
+import org.openhab.core.thing.binding.builder.ChannelBuilder;
+import org.openhab.core.thing.binding.builder.ThingBuilder;
+import org.openhab.core.thing.type.ChannelTypeUID;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+/**
+ * The {@link SensorThermostatThingHandlerTest} contains test classes for the {@link SensorThermostatThingHandler}
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+@NonNullByDefault
+public class SensorThermostatThingHandlerTest extends JavaTest {
+
+ private static final ThingUID BRIDGE_UID = new ThingUID(BRIDGE_TYPE, "bridge");
+ private static final ThingUID THING_UID = new ThingUID(THING_TYPE_THERMOSTAT, "thing");
+
+ private @Mock @NonNullByDefault({}) Bridge bridge;
+ private @Mock @NonNullByDefault({}) ThingHandlerCallback callback;
+
+ private @Mock @NonNullByDefault({}) DeconzBridgeHandler bridgeHandler;
+ private @Mock @NonNullByDefault({}) WebSocketConnection webSocketConnection;
+ private @Mock @NonNullByDefault({}) BridgeFullState bridgeFullState;
+
+ private @NonNullByDefault({}) Gson gson;
+ private @NonNullByDefault({}) Thing thing;
+ private @NonNullByDefault({}) SensorThermostatThingHandler thingHandler;
+ private @NonNullByDefault({}) SensorMessage sensorMessage;
+
+ @BeforeEach
+ public void setup() {
+ GsonBuilder gsonBuilder = new GsonBuilder();
+ gsonBuilder.registerTypeAdapter(LightType.class, new LightTypeDeserializer());
+ gsonBuilder.registerTypeAdapter(ThermostatMode.class, new ThermostatModeGsonTypeAdapter());
+ gson = gsonBuilder.create();
+
+ ThingBuilder thingBuilder = ThingBuilder.create(THING_TYPE_THERMOSTAT, THING_UID);
+ thingBuilder.withBridge(BRIDGE_UID);
+ for (String channelId : List.of(CHANNEL_TEMPERATURE, CHANNEL_HEATSETPOINT, CHANNEL_THERMOSTAT_MODE,
+ CHANNEL_TEMPERATURE_OFFSET, CHANNEL_LAST_UPDATED)) {
+ Channel channel = ChannelBuilder.create(new ChannelUID(THING_UID, channelId))
+ .withType(new ChannelTypeUID(BINDING_ID, channelId)).build();
+ thingBuilder.withChannel(channel);
+ }
+ thingBuilder.withConfiguration(new Configuration(Map.of(CONFIG_ID, "1")));
+ thing = thingBuilder.build();
+
+ thingHandler = new SensorThermostatThingHandler(thing, gson);
+ thingHandler.setCallback(callback);
+
+ when(callback.getBridge(BRIDGE_UID)).thenReturn(bridge);
+ when(callback.createChannelBuilder(any(ChannelUID.class), any(ChannelTypeUID.class)))
+ .thenAnswer(i -> ChannelBuilder.create((ChannelUID) i.getArgument(0)).withType(i.getArgument(1)));
+ doAnswer(i -> {
+ thing = i.getArgument(0);
+ thingHandler.thingUpdated(thing);
+ return null;
+ }).when(callback).thingUpdated(any(Thing.class));
+
+ when(bridge.getStatusInfo()).thenReturn(new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, ""));
+ when(bridge.getHandler()).thenReturn(bridgeHandler);
+
+ when(bridgeHandler.getWebSocketConnection()).thenReturn(webSocketConnection);
+ when(bridgeHandler.getBridgeFullState())
+ .thenReturn(CompletableFuture.completedFuture(Optional.of(bridgeFullState)));
+
+ when(bridgeFullState.getMessage(ResourceType.SENSORS, "1")).thenAnswer(i -> sensorMessage);
+ }
+
+ @Test
+ public void testDanfoss() throws IOException {
+ Set<TestParam> expected = Set.of(
+ // standard channels
+ new TestParam(CHANNEL_TEMPERATURE, new QuantityType<>("21.45 °C")),
+ new TestParam(CHANNEL_HEATSETPOINT, new QuantityType<>("21.00 °C")),
+ new TestParam(CHANNEL_THERMOSTAT_MODE, new StringType("HEAT")),
+ new TestParam(CHANNEL_TEMPERATURE_OFFSET, new QuantityType<>("0.0 °C")),
+ new TestParam(CHANNEL_LAST_UPDATED, Util.convertTimestampToDateTime("2023-03-18T05:52:29.506")),
+ // battery
+ new TestParam(CHANNEL_BATTERY_LEVEL, new DecimalType(41)),
+ new TestParam(CHANNEL_BATTERY_LOW, OnOffType.OFF),
+ // last seen
+ new TestParam(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime("2023-03-18T05:58Z")),
+ // dynamic channels
+ new TestParam(CHANNEL_EXTERNAL_WINDOW_OPEN, OpenClosedType.CLOSED),
+ new TestParam(CHANNEL_VALVE_POSITION, new QuantityType<>("1 %")),
+ new TestParam(CHANNEL_THERMOSTAT_LOCKED, OnOffType.OFF),
+ new TestParam(CHANNEL_THERMOSTAT_ON, OnOffType.OFF),
+ new TestParam(CHANNEL_WINDOW_OPEN, OpenClosedType.CLOSED));
+
+ assertThermostat("json/thermostat/danfoss.json", expected);
+ }
+
+ @Test
+ public void testNamron() throws IOException {
+ Set<TestParam> expected = Set.of(
+ // standard channels
+ new TestParam(CHANNEL_TEMPERATURE, new QuantityType<>("20.39 °C")),
+ new TestParam(CHANNEL_HEATSETPOINT, new QuantityType<>("22.00 °C")),
+ new TestParam(CHANNEL_THERMOSTAT_MODE, new StringType("OFF")),
+ new TestParam(CHANNEL_TEMPERATURE_OFFSET, new QuantityType<>("0.0 °C")),
+ new TestParam(CHANNEL_LAST_UPDATED, Util.convertTimestampToDateTime("2023-03-18T18:10:39.296")),
+ // last seen
+ new TestParam(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime("2023-03-18T18:10Z")),
+ // dynamic channels
+ new TestParam(CHANNEL_THERMOSTAT_LOCKED, OnOffType.OFF),
+ new TestParam(CHANNEL_THERMOSTAT_ON, OnOffType.OFF));
+
+ assertThermostat("json/thermostat/namron_ZB_E1.json", expected);
+ }
+
+ @Test
+ public void testEurotronicValid() throws IOException {
+ Set<TestParam> expected = Set.of(
+ // standard channels
+ new TestParam(CHANNEL_TEMPERATURE, new QuantityType<>("16.50 °C")),
+ new TestParam(CHANNEL_HEATSETPOINT, new QuantityType<>("25.00 °C")),
+ new TestParam(CHANNEL_THERMOSTAT_MODE, new StringType("AUTO")),
+ new TestParam(CHANNEL_TEMPERATURE_OFFSET, new QuantityType<>("0.0 °C")),
+ new TestParam(CHANNEL_LAST_UPDATED, Util.convertTimestampToDateTime("2020-05-31T20:24:55.819")),
+ // battery
+ new TestParam(CHANNEL_BATTERY_LEVEL, new DecimalType(85)),
+ new TestParam(CHANNEL_BATTERY_LOW, OnOffType.OFF),
+ // last seen
+ new TestParam(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime("2020-05-31T20:24:55.819")),
+ // dynamic channels
+ new TestParam(CHANNEL_VALVE_POSITION, new QuantityType<>("99 %")),
+ new TestParam(CHANNEL_THERMOSTAT_ON, OnOffType.ON));
+
+ assertThermostat("json/thermostat/eurotronic.json", expected);
+ }
+
+ @Test
+ public void testEurotronicInvalid() throws IOException {
+ Set<TestParam> expected = Set.of(
+ // standard channels
+ new TestParam(CHANNEL_TEMPERATURE, new QuantityType<>("16.50 °C")),
+ new TestParam(CHANNEL_HEATSETPOINT, new QuantityType<>("25.00 °C")),
+ new TestParam(CHANNEL_THERMOSTAT_MODE, new StringType("AUTO")),
+ new TestParam(CHANNEL_TEMPERATURE_OFFSET, new QuantityType<>("0.0 °C")),
+ new TestParam(CHANNEL_LAST_UPDATED, Util.convertTimestampToDateTime("2020-05-31T20:24:55.819")),
+ // battery
+ new TestParam(CHANNEL_BATTERY_LEVEL, new DecimalType(85)),
+ new TestParam(CHANNEL_BATTERY_LOW, OnOffType.OFF),
+ // last seen
+ new TestParam(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime("2020-05-31T20:24:55.819")),
+ // dynamic channels
+ new TestParam(CHANNEL_VALVE_POSITION, UnDefType.UNDEF),
+ new TestParam(CHANNEL_THERMOSTAT_ON, OnOffType.ON));
+
+ assertThermostat("json/thermostat/eurotronic-invalid.json", expected);
+ }
+
+ private void assertThermostat(String fileName, Set<TestParam> expected) throws IOException {
+ sensorMessage = DeconzTest.getObjectFromJson(fileName, SensorMessage.class, gson);
+
+ thingHandler.initialize();
+
+ ArgumentCaptor<ThingStatusInfo> captor = ArgumentCaptor.forClass(ThingStatusInfo.class);
+ verify(callback, times(6)).statusUpdated(eq(thing), captor.capture());
+
+ List<ThingStatusInfo> statusInfoList = captor.getAllValues();
+ assertThat(statusInfoList.get(0).getStatus(), is(ThingStatus.UNKNOWN));
+ assertThat(statusInfoList.get(5).getStatus(), is(ThingStatus.ONLINE));
+
+ assertThat(thing.getChannels().size(), is(expected.size()));
+ for (TestParam testParam : expected) {
+ Channel channel = thing.getChannel(testParam.channelId());
+ assertThat(channel + "expected but missing", channel, is(notNullValue()));
+
+ State state = testParam.state;
+ if (state != null) {
+ verify(callback, times(3).description(channel + " did not receive an update"))
+ .stateUpdated(eq(channel.getUID()), eq(state));
+ }
+ }
+ }
+
+ private record TestParam(String channelId, @Nullable State state) {
+ }
+}
--- /dev/null
+{
+ "config": {
+ "battery": 41,
+ "displayflipped": false,
+ "externalsensortemp": -8000,
+ "externalwindowopen": false,
+ "heatsetpoint": 2100,
+ "locked": false,
+ "mode": "heat",
+ "mountingmode": false,
+ "offset": 0,
+ "on": true,
+ "reachable": true,
+ "schedule": {},
+ "schedule_on": false
+ },
+ "ep": 1,
+ "etag": "ef283096d058861074798efae930ab36",
+ "lastannounced": null,
+ "lastseen": "2023-03-18T05:58Z",
+ "manufacturername": "Danfoss",
+ "modelid": "eTRV0103",
+ "name": "Thermostat Flur",
+ "state": {
+ "errorcode": "0",
+ "lastupdated": "2023-03-18T05:52:29.506",
+ "mountingmodeactive": false,
+ "on": false,
+ "temperature": 2145,
+ "valve": 1,
+ "windowopen": "Closed"
+ },
+ "swversion": "00.20.0008 00.20",
+ "type": "ZHAThermostat",
+ "uniqueid": "xxxx"
+}
\ No newline at end of file
--- /dev/null
+{
+ "config": {
+ "battery": 85,
+ "displayflipped": null,
+ "heatsetpoint": 2500,
+ "locked": null,
+ "mode": "auto",
+ "offset": 0,
+ "on": true,
+ "reachable": true
+ },
+ "ep": 1,
+ "etag": "717549a99371f3ea1a5f0b40f1537094",
+ "lastseen": "2020-05-31T20:24:55.819",
+ "manufacturername": "Eurotronic",
+ "modelid": "SPZB0001",
+ "name": "Test Thermostat",
+ "state": {
+ "lastupdated": "2020-05-31T20:24:55.819",
+ "on": true,
+ "temperature": 1650,
+ "valve": 255
+ },
+ "swversion": "20191014",
+ "type": "ZHAThermostat",
+ "uniqueid": "00:15:8d:00:01:ff:8a:00-01-0201"
+}
\ No newline at end of file
--- /dev/null
+{
+ "config": {
+ "battery": 85,
+ "displayflipped": null,
+ "heatsetpoint": 2500,
+ "locked": null,
+ "mode": "auto",
+ "offset": 0,
+ "on": true,
+ "reachable": true
+ },
+ "ep": 1,
+ "etag": "717549a99371f3ea1a5f0b40f1537094",
+ "lastseen": "2020-05-31T20:24:55.819",
+ "manufacturername": "Eurotronic",
+ "modelid": "SPZB0001",
+ "name": "Test Thermostat",
+ "state": {
+ "lastupdated": "2020-05-31T20:24:55.819",
+ "on": true,
+ "temperature": 1650,
+ "valve": 99
+ },
+ "swversion": "20191014",
+ "type": "ZHAThermostat",
+ "uniqueid": "00:15:8d:00:01:ff:8a:00-01-0201"
+}
\ No newline at end of file
--- /dev/null
+{
+ "config": {
+ "heatsetpoint": 2200,
+ "locked": false,
+ "mode": "off",
+ "offset": 0,
+ "on": true,
+ "reachable": true
+ },
+ "ep": 1,
+ "etag": "etagXXXXXXXXXXXXXX",
+ "lastannounced": "2023-03-10T06:11:09Z",
+ "lastseen": "2023-03-18T18:10Z",
+ "manufacturername": "NAMRON AS",
+ "modelid": "5401395",
+ "name": "ZB_E1_PanelOvn",
+ "state": {
+ "lastupdated": "2023-03-18T18:10:39.296",
+ "on": false,
+ "temperature": 2039
+ },
+ "swversion": "6.9.1.0_r4",
+ "type": "ZHAThermostat",
+ "uniqueid": "IDXXXXXXXXX"
+}
\ No newline at end of file
+++ /dev/null
-{
- "config": {
- "battery": 85,
- "displayflipped": null,
- "heatsetpoint": 2500,
- "locked": null,
- "mode": "auto",
- "offset": 0,
- "on": true,
- "reachable": true
- },
- "ep": 1,
- "etag": "717549a99371f3ea1a5f0b40f1537094",
- "lastseen": "2020-05-31T20:24:55.819",
- "manufacturername": "Eurotronic",
- "modelid": "SPZB0001",
- "name": "Test Thermostat",
- "state": {
- "lastupdated": "2020-05-31T20:24:55.819",
- "on": true,
- "temperature": 1650,
- "valve": 255
- },
- "swversion": "20191014",
- "type": "ZHAThermostat",
- "uniqueid": "00:15:8d:00:01:ff:8a:00-01-0201"
-}
\ No newline at end of file
+++ /dev/null
-{
- "config": {
- "battery": 85,
- "displayflipped": null,
- "heatsetpoint": 2500,
- "locked": null,
- "mode": "auto",
- "offset": 0,
- "on": true,
- "reachable": true
- },
- "ep": 1,
- "etag": "717549a99371f3ea1a5f0b40f1537094",
- "lastseen": "2020-05-31T20:24:55.819",
- "manufacturername": "Eurotronic",
- "modelid": "SPZB0001",
- "name": "Test Thermostat",
- "state": {
- "lastupdated": "2020-05-31T20:24:55.819",
- "on": true,
- "temperature": 1650,
- "valve": 99
- },
- "swversion": "20191014",
- "type": "ZHAThermostat",
- "uniqueid": "00:15:8d:00:01:ff:8a:00-01-0201"
-}
\ No newline at end of file