]> git.basschouten.com Git - openhab-addons.git/blob
9c41c9b4b31b0dca6f2fe34c691613c8e1688be4
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.deconz.internal.handler;
14
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.*;
25
26 import java.io.IOException;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Optional;
30 import java.util.Set;
31 import java.util.concurrent.CompletableFuture;
32
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;
74
75 import com.google.gson.Gson;
76 import com.google.gson.GsonBuilder;
77
78 /**
79  * The {@link SensorThermostatThingHandlerTest} contains test classes for the {@link SensorThermostatThingHandler}
80  *
81  * @author Jan N. Klug - Initial contribution
82  */
83 @ExtendWith(MockitoExtension.class)
84 @MockitoSettings(strictness = Strictness.LENIENT)
85 @NonNullByDefault
86 public class SensorThermostatThingHandlerTest extends JavaTest {
87
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");
90
91     private @Mock @NonNullByDefault({}) Bridge bridge;
92     private @Mock @NonNullByDefault({}) ThingHandlerCallback callback;
93
94     private @Mock @NonNullByDefault({}) DeconzBridgeHandler bridgeHandler;
95     private @Mock @NonNullByDefault({}) WebSocketConnection webSocketConnection;
96     private @Mock @NonNullByDefault({}) BridgeFullState bridgeFullState;
97
98     private @NonNullByDefault({}) Gson gson;
99     private @NonNullByDefault({}) Thing thing;
100     private @NonNullByDefault({}) SensorThermostatThingHandler thingHandler;
101     private @NonNullByDefault({}) SensorMessage sensorMessage;
102
103     @BeforeEach
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();
109
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);
117         }
118         thingBuilder.withConfiguration(new Configuration(Map.of(CONFIG_ID, "1")));
119         thing = thingBuilder.build();
120
121         thingHandler = new SensorThermostatThingHandler(thing, gson);
122         thingHandler.setCallback(callback);
123
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)));
127         doAnswer(i -> {
128             thing = i.getArgument(0);
129             thingHandler.thingUpdated(thing);
130             return null;
131         }).when(callback).thingUpdated(any(Thing.class));
132
133         when(bridge.getStatusInfo()).thenReturn(new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, ""));
134         when(bridge.getHandler()).thenReturn(bridgeHandler);
135
136         when(bridgeHandler.getWebSocketConnection()).thenReturn(webSocketConnection);
137         when(bridgeHandler.getBridgeFullState())
138                 .thenReturn(CompletableFuture.completedFuture(Optional.of(bridgeFullState)));
139
140         when(bridgeFullState.getMessage(ResourceType.SENSORS, "1")).thenAnswer(i -> sensorMessage);
141     }
142
143     @Test
144     public void testDanfoss() throws IOException {
145         Set<TestParam> expected = Set.of(
146                 // standard channels
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")),
152                 // battery
153                 new TestParam(CHANNEL_BATTERY_LEVEL, new DecimalType(41)),
154                 new TestParam(CHANNEL_BATTERY_LOW, OnOffType.OFF),
155                 // last seen
156                 new TestParam(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime("2023-03-18T05:58Z")),
157                 // dynamic channels
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));
163
164         assertThermostat("json/thermostat/danfoss.json", expected);
165     }
166
167     @Test
168     public void testNamron() throws IOException {
169         Set<TestParam> expected = Set.of(
170                 // standard channels
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")),
176                 // last seen
177                 new TestParam(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime("2023-03-18T18:10Z")),
178                 // dynamic channels
179                 new TestParam(CHANNEL_THERMOSTAT_LOCKED, OnOffType.OFF),
180                 new TestParam(CHANNEL_THERMOSTAT_ON, OnOffType.OFF));
181
182         assertThermostat("json/thermostat/namron_ZB_E1.json", expected);
183     }
184
185     @Test
186     public void testEurotronicValid() throws IOException {
187         Set<TestParam> expected = Set.of(
188                 // standard channels
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")),
194                 // battery
195                 new TestParam(CHANNEL_BATTERY_LEVEL, new DecimalType(85)),
196                 new TestParam(CHANNEL_BATTERY_LOW, OnOffType.OFF),
197                 // last seen
198                 new TestParam(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime("2020-05-31T20:24:55.819")),
199                 // dynamic channels
200                 new TestParam(CHANNEL_VALVE_POSITION, new QuantityType<>("99 %")),
201                 new TestParam(CHANNEL_THERMOSTAT_ON, OnOffType.ON));
202
203         assertThermostat("json/thermostat/eurotronic.json", expected);
204     }
205
206     @Test
207     public void testEurotronicInvalid() throws IOException {
208         Set<TestParam> expected = Set.of(
209                 // standard channels
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")),
215                 // battery
216                 new TestParam(CHANNEL_BATTERY_LEVEL, new DecimalType(85)),
217                 new TestParam(CHANNEL_BATTERY_LOW, OnOffType.OFF),
218                 // last seen
219                 new TestParam(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime("2020-05-31T20:24:55.819")),
220                 // dynamic channels
221                 new TestParam(CHANNEL_VALVE_POSITION, UnDefType.UNDEF),
222                 new TestParam(CHANNEL_THERMOSTAT_ON, OnOffType.ON));
223
224         assertThermostat("json/thermostat/eurotronic-invalid.json", expected);
225     }
226
227     private void assertThermostat(String fileName, Set<TestParam> expected) throws IOException {
228         sensorMessage = DeconzTest.getObjectFromJson(fileName, SensorMessage.class, gson);
229
230         thingHandler.initialize();
231
232         ArgumentCaptor<ThingStatusInfo> captor = ArgumentCaptor.forClass(ThingStatusInfo.class);
233         verify(callback, times(6)).statusUpdated(eq(thing), captor.capture());
234
235         List<ThingStatusInfo> statusInfoList = captor.getAllValues();
236         assertThat(statusInfoList.get(0).getStatus(), is(ThingStatus.UNKNOWN));
237         assertThat(statusInfoList.get(5).getStatus(), is(ThingStatus.ONLINE));
238
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()));
243
244             State state = testParam.state;
245             if (state != null) {
246                 verify(callback, times(3).description(channel + " did not receive an update"))
247                         .stateUpdated(eq(channel.getUID()), eq(state));
248             }
249         }
250     }
251
252     private record TestParam(String channelId, @Nullable State state) {
253     }
254 }