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.Instant;
18 import java.time.LocalDateTime;
19 import java.time.LocalTime;
20 import java.time.ZoneId;
21 import java.time.ZonedDateTime;
22 import java.time.temporal.ChronoUnit;
23 import java.util.ArrayList;
24 import java.util.Iterator;
25 import java.util.List;
27 import javax.measure.quantity.Energy;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.json.JSONObject;
31 import org.junit.jupiter.api.Test;
32 import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants;
33 import org.openhab.binding.solarforecast.internal.SolarForecastException;
34 import org.openhab.binding.solarforecast.internal.actions.SolarForecast;
35 import org.openhab.binding.solarforecast.internal.solcast.SolcastConstants;
36 import org.openhab.binding.solarforecast.internal.solcast.SolcastObject;
37 import org.openhab.binding.solarforecast.internal.solcast.SolcastObject.QueryMode;
38 import org.openhab.binding.solarforecast.internal.solcast.handler.SolcastBridgeHandler;
39 import org.openhab.binding.solarforecast.internal.solcast.handler.SolcastPlaneHandler;
40 import org.openhab.binding.solarforecast.internal.solcast.handler.SolcastPlaneMock;
41 import org.openhab.core.library.types.QuantityType;
42 import org.openhab.core.library.unit.Units;
43 import org.openhab.core.thing.internal.BridgeImpl;
44 import org.openhab.core.types.State;
45 import org.openhab.core.types.TimeSeries;
48 * The {@link SolcastTest} tests responses from forecast solar website
50 * @author Bernd Weymann - Initial contribution
54 public static final ZoneId TEST_ZONE = ZoneId.of("Europe/Berlin");
55 private static final TimeZP TIMEZONEPROVIDER = new TimeZP();
56 // double comparison tolerance = 1 Watt
57 private static final double TOLERANCE = 0.001;
59 public static final String TOO_LATE_INDICATOR = "too late";
60 public static final String DAY_MISSING_INDICATOR = "not available in forecast";
63 * "2022-07-18T00:00+02:00[Europe/Berlin]": 0,
64 * "2022-07-18T00:30+02:00[Europe/Berlin]": 0,
65 * "2022-07-18T01:00+02:00[Europe/Berlin]": 0,
66 * "2022-07-18T01:30+02:00[Europe/Berlin]": 0,
67 * "2022-07-18T02:00+02:00[Europe/Berlin]": 0,
68 * "2022-07-18T02:30+02:00[Europe/Berlin]": 0,
69 * "2022-07-18T03:00+02:00[Europe/Berlin]": 0,
70 * "2022-07-18T03:30+02:00[Europe/Berlin]": 0,
71 * "2022-07-18T04:00+02:00[Europe/Berlin]": 0,
72 * "2022-07-18T04:30+02:00[Europe/Berlin]": 0,
73 * "2022-07-18T05:00+02:00[Europe/Berlin]": 0,
74 * "2022-07-18T05:30+02:00[Europe/Berlin]": 0,
75 * "2022-07-18T06:00+02:00[Europe/Berlin]": 0.0205,
76 * "2022-07-18T06:30+02:00[Europe/Berlin]": 0.1416,
77 * "2022-07-18T07:00+02:00[Europe/Berlin]": 0.4478,
78 * "2022-07-18T07:30+02:00[Europe/Berlin]": 0.763,
79 * "2022-07-18T08:00+02:00[Europe/Berlin]": 1.1367,
80 * "2022-07-18T08:30+02:00[Europe/Berlin]": 1.4044,
81 * "2022-07-18T09:00+02:00[Europe/Berlin]": 1.6632,
82 * "2022-07-18T09:30+02:00[Europe/Berlin]": 1.8667,
83 * "2022-07-18T10:00+02:00[Europe/Berlin]": 2.0729,
84 * "2022-07-18T10:30+02:00[Europe/Berlin]": 2.2377,
85 * "2022-07-18T11:00+02:00[Europe/Berlin]": 2.3516,
86 * "2022-07-18T11:30+02:00[Europe/Berlin]": 2.4295,
87 * "2022-07-18T12:00+02:00[Europe/Berlin]": 2.5136,
88 * "2022-07-18T12:30+02:00[Europe/Berlin]": 2.5295,
89 * "2022-07-18T13:00+02:00[Europe/Berlin]": 2.526,
90 * "2022-07-18T13:30+02:00[Europe/Berlin]": 2.4879,
91 * "2022-07-18T14:00+02:00[Europe/Berlin]": 2.4092,
92 * "2022-07-18T14:30+02:00[Europe/Berlin]": 2.3309,
93 * "2022-07-18T15:00+02:00[Europe/Berlin]": 2.1984,
94 * "2022-07-18T15:30+02:00[Europe/Berlin]": 2.0416,
95 * "2022-07-18T16:00+02:00[Europe/Berlin]": 1.9076,
96 * "2022-07-18T16:30+02:00[Europe/Berlin]": 1.7416,
97 * "2022-07-18T17:00+02:00[Europe/Berlin]": 1.5414,
98 * "2022-07-18T17:30+02:00[Europe/Berlin]": 1.3683,
99 * "2022-07-18T18:00+02:00[Europe/Berlin]": 1.1603,
100 * "2022-07-18T18:30+02:00[Europe/Berlin]": 0.9527,
101 * "2022-07-18T19:00+02:00[Europe/Berlin]": 0.7705,
102 * "2022-07-18T19:30+02:00[Europe/Berlin]": 0.5673,
103 * "2022-07-18T20:00+02:00[Europe/Berlin]": 0.3588,
104 * "2022-07-18T20:30+02:00[Europe/Berlin]": 0.1948,
105 * "2022-07-18T21:00+02:00[Europe/Berlin]": 0.0654,
106 * "2022-07-18T21:30+02:00[Europe/Berlin]": 0.0118,
107 * "2022-07-18T22:00+02:00[Europe/Berlin]": 0,
108 * "2022-07-18T22:30+02:00[Europe/Berlin]": 0,
109 * "2022-07-18T23:00+02:00[Europe/Berlin]": 0,
110 * "2022-07-18T23:30+02:00[Europe/Berlin]": 0
113 void testForecastObject() {
114 String content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
115 ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 0, 0).atZone(TEST_ZONE);
116 SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
117 content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
119 // test one day, step ahead in time and cross check channel values
120 double dayTotal = scfo.getDayTotal(now.toLocalDate(), QueryMode.Average);
121 double actual = scfo.getActualEnergyValue(now, QueryMode.Average);
122 double remain = scfo.getRemainingProduction(now, QueryMode.Average);
123 assertEquals(0.0, actual, TOLERANCE, "Begin of day actual");
124 assertEquals(23.107, remain, TOLERANCE, "Begin of day remaining");
125 assertEquals(23.107, dayTotal, TOLERANCE, "Day total");
126 assertEquals(0.0, scfo.getActualPowerValue(now, QueryMode.Average), TOLERANCE, "Begin of day power");
127 double previousPower = 0;
128 for (int i = 0; i < 47; i++) {
129 now = now.plusMinutes(30);
130 double power = scfo.getActualPowerValue(now, QueryMode.Average) / 2.0;
131 double powerAddOn = ((power + previousPower) / 2.0);
132 actual += powerAddOn;
133 assertEquals(actual, scfo.getActualEnergyValue(now, QueryMode.Average), TOLERANCE, "Actual at " + now);
134 remain -= powerAddOn;
135 assertEquals(remain, scfo.getRemainingProduction(now, QueryMode.Average), TOLERANCE, "Remain at " + now);
136 assertEquals(dayTotal, actual + remain, TOLERANCE, "Total sum at " + now);
137 previousPower = power;
143 String content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
144 ZonedDateTime now = LocalDateTime.of(2022, 7, 23, 16, 00).atZone(TEST_ZONE);
145 SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
146 content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
151 * "pv_estimate": 1.9176,
152 * "pv_estimate10": 0.8644,
153 * "pv_estimate90": 2.0456,
154 * "period_end": "2022-07-23T14:00:00.0000000Z",
158 * "pv_estimate": 1.7544,
159 * "pv_estimate10": 0.7708,
160 * "pv_estimate90": 1.864,
161 * "period_end": "2022-07-23T14:30:00.0000000Z",
164 assertEquals(1.9176, scfo.getActualPowerValue(now, QueryMode.Average), TOLERANCE, "Estimate power " + now);
165 assertEquals(1.9176, scfo.getPower(now.toInstant(), "average").doubleValue(), TOLERANCE,
166 "Estimate power " + now);
167 assertEquals(1.754, scfo.getActualPowerValue(now.plusMinutes(30), QueryMode.Average), TOLERANCE,
168 "Estimate power " + now.plusMinutes(30));
170 assertEquals(2.046, scfo.getActualPowerValue(now, QueryMode.Optimistic), TOLERANCE, "Optimistic power " + now);
171 assertEquals(1.864, scfo.getActualPowerValue(now.plusMinutes(30), QueryMode.Optimistic), TOLERANCE,
172 "Optimistic power " + now.plusMinutes(30));
174 assertEquals(0.864, scfo.getActualPowerValue(now, QueryMode.Pessimistic), TOLERANCE,
175 "Pessimistic power " + now);
176 assertEquals(0.771, scfo.getActualPowerValue(now.plusMinutes(30), QueryMode.Pessimistic), TOLERANCE,
177 "Pessimistic power " + now.plusMinutes(30));
181 * "pv_estimate": 1.9318,
182 * "period_end": "2022-07-17T14:30:00.0000000Z",
186 * "pv_estimate": 1.724,
187 * "period_end": "2022-07-17T15:00:00.0000000Z",
191 // get same values for optimistic / pessimistic and estimate in the past
192 ZonedDateTime past = LocalDateTime.of(2022, 7, 17, 16, 30).atZone(TEST_ZONE);
193 assertEquals(1.932, scfo.getActualPowerValue(past, QueryMode.Average), TOLERANCE, "Estimate power " + past);
194 assertEquals(1.724, scfo.getActualPowerValue(past.plusMinutes(30), QueryMode.Average), TOLERANCE,
195 "Estimate power " + now.plusMinutes(30));
197 assertEquals(1.932, scfo.getActualPowerValue(past, QueryMode.Optimistic), TOLERANCE,
198 "Optimistic power " + past);
199 assertEquals(1.724, scfo.getActualPowerValue(past.plusMinutes(30), QueryMode.Optimistic), TOLERANCE,
200 "Optimistic power " + past.plusMinutes(30));
202 assertEquals(1.932, scfo.getActualPowerValue(past, QueryMode.Pessimistic), TOLERANCE,
203 "Pessimistic power " + past);
204 assertEquals(1.724, scfo.getActualPowerValue(past.plusMinutes(30), QueryMode.Pessimistic), TOLERANCE,
205 "Pessimistic power " + past.plusMinutes(30));
209 * Data from TreeMap for manual validation
210 * 2022-07-17T04:30+02:00[Europe/Berlin]=0.0,
211 * 2022-07-17T05:00+02:00[Europe/Berlin]=0.0,
212 * 2022-07-17T05:30+02:00[Europe/Berlin]=0.0,
213 * 2022-07-17T06:00+02:00[Europe/Berlin]=0.0262,
214 * 2022-07-17T06:30+02:00[Europe/Berlin]=0.4252,
215 * 2022-07-17T07:00+02:00[Europe/Berlin]=0.7772, <<<
216 * 2022-07-17T07:30+02:00[Europe/Berlin]=1.0663,
217 * 2022-07-17T08:00+02:00[Europe/Berlin]=1.3848,
218 * 2022-07-17T08:30+02:00[Europe/Berlin]=1.6401,
219 * 2022-07-17T09:00+02:00[Europe/Berlin]=1.8614,
220 * 2022-07-17T09:30+02:00[Europe/Berlin]=2.0613,
221 * 2022-07-17T10:00+02:00[Europe/Berlin]=2.2365,
222 * 2022-07-17T10:30+02:00[Europe/Berlin]=2.3766,
223 * 2022-07-17T11:00+02:00[Europe/Berlin]=2.4719,
224 * 2022-07-17T11:30+02:00[Europe/Berlin]=2.5438,
225 * 2022-07-17T12:00+02:00[Europe/Berlin]=2.602,
226 * 2022-07-17T12:30+02:00[Europe/Berlin]=2.6213,
227 * 2022-07-17T13:00+02:00[Europe/Berlin]=2.6061,
228 * 2022-07-17T13:30+02:00[Europe/Berlin]=2.6181,
229 * 2022-07-17T14:00+02:00[Europe/Berlin]=2.5378,
230 * 2022-07-17T14:30+02:00[Europe/Berlin]=2.4651,
231 * 2022-07-17T15:00+02:00[Europe/Berlin]=2.3656,
232 * 2022-07-17T15:30+02:00[Europe/Berlin]=2.2374,
233 * 2022-07-17T16:00+02:00[Europe/Berlin]=2.1015,
234 * 2022-07-17T16:30+02:00[Europe/Berlin]=1.9318,
235 * 2022-07-17T17:00+02:00[Europe/Berlin]=1.724,
236 * 2022-07-17T17:30+02:00[Europe/Berlin]=1.5031,
237 * 2022-07-17T18:00+02:00[Europe/Berlin]=1.2834,
238 * 2022-07-17T18:30+02:00[Europe/Berlin]=1.0839,
239 * 2022-07-17T19:00+02:00[Europe/Berlin]=0.8581,
240 * 2022-07-17T19:30+02:00[Europe/Berlin]=0.6164,
241 * 2022-07-17T20:00+02:00[Europe/Berlin]=0.4465,
242 * 2022-07-17T20:30+02:00[Europe/Berlin]=0.2543,
243 * 2022-07-17T21:00+02:00[Europe/Berlin]=0.0848,
244 * 2022-07-17T21:30+02:00[Europe/Berlin]=0.0132,
245 * 2022-07-17T22:00+02:00[Europe/Berlin]=0.0,
246 * 2022-07-17T22:30+02:00[Europe/Berlin]=0.0
248 * <<< = 0.0262 + 0.4252 + 0.7772 = 1.2286 / 2 = 0.6143
251 void testForecastTreeMap() {
252 String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
253 ZonedDateTime now = LocalDateTime.of(2022, 7, 17, 7, 0).atZone(TEST_ZONE);
254 SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
255 assertEquals(0.42, scfo.getActualEnergyValue(now, QueryMode.Average), TOLERANCE, "Actual estimation");
256 assertEquals(25.413, scfo.getDayTotal(now.toLocalDate(), QueryMode.Average), TOLERANCE, "Day total");
261 String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
262 ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE);
263 SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
265 double d = scfo.getActualEnergyValue(now, QueryMode.Average);
266 fail("Exception expected instead of " + d);
267 } catch (SolarForecastException sfe) {
268 String message = sfe.getMessage();
269 assertNotNull(message);
270 assertTrue(message.contains(TOO_LATE_INDICATOR),
271 "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage());
273 content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
275 assertEquals(18.946, scfo.getActualEnergyValue(now, QueryMode.Average), 0.01, "Actual data");
276 assertEquals(23.107, scfo.getDayTotal(now.toLocalDate(), QueryMode.Average), 0.01, "Today data");
277 JSONObject rawJson = new JSONObject(scfo.getRaw());
278 assertTrue(rawJson.has("forecasts"));
279 assertTrue(rawJson.has("estimated_actuals"));
284 String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
285 ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE);
286 SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
288 double d = scfo.getActualEnergyValue(now, QueryMode.Average);
289 fail("Exception expected instead of " + d);
290 } catch (SolarForecastException sfe) {
291 String message = sfe.getMessage();
292 assertNotNull(message);
293 assertTrue(message.contains(TOO_LATE_INDICATOR),
294 "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage());
297 content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
300 assertEquals("2022-07-10T23:30+02:00[Europe/Berlin]", scfo.getForecastBegin().atZone(TEST_ZONE).toString(),
302 assertEquals("2022-07-24T23:00+02:00[Europe/Berlin]", scfo.getForecastEnd().atZone(TEST_ZONE).toString(),
304 // test daily forecasts + cumulated getEnergy
305 double totalEnergy = 0;
306 ZonedDateTime start = LocalDateTime.of(2022, 7, 18, 0, 0).atZone(TEST_ZONE);
307 for (int i = 0; i < 6; i++) {
308 QuantityType<Energy> qt = scfo.getDay(start.toLocalDate().plusDays(i));
309 QuantityType<Energy> eqt = scfo.getEnergy(start.plusDays(i).toInstant(), start.plusDays(i + 1).toInstant());
311 // check if energy calculation fits to daily query
312 assertEquals(qt.doubleValue(), eqt.doubleValue(), TOLERANCE, "Total " + i + " days forecast");
313 totalEnergy += qt.doubleValue();
315 // check if sum is fitting to total energy query
316 qt = scfo.getEnergy(start.toInstant(), start.plusDays(i + 1).toInstant());
317 assertEquals(totalEnergy, qt.doubleValue(), TOLERANCE * 2, "Total " + i + " days forecast");
322 void testOptimisticPessimistic() {
323 String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
324 ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE);
325 SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
326 content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
328 assertEquals(19.389, scfo.getDayTotal(now.toLocalDate().plusDays(2), QueryMode.Average), TOLERANCE,
330 assertEquals(7.358, scfo.getDayTotal(now.toLocalDate().plusDays(2), QueryMode.Pessimistic), TOLERANCE,
332 assertEquals(22.283, scfo.getDayTotal(now.toLocalDate().plusDays(2), QueryMode.Optimistic), TOLERANCE,
334 assertEquals(23.316, scfo.getDayTotal(now.toLocalDate().plusDays(6), QueryMode.Average), TOLERANCE,
336 assertEquals(9.8, scfo.getDayTotal(now.toLocalDate().plusDays(6), QueryMode.Pessimistic), TOLERANCE,
338 assertEquals(23.949, scfo.getDayTotal(now.toLocalDate().plusDays(6), QueryMode.Optimistic), TOLERANCE,
341 // access in past shall be rejected
342 Instant past = Instant.now().minus(5, ChronoUnit.MINUTES);
344 scfo.getPower(past, SolarForecast.OPTIMISTIC);
346 } catch (IllegalArgumentException e) {
347 assertEquals("Solcast argument optimistic only available for future values", e.getMessage(),
351 scfo.getPower(past, SolarForecast.PESSIMISTIC);
353 } catch (IllegalArgumentException e) {
354 assertEquals("Solcast argument pessimistic only available for future values", e.getMessage(),
355 "Pessimistic Power");
358 scfo.getPower(past, "total", "rubbish");
360 } catch (IllegalArgumentException e) {
361 assertEquals("Solcast doesn't support 2 arguments", e.getMessage(), "Too many qrguments");
364 scfo.getPower(past.plus(2, ChronoUnit.HOURS), "rubbish");
366 } catch (IllegalArgumentException e) {
367 assertEquals("Solcast doesn't support argument rubbish", e.getMessage(), "Rubbish argument");
371 fail("Exception expected");
372 } catch (SolarForecastException sfe) {
373 String message = sfe.getMessage();
374 assertNotNull(message);
375 assertTrue(message.contains(TOO_LATE_INDICATOR),
376 "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage());
382 String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
383 ZonedDateTime now = ZonedDateTime.now(TEST_ZONE);
384 SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
386 double d = scfo.getActualEnergyValue(now, QueryMode.Average);
387 fail("Exception expected instead of " + d);
388 } catch (SolarForecastException sfe) {
389 String message = sfe.getMessage();
390 assertNotNull(message);
391 assertTrue(message.contains(TOO_LATE_INDICATOR),
392 "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage());
394 content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
397 double d = scfo.getActualEnergyValue(now, QueryMode.Average);
398 fail("Exception expected instead of " + d);
399 } catch (SolarForecastException sfe) {
400 String message = sfe.getMessage();
401 assertNotNull(message);
402 assertTrue(message.contains(TOO_LATE_INDICATOR),
403 "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage());
406 double d = scfo.getDayTotal(now.toLocalDate(), QueryMode.Average);
407 fail("Exception expected instead of " + d);
408 } catch (SolarForecastException sfe) {
409 String message = sfe.getMessage();
410 assertNotNull(message);
411 assertTrue(message.contains(DAY_MISSING_INDICATOR),
412 "Expected: " + DAY_MISSING_INDICATOR + " Received: " + sfe.getMessage());
417 void testPowerInterpolation() {
418 String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
419 ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 15, 0).atZone(TEST_ZONE);
420 SolcastObject sco = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
421 content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
424 double startValue = sco.getActualPowerValue(now, QueryMode.Average);
425 double endValue = sco.getActualPowerValue(now.plusMinutes(30), QueryMode.Average);
426 for (int i = 0; i < 31; i++) {
427 double interpolation = i / 30.0;
428 double expected = ((1 - interpolation) * startValue) + (interpolation * endValue);
429 assertEquals(expected, sco.getActualPowerValue(now.plusMinutes(i), QueryMode.Average), TOLERANCE,
435 void testEnergyInterpolation() {
436 String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
437 ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 5, 30).atZone(TEST_ZONE);
438 SolcastObject sco = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
439 content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
443 double productionExpected = 0;
444 for (int i = 0; i < 1000; i++) {
445 double forecast = sco.getActualEnergyValue(now.plusMinutes(i), QueryMode.Average);
446 double addOnExpected = sco.getActualPowerValue(now.plusMinutes(i), QueryMode.Average) / 60.0;
447 productionExpected += addOnExpected;
448 double diff = forecast - productionExpected;
449 maxDiff = Math.max(diff, maxDiff);
450 assertEquals(productionExpected, sco.getActualEnergyValue(now.plusMinutes(i), QueryMode.Average),
451 100 * TOLERANCE, "Step " + i);
456 void testRawChannel() {
457 String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
458 ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE);
459 SolcastObject sco = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
460 content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
462 JSONObject joined = new JSONObject(sco.getRaw());
463 assertTrue(joined.has("forecasts"), "Forecasts available");
464 assertTrue(joined.has("estimated_actuals"), "Actual data available");
469 String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
470 ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE);
471 SolcastObject sco = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
472 content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
474 JSONObject joined = new JSONObject(sco.getRaw());
475 assertTrue(joined.has("forecasts"), "Forecasts available");
476 assertTrue(joined.has("estimated_actuals"), "Actual data available");
480 void testUnitDetection() {
481 assertEquals("kW", SolcastConstants.KILOWATT_UNIT.toString(), "Kilowatt");
482 assertEquals("W", Units.WATT.toString(), "Watt");
487 String utcTimeString = "2022-07-17T19:30:00.0000000Z";
488 SolcastObject so = new SolcastObject("sc-test", TIMEZONEPROVIDER);
489 ZonedDateTime zdt = so.getZdtFromUTC(utcTimeString);
491 assertEquals("2022-07-17T21:30+02:00[Europe/Berlin]", zdt.toString(), "ZonedDateTime");
492 LocalDateTime ldt = zdt.toLocalDateTime();
493 assertEquals("2022-07-17T21:30", ldt.toString(), "LocalDateTime");
494 LocalTime lt = zdt.toLocalTime();
495 assertEquals("21:30", lt.toString(), "LocalTime");
499 void testPowerTimeSeries() {
500 String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
501 ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE);
502 SolcastObject sco = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
503 content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
506 TimeSeries powerSeries = sco.getPowerTimeSeries(QueryMode.Average);
507 List<QuantityType<?>> estimateL = new ArrayList<>();
508 assertEquals(672, powerSeries.size());
509 powerSeries.getStates().forEachOrdered(entry -> {
510 State s = entry.state();
511 assertTrue(s instanceof QuantityType<?>);
512 assertEquals("kW", ((QuantityType<?>) s).getUnit().toString());
513 if (s instanceof QuantityType<?> qt) {
520 TimeSeries powerSeries10 = sco.getPowerTimeSeries(QueryMode.Pessimistic);
521 List<QuantityType<?>> estimate10 = new ArrayList<>();
522 assertEquals(672, powerSeries10.size());
523 powerSeries10.getStates().forEachOrdered(entry -> {
524 State s = entry.state();
525 assertTrue(s instanceof QuantityType<?>);
526 assertEquals("kW", ((QuantityType<?>) s).getUnit().toString());
527 if (s instanceof QuantityType<?> qt) {
534 TimeSeries powerSeries90 = sco.getPowerTimeSeries(QueryMode.Optimistic);
535 List<QuantityType<?>> estimate90 = new ArrayList<>();
536 assertEquals(672, powerSeries90.size());
537 powerSeries90.getStates().forEachOrdered(entry -> {
538 State s = entry.state();
539 assertTrue(s instanceof QuantityType<?>);
540 assertEquals("kW", ((QuantityType<?>) s).getUnit().toString());
541 if (s instanceof QuantityType<?> qt) {
548 for (int i = 0; i < estimateL.size(); i++) {
549 double lowValue = estimate10.get(i).doubleValue();
550 double estValue = estimateL.get(i).doubleValue();
551 double highValue = estimate90.get(i).doubleValue();
552 assertTrue(lowValue <= estValue && estValue <= highValue);
557 void testEnergyTimeSeries() {
558 String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json");
559 ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE);
560 SolcastObject sco = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER);
561 content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json");
564 TimeSeries energySeries = sco.getEnergyTimeSeries(QueryMode.Average);
565 List<QuantityType<?>> estimateL = new ArrayList<>();
566 assertEquals(672, energySeries.size()); // 18 values each day for 2 days
567 energySeries.getStates().forEachOrdered(entry -> {
568 State s = entry.state();
569 assertTrue(s instanceof QuantityType<?>);
570 assertEquals("kWh", ((QuantityType<?>) s).getUnit().toString());
571 if (s instanceof QuantityType<?> qt) {
578 TimeSeries energySeries10 = sco.getEnergyTimeSeries(QueryMode.Pessimistic);
579 List<QuantityType<?>> estimate10 = new ArrayList<>();
580 assertEquals(672, energySeries10.size()); // 18 values each day for 2 days
581 energySeries10.getStates().forEachOrdered(entry -> {
582 State s = entry.state();
583 assertTrue(s instanceof QuantityType<?>);
584 assertEquals("kWh", ((QuantityType<?>) s).getUnit().toString());
585 if (s instanceof QuantityType<?> qt) {
592 TimeSeries energySeries90 = sco.getEnergyTimeSeries(QueryMode.Optimistic);
593 List<QuantityType<?>> estimate90 = new ArrayList<>();
594 assertEquals(672, energySeries90.size()); // 18 values each day for 2 days
595 energySeries90.getStates().forEachOrdered(entry -> {
596 State s = entry.state();
597 assertTrue(s instanceof QuantityType<?>);
598 assertEquals("kWh", ((QuantityType<?>) s).getUnit().toString());
599 if (s instanceof QuantityType<?> qt) {
606 for (int i = 0; i < estimateL.size(); i++) {
607 double lowValue = estimate10.get(i).doubleValue();
608 double estValue = estimateL.get(i).doubleValue();
609 double highValue = estimate90.get(i).doubleValue();
610 assertTrue(lowValue <= estValue && estValue <= highValue);
615 void testCombinedPowerTimeSeries() {
616 BridgeImpl bi = new BridgeImpl(SolarForecastBindingConstants.SOLCAST_SITE, "bridge");
617 SolcastBridgeHandler scbh = new SolcastBridgeHandler(bi, new TimeZP());
619 CallbackMock cm = new CallbackMock();
620 scbh.setCallback(cm);
621 SolcastPlaneHandler scph1 = new SolcastPlaneMock(bi);
622 CallbackMock cm1 = new CallbackMock();
624 scph1.setCallback(cm1);
627 SolcastPlaneHandler scph2 = new SolcastPlaneMock(bi);
628 CallbackMock cm2 = new CallbackMock();
630 scph2.setCallback(cm2);
633 TimeSeries ts1 = cm.getTimeSeries("solarforecast:sc-site:bridge:average#power-estimate");
634 TimeSeries ts2 = cm2.getTimeSeries("solarforecast:sc-plane:thing:average#power-estimate");
635 assertEquals(336, ts1.size(), "TimeSeries size");
636 assertEquals(336, ts2.size(), "TimeSeries size");
637 Iterator<TimeSeries.Entry> iter1 = ts1.getStates().iterator();
638 Iterator<TimeSeries.Entry> iter2 = ts2.getStates().iterator();
639 while (iter1.hasNext()) {
640 TimeSeries.Entry e1 = iter1.next();
641 TimeSeries.Entry e2 = iter2.next();
642 assertEquals("kW", ((QuantityType<?>) e1.state()).getUnit().toString(), "Power Unit");
643 assertEquals("kW", ((QuantityType<?>) e2.state()).getUnit().toString(), "Power Unit");
644 assertEquals(((QuantityType<?>) e1.state()).doubleValue(), ((QuantityType<?>) e2.state()).doubleValue() * 2,
645 0.01, "Power Value");
653 void testCombinedEnergyTimeSeries() {
654 BridgeImpl bi = new BridgeImpl(SolarForecastBindingConstants.SOLCAST_SITE, "bridge");
655 SolcastBridgeHandler scbh = new SolcastBridgeHandler(bi, new TimeZP());
657 CallbackMock cm = new CallbackMock();
658 scbh.setCallback(cm);
660 SolcastPlaneHandler scph1 = new SolcastPlaneMock(bi);
661 CallbackMock cm1 = new CallbackMock();
663 scph1.setCallback(cm1);
665 SolcastPlaneHandler scph2 = new SolcastPlaneMock(bi);
666 CallbackMock cm2 = new CallbackMock();
668 scph2.setCallback(cm2);
670 // simulate trigger of refresh job
673 TimeSeries ts1 = cm.getTimeSeries("solarforecast:sc-site:bridge:average#energy-estimate");
674 TimeSeries ts2 = cm2.getTimeSeries("solarforecast:sc-plane:thing:average#energy-estimate");
675 assertEquals(336, ts1.size(), "TimeSeries size");
676 assertEquals(336, ts2.size(), "TimeSeries size");
678 Iterator<TimeSeries.Entry> iter1 = ts1.getStates().iterator();
679 Iterator<TimeSeries.Entry> iter2 = ts2.getStates().iterator();
680 while (iter1.hasNext()) {
681 TimeSeries.Entry e1 = iter1.next();
682 TimeSeries.Entry e2 = iter2.next();
683 assertEquals("kWh", ((QuantityType<?>) e1.state()).getUnit().toString(), "Power Unit");
684 assertEquals("kWh", ((QuantityType<?>) e2.state()).getUnit().toString(), "Power Unit");
685 assertEquals(((QuantityType<?>) e1.state()).doubleValue(), ((QuantityType<?>) e2.state()).doubleValue() * 2,
694 void testSingleEnergyTimeSeries() {
695 BridgeImpl bi = new BridgeImpl(SolarForecastBindingConstants.SOLCAST_SITE, "bridge");
696 SolcastBridgeHandler scbh = new SolcastBridgeHandler(bi, new TimeZP());
698 CallbackMock cm = new CallbackMock();
699 scbh.setCallback(cm);
701 SolcastPlaneHandler scph1 = new SolcastPlaneMock(bi);
702 CallbackMock cm1 = new CallbackMock();
704 scph1.setCallback(cm1);
706 // simulate trigger of refresh job
709 TimeSeries ts1 = cm.getTimeSeries("solarforecast:sc-site:bridge:average#energy-estimate");
710 assertEquals(336, ts1.size(), "TimeSeries size");
711 Iterator<TimeSeries.Entry> iter1 = ts1.getStates().iterator();
712 while (iter1.hasNext()) {
713 TimeSeries.Entry e1 = iter1.next();
714 assertEquals("kWh", ((QuantityType<?>) e1.state()).getUnit().toString(), "Power Unit");