]> git.basschouten.com Git - openhab-addons.git/blob
608bc02ebec7f0cd1881d50e7e2239f9b1bc3340
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.solarforecast;
14
15 import static org.junit.jupiter.api.Assertions.*;
16
17 import java.time.Instant;
18 import java.time.LocalDate;
19 import java.time.LocalDateTime;
20 import java.time.ZoneId;
21 import java.time.ZonedDateTime;
22 import java.time.format.DateTimeFormatter;
23 import java.time.temporal.ChronoUnit;
24 import java.util.Iterator;
25 import java.util.Optional;
26
27 import javax.measure.quantity.Energy;
28 import javax.measure.quantity.Power;
29
30 import org.eclipse.jdt.annotation.NonNullByDefault;
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.SolarForecastActions;
35 import org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarObject;
36 import org.openhab.binding.solarforecast.internal.forecastsolar.handler.ForecastSolarBridgeHandler;
37 import org.openhab.binding.solarforecast.internal.forecastsolar.handler.ForecastSolarPlaneHandler;
38 import org.openhab.binding.solarforecast.internal.forecastsolar.handler.ForecastSolarPlaneMock;
39 import org.openhab.binding.solarforecast.internal.solcast.SolcastObject.QueryMode;
40 import org.openhab.binding.solarforecast.internal.utils.Utils;
41 import org.openhab.core.library.types.PointType;
42 import org.openhab.core.library.types.QuantityType;
43 import org.openhab.core.library.unit.Units;
44 import org.openhab.core.thing.internal.BridgeImpl;
45 import org.openhab.core.types.State;
46 import org.openhab.core.types.TimeSeries;
47
48 /**
49  * The {@link ForecastSolarTest} tests responses from forecast solar object
50  *
51  * @author Bernd Weymann - Initial contribution
52  */
53 @NonNullByDefault
54 class ForecastSolarTest {
55     private static final double TOLERANCE = 0.001;
56     public static final ZoneId TEST_ZONE = ZoneId.of("Europe/Berlin");
57     public static final QuantityType<Power> POWER_UNDEF = Utils.getPowerState(-1);
58     public static final QuantityType<Energy> ENERGY_UNDEF = Utils.getEnergyState(-1);
59
60     public static final String TOO_EARLY_INDICATOR = "too early";
61     public static final String TOO_LATE_INDICATOR = "too late";
62     public static final String INVALID_RANGE_INDICATOR = "invalid time range";
63     public static final String NO_GORECAST_INDICATOR = "No forecast data";
64     public static final String DAY_MISSING_INDICATOR = "not available in forecast";
65
66     @Test
67     void testForecastObject() {
68         String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json");
69         ZonedDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 17, 00).atZone(TEST_ZONE);
70         ForecastSolarObject fo = new ForecastSolarObject("fs-test", content,
71                 queryDateTime.toInstant().plus(1, ChronoUnit.DAYS));
72         // "2022-07-17 21:32:00": 63583,
73         assertEquals(63.583, fo.getDayTotal(queryDateTime.toLocalDate()), TOLERANCE, "Total production");
74         // "2022-07-17 17:00:00": 52896,
75         assertEquals(52.896, fo.getActualEnergyValue(queryDateTime), TOLERANCE, "Current Production");
76         // 63583 - 52896 = 10687
77         assertEquals(10.687, fo.getRemainingProduction(queryDateTime), TOLERANCE, "Current Production");
78         // sum cross check
79         assertEquals(fo.getDayTotal(queryDateTime.toLocalDate()),
80                 fo.getActualEnergyValue(queryDateTime) + fo.getRemainingProduction(queryDateTime), TOLERANCE,
81                 "actual + remain = total");
82
83         queryDateTime = LocalDateTime.of(2022, 7, 18, 19, 00).atZone(TEST_ZONE);
84         // "2022-07-18 19:00:00": 63067,
85         assertEquals(63.067, fo.getActualEnergyValue(queryDateTime), TOLERANCE, "Actual production");
86         // "2022-07-18 21:31:00": 65554
87         assertEquals(65.554, fo.getDayTotal(queryDateTime.toLocalDate()), TOLERANCE, "Total production");
88     }
89
90     @Test
91     void testActualPower() {
92         String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json");
93         ZonedDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 10, 00).atZone(TEST_ZONE);
94         ForecastSolarObject fo = new ForecastSolarObject("fs-test", content,
95                 queryDateTime.toInstant().plus(1, ChronoUnit.DAYS));
96         // "2022-07-17 10:00:00": 4874,
97         assertEquals(4.874, fo.getActualPowerValue(queryDateTime), TOLERANCE, "Actual estimation");
98
99         queryDateTime = LocalDateTime.of(2022, 7, 18, 14, 00).atZone(TEST_ZONE);
100         // "2022-07-18 14:00:00": 7054,
101         assertEquals(7.054, fo.getActualPowerValue(queryDateTime), TOLERANCE, "Actual estimation");
102     }
103
104     @Test
105     void testInterpolation() {
106         String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json");
107         ZonedDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 0).atZone(TEST_ZONE);
108         ForecastSolarObject fo = new ForecastSolarObject("fs-test", content,
109                 queryDateTime.toInstant().plus(1, ChronoUnit.DAYS));
110
111         // test steady value increase
112         double previousValue = 0;
113         for (int i = 0; i < 60; i++) {
114             queryDateTime = queryDateTime.plusMinutes(1);
115             assertTrue(previousValue < fo.getActualEnergyValue(queryDateTime));
116             previousValue = fo.getActualEnergyValue(queryDateTime);
117         }
118
119         queryDateTime = LocalDateTime.of(2022, 7, 18, 6, 23).atZone(TEST_ZONE);
120         // "2022-07-18 06:00:00": 132,
121         // "2022-07-18 07:00:00": 1188,
122         // 1188 - 132 = 1056 | 1056 * 23 / 60 = 404 | 404 + 131 = 535
123         assertEquals(0.535, fo.getActualEnergyValue(queryDateTime), 0.002, "Actual estimation");
124     }
125
126     @Test
127     void testForecastSum() {
128         String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json");
129         ZonedDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 23).atZone(TEST_ZONE);
130         ForecastSolarObject fo = new ForecastSolarObject("fs-test", content,
131                 queryDateTime.toInstant().plus(1, ChronoUnit.DAYS));
132         QuantityType<Energy> actual = QuantityType.valueOf(0, Units.KILOWATT_HOUR);
133         QuantityType<Energy> st = Utils.getEnergyState(fo.getActualEnergyValue(queryDateTime));
134         assertTrue(st instanceof QuantityType);
135         actual = actual.add(st);
136         assertEquals(49.431, actual.floatValue(), TOLERANCE, "Current Production");
137         actual = actual.add(st);
138         assertEquals(98.862, actual.floatValue(), TOLERANCE, "Doubled Current Production");
139     }
140
141     @Test
142     void testCornerCases() {
143         // invalid object
144         ForecastSolarObject fo = new ForecastSolarObject("fs-test");
145         ZonedDateTime query = LocalDateTime.of(2022, 7, 17, 16, 23).atZone(TEST_ZONE);
146         try {
147             double d = fo.getActualEnergyValue(query);
148             fail("Exception expected instead of " + d);
149         } catch (SolarForecastException sfe) {
150             String message = sfe.getMessage();
151             assertNotNull(message);
152             assertTrue(message.contains(INVALID_RANGE_INDICATOR),
153                     "Expected: " + INVALID_RANGE_INDICATOR + " Received: " + sfe.getMessage());
154         }
155         try {
156             double d = fo.getRemainingProduction(query);
157             fail("Exception expected instead of " + d);
158         } catch (SolarForecastException sfe) {
159             String message = sfe.getMessage();
160             assertNotNull(message);
161             assertTrue(message.contains(NO_GORECAST_INDICATOR),
162                     "Expected: " + NO_GORECAST_INDICATOR + " Received: " + sfe.getMessage());
163         }
164         try {
165             double d = fo.getDayTotal(query.toLocalDate());
166             fail("Exception expected instead of " + d);
167         } catch (SolarForecastException sfe) {
168             String message = sfe.getMessage();
169             assertNotNull(message);
170             assertTrue(message.contains(NO_GORECAST_INDICATOR),
171                     "Expected: " + NO_GORECAST_INDICATOR + " Received: " + sfe.getMessage());
172         }
173         try {
174             double d = fo.getDayTotal(query.plusDays(1).toLocalDate());
175             fail("Exception expected instead of " + d);
176         } catch (SolarForecastException sfe) {
177             String message = sfe.getMessage();
178             assertNotNull(message);
179             assertTrue(message.contains(NO_GORECAST_INDICATOR),
180                     "Expected: " + NO_GORECAST_INDICATOR + " Received: " + sfe.getMessage());
181         }
182
183         // valid object - query date one day too early
184         String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json");
185         query = LocalDateTime.of(2022, 7, 16, 23, 59).atZone(TEST_ZONE);
186         fo = new ForecastSolarObject("fs-test", content, query.toInstant());
187         try {
188             double d = fo.getActualEnergyValue(query);
189             fail("Exception expected instead of " + d);
190         } catch (SolarForecastException sfe) {
191             String message = sfe.getMessage();
192             assertNotNull(message);
193             assertTrue(message.contains(TOO_EARLY_INDICATOR),
194                     "Expected: " + TOO_EARLY_INDICATOR + " Received: " + sfe.getMessage());
195         }
196         try {
197             double d = fo.getRemainingProduction(query);
198             fail("Exception expected instead of " + d);
199         } catch (SolarForecastException sfe) {
200             String message = sfe.getMessage();
201             assertNotNull(message);
202             assertTrue(message.contains(DAY_MISSING_INDICATOR),
203                     "Expected: " + DAY_MISSING_INDICATOR + " Received: " + sfe.getMessage());
204         }
205         try {
206             double d = fo.getActualPowerValue(query);
207             fail("Exception expected instead of " + d);
208         } catch (SolarForecastException sfe) {
209             String message = sfe.getMessage();
210             assertNotNull(message);
211             assertTrue(message.contains(TOO_EARLY_INDICATOR),
212                     "Expected: " + TOO_EARLY_INDICATOR + " Received: " + sfe.getMessage());
213         }
214         try {
215             double d = fo.getDayTotal(query.toLocalDate());
216             fail("Exception expected instead of " + d);
217         } catch (SolarForecastException sfe) {
218             String message = sfe.getMessage();
219             assertNotNull(message);
220             assertTrue(message.contains(DAY_MISSING_INDICATOR),
221                     "Expected: " + DAY_MISSING_INDICATOR + " Received: " + sfe.getMessage());
222         }
223
224         // one minute later we reach a valid date
225         query = query.plusMinutes(1);
226         assertEquals(63.583, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope");
227         assertEquals(0.0, fo.getActualEnergyValue(query), TOLERANCE, "Actual out of scope");
228         assertEquals(63.583, fo.getRemainingProduction(query), TOLERANCE, "Remain out of scope");
229         assertEquals(0.0, fo.getActualPowerValue(query), TOLERANCE, "Remain out of scope");
230
231         // valid object - query date one day too late
232         query = LocalDateTime.of(2022, 7, 19, 0, 0).atZone(TEST_ZONE);
233         try {
234             double d = fo.getActualEnergyValue(query);
235             fail("Exception expected instead of " + d);
236         } catch (SolarForecastException sfe) {
237             String message = sfe.getMessage();
238             assertNotNull(message);
239             assertTrue(message.contains(TOO_LATE_INDICATOR),
240                     "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage());
241         }
242         try {
243             double d = fo.getRemainingProduction(query);
244             fail("Exception expected instead of " + d);
245         } catch (SolarForecastException sfe) {
246             String message = sfe.getMessage();
247             assertNotNull(message);
248             assertTrue(message.contains(DAY_MISSING_INDICATOR),
249                     "Expected: " + DAY_MISSING_INDICATOR + " Received: " + sfe.getMessage());
250         }
251         try {
252             double d = fo.getActualPowerValue(query);
253             fail("Exception expected instead of " + d);
254         } catch (SolarForecastException sfe) {
255             String message = sfe.getMessage();
256             assertNotNull(message);
257             assertTrue(message.contains(TOO_LATE_INDICATOR),
258                     "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage());
259         }
260         try {
261             double d = fo.getDayTotal(query.toLocalDate());
262             fail("Exception expected instead of " + d);
263         } catch (SolarForecastException sfe) {
264             String message = sfe.getMessage();
265             assertNotNull(message);
266             assertTrue(message.contains(DAY_MISSING_INDICATOR),
267                     "Expected: " + DAY_MISSING_INDICATOR + " Received: " + sfe.getMessage());
268         }
269
270         // one minute earlier we reach a valid date
271         query = query.minusMinutes(1);
272         assertEquals(65.554, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope");
273         assertEquals(65.554, fo.getActualEnergyValue(query), TOLERANCE, "Actual out of scope");
274         assertEquals(0.0, fo.getRemainingProduction(query), TOLERANCE, "Remain out of scope");
275         assertEquals(0.0, fo.getActualPowerValue(query), TOLERANCE, "Remain out of scope");
276
277         // test times between 2 dates
278         query = LocalDateTime.of(2022, 7, 17, 23, 59).atZone(TEST_ZONE);
279         assertEquals(63.583, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope");
280         assertEquals(63.583, fo.getActualEnergyValue(query), TOLERANCE, "Actual out of scope");
281         assertEquals(0.0, fo.getRemainingProduction(query), TOLERANCE, "Remain out of scope");
282         assertEquals(0.0, fo.getActualPowerValue(query), TOLERANCE, "Remain out of scope");
283         query = query.plusMinutes(1);
284         assertEquals(65.554, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope");
285         assertEquals(0.0, fo.getActualEnergyValue(query), TOLERANCE, "Actual out of scope");
286         assertEquals(65.554, fo.getRemainingProduction(query), TOLERANCE, "Remain out of scope");
287         assertEquals(0.0, fo.getActualPowerValue(query), TOLERANCE, "Remain out of scope");
288     }
289
290     @Test
291     void testExceptions() {
292         String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json");
293         ZonedDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 23).atZone(TEST_ZONE);
294         ForecastSolarObject fo = new ForecastSolarObject("fs-test", content, queryDateTime.toInstant());
295         assertEquals("2022-07-17T05:31:00",
296                 fo.getForecastBegin().atZone(TEST_ZONE).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
297                 "Forecast begin");
298         assertEquals("2022-07-18T21:31:00",
299                 fo.getForecastEnd().atZone(TEST_ZONE).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), "Forecast end");
300         assertEquals(QuantityType.valueOf(63.583, Units.KILOWATT_HOUR).toString(),
301                 fo.getDay(queryDateTime.toLocalDate()).toFullString(), "Actual out of scope");
302
303         queryDateTime = LocalDateTime.of(2022, 7, 10, 0, 0).atZone(TEST_ZONE);
304         // "watt_hours_day": {
305         // "2022-07-17": 63583,
306         // "2022-07-18": 65554
307         // }
308         try {
309             fo.getEnergy(queryDateTime.toInstant(), queryDateTime.plusDays(2).toInstant());
310             fail("Too early exception missing");
311         } catch (SolarForecastException sfe) {
312             String message = sfe.getMessage();
313             assertNotNull(message);
314             assertTrue(message.contains("not available"), "not available expected: " + sfe.getMessage());
315         }
316         try {
317             fo.getDay(queryDateTime.toLocalDate(), "optimistic");
318             fail();
319         } catch (IllegalArgumentException e) {
320             assertEquals("ForecastSolar doesn't accept arguments", e.getMessage(), "optimistic");
321         }
322         try {
323             fo.getDay(queryDateTime.toLocalDate(), "pessimistic");
324             fail();
325         } catch (IllegalArgumentException e) {
326             assertEquals("ForecastSolar doesn't accept arguments", e.getMessage(), "pessimistic");
327         }
328         try {
329             fo.getDay(queryDateTime.toLocalDate(), "total", "rubbish");
330             fail();
331         } catch (IllegalArgumentException e) {
332             assertEquals("ForecastSolar doesn't accept arguments", e.getMessage(), "rubbish");
333         }
334     }
335
336     @Test
337     void testTimeSeries() {
338         String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json");
339         ZonedDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 23).atZone(TEST_ZONE);
340         ForecastSolarObject fo = new ForecastSolarObject("fs-test", content, queryDateTime.toInstant());
341
342         TimeSeries powerSeries = fo.getPowerTimeSeries(QueryMode.Average);
343         assertEquals(36, powerSeries.size()); // 18 values each day for 2 days
344         powerSeries.getStates().forEachOrdered(entry -> {
345             State s = entry.state();
346             assertTrue(s instanceof QuantityType<?>);
347             assertEquals("kW", ((QuantityType<?>) s).getUnit().toString());
348         });
349
350         TimeSeries energySeries = fo.getEnergyTimeSeries(QueryMode.Average);
351         assertEquals(36, energySeries.size());
352         energySeries.getStates().forEachOrdered(entry -> {
353             State s = entry.state();
354             assertTrue(s instanceof QuantityType<?>);
355             assertEquals("kWh", ((QuantityType<?>) s).getUnit().toString());
356         });
357     }
358
359     @Test
360     void testPowerTimeSeries() {
361         ForecastSolarBridgeHandler fsbh = new ForecastSolarBridgeHandler(
362                 new BridgeImpl(SolarForecastBindingConstants.FORECAST_SOLAR_SITE, "bridge"),
363                 Optional.of(PointType.valueOf("1,2")));
364         CallbackMock cm = new CallbackMock();
365         fsbh.setCallback(cm);
366
367         String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json");
368         ForecastSolarObject fso1 = new ForecastSolarObject("fs-test", content, Instant.now().plus(1, ChronoUnit.DAYS));
369         ForecastSolarPlaneHandler fsph1 = new ForecastSolarPlaneMock(fso1);
370         fsbh.addPlane(fsph1);
371         fsbh.forecastUpdate();
372         TimeSeries ts1 = cm.getTimeSeries("solarforecast:fs-site:bridge:power-estimate");
373
374         ForecastSolarPlaneHandler fsph2 = new ForecastSolarPlaneMock(fso1);
375         fsbh.addPlane(fsph2);
376         fsbh.forecastUpdate();
377         TimeSeries ts2 = cm.getTimeSeries("solarforecast:fs-site:bridge:power-estimate");
378         Iterator<TimeSeries.Entry> iter1 = ts1.getStates().iterator();
379         Iterator<TimeSeries.Entry> iter2 = ts2.getStates().iterator();
380         while (iter1.hasNext()) {
381             TimeSeries.Entry e1 = iter1.next();
382             TimeSeries.Entry e2 = iter2.next();
383             assertEquals("kW", ((QuantityType<?>) e1.state()).getUnit().toString(), "Power Unit");
384             assertEquals("kW", ((QuantityType<?>) e2.state()).getUnit().toString(), "Power Unit");
385             assertEquals(((QuantityType<?>) e1.state()).doubleValue(), ((QuantityType<?>) e2.state()).doubleValue() / 2,
386                     0.1, "Power Value");
387         }
388     }
389
390     @Test
391     void testCommonForecastStartEnd() {
392         ForecastSolarBridgeHandler fsbh = new ForecastSolarBridgeHandler(
393                 new BridgeImpl(SolarForecastBindingConstants.FORECAST_SOLAR_SITE, "bridge"),
394                 Optional.of(PointType.valueOf("1,2")));
395         CallbackMock cmSite = new CallbackMock();
396         fsbh.setCallback(cmSite);
397         String contentOne = FileReader.readFileInString("src/test/resources/forecastsolar/result.json");
398         ForecastSolarObject fso1One = new ForecastSolarObject("fs-test", contentOne,
399                 Instant.now().plus(1, ChronoUnit.DAYS));
400         ForecastSolarPlaneHandler fsph1 = new ForecastSolarPlaneMock(fso1One);
401         fsbh.addPlane(fsph1);
402         fsbh.forecastUpdate();
403
404         String contentTwo = FileReader.readFileInString("src/test/resources/forecastsolar/resultNextDay.json");
405         ForecastSolarObject fso1Two = new ForecastSolarObject("fs-plane", contentTwo,
406                 Instant.now().plus(1, ChronoUnit.DAYS));
407         ForecastSolarPlaneHandler fsph2 = new ForecastSolarPlaneMock(fso1Two);
408         CallbackMock cmPlane = new CallbackMock();
409         fsph2.setCallback(cmPlane);
410         ((ForecastSolarPlaneMock) fsph2).updateForecast(fso1Two);
411         fsbh.addPlane(fsph2);
412         fsbh.forecastUpdate();
413
414         TimeSeries tsPlaneOne = cmPlane.getTimeSeries("test::plane:power-estimate");
415         TimeSeries tsSite = cmSite.getTimeSeries("solarforecast:fs-site:bridge:power-estimate");
416         Iterator<TimeSeries.Entry> planeIter = tsPlaneOne.getStates().iterator();
417         Iterator<TimeSeries.Entry> siteIter = tsSite.getStates().iterator();
418         while (siteIter.hasNext()) {
419             TimeSeries.Entry planeEntry = planeIter.next();
420             TimeSeries.Entry siteEntry = siteIter.next();
421             assertEquals("kW", ((QuantityType<?>) planeEntry.state()).getUnit().toString(), "Power Unit");
422             assertEquals("kW", ((QuantityType<?>) siteEntry.state()).getUnit().toString(), "Power Unit");
423             assertEquals(((QuantityType<?>) planeEntry.state()).doubleValue(),
424                     ((QuantityType<?>) siteEntry.state()).doubleValue() / 2, 0.1, "Power Value");
425         }
426         // only one day shall be reported which is available in both planes
427         LocalDate ld = LocalDate.of(2022, 7, 18);
428         assertEquals(ld.atStartOfDay(ZoneId.of("UTC")).toInstant(), tsSite.getBegin().truncatedTo(ChronoUnit.DAYS),
429                 "TimeSeries start");
430         assertEquals(ld.atStartOfDay(ZoneId.of("UTC")).toInstant(), tsSite.getEnd().truncatedTo(ChronoUnit.DAYS),
431                 "TimeSeries end");
432     }
433
434     @Test
435     void testActions() {
436         ForecastSolarBridgeHandler fsbh = new ForecastSolarBridgeHandler(
437                 new BridgeImpl(SolarForecastBindingConstants.FORECAST_SOLAR_SITE, "bridge"),
438                 Optional.of(PointType.valueOf("1,2")));
439         CallbackMock cmSite = new CallbackMock();
440         fsbh.setCallback(cmSite);
441         String contentOne = FileReader.readFileInString("src/test/resources/forecastsolar/result.json");
442         ForecastSolarObject fso1One = new ForecastSolarObject("fs-test", contentOne,
443                 Instant.now().plus(1, ChronoUnit.DAYS));
444         ForecastSolarPlaneHandler fsph1 = new ForecastSolarPlaneMock(fso1One);
445         fsbh.addPlane(fsph1);
446         fsbh.forecastUpdate();
447
448         String contentTwo = FileReader.readFileInString("src/test/resources/forecastsolar/resultNextDay.json");
449         ForecastSolarObject fso1Two = new ForecastSolarObject("fs-plane", contentTwo,
450                 Instant.now().plus(1, ChronoUnit.DAYS));
451         ForecastSolarPlaneHandler fsph2 = new ForecastSolarPlaneMock(fso1Two);
452         CallbackMock cmPlane = new CallbackMock();
453         fsph2.setCallback(cmPlane);
454         ((ForecastSolarPlaneMock) fsph2).updateForecast(fso1Two);
455         fsbh.addPlane(fsph2);
456         fsbh.forecastUpdate();
457
458         SolarForecastActions sfa = new SolarForecastActions();
459         sfa.setThingHandler(fsbh);
460         // only one day shall be reported which is available in both planes
461         LocalDate ld = LocalDate.of(2022, 7, 18);
462         assertEquals(ld.atStartOfDay(ZoneId.of("UTC")).toInstant(), sfa.getForecastBegin().truncatedTo(ChronoUnit.DAYS),
463                 "TimeSeries start");
464         assertEquals(ld.atStartOfDay(ZoneId.of("UTC")).toInstant(), sfa.getForecastEnd().truncatedTo(ChronoUnit.DAYS),
465                 "TimeSeries end");
466     }
467
468     @Test
469     void testEnergyTimeSeries() {
470         ForecastSolarBridgeHandler fsbh = new ForecastSolarBridgeHandler(
471                 new BridgeImpl(SolarForecastBindingConstants.FORECAST_SOLAR_SITE, "bridge"),
472                 Optional.of(PointType.valueOf("1,2")));
473         CallbackMock cm = new CallbackMock();
474         fsbh.setCallback(cm);
475
476         String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json");
477         ForecastSolarObject fso1 = new ForecastSolarObject("fs-test", content, Instant.now().plus(1, ChronoUnit.DAYS));
478         ForecastSolarPlaneHandler fsph1 = new ForecastSolarPlaneMock(fso1);
479         fsbh.addPlane(fsph1);
480         fsbh.forecastUpdate();
481         TimeSeries ts1 = cm.getTimeSeries("solarforecast:fs-site:bridge:energy-estimate");
482
483         ForecastSolarPlaneHandler fsph2 = new ForecastSolarPlaneMock(fso1);
484         fsbh.addPlane(fsph2);
485         fsbh.forecastUpdate();
486         TimeSeries ts2 = cm.getTimeSeries("solarforecast:fs-site:bridge:energy-estimate");
487         Iterator<TimeSeries.Entry> iter1 = ts1.getStates().iterator();
488         Iterator<TimeSeries.Entry> iter2 = ts2.getStates().iterator();
489         while (iter1.hasNext()) {
490             TimeSeries.Entry e1 = iter1.next();
491             TimeSeries.Entry e2 = iter2.next();
492             assertEquals("kWh", ((QuantityType<?>) e1.state()).getUnit().toString(), "Power Unit");
493             assertEquals("kWh", ((QuantityType<?>) e2.state()).getUnit().toString(), "Power Unit");
494             assertEquals(((QuantityType<?>) e1.state()).doubleValue(), ((QuantityType<?>) e2.state()).doubleValue() / 2,
495                     0.1, "Power Value");
496         }
497     }
498 }