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.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;
28 import javax.measure.quantity.Energy;
29 import javax.measure.quantity.Power;
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;
54 * The {@link ForecastSolarTest} tests responses from forecast solar object
56 * @author Bernd Weymann - Initial contribution
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);
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";
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");
84 assertEquals(fo.getDayTotal(queryDateTime.toLocalDate()),
85 fo.getActualEnergyValue(queryDateTime) + fo.getRemainingProduction(queryDateTime), TOLERANCE,
86 "actual + remain = total");
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");
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");
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");
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));
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);
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");
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");
147 void testCornerCases() {
149 ForecastSolarObject fo = new ForecastSolarObject("fs-test");
150 ZonedDateTime query = LocalDateTime.of(2022, 7, 17, 16, 23).atZone(TEST_ZONE);
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());
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());
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());
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());
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());
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());
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());
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());
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());
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");
236 // valid object - query date one day too late
237 query = LocalDateTime.of(2022, 7, 19, 0, 0).atZone(TEST_ZONE);
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());
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());
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());
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());
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");
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");
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),
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");
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
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());
322 fo.getDay(queryDateTime.toLocalDate(), "optimistic");
324 } catch (IllegalArgumentException e) {
325 assertEquals("ForecastSolar doesn't accept arguments", e.getMessage(), "optimistic");
328 fo.getDay(queryDateTime.toLocalDate(), "pessimistic");
330 } catch (IllegalArgumentException e) {
331 assertEquals("ForecastSolar doesn't accept arguments", e.getMessage(), "pessimistic");
334 fo.getDay(queryDateTime.toLocalDate(), "total", "rubbish");
336 } catch (IllegalArgumentException e) {
337 assertEquals("ForecastSolar doesn't accept arguments", e.getMessage(), "rubbish");
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());
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());
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());
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);
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");
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,
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);
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();
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();
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");
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),
444 assertEquals(ld.atStartOfDay(ZoneId.of("UTC")).toInstant(), tsSite.getEnd().truncatedTo(ChronoUnit.DAYS),
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);
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();
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();
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),
483 assertEquals(ld.atStartOfDay(ZoneId.of("UTC")).toInstant(), sfa.getForecastEnd().truncatedTo(ChronoUnit.DAYS),
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);
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");
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,
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);
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");
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");
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");
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);
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");