From 6444964bf613a366c506ef3f352e9123e39532a7 Mon Sep 17 00:00:00 2001 From: J-N-K Date: Sun, 19 Mar 2023 20:43:15 +0100 Subject: [PATCH] [deconz] Add On/Off thermostats (#14636) * [deconz] Add On/Off thermostats * further work * fix regression Signed-off-by: Jan N. Klug --- bundles/org.openhab.binding.deconz/README.md | 5 +- .../deconz/internal/BindingConstants.java | 1 + .../deconz/internal/dto/SensorState.java | 1 + .../handler/SensorThermostatThingHandler.java | 54 ++-- .../resources/OH-INF/i18n/deconz.properties | 5 +- .../OH-INF/thing/sensor-thing-types.xml | 8 +- .../openhab/binding/deconz/DeconzTest.java | 2 +- .../openhab/binding/deconz/SensorsTest.java | 44 --- .../SensorThermostatThingHandlerTest.java | 254 ++++++++++++++++++ .../deconz/json/thermostat/danfoss.json | 36 +++ .../thermostat/eurotronic-invalid.json} | 0 .../thermostat/eurotronic.json} | 0 .../deconz/json/thermostat/namron_ZB_E1.json | 25 ++ 13 files changed, 348 insertions(+), 87 deletions(-) create mode 100644 bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandlerTest.java create mode 100644 bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/json/thermostat/danfoss.json rename bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/{thermostat-undef.json => json/thermostat/eurotronic-invalid.json} (100%) rename bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/{thermostat.json => json/thermostat/eurotronic.json} (100%) create mode 100644 bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/json/thermostat/namron_ZB_E1.json diff --git a/bundles/org.openhab.binding.deconz/README.md b/bundles/org.openhab.binding.deconz/README.md index be8def99a34..d580134e468 100644 --- a/bundles/org.openhab.binding.deconz/README.md +++ b/bundles/org.openhab.binding.deconz/README.md @@ -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 | diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java index 1d8c6c58aca..c29e7063ab0 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java @@ -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"; diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/SensorState.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/SensorState.java index 2d38f8c0bb1..ab955492762 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/SensorState.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/SensorState.java @@ -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; diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandler.java index 0f04d09e06d..a7780f47057 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandler.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandler.java @@ -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 SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_THERMOSTAT); - private static final List 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 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); - } } diff --git a/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/i18n/deconz.properties b/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/i18n/deconz.properties index 5f507c1bbde..0dd13ca1ffd 100644 --- a/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/i18n/deconz.properties +++ b/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/i18n/deconz.properties @@ -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 diff --git a/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/sensor-thing-types.xml b/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/sensor-thing-types.xml index 60ab1112264..05c9454cbfc 100644 --- a/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/sensor-thing-types.xml +++ b/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/sensor-thing-types.xml @@ -491,7 +491,7 @@ - + @@ -552,7 +552,6 @@ - uid @@ -605,5 +604,10 @@ Current valve position + + Switch + + + diff --git a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/DeconzTest.java b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/DeconzTest.java index 5091411cc93..8a0a512d4b5 100644 --- a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/DeconzTest.java +++ b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/DeconzTest.java @@ -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; diff --git a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/SensorsTest.java b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/SensorsTest.java index 1c9d51635c5..dee22cb7e3f 100644 --- a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/SensorsTest.java +++ b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/SensorsTest.java @@ -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 index 00000000000..9c41c9b4b31 --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandlerTest.java @@ -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 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 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 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 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 expected) throws IOException { + sensorMessage = DeconzTest.getObjectFromJson(fileName, SensorMessage.class, gson); + + thingHandler.initialize(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ThingStatusInfo.class); + verify(callback, times(6)).statusUpdated(eq(thing), captor.capture()); + + List 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 index 00000000000..b03e6b98de6 --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/json/thermostat/danfoss.json @@ -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/thermostat-undef.json b/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/json/thermostat/eurotronic-invalid.json similarity index 100% rename from bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/thermostat-undef.json rename to bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/json/thermostat/eurotronic-invalid.json 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/json/thermostat/eurotronic.json similarity index 100% rename from bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/thermostat.json rename to bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/json/thermostat/eurotronic.json 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 index 00000000000..b3e6248c7b6 --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/json/thermostat/namron_ZB_E1.json @@ -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