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