2 * Copyright (c) 2010-2024 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.solarforecast;
15 import static org.junit.jupiter.api.Assertions.*;
17 import java.time.Clock;
18 import java.time.Instant;
19 import java.time.LocalDateTime;
20 import java.time.LocalTime;
21 import java.time.ZoneId;
22 import java.time.ZonedDateTime;
23 import java.time.temporal.ChronoUnit;
24 import java.util.ArrayList;
25 import java.util.Iterator;
26 import java.util.List;
28 import javax.measure.quantity.Energy;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.json.JSONObject;
32 import org.junit.jupiter.api.BeforeAll;
33 import org.junit.jupiter.api.Test;
34 import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants;
35 import org.openhab.binding.solarforecast.internal.SolarForecastException;
36 import org.openhab.binding.solarforecast.internal.actions.SolarForecast;
37 import org.openhab.binding.solarforecast.internal.solcast.SolcastConstants;
38 import org.openhab.binding.solarforecast.internal.solcast.SolcastObject;
39 import org.openhab.binding.solarforecast.internal.solcast.SolcastObject.QueryMode;
40 import org.openhab.binding.solarforecast.internal.solcast.handler.SolcastBridgeHandler;
41 import org.openhab.binding.solarforecast.internal.solcast.handler.SolcastPlaneHandler;
42 import org.openhab.binding.solarforecast.internal.solcast.handler.SolcastPlaneMock;
43 import org.openhab.binding.solarforecast.internal.utils.Utils;
44 import org.openhab.core.library.types.QuantityType;
45 import org.openhab.core.library.unit.Units;
46 import org.openhab.core.thing.internal.BridgeImpl;
47 import org.openhab.core.types.State;
48 import org.openhab.core.types.TimeSeries;
51 * The {@link SolcastTest} tests responses from forecast solar website
53 * @author Bernd Weymann - Initial contribution
57 public static final ZoneId TEST_ZONE = ZoneId.of("Europe/Berlin");
58 private static final TimeZP TIMEZONEPROVIDER = new TimeZP();
59 // double comparison tolerance = 1 Watt
60 private static final double TOLERANCE = 0.001;
62 public static final String TOO_LATE_INDICATOR = "too late";
63 public static final String DAY_MISSING_INDICATOR = "not available in forecast";
66 static void setFixedTimeJul17() {
67 // Instant matching the date of test resources
68 Instant fixedInstant = Instant.parse("2022-07-17T21:00:00Z");
69 Clock fixedClock = Clock.fixed(fixedInstant, TEST_ZONE);
70 Utils.setClock(fixedClock);
73 static void setFixedTimeJul18() {
74 // Instant matching the date of test resources
75 Instant fixedInstant = Instant.parse("2022-07-18T14:23:00Z");
76 Clock fixedClock = Clock.fixed(fixedInstant, TEST_ZONE);
77 Utils.setClock(fixedClock);
81 * "2022-07-18T00:00+02:00[Europe/Berlin]": 0,
82 * "2022-07-18T00:30+02:00[Europe/Berlin]": 0,
83 * "2022-07-18T01:00+02:00[Europe/Berlin]": 0,
84 * "2022-07-18T01:30+02:00[Europe/Berlin]": 0,
85 * "2022-07-18T02:00+02:00[Europe/Berlin]": 0,
86 * "2022-07-18T02:30+02:00[Europe/Berlin]": 0,
87 * "2022-07-18T03:00+02:00[Europe/Berlin]": 0,
88 * "2022-07-18T03:30+02:00[Europe/Berlin]": 0,
89 * "2022-07-18T04:00+02:00[Europe/Berlin]": 0,
90 * "2022-07-18T04:30+02:00[Europe/Berlin]": 0,
91 * "2022-07-18T05:00+02:00[Europe/Berlin]": 0,
92 * "2022-07-18T05:30+02:00[Europe/Berlin]": 0,
93 * "2022-07-18T06:00+02:00[Europe/Berlin]": 0.0205,
94 * "2022-07-18T06:30+02:00[Europe/Berlin]": 0.1416,
95 * "2022-07-18T07:00+02:00[Europe/Berlin]": 0.4478,
96 * "2022-07-18T07:30+02:00[Europe/Berlin]": 0.763,
97 * "2022-07-18T08:00+02:00[Europe/Berlin]": 1.1367,
98 * "2022-07-18T08:30+02:00[Europe/Berlin]": 1.4044,
99 * "2022-07-18T09:00+02:00[Europe/Berlin]": 1.6632,
100 * "2022-07-18T09:30+02:00[Europe/Berlin]": 1.8667,
101 * "2022-07-18T10:00+02:00[Europe/Berlin]": 2.0729,
102 * "2022-07-18T10:30+02:00[Europe/Berlin]": 2.2377,
103 * "2022-07-18T11:00+02:00[Europe/Berlin]": 2.3516,
104 * "2022-07-18T11:30+02:00[Europe/Berlin]": 2.4295,
105 * "2022-07-18T12:00+02:00[Europe/Berlin]": 2.5136,
106 * "2022-07-18T12:30+02:00[Europe/Berlin]": 2.5295,
107 * "2022-07-18T13:00+02:00[Europe/Berlin]": 2.526,
108 * "2022-07-18T13:30+02:00[Europe/Berlin]": 2.4879,
109 * "2022-07-18T14:00+02:00[Europe/Berlin]": 2.4092,
110 * "2022-07-18T14:30+02:00[Europe/Berlin]": 2.3309,
111 * "2022-07-18T15:00+02:00[Europe/Berlin]": 2.1984,
112 * "2022-07-18T15:30+02:00[Europe/Berlin]": 2.0416,
113 * "2022-07-18T16:00+02:00[Europe/Berlin]": 1.9076,
114 * "2022-07-18T16:30+02:00[Europe/Berlin]": 1.7416,
115 * "2022-07-18T17:00+02:00[Europe/Berlin]": 1.5414,
116 * "2022-07-18T17:30+02:00[Europe/Berlin]": 1.3683,
117 * "2022-07-18T18:00+02:00[Europe/Berlin]": 1.1603,
118 * "2022-07-18T18:30+02:00[Europe/Berlin]": 0.9527,
119 * "2022-07-18T19:00+02:00[Europe/Berlin]": 0.7705,
120 * "2022-07-18T19:30+02:00[Europe/Berlin]": 0.5673,
121 * "2022-07-18T20:00+02:00[Europe/Berlin]": 0.3588,
122 * "2022-07-18T20:30+02:00[Europe/Berlin]": 0.1948,
123 * "2022-07-18T21:00+02:00[Europe/Berlin]": 0.0654,
124 * "2022-07-18T21:30+02:00[Europe/Berlin]": 0.0118,
125 * "2022-07-18T22:00+02:00[Europe/Berlin]": 0,
126 * "2022-07-18T22:30+02:00[Europe/Berlin]": 0,
127 * "2022-07-18T23:00+02:00[Europe/Berlin]": 0,
128 * "2022-07-18T23:30+02:00[Europe/Berlin]": 0
131 void testForecastObject() {
132 String content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
133 ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 0, 0).atZone(TEST_ZONE);
134 SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
135 content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
137 // test one day, step ahead in time and cross check channel values
138 double dayTotal = scfo.getDayTotal(now.toLocalDate(), QueryMode.Average);
139 double actual = scfo.getActualEnergyValue(now, QueryMode.Average);
140 double remain = scfo.getRemainingProduction(now, QueryMode.Average);
141 assertEquals(0.0, actual, TOLERANCE, "Begin of day actual");
142 assertEquals(23.107, remain, TOLERANCE, "Begin of day remaining");
143 assertEquals(23.107, dayTotal, TOLERANCE, "Day total");
144 assertEquals(0.0, scfo.getActualPowerValue(now, QueryMode.Average), TOLERANCE, "Begin of day power");
145 double previousPower = 0;
146 for (int i = 0; i < 47; i++) {
147 now = now.plusMinutes(30);
148 double power = scfo.getActualPowerValue(now, QueryMode.Average) / 2.0;
149 double powerAddOn = ((power + previousPower) / 2.0);
150 actual += powerAddOn;
151 assertEquals(actual, scfo.getActualEnergyValue(now, QueryMode.Average), TOLERANCE, "Actual at " + now);
152 remain -= powerAddOn;
153 assertEquals(remain, scfo.getRemainingProduction(now, QueryMode.Average), TOLERANCE, "Remain at " + now);
154 assertEquals(dayTotal, actual + remain, TOLERANCE, "Total sum at " + now);
155 previousPower = power;
161 String content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
162 ZonedDateTime now = LocalDateTime.of(2022, 7, 23, 16, 00).atZone(TEST_ZONE);
163 SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
164 content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
169 * "pv_estimate": 1.9176,
170 * "pv_estimate10": 0.8644,
171 * "pv_estimate90": 2.0456,
172 * "period_end": "2022-07-23T14:00:00.0000000Z",
176 * "pv_estimate": 1.7544,
177 * "pv_estimate10": 0.7708,
178 * "pv_estimate90": 1.864,
179 * "period_end": "2022-07-23T14:30:00.0000000Z",
182 assertEquals(1.9176, scfo.getActualPowerValue(now, QueryMode.Average), TOLERANCE, "Estimate power " + now);
183 assertEquals(1.9176, scfo.getPower(now.toInstant(), "average").doubleValue(), TOLERANCE,
184 "Estimate power " + now);
185 assertEquals(1.754, scfo.getActualPowerValue(now.plusMinutes(30), QueryMode.Average), TOLERANCE,
186 "Estimate power " + now.plusMinutes(30));
188 assertEquals(2.046, scfo.getActualPowerValue(now, QueryMode.Optimistic), TOLERANCE, "Optimistic power " + now);
189 assertEquals(1.864, scfo.getActualPowerValue(now.plusMinutes(30), QueryMode.Optimistic), TOLERANCE,
190 "Optimistic power " + now.plusMinutes(30));
192 assertEquals(0.864, scfo.getActualPowerValue(now, QueryMode.Pessimistic), TOLERANCE,
193 "Pessimistic power " + now);
194 assertEquals(0.771, scfo.getActualPowerValue(now.plusMinutes(30), QueryMode.Pessimistic), TOLERANCE,
195 "Pessimistic power " + now.plusMinutes(30));
199 * "pv_estimate": 1.9318,
200 * "period_end": "2022-07-17T14:30:00.0000000Z",
204 * "pv_estimate": 1.724,
205 * "period_end": "2022-07-17T15:00:00.0000000Z",
209 // get same values for optimistic / pessimistic and estimate in the past
210 ZonedDateTime past = LocalDateTime.of(2022, 7, 17, 16, 30).atZone(TEST_ZONE);
211 assertEquals(1.932, scfo.getActualPowerValue(past, QueryMode.Average), TOLERANCE, "Estimate power " + past);
212 assertEquals(1.724, scfo.getActualPowerValue(past.plusMinutes(30), QueryMode.Average), TOLERANCE,
213 "Estimate power " + now.plusMinutes(30));
215 assertEquals(1.932, scfo.getActualPowerValue(past, QueryMode.Optimistic), TOLERANCE,
216 "Optimistic power " + past);
217 assertEquals(1.724, scfo.getActualPowerValue(past.plusMinutes(30), QueryMode.Optimistic), TOLERANCE,
218 "Optimistic power " + past.plusMinutes(30));
220 assertEquals(1.932, scfo.getActualPowerValue(past, QueryMode.Pessimistic), TOLERANCE,
221 "Pessimistic power " + past);
222 assertEquals(1.724, scfo.getActualPowerValue(past.plusMinutes(30), QueryMode.Pessimistic), TOLERANCE,
223 "Pessimistic power " + past.plusMinutes(30));
227 * Data from TreeMap for manual validation
228 * 2022-07-17T04:30+02:00[Europe/Berlin]=0.0,
229 * 2022-07-17T05:00+02:00[Europe/Berlin]=0.0,
230 * 2022-07-17T05:30+02:00[Europe/Berlin]=0.0,
231 * 2022-07-17T06:00+02:00[Europe/Berlin]=0.0262,
232 * 2022-07-17T06:30+02:00[Europe/Berlin]=0.4252,
233 * 2022-07-17T07:00+02:00[Europe/Berlin]=0.7772, <<<
234 * 2022-07-17T07:30+02:00[Europe/Berlin]=1.0663,
235 * 2022-07-17T08:00+02:00[Europe/Berlin]=1.3848,
236 * 2022-07-17T08:30+02:00[Europe/Berlin]=1.6401,
237 * 2022-07-17T09:00+02:00[Europe/Berlin]=1.8614,
238 * 2022-07-17T09:30+02:00[Europe/Berlin]=2.0613,
239 * 2022-07-17T10:00+02:00[Europe/Berlin]=2.2365,
240 * 2022-07-17T10:30+02:00[Europe/Berlin]=2.3766,
241 * 2022-07-17T11:00+02:00[Europe/Berlin]=2.4719,
242 * 2022-07-17T11:30+02:00[Europe/Berlin]=2.5438,
243 * 2022-07-17T12:00+02:00[Europe/Berlin]=2.602,
244 * 2022-07-17T12:30+02:00[Europe/Berlin]=2.6213,
245 * 2022-07-17T13:00+02:00[Europe/Berlin]=2.6061,
246 * 2022-07-17T13:30+02:00[Europe/Berlin]=2.6181,
247 * 2022-07-17T14:00+02:00[Europe/Berlin]=2.5378,
248 * 2022-07-17T14:30+02:00[Europe/Berlin]=2.4651,
249 * 2022-07-17T15:00+02:00[Europe/Berlin]=2.3656,
250 * 2022-07-17T15:30+02:00[Europe/Berlin]=2.2374,
251 * 2022-07-17T16:00+02:00[Europe/Berlin]=2.1015,
252 * 2022-07-17T16:30+02:00[Europe/Berlin]=1.9318,
253 * 2022-07-17T17:00+02:00[Europe/Berlin]=1.724,
254 * 2022-07-17T17:30+02:00[Europe/Berlin]=1.5031,
255 * 2022-07-17T18:00+02:00[Europe/Berlin]=1.2834,
256 * 2022-07-17T18:30+02:00[Europe/Berlin]=1.0839,
257 * 2022-07-17T19:00+02:00[Europe/Berlin]=0.8581,
258 * 2022-07-17T19:30+02:00[Europe/Berlin]=0.6164,
259 * 2022-07-17T20:00+02:00[Europe/Berlin]=0.4465,
260 * 2022-07-17T20:30+02:00[Europe/Berlin]=0.2543,
261 * 2022-07-17T21:00+02:00[Europe/Berlin]=0.0848,
262 * 2022-07-17T21:30+02:00[Europe/Berlin]=0.0132,
263 * 2022-07-17T22:00+02:00[Europe/Berlin]=0.0,
264 * 2022-07-17T22:30+02:00[Europe/Berlin]=0.0
266 * <<< = 0.0262 + 0.4252 + 0.7772 = 1.2286 / 2 = 0.6143
269 void testForecastTreeMap() {
270 String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
271 ZonedDateTime now = LocalDateTime.of(2022, 7, 17, 7, 0).atZone(TEST_ZONE);
272 SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
273 assertEquals(0.42, scfo.getActualEnergyValue(now, QueryMode.Average), TOLERANCE, "Actual estimation");
274 assertEquals(25.413, scfo.getDayTotal(now.toLocalDate(), QueryMode.Average), TOLERANCE, "Day total");
279 String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
280 ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE);
281 SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
283 double d = scfo.getActualEnergyValue(now, QueryMode.Average);
284 fail("Exception expected instead of " + d);
285 } catch (SolarForecastException sfe) {
286 String message = sfe.getMessage();
287 assertNotNull(message);
288 assertTrue(message.contains(TOO_LATE_INDICATOR),
289 "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage());
291 content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
293 assertEquals(18.946, scfo.getActualEnergyValue(now, QueryMode.Average), 0.01, "Actual data");
294 assertEquals(23.107, scfo.getDayTotal(now.toLocalDate(), QueryMode.Average), 0.01, "Today data");
295 JSONObject rawJson = new JSONObject(scfo.getRaw());
296 assertTrue(rawJson.has("forecasts"));
297 assertTrue(rawJson.has("estimated_actuals"));
302 String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
303 ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE);
304 SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
306 double d = scfo.getActualEnergyValue(now, QueryMode.Average);
307 fail("Exception expected instead of " + d);
308 } catch (SolarForecastException sfe) {
309 String message = sfe.getMessage();
310 assertNotNull(message);
311 assertTrue(message.contains(TOO_LATE_INDICATOR),
312 "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage());
315 content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
318 assertEquals("2022-07-10T23:30+02:00[Europe/Berlin]", scfo.getForecastBegin().atZone(TEST_ZONE).toString(),
320 assertEquals("2022-07-24T23:00+02:00[Europe/Berlin]", scfo.getForecastEnd().atZone(TEST_ZONE).toString(),
322 // test daily forecasts + cumulated getEnergy
323 double totalEnergy = 0;
324 ZonedDateTime start = LocalDateTime.of(2022, 7, 18, 0, 0).atZone(TEST_ZONE);
325 for (int i = 0; i < 6; i++) {
326 QuantityType<Energy> qt = scfo.getDay(start.toLocalDate().plusDays(i));
327 QuantityType<Energy> eqt = scfo.getEnergy(start.plusDays(i).toInstant(), start.plusDays(i + 1).toInstant());
329 // check if energy calculation fits to daily query
330 assertEquals(qt.doubleValue(), eqt.doubleValue(), TOLERANCE, "Total " + i + " days forecast");
331 totalEnergy += qt.doubleValue();
333 // check if sum is fitting to total energy query
334 qt = scfo.getEnergy(start.toInstant(), start.plusDays(i + 1).toInstant());
335 assertEquals(totalEnergy, qt.doubleValue(), TOLERANCE * 2, "Total " + i + " days forecast");
340 void testOptimisticPessimistic() {
341 String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
342 ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE);
343 SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
344 content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
346 assertEquals(19.389, scfo.getDayTotal(now.toLocalDate().plusDays(2), QueryMode.Average), TOLERANCE,
348 assertEquals(7.358, scfo.getDayTotal(now.toLocalDate().plusDays(2), QueryMode.Pessimistic), TOLERANCE,
350 assertEquals(22.283, scfo.getDayTotal(now.toLocalDate().plusDays(2), QueryMode.Optimistic), TOLERANCE,
352 assertEquals(23.316, scfo.getDayTotal(now.toLocalDate().plusDays(6), QueryMode.Average), TOLERANCE,
354 assertEquals(9.8, scfo.getDayTotal(now.toLocalDate().plusDays(6), QueryMode.Pessimistic), TOLERANCE,
356 assertEquals(23.949, scfo.getDayTotal(now.toLocalDate().plusDays(6), QueryMode.Optimistic), TOLERANCE,
359 // access in past shall be rejected
360 Instant past = Instant.now().minus(5, ChronoUnit.MINUTES);
362 scfo.getPower(past, SolarForecast.OPTIMISTIC);
364 } catch (IllegalArgumentException e) {
365 assertEquals("Solcast argument optimistic only available for future values", e.getMessage(),
369 scfo.getPower(past, SolarForecast.PESSIMISTIC);
371 } catch (IllegalArgumentException e) {
372 assertEquals("Solcast argument pessimistic only available for future values", e.getMessage(),
373 "Pessimistic Power");
376 scfo.getPower(past, "total", "rubbish");
378 } catch (IllegalArgumentException e) {
379 assertEquals("Solcast doesn't support 2 arguments", e.getMessage(), "Too many qrguments");
382 scfo.getPower(past.plus(2, ChronoUnit.HOURS), "rubbish");
384 } catch (IllegalArgumentException e) {
385 assertEquals("Solcast doesn't support argument rubbish", e.getMessage(), "Rubbish argument");
389 fail("Exception expected");
390 } catch (SolarForecastException sfe) {
391 String message = sfe.getMessage();
392 assertNotNull(message);
393 assertTrue(message.contains(TOO_LATE_INDICATOR),
394 "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage());
400 String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
401 ZonedDateTime now = ZonedDateTime.now(TEST_ZONE);
402 SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
404 double d = scfo.getActualEnergyValue(now, QueryMode.Average);
405 fail("Exception expected instead of " + d);
406 } catch (SolarForecastException sfe) {
407 String message = sfe.getMessage();
408 assertNotNull(message);
409 assertTrue(message.contains(TOO_LATE_INDICATOR),
410 "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage());
412 content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
415 double d = scfo.getActualEnergyValue(now, QueryMode.Average);
416 fail("Exception expected instead of " + d);
417 } catch (SolarForecastException sfe) {
418 String message = sfe.getMessage();
419 assertNotNull(message);
420 assertTrue(message.contains(TOO_LATE_INDICATOR),
421 "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage());
424 double d = scfo.getDayTotal(now.toLocalDate(), QueryMode.Average);
425 fail("Exception expected instead of " + d);
426 } catch (SolarForecastException sfe) {
427 String message = sfe.getMessage();
428 assertNotNull(message);
429 assertTrue(message.contains(DAY_MISSING_INDICATOR),
430 "Expected: " + DAY_MISSING_INDICATOR + " Received: " + sfe.getMessage());
435 void testPowerInterpolation() {
436 String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
437 ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 15, 0).atZone(TEST_ZONE);
438 SolcastObject sco = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
439 content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
442 double startValue = sco.getActualPowerValue(now, QueryMode.Average);
443 double endValue = sco.getActualPowerValue(now.plusMinutes(30), QueryMode.Average);
444 for (int i = 0; i < 31; i++) {
445 double interpolation = i / 30.0;
446 double expected = ((1 - interpolation) * startValue) + (interpolation * endValue);
447 assertEquals(expected, sco.getActualPowerValue(now.plusMinutes(i), QueryMode.Average), TOLERANCE,
453 void testEnergyInterpolation() {
454 String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
455 ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 5, 30).atZone(TEST_ZONE);
456 SolcastObject sco = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
457 content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
461 double productionExpected = 0;
462 for (int i = 0; i < 1000; i++) {
463 double forecast = sco.getActualEnergyValue(now.plusMinutes(i), QueryMode.Average);
464 double addOnExpected = sco.getActualPowerValue(now.plusMinutes(i), QueryMode.Average) / 60.0;
465 productionExpected += addOnExpected;
466 double diff = forecast - productionExpected;
467 maxDiff = Math.max(diff, maxDiff);
468 assertEquals(productionExpected, sco.getActualEnergyValue(now.plusMinutes(i), QueryMode.Average),
469 100 * TOLERANCE, "Step " + i);
474 void testRawChannel() {
475 String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
476 ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE);
477 SolcastObject sco = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
478 content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
480 JSONObject joined = new JSONObject(sco.getRaw());
481 assertTrue(joined.has("forecasts"), "Forecasts available");
482 assertTrue(joined.has("estimated_actuals"), "Actual data available");
487 String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
488 ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE);
489 SolcastObject sco = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
490 content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
492 JSONObject joined = new JSONObject(sco.getRaw());
493 assertTrue(joined.has("forecasts"), "Forecasts available");
494 assertTrue(joined.has("estimated_actuals"), "Actual data available");
498 void testUnitDetection() {
499 assertEquals("kW", SolcastConstants.KILOWATT_UNIT.toString(), "Kilowatt");
500 assertEquals("W", Units.WATT.toString(), "Watt");
505 String utcTimeString = "2022-07-17T19:30:00.0000000Z";
506 SolcastObject so = new SolcastObject("sc-test", TIMEZONEPROVIDER);
507 ZonedDateTime zdt = so.getZdtFromUTC(utcTimeString);
509 assertEquals("2022-07-17T21:30+02:00[Europe/Berlin]", zdt.toString(), "ZonedDateTime");
510 LocalDateTime ldt = zdt.toLocalDateTime();
511 assertEquals("2022-07-17T21:30", ldt.toString(), "LocalDateTime");
512 LocalTime lt = zdt.toLocalTime();
513 assertEquals("21:30", lt.toString(), "LocalTime");
517 void testPowerTimeSeries() {
519 Instant now = Instant.now(Utils.getClock());
520 String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
521 SolcastObject sco = new SolcastObject("sc-test", content, now, TIMEZONEPROVIDER);
522 content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
525 TimeSeries powerSeries = sco.getPowerTimeSeries(QueryMode.Average);
526 List<QuantityType<?>> estimateL = new ArrayList<>();
527 assertEquals(302, powerSeries.size());
528 powerSeries.getStates().forEachOrdered(entry -> {
529 assertTrue(entry.timestamp().isAfter(Instant.now(Utils.getClock())));
530 State s = entry.state();
531 assertTrue(s instanceof QuantityType<?>);
532 assertEquals("kW", ((QuantityType<?>) s).getUnit().toString());
533 if (s instanceof QuantityType<?> qt) {
540 TimeSeries powerSeries10 = sco.getPowerTimeSeries(QueryMode.Pessimistic);
541 List<QuantityType<?>> estimate10 = new ArrayList<>();
542 assertEquals(302, powerSeries10.size());
543 powerSeries10.getStates().forEachOrdered(entry -> {
544 assertTrue(entry.timestamp().isAfter(Instant.now(Utils.getClock())));
545 State s = entry.state();
546 assertTrue(s instanceof QuantityType<?>);
547 assertEquals("kW", ((QuantityType<?>) s).getUnit().toString());
548 if (s instanceof QuantityType<?> qt) {
555 TimeSeries powerSeries90 = sco.getPowerTimeSeries(QueryMode.Optimistic);
556 List<QuantityType<?>> estimate90 = new ArrayList<>();
557 assertEquals(302, powerSeries90.size());
558 powerSeries90.getStates().forEachOrdered(entry -> {
559 assertTrue(entry.timestamp().isAfter(Instant.now(Utils.getClock())));
560 State s = entry.state();
561 assertTrue(s instanceof QuantityType<?>);
562 assertEquals("kW", ((QuantityType<?>) s).getUnit().toString());
563 if (s instanceof QuantityType<?> qt) {
570 for (int i = 0; i < estimateL.size(); i++) {
571 double lowValue = estimate10.get(i).doubleValue();
572 double estValue = estimateL.get(i).doubleValue();
573 double highValue = estimate90.get(i).doubleValue();
574 assertTrue(lowValue <= estValue && estValue <= highValue);
579 void testEnergyTimeSeries() {
581 Instant now = Instant.now(Utils.getClock());
582 String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
583 SolcastObject sco = new SolcastObject("sc-test", content, now, TIMEZONEPROVIDER);
584 content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
587 TimeSeries energySeries = sco.getEnergyTimeSeries(QueryMode.Average);
588 List<QuantityType<?>> estimateL = new ArrayList<>();
589 assertEquals(302, energySeries.size()); // 48 values each day for next 7 days
590 energySeries.getStates().forEachOrdered(entry -> {
591 assertTrue(Utils.isAfterOrEqual(entry.timestamp(), now));
592 State s = entry.state();
593 assertTrue(s instanceof QuantityType<?>);
594 assertEquals("kWh", ((QuantityType<?>) s).getUnit().toString());
595 if (s instanceof QuantityType<?> qt) {
602 TimeSeries energySeries10 = sco.getEnergyTimeSeries(QueryMode.Pessimistic);
603 List<QuantityType<?>> estimate10 = new ArrayList<>();
604 assertEquals(302, energySeries10.size()); // 48 values each day for next 7 days
605 energySeries10.getStates().forEachOrdered(entry -> {
606 assertTrue(Utils.isAfterOrEqual(entry.timestamp(), now));
607 State s = entry.state();
608 assertTrue(s instanceof QuantityType<?>);
609 assertEquals("kWh", ((QuantityType<?>) s).getUnit().toString());
610 if (s instanceof QuantityType<?> qt) {
617 TimeSeries energySeries90 = sco.getEnergyTimeSeries(QueryMode.Optimistic);
618 List<QuantityType<?>> estimate90 = new ArrayList<>();
619 assertEquals(302, energySeries90.size()); // 48 values each day for next 7 days
620 energySeries90.getStates().forEachOrdered(entry -> {
621 assertTrue(Utils.isAfterOrEqual(entry.timestamp(), now));
622 State s = entry.state();
623 assertTrue(s instanceof QuantityType<?>);
624 assertEquals("kWh", ((QuantityType<?>) s).getUnit().toString());
625 if (s instanceof QuantityType<?> qt) {
632 for (int i = 0; i < estimateL.size(); i++) {
633 double lowValue = estimate10.get(i).doubleValue();
634 double estValue = estimateL.get(i).doubleValue();
635 double highValue = estimate90.get(i).doubleValue();
636 assertTrue(lowValue <= estValue && estValue <= highValue);
641 void testCombinedPowerTimeSeries() {
643 BridgeImpl bi = new BridgeImpl(SolarForecastBindingConstants.SOLCAST_SITE, "bridge");
644 SolcastBridgeHandler scbh = new SolcastBridgeHandler(bi, new TimeZP());
646 CallbackMock cm = new CallbackMock();
647 scbh.setCallback(cm);
648 SolcastPlaneHandler scph1 = new SolcastPlaneMock(bi);
649 CallbackMock cm1 = new CallbackMock();
651 scph1.setCallback(cm1);
654 SolcastPlaneHandler scph2 = new SolcastPlaneMock(bi);
655 CallbackMock cm2 = new CallbackMock();
657 scph2.setCallback(cm2);
660 TimeSeries ts1 = cm.getTimeSeries("solarforecast:sc-site:bridge:average#power-estimate");
661 TimeSeries ts2 = cm2.getTimeSeries("solarforecast:sc-plane:thing:average#power-estimate");
662 assertEquals(302, ts1.size(), "TimeSeries size");
663 assertEquals(302, ts2.size(), "TimeSeries size");
664 Iterator<TimeSeries.Entry> iter1 = ts1.getStates().iterator();
665 Iterator<TimeSeries.Entry> iter2 = ts2.getStates().iterator();
666 while (iter1.hasNext()) {
667 TimeSeries.Entry e1 = iter1.next();
668 TimeSeries.Entry e2 = iter2.next();
669 assertEquals("kW", ((QuantityType<?>) e1.state()).getUnit().toString(), "Power Unit");
670 assertEquals("kW", ((QuantityType<?>) e2.state()).getUnit().toString(), "Power Unit");
671 assertEquals(((QuantityType<?>) e1.state()).doubleValue(), ((QuantityType<?>) e2.state()).doubleValue() * 2,
672 0.01, "Power Value");
680 void testCombinedEnergyTimeSeries() {
682 BridgeImpl bi = new BridgeImpl(SolarForecastBindingConstants.SOLCAST_SITE, "bridge");
683 SolcastBridgeHandler scbh = new SolcastBridgeHandler(bi, new TimeZP());
685 CallbackMock cm = new CallbackMock();
686 scbh.setCallback(cm);
688 SolcastPlaneHandler scph1 = new SolcastPlaneMock(bi);
689 CallbackMock cm1 = new CallbackMock();
691 scph1.setCallback(cm1);
693 SolcastPlaneHandler scph2 = new SolcastPlaneMock(bi);
694 CallbackMock cm2 = new CallbackMock();
696 scph2.setCallback(cm2);
698 // simulate trigger of refresh job
701 TimeSeries ts1 = cm.getTimeSeries("solarforecast:sc-site:bridge:average#energy-estimate");
702 TimeSeries ts2 = cm2.getTimeSeries("solarforecast:sc-plane:thing:average#energy-estimate");
703 assertEquals(302, ts1.size(), "TimeSeries size");
704 assertEquals(302, ts2.size(), "TimeSeries size");
706 Iterator<TimeSeries.Entry> iter1 = ts1.getStates().iterator();
707 Iterator<TimeSeries.Entry> iter2 = ts2.getStates().iterator();
708 while (iter1.hasNext()) {
709 TimeSeries.Entry e1 = iter1.next();
710 TimeSeries.Entry e2 = iter2.next();
711 assertEquals("kWh", ((QuantityType<?>) e1.state()).getUnit().toString(), "Power Unit");
712 assertEquals("kWh", ((QuantityType<?>) e2.state()).getUnit().toString(), "Power Unit");
713 assertEquals(((QuantityType<?>) e1.state()).doubleValue(), ((QuantityType<?>) e2.state()).doubleValue() * 2,
722 void testSingleEnergyTimeSeries() {
724 BridgeImpl bi = new BridgeImpl(SolarForecastBindingConstants.SOLCAST_SITE, "bridge");
725 SolcastBridgeHandler scbh = new SolcastBridgeHandler(bi, new TimeZP());
727 CallbackMock cm = new CallbackMock();
728 scbh.setCallback(cm);
730 SolcastPlaneHandler scph1 = new SolcastPlaneMock(bi);
731 CallbackMock cm1 = new CallbackMock();
733 scph1.setCallback(cm1);
735 // simulate trigger of refresh job
738 TimeSeries ts1 = cm.getTimeSeries("solarforecast:sc-site:bridge:average#energy-estimate");
739 assertEquals(302, ts1.size(), "TimeSeries size");
740 Iterator<TimeSeries.Entry> iter1 = ts1.getStates().iterator();
741 while (iter1.hasNext()) {
742 TimeSeries.Entry e1 = iter1.next();
743 assertEquals("kWh", ((QuantityType<?>) e1.state()).getUnit().toString(), "Power Unit");