]> git.basschouten.com Git - openhab-addons.git/blob
3170973a0b0ec33123f930eab741fa08adf4207a
[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.energidataservice.internal.action;
14
15 import static org.hamcrest.CoreMatchers.*;
16 import static org.hamcrest.MatcherAssert.assertThat;
17 import static org.mockito.Mockito.when;
18
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.math.BigDecimal;
22 import java.nio.charset.StandardCharsets;
23 import java.time.Clock;
24 import java.time.Duration;
25 import java.time.Instant;
26 import java.time.LocalDateTime;
27 import java.util.Arrays;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Objects;
31 import java.util.stream.Collectors;
32
33 import javax.measure.quantity.Power;
34
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.junit.jupiter.api.BeforeEach;
37 import org.junit.jupiter.api.Test;
38 import org.junit.jupiter.api.extension.ExtendWith;
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.energidataservice.internal.EnergiDataServiceBindingConstants;
44 import org.openhab.binding.energidataservice.internal.PriceListParser;
45 import org.openhab.binding.energidataservice.internal.api.dto.DatahubPricelistRecords;
46 import org.openhab.binding.energidataservice.internal.api.serialization.InstantDeserializer;
47 import org.openhab.binding.energidataservice.internal.api.serialization.LocalDateTimeDeserializer;
48 import org.openhab.binding.energidataservice.internal.handler.EnergiDataServiceHandler;
49 import org.openhab.core.library.types.QuantityType;
50 import org.openhab.core.library.unit.Units;
51 import org.slf4j.LoggerFactory;
52
53 import com.google.gson.Gson;
54 import com.google.gson.GsonBuilder;
55
56 import ch.qos.logback.classic.Level;
57 import ch.qos.logback.classic.Logger;
58
59 /**
60  * Tests for {@link EnergiDataServiceActions}.
61  * 
62  * @author Jacob Laursen - Initial contribution
63  */
64 @NonNullByDefault
65 @ExtendWith(MockitoExtension.class)
66 @MockitoSettings(strictness = Strictness.LENIENT)
67 public class EnergiDataServiceActionsTest {
68
69     private @NonNullByDefault({}) @Mock EnergiDataServiceHandler handler;
70     private EnergiDataServiceActions actions = new EnergiDataServiceActions();
71
72     private Gson gson = new GsonBuilder().registerTypeAdapter(Instant.class, new InstantDeserializer())
73             .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeDeserializer()).create();
74
75     private record SpotPrice(Instant hourStart, BigDecimal spotPrice) {
76     }
77
78     private <T> T getObjectFromJson(String filename, Class<T> clazz) throws IOException {
79         try (InputStream inputStream = EnergiDataServiceActionsTest.class.getResourceAsStream(filename)) {
80             if (inputStream == null) {
81                 throw new IOException("Input stream is null");
82             }
83             byte[] bytes = inputStream.readAllBytes();
84             if (bytes == null) {
85                 throw new IOException("Resulting byte-array empty");
86             }
87             String json = new String(bytes, StandardCharsets.UTF_8);
88             return Objects.requireNonNull(gson.fromJson(json, clazz));
89         }
90     }
91
92     @BeforeEach
93     void setUp() {
94         final Logger logger = (Logger) LoggerFactory.getLogger(EnergiDataServiceActions.class);
95         logger.setLevel(Level.OFF);
96
97         actions = new EnergiDataServiceActions();
98     }
99
100     @Test
101     void getPricesSpotPrice() throws IOException {
102         mockCommonDatasets(actions);
103
104         Map<Instant, BigDecimal> actual = actions.getPrices("SpotPrice");
105         assertThat(actual.size(), is(35));
106         assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("0.992840027"))));
107         assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("1.267680054"))));
108     }
109
110     @Test
111     void getPricesNetTariff() throws IOException {
112         mockCommonDatasets(actions);
113
114         Map<Instant, BigDecimal> actual = actions.getPrices("NetTariff");
115         assertThat(actual.size(), is(60));
116         assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("0.432225"))));
117         assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("1.05619"))));
118     }
119
120     @Test
121     void getPricesSystemTariff() throws IOException {
122         mockCommonDatasets(actions);
123
124         Map<Instant, BigDecimal> actual = actions.getPrices("SystemTariff");
125         assertThat(actual.size(), is(60));
126         assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("0.054"))));
127         assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("0.054"))));
128     }
129
130     @Test
131     void getPricesElectricityTax() throws IOException {
132         mockCommonDatasets(actions);
133
134         Map<Instant, BigDecimal> actual = actions.getPrices("ElectricityTax");
135         assertThat(actual.size(), is(60));
136         assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("0.008"))));
137         assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("0.008"))));
138     }
139
140     @Test
141     void getPricesTransmissionNetTariff() throws IOException {
142         mockCommonDatasets(actions);
143
144         Map<Instant, BigDecimal> actual = actions.getPrices("TransmissionNetTariff");
145         assertThat(actual.size(), is(60));
146         assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("0.058"))));
147         assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("0.058"))));
148     }
149
150     @Test
151     void getPricesSpotPriceNetTariff() throws IOException {
152         mockCommonDatasets(actions);
153
154         Map<Instant, BigDecimal> actual = actions.getPrices("SpotPrice,NetTariff");
155         assertThat(actual.size(), is(35));
156         assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("1.425065027"))));
157         assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("2.323870054"))));
158     }
159
160     @Test
161     void getPricesSpotPriceNetTariffElectricityTax() throws IOException {
162         mockCommonDatasets(actions);
163
164         Map<Instant, BigDecimal> actual = actions.getPrices("SpotPrice,NetTariff,ElectricityTax");
165         assertThat(actual.size(), is(35));
166         assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("1.433065027"))));
167         assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("2.331870054"))));
168     }
169
170     @Test
171     void getPricesTotal() throws IOException {
172         mockCommonDatasets(actions);
173
174         Map<Instant, BigDecimal> actual = actions.getPrices();
175         assertThat(actual.size(), is(35));
176         assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("1.545065027"))));
177         assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("2.443870054"))));
178     }
179
180     @Test
181     void getPricesTotalAllElements() throws IOException {
182         mockCommonDatasets(actions);
183
184         Map<Instant, BigDecimal> actual = actions
185                 .getPrices("spotprice,nettariff,systemtariff,electricitytax,transmissionnettariff");
186         assertThat(actual.size(), is(35));
187         assertThat(actual.get(Instant.parse("2023-02-04T12:00:00Z")), is(equalTo(new BigDecimal("1.545065027"))));
188         assertThat(actual.get(Instant.parse("2023-02-04T15:00:00Z")), is(equalTo(new BigDecimal("1.708765039"))));
189         assertThat(actual.get(Instant.parse("2023-02-04T16:00:00Z")), is(equalTo(new BigDecimal("2.443870054"))));
190     }
191
192     @Test
193     void getPricesInvalidPriceElement() throws IOException {
194         mockCommonDatasets(actions);
195
196         Map<Instant, BigDecimal> actual = actions.getPrices("spotprice,nettarif");
197         assertThat(actual.size(), is(0));
198     }
199
200     @Test
201     void getPricesMixedCurrencies() throws IOException {
202         mockCommonDatasets(actions);
203         when(handler.getCurrency()).thenReturn(EnergiDataServiceBindingConstants.CURRENCY_EUR);
204
205         Map<Instant, BigDecimal> actual = actions.getPrices("spotprice,nettariff");
206         assertThat(actual.size(), is(0));
207     }
208
209     /**
210      * Calculate price in period 15:30-16:30 (UTC) with consumption 150 W and the following total prices:
211      * 15:00:00: 1.708765039
212      * 16:00:00: 2.443870054
213      *
214      * Result = (1.708765039 / 2) + (2.443870054 / 2) * 0.150
215      *
216      * @throws IOException
217      */
218     @Test
219     void calculatePriceSimple() throws IOException {
220         mockCommonDatasets(actions);
221
222         BigDecimal actual = actions.calculatePrice(Instant.parse("2023-02-04T15:30:00Z"),
223                 Instant.parse("2023-02-04T16:30:00Z"), new QuantityType<>(150, Units.WATT));
224         assertThat(actual, is(equalTo(new BigDecimal("0.311447631975000000")))); // 0.3114476319750
225     }
226
227     /**
228      * Calculate price in period 15:00-17:00 (UTC) with consumption 1000 W and the following total prices:
229      * 15:00:00: 1.708765039
230      * 16:00:00: 2.443870054
231      *
232      * Result = 1.708765039 + 2.443870054
233      *
234      * @throws IOException
235      */
236     @Test
237     void calculatePriceFullHours() throws IOException {
238         mockCommonDatasets(actions);
239
240         BigDecimal actual = actions.calculatePrice(Instant.parse("2023-02-04T15:00:00Z"),
241                 Instant.parse("2023-02-04T17:00:00Z"), new QuantityType<>(1, Units.KILOVAR));
242         assertThat(actual, is(equalTo(new BigDecimal("4.152635093000000000")))); // 4.152635093
243     }
244
245     @Test
246     void calculatePriceOutOfRangeStart() throws IOException {
247         mockCommonDatasets(actions);
248
249         BigDecimal actual = actions.calculatePrice(Instant.parse("2023-02-03T23:59:00Z"),
250                 Instant.parse("2023-02-04T12:30:00Z"), new QuantityType<>(1000, Units.WATT));
251         assertThat(actual, is(equalTo(BigDecimal.ZERO)));
252     }
253
254     @Test
255     void calculatePriceOutOfRangeEnd() throws IOException {
256         mockCommonDatasets(actions);
257
258         BigDecimal actual = actions.calculatePrice(Instant.parse("2023-02-05T22:00:00Z"),
259                 Instant.parse("2023-02-05T23:01:00Z"), new QuantityType<>(1000, Units.WATT));
260         assertThat(actual, is(equalTo(BigDecimal.ZERO)));
261     }
262
263     /**
264      * Miele G 6895 SCVi XXL K2O dishwasher, program ECO.
265      *
266      * @throws IOException
267      */
268     @Test
269     void calculateCheapestPeriodWithPowerDishwasher() throws IOException {
270         mockCommonDatasets(actions, "SpotPrices20230205.json");
271
272         List<Duration> durations = List.of(Duration.ofMinutes(37), Duration.ofMinutes(8), Duration.ofMinutes(4),
273                 Duration.ofMinutes(2), Duration.ofMinutes(4), Duration.ofMinutes(36), Duration.ofMinutes(41),
274                 Duration.ofMinutes(104));
275         List<QuantityType<Power>> consumptions = List.of(QuantityType.valueOf(162.162162, Units.WATT),
276                 QuantityType.valueOf(750, Units.WATT), QuantityType.valueOf(1500, Units.WATT),
277                 QuantityType.valueOf(3000, Units.WATT), QuantityType.valueOf(1500, Units.WATT),
278                 QuantityType.valueOf(166.666666, Units.WATT), QuantityType.valueOf(146.341463, Units.WATT),
279                 QuantityType.valueOf(0, Units.WATT));
280         Map<String, Object> actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"),
281                 Instant.parse("2023-02-06T06:00:00Z"), durations, consumptions);
282         assertThat(actual.get("LowestPrice"), is(equalTo(new BigDecimal("1.024218147103792520"))));
283         assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T19:23:00Z"))));
284         assertThat(actual.get("HighestPrice"), is(equalTo(new BigDecimal("1.530671034828983196"))));
285         assertThat(actual.get("MostExpensiveStart"), is(equalTo(Instant.parse("2023-02-05T16:00:00Z"))));
286     }
287
288     @Test
289     void calculateCheapestPeriodWithPowerOutOfRange() throws IOException {
290         mockCommonDatasets(actions);
291
292         List<Duration> durations = List.of(Duration.ofMinutes(61));
293         List<QuantityType<Power>> consumptions = List.of(QuantityType.valueOf(1000, Units.WATT));
294         Map<String, Object> actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-04T12:00:00Z"),
295                 Instant.parse("2023-02-06T00:01:00Z"), durations, consumptions);
296         assertThat(actual.size(), is(equalTo(0)));
297     }
298
299     /**
300      * Miele G 6895 SCVi XXL K2O dishwasher, program ECO.
301      *
302      * @throws IOException
303      */
304     @Test
305     void calculateCheapestPeriodWithEnergyDishwasher() throws IOException {
306         mockCommonDatasets(actions, "SpotPrices20230205.json");
307
308         List<Duration> durations = List.of(Duration.ofMinutes(37), Duration.ofMinutes(8), Duration.ofMinutes(4),
309                 Duration.ofMinutes(2), Duration.ofMinutes(4), Duration.ofMinutes(36), Duration.ofMinutes(41));
310         Map<String, Object> actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"),
311                 Instant.parse("2023-02-06T06:00:00Z"), Duration.ofMinutes(236), durations,
312                 QuantityType.valueOf(0.1, Units.KILOWATT_HOUR));
313         assertThat(actual.get("LowestPrice"), is(equalTo(new BigDecimal("1.024218147103792520"))));
314         assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T19:23:00Z"))));
315         assertThat(actual.get("HighestPrice"), is(equalTo(new BigDecimal("1.530671034828983196"))));
316         assertThat(actual.get("MostExpensiveStart"), is(equalTo(Instant.parse("2023-02-05T16:00:00Z"))));
317     }
318
319     @Test
320     void calculateCheapestPeriodWithEnergyTotalDurationIsExactSum() throws IOException {
321         mockCommonDatasets(actions, "SpotPrices20230205.json");
322
323         List<Duration> durations = List.of(Duration.ofMinutes(60), Duration.ofMinutes(60));
324         Map<String, Object> actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"),
325                 Instant.parse("2023-02-06T06:00:00Z"), Duration.ofMinutes(120), durations,
326                 QuantityType.valueOf(100, Units.WATT_HOUR));
327         assertThat(actual.get("LowestPrice"), is(equalTo(new BigDecimal("0.293540001200000000"))));
328         assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T19:00:00Z"))));
329     }
330
331     @Test
332     void calculateCheapestPeriodWithEnergyTotalDurationInvalid() throws IOException {
333         mockCommonDatasets(actions, "SpotPrices20230205.json");
334
335         List<Duration> durations = List.of(Duration.ofMinutes(60), Duration.ofMinutes(60));
336         Map<String, Object> actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"),
337                 Instant.parse("2023-02-06T06:00:00Z"), Duration.ofMinutes(119), durations,
338                 QuantityType.valueOf(0.1, Units.KILOWATT_HOUR));
339         assertThat(actual.size(), is(equalTo(0)));
340     }
341
342     /**
343      * Like {@link #calculateCheapestPeriodWithEnergyDishwasher} but with unknown consumption/timetable map.
344      *
345      * @throws IOException
346      */
347     @Test
348     void calculateCheapestPeriodAssumingLinearUnknownConsumption() throws IOException {
349         mockCommonDatasets(actions, "SpotPrices20230205.json");
350
351         Map<String, Object> actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-05T16:00:00Z"),
352                 Instant.parse("2023-02-06T06:00:00Z"), Duration.ofMinutes(236));
353         assertThat(actual.get("LowestPrice"), is(nullValue()));
354         assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T19:00:00Z"))));
355         assertThat(actual.get("HighestPrice"), is(nullValue()));
356         assertThat(actual.get("MostExpensiveStart"), is(equalTo(Instant.parse("2023-02-05T16:00:00Z"))));
357     }
358
359     @Test
360     void calculateCheapestPeriodForLinearPowerUsage() throws IOException {
361         mockCommonDatasets(actions);
362
363         Map<String, Object> actual = actions.calculateCheapestPeriod(Instant.parse("2023-02-04T12:00:00Z"),
364                 Instant.parse("2023-02-05T23:00:00Z"), Duration.ofMinutes(61), QuantityType.valueOf(1000, Units.WATT));
365         assertThat(actual.get("LowestPrice"), is(equalTo(new BigDecimal("1.323990859575000000"))));
366         assertThat(actual.get("CheapestStart"), is(equalTo(Instant.parse("2023-02-05T12:00:00Z"))));
367         assertThat(actual.get("HighestPrice"), is(equalTo(new BigDecimal("2.589061780353348000"))));
368         assertThat(actual.get("MostExpensiveStart"), is(equalTo(Instant.parse("2023-02-04T17:00:00Z"))));
369     }
370
371     private void mockCommonDatasets(EnergiDataServiceActions actions) throws IOException {
372         mockCommonDatasets(actions, "SpotPrices20230204.json");
373     }
374
375     private void mockCommonDatasets(EnergiDataServiceActions actions, String spotPricesFilename) throws IOException {
376         SpotPrice[] spotPriceRecords = getObjectFromJson(spotPricesFilename, SpotPrice[].class);
377         Map<Instant, BigDecimal> spotPrices = Arrays.stream(spotPriceRecords)
378                 .collect(Collectors.toMap(SpotPrice::hourStart, SpotPrice::spotPrice));
379
380         PriceListParser priceListParser = new PriceListParser(
381                 Clock.fixed(spotPriceRecords[0].hourStart, EnergiDataServiceBindingConstants.DATAHUB_TIMEZONE));
382         DatahubPricelistRecords datahubRecords = getObjectFromJson("NetTariffs.json", DatahubPricelistRecords.class);
383         Map<Instant, BigDecimal> netTariffs = priceListParser
384                 .toHourly(Arrays.stream(datahubRecords.records()).toList());
385         datahubRecords = getObjectFromJson("SystemTariffs.json", DatahubPricelistRecords.class);
386         Map<Instant, BigDecimal> systemTariffs = priceListParser
387                 .toHourly(Arrays.stream(datahubRecords.records()).toList());
388         datahubRecords = getObjectFromJson("ElectricityTaxes.json", DatahubPricelistRecords.class);
389         Map<Instant, BigDecimal> electricityTaxes = priceListParser
390                 .toHourly(Arrays.stream(datahubRecords.records()).toList());
391         datahubRecords = getObjectFromJson("TransmissionNetTariffs.json", DatahubPricelistRecords.class);
392         Map<Instant, BigDecimal> transmissionNetTariffs = priceListParser
393                 .toHourly(Arrays.stream(datahubRecords.records()).toList());
394
395         when(handler.getSpotPrices()).thenReturn(spotPrices);
396         when(handler.getNetTariffs()).thenReturn(netTariffs);
397         when(handler.getSystemTariffs()).thenReturn(systemTariffs);
398         when(handler.getElectricityTaxes()).thenReturn(electricityTaxes);
399         when(handler.getTransmissionNetTariffs()).thenReturn(transmissionNetTariffs);
400         when(handler.getCurrency()).thenReturn(EnergiDataServiceBindingConstants.CURRENCY_DKK);
401         actions.setThingHandler(handler);
402     }
403 }