mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[deconz] Add On/Off thermostats (#14636)
* [deconz] Add On/Off thermostats * further work * fix regression Signed-off-by: Jan N. Klug <github@klug.nrw>
This commit is contained in:
parent
aee57afb0e
commit
6444964bf6
@ -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 |
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -491,7 +491,7 @@
|
||||
<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"/>
|
||||
@ -552,7 +552,6 @@
|
||||
<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>
|
||||
@ -605,5 +604,10 @@
|
||||
<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>
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
Loading…
Reference in New Issue
Block a user