2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.deconz.internal.handler;
15 import static org.hamcrest.MatcherAssert.assertThat;
16 import static org.hamcrest.Matchers.is;
17 import static org.hamcrest.Matchers.notNullValue;
18 import static org.mockito.ArgumentMatchers.any;
19 import static org.mockito.ArgumentMatchers.eq;
20 import static org.mockito.Mockito.doAnswer;
21 import static org.mockito.Mockito.times;
22 import static org.mockito.Mockito.verify;
23 import static org.mockito.Mockito.when;
24 import static org.openhab.binding.deconz.internal.BindingConstants.*;
26 import java.io.IOException;
27 import java.util.List;
29 import java.util.Optional;
31 import java.util.concurrent.CompletableFuture;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.junit.jupiter.api.BeforeEach;
36 import org.junit.jupiter.api.Test;
37 import org.junit.jupiter.api.extension.ExtendWith;
38 import org.mockito.ArgumentCaptor;
39 import org.mockito.Mock;
40 import org.mockito.junit.jupiter.MockitoExtension;
41 import org.mockito.junit.jupiter.MockitoSettings;
42 import org.mockito.quality.Strictness;
43 import org.openhab.binding.deconz.DeconzTest;
44 import org.openhab.binding.deconz.internal.Util;
45 import org.openhab.binding.deconz.internal.dto.BridgeFullState;
46 import org.openhab.binding.deconz.internal.dto.SensorMessage;
47 import org.openhab.binding.deconz.internal.netutils.WebSocketConnection;
48 import org.openhab.binding.deconz.internal.types.LightType;
49 import org.openhab.binding.deconz.internal.types.LightTypeDeserializer;
50 import org.openhab.binding.deconz.internal.types.ResourceType;
51 import org.openhab.binding.deconz.internal.types.ThermostatMode;
52 import org.openhab.binding.deconz.internal.types.ThermostatModeGsonTypeAdapter;
53 import org.openhab.core.config.core.Configuration;
54 import org.openhab.core.library.types.DecimalType;
55 import org.openhab.core.library.types.OnOffType;
56 import org.openhab.core.library.types.OpenClosedType;
57 import org.openhab.core.library.types.QuantityType;
58 import org.openhab.core.library.types.StringType;
59 import org.openhab.core.test.java.JavaTest;
60 import org.openhab.core.thing.Bridge;
61 import org.openhab.core.thing.Channel;
62 import org.openhab.core.thing.ChannelUID;
63 import org.openhab.core.thing.Thing;
64 import org.openhab.core.thing.ThingStatus;
65 import org.openhab.core.thing.ThingStatusDetail;
66 import org.openhab.core.thing.ThingStatusInfo;
67 import org.openhab.core.thing.ThingUID;
68 import org.openhab.core.thing.binding.ThingHandlerCallback;
69 import org.openhab.core.thing.binding.builder.ChannelBuilder;
70 import org.openhab.core.thing.binding.builder.ThingBuilder;
71 import org.openhab.core.thing.type.ChannelTypeUID;
72 import org.openhab.core.types.State;
73 import org.openhab.core.types.UnDefType;
75 import com.google.gson.Gson;
76 import com.google.gson.GsonBuilder;
79 * The {@link SensorThermostatThingHandlerTest} contains test classes for the {@link SensorThermostatThingHandler}
81 * @author Jan N. Klug - Initial contribution
83 @ExtendWith(MockitoExtension.class)
84 @MockitoSettings(strictness = Strictness.LENIENT)
86 public class SensorThermostatThingHandlerTest extends JavaTest {
88 private static final ThingUID BRIDGE_UID = new ThingUID(BRIDGE_TYPE, "bridge");
89 private static final ThingUID THING_UID = new ThingUID(THING_TYPE_THERMOSTAT, "thing");
91 private @Mock @NonNullByDefault({}) Bridge bridge;
92 private @Mock @NonNullByDefault({}) ThingHandlerCallback callback;
94 private @Mock @NonNullByDefault({}) DeconzBridgeHandler bridgeHandler;
95 private @Mock @NonNullByDefault({}) WebSocketConnection webSocketConnection;
96 private @Mock @NonNullByDefault({}) BridgeFullState bridgeFullState;
98 private @NonNullByDefault({}) Gson gson;
99 private @NonNullByDefault({}) Thing thing;
100 private @NonNullByDefault({}) SensorThermostatThingHandler thingHandler;
101 private @NonNullByDefault({}) SensorMessage sensorMessage;
104 public void setup() {
105 GsonBuilder gsonBuilder = new GsonBuilder();
106 gsonBuilder.registerTypeAdapter(LightType.class, new LightTypeDeserializer());
107 gsonBuilder.registerTypeAdapter(ThermostatMode.class, new ThermostatModeGsonTypeAdapter());
108 gson = gsonBuilder.create();
110 ThingBuilder thingBuilder = ThingBuilder.create(THING_TYPE_THERMOSTAT, THING_UID);
111 thingBuilder.withBridge(BRIDGE_UID);
112 for (String channelId : List.of(CHANNEL_TEMPERATURE, CHANNEL_HEATSETPOINT, CHANNEL_THERMOSTAT_MODE,
113 CHANNEL_TEMPERATURE_OFFSET, CHANNEL_LAST_UPDATED)) {
114 Channel channel = ChannelBuilder.create(new ChannelUID(THING_UID, channelId))
115 .withType(new ChannelTypeUID(BINDING_ID, channelId)).build();
116 thingBuilder.withChannel(channel);
118 thingBuilder.withConfiguration(new Configuration(Map.of(CONFIG_ID, "1")));
119 thing = thingBuilder.build();
121 thingHandler = new SensorThermostatThingHandler(thing, gson);
122 thingHandler.setCallback(callback);
124 when(callback.getBridge(BRIDGE_UID)).thenReturn(bridge);
125 when(callback.createChannelBuilder(any(ChannelUID.class), any(ChannelTypeUID.class)))
126 .thenAnswer(i -> ChannelBuilder.create((ChannelUID) i.getArgument(0)).withType(i.getArgument(1)));
128 thing = i.getArgument(0);
129 thingHandler.thingUpdated(thing);
131 }).when(callback).thingUpdated(any(Thing.class));
133 when(bridge.getStatusInfo()).thenReturn(new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, ""));
134 when(bridge.getHandler()).thenReturn(bridgeHandler);
136 when(bridgeHandler.getWebSocketConnection()).thenReturn(webSocketConnection);
137 when(bridgeHandler.getBridgeFullState())
138 .thenReturn(CompletableFuture.completedFuture(Optional.of(bridgeFullState)));
140 when(bridgeFullState.getMessage(ResourceType.SENSORS, "1")).thenAnswer(i -> sensorMessage);
144 public void testDanfoss() throws IOException {
145 Set<TestParam> expected = Set.of(
147 new TestParam(CHANNEL_TEMPERATURE, new QuantityType<>("21.45 °C")),
148 new TestParam(CHANNEL_HEATSETPOINT, new QuantityType<>("21.00 °C")),
149 new TestParam(CHANNEL_THERMOSTAT_MODE, new StringType("HEAT")),
150 new TestParam(CHANNEL_TEMPERATURE_OFFSET, new QuantityType<>("0.0 °C")),
151 new TestParam(CHANNEL_LAST_UPDATED, Util.convertTimestampToDateTime("2023-03-18T05:52:29.506")),
153 new TestParam(CHANNEL_BATTERY_LEVEL, new DecimalType(41)),
154 new TestParam(CHANNEL_BATTERY_LOW, OnOffType.OFF),
156 new TestParam(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime("2023-03-18T05:58Z")),
158 new TestParam(CHANNEL_EXTERNAL_WINDOW_OPEN, OpenClosedType.CLOSED),
159 new TestParam(CHANNEL_VALVE_POSITION, new QuantityType<>("1 %")),
160 new TestParam(CHANNEL_THERMOSTAT_LOCKED, OnOffType.OFF),
161 new TestParam(CHANNEL_THERMOSTAT_ON, OnOffType.OFF),
162 new TestParam(CHANNEL_WINDOW_OPEN, OpenClosedType.CLOSED));
164 assertThermostat("json/thermostat/danfoss.json", expected);
168 public void testNamron() throws IOException {
169 Set<TestParam> expected = Set.of(
171 new TestParam(CHANNEL_TEMPERATURE, new QuantityType<>("20.39 °C")),
172 new TestParam(CHANNEL_HEATSETPOINT, new QuantityType<>("22.00 °C")),
173 new TestParam(CHANNEL_THERMOSTAT_MODE, new StringType("OFF")),
174 new TestParam(CHANNEL_TEMPERATURE_OFFSET, new QuantityType<>("0.0 °C")),
175 new TestParam(CHANNEL_LAST_UPDATED, Util.convertTimestampToDateTime("2023-03-18T18:10:39.296")),
177 new TestParam(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime("2023-03-18T18:10Z")),
179 new TestParam(CHANNEL_THERMOSTAT_LOCKED, OnOffType.OFF),
180 new TestParam(CHANNEL_THERMOSTAT_ON, OnOffType.OFF));
182 assertThermostat("json/thermostat/namron_ZB_E1.json", expected);
186 public void testEurotronicValid() throws IOException {
187 Set<TestParam> expected = Set.of(
189 new TestParam(CHANNEL_TEMPERATURE, new QuantityType<>("16.50 °C")),
190 new TestParam(CHANNEL_HEATSETPOINT, new QuantityType<>("25.00 °C")),
191 new TestParam(CHANNEL_THERMOSTAT_MODE, new StringType("AUTO")),
192 new TestParam(CHANNEL_TEMPERATURE_OFFSET, new QuantityType<>("0.0 °C")),
193 new TestParam(CHANNEL_LAST_UPDATED, Util.convertTimestampToDateTime("2020-05-31T20:24:55.819")),
195 new TestParam(CHANNEL_BATTERY_LEVEL, new DecimalType(85)),
196 new TestParam(CHANNEL_BATTERY_LOW, OnOffType.OFF),
198 new TestParam(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime("2020-05-31T20:24:55.819")),
200 new TestParam(CHANNEL_VALVE_POSITION, new QuantityType<>("99 %")),
201 new TestParam(CHANNEL_THERMOSTAT_ON, OnOffType.ON));
203 assertThermostat("json/thermostat/eurotronic.json", expected);
207 public void testEurotronicInvalid() throws IOException {
208 Set<TestParam> expected = Set.of(
210 new TestParam(CHANNEL_TEMPERATURE, new QuantityType<>("16.50 °C")),
211 new TestParam(CHANNEL_HEATSETPOINT, new QuantityType<>("25.00 °C")),
212 new TestParam(CHANNEL_THERMOSTAT_MODE, new StringType("AUTO")),
213 new TestParam(CHANNEL_TEMPERATURE_OFFSET, new QuantityType<>("0.0 °C")),
214 new TestParam(CHANNEL_LAST_UPDATED, Util.convertTimestampToDateTime("2020-05-31T20:24:55.819")),
216 new TestParam(CHANNEL_BATTERY_LEVEL, new DecimalType(85)),
217 new TestParam(CHANNEL_BATTERY_LOW, OnOffType.OFF),
219 new TestParam(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime("2020-05-31T20:24:55.819")),
221 new TestParam(CHANNEL_VALVE_POSITION, UnDefType.UNDEF),
222 new TestParam(CHANNEL_THERMOSTAT_ON, OnOffType.ON));
224 assertThermostat("json/thermostat/eurotronic-invalid.json", expected);
227 private void assertThermostat(String fileName, Set<TestParam> expected) throws IOException {
228 sensorMessage = DeconzTest.getObjectFromJson(fileName, SensorMessage.class, gson);
230 thingHandler.initialize();
232 ArgumentCaptor<ThingStatusInfo> captor = ArgumentCaptor.forClass(ThingStatusInfo.class);
233 verify(callback, times(6)).statusUpdated(eq(thing), captor.capture());
235 List<ThingStatusInfo> statusInfoList = captor.getAllValues();
236 assertThat(statusInfoList.get(0).getStatus(), is(ThingStatus.UNKNOWN));
237 assertThat(statusInfoList.get(5).getStatus(), is(ThingStatus.ONLINE));
239 assertThat(thing.getChannels().size(), is(expected.size()));
240 for (TestParam testParam : expected) {
241 Channel channel = thing.getChannel(testParam.channelId());
242 assertThat(channel + "expected but missing", channel, is(notNullValue()));
244 State state = testParam.state;
246 verify(callback, times(3).description(channel + " did not receive an update"))
247 .stateUpdated(eq(channel.getUID()), eq(state));
252 private record TestParam(String channelId, @Nullable State state) {