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