]> git.basschouten.com Git - openhab-addons.git/commitdiff
[deconz] Add On/Off thermostats (#14636)
authorJ-N-K <github@klug.nrw>
Sun, 19 Mar 2023 19:43:15 +0000 (20:43 +0100)
committerGitHub <noreply@github.com>
Sun, 19 Mar 2023 19:43:15 +0000 (20:43 +0100)
* [deconz] Add On/Off thermostats
* further work
* fix regression

Signed-off-by: Jan N. Klug <github@klug.nrw>
15 files changed:
bundles/org.openhab.binding.deconz/README.md
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/SensorState.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandler.java
bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/i18n/deconz.properties
bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/sensor-thing-types.xml
bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/DeconzTest.java
bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/SensorsTest.java
bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandlerTest.java [new file with mode: 0644]
bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/json/thermostat/danfoss.json [new file with mode: 0644]
bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/json/thermostat/eurotronic-invalid.json [new file with mode: 0644]
bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/json/thermostat/eurotronic.json [new file with mode: 0644]
bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/json/thermostat/namron_ZB_E1.json [new file with mode: 0644]
bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/thermostat-undef.json [deleted file]
bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/thermostat.json [deleted file]

index be8def99a340be4f1ca000ece58fd846d218a0d9..d580134e468f23c6ad904cddf9c67fc6b39ff85a 100644 (file)
@@ -156,8 +156,9 @@ The sensor devices support some of the following channels:
 | 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                                    |
index 1d8c6c58aca3a51eb1cc825177ef2a0f8608a6b4..c29e7063ab0f6734f377be6aba16c6fca1a293f1 100644 (file)
@@ -110,6 +110,7 @@ public class BindingConstants {
     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";
index 2d38f8c0bb142dcc1a649ae6959e5639a82437ed..ab95549276259723da7eef4bc6c89816cb1a995e 100644 (file)
@@ -79,6 +79,7 @@ public class SensorState {
     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;
index 0f04d09e06d815afb4b3a4fef7ac36db762d3846..a7780f47057e48c3344a41e0adce46e6953b136d 100644 (file)
@@ -17,7 +17,6 @@ import static org.openhab.core.library.unit.SIUnits.CELSIUS;
 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;
@@ -26,9 +25,7 @@ import javax.measure.quantity.Temperature;
 
 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;
@@ -68,8 +65,9 @@ import com.google.gson.Gson;
 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);
 
@@ -172,6 +170,7 @@ public class SensorThermostatThingHandler extends SensorBaseThingHandler {
                     updateState(channelUID, "Closed".equals(open) ? OpenClosedType.CLOSED : OpenClosedType.OPEN);
                 }
             }
+            case CHANNEL_THERMOSTAT_ON -> updateSwitchChannel(channelUID, newState.on);
         }
     }
 
@@ -182,6 +181,20 @@ public class SensorThermostatThingHandler extends SensorBaseThingHandler {
         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;
     }
 
@@ -207,35 +220,4 @@ public class SensorThermostatThingHandler extends SensorBaseThingHandler {
         }
         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);
-    }
 }
index 5f507c1bbde1e8797c07655915bd475e5ac97729..0dd13ca1ffd8ac8d0ab8403bf0fb851a31aad929 100644 (file)
@@ -5,7 +5,7 @@ addon.deconz.description = Allows to use the real-time channel of the deCONZ sof
 
 # 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
@@ -139,7 +139,7 @@ channel-type.deconz.gesture.state.option.7 = Rotate Clockwise
 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
@@ -169,6 +169,7 @@ channel-type.deconz.moisture.label = Moisture
 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
index 60ab111226470c44fe338ce5d549ce77851e61fc..05c9454cbfcdfa0ebc65e16e3ca05842dda67da2 100644 (file)
                <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>
index 5091411cc93278d49a2a1667563e38f4ae0aeb78..8a0a512d4b54cd27dd71584ffc160dd96b505760 100644 (file)
@@ -60,7 +60,7 @@ import com.google.gson.GsonBuilder;
  * @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;
index 1c9d51635c5117b4bb47d9d924e79d7cba5338bc..dee22cb7e3f9669b3e6d35e95eeff60bb2e9e467 100644 (file)
@@ -26,25 +26,19 @@ import org.mockito.Mock;
 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;
@@ -67,7 +61,6 @@ public class SensorsTest {
     public void initialize() {
         GsonBuilder gsonBuilder = new GsonBuilder();
         gsonBuilder.registerTypeAdapter(LightType.class, new LightTypeDeserializer());
-        gsonBuilder.registerTypeAdapter(ThermostatMode.class, new ThermostatModeGsonTypeAdapter());
         gson = gsonBuilder.create();
     }
 
@@ -127,43 +120,6 @@ public class SensorsTest {
         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);
diff --git a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandlerTest.java b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandlerTest.java
new file mode 100644 (file)
index 0000000..9c41c9b
--- /dev/null
@@ -0,0 +1,254 @@
+/**
+ * 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) {
+    }
+}
diff --git a/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/json/thermostat/danfoss.json b/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/json/thermostat/danfoss.json
new file mode 100644 (file)
index 0000000..b03e6b9
--- /dev/null
@@ -0,0 +1,36 @@
+{
+  "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
diff --git a/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/json/thermostat/eurotronic-invalid.json b/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/json/thermostat/eurotronic-invalid.json
new file mode 100644 (file)
index 0000000..0d314fe
--- /dev/null
@@ -0,0 +1,27 @@
+{
+    "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
diff --git a/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/json/thermostat/eurotronic.json b/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/json/thermostat/eurotronic.json
new file mode 100644 (file)
index 0000000..e92f57b
--- /dev/null
@@ -0,0 +1,27 @@
+{
+    "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
diff --git a/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/json/thermostat/namron_ZB_E1.json b/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/json/thermostat/namron_ZB_E1.json
new file mode 100644 (file)
index 0000000..b3e6248
--- /dev/null
@@ -0,0 +1,25 @@
+{
+  "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
diff --git a/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/thermostat-undef.json b/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/thermostat-undef.json
deleted file mode 100644 (file)
index 0d314fe..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-{
-    "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
diff --git a/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/thermostat.json b/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/thermostat.json
deleted file mode 100644 (file)
index e92f57b..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-{
-    "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