From 71d335df9e86d6dc4ed8076721ccf2d676367b01 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Thu, 2 May 2024 20:26:09 +0200 Subject: [PATCH] [solarforecast] Initial contribution (#13308) Signed-off-by: Bernd Weymann --- bom/openhab-addons/pom.xml | 5 + .../org.openhab.binding.solarforecast/NOTICE | 13 + .../README.md | 356 +++ .../doc/SolcastCumulated.png | Bin 0 -> 64475 bytes .../doc/SolcastPower.png | Bin 0 -> 83573 bytes .../org.openhab.binding.solarforecast/pom.xml | 26 + .../src/main/feature/feature.xml | 9 + .../SolarForecastBindingConstants.java | 59 + .../internal/SolarForecastException.java | 30 + .../internal/SolarForecastHandlerFactory.java | 83 + .../internal/actions/SolarForecast.java | 110 + .../actions/SolarForecastActions.java | 195 ++ .../actions/SolarForecastProvider.java | 33 + .../forecastsolar/ForecastSolarObject.java | 345 +++ .../ForecastSolarBridgeConfiguration.java | 28 + .../ForecastSolarPlaneConfiguration.java | 32 + .../handler/ForecastSolarBridgeHandler.java | 235 ++ .../handler/ForecastSolarPlaneHandler.java | 225 ++ .../internal/solcast/SolcastConstants.java | 34 + .../internal/solcast/SolcastObject.java | 498 ++++ .../config/SolcastBridgeConfiguration.java | 27 + .../config/SolcastPlaneConfiguration.java | 27 + .../solcast/handler/SolcastBridgeHandler.java | 268 ++ .../solcast/handler/SolcastPlaneHandler.java | 254 ++ .../solarforecast/internal/utils/Utils.java | 106 + .../src/main/resources/OH-INF/addon/addon.xml | 10 + .../OH-INF/config/fs-plane-config.xml | 43 + .../OH-INF/config/fs-site-config.xml | 22 + .../OH-INF/config/sc-plane-config.xml | 18 + .../OH-INF/config/sc-site-config.xml | 18 + .../OH-INF/i18n/solarforecast.properties | 108 + .../resources/OH-INF/thing/average-group.xml | 18 + .../resources/OH-INF/thing/channel-types.xml | 48 + .../resources/OH-INF/thing/fs-plane-type.xml | 27 + .../resources/OH-INF/thing/fs-site-type.xml | 22 + .../OH-INF/thing/optimistic-group.xml | 18 + .../OH-INF/thing/pessimistic-group.xml | 18 + .../main/resources/OH-INF/thing/raw-group.xml | 13 + .../resources/OH-INF/thing/sc-plane-type.xml | 24 + .../resources/OH-INF/thing/sc-site-type.xml | 18 + .../binding/solarforecast/CallbackMock.java | 134 + .../binding/solarforecast/FileReader.java | 48 + .../solarforecast/ForecastSolarTest.java | 498 ++++ .../binding/solarforecast/SolcastTest.java | 717 +++++ .../openhab/binding/solarforecast/TimeZP.java | 32 + .../handler/ForecastSolarPlaneMock.java | 45 + .../solcast/handler/SolcastPlaneMock.java | 67 + .../test/resources/forecastsolar/result.json | 100 + .../forecastsolar/resultNextDay.json | 100 + .../resources/solcast/estimated-actuals.json | 1684 ++++++++++++ .../src/test/resources/solcast/forecasts.json | 2356 +++++++++++++++++ bundles/pom.xml | 1 + 52 files changed, 9205 insertions(+) create mode 100644 bundles/org.openhab.binding.solarforecast/NOTICE create mode 100644 bundles/org.openhab.binding.solarforecast/README.md create mode 100644 bundles/org.openhab.binding.solarforecast/doc/SolcastCumulated.png create mode 100644 bundles/org.openhab.binding.solarforecast/doc/SolcastPower.png create mode 100644 bundles/org.openhab.binding.solarforecast/pom.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastBindingConstants.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastException.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastHandlerFactory.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecast.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecastActions.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecastProvider.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarObject.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/config/ForecastSolarBridgeConfiguration.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/config/ForecastSolarPlaneConfiguration.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarBridgeHandler.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarPlaneHandler.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastConstants.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastObject.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/config/SolcastBridgeConfiguration.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/config/SolcastPlaneConfiguration.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastBridgeHandler.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastPlaneHandler.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/utils/Utils.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/addon/addon.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-config.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-site-config.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-config.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-site-config.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast.properties create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/average-group.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/channel-types.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-plane-type.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-site-type.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/optimistic-group.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/pessimistic-group.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/raw-group.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-plane-type.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-site-type.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/CallbackMock.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/FileReader.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/ForecastSolarTest.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastTest.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/TimeZP.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarPlaneMock.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastPlaneMock.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/result.json create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/resultNextDay.json create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/resources/solcast/estimated-actuals.json create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/resources/solcast/forecasts.json diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 96a0264b851..3db66110ec2 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1641,6 +1641,11 @@ org.openhab.binding.solaredge ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.solarforecast + ${project.version} + org.openhab.addons.bundles org.openhab.binding.solarlog diff --git a/bundles/org.openhab.binding.solarforecast/NOTICE b/bundles/org.openhab.binding.solarforecast/NOTICE new file mode 100644 index 00000000000..38d625e3492 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md new file mode 100644 index 00000000000..1de5035db8c --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -0,0 +1,356 @@ +# SolarForecast Binding + +This binding provides data from Solar Forecast services. +Use it to estimate your daily production, plan electric consumers like Electric Vehicle charging, heating or HVAC. +Look ahead the next days in order to identify surplus / shortages in your energy planning. + +Supported Services + +- [Solcast](https://solcast.com/) + - Free [Hobbyist Plan](https://toolkit.solcast.com.au/register/hobbyist) with registration +- [Forecast.Solar](https://forecast.solar/) + - Public, Personal and Professional [plans](https://forecast.solar/#accounts) available + +Display Power values of Forecast and PV Inverter items + + + +Display Energy values of Forecast and PV inverter items +Yellow line shows *Daily Total Forecast*. + + + +## Supported Things + +Each service needs one `xx-site` for your location and at least one photovoltaic `xx-plane`. + +| Name | Thing Type ID | +|-----------------------------------|---------------| +| Solcast service site definition | sc-site | +| Solcast PV Plane | sc-plane | +| Forecast Solar site location | fs-site | +| Forecast Solar PV Plane | fs-plane | + +## Solcast Configuration + +[Solcast service](https://solcast.com/) requires a personal registration with an e-mail address. +A free version for your personal home PV system is available in [Hobbyist Plan](https://toolkit.solcast.com.au/register/hobbyist) +You need to configure your home photovoltaic system within the web interface. +The `resourceId` for each PV plane is provided afterwards. + +In order to receive proper timestamps double check your time zone in *openHAB - Settings - Regional Settings*. +Correct time zone is necessary to show correct forecast times in UI. + +### Solcast Bridge Configuration + +| Name | Type | Description | Default | Required | Advanced | +|------------------------|---------|---------------------------------------|-------------|----------|----------| +| apiKey | text | API Key | N/A | yes | no | +| timeZone | text | Time Zone of forecast location | empty | no | yes | + +`apiKey` can be obtained in your [Account Settings](https://toolkit.solcast.com.au/account) + +`timeZone` can be left empty to evaluate Regional Settings of your openHAB installation. +See [DateTime](#date-time) section for more information. + +### Solcast Plane Configuration + +| Name | Type | Description | Default | Required | Advanced | +|-----------------|---------|--------------------------------------------------------|-----------------|----------|----------| +| resourceId | text | Resource Id of Solcast rooftop site | N/A | yes | no | +| refreshInterval | integer | Forecast Refresh Interval in minutes | 120 | yes | no | + +`resourceId` for each plane can be obtained in your [Rooftop Sites](https://toolkit.solcast.com.au/rooftop-sites) + +`refreshInterval` of forecast data needs to respect the throttling of the Solcast service. +If you have 25 free calls per day, each plane needs 2 calls per update a refresh interval of 120 minutes will result in 24 calls per day. + +## Solcast Channels + +Each `sc-plane` reports its own values including a `json` channel holding JSON content. +The `sc-site` bridge sums up all attached `sc-plane` values and provides total forecast for your home location. + +Channels are covering today's actual data with current, remaining and today's total prediction. +Forecasts are delivered up to 6 days in advance. +Scenarios are clustered in groups: + +- `average` scenario +- `pessimistic` scenario: 10th percentile +- `optimistic` scenario: 90th percentile + +| Channel | Type | Unit | Description | Advanced | +|-------------------------|---------------|------|-------------------------------------------------|----------| +| power-estimate | Number:Power | W | Power forecast for next hours/days | no | +| energy-estimate | Number:Energy | kWh | Energy forecast for next hours/days | no | +| power-actual | Number:Power | W | Power prediction for this moment | no | +| energy-actual | Number:Energy | kWh | Today's forecast till now | no | +| energy-remain | Number:Energy | kWh | Today's remaining forecast till sunset | no | +| energy-today | Number:Energy | kWh | Today's forecast in total | no | +| json | String | - | Plain JSON response without conversions | yes | + +## ForecastSolar Configuration + +[ForecastSolar service](https://forecast.solar/) provides a [public free](https://forecast.solar/#accounts) plan. +You can try it without any registration or other preconditions. + +### ForecastSolar Bridge Configuration + +| Name | Type | Description | Default | Required | +|------------------------|---------|---------------------------------------|--------------|----------| +| location | text | Location of Photovoltaic system. | empty | no | +| apiKey | text | API Key | N/A | no | + +`location` defines latitude, longitude values of your PV system. +In case of empty the location configured in openHAB is obtained. + +`apiKey` can be given in case you subscribed to a paid plan. + +### ForecastSolar Plane Configuration + +| Name | Type | Description | Default | Required | Advanced | +|-----------------|---------|------------------------------------------------------------------------------|---------|----------|----------| +| refreshInterval | integer | Forecast Refresh Interval in minutes | 30 | yes | false | +| declination | integer | Plane Declination: 0 for horizontal till 90 for vertical declination | N/A | yes | false | +| azimuth | integer | Plane Azimuth: -180 = north, -90 = east, 0 = south, 90 = west, 180 = north | N/A | yes | false | +| kwp | decimal | Installed Kilowatt Peak | N/A | yes | false | +| dampAM | decimal | Damping factor of morning hours | N/A | no | true | +| dampPM | decimal | Damping factor of evening hours | N/A | no | true | +| horizon | text | Horizon definition as comma separated integer values | N/A | no | true | + +`refreshInterval` of forecast data needs to respect the throttling of the ForecastSolar service. +12 calls per hour allowed from your caller IP address so for 2 planes lowest possible refresh rate is 10 minutes. + +#### Advanced Configuration + +Advanced configuration parameters are available to *fine tune* your forecast data. +Read linked documentation in order to know what you're doing. + +[Damping factors](https://doc.forecast.solar/doku.php?id=damping) for morning and evening. + +[Horizon information](https://doc.forecast.solar/doku.php?id=api) as comma-separated integer list. +This configuration item is aimed to expert users. +You need to understand the [horizon concept](https://joint-research-centre.ec.europa.eu/pvgis-photovoltaic-geographical-information-system/getting-started-pvgis/pvgis-user-manual_en#ref-2-using-horizon-information). +Shadow obstacles like mountains, hills, buildings can be expressed here. +First step can be a download from [PVGIS tool](https://re.jrc.ec.europa.eu/pvg_tools/en/) and downloading the *terrain shadows*. +But it doesn't fit 100% to the required configuration. +Currently there's no tool available which is providing the configuration information 1 to 1. +So you need to know what you're doing. + +## ForecastSolar Channels + +Each `fs-plane` reports its own values including a `json` channel holding JSON content. +The `fs-site` bridge sums up all attached `fs-plane` values and provides the total forecast for your home location. + +Channels are covering today's actual data with current, remaining and total prediction. +Forecasts are delivered up to 3 days for paid personal plans. + +| Channel | Type | Unit | Description | Advanced | +|-------------------------|---------------|------|-------------------------------------------------|----------| +| power-estimate | Number:Power | W | Power forecast for next hours/days | no | +| energy-estimate | Number:Energy | kWh | Energy forecast for next hours/days | no | +| power-actual | Number:Power | W | Power prediction for this moment | no | +| energy-actual | Number:Energy | kWh | Today's forecast till now | no | +| energy-remain | Number:Energy | kWh | Today's remaining forecast till sunset | no | +| energy-today | Number:Energy | kWh | Today's forecast in total | no | +| json | String | - | Plain JSON response without conversions | yes | + +## Thing Actions + +All things `sc-site`, `sc-plane`, `fs-site` and `fs-plane` are providing the same Actions. +Channels are providing actual forecast data and daily forecasts in future. +Actions provides an interface to execute more sophisticated handling in rules. +You can execute this for each `xx-plane` for specific plane values or `xx-site` to sum up all attached planes. + +See [Date Time](#date-time) section for more information. +Double check your time zone in *openHAB - Settings - Regional Settings* which is crucial for calculation. + +### `getForecastBegin` + +Returns `Instant` of the earliest possible forecast data available. +It's located in the past, e.g. Solcast provides data from the last 7 days. +`Instant.MAX` is returned in case of no forecast data is available. + +### `getForecastEnd` + +Returns `Instant` of the latest possible forecast data available. +`Instant.MIN` is returned in case of no forecast data is available. + +### `getPower` + +| Parameter | Type | Description | +|-----------|---------------|--------------------------------------------------------------------------------------------| +| timestamp | Instant | Timestamp of power query | +| mode | String | Choose `average`, `optimistic` or `pessimistic` to select forecast scenario. Only Solcast. | + +Returns `QuantityType` at the given `Instant` timestamp. +Respect `getForecastBegin` and `getForecastEnd` to get a valid value. + +Check log or catch exceptions for error handling + +- `IllegalArgumentException` thrown in case of problems with call arguments +- `SolarForecastException` thrown in case of problems with timestamp and available forecast data + +### `getDay` + +| Parameter | Type | Description | +|-----------|---------------|--------------------------------------------------------------------------------------------| +| date | LocalDate | Date of the day | +| mode | String | Choose `average`, `optimistic` or `pessimistic` to select forecast scenario. Only Solcast. | + +Returns `QuantityType` at the given `localDate`. +Respect `getForecastBegin` and `getForecastEnd` to avoid ambiguous values. + +Check log or catch exceptions for error handling + +- `IllegalArgumentException` thrown in case of problems with call arguments +- `SolarForecastException` thrown in case of problems with timestamp and available forecast data + +### `getEnergy` + +| Parameter | Type | Description | +|-----------------|---------------|--------------------------------------------------------------------------------------------------------------| +| startTimestamp | Instant | Start timestamp of energy query | +| endTimestamp | Instant | End timestamp of energy query | +| mode | String | Choose `optimistic` or `pessimistic` to get values for a positive or negative future scenario. Only Solcast. | + +Returns `QuantityType` between the timestamps `startTimestamp` and `endTimestamp`. +Respect `getForecastBegin` and `getForecastEnd` to avoid ambiguous values. + +Check log or catch exceptions for error handling + +- `IllegalArgumentException` thrown in case of problems with call arguments +- `SolarForecastException` thrown in case of problems with timestamp and available forecast data + +## Date Time + +Each forecast is bound to a certain location which automatically defines the time zone. +Most common use case is forecast and your location are matching the same time zone. +Action interface is using `Instant` as timestamps which enables you translating to any time zone. +This allows you with an easy conversion to query also foreign forecast locations. + +Examples are showing + +- how to translate `Instant` to `ZonedDateTime` objects and +- how to translate `ZonedDateTime` to `Instant` objects + +## Example + +Example is based on Forecast.Solar service without any registration. +Exchange the configuration data in [thing file](#thing-file) and you're ready to go. + +### Thing file + +```java +Bridge solarforecast:fs-site:homeSite "ForecastSolar Home" [ location="54.321,8.976"] { + Thing fs-plane homeSouthWest "ForecastSolar Home South-West" [ refreshInterval=15, azimuth=45, declination=35, kwp=5.5] + Thing fs-plane homeNorthEast "ForecastSolar Home North-East" [ refreshInterval=15, azimuth=-145, declination=35, kwp=4.425] +} +``` + +### Items file + +```java +// channel items +Number:Power ForecastSolarHome_Actual_Power "Power prediction for this moment" { channel="solarforecast:fs-site:homeSite:power-actual", stateDescription=" "[ pattern="%.0f %unit%" ], unit="W" } +Number:Energy ForecastSolarHome_Actual "Today's forecast till now" { channel="solarforecast:fs-site:homeSite:energy-actual", stateDescription=" "[ pattern="%.3f %unit%" ], unit="kWh" } +Number:Energy ForecastSolarHome_Remaining "Today's remaining forecast till sunset" { channel="solarforecast:fs-site:homeSite:energy-remain", stateDescription=" "[ pattern="%.3f %unit%" ], unit="kWh" } +Number:Energy ForecastSolarHome_Today "Today's total energy forecast" { channel="solarforecast:fs-site:homeSite:energy-today", stateDescription=" "[ pattern="%.1f %unit%" ], unit="kWh" } +// calculated by rule +Number:Energy ForecastSolarHome_Tomorrow "Tomorrow's total energy forecast" { stateDescription=" "[ pattern="%.1f %unit%" ], unit="kWh" } + +Number:Power ForecastSolarHome_Actual_Power_NE "NE Power prediction for this moment" { channel="solarforecast:fs-plane:homeSite:homeNorthEast:power-actual", stateDescription=" "[ pattern="%.0f %unit%" ], unit="W" } +Number:Energy ForecastSolarHome_Actual_NE "NE Today's forecast till now" { channel="solarforecast:fs-plane:homeSite:homeNorthEast:energy-actual", stateDescription=" "[ pattern="%.3f %unit%" ], unit="kWh" } +Number:Energy ForecastSolarHome_Remaining_NE "NE Today's remaining forecast till sunset" { channel="solarforecast:fs-plane:homeSite:homeNorthEast:energy-remain", stateDescription=" "[ pattern="%.3f %unit%" ], unit="kWh" } +Number:Energy ForecastSolarHome_Today_NE "NE Today's total energy forecast" { channel="solarforecast:fs-plane:homeSite:homeNorthEast:energy-today", stateDescription=" "[ pattern="%.1f %unit%" ], unit="kWh" } + +Number:Power ForecastSolarHome_Actual_Power_SW "SW Power prediction for this moment" { channel="solarforecast:fs-plane:homeSite:homeSouthWest:power-actual", stateDescription=" "[ pattern="%.0f %unit%" ], unit="W" } +Number:Energy ForecastSolarHome_Actual_SW "SW Today's forecast till now" { channel="solarforecast:fs-plane:homeSite:homeSouthWest:energy-actual", stateDescription=" "[ pattern="%.3f %unit%" ], unit="kWh" } +Number:Energy ForecastSolarHome_Remaining_SW "SW Today's remaining forecast till sunset" { channel="solarforecast:fs-plane:homeSite:homeSouthWest:energy-remain", stateDescription=" "[ pattern="%.3f %unit%" ], unit="kWh" } +Number:Energy ForecastSolarHome_Today_SW "SW Today's total energy forecast" { channel="solarforecast:fs-plane:homeSite:homeSouthWest:energy-today", stateDescription=" "[ pattern="%.1f %unit%" ], unit="kWh" } + +// estimaion items +Group influxdb +Number:Power ForecastSolarHome_Power_Estimate "Power estimations" (influxdb) { channel="solarforecast:fs-site:homeSite:power-estimate", stateDescription=" "[ pattern="%.0f %unit%" ], unit="W" } +Number:Energy ForecastSolarHome_Energy_Estimate "Energy estimations" (influxdb) { channel="solarforecast:fs-site:homeSite:energy-estimate", stateDescription=" "[ pattern="%.3f %unit%" ], unit="kWh" } +Number:Power ForecastSolarHome_Power_Estimate_SW "SW Power estimations" (influxdb) { channel="solarforecast:fs-plane:homeSite:homeSouthWest:power-estimate", stateDescription=" "[ pattern="%.0f %unit%" ], unit="W" } +Number:Energy ForecastSolarHome_Energy_Estimate_SW "SW Energy estimations" (influxdb) { channel="solarforecast:fs-plane:homeSite:homeSouthWest:energy-estimate", stateDescription=" "[ pattern="%.3f %unit%" ], unit="kWh" } +``` + +### Persistence file + +```java +// persistence strategies have a name and definition and are referred to in the "Items" section +Strategies { + everyHour : "0 0 * * * ?" + everyDay : "0 0 0 * * ?" +} + +/* + * Each line in this section defines for which Item(s) which strategy(ies) should be applied. + * You can list single items, use "*" for all items or "groupitem*" for all members of a group + * Item (excl. the group Item itself). + */ +Items { + influxdb* : strategy = restoreOnStartup, forecast +} +``` + +### Actions rule + +```java +rule "Tomorrow Forecast Calculation" + when + Item ForecastSolarHome_Today received update + then + val solarforecastActions = getActions("solarforecast","solarforecast:fs-site:homeSite") + val energyState = solarforecastActions.getDay(LocalDate.now.plusDays(1)) + logInfo("SF Tests","{}",energyState) + ForecastSolarHome_Tomorrow.postUpdate(energyState) +end +``` + +### Handle exceptions + +```java +import java.time.temporal.ChronoUnit + +rule "Exception Handling" + when + System started + then + val solcastActions = getActions("solarforecast","solarforecast:sc-site:3cadcde4dc") + try { + val forecast = solcastActions.getPower(solcastActions.getForecastEnd.plus(30,ChronoUnit.MINUTES)) + } catch(RuntimeException e) { + logError("Exception","Handle {}",e.getMessage) + } +end +``` + +### Actions rule with Arguments + +```java +import java.time.temporal.ChronoUnit + +rule "Solcast Actions" + when + Time cron "0 0 23 * * ?" // trigger whatever you like + then + // Query forecast via Actions + val solarforecastActions = getActions("solarforecast","solarforecast:sc-site:homeSite") + val startTimestamp = Instant.now + val endTimestamp = Instant.now.plus(6, ChronoUnit.DAYS) + val sixDayForecast = solarforecastActions.getEnergy(startTimestamp,endTimestamp) + logInfo("SF Tests","Forecast Average 6 days "+ sixDayForecast) + val sixDayOptimistic = solarforecastActions.getEnergy(startTimestamp,endTimestamp, "optimistic") + logInfo("SF Tests","Forecast Optimist 6 days "+ sixDayOptimistic) + val sixDayPessimistic = solarforecastActions.getEnergy(startTimestamp,endTimestamp, "pessimistic") + logInfo("SF Tests","Forecast Pessimist 6 days "+ sixDayPessimistic) + + // Query forecast TimesSeries Items via historicStata + val energyAverage = (Solcast_Site_Average_Energyestimate.historicState(now.plusDays(1)).state as Number) + logInfo("SF Tests","Average energy {}",energyAverage) + val energyOptimistic = (Solcast_Site_Optimistic_Energyestimate.historicState(now.plusDays(1)).state as Number) + logInfo("SF Tests","Optimist energy {}",energyOptimistic) +end +``` diff --git a/bundles/org.openhab.binding.solarforecast/doc/SolcastCumulated.png b/bundles/org.openhab.binding.solarforecast/doc/SolcastCumulated.png new file mode 100644 index 0000000000000000000000000000000000000000..d1c692ec3b7570b846dca247edc04958bc17a96e GIT binary patch literal 64475 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 G4gWWSJ8a7U literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.solarforecast/doc/SolcastPower.png b/bundles/org.openhab.binding.solarforecast/doc/SolcastPower.png new file mode 100644 index 0000000000000000000000000000000000000000..fb4ab56f59c7fe262b489a2624c722b70af3f8a9 GIT binary patch literal 83573 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 literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.solarforecast/pom.xml b/bundles/org.openhab.binding.solarforecast/pom.xml new file mode 100644 index 00000000000..ecd0e93a857 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/pom.xml @@ -0,0 +1,26 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.2.0-SNAPSHOT + + + org.openhab.binding.solarforecast + + openHAB Add-ons :: Bundles :: SolarForecast Binding + + + + org.json + json + 20231013 + compile + + + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/feature/feature.xml b/bundles/org.openhab.binding.solarforecast/src/main/feature/feature.xml new file mode 100644 index 00000000000..9237c11c88d --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.solarforecast/${project.version} + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastBindingConstants.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastBindingConstants.java new file mode 100644 index 00000000000..7b4ba46b507 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastBindingConstants.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast.internal; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link SolarForecastBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class SolarForecastBindingConstants { + + private static final String BINDING_ID = "solarforecast"; + + // Things + public static final ThingTypeUID FORECAST_SOLAR_SITE = new ThingTypeUID(BINDING_ID, "fs-site"); + public static final ThingTypeUID FORECAST_SOLAR_PLANE = new ThingTypeUID(BINDING_ID, "fs-plane"); + public static final ThingTypeUID SOLCAST_SITE = new ThingTypeUID(BINDING_ID, "sc-site"); + public static final ThingTypeUID SOLCAST_PLANE = new ThingTypeUID(BINDING_ID, "sc-plane"); + public static final Set SUPPORTED_THING_SET = Set.of(FORECAST_SOLAR_SITE, FORECAST_SOLAR_PLANE, + SOLCAST_SITE, SOLCAST_PLANE); + + // Channel groups + public static final String GROUP_AVERAGE = "average"; + public static final String GROUP_OPTIMISTIC = "optimistic"; + public static final String GROUP_PESSIMISTIC = "pessimistic"; + public static final String GROUP_RAW = "raw"; + + // Channels + public static final String CHANNEL_POWER_ESTIMATE = "power-estimate"; + public static final String CHANNEL_ENERGY_ESTIMATE = "energy-estimate"; + public static final String CHANNEL_POWER_ACTUAL = "power-actual"; + public static final String CHANNEL_ENERGY_ACTUAL = "energy-actual"; + public static final String CHANNEL_ENERGY_REMAIN = "energy-remain"; + public static final String CHANNEL_ENERGY_TODAY = "energy-today"; + public static final String CHANNEL_JSON = "json"; + + // Other + public static final int REFRESH_ACTUAL_INTERVAL = 1; + public static final String SLASH = "/"; + public static final String EMPTY = ""; + public static final String PATTERN_FORMAT = "yyyy-MM-dd HH:mm:ss"; +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastException.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastException.java new file mode 100644 index 00000000000..06c8c856cf1 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastException.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.solarforecast.internal.actions.SolarForecast; + +/** + * The {@link SolarForecastException} is thrown if forecast data is invalid + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +@SuppressWarnings("serial") +public class SolarForecastException extends RuntimeException { + + public SolarForecastException(SolarForecast ref, String message) { + super(ref.getIdentifier() + " # " + message); + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastHandlerFactory.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastHandlerFactory.java new file mode 100644 index 00000000000..9597ab2fdb6 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastHandlerFactory.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast.internal; + +import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; + +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.solarforecast.internal.forecastsolar.handler.ForecastSolarBridgeHandler; +import org.openhab.binding.solarforecast.internal.forecastsolar.handler.ForecastSolarPlaneHandler; +import org.openhab.binding.solarforecast.internal.solcast.handler.SolcastBridgeHandler; +import org.openhab.binding.solarforecast.internal.solcast.handler.SolcastPlaneHandler; +import org.openhab.core.i18n.LocationProvider; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.library.types.PointType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link SolarForecastHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.solarforecast", service = ThingHandlerFactory.class) +public class SolarForecastHandlerFactory extends BaseThingHandlerFactory { + private final TimeZoneProvider timeZoneProvider; + private final HttpClient httpClient; + private Optional location = Optional.empty(); + + @Activate + public SolarForecastHandlerFactory(final @Reference HttpClientFactory hcf, final @Reference LocationProvider lp, + final @Reference TimeZoneProvider tzp) { + timeZoneProvider = tzp; + httpClient = hcf.getCommonHttpClient(); + PointType pt = lp.getLocation(); + if (pt != null) { + location = Optional.of(pt); + } + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SolarForecastBindingConstants.SUPPORTED_THING_SET.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + if (FORECAST_SOLAR_SITE.equals(thingTypeUID)) { + return new ForecastSolarBridgeHandler((Bridge) thing, location); + } else if (FORECAST_SOLAR_PLANE.equals(thingTypeUID)) { + return new ForecastSolarPlaneHandler(thing, httpClient); + } else if (SOLCAST_SITE.equals(thingTypeUID)) { + return new SolcastBridgeHandler((Bridge) thing, timeZoneProvider); + } else if (SOLCAST_PLANE.equals(thingTypeUID)) { + return new SolcastPlaneHandler(thing, httpClient); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecast.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecast.java new file mode 100644 index 00000000000..b6d37bb2697 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecast.java @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast.internal.actions; + +import java.time.Instant; +import java.time.LocalDate; + +import javax.measure.quantity.Energy; +import javax.measure.quantity.Power; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.solarforecast.internal.solcast.SolcastObject.QueryMode; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.types.TimeSeries; + +/** + * The {@link SolarForecast} Interface needed for Actions + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public interface SolarForecast { + /** + * Argument can be used to query an average forecast scenario + */ + public static final String AVERAGE = "average"; + /** + * Argument can be used to query an optimistic forecast scenario + */ + public static final String OPTIMISTIC = "optimistic"; + /** + * Argument can be used to query a pessimistic forecast scenario + */ + public static final String PESSIMISTIC = "pessimistic"; + + /** + * Returns electric energy production for one day + * + * @param date + * @param args possible arguments from this interface + * @return QuantityType in kW/h + */ + QuantityType getDay(LocalDate date, String... args); + + /** + * Returns electric energy between two timestamps + * + * @param start + * @param end + * @param args possible arguments from this interface + * @return QuantityType in kW/h + */ + QuantityType getEnergy(Instant start, Instant end, String... args); + + /** + * Returns electric power at one specific point of time + * + * @param timestamp + * @param args possible arguments from this interface + * @return QuantityType in kW + */ + QuantityType getPower(Instant timestamp, String... args); + + /** + * Get the first date and time of forecast data + * + * @return date time + */ + Instant getForecastBegin(); + + /** + * Get the last date and time of forecast data + * + * @return date time + */ + Instant getForecastEnd(); + + /** + * Get TimeSeries for Power forecast + * + * @param mode QueryMode for optimistic, pessimistic or average estimation + * @return TimeSeries containing QuantityType + */ + TimeSeries getPowerTimeSeries(QueryMode mode); + + /** + * Get TimeSeries for Energy forecast + * + * @param mode QueryMode for optimistic, pessimistic or average estimation + * @return TimeSeries containing QuantityType + */ + TimeSeries getEnergyTimeSeries(QueryMode mode); + + /** + * SolarForecast identifier + * + * @return unique String to identify solar plane + */ + String getIdentifier(); +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecastActions.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecastActions.java new file mode 100644 index 00000000000..c794ebb1c3d --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecastActions.java @@ -0,0 +1,195 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast.internal.actions; + +import java.time.Instant; +import java.time.LocalDate; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; + +import javax.measure.MetricPrefix; +import javax.measure.quantity.Energy; +import javax.measure.quantity.Power; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.solarforecast.internal.utils.Utils; +import org.openhab.core.automation.annotation.ActionInput; +import org.openhab.core.automation.annotation.RuleAction; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.binding.ThingActions; +import org.openhab.core.thing.binding.ThingActionsScope; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Actions to query forecast objects + * + * @author Bernd Weymann - Initial contribution + */ +@ThingActionsScope(name = "solarforecast") +@NonNullByDefault +public class SolarForecastActions implements ThingActions { + private final Logger logger = LoggerFactory.getLogger(SolarForecastActions.class); + private Optional thingHandler = Optional.empty(); + + @RuleAction(label = "@text/actionDayLabel", description = "@text/actionDayDesc") + public QuantityType getDay( + @ActionInput(name = "localDate", label = "@text/actionInputDayLabel", description = "@text/actionInputDayDesc") LocalDate localDate, + String... args) { + if (thingHandler.isPresent()) { + List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); + if (!l.isEmpty()) { + QuantityType measure = QuantityType.valueOf(0, Units.KILOWATT_HOUR); + for (Iterator iterator = l.iterator(); iterator.hasNext();) { + SolarForecast solarForecast = iterator.next(); + QuantityType qt = solarForecast.getDay(localDate, args); + if (qt.floatValue() >= 0) { + measure = measure.add(qt); + } else { + // break in case of failure getting values to avoid ambiguous values + logger.debug("Ambiguous measure {} found for {}", qt, localDate); + return Utils.getEnergyState(-1); + } + } + return measure; + } else { + logger.debug("No forecasts found for {}", localDate); + return Utils.getEnergyState(-1); + } + } else { + logger.trace("Handler missing"); + return Utils.getEnergyState(-1); + } + } + + @RuleAction(label = "@text/actionPowerLabel", description = "@text/actionPowerDesc") + public QuantityType getPower( + @ActionInput(name = "timestamp", label = "@text/actionInputDateTimeLabel", description = "@text/actionInputDateTimeDesc") Instant timestamp, + String... args) { + if (thingHandler.isPresent()) { + List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); + if (!l.isEmpty()) { + QuantityType measure = QuantityType.valueOf(0, MetricPrefix.KILO(Units.WATT)); + for (Iterator iterator = l.iterator(); iterator.hasNext();) { + SolarForecast solarForecast = iterator.next(); + QuantityType qt = solarForecast.getPower(timestamp, args); + if (qt.floatValue() >= 0) { + measure = measure.add(qt); + } else { + // break in case of failure getting values to avoid ambiguous values + logger.debug("Ambiguous measure {} found for {}", qt, timestamp); + return Utils.getPowerState(-1); + } + } + return measure; + } else { + logger.debug("No forecasts found for {}", timestamp); + return Utils.getPowerState(-1); + } + } else { + logger.trace("Handler missing"); + return Utils.getPowerState(-1); + } + } + + @RuleAction(label = "@text/actionEnergyLabel", description = "@text/actionEnergyDesc") + public QuantityType getEnergy( + @ActionInput(name = "start", label = "@text/actionInputDateTimeBeginLabel", description = "@text/actionInputDateTimeBeginDesc") Instant start, + @ActionInput(name = "end", label = "@text/actionInputDateTimeEndLabel", description = "@text/actionInputDateTimeEndDesc") Instant end, + String... args) { + if (thingHandler.isPresent()) { + List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); + if (!l.isEmpty()) { + QuantityType measure = QuantityType.valueOf(0, Units.KILOWATT_HOUR); + for (Iterator iterator = l.iterator(); iterator.hasNext();) { + SolarForecast solarForecast = iterator.next(); + QuantityType qt = solarForecast.getEnergy(start, end, args); + if (qt.floatValue() >= 0) { + measure = measure.add(qt); + } else { + // break in case of failure getting values to avoid ambiguous values + logger.debug("Ambiguous measure {} found between {} and {}", qt, start, end); + return Utils.getEnergyState(-1); + } + } + return measure; + } else { + logger.debug("No forecasts found for between {} and {}", start, end); + return Utils.getEnergyState(-1); + } + } else { + logger.trace("Handler missing"); + return Utils.getEnergyState(-1); + } + } + + @RuleAction(label = "@text/actionForecastBeginLabel", description = "@text/actionForecastBeginDesc") + public Instant getForecastBegin() { + if (thingHandler.isPresent()) { + List forecastObjectList = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); + return Utils.getCommonStartTime(forecastObjectList); + } else { + logger.trace("Handler missing - return invalid date MAX"); + return Instant.MAX; + } + } + + @RuleAction(label = "@text/actionForecastEndLabel", description = "@text/actionForecastEndDesc") + public Instant getForecastEnd() { + if (thingHandler.isPresent()) { + List forecastObjectList = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); + return Utils.getCommonEndTime(forecastObjectList); + } else { + logger.trace("Handler missing - return invalid date MIN"); + return Instant.MIN; + } + } + + public static State getDay(ThingActions actions, LocalDate ld, String... args) { + return ((SolarForecastActions) actions).getDay(ld, args); + } + + public static State getPower(ThingActions actions, Instant dateTime, String... args) { + return ((SolarForecastActions) actions).getPower(dateTime, args); + } + + public static State getEnergy(ThingActions actions, Instant begin, Instant end, String... args) { + return ((SolarForecastActions) actions).getEnergy(begin, end, args); + } + + public static Instant getForecastBegin(ThingActions actions) { + return ((SolarForecastActions) actions).getForecastBegin(); + } + + public static Instant getForecastEnd(ThingActions actions) { + return ((SolarForecastActions) actions).getForecastEnd(); + } + + @Override + public void setThingHandler(ThingHandler handler) { + thingHandler = Optional.of(handler); + } + + @Override + public @Nullable ThingHandler getThingHandler() { + if (thingHandler.isPresent()) { + return thingHandler.get(); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecastProvider.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecastProvider.java new file mode 100644 index 00000000000..0163cf2bb5b --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecastProvider.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast.internal.actions; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link SolarForecastProvider} Interface needed for Actions + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public interface SolarForecastProvider { + + /** + * Provides List of available SolarForecast Interface implementations + * + * @return list of SolarForecast objects + */ + List getSolarForecasts(); +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarObject.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarObject.java new file mode 100644 index 00000000000..de15a50c063 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarObject.java @@ -0,0 +1,345 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast.internal.forecastsolar; + +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.TreeMap; + +import javax.measure.quantity.Energy; +import javax.measure.quantity.Power; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONException; +import org.json.JSONObject; +import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; +import org.openhab.binding.solarforecast.internal.SolarForecastException; +import org.openhab.binding.solarforecast.internal.actions.SolarForecast; +import org.openhab.binding.solarforecast.internal.solcast.SolcastObject.QueryMode; +import org.openhab.binding.solarforecast.internal.utils.Utils; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.types.TimeSeries; +import org.openhab.core.types.TimeSeries.Policy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ForecastSolarObject} holds complete data for forecast + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class ForecastSolarObject implements SolarForecast { + private final Logger logger = LoggerFactory.getLogger(ForecastSolarObject.class); + private final TreeMap wattHourMap = new TreeMap<>(); + private final TreeMap wattMap = new TreeMap<>(); + private final DateTimeFormatter dateInputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + private DateTimeFormatter dateOutputFormatter = DateTimeFormatter + .ofPattern(SolarForecastBindingConstants.PATTERN_FORMAT).withZone(ZoneId.systemDefault()); + private ZoneId zone = ZoneId.systemDefault(); + private Optional rawData = Optional.empty(); + private Instant expirationDateTime; + private String identifier; + + public ForecastSolarObject(String id) { + expirationDateTime = Instant.now().minusSeconds(1); + identifier = id; + } + + public ForecastSolarObject(String id, String content, Instant expirationDate) throws SolarForecastException { + expirationDateTime = expirationDate; + identifier = id; + if (!content.isEmpty()) { + rawData = Optional.of(content); + try { + JSONObject contentJson = new JSONObject(content); + JSONObject resultJson = contentJson.getJSONObject("result"); + JSONObject wattHourJson = resultJson.getJSONObject("watt_hours"); + JSONObject wattJson = resultJson.getJSONObject("watts"); + String zoneStr = contentJson.getJSONObject("message").getJSONObject("info").getString("timezone"); + zone = ZoneId.of(zoneStr); + dateOutputFormatter = DateTimeFormatter.ofPattern(SolarForecastBindingConstants.PATTERN_FORMAT) + .withZone(zone); + Iterator iter = wattHourJson.keys(); + // put all values of the current day into sorted tree map + while (iter.hasNext()) { + String dateStr = iter.next(); + // convert date time into machine readable format + try { + ZonedDateTime zdt = LocalDateTime.parse(dateStr, dateInputFormatter).atZone(zone); + wattHourMap.put(zdt, wattHourJson.getDouble(dateStr)); + wattMap.put(zdt, wattJson.getDouble(dateStr)); + } catch (DateTimeParseException dtpe) { + logger.warn("Error parsing time {} Reason: {}", dateStr, dtpe.getMessage()); + throw new SolarForecastException(this, + "Error parsing time " + dateStr + " Reason: " + dtpe.getMessage()); + } + } + } catch (JSONException je) { + throw new SolarForecastException(this, + "Error parsing JSON response " + content + " Reason: " + je.getMessage()); + } + } + } + + public boolean isExpired() { + return expirationDateTime.isBefore(Instant.now()); + } + + public double getActualEnergyValue(ZonedDateTime queryDateTime) throws SolarForecastException { + Entry f = wattHourMap.floorEntry(queryDateTime); + Entry c = wattHourMap.ceilingEntry(queryDateTime); + if (f != null && c == null) { + // only floor available + if (f.getKey().toLocalDate().equals(queryDateTime.toLocalDate())) { + // floor has valid date + return f.getValue() / 1000.0; + } else { + // floor date doesn't fit + throwOutOfRangeException(queryDateTime.toInstant()); + } + } else if (f == null && c != null) { + if (c.getKey().toLocalDate().equals(queryDateTime.toLocalDate())) { + // only ceiling from correct date available - no valid data reached yet + return 0; + } else { + // ceiling date doesn't fit + throwOutOfRangeException(queryDateTime.toInstant()); + } + } else if (f != null && c != null) { + // ceiling and floor available + if (f.getKey().toLocalDate().equals(queryDateTime.toLocalDate())) { + if (c.getKey().toLocalDate().equals(queryDateTime.toLocalDate())) { + // we're during suntime! + double production = c.getValue() - f.getValue(); + long floorToCeilingDuration = Duration.between(f.getKey(), c.getKey()).toMinutes(); + if (floorToCeilingDuration == 0) { + return f.getValue() / 1000.0; + } + long floorToQueryDuration = Duration.between(f.getKey(), queryDateTime).toMinutes(); + double interpolation = (double) floorToQueryDuration / (double) floorToCeilingDuration; + double interpolationProduction = production * interpolation; + double actualProduction = f.getValue() + interpolationProduction; + return actualProduction / 1000.0; + } else { + // ceiling from wrong date, but floor is valid + return f.getValue() / 1000.0; + } + } else { + // floor invalid - ceiling not reached + return 0; + } + } // else both null - date time doesn't fit to forecast data + throwOutOfRangeException(queryDateTime.toInstant()); + return -1; + } + + @Override + public TimeSeries getEnergyTimeSeries(QueryMode mode) { + TimeSeries ts = new TimeSeries(Policy.REPLACE); + wattHourMap.forEach((timestamp, energy) -> { + ts.add(timestamp.toInstant(), Utils.getEnergyState(energy / 1000.0)); + }); + return ts; + } + + public double getActualPowerValue(ZonedDateTime queryDateTime) { + double actualPowerValue = 0; + Entry f = wattMap.floorEntry(queryDateTime); + Entry c = wattMap.ceilingEntry(queryDateTime); + if (f != null && c == null) { + // only floor available + if (f.getKey().toLocalDate().equals(queryDateTime.toLocalDate())) { + // floor has valid date + return f.getValue() / 1000.0; + } else { + // floor date doesn't fit + throwOutOfRangeException(queryDateTime.toInstant()); + } + } else if (f == null && c != null) { + if (c.getKey().toLocalDate().equals(queryDateTime.toLocalDate())) { + // only ceiling from correct date available - no valid data reached yet + return 0; + } else { + // ceiling date doesn't fit + throwOutOfRangeException(queryDateTime.toInstant()); + } + } else if (f != null && c != null) { + // we're during suntime! + long floorToCeilingDuration = Duration.between(f.getKey(), c.getKey()).toMinutes(); + double powerFloor = f.getValue(); + if (floorToCeilingDuration == 0) { + return powerFloor / 1000.0; + } + double powerCeiling = c.getValue(); + // calculate in minutes from floor to now, e.g. 20 minutes + // => take 2/3 of floor and 1/3 of ceiling + long floorToQueryDuration = Duration.between(f.getKey(), queryDateTime).toMinutes(); + double interpolation = (double) floorToQueryDuration / (double) floorToCeilingDuration; + actualPowerValue = ((1 - interpolation) * powerFloor) + (interpolation * powerCeiling); + return actualPowerValue / 1000.0; + } // else both null - this shall not happen + throwOutOfRangeException(queryDateTime.toInstant()); + return -1; + } + + @Override + public TimeSeries getPowerTimeSeries(QueryMode mode) { + TimeSeries ts = new TimeSeries(Policy.REPLACE); + wattMap.forEach((timestamp, power) -> { + ts.add(timestamp.toInstant(), Utils.getPowerState(power / 1000.0)); + }); + return ts; + } + + public double getDayTotal(LocalDate queryDate) { + if (rawData.isEmpty()) { + throw new SolarForecastException(this, "No forecast data available"); + } + JSONObject contentJson = new JSONObject(rawData.get()); + JSONObject resultJson = contentJson.getJSONObject("result"); + JSONObject wattsDay = resultJson.getJSONObject("watt_hours_day"); + + if (wattsDay.has(queryDate.toString())) { + return wattsDay.getDouble(queryDate.toString()) / 1000.0; + } else { + throw new SolarForecastException(this, + "Day " + queryDate + " not available in forecast. " + getTimeRange()); + } + } + + public double getRemainingProduction(ZonedDateTime queryDateTime) { + double daily = getDayTotal(queryDateTime.toLocalDate()); + double actual = getActualEnergyValue(queryDateTime); + return daily - actual; + } + + public String getRaw() { + if (rawData.isPresent()) { + return rawData.get(); + } + return "{}"; + } + + public ZoneId getZone() { + return zone; + } + + @Override + public String toString() { + return "Expiration: " + expirationDateTime + ", Data:" + wattHourMap; + } + + /** + * SolarForecast Interface + */ + @Override + public QuantityType getDay(LocalDate localDate, String... args) throws IllegalArgumentException { + if (args.length > 0) { + throw new IllegalArgumentException("ForecastSolar doesn't accept arguments"); + } + double measure = getDayTotal(localDate); + return Utils.getEnergyState(measure); + } + + @Override + public QuantityType getEnergy(Instant start, Instant end, String... args) throws IllegalArgumentException { + if (args.length > 0) { + throw new IllegalArgumentException("ForecastSolar doesn't accept arguments"); + } + LocalDate beginDate = start.atZone(zone).toLocalDate(); + LocalDate endDate = end.atZone(zone).toLocalDate(); + double measure = -1; + if (beginDate.equals(endDate)) { + measure = getDayTotal(beginDate) - getActualEnergyValue(start.atZone(zone)) + - getRemainingProduction(end.atZone(zone)); + } else { + measure = getRemainingProduction(start.atZone(zone)); + beginDate = beginDate.plusDays(1); + while (beginDate.isBefore(endDate) && measure >= 0) { + double day = getDayTotal(beginDate); + if (day > 0) { + measure += day; + } + beginDate = beginDate.plusDays(1); + } + double lastDay = getActualEnergyValue(end.atZone(zone)); + if (lastDay >= 0) { + measure += lastDay; + } + } + return Utils.getEnergyState(measure); + } + + @Override + public QuantityType getPower(Instant timestamp, String... args) throws IllegalArgumentException { + if (args.length > 0) { + throw new IllegalArgumentException("ForecastSolar doesn't accept arguments"); + } + double measure = getActualPowerValue(timestamp.atZone(zone)); + return Utils.getPowerState(measure); + } + + @Override + public Instant getForecastBegin() { + if (wattHourMap.isEmpty()) { + return Instant.MAX; + } + ZonedDateTime zdt = wattHourMap.firstEntry().getKey(); + return zdt.toInstant(); + } + + @Override + public Instant getForecastEnd() { + if (wattHourMap.isEmpty()) { + return Instant.MIN; + } + ZonedDateTime zdt = wattHourMap.lastEntry().getKey(); + return zdt.toInstant(); + } + + private void throwOutOfRangeException(Instant query) { + if (getForecastBegin().equals(Instant.MAX) || getForecastEnd().equals(Instant.MIN)) { + throw new SolarForecastException(this, "Forecast invalid time range"); + } + if (query.isBefore(getForecastBegin())) { + throw new SolarForecastException(this, + "Query " + dateOutputFormatter.format(query) + " too early. " + getTimeRange()); + } else if (query.isAfter(getForecastEnd())) { + throw new SolarForecastException(this, + "Query " + dateOutputFormatter.format(query) + " too late. " + getTimeRange()); + } else { + logger.warn("Query {} is fine. {}", dateOutputFormatter.format(query), getTimeRange()); + } + } + + private String getTimeRange() { + return "Valid range: " + dateOutputFormatter.format(getForecastBegin()) + " - " + + dateOutputFormatter.format(getForecastEnd()); + } + + @Override + public String getIdentifier() { + return identifier; + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/config/ForecastSolarBridgeConfiguration.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/config/ForecastSolarBridgeConfiguration.java new file mode 100644 index 00000000000..89f1fd12082 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/config/ForecastSolarBridgeConfiguration.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast.internal.forecastsolar.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; + +/** + * The {@link ForecastSolarBridgeConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class ForecastSolarBridgeConfiguration { + public String location = ""; + public String apiKey = SolarForecastBindingConstants.EMPTY; + public double inverterKwp = Double.MAX_VALUE; +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/config/ForecastSolarPlaneConfiguration.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/config/ForecastSolarPlaneConfiguration.java new file mode 100644 index 00000000000..ed2da9a384b --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/config/ForecastSolarPlaneConfiguration.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast.internal.forecastsolar.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; + +/** + * The {@link ForecastSolarPlaneConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class ForecastSolarPlaneConfiguration { + public int declination = -1; + public int azimuth = -1; + public double kwp = 0; + public long refreshInterval = 30; + public double dampAM = 0.25; + public double dampPM = 0.25; + public String horizon = SolarForecastBindingConstants.EMPTY; +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarBridgeHandler.java new file mode 100644 index 00000000000..487d93e86dd --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarBridgeHandler.java @@ -0,0 +1,235 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast.internal.forecastsolar.handler; + +import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; + +import java.time.Instant; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.TreeMap; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.solarforecast.internal.SolarForecastException; +import org.openhab.binding.solarforecast.internal.actions.SolarForecast; +import org.openhab.binding.solarforecast.internal.actions.SolarForecastActions; +import org.openhab.binding.solarforecast.internal.actions.SolarForecastProvider; +import org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarObject; +import org.openhab.binding.solarforecast.internal.forecastsolar.config.ForecastSolarBridgeConfiguration; +import org.openhab.binding.solarforecast.internal.solcast.SolcastObject.QueryMode; +import org.openhab.binding.solarforecast.internal.utils.Utils; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.PointType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.TimeSeries; +import org.openhab.core.types.TimeSeries.Policy; + +/** + * The {@link ForecastSolarBridgeHandler} is a non active handler instance. It will be triggerer by the bridge. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class ForecastSolarBridgeHandler extends BaseBridgeHandler implements SolarForecastProvider { + private List planes = new ArrayList<>(); + private Optional homeLocation; + private Optional configuration = Optional.empty(); + private Optional> refreshJob = Optional.empty(); + + public ForecastSolarBridgeHandler(Bridge bridge, Optional location) { + super(bridge); + homeLocation = location; + } + + @Override + public Collection> getServices() { + return List.of(SolarForecastActions.class); + } + + @Override + public void initialize() { + ForecastSolarBridgeConfiguration config = getConfigAs(ForecastSolarBridgeConfiguration.class); + PointType locationConfigured; + + // handle location error cases + if (config.location.isBlank()) { + if (homeLocation.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/solarforecast.site.status.location-missing"); + return; + } else { + locationConfigured = homeLocation.get(); + // continue with openHAB location + } + } else { + try { + locationConfigured = new PointType(config.location); + // continue with location from configuration + } catch (Exception e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + return; + } + } + Configuration editConfig = editConfiguration(); + editConfig.put("location", locationConfigured.toString()); + updateConfiguration(editConfig); + config = getConfigAs(ForecastSolarBridgeConfiguration.class); + configuration = Optional.of(config); + updateStatus(ThingStatus.UNKNOWN); + refreshJob = Optional + .of(scheduler.scheduleWithFixedDelay(this::getData, 0, REFRESH_ACTUAL_INTERVAL, TimeUnit.MINUTES)); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + String channel = channelUID.getIdWithoutGroup(); + switch (channel) { + case CHANNEL_ENERGY_ACTUAL: + case CHANNEL_ENERGY_REMAIN: + case CHANNEL_ENERGY_TODAY: + case CHANNEL_POWER_ACTUAL: + getData(); + break; + case CHANNEL_POWER_ESTIMATE: + case CHANNEL_ENERGY_ESTIMATE: + forecastUpdate(); + break; + } + } + } + + /** + * Get data for all planes. Synchronized to protect plane list from being modified during update + */ + private synchronized void getData() { + if (planes.isEmpty()) { + return; + } + boolean update = true; + double energySum = 0; + double powerSum = 0; + double daySum = 0; + for (Iterator iterator = planes.iterator(); iterator.hasNext();) { + try { + ForecastSolarPlaneHandler sfph = iterator.next(); + ForecastSolarObject fo = sfph.fetchData(); + ZonedDateTime now = ZonedDateTime.now(fo.getZone()); + energySum += fo.getActualEnergyValue(now); + powerSum += fo.getActualPowerValue(now); + daySum += fo.getDayTotal(now.toLocalDate()); + } catch (SolarForecastException sfe) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, + "@text/solarforecast.site.status.exception [\"" + sfe.getMessage() + "\"]"); + update = false; + } + } + if (update) { + updateStatus(ThingStatus.ONLINE); + updateState(CHANNEL_ENERGY_ACTUAL, Utils.getEnergyState(energySum)); + updateState(CHANNEL_ENERGY_REMAIN, Utils.getEnergyState(daySum - energySum)); + updateState(CHANNEL_ENERGY_TODAY, Utils.getEnergyState(daySum)); + updateState(CHANNEL_POWER_ACTUAL, Utils.getPowerState(powerSum)); + } + } + + public synchronized void forecastUpdate() { + if (planes.isEmpty()) { + return; + } + TreeMap> combinedPowerForecast = new TreeMap<>(); + TreeMap> combinedEnergyForecast = new TreeMap<>(); + List forecastObjects = new ArrayList<>(); + for (Iterator iterator = planes.iterator(); iterator.hasNext();) { + ForecastSolarPlaneHandler sfph = iterator.next(); + forecastObjects.addAll(sfph.getSolarForecasts()); + } + + // bugfix: https://github.com/weymann/OH3-SolarForecast-Drops/issues/5 + // find common start and end time which fits to all forecast objects to avoid ambiguous values + final Instant commonStart = Utils.getCommonStartTime(forecastObjects); + final Instant commonEnd = Utils.getCommonEndTime(forecastObjects); + forecastObjects.forEach(fc -> { + TimeSeries powerTS = fc.getPowerTimeSeries(QueryMode.Average); + powerTS.getStates().forEach(entry -> { + if (Utils.isAfterOrEqual(entry.timestamp(), commonStart) + && Utils.isBeforeOrEqual(entry.timestamp(), commonEnd)) { + Utils.addState(combinedPowerForecast, entry); + } + }); + TimeSeries energyTS = fc.getEnergyTimeSeries(QueryMode.Average); + energyTS.getStates().forEach(entry -> { + if (Utils.isAfterOrEqual(entry.timestamp(), commonStart) + && Utils.isBeforeOrEqual(entry.timestamp(), commonEnd)) { + Utils.addState(combinedEnergyForecast, entry); + } + }); + }); + + TimeSeries powerSeries = new TimeSeries(Policy.REPLACE); + combinedPowerForecast.forEach((timestamp, state) -> { + powerSeries.add(timestamp, state); + }); + sendTimeSeries(CHANNEL_POWER_ESTIMATE, powerSeries); + + TimeSeries energySeries = new TimeSeries(Policy.REPLACE); + combinedEnergyForecast.forEach((timestamp, state) -> { + energySeries.add(timestamp, state); + }); + sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, energySeries); + } + + @Override + public void dispose() { + refreshJob.ifPresent(job -> job.cancel(true)); + } + + public synchronized void addPlane(ForecastSolarPlaneHandler sfph) { + planes.add(sfph); + // update passive PV plane with necessary data + if (configuration.isPresent()) { + sfph.setLocation(new PointType(configuration.get().location)); + if (!configuration.get().apiKey.isBlank()) { + sfph.setApiKey(configuration.get().apiKey); + } + } + getData(); + } + + public synchronized void removePlane(ForecastSolarPlaneHandler sfph) { + planes.remove(sfph); + } + + @Override + public synchronized List getSolarForecasts() { + List l = new ArrayList(); + planes.forEach(entry -> { + l.addAll(entry.getSolarForecasts()); + }); + return l; + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarPlaneHandler.java new file mode 100644 index 00000000000..a15f617fdc3 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarPlaneHandler.java @@ -0,0 +1,225 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast.internal.forecastsolar.handler; + +import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; + +import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; +import org.openhab.binding.solarforecast.internal.SolarForecastException; +import org.openhab.binding.solarforecast.internal.actions.SolarForecast; +import org.openhab.binding.solarforecast.internal.actions.SolarForecastActions; +import org.openhab.binding.solarforecast.internal.actions.SolarForecastProvider; +import org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarObject; +import org.openhab.binding.solarforecast.internal.forecastsolar.config.ForecastSolarPlaneConfiguration; +import org.openhab.binding.solarforecast.internal.solcast.SolcastObject.QueryMode; +import org.openhab.binding.solarforecast.internal.utils.Utils; +import org.openhab.core.library.types.PointType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.thing.binding.BridgeHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ForecastSolarPlaneHandler} is a non active handler instance. It will be triggered by the bridge. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class ForecastSolarPlaneHandler extends BaseThingHandler implements SolarForecastProvider { + public static final String BASE_URL = "https://api.forecast.solar/"; + + private final Logger logger = LoggerFactory.getLogger(ForecastSolarPlaneHandler.class); + private final HttpClient httpClient; + + private Optional configuration = Optional.empty(); + private Optional bridgeHandler = Optional.empty(); + private Optional location = Optional.empty(); + private Optional apiKey = Optional.empty(); + private ForecastSolarObject forecast; + + public ForecastSolarPlaneHandler(Thing thing, HttpClient hc) { + super(thing); + httpClient = hc; + forecast = new ForecastSolarObject(thing.getUID().getAsString()); + } + + @Override + public Collection> getServices() { + return List.of(SolarForecastActions.class); + } + + @Override + public void initialize() { + ForecastSolarPlaneConfiguration c = getConfigAs(ForecastSolarPlaneConfiguration.class); + configuration = Optional.of(c); + Bridge bridge = getBridge(); + if (bridge != null) { + BridgeHandler handler = bridge.getHandler(); + if (handler != null) { + if (handler instanceof ForecastSolarBridgeHandler fsbh) { + bridgeHandler = Optional.of(fsbh); + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, + "@text/solarforecast.plane.status.await-feedback"); + fsbh.addPlane(this); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/solarforecast.plane.status.wrong-handler" + " [\"" + handler + "\"]"); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/solarforecast.plane.status.bridge-handler-not-found"); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/solarforecast.plane.status.bridge-missing"); + } + } + + @Override + public void dispose() { + super.dispose(); + if (bridgeHandler.isPresent()) { + bridgeHandler.get().removePlane(this); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + if (CHANNEL_POWER_ESTIMATE.equals(channelUID.getIdWithoutGroup())) { + sendTimeSeries(CHANNEL_POWER_ESTIMATE, forecast.getPowerTimeSeries(QueryMode.Average)); + } else if (CHANNEL_ENERGY_ESTIMATE.equals(channelUID.getIdWithoutGroup())) { + sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, forecast.getEnergyTimeSeries(QueryMode.Average)); + } else if (CHANNEL_JSON.equals(channelUID.getIdWithoutGroup())) { + updateState(CHANNEL_JSON, StringType.valueOf(forecast.getRaw())); + } else { + fetchData(); + } + } + } + + /** + * https://doc.forecast.solar/doku.php?id=api:estimate + */ + protected ForecastSolarObject fetchData() { + if (location.isPresent()) { + if (forecast.isExpired()) { + String url = getBaseUrl() + "estimate/" + location.get().getLatitude() + SLASH + + location.get().getLongitude() + SLASH + configuration.get().declination + SLASH + + configuration.get().azimuth + SLASH + configuration.get().kwp + "?damping=" + + configuration.get().dampAM + "," + configuration.get().dampPM; + if (!SolarForecastBindingConstants.EMPTY.equals(configuration.get().horizon)) { + url += "&horizon=" + configuration.get().horizon; + } + try { + ContentResponse cr = httpClient.GET(url); + if (cr.getStatus() == 200) { + try { + ForecastSolarObject localForecast = new ForecastSolarObject(thing.getUID().getAsString(), + cr.getContentAsString(), + Instant.now().plus(configuration.get().refreshInterval, ChronoUnit.MINUTES)); + updateStatus(ThingStatus.ONLINE); + updateState(CHANNEL_JSON, StringType.valueOf(cr.getContentAsString())); + setForecast(localForecast); + } catch (SolarForecastException fse) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, + "@text/solarforecast.plane.status.json-status [\"" + fse.getMessage() + "\"]"); + } + } else { + logger.trace("Call {} failed with status {}. Response: {}", url, cr.getStatus(), + cr.getContentAsString()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/solarforecast.plane.status.http-status [\"" + cr.getStatus() + "\"]"); + } + } catch (ExecutionException | TimeoutException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } catch (InterruptedException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + Thread.currentThread().interrupt(); + } + } else { + // else use available forecast + updateChannels(forecast); + } + } else { + logger.warn("{} Location not present", thing.getLabel()); + } + return forecast; + } + + private void updateChannels(ForecastSolarObject f) { + ZonedDateTime now = ZonedDateTime.now(f.getZone()); + double energyDay = f.getDayTotal(now.toLocalDate()); + double energyProduced = f.getActualEnergyValue(now); + updateState(CHANNEL_ENERGY_ACTUAL, Utils.getEnergyState(energyProduced)); + updateState(CHANNEL_ENERGY_REMAIN, Utils.getEnergyState(energyDay - energyProduced)); + updateState(CHANNEL_ENERGY_TODAY, Utils.getEnergyState(energyDay)); + updateState(CHANNEL_POWER_ACTUAL, Utils.getPowerState(f.getActualPowerValue(now))); + } + + /** + * Used by Bridge to set location directly + * + * @param loc + */ + void setLocation(PointType loc) { + location = Optional.of(loc); + } + + void setApiKey(String key) { + apiKey = Optional.of(key); + } + + String getBaseUrl() { + String url = BASE_URL; + if (apiKey.isPresent()) { + url += apiKey.get() + SLASH; + } + return url; + } + + protected synchronized void setForecast(ForecastSolarObject f) { + forecast = f; + sendTimeSeries(CHANNEL_POWER_ESTIMATE, forecast.getPowerTimeSeries(QueryMode.Average)); + sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, forecast.getEnergyTimeSeries(QueryMode.Average)); + bridgeHandler.ifPresent(h -> { + h.forecastUpdate(); + }); + } + + @Override + public synchronized List getSolarForecasts() { + return List.of(forecast); + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastConstants.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastConstants.java new file mode 100644 index 00000000000..f55b807eb9d --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastConstants.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast.internal.solcast; + +import javax.measure.Unit; +import javax.measure.quantity.Power; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.unit.MetricPrefix; +import org.openhab.core.library.unit.Units; + +/** + * The {@link SolcastConstants} class defines common constants for Solcast Service + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class SolcastConstants { + private static final String BASE_URL = "https://api.solcast.com.au/rooftop_sites/"; + public static final String FORECAST_URL = BASE_URL + "%s/forecasts?format=json&hours=168"; + public static final String CURRENT_ESTIMATE_URL = BASE_URL + "%s/estimated_actuals?format=json"; + public static final String BEARER = "Bearer "; + public static final Unit KILOWATT_UNIT = MetricPrefix.KILO(Units.WATT); +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastObject.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastObject.java new file mode 100644 index 00000000000..667c0e77ff9 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastObject.java @@ -0,0 +1,498 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast.internal.solcast; + +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.TreeMap; + +import javax.measure.quantity.Energy; +import javax.measure.quantity.Power; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.json.JSONArray; +import org.json.JSONObject; +import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; +import org.openhab.binding.solarforecast.internal.SolarForecastException; +import org.openhab.binding.solarforecast.internal.actions.SolarForecast; +import org.openhab.binding.solarforecast.internal.utils.Utils; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.types.TimeSeries; +import org.openhab.core.types.TimeSeries.Policy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SolcastObject} holds complete data for forecast + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class SolcastObject implements SolarForecast { + private static final TreeMap EMPTY_MAP = new TreeMap<>(); + + private final Logger logger = LoggerFactory.getLogger(SolcastObject.class); + private final TreeMap estimationDataMap = new TreeMap<>(); + private final TreeMap optimisticDataMap = new TreeMap<>(); + private final TreeMap pessimisticDataMap = new TreeMap<>(); + private final TimeZoneProvider timeZoneProvider; + + private DateTimeFormatter dateOutputFormatter; + private String identifier; + private Optional rawData = Optional.of(new JSONObject()); + private Instant expirationDateTime; + private long period = 30; + + public enum QueryMode { + Average(SolarForecast.AVERAGE), + Optimistic(SolarForecast.OPTIMISTIC), + Pessimistic(SolarForecast.PESSIMISTIC), + Error("Error"); + + String modeDescirption; + + QueryMode(String description) { + modeDescirption = description; + } + + @Override + public String toString() { + return modeDescirption; + } + } + + public SolcastObject(String id, TimeZoneProvider tzp) { + // invalid forecast object + identifier = id; + timeZoneProvider = tzp; + dateOutputFormatter = DateTimeFormatter.ofPattern(SolarForecastBindingConstants.PATTERN_FORMAT) + .withZone(tzp.getTimeZone()); + expirationDateTime = Instant.now().minusSeconds(1); + } + + public SolcastObject(String id, String content, Instant expiration, TimeZoneProvider tzp) { + identifier = id; + expirationDateTime = expiration; + timeZoneProvider = tzp; + dateOutputFormatter = DateTimeFormatter.ofPattern(SolarForecastBindingConstants.PATTERN_FORMAT) + .withZone(tzp.getTimeZone()); + add(content); + } + + public void join(String content) { + add(content); + } + + private void add(String content) { + if (!content.isEmpty()) { + JSONObject contentJson = new JSONObject(content); + JSONArray resultJsonArray; + + // prepare data for raw channel + if (contentJson.has("forecasts")) { + resultJsonArray = contentJson.getJSONArray("forecasts"); + addJSONArray(resultJsonArray); + rawData.get().put("forecasts", resultJsonArray); + } + if (contentJson.has("estimated_actuals")) { + resultJsonArray = contentJson.getJSONArray("estimated_actuals"); + addJSONArray(resultJsonArray); + rawData.get().put("estimated_actuals", resultJsonArray); + } + } + } + + private void addJSONArray(JSONArray resultJsonArray) { + // sort data into TreeMaps + for (int i = 0; i < resultJsonArray.length(); i++) { + JSONObject jo = resultJsonArray.getJSONObject(i); + String periodEnd = jo.getString("period_end"); + ZonedDateTime periodEndZdt = getZdtFromUTC(periodEnd); + if (periodEndZdt == null) { + return; + } + estimationDataMap.put(periodEndZdt, jo.getDouble("pv_estimate")); + + // fill pessimistic values + if (jo.has("pv_estimate10")) { + pessimisticDataMap.put(periodEndZdt, jo.getDouble("pv_estimate10")); + } else { + pessimisticDataMap.put(periodEndZdt, jo.getDouble("pv_estimate")); + } + + // fill optimistic values + if (jo.has("pv_estimate90")) { + optimisticDataMap.put(periodEndZdt, jo.getDouble("pv_estimate90")); + } else { + optimisticDataMap.put(periodEndZdt, jo.getDouble("pv_estimate")); + } + if (jo.has("period")) { + period = Duration.parse(jo.getString("period")).toMinutes(); + } + } + } + + public boolean isExpired() { + return expirationDateTime.isBefore(Instant.now()); + } + + public double getActualEnergyValue(ZonedDateTime query, QueryMode mode) { + // calculate energy from day begin to latest entry BEFORE query + ZonedDateTime iterationDateTime = query.withHour(0).withMinute(0).withSecond(0); + TreeMap dtm = getDataMap(mode); + Entry nextEntry = dtm.higherEntry(iterationDateTime); + if (nextEntry == null) { + throwOutOfRangeException(query.toInstant()); + return -1; + } + double forecastValue = 0; + double previousEstimate = 0; + while (nextEntry.getKey().isBefore(query) || nextEntry.getKey().isEqual(query)) { + // value are reported in PT30M = 30 minutes interval with kw value + // for kw/h it's half the value + Double endValue = nextEntry.getValue(); + // production during period is half of previous and next value + double addedValue = ((endValue.doubleValue() + previousEstimate) / 2.0) * period / 60.0; + forecastValue += addedValue; + previousEstimate = endValue.doubleValue(); + iterationDateTime = nextEntry.getKey(); + nextEntry = dtm.higherEntry(iterationDateTime); + if (nextEntry == null) { + break; + } + } + // interpolate minutes AFTER query + Entry f = dtm.floorEntry(query); + Entry c = dtm.ceilingEntry(query); + if (f != null) { + if (c != null) { + long duration = Duration.between(f.getKey(), c.getKey()).toMinutes(); + // floor == ceiling: no addon calculation needed + if (duration == 0) { + return forecastValue; + } + if (c.getValue() > 0) { + double interpolation = Duration.between(f.getKey(), query).toMinutes() / 60.0; + double interpolationProduction = getActualPowerValue(query, mode) * interpolation; + forecastValue += interpolationProduction; + return forecastValue; + } else { + // if ceiling value is 0 there's no further production in this period + return forecastValue; + } + } else { + // if ceiling is null we're at the very end of the day + return forecastValue; + } + } else { + // if floor is null we're at the very beginning of the day => 0 + return 0; + } + } + + @Override + public TimeSeries getEnergyTimeSeries(QueryMode mode) { + TreeMap dtm = getDataMap(mode); + TimeSeries ts = new TimeSeries(Policy.REPLACE); + dtm.forEach((timestamp, energy) -> { + ts.add(timestamp.toInstant(), Utils.getEnergyState(getActualEnergyValue(timestamp, mode))); + }); + return ts; + } + + /** + * Get power values + */ + public double getActualPowerValue(ZonedDateTime query, QueryMode mode) { + if (query.toInstant().isBefore(getForecastBegin()) || query.toInstant().isAfter(getForecastEnd())) { + throwOutOfRangeException(query.toInstant()); + } + TreeMap dtm = getDataMap(mode); + double actualPowerValue = 0; + Entry f = dtm.floorEntry(query); + Entry c = dtm.ceilingEntry(query); + if (f != null) { + if (c != null) { + double powerCeiling = c.getValue(); + long duration = Duration.between(f.getKey(), c.getKey()).toMinutes(); + // floor == ceiling: return power from node, no interpolation needed + if (duration == 0) { + return powerCeiling; + } + if (powerCeiling > 0) { + double powerFloor = f.getValue(); + // calculate in minutes from floor to now, e.g. 20 minutes from PT30M 30 minutes + // => take 1/3 of floor and 2/3 of ceiling + double interpolation = Duration.between(f.getKey(), query).toMinutes() / (double) period; + actualPowerValue = ((1 - interpolation) * powerFloor) + (interpolation * powerCeiling); + return actualPowerValue; + } else { + // if power ceiling == 0 there's no production in this period + return 0; + } + } else { + // if ceiling is null we're at the very end of this day => 0 + return 0; + } + } else { + // if floor is null we're at the very beginning of this day => 0 + return 0; + } + } + + @Override + public TimeSeries getPowerTimeSeries(QueryMode mode) { + TreeMap dtm = getDataMap(mode); + TimeSeries ts = new TimeSeries(Policy.REPLACE); + dtm.forEach((timestamp, power) -> { + ts.add(timestamp.toInstant(), Utils.getPowerState(power)); + }); + return ts; + } + + /** + * Daily totals + */ + public double getDayTotal(LocalDate query, QueryMode mode) { + TreeMap dtm = getDataMap(mode); + ZonedDateTime iterationDateTime = query.atStartOfDay(timeZoneProvider.getTimeZone()); + Entry nextEntry = dtm.higherEntry(iterationDateTime); + if (nextEntry == null) { + throw new SolarForecastException(this, "Day " + query + " not available in forecast. " + getTimeRange()); + } + ZonedDateTime endDateTime = iterationDateTime.plusDays(1); + double forecastValue = 0; + double previousEstimate = 0; + while (nextEntry.getKey().isBefore(endDateTime)) { + // value are reported in PT30M = 30 minutes interval with kw value + // for kw/h it's half the value + Double endValue = nextEntry.getValue(); + // production during period is half of previous and next value + double addedValue = ((endValue.doubleValue() + previousEstimate) / 2.0) * period / 60.0; + forecastValue += addedValue; + previousEstimate = endValue.doubleValue(); + iterationDateTime = nextEntry.getKey(); + nextEntry = dtm.higherEntry(iterationDateTime); + if (nextEntry == null) { + break; + } + } + return forecastValue; + } + + public double getRemainingProduction(ZonedDateTime query, QueryMode mode) { + return getDayTotal(query.toLocalDate(), mode) - getActualEnergyValue(query, mode); + } + + @Override + public String toString() { + return "Expiration: " + expirationDateTime + ", Data: " + estimationDataMap; + } + + public String getRaw() { + if (rawData.isPresent()) { + return rawData.get().toString(); + } + return "{}"; + } + + private TreeMap getDataMap(QueryMode mode) { + TreeMap returnMap = EMPTY_MAP; + switch (mode) { + case Average: + returnMap = estimationDataMap; + break; + case Optimistic: + returnMap = optimisticDataMap; + break; + case Pessimistic: + returnMap = pessimisticDataMap; + break; + case Error: + // nothing to do + break; + default: + // nothing to do + break; + } + return returnMap; + } + + public @Nullable ZonedDateTime getZdtFromUTC(String utc) { + try { + Instant timestamp = Instant.parse(utc); + return timestamp.atZone(timeZoneProvider.getTimeZone()); + } catch (DateTimeParseException dtpe) { + logger.warn("Exception parsing time {} Reason: {}", utc, dtpe.getMessage()); + } + return null; + } + + /** + * SolarForecast Interface + */ + @Override + public QuantityType getDay(LocalDate date, String... args) throws IllegalArgumentException { + QueryMode mode = evalArguments(args); + if (mode.equals(QueryMode.Error)) { + if (args.length > 1) { + throw new IllegalArgumentException("Solcast doesn't support " + args.length + " arguments"); + } else { + throw new IllegalArgumentException("Solcast doesn't support argument " + args[0]); + } + } else if (mode.equals(QueryMode.Optimistic) || mode.equals(QueryMode.Pessimistic)) { + if (date.isBefore(LocalDate.now())) { + throw new IllegalArgumentException( + "Solcast argument " + mode.toString() + " only available for future values"); + } + } + double measure = getDayTotal(date, mode); + return Utils.getEnergyState(measure); + } + + @Override + public QuantityType getEnergy(Instant start, Instant end, String... args) throws IllegalArgumentException { + if (end.isBefore(start)) { + if (args.length > 1) { + throw new IllegalArgumentException("Solcast doesn't support " + args.length + " arguments"); + } else { + throw new IllegalArgumentException("Solcast doesn't support argument " + args[0]); + } + } + QueryMode mode = evalArguments(args); + if (mode.equals(QueryMode.Error)) { + return Utils.getEnergyState(-1); + } else if (mode.equals(QueryMode.Optimistic) || mode.equals(QueryMode.Pessimistic)) { + if (end.isBefore(Instant.now())) { + throw new IllegalArgumentException( + "Solcast argument " + mode.toString() + " only available for future values"); + } + } + LocalDate beginDate = start.atZone(timeZoneProvider.getTimeZone()).toLocalDate(); + LocalDate endDate = end.atZone(timeZoneProvider.getTimeZone()).toLocalDate(); + double measure = -1; + if (beginDate.isEqual(endDate)) { + measure = getDayTotal(beginDate, mode) + - getActualEnergyValue(start.atZone(timeZoneProvider.getTimeZone()), mode) + - getRemainingProduction(end.atZone(timeZoneProvider.getTimeZone()), mode); + } else { + measure = getRemainingProduction(start.atZone(timeZoneProvider.getTimeZone()), mode); + beginDate = beginDate.plusDays(1); + while (beginDate.isBefore(endDate) && measure >= 0) { + double day = getDayTotal(beginDate, mode); + if (day > 0) { + measure += day; + } + beginDate = beginDate.plusDays(1); + } + double lastDay = getActualEnergyValue(end.atZone(timeZoneProvider.getTimeZone()), mode); + if (lastDay >= 0) { + measure += lastDay; + } + } + return Utils.getEnergyState(measure); + } + + @Override + public QuantityType getPower(Instant timestamp, String... args) throws IllegalArgumentException { + // eliminate error cases and return immediately + QueryMode mode = evalArguments(args); + if (mode.equals(QueryMode.Error)) { + if (args.length > 1) { + throw new IllegalArgumentException("Solcast doesn't support " + args.length + " arguments"); + } else { + throw new IllegalArgumentException("Solcast doesn't support argument " + args[0]); + } + } else if (mode.equals(QueryMode.Optimistic) || mode.equals(QueryMode.Pessimistic)) { + if (timestamp.isBefore(Instant.now().minus(1, ChronoUnit.MINUTES))) { + throw new IllegalArgumentException( + "Solcast argument " + mode.toString() + " only available for future values"); + } + } + double measure = getActualPowerValue(ZonedDateTime.ofInstant(timestamp, timeZoneProvider.getTimeZone()), mode); + return Utils.getPowerState(measure); + } + + @Override + public Instant getForecastBegin() { + if (!estimationDataMap.isEmpty()) { + return estimationDataMap.firstEntry().getKey().toInstant(); + } + return Instant.MAX; + } + + @Override + public Instant getForecastEnd() { + if (!estimationDataMap.isEmpty()) { + return estimationDataMap.lastEntry().getKey().toInstant(); + } + return Instant.MIN; + } + + private QueryMode evalArguments(String[] args) { + if (args.length > 0) { + if (args.length > 1) { + logger.info("Too many arguments {}", Arrays.toString(args)); + return QueryMode.Error; + } + + if (SolarForecast.OPTIMISTIC.equals(args[0])) { + return QueryMode.Optimistic; + } else if (SolarForecast.PESSIMISTIC.equals(args[0])) { + return QueryMode.Pessimistic; + } else if (SolarForecast.AVERAGE.equals(args[0])) { + return QueryMode.Average; + } else { + logger.info("Argument {} not supported", args[0]); + return QueryMode.Error; + } + } else { + return QueryMode.Average; + } + } + + @Override + public String getIdentifier() { + return identifier; + } + + private void throwOutOfRangeException(Instant query) { + if (getForecastBegin().equals(Instant.MAX) || getForecastEnd().equals(Instant.MIN)) { + throw new SolarForecastException(this, "Forecast invalid time range"); + } + if (query.isBefore(getForecastBegin())) { + throw new SolarForecastException(this, + "Query " + dateOutputFormatter.format(query) + " too early. " + getTimeRange()); + } else if (query.isAfter(getForecastEnd())) { + throw new SolarForecastException(this, + "Query " + dateOutputFormatter.format(query) + " too late. " + getTimeRange()); + } else { + logger.warn("Query {} is fine. {}", dateOutputFormatter.format(query), getTimeRange()); + } + } + + private String getTimeRange() { + return "Valid range: " + dateOutputFormatter.format(getForecastBegin()) + " - " + + dateOutputFormatter.format(getForecastEnd()); + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/config/SolcastBridgeConfiguration.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/config/SolcastBridgeConfiguration.java new file mode 100644 index 00000000000..248b718854c --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/config/SolcastBridgeConfiguration.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast.internal.solcast.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; + +/** + * The {@link SolcastBridgeConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class SolcastBridgeConfiguration { + public String apiKey = SolarForecastBindingConstants.EMPTY; + public String timeZone = SolarForecastBindingConstants.EMPTY; +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/config/SolcastPlaneConfiguration.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/config/SolcastPlaneConfiguration.java new file mode 100644 index 00000000000..1a2d2c1de8a --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/config/SolcastPlaneConfiguration.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast.internal.solcast.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; + +/** + * The {@link SolcastPlaneConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class SolcastPlaneConfiguration { + public String resourceId = SolarForecastBindingConstants.EMPTY; + public long refreshInterval = 120; +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastBridgeHandler.java new file mode 100644 index 00000000000..de5fe18eab2 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastBridgeHandler.java @@ -0,0 +1,268 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast.internal.solcast.handler; + +import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; + +import java.time.DateTimeException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.TreeMap; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.solarforecast.internal.SolarForecastException; +import org.openhab.binding.solarforecast.internal.actions.SolarForecast; +import org.openhab.binding.solarforecast.internal.actions.SolarForecastActions; +import org.openhab.binding.solarforecast.internal.actions.SolarForecastProvider; +import org.openhab.binding.solarforecast.internal.solcast.SolcastObject; +import org.openhab.binding.solarforecast.internal.solcast.SolcastObject.QueryMode; +import org.openhab.binding.solarforecast.internal.solcast.config.SolcastBridgeConfiguration; +import org.openhab.binding.solarforecast.internal.utils.Utils; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.TimeSeries; +import org.openhab.core.types.TimeSeries.Policy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SolcastBridgeHandler} is a non active handler instance. It will be triggered by the bridge. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class SolcastBridgeHandler extends BaseBridgeHandler implements SolarForecastProvider, TimeZoneProvider { + private final Logger logger = LoggerFactory.getLogger(SolcastBridgeHandler.class); + + private List planes = new ArrayList<>(); + private Optional> refreshJob = Optional.empty(); + private SolcastBridgeConfiguration configuration = new SolcastBridgeConfiguration(); + private ZoneId timeZone; + + public SolcastBridgeHandler(Bridge bridge, TimeZoneProvider tzp) { + super(bridge); + timeZone = tzp.getTimeZone(); + } + + @Override + public Collection> getServices() { + return List.of(SolarForecastActions.class); + } + + @Override + public void initialize() { + configuration = getConfigAs(SolcastBridgeConfiguration.class); + if (!configuration.apiKey.isBlank()) { + if (!configuration.timeZone.isBlank()) { + try { + timeZone = ZoneId.of(configuration.timeZone); + } catch (DateTimeException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/solarforecast.site.status.timezone" + " [\"" + configuration.timeZone + "\"]"); + return; + } + } + updateStatus(ThingStatus.UNKNOWN); + refreshJob = Optional + .of(scheduler.scheduleWithFixedDelay(this::getData, 0, REFRESH_ACTUAL_INTERVAL, TimeUnit.MINUTES)); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/solarforecast.site.status.api-key-missing"); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + String channel = channelUID.getIdWithoutGroup(); + switch (channel) { + case CHANNEL_ENERGY_ACTUAL: + case CHANNEL_ENERGY_REMAIN: + case CHANNEL_ENERGY_TODAY: + case CHANNEL_POWER_ACTUAL: + getData(); + break; + case CHANNEL_POWER_ESTIMATE: + case CHANNEL_ENERGY_ESTIMATE: + forecastUpdate(); + break; + } + } + } + + @Override + public void dispose() { + refreshJob.ifPresent(job -> job.cancel(true)); + } + + /** + * Get data for all planes. Protect plane list from being modified during update + */ + public synchronized void getData() { + if (planes.isEmpty()) { + logger.debug("No PV plane defined yet"); + return; + } + ZonedDateTime now = ZonedDateTime.now(getTimeZone()); + List modes = List.of(QueryMode.Average, QueryMode.Pessimistic, QueryMode.Optimistic); + modes.forEach(mode -> { + String group = switch (mode) { + case Average -> GROUP_AVERAGE; + case Optimistic -> GROUP_OPTIMISTIC; + case Pessimistic -> GROUP_PESSIMISTIC; + default -> GROUP_AVERAGE; + }; + boolean update = true; + double energySum = 0; + double powerSum = 0; + double daySum = 0; + for (Iterator iterator = planes.iterator(); iterator.hasNext();) { + try { + SolcastPlaneHandler sfph = iterator.next(); + SolcastObject fo = sfph.fetchData(); + energySum += fo.getActualEnergyValue(now, mode); + powerSum += fo.getActualPowerValue(now, mode); + daySum += fo.getDayTotal(now.toLocalDate(), mode); + } catch (SolarForecastException sfe) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, + "@text/solarforecast.site.status.exception [\"" + sfe.getMessage() + "\"]"); + update = false; + } + } + if (update) { + updateStatus(ThingStatus.ONLINE); + updateState(group + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_ACTUAL, + Utils.getEnergyState(energySum)); + updateState(group + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_REMAIN, + Utils.getEnergyState(daySum - energySum)); + updateState(group + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_TODAY, + Utils.getEnergyState(daySum)); + updateState(group + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_POWER_ACTUAL, + Utils.getPowerState(powerSum)); + } + }); + } + + public synchronized void forecastUpdate() { + if (planes.isEmpty()) { + return; + } + // get all available forecasts + List forecastObjects = new ArrayList<>(); + for (Iterator iterator = planes.iterator(); iterator.hasNext();) { + SolcastPlaneHandler sfph = iterator.next(); + forecastObjects.addAll(sfph.getSolarForecasts()); + } + // sort in Tree according to times for each scenario + List modes = List.of(QueryMode.Average, QueryMode.Pessimistic, QueryMode.Optimistic); + modes.forEach(mode -> { + TreeMap> combinedPowerForecast = new TreeMap<>(); + TreeMap> combinedEnergyForecast = new TreeMap<>(); + + // bugfix: https://github.com/weymann/OH3-SolarForecast-Drops/issues/5 + // find common start and end time which fits to all forecast objects to avoid ambiguous values + final Instant commonStart = Utils.getCommonStartTime(forecastObjects); + final Instant commonEnd = Utils.getCommonEndTime(forecastObjects); + forecastObjects.forEach(fc -> { + TimeSeries powerTS = fc.getPowerTimeSeries(mode); + powerTS.getStates().forEach(entry -> { + if (Utils.isAfterOrEqual(entry.timestamp(), commonStart) + && Utils.isBeforeOrEqual(entry.timestamp(), commonEnd)) { + Utils.addState(combinedPowerForecast, entry); + } + }); + TimeSeries energyTS = fc.getEnergyTimeSeries(mode); + energyTS.getStates().forEach(entry -> { + if (Utils.isAfterOrEqual(entry.timestamp(), commonStart) + && Utils.isBeforeOrEqual(entry.timestamp(), commonEnd)) { + Utils.addState(combinedEnergyForecast, entry); + } + }); + }); + // create TimeSeries and distribute + TimeSeries powerSeries = new TimeSeries(Policy.REPLACE); + combinedPowerForecast.forEach((timestamp, state) -> { + powerSeries.add(timestamp, state); + }); + + TimeSeries energySeries = new TimeSeries(Policy.REPLACE); + combinedEnergyForecast.forEach((timestamp, state) -> { + energySeries.add(timestamp, state); + }); + switch (mode) { + case Average: + sendTimeSeries(GROUP_AVERAGE + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_ESTIMATE, + energySeries); + sendTimeSeries(GROUP_AVERAGE + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_POWER_ESTIMATE, + powerSeries); + break; + case Optimistic: + sendTimeSeries(GROUP_OPTIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_ESTIMATE, + energySeries); + sendTimeSeries(GROUP_OPTIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_POWER_ESTIMATE, + powerSeries); + break; + case Pessimistic: + sendTimeSeries(GROUP_PESSIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_ESTIMATE, + energySeries); + sendTimeSeries(GROUP_PESSIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_POWER_ESTIMATE, + powerSeries); + break; + default: + break; + } + }); + } + + public synchronized void addPlane(SolcastPlaneHandler sph) { + planes.add(sph); + } + + public synchronized void removePlane(SolcastPlaneHandler sph) { + planes.remove(sph); + } + + String getApiKey() { + return configuration.apiKey; + } + + @Override + public synchronized List getSolarForecasts() { + List l = new ArrayList<>(); + planes.forEach(entry -> { + l.addAll(entry.getSolarForecasts()); + }); + return l; + } + + @Override + public ZoneId getTimeZone() { + return timeZone; + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastPlaneHandler.java new file mode 100644 index 00000000000..89c46564cfa --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastPlaneHandler.java @@ -0,0 +1,254 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast.internal.solcast.handler; + +import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; +import static org.openhab.binding.solarforecast.internal.solcast.SolcastConstants.*; + +import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpHeader; +import org.openhab.binding.solarforecast.internal.actions.SolarForecast; +import org.openhab.binding.solarforecast.internal.actions.SolarForecastActions; +import org.openhab.binding.solarforecast.internal.actions.SolarForecastProvider; +import org.openhab.binding.solarforecast.internal.solcast.SolcastObject; +import org.openhab.binding.solarforecast.internal.solcast.SolcastObject.QueryMode; +import org.openhab.binding.solarforecast.internal.solcast.config.SolcastPlaneConfiguration; +import org.openhab.binding.solarforecast.internal.utils.Utils; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.thing.binding.BridgeHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SolcastPlaneHandler} is a non active handler instance. It will be triggerer by the bridge. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class SolcastPlaneHandler extends BaseThingHandler implements SolarForecastProvider { + private final Logger logger = LoggerFactory.getLogger(SolcastPlaneHandler.class); + private final HttpClient httpClient; + private SolcastPlaneConfiguration configuration = new SolcastPlaneConfiguration(); + private Optional bridgeHandler = Optional.empty(); + protected Optional forecast = Optional.empty(); + + public SolcastPlaneHandler(Thing thing, HttpClient hc) { + super(thing); + httpClient = hc; + } + + @Override + public Collection> getServices() { + return List.of(SolarForecastActions.class); + } + + @Override + public void initialize() { + configuration = getConfigAs(SolcastPlaneConfiguration.class); + + // connect Bridge & Status + Bridge bridge = getBridge(); + if (bridge != null) { + BridgeHandler handler = bridge.getHandler(); + if (handler != null) { + if (handler instanceof SolcastBridgeHandler sbh) { + bridgeHandler = Optional.of(sbh); + forecast = Optional.of(new SolcastObject(thing.getUID().getAsString(), sbh)); + sbh.addPlane(this); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/solarforecast.plane.status.wrong-handler [\"" + handler + "\"]"); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/solarforecast.plane.status.bridge-handler-not-found"); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/solarforecast.plane.status.bridge-missing"); + } + } + + @Override + public void dispose() { + super.dispose(); + bridgeHandler.ifPresent(bridge -> bridge.removePlane(this)); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + forecast.ifPresent(forecastObject -> { + String group = channelUID.getGroupId(); + if (group == null) { + group = EMPTY; + } + String channel = channelUID.getIdWithoutGroup(); + QueryMode mode = QueryMode.Average; + switch (group) { + case GROUP_AVERAGE: + mode = QueryMode.Average; + break; + case GROUP_OPTIMISTIC: + mode = QueryMode.Optimistic; + break; + case GROUP_PESSIMISTIC: + mode = QueryMode.Pessimistic; + break; + case GROUP_RAW: + forecast.ifPresent(f -> { + updateState(GROUP_RAW + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_JSON, + StringType.valueOf(f.getRaw())); + }); + } + switch (channel) { + case CHANNEL_ENERGY_ESTIMATE: + sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, forecastObject.getEnergyTimeSeries(mode)); + break; + case CHANNEL_POWER_ESTIMATE: + sendTimeSeries(CHANNEL_POWER_ESTIMATE, forecastObject.getPowerTimeSeries(mode)); + break; + default: + updateChannels(forecastObject); + } + }); + } + } + + protected synchronized SolcastObject fetchData() { + bridgeHandler.ifPresent(bridge -> { + forecast.ifPresent(forecastObject -> { + if (forecastObject.isExpired()) { + logger.trace("Get new forecast {}", forecastObject.toString()); + String forecastUrl = String.format(FORECAST_URL, configuration.resourceId); + String currentEstimateUrl = String.format(CURRENT_ESTIMATE_URL, configuration.resourceId); + try { + // get actual estimate + Request estimateRequest = httpClient.newRequest(currentEstimateUrl); + estimateRequest.header(HttpHeader.AUTHORIZATION, BEARER + bridge.getApiKey()); + ContentResponse crEstimate = estimateRequest.send(); + if (crEstimate.getStatus() == 200) { + SolcastObject localForecast = new SolcastObject(thing.getUID().getAsString(), + crEstimate.getContentAsString(), + Instant.now().plus(configuration.refreshInterval, ChronoUnit.MINUTES), bridge); + + // get forecast + Request forecastRequest = httpClient.newRequest(forecastUrl); + forecastRequest.header(HttpHeader.AUTHORIZATION, BEARER + bridge.getApiKey()); + ContentResponse crForecast = forecastRequest.send(); + + if (crForecast.getStatus() == 200) { + localForecast.join(crForecast.getContentAsString()); + setForecast(localForecast); + updateState(GROUP_RAW + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_JSON, + StringType.valueOf(forecast.get().getRaw())); + updateStatus(ThingStatus.ONLINE); + } else { + logger.debug("{} Call {} failed {}", thing.getLabel(), forecastUrl, + crForecast.getStatus()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/solarforecast.plane.status.http-status [\"" + crForecast.getStatus() + + "\"]"); + } + } else { + logger.debug("{} Call {} failed {}", thing.getLabel(), currentEstimateUrl, + crEstimate.getStatus()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/solarforecast.plane.status.http-status [\"" + crEstimate.getStatus() + + "\"]"); + } + } catch (ExecutionException | TimeoutException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } catch (InterruptedException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + Thread.currentThread().interrupt(); + } + } else { + updateChannels(forecastObject); + } + }); + }); + return forecast.get(); + } + + protected void updateChannels(SolcastObject f) { + if (bridgeHandler.isEmpty()) { + return; + } + ZonedDateTime now = ZonedDateTime.now(bridgeHandler.get().getTimeZone()); + List modes = List.of(QueryMode.Average, QueryMode.Pessimistic, QueryMode.Optimistic); + modes.forEach(mode -> { + double energyDay = f.getDayTotal(now.toLocalDate(), mode); + double energyProduced = f.getActualEnergyValue(now, mode); + String group = switch (mode) { + case Average -> GROUP_AVERAGE; + case Optimistic -> GROUP_OPTIMISTIC; + case Pessimistic -> GROUP_PESSIMISTIC; + case Error -> throw new IllegalStateException("mode " + mode + " not expected"); + }; + updateState(group + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_ACTUAL, + Utils.getEnergyState(energyProduced)); + updateState(group + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_REMAIN, + Utils.getEnergyState(energyDay - energyProduced)); + updateState(group + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_TODAY, + Utils.getEnergyState(energyDay)); + updateState(group + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_POWER_ACTUAL, + Utils.getPowerState(f.getActualPowerValue(now, QueryMode.Average))); + }); + } + + protected synchronized void setForecast(SolcastObject f) { + forecast = Optional.of(f); + sendTimeSeries(GROUP_AVERAGE + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_POWER_ESTIMATE, + f.getPowerTimeSeries(QueryMode.Average)); + sendTimeSeries(GROUP_AVERAGE + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_ESTIMATE, + f.getEnergyTimeSeries(QueryMode.Average)); + sendTimeSeries(GROUP_OPTIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_POWER_ESTIMATE, + f.getPowerTimeSeries(QueryMode.Optimistic)); + sendTimeSeries(GROUP_OPTIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_ESTIMATE, + f.getEnergyTimeSeries(QueryMode.Optimistic)); + sendTimeSeries(GROUP_PESSIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_POWER_ESTIMATE, + f.getPowerTimeSeries(QueryMode.Pessimistic)); + sendTimeSeries(GROUP_PESSIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_ESTIMATE, + f.getEnergyTimeSeries(QueryMode.Pessimistic)); + bridgeHandler.ifPresent(h -> { + h.forecastUpdate(); + }); + } + + @Override + public synchronized List getSolarForecasts() { + return List.of(forecast.get()); + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/utils/Utils.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/utils/Utils.java new file mode 100644 index 00000000000..44844a6db25 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/utils/Utils.java @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast.internal.utils; + +import java.time.Instant; +import java.util.Iterator; +import java.util.List; +import java.util.TreeMap; + +import javax.measure.MetricPrefix; +import javax.measure.quantity.Energy; +import javax.measure.quantity.Power; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.solarforecast.internal.actions.SolarForecast; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.types.TimeSeries.Entry; + +/** + * The {@link Utils} Helpers for Solcast and ForecastSolar + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class Utils { + public static QuantityType getEnergyState(double d) { + if (d < 0) { + return QuantityType.valueOf(-1, Units.KILOWATT_HOUR); + } + return QuantityType.valueOf(Math.round(d * 1000) / 1000.0, Units.KILOWATT_HOUR); + } + + public static QuantityType getPowerState(double d) { + if (d < 0) { + return QuantityType.valueOf(-1, MetricPrefix.KILO(Units.WATT)); + } + return QuantityType.valueOf(Math.round(d * 1000) / 1000.0, MetricPrefix.KILO(Units.WATT)); + } + + public static void addState(TreeMap> map, Entry entry) { + Instant timestamp = entry.timestamp(); + QuantityType qt1 = map.get(timestamp); + if (qt1 != null) { + QuantityType qt2 = (QuantityType) entry.state(); + double combinedValue = qt1.doubleValue() + qt2.doubleValue(); + map.put(timestamp, QuantityType.valueOf(combinedValue, qt2.getUnit())); + } else { + map.put(timestamp, (QuantityType) entry.state()); + } + } + + public static boolean isBeforeOrEqual(Instant query, Instant reference) { + return !query.isAfter(reference); + } + + public static boolean isAfterOrEqual(Instant query, Instant reference) { + return !query.isBefore(reference); + } + + public static Instant getCommonStartTime(List forecastObjects) { + if (forecastObjects.isEmpty()) { + return Instant.MAX; + } + Instant start = Instant.MIN; + for (Iterator iterator = forecastObjects.iterator(); iterator.hasNext();) { + SolarForecast sf = iterator.next(); + // if start is maximum there's no forecast data available - return immediately + if (sf.getForecastBegin().equals(Instant.MAX)) { + return Instant.MAX; + } else if (sf.getForecastBegin().isAfter(start)) { + // take latest timestamp from all forecasts + start = sf.getForecastBegin(); + } + } + return start; + } + + public static Instant getCommonEndTime(List forecastObjects) { + if (forecastObjects.isEmpty()) { + return Instant.MIN; + } + Instant end = Instant.MAX; + for (Iterator iterator = forecastObjects.iterator(); iterator.hasNext();) { + SolarForecast sf = iterator.next(); + // if end is minimum there's no forecast data available - return immediately + if (sf.getForecastEnd().equals(Instant.MIN)) { + return Instant.MIN; + } else if (sf.getForecastEnd().isBefore(end)) { + // take earliest timestamp from all forecast + end = sf.getForecastEnd(); + } + } + return end; + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 00000000000..faaa013112b --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,10 @@ + + + + binding + SolarForecast Binding + Solar Forecast for your location + cloud + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-config.xml new file mode 100644 index 00000000000..3e413fcb328 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-config.xml @@ -0,0 +1,43 @@ + + + + + + + Data refresh rate of forecast data in minutes + 30 + + + + 0 for horizontal till 90 for vertical declination + + + + -180 = north, -90 = east, 0 = south, 90 = west, 180 = north + + + + Installed module power of this plane + + + + Damping factor of morning hours + 0.25 + true + + + + Damping factor of evening hours + 0.25 + true + + + + Horizon definition as comma-separated integer values + true + + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-site-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-site-config.xml new file mode 100644 index 00000000000..081ac44e099 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-site-config.xml @@ -0,0 +1,22 @@ + + + + + + location + + Location of photovoltaic system. Location from openHAB settings is used in case of empty value. + + + + If you have a paid subscription plan + + + + Inverter maximum kilowatt peak capability + + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-config.xml new file mode 100644 index 00000000000..d17426c82e4 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-config.xml @@ -0,0 +1,18 @@ + + + + + + + Resource Id of Solcast rooftop site + + + + Data refresh rate of forecast data in minutes + 120 + + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-site-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-site-config.xml new file mode 100644 index 00000000000..956eec83c08 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-site-config.xml @@ -0,0 +1,18 @@ + + + + + + + API key from your subscription + + + + Time zone of forecast location + true + + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast.properties b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast.properties new file mode 100644 index 00000000000..a2fe132236b --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast.properties @@ -0,0 +1,108 @@ +# add-on + +addon.solarforecast.name = SolarForecast Binding +addon.solarforecast.description = Solar Forecast for your location + +# thing types + +thing-type.solarforecast.fs-plane.label = ForecastSolar PV Plane +thing-type.solarforecast.fs-plane.description = PV Plane as part of Multi Plane Bridge +thing-type.solarforecast.fs-site.label = ForecastSolar Site +thing-type.solarforecast.fs-site.description = Site location for Forecast Solar +thing-type.solarforecast.sc-plane.label = Solcast PV Plane +thing-type.solarforecast.sc-plane.description = PV Plane as part of Multi Plane Bridge +thing-type.solarforecast.sc-site.label = Solcast Site +thing-type.solarforecast.sc-site.description = Solcast service site definition + +# thing types config + +thing-type.config.solarforecast.fs-plane.azimuth.label = Plane Azimuth +thing-type.config.solarforecast.fs-plane.azimuth.description = -180 = north, -90 = east, 0 = south, 90 = west, 180 = north +thing-type.config.solarforecast.fs-plane.dampAM.label = Morning Damping Factor +thing-type.config.solarforecast.fs-plane.dampAM.description = Damping factor of morning hours +thing-type.config.solarforecast.fs-plane.dampPM.label = Evening Damping Factor +thing-type.config.solarforecast.fs-plane.dampPM.description = Damping factor of evening hours +thing-type.config.solarforecast.fs-plane.declination.label = Plane Declination +thing-type.config.solarforecast.fs-plane.declination.description = 0 for horizontal till 90 for vertical declination +thing-type.config.solarforecast.fs-plane.horizon.label = Horizon +thing-type.config.solarforecast.fs-plane.horizon.description = Horizon definition as comma-separated integer values +thing-type.config.solarforecast.fs-plane.kwp.label = Installed Kilowatt Peak +thing-type.config.solarforecast.fs-plane.kwp.description = Installed module power of this plane +thing-type.config.solarforecast.fs-plane.refreshInterval.label = Forecast Refresh Interval +thing-type.config.solarforecast.fs-plane.refreshInterval.description = Data refresh rate of forecast data in minutes +thing-type.config.solarforecast.fs-site.apiKey.label = API Key +thing-type.config.solarforecast.fs-site.apiKey.description = If you have a paid subscription plan +thing-type.config.solarforecast.fs-site.inverterKwp.label = Inverter Kilowatt Peak +thing-type.config.solarforecast.fs-site.inverterKwp.description = Inverter maximum kilowatt peak capability +thing-type.config.solarforecast.fs-site.location.label = PV Location +thing-type.config.solarforecast.fs-site.location.description = Location of photovoltaic system +thing-type.config.solarforecast.sc-plane.refreshInterval.label = Forecast Refresh Interval +thing-type.config.solarforecast.sc-plane.refreshInterval.description = Data refresh rate of forecast data in minutes +thing-type.config.solarforecast.sc-plane.resourceId.label = Rooftop Resource Id +thing-type.config.solarforecast.sc-plane.resourceId.description = Resource Id of Solcast rooftop site +thing-type.config.solarforecast.sc-site.apiKey.label = API Key +thing-type.config.solarforecast.sc-site.apiKey.description = API key from your subscription +thing-type.config.solarforecast.sc-site.timeZone.label = Time Zone +thing-type.config.solarforecast.sc-site.timeZone.description = Time zone of forecast location + +# channel group types + +channel-group-type.solarforecast.average-values.label = Average Forecast Values +channel-group-type.solarforecast.average-values.description = Forecast values showing average case data +channel-group-type.solarforecast.optimistic-values.label = Optimistic Forecast Values +channel-group-type.solarforecast.optimistic-values.description = Forecast values showing 90th percentile case data +channel-group-type.solarforecast.pessimistic-values.label = Pessimistic Forecast Values +channel-group-type.solarforecast.pessimistic-values.description = Forecast values showing 10th percentile case data +channel-group-type.solarforecast.raw-values.label = Raw Forecast Values +channel-group-type.solarforecast.raw-values.description = Raw response from service provider + +# channel types + +channel-type.solarforecast.energy-actual.label = Actual Energy Forecast +channel-type.solarforecast.energy-actual.description = Today's forecast till now +channel-type.solarforecast.energy-estimate.label = Energy Forecast +channel-type.solarforecast.energy-estimate.description = Energy forecast for next hours/days +channel-type.solarforecast.energy-remain.label = Remaining Energy Forecast +channel-type.solarforecast.energy-remain.description = Today's remaining forecast till sunset +channel-type.solarforecast.energy-today.label = Todays Energy Forecast +channel-type.solarforecast.energy-today.description = Today's total energy forecast +channel-type.solarforecast.json.label = Raw JSON Response +channel-type.solarforecast.json.description = Plain JSON response without conversions +channel-type.solarforecast.power-actual.label = Actual Power +channel-type.solarforecast.power-actual.description = Power prediction for this moment +channel-type.solarforecast.power-estimate.label = Power Forecast +channel-type.solarforecast.power-estimate.description = Power forecast for next hours/days + +# status details + +solarforecast.site.status.api-key-missing = API key is mandatory +solarforecast.site.status.timezone = Time zone {0} not found +solarforecast.site.status.location-missing = Location neither configured in openHAB nor configuration +solarforecast.site.status.exception = Exception during update: {0} +solarforecast.plane.status.bridge-missing = Bridge not set +solarforecast.plane.status.bridge-handler-not-found = Bridge handler not found +solarforecast.plane.status.wrong-handler = Wrong handler {0} +solarforecast.plane.status.await-feedback = Await first feedback +solarforecast.plane.status.http-status = HTTP Status Code {0} +solarforecast.plane.status.json-status = JSON error: {0} + +# thing actions + +actionDayLabel = Daily Energy Production +actionDayDesc = Returns energy production for complete day in kWh +actionInputDayLabel = Date +actionInputDayDesc = LocalDate for daily energy query +actionPowerLabel = Power +actionPowerDesc = Returns power in W for a specific point in time +actionInputDateTimeLabel = Date Time +actionInputDateTimeDesc = Instant timestamp for power query +actionEnergyLabel = Energy Production +actionEnergyDesc = Returns energy productions between two different timestamps +actionInputDateTimeBeginLabel = Timestamp Begin +actionInputDateTimeBeginDesc = Instant timestamp as starting point of the energy query +actionInputDateTimeEndLabel = TimeStamp End +actionInputDateTimeEndDesc = Instant timestamp as end point of the energy query +actionForecastBeginLabel = Forecast Startpoint +actionForecastBeginDesc = Returns earliest timestamp of forecast data +actionForecastEndLabel = Forecast End +actionForecastEndDesc = Returns latest timestamp of forecast data diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/average-group.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/average-group.xml new file mode 100644 index 00000000000..8f00a6c3f59 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/average-group.xml @@ -0,0 +1,18 @@ + + + + + Forecast values showing average case data + + + + + + + + + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/channel-types.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/channel-types.xml new file mode 100644 index 00000000000..b6302715b91 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/channel-types.xml @@ -0,0 +1,48 @@ + + + + + Number:Power + + Power prediction for this moment + + + + Number:Power + + Power forecast for next hours/days + + + + Number:Energy + + Today's forecast till now + + + + Number:Energy + + Today's remaining forecast till sunset + + + + Number:Energy + + Today's total energy forecast + + + + Number:Energy + + Energy forecast for next hours/days + + + + String + + Plain JSON response without conversions + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-plane-type.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-plane-type.xml new file mode 100644 index 00000000000..f37bd94d41b --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-plane-type.xml @@ -0,0 +1,27 @@ + + + + + + + + + + One PV Plane of Multi Plane Bridge + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-site-type.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-site-type.xml new file mode 100644 index 00000000000..0e7c2b91f19 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-site-type.xml @@ -0,0 +1,22 @@ + + + + + + Site location for Forecast Solar + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/optimistic-group.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/optimistic-group.xml new file mode 100644 index 00000000000..6ca53734dcd --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/optimistic-group.xml @@ -0,0 +1,18 @@ + + + + + Forecast values showing 90th percentile case data + + + + + + + + + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/pessimistic-group.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/pessimistic-group.xml new file mode 100644 index 00000000000..e5c61debd81 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/pessimistic-group.xml @@ -0,0 +1,18 @@ + + + + + Forecast values showing 10th percentile case data + + + + + + + + + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/raw-group.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/raw-group.xml new file mode 100644 index 00000000000..3427c90e0a9 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/raw-group.xml @@ -0,0 +1,13 @@ + + + + + Raw response from service provider + + + + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-plane-type.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-plane-type.xml new file mode 100644 index 00000000000..c549cc8fe19 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-plane-type.xml @@ -0,0 +1,24 @@ + + + + + + + + + + One PV Plane of Multi Plane Bridge + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-site-type.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-site-type.xml new file mode 100644 index 00000000000..aab7c4418d3 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-site-type.xml @@ -0,0 +1,18 @@ + + + + + + Solcast service site definition + + + + + + + + + diff --git a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/CallbackMock.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/CallbackMock.java new file mode 100644 index 00000000000..a47b9d9c38b --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/CallbackMock.java @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.config.core.ConfigDescription; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelGroupUID; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatusInfo; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.openhab.core.thing.type.ChannelGroupTypeUID; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.TimeSeries; +import org.openhab.core.types.TimeSeries.Policy; + +/** + * The {@link CallbackMock} is a helper for unit tests to receive callbacks + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class CallbackMock implements ThingHandlerCallback { + + Map seriesMap = new HashMap(); + + @Override + public void stateUpdated(ChannelUID channelUID, State state) { + } + + @Override + public void postCommand(ChannelUID channelUID, Command command) { + } + + @Override + public void sendTimeSeries(ChannelUID channelUID, TimeSeries timeSeries) { + seriesMap.put(channelUID.getAsString(), timeSeries); + } + + public TimeSeries getTimeSeries(String cuid) { + TimeSeries ts = seriesMap.get(cuid); + if (ts == null) { + ts = new TimeSeries(Policy.REPLACE); + } + return ts; + } + + @Override + public void statusUpdated(Thing thing, ThingStatusInfo thingStatus) { + } + + @Override + public void thingUpdated(Thing thing) { + } + + @Override + public void validateConfigurationParameters(Thing thing, Map configurationParameters) { + } + + @Override + public void validateConfigurationParameters(Channel channel, Map configurationParameters) { + } + + @Override + public @Nullable ConfigDescription getConfigDescription(ChannelTypeUID channelTypeUID) { + return null; + } + + @Override + public @Nullable ConfigDescription getConfigDescription(ThingTypeUID thingTypeUID) { + return null; + } + + @Override + public void configurationUpdated(Thing thing) { + } + + @Override + public void migrateThingType(Thing thing, ThingTypeUID thingTypeUID, Configuration configuration) { + } + + @Override + public void channelTriggered(Thing thing, ChannelUID channelUID, String event) { + } + + @Override + public ChannelBuilder createChannelBuilder(ChannelUID channelUID, ChannelTypeUID channelTypeUID) { + return ChannelBuilder.create(channelUID); + } + + @Override + public ChannelBuilder editChannel(Thing thing, ChannelUID channelUID) { + return ChannelBuilder.create(channelUID); + } + + @Override + public List createChannelBuilders(ChannelGroupUID channelGroupUID, + ChannelGroupTypeUID channelGroupTypeUID) { + return List.of(); + } + + @Override + public boolean isChannelLinked(ChannelUID channelUID) { + return false; + } + + @Override + public @Nullable Bridge getBridge(ThingUID bridgeUID) { + return null; + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/FileReader.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/FileReader.java new file mode 100644 index 00000000000..1ae51267989 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/FileReader.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; + +/** + * The {@link FileReader} Helper Util to read test resource files + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class FileReader { + + public static String readFileInString(String filename) { + try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(filename), "UTF-8"));) { + StringBuilder buf = new StringBuilder(); + String sCurrentLine; + + while ((sCurrentLine = br.readLine()) != null) { + buf.append(sCurrentLine); + } + return buf.toString(); + } catch (IOException e) { + // fail if file cannot be read + assertFalse(filename.isBlank(), "Read failure " + filename); + } + return SolarForecastBindingConstants.EMPTY; + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/ForecastSolarTest.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/ForecastSolarTest.java new file mode 100644 index 00000000000..608bc02ebec --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/ForecastSolarTest.java @@ -0,0 +1,498 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.Iterator; +import java.util.Optional; + +import javax.measure.quantity.Energy; +import javax.measure.quantity.Power; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; +import org.openhab.binding.solarforecast.internal.SolarForecastException; +import org.openhab.binding.solarforecast.internal.actions.SolarForecastActions; +import org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarObject; +import org.openhab.binding.solarforecast.internal.forecastsolar.handler.ForecastSolarBridgeHandler; +import org.openhab.binding.solarforecast.internal.forecastsolar.handler.ForecastSolarPlaneHandler; +import org.openhab.binding.solarforecast.internal.forecastsolar.handler.ForecastSolarPlaneMock; +import org.openhab.binding.solarforecast.internal.solcast.SolcastObject.QueryMode; +import org.openhab.binding.solarforecast.internal.utils.Utils; +import org.openhab.core.library.types.PointType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.internal.BridgeImpl; +import org.openhab.core.types.State; +import org.openhab.core.types.TimeSeries; + +/** + * The {@link ForecastSolarTest} tests responses from forecast solar object + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +class ForecastSolarTest { + private static final double TOLERANCE = 0.001; + public static final ZoneId TEST_ZONE = ZoneId.of("Europe/Berlin"); + public static final QuantityType POWER_UNDEF = Utils.getPowerState(-1); + public static final QuantityType ENERGY_UNDEF = Utils.getEnergyState(-1); + + public static final String TOO_EARLY_INDICATOR = "too early"; + public static final String TOO_LATE_INDICATOR = "too late"; + public static final String INVALID_RANGE_INDICATOR = "invalid time range"; + public static final String NO_GORECAST_INDICATOR = "No forecast data"; + public static final String DAY_MISSING_INDICATOR = "not available in forecast"; + + @Test + void testForecastObject() { + String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); + ZonedDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 17, 00).atZone(TEST_ZONE); + ForecastSolarObject fo = new ForecastSolarObject("fs-test", content, + queryDateTime.toInstant().plus(1, ChronoUnit.DAYS)); + // "2022-07-17 21:32:00": 63583, + assertEquals(63.583, fo.getDayTotal(queryDateTime.toLocalDate()), TOLERANCE, "Total production"); + // "2022-07-17 17:00:00": 52896, + assertEquals(52.896, fo.getActualEnergyValue(queryDateTime), TOLERANCE, "Current Production"); + // 63583 - 52896 = 10687 + assertEquals(10.687, fo.getRemainingProduction(queryDateTime), TOLERANCE, "Current Production"); + // sum cross check + assertEquals(fo.getDayTotal(queryDateTime.toLocalDate()), + fo.getActualEnergyValue(queryDateTime) + fo.getRemainingProduction(queryDateTime), TOLERANCE, + "actual + remain = total"); + + queryDateTime = LocalDateTime.of(2022, 7, 18, 19, 00).atZone(TEST_ZONE); + // "2022-07-18 19:00:00": 63067, + assertEquals(63.067, fo.getActualEnergyValue(queryDateTime), TOLERANCE, "Actual production"); + // "2022-07-18 21:31:00": 65554 + assertEquals(65.554, fo.getDayTotal(queryDateTime.toLocalDate()), TOLERANCE, "Total production"); + } + + @Test + void testActualPower() { + String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); + ZonedDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 10, 00).atZone(TEST_ZONE); + ForecastSolarObject fo = new ForecastSolarObject("fs-test", content, + queryDateTime.toInstant().plus(1, ChronoUnit.DAYS)); + // "2022-07-17 10:00:00": 4874, + assertEquals(4.874, fo.getActualPowerValue(queryDateTime), TOLERANCE, "Actual estimation"); + + queryDateTime = LocalDateTime.of(2022, 7, 18, 14, 00).atZone(TEST_ZONE); + // "2022-07-18 14:00:00": 7054, + assertEquals(7.054, fo.getActualPowerValue(queryDateTime), TOLERANCE, "Actual estimation"); + } + + @Test + void testInterpolation() { + String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); + ZonedDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 0).atZone(TEST_ZONE); + ForecastSolarObject fo = new ForecastSolarObject("fs-test", content, + queryDateTime.toInstant().plus(1, ChronoUnit.DAYS)); + + // test steady value increase + double previousValue = 0; + for (int i = 0; i < 60; i++) { + queryDateTime = queryDateTime.plusMinutes(1); + assertTrue(previousValue < fo.getActualEnergyValue(queryDateTime)); + previousValue = fo.getActualEnergyValue(queryDateTime); + } + + queryDateTime = LocalDateTime.of(2022, 7, 18, 6, 23).atZone(TEST_ZONE); + // "2022-07-18 06:00:00": 132, + // "2022-07-18 07:00:00": 1188, + // 1188 - 132 = 1056 | 1056 * 23 / 60 = 404 | 404 + 131 = 535 + assertEquals(0.535, fo.getActualEnergyValue(queryDateTime), 0.002, "Actual estimation"); + } + + @Test + void testForecastSum() { + String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); + ZonedDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 23).atZone(TEST_ZONE); + ForecastSolarObject fo = new ForecastSolarObject("fs-test", content, + queryDateTime.toInstant().plus(1, ChronoUnit.DAYS)); + QuantityType actual = QuantityType.valueOf(0, Units.KILOWATT_HOUR); + QuantityType st = Utils.getEnergyState(fo.getActualEnergyValue(queryDateTime)); + assertTrue(st instanceof QuantityType); + actual = actual.add(st); + assertEquals(49.431, actual.floatValue(), TOLERANCE, "Current Production"); + actual = actual.add(st); + assertEquals(98.862, actual.floatValue(), TOLERANCE, "Doubled Current Production"); + } + + @Test + void testCornerCases() { + // invalid object + ForecastSolarObject fo = new ForecastSolarObject("fs-test"); + ZonedDateTime query = LocalDateTime.of(2022, 7, 17, 16, 23).atZone(TEST_ZONE); + try { + double d = fo.getActualEnergyValue(query); + fail("Exception expected instead of " + d); + } catch (SolarForecastException sfe) { + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(INVALID_RANGE_INDICATOR), + "Expected: " + INVALID_RANGE_INDICATOR + " Received: " + sfe.getMessage()); + } + try { + double d = fo.getRemainingProduction(query); + fail("Exception expected instead of " + d); + } catch (SolarForecastException sfe) { + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(NO_GORECAST_INDICATOR), + "Expected: " + NO_GORECAST_INDICATOR + " Received: " + sfe.getMessage()); + } + try { + double d = fo.getDayTotal(query.toLocalDate()); + fail("Exception expected instead of " + d); + } catch (SolarForecastException sfe) { + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(NO_GORECAST_INDICATOR), + "Expected: " + NO_GORECAST_INDICATOR + " Received: " + sfe.getMessage()); + } + try { + double d = fo.getDayTotal(query.plusDays(1).toLocalDate()); + fail("Exception expected instead of " + d); + } catch (SolarForecastException sfe) { + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(NO_GORECAST_INDICATOR), + "Expected: " + NO_GORECAST_INDICATOR + " Received: " + sfe.getMessage()); + } + + // valid object - query date one day too early + String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); + query = LocalDateTime.of(2022, 7, 16, 23, 59).atZone(TEST_ZONE); + fo = new ForecastSolarObject("fs-test", content, query.toInstant()); + try { + double d = fo.getActualEnergyValue(query); + fail("Exception expected instead of " + d); + } catch (SolarForecastException sfe) { + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(TOO_EARLY_INDICATOR), + "Expected: " + TOO_EARLY_INDICATOR + " Received: " + sfe.getMessage()); + } + try { + double d = fo.getRemainingProduction(query); + fail("Exception expected instead of " + d); + } catch (SolarForecastException sfe) { + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(DAY_MISSING_INDICATOR), + "Expected: " + DAY_MISSING_INDICATOR + " Received: " + sfe.getMessage()); + } + try { + double d = fo.getActualPowerValue(query); + fail("Exception expected instead of " + d); + } catch (SolarForecastException sfe) { + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(TOO_EARLY_INDICATOR), + "Expected: " + TOO_EARLY_INDICATOR + " Received: " + sfe.getMessage()); + } + try { + double d = fo.getDayTotal(query.toLocalDate()); + fail("Exception expected instead of " + d); + } catch (SolarForecastException sfe) { + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(DAY_MISSING_INDICATOR), + "Expected: " + DAY_MISSING_INDICATOR + " Received: " + sfe.getMessage()); + } + + // one minute later we reach a valid date + query = query.plusMinutes(1); + assertEquals(63.583, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope"); + assertEquals(0.0, fo.getActualEnergyValue(query), TOLERANCE, "Actual out of scope"); + assertEquals(63.583, fo.getRemainingProduction(query), TOLERANCE, "Remain out of scope"); + assertEquals(0.0, fo.getActualPowerValue(query), TOLERANCE, "Remain out of scope"); + + // valid object - query date one day too late + query = LocalDateTime.of(2022, 7, 19, 0, 0).atZone(TEST_ZONE); + try { + double d = fo.getActualEnergyValue(query); + fail("Exception expected instead of " + d); + } catch (SolarForecastException sfe) { + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(TOO_LATE_INDICATOR), + "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage()); + } + try { + double d = fo.getRemainingProduction(query); + fail("Exception expected instead of " + d); + } catch (SolarForecastException sfe) { + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(DAY_MISSING_INDICATOR), + "Expected: " + DAY_MISSING_INDICATOR + " Received: " + sfe.getMessage()); + } + try { + double d = fo.getActualPowerValue(query); + fail("Exception expected instead of " + d); + } catch (SolarForecastException sfe) { + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(TOO_LATE_INDICATOR), + "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage()); + } + try { + double d = fo.getDayTotal(query.toLocalDate()); + fail("Exception expected instead of " + d); + } catch (SolarForecastException sfe) { + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(DAY_MISSING_INDICATOR), + "Expected: " + DAY_MISSING_INDICATOR + " Received: " + sfe.getMessage()); + } + + // one minute earlier we reach a valid date + query = query.minusMinutes(1); + assertEquals(65.554, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope"); + assertEquals(65.554, fo.getActualEnergyValue(query), TOLERANCE, "Actual out of scope"); + assertEquals(0.0, fo.getRemainingProduction(query), TOLERANCE, "Remain out of scope"); + assertEquals(0.0, fo.getActualPowerValue(query), TOLERANCE, "Remain out of scope"); + + // test times between 2 dates + query = LocalDateTime.of(2022, 7, 17, 23, 59).atZone(TEST_ZONE); + assertEquals(63.583, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope"); + assertEquals(63.583, fo.getActualEnergyValue(query), TOLERANCE, "Actual out of scope"); + assertEquals(0.0, fo.getRemainingProduction(query), TOLERANCE, "Remain out of scope"); + assertEquals(0.0, fo.getActualPowerValue(query), TOLERANCE, "Remain out of scope"); + query = query.plusMinutes(1); + assertEquals(65.554, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope"); + assertEquals(0.0, fo.getActualEnergyValue(query), TOLERANCE, "Actual out of scope"); + assertEquals(65.554, fo.getRemainingProduction(query), TOLERANCE, "Remain out of scope"); + assertEquals(0.0, fo.getActualPowerValue(query), TOLERANCE, "Remain out of scope"); + } + + @Test + void testExceptions() { + String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); + ZonedDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 23).atZone(TEST_ZONE); + ForecastSolarObject fo = new ForecastSolarObject("fs-test", content, queryDateTime.toInstant()); + assertEquals("2022-07-17T05:31:00", + fo.getForecastBegin().atZone(TEST_ZONE).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), + "Forecast begin"); + assertEquals("2022-07-18T21:31:00", + fo.getForecastEnd().atZone(TEST_ZONE).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), "Forecast end"); + assertEquals(QuantityType.valueOf(63.583, Units.KILOWATT_HOUR).toString(), + fo.getDay(queryDateTime.toLocalDate()).toFullString(), "Actual out of scope"); + + queryDateTime = LocalDateTime.of(2022, 7, 10, 0, 0).atZone(TEST_ZONE); + // "watt_hours_day": { + // "2022-07-17": 63583, + // "2022-07-18": 65554 + // } + try { + fo.getEnergy(queryDateTime.toInstant(), queryDateTime.plusDays(2).toInstant()); + fail("Too early exception missing"); + } catch (SolarForecastException sfe) { + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains("not available"), "not available expected: " + sfe.getMessage()); + } + try { + fo.getDay(queryDateTime.toLocalDate(), "optimistic"); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("ForecastSolar doesn't accept arguments", e.getMessage(), "optimistic"); + } + try { + fo.getDay(queryDateTime.toLocalDate(), "pessimistic"); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("ForecastSolar doesn't accept arguments", e.getMessage(), "pessimistic"); + } + try { + fo.getDay(queryDateTime.toLocalDate(), "total", "rubbish"); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("ForecastSolar doesn't accept arguments", e.getMessage(), "rubbish"); + } + } + + @Test + void testTimeSeries() { + String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); + ZonedDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 23).atZone(TEST_ZONE); + ForecastSolarObject fo = new ForecastSolarObject("fs-test", content, queryDateTime.toInstant()); + + TimeSeries powerSeries = fo.getPowerTimeSeries(QueryMode.Average); + assertEquals(36, powerSeries.size()); // 18 values each day for 2 days + powerSeries.getStates().forEachOrdered(entry -> { + State s = entry.state(); + assertTrue(s instanceof QuantityType); + assertEquals("kW", ((QuantityType) s).getUnit().toString()); + }); + + TimeSeries energySeries = fo.getEnergyTimeSeries(QueryMode.Average); + assertEquals(36, energySeries.size()); + energySeries.getStates().forEachOrdered(entry -> { + State s = entry.state(); + assertTrue(s instanceof QuantityType); + assertEquals("kWh", ((QuantityType) s).getUnit().toString()); + }); + } + + @Test + void testPowerTimeSeries() { + ForecastSolarBridgeHandler fsbh = new ForecastSolarBridgeHandler( + new BridgeImpl(SolarForecastBindingConstants.FORECAST_SOLAR_SITE, "bridge"), + Optional.of(PointType.valueOf("1,2"))); + CallbackMock cm = new CallbackMock(); + fsbh.setCallback(cm); + + String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); + ForecastSolarObject fso1 = new ForecastSolarObject("fs-test", content, Instant.now().plus(1, ChronoUnit.DAYS)); + ForecastSolarPlaneHandler fsph1 = new ForecastSolarPlaneMock(fso1); + fsbh.addPlane(fsph1); + fsbh.forecastUpdate(); + TimeSeries ts1 = cm.getTimeSeries("solarforecast:fs-site:bridge:power-estimate"); + + ForecastSolarPlaneHandler fsph2 = new ForecastSolarPlaneMock(fso1); + fsbh.addPlane(fsph2); + fsbh.forecastUpdate(); + TimeSeries ts2 = cm.getTimeSeries("solarforecast:fs-site:bridge:power-estimate"); + Iterator iter1 = ts1.getStates().iterator(); + Iterator iter2 = ts2.getStates().iterator(); + while (iter1.hasNext()) { + TimeSeries.Entry e1 = iter1.next(); + TimeSeries.Entry e2 = iter2.next(); + assertEquals("kW", ((QuantityType) e1.state()).getUnit().toString(), "Power Unit"); + assertEquals("kW", ((QuantityType) e2.state()).getUnit().toString(), "Power Unit"); + assertEquals(((QuantityType) e1.state()).doubleValue(), ((QuantityType) e2.state()).doubleValue() / 2, + 0.1, "Power Value"); + } + } + + @Test + void testCommonForecastStartEnd() { + ForecastSolarBridgeHandler fsbh = new ForecastSolarBridgeHandler( + new BridgeImpl(SolarForecastBindingConstants.FORECAST_SOLAR_SITE, "bridge"), + Optional.of(PointType.valueOf("1,2"))); + CallbackMock cmSite = new CallbackMock(); + fsbh.setCallback(cmSite); + String contentOne = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); + ForecastSolarObject fso1One = new ForecastSolarObject("fs-test", contentOne, + Instant.now().plus(1, ChronoUnit.DAYS)); + ForecastSolarPlaneHandler fsph1 = new ForecastSolarPlaneMock(fso1One); + fsbh.addPlane(fsph1); + fsbh.forecastUpdate(); + + String contentTwo = FileReader.readFileInString("src/test/resources/forecastsolar/resultNextDay.json"); + ForecastSolarObject fso1Two = new ForecastSolarObject("fs-plane", contentTwo, + Instant.now().plus(1, ChronoUnit.DAYS)); + ForecastSolarPlaneHandler fsph2 = new ForecastSolarPlaneMock(fso1Two); + CallbackMock cmPlane = new CallbackMock(); + fsph2.setCallback(cmPlane); + ((ForecastSolarPlaneMock) fsph2).updateForecast(fso1Two); + fsbh.addPlane(fsph2); + fsbh.forecastUpdate(); + + TimeSeries tsPlaneOne = cmPlane.getTimeSeries("test::plane:power-estimate"); + TimeSeries tsSite = cmSite.getTimeSeries("solarforecast:fs-site:bridge:power-estimate"); + Iterator planeIter = tsPlaneOne.getStates().iterator(); + Iterator siteIter = tsSite.getStates().iterator(); + while (siteIter.hasNext()) { + TimeSeries.Entry planeEntry = planeIter.next(); + TimeSeries.Entry siteEntry = siteIter.next(); + assertEquals("kW", ((QuantityType) planeEntry.state()).getUnit().toString(), "Power Unit"); + assertEquals("kW", ((QuantityType) siteEntry.state()).getUnit().toString(), "Power Unit"); + assertEquals(((QuantityType) planeEntry.state()).doubleValue(), + ((QuantityType) siteEntry.state()).doubleValue() / 2, 0.1, "Power Value"); + } + // only one day shall be reported which is available in both planes + LocalDate ld = LocalDate.of(2022, 7, 18); + assertEquals(ld.atStartOfDay(ZoneId.of("UTC")).toInstant(), tsSite.getBegin().truncatedTo(ChronoUnit.DAYS), + "TimeSeries start"); + assertEquals(ld.atStartOfDay(ZoneId.of("UTC")).toInstant(), tsSite.getEnd().truncatedTo(ChronoUnit.DAYS), + "TimeSeries end"); + } + + @Test + void testActions() { + ForecastSolarBridgeHandler fsbh = new ForecastSolarBridgeHandler( + new BridgeImpl(SolarForecastBindingConstants.FORECAST_SOLAR_SITE, "bridge"), + Optional.of(PointType.valueOf("1,2"))); + CallbackMock cmSite = new CallbackMock(); + fsbh.setCallback(cmSite); + String contentOne = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); + ForecastSolarObject fso1One = new ForecastSolarObject("fs-test", contentOne, + Instant.now().plus(1, ChronoUnit.DAYS)); + ForecastSolarPlaneHandler fsph1 = new ForecastSolarPlaneMock(fso1One); + fsbh.addPlane(fsph1); + fsbh.forecastUpdate(); + + String contentTwo = FileReader.readFileInString("src/test/resources/forecastsolar/resultNextDay.json"); + ForecastSolarObject fso1Two = new ForecastSolarObject("fs-plane", contentTwo, + Instant.now().plus(1, ChronoUnit.DAYS)); + ForecastSolarPlaneHandler fsph2 = new ForecastSolarPlaneMock(fso1Two); + CallbackMock cmPlane = new CallbackMock(); + fsph2.setCallback(cmPlane); + ((ForecastSolarPlaneMock) fsph2).updateForecast(fso1Two); + fsbh.addPlane(fsph2); + fsbh.forecastUpdate(); + + SolarForecastActions sfa = new SolarForecastActions(); + sfa.setThingHandler(fsbh); + // only one day shall be reported which is available in both planes + LocalDate ld = LocalDate.of(2022, 7, 18); + assertEquals(ld.atStartOfDay(ZoneId.of("UTC")).toInstant(), sfa.getForecastBegin().truncatedTo(ChronoUnit.DAYS), + "TimeSeries start"); + assertEquals(ld.atStartOfDay(ZoneId.of("UTC")).toInstant(), sfa.getForecastEnd().truncatedTo(ChronoUnit.DAYS), + "TimeSeries end"); + } + + @Test + void testEnergyTimeSeries() { + ForecastSolarBridgeHandler fsbh = new ForecastSolarBridgeHandler( + new BridgeImpl(SolarForecastBindingConstants.FORECAST_SOLAR_SITE, "bridge"), + Optional.of(PointType.valueOf("1,2"))); + CallbackMock cm = new CallbackMock(); + fsbh.setCallback(cm); + + String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); + ForecastSolarObject fso1 = new ForecastSolarObject("fs-test", content, Instant.now().plus(1, ChronoUnit.DAYS)); + ForecastSolarPlaneHandler fsph1 = new ForecastSolarPlaneMock(fso1); + fsbh.addPlane(fsph1); + fsbh.forecastUpdate(); + TimeSeries ts1 = cm.getTimeSeries("solarforecast:fs-site:bridge:energy-estimate"); + + ForecastSolarPlaneHandler fsph2 = new ForecastSolarPlaneMock(fso1); + fsbh.addPlane(fsph2); + fsbh.forecastUpdate(); + TimeSeries ts2 = cm.getTimeSeries("solarforecast:fs-site:bridge:energy-estimate"); + Iterator iter1 = ts1.getStates().iterator(); + Iterator iter2 = ts2.getStates().iterator(); + while (iter1.hasNext()) { + TimeSeries.Entry e1 = iter1.next(); + TimeSeries.Entry e2 = iter2.next(); + assertEquals("kWh", ((QuantityType) e1.state()).getUnit().toString(), "Power Unit"); + assertEquals("kWh", ((QuantityType) e2.state()).getUnit().toString(), "Power Unit"); + assertEquals(((QuantityType) e1.state()).doubleValue(), ((QuantityType) e2.state()).doubleValue() / 2, + 0.1, "Power Value"); + } + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastTest.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastTest.java new file mode 100644 index 00000000000..a6606d54f37 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastTest.java @@ -0,0 +1,717 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.measure.quantity.Energy; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.junit.jupiter.api.Test; +import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; +import org.openhab.binding.solarforecast.internal.SolarForecastException; +import org.openhab.binding.solarforecast.internal.actions.SolarForecast; +import org.openhab.binding.solarforecast.internal.solcast.SolcastConstants; +import org.openhab.binding.solarforecast.internal.solcast.SolcastObject; +import org.openhab.binding.solarforecast.internal.solcast.SolcastObject.QueryMode; +import org.openhab.binding.solarforecast.internal.solcast.handler.SolcastBridgeHandler; +import org.openhab.binding.solarforecast.internal.solcast.handler.SolcastPlaneHandler; +import org.openhab.binding.solarforecast.internal.solcast.handler.SolcastPlaneMock; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.internal.BridgeImpl; +import org.openhab.core.types.State; +import org.openhab.core.types.TimeSeries; + +/** + * The {@link SolcastTest} tests responses from forecast solar website + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +class SolcastTest { + public static final ZoneId TEST_ZONE = ZoneId.of("Europe/Berlin"); + private static final TimeZP TIMEZONEPROVIDER = new TimeZP(); + // double comparison tolerance = 1 Watt + private static final double TOLERANCE = 0.001; + + public static final String TOO_LATE_INDICATOR = "too late"; + public static final String DAY_MISSING_INDICATOR = "not available in forecast"; + + /** + * "2022-07-18T00:00+02:00[Europe/Berlin]": 0, + * "2022-07-18T00:30+02:00[Europe/Berlin]": 0, + * "2022-07-18T01:00+02:00[Europe/Berlin]": 0, + * "2022-07-18T01:30+02:00[Europe/Berlin]": 0, + * "2022-07-18T02:00+02:00[Europe/Berlin]": 0, + * "2022-07-18T02:30+02:00[Europe/Berlin]": 0, + * "2022-07-18T03:00+02:00[Europe/Berlin]": 0, + * "2022-07-18T03:30+02:00[Europe/Berlin]": 0, + * "2022-07-18T04:00+02:00[Europe/Berlin]": 0, + * "2022-07-18T04:30+02:00[Europe/Berlin]": 0, + * "2022-07-18T05:00+02:00[Europe/Berlin]": 0, + * "2022-07-18T05:30+02:00[Europe/Berlin]": 0, + * "2022-07-18T06:00+02:00[Europe/Berlin]": 0.0205, + * "2022-07-18T06:30+02:00[Europe/Berlin]": 0.1416, + * "2022-07-18T07:00+02:00[Europe/Berlin]": 0.4478, + * "2022-07-18T07:30+02:00[Europe/Berlin]": 0.763, + * "2022-07-18T08:00+02:00[Europe/Berlin]": 1.1367, + * "2022-07-18T08:30+02:00[Europe/Berlin]": 1.4044, + * "2022-07-18T09:00+02:00[Europe/Berlin]": 1.6632, + * "2022-07-18T09:30+02:00[Europe/Berlin]": 1.8667, + * "2022-07-18T10:00+02:00[Europe/Berlin]": 2.0729, + * "2022-07-18T10:30+02:00[Europe/Berlin]": 2.2377, + * "2022-07-18T11:00+02:00[Europe/Berlin]": 2.3516, + * "2022-07-18T11:30+02:00[Europe/Berlin]": 2.4295, + * "2022-07-18T12:00+02:00[Europe/Berlin]": 2.5136, + * "2022-07-18T12:30+02:00[Europe/Berlin]": 2.5295, + * "2022-07-18T13:00+02:00[Europe/Berlin]": 2.526, + * "2022-07-18T13:30+02:00[Europe/Berlin]": 2.4879, + * "2022-07-18T14:00+02:00[Europe/Berlin]": 2.4092, + * "2022-07-18T14:30+02:00[Europe/Berlin]": 2.3309, + * "2022-07-18T15:00+02:00[Europe/Berlin]": 2.1984, + * "2022-07-18T15:30+02:00[Europe/Berlin]": 2.0416, + * "2022-07-18T16:00+02:00[Europe/Berlin]": 1.9076, + * "2022-07-18T16:30+02:00[Europe/Berlin]": 1.7416, + * "2022-07-18T17:00+02:00[Europe/Berlin]": 1.5414, + * "2022-07-18T17:30+02:00[Europe/Berlin]": 1.3683, + * "2022-07-18T18:00+02:00[Europe/Berlin]": 1.1603, + * "2022-07-18T18:30+02:00[Europe/Berlin]": 0.9527, + * "2022-07-18T19:00+02:00[Europe/Berlin]": 0.7705, + * "2022-07-18T19:30+02:00[Europe/Berlin]": 0.5673, + * "2022-07-18T20:00+02:00[Europe/Berlin]": 0.3588, + * "2022-07-18T20:30+02:00[Europe/Berlin]": 0.1948, + * "2022-07-18T21:00+02:00[Europe/Berlin]": 0.0654, + * "2022-07-18T21:30+02:00[Europe/Berlin]": 0.0118, + * "2022-07-18T22:00+02:00[Europe/Berlin]": 0, + * "2022-07-18T22:30+02:00[Europe/Berlin]": 0, + * "2022-07-18T23:00+02:00[Europe/Berlin]": 0, + * "2022-07-18T23:30+02:00[Europe/Berlin]": 0 + **/ + @Test + void testForecastObject() { + String content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 0, 0).atZone(TEST_ZONE); + SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER); + content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); + scfo.join(content); + // test one day, step ahead in time and cross check channel values + double dayTotal = scfo.getDayTotal(now.toLocalDate(), QueryMode.Average); + double actual = scfo.getActualEnergyValue(now, QueryMode.Average); + double remain = scfo.getRemainingProduction(now, QueryMode.Average); + assertEquals(0.0, actual, TOLERANCE, "Begin of day actual"); + assertEquals(23.107, remain, TOLERANCE, "Begin of day remaining"); + assertEquals(23.107, dayTotal, TOLERANCE, "Day total"); + assertEquals(0.0, scfo.getActualPowerValue(now, QueryMode.Average), TOLERANCE, "Begin of day power"); + double previousPower = 0; + for (int i = 0; i < 47; i++) { + now = now.plusMinutes(30); + double power = scfo.getActualPowerValue(now, QueryMode.Average) / 2.0; + double powerAddOn = ((power + previousPower) / 2.0); + actual += powerAddOn; + assertEquals(actual, scfo.getActualEnergyValue(now, QueryMode.Average), TOLERANCE, "Actual at " + now); + remain -= powerAddOn; + assertEquals(remain, scfo.getRemainingProduction(now, QueryMode.Average), TOLERANCE, "Remain at " + now); + assertEquals(dayTotal, actual + remain, TOLERANCE, "Total sum at " + now); + previousPower = power; + } + } + + @Test + void testPower() { + String content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + ZonedDateTime now = LocalDateTime.of(2022, 7, 23, 16, 00).atZone(TEST_ZONE); + SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER); + content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); + scfo.join(content); + + /** + * { + * "pv_estimate": 1.9176, + * "pv_estimate10": 0.8644, + * "pv_estimate90": 2.0456, + * "period_end": "2022-07-23T14:00:00.0000000Z", + * "period": "PT30M" + * }, + * { + * "pv_estimate": 1.7544, + * "pv_estimate10": 0.7708, + * "pv_estimate90": 1.864, + * "period_end": "2022-07-23T14:30:00.0000000Z", + * "period": "PT30M" + */ + assertEquals(1.9176, scfo.getActualPowerValue(now, QueryMode.Average), TOLERANCE, "Estimate power " + now); + assertEquals(1.9176, scfo.getPower(now.toInstant(), "average").doubleValue(), TOLERANCE, + "Estimate power " + now); + assertEquals(1.754, scfo.getActualPowerValue(now.plusMinutes(30), QueryMode.Average), TOLERANCE, + "Estimate power " + now.plusMinutes(30)); + + assertEquals(2.046, scfo.getActualPowerValue(now, QueryMode.Optimistic), TOLERANCE, "Optimistic power " + now); + assertEquals(1.864, scfo.getActualPowerValue(now.plusMinutes(30), QueryMode.Optimistic), TOLERANCE, + "Optimistic power " + now.plusMinutes(30)); + + assertEquals(0.864, scfo.getActualPowerValue(now, QueryMode.Pessimistic), TOLERANCE, + "Pessimistic power " + now); + assertEquals(0.771, scfo.getActualPowerValue(now.plusMinutes(30), QueryMode.Pessimistic), TOLERANCE, + "Pessimistic power " + now.plusMinutes(30)); + + /** + * { + * "pv_estimate": 1.9318, + * "period_end": "2022-07-17T14:30:00.0000000Z", + * "period": "PT30M" + * }, + * { + * "pv_estimate": 1.724, + * "period_end": "2022-07-17T15:00:00.0000000Z", + * "period": "PT30M" + * }, + **/ + // get same values for optimistic / pessimistic and estimate in the past + ZonedDateTime past = LocalDateTime.of(2022, 7, 17, 16, 30).atZone(TEST_ZONE); + assertEquals(1.932, scfo.getActualPowerValue(past, QueryMode.Average), TOLERANCE, "Estimate power " + past); + assertEquals(1.724, scfo.getActualPowerValue(past.plusMinutes(30), QueryMode.Average), TOLERANCE, + "Estimate power " + now.plusMinutes(30)); + + assertEquals(1.932, scfo.getActualPowerValue(past, QueryMode.Optimistic), TOLERANCE, + "Optimistic power " + past); + assertEquals(1.724, scfo.getActualPowerValue(past.plusMinutes(30), QueryMode.Optimistic), TOLERANCE, + "Optimistic power " + past.plusMinutes(30)); + + assertEquals(1.932, scfo.getActualPowerValue(past, QueryMode.Pessimistic), TOLERANCE, + "Pessimistic power " + past); + assertEquals(1.724, scfo.getActualPowerValue(past.plusMinutes(30), QueryMode.Pessimistic), TOLERANCE, + "Pessimistic power " + past.plusMinutes(30)); + } + + /** + * Data from TreeMap for manual validation + * 2022-07-17T04:30+02:00[Europe/Berlin]=0.0, + * 2022-07-17T05:00+02:00[Europe/Berlin]=0.0, + * 2022-07-17T05:30+02:00[Europe/Berlin]=0.0, + * 2022-07-17T06:00+02:00[Europe/Berlin]=0.0262, + * 2022-07-17T06:30+02:00[Europe/Berlin]=0.4252, + * 2022-07-17T07:00+02:00[Europe/Berlin]=0.7772, <<< + * 2022-07-17T07:30+02:00[Europe/Berlin]=1.0663, + * 2022-07-17T08:00+02:00[Europe/Berlin]=1.3848, + * 2022-07-17T08:30+02:00[Europe/Berlin]=1.6401, + * 2022-07-17T09:00+02:00[Europe/Berlin]=1.8614, + * 2022-07-17T09:30+02:00[Europe/Berlin]=2.0613, + * 2022-07-17T10:00+02:00[Europe/Berlin]=2.2365, + * 2022-07-17T10:30+02:00[Europe/Berlin]=2.3766, + * 2022-07-17T11:00+02:00[Europe/Berlin]=2.4719, + * 2022-07-17T11:30+02:00[Europe/Berlin]=2.5438, + * 2022-07-17T12:00+02:00[Europe/Berlin]=2.602, + * 2022-07-17T12:30+02:00[Europe/Berlin]=2.6213, + * 2022-07-17T13:00+02:00[Europe/Berlin]=2.6061, + * 2022-07-17T13:30+02:00[Europe/Berlin]=2.6181, + * 2022-07-17T14:00+02:00[Europe/Berlin]=2.5378, + * 2022-07-17T14:30+02:00[Europe/Berlin]=2.4651, + * 2022-07-17T15:00+02:00[Europe/Berlin]=2.3656, + * 2022-07-17T15:30+02:00[Europe/Berlin]=2.2374, + * 2022-07-17T16:00+02:00[Europe/Berlin]=2.1015, + * 2022-07-17T16:30+02:00[Europe/Berlin]=1.9318, + * 2022-07-17T17:00+02:00[Europe/Berlin]=1.724, + * 2022-07-17T17:30+02:00[Europe/Berlin]=1.5031, + * 2022-07-17T18:00+02:00[Europe/Berlin]=1.2834, + * 2022-07-17T18:30+02:00[Europe/Berlin]=1.0839, + * 2022-07-17T19:00+02:00[Europe/Berlin]=0.8581, + * 2022-07-17T19:30+02:00[Europe/Berlin]=0.6164, + * 2022-07-17T20:00+02:00[Europe/Berlin]=0.4465, + * 2022-07-17T20:30+02:00[Europe/Berlin]=0.2543, + * 2022-07-17T21:00+02:00[Europe/Berlin]=0.0848, + * 2022-07-17T21:30+02:00[Europe/Berlin]=0.0132, + * 2022-07-17T22:00+02:00[Europe/Berlin]=0.0, + * 2022-07-17T22:30+02:00[Europe/Berlin]=0.0 + * + * <<< = 0.0262 + 0.4252 + 0.7772 = 1.2286 / 2 = 0.6143 + */ + @Test + void testForecastTreeMap() { + String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); + ZonedDateTime now = LocalDateTime.of(2022, 7, 17, 7, 0).atZone(TEST_ZONE); + SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER); + assertEquals(0.42, scfo.getActualEnergyValue(now, QueryMode.Average), TOLERANCE, "Actual estimation"); + assertEquals(25.413, scfo.getDayTotal(now.toLocalDate(), QueryMode.Average), TOLERANCE, "Day total"); + } + + @Test + void testJoin() { + String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); + ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE); + SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER); + try { + double d = scfo.getActualEnergyValue(now, QueryMode.Average); + fail("Exception expected instead of " + d); + } catch (SolarForecastException sfe) { + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(TOO_LATE_INDICATOR), + "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage()); + } + content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + scfo.join(content); + assertEquals(18.946, scfo.getActualEnergyValue(now, QueryMode.Average), 0.01, "Actual data"); + assertEquals(23.107, scfo.getDayTotal(now.toLocalDate(), QueryMode.Average), 0.01, "Today data"); + JSONObject rawJson = new JSONObject(scfo.getRaw()); + assertTrue(rawJson.has("forecasts")); + assertTrue(rawJson.has("estimated_actuals")); + } + + @Test + void testActions() { + String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); + ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE); + SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER); + try { + double d = scfo.getActualEnergyValue(now, QueryMode.Average); + fail("Exception expected instead of " + d); + } catch (SolarForecastException sfe) { + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(TOO_LATE_INDICATOR), + "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage()); + } + + content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + scfo.join(content); + + assertEquals("2022-07-10T23:30+02:00[Europe/Berlin]", scfo.getForecastBegin().atZone(TEST_ZONE).toString(), + "Forecast begin"); + assertEquals("2022-07-24T23:00+02:00[Europe/Berlin]", scfo.getForecastEnd().atZone(TEST_ZONE).toString(), + "Forecast end"); + // test daily forecasts + cumulated getEnergy + double totalEnergy = 0; + ZonedDateTime start = LocalDateTime.of(2022, 7, 18, 0, 0).atZone(TEST_ZONE); + for (int i = 0; i < 6; i++) { + QuantityType qt = scfo.getDay(start.toLocalDate().plusDays(i)); + QuantityType eqt = scfo.getEnergy(start.plusDays(i).toInstant(), start.plusDays(i + 1).toInstant()); + + // check if energy calculation fits to daily query + assertEquals(qt.doubleValue(), eqt.doubleValue(), TOLERANCE, "Total " + i + " days forecast"); + totalEnergy += qt.doubleValue(); + + // check if sum is fitting to total energy query + qt = scfo.getEnergy(start.toInstant(), start.plusDays(i + 1).toInstant()); + assertEquals(totalEnergy, qt.doubleValue(), TOLERANCE * 2, "Total " + i + " days forecast"); + } + } + + @Test + void testOptimisticPessimistic() { + String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); + ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE); + SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER); + content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + scfo.join(content); + assertEquals(19.389, scfo.getDayTotal(now.toLocalDate().plusDays(2), QueryMode.Average), TOLERANCE, + "Estimation"); + assertEquals(7.358, scfo.getDayTotal(now.toLocalDate().plusDays(2), QueryMode.Pessimistic), TOLERANCE, + "Estimation"); + assertEquals(22.283, scfo.getDayTotal(now.toLocalDate().plusDays(2), QueryMode.Optimistic), TOLERANCE, + "Estimation"); + assertEquals(23.316, scfo.getDayTotal(now.toLocalDate().plusDays(6), QueryMode.Average), TOLERANCE, + "Estimation"); + assertEquals(9.8, scfo.getDayTotal(now.toLocalDate().plusDays(6), QueryMode.Pessimistic), TOLERANCE, + "Estimation"); + assertEquals(23.949, scfo.getDayTotal(now.toLocalDate().plusDays(6), QueryMode.Optimistic), TOLERANCE, + "Estimation"); + + // access in past shall be rejected + Instant past = Instant.now().minus(5, ChronoUnit.MINUTES); + try { + scfo.getPower(past, SolarForecast.OPTIMISTIC); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Solcast argument optimistic only available for future values", e.getMessage(), + "Optimistic Power"); + } + try { + scfo.getPower(past, SolarForecast.PESSIMISTIC); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Solcast argument pessimistic only available for future values", e.getMessage(), + "Pessimistic Power"); + } + try { + scfo.getPower(past, "total", "rubbish"); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Solcast doesn't support 2 arguments", e.getMessage(), "Too many qrguments"); + } + try { + scfo.getPower(past.plus(2, ChronoUnit.HOURS), "rubbish"); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Solcast doesn't support argument rubbish", e.getMessage(), "Rubbish argument"); + } + try { + scfo.getPower(past); + fail("Exception expected"); + } catch (SolarForecastException sfe) { + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(TOO_LATE_INDICATOR), + "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage()); + } + } + + @Test + void testInavlid() { + String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); + ZonedDateTime now = ZonedDateTime.now(TEST_ZONE); + SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER); + try { + double d = scfo.getActualEnergyValue(now, QueryMode.Average); + fail("Exception expected instead of " + d); + } catch (SolarForecastException sfe) { + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(TOO_LATE_INDICATOR), + "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage()); + } + content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + scfo.join(content); + try { + double d = scfo.getActualEnergyValue(now, QueryMode.Average); + fail("Exception expected instead of " + d); + } catch (SolarForecastException sfe) { + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(TOO_LATE_INDICATOR), + "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage()); + } + try { + double d = scfo.getDayTotal(now.toLocalDate(), QueryMode.Average); + fail("Exception expected instead of " + d); + } catch (SolarForecastException sfe) { + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(DAY_MISSING_INDICATOR), + "Expected: " + DAY_MISSING_INDICATOR + " Received: " + sfe.getMessage()); + } + } + + @Test + void testPowerInterpolation() { + String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); + ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 15, 0).atZone(TEST_ZONE); + SolcastObject sco = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER); + content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + sco.join(content); + + double startValue = sco.getActualPowerValue(now, QueryMode.Average); + double endValue = sco.getActualPowerValue(now.plusMinutes(30), QueryMode.Average); + for (int i = 0; i < 31; i++) { + double interpolation = i / 30.0; + double expected = ((1 - interpolation) * startValue) + (interpolation * endValue); + assertEquals(expected, sco.getActualPowerValue(now.plusMinutes(i), QueryMode.Average), TOLERANCE, + "Step " + i); + } + } + + @Test + void testEnergyInterpolation() { + String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); + ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 5, 30).atZone(TEST_ZONE); + SolcastObject sco = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER); + content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + sco.join(content); + + double maxDiff = 0; + double productionExpected = 0; + for (int i = 0; i < 1000; i++) { + double forecast = sco.getActualEnergyValue(now.plusMinutes(i), QueryMode.Average); + double addOnExpected = sco.getActualPowerValue(now.plusMinutes(i), QueryMode.Average) / 60.0; + productionExpected += addOnExpected; + double diff = forecast - productionExpected; + maxDiff = Math.max(diff, maxDiff); + assertEquals(productionExpected, sco.getActualEnergyValue(now.plusMinutes(i), QueryMode.Average), + 100 * TOLERANCE, "Step " + i); + } + } + + @Test + void testRawChannel() { + String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); + ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE); + SolcastObject sco = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER); + content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + sco.join(content); + JSONObject joined = new JSONObject(sco.getRaw()); + assertTrue(joined.has("forecasts"), "Forecasts available"); + assertTrue(joined.has("estimated_actuals"), "Actual data available"); + } + + @Test + void testUpdates() { + String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); + ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE); + SolcastObject sco = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER); + content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + sco.join(content); + JSONObject joined = new JSONObject(sco.getRaw()); + assertTrue(joined.has("forecasts"), "Forecasts available"); + assertTrue(joined.has("estimated_actuals"), "Actual data available"); + } + + @Test + void testUnitDetection() { + assertEquals("kW", SolcastConstants.KILOWATT_UNIT.toString(), "Kilowatt"); + assertEquals("W", Units.WATT.toString(), "Watt"); + } + + @Test + void testTimes() { + String utcTimeString = "2022-07-17T19:30:00.0000000Z"; + SolcastObject so = new SolcastObject("sc-test", TIMEZONEPROVIDER); + ZonedDateTime zdt = so.getZdtFromUTC(utcTimeString); + assertNotNull(zdt); + assertEquals("2022-07-17T21:30+02:00[Europe/Berlin]", zdt.toString(), "ZonedDateTime"); + LocalDateTime ldt = zdt.toLocalDateTime(); + assertEquals("2022-07-17T21:30", ldt.toString(), "LocalDateTime"); + LocalTime lt = zdt.toLocalTime(); + assertEquals("21:30", lt.toString(), "LocalTime"); + } + + @Test + void testPowerTimeSeries() { + String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); + ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE); + SolcastObject sco = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER); + content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + sco.join(content); + + TimeSeries powerSeries = sco.getPowerTimeSeries(QueryMode.Average); + List> estimateL = new ArrayList<>(); + assertEquals(672, powerSeries.size()); + powerSeries.getStates().forEachOrdered(entry -> { + State s = entry.state(); + assertTrue(s instanceof QuantityType); + assertEquals("kW", ((QuantityType) s).getUnit().toString()); + if (s instanceof QuantityType qt) { + estimateL.add(qt); + } else { + fail(); + } + }); + + TimeSeries powerSeries10 = sco.getPowerTimeSeries(QueryMode.Pessimistic); + List> estimate10 = new ArrayList<>(); + assertEquals(672, powerSeries10.size()); + powerSeries10.getStates().forEachOrdered(entry -> { + State s = entry.state(); + assertTrue(s instanceof QuantityType); + assertEquals("kW", ((QuantityType) s).getUnit().toString()); + if (s instanceof QuantityType qt) { + estimate10.add(qt); + } else { + fail(); + } + }); + + TimeSeries powerSeries90 = sco.getPowerTimeSeries(QueryMode.Optimistic); + List> estimate90 = new ArrayList<>(); + assertEquals(672, powerSeries90.size()); + powerSeries90.getStates().forEachOrdered(entry -> { + State s = entry.state(); + assertTrue(s instanceof QuantityType); + assertEquals("kW", ((QuantityType) s).getUnit().toString()); + if (s instanceof QuantityType qt) { + estimate90.add(qt); + } else { + fail(); + } + }); + + for (int i = 0; i < estimateL.size(); i++) { + double lowValue = estimate10.get(i).doubleValue(); + double estValue = estimateL.get(i).doubleValue(); + double highValue = estimate90.get(i).doubleValue(); + assertTrue(lowValue <= estValue && estValue <= highValue); + } + } + + @Test + void testEnergyTimeSeries() { + String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); + ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE); + SolcastObject sco = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER); + content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + sco.join(content); + + TimeSeries energySeries = sco.getEnergyTimeSeries(QueryMode.Average); + List> estimateL = new ArrayList<>(); + assertEquals(672, energySeries.size()); // 18 values each day for 2 days + energySeries.getStates().forEachOrdered(entry -> { + State s = entry.state(); + assertTrue(s instanceof QuantityType); + assertEquals("kWh", ((QuantityType) s).getUnit().toString()); + if (s instanceof QuantityType qt) { + estimateL.add(qt); + } else { + fail(); + } + }); + + TimeSeries energySeries10 = sco.getEnergyTimeSeries(QueryMode.Pessimistic); + List> estimate10 = new ArrayList<>(); + assertEquals(672, energySeries10.size()); // 18 values each day for 2 days + energySeries10.getStates().forEachOrdered(entry -> { + State s = entry.state(); + assertTrue(s instanceof QuantityType); + assertEquals("kWh", ((QuantityType) s).getUnit().toString()); + if (s instanceof QuantityType qt) { + estimate10.add(qt); + } else { + fail(); + } + }); + + TimeSeries energySeries90 = sco.getEnergyTimeSeries(QueryMode.Optimistic); + List> estimate90 = new ArrayList<>(); + assertEquals(672, energySeries90.size()); // 18 values each day for 2 days + energySeries90.getStates().forEachOrdered(entry -> { + State s = entry.state(); + assertTrue(s instanceof QuantityType); + assertEquals("kWh", ((QuantityType) s).getUnit().toString()); + if (s instanceof QuantityType qt) { + estimate90.add(qt); + } else { + fail(); + } + }); + + for (int i = 0; i < estimateL.size(); i++) { + double lowValue = estimate10.get(i).doubleValue(); + double estValue = estimateL.get(i).doubleValue(); + double highValue = estimate90.get(i).doubleValue(); + assertTrue(lowValue <= estValue && estValue <= highValue); + } + } + + @Test + void testCombinedPowerTimeSeries() { + BridgeImpl bi = new BridgeImpl(SolarForecastBindingConstants.SOLCAST_SITE, "bridge"); + SolcastBridgeHandler scbh = new SolcastBridgeHandler(bi, new TimeZP()); + bi.setHandler(scbh); + CallbackMock cm = new CallbackMock(); + scbh.setCallback(cm); + SolcastPlaneHandler scph1 = new SolcastPlaneMock(bi); + CallbackMock cm1 = new CallbackMock(); + scph1.initialize(); + scph1.setCallback(cm1); + scbh.getData(); + + SolcastPlaneHandler scph2 = new SolcastPlaneMock(bi); + CallbackMock cm2 = new CallbackMock(); + scph2.initialize(); + scph2.setCallback(cm2); + scbh.getData(); + + TimeSeries ts1 = cm.getTimeSeries("solarforecast:sc-site:bridge:average#power-estimate"); + TimeSeries ts2 = cm2.getTimeSeries("solarforecast:sc-plane:thing:average#power-estimate"); + assertEquals(336, ts1.size(), "TimeSeries size"); + assertEquals(336, ts2.size(), "TimeSeries size"); + Iterator iter1 = ts1.getStates().iterator(); + Iterator iter2 = ts2.getStates().iterator(); + while (iter1.hasNext()) { + TimeSeries.Entry e1 = iter1.next(); + TimeSeries.Entry e2 = iter2.next(); + assertEquals("kW", ((QuantityType) e1.state()).getUnit().toString(), "Power Unit"); + assertEquals("kW", ((QuantityType) e2.state()).getUnit().toString(), "Power Unit"); + assertEquals(((QuantityType) e1.state()).doubleValue(), ((QuantityType) e2.state()).doubleValue() * 2, + 0.01, "Power Value"); + } + scbh.dispose(); + scph1.dispose(); + scph2.dispose(); + } + + @Test + void testCombinedEnergyTimeSeries() { + BridgeImpl bi = new BridgeImpl(SolarForecastBindingConstants.SOLCAST_SITE, "bridge"); + SolcastBridgeHandler scbh = new SolcastBridgeHandler(bi, new TimeZP()); + bi.setHandler(scbh); + CallbackMock cm = new CallbackMock(); + scbh.setCallback(cm); + + SolcastPlaneHandler scph1 = new SolcastPlaneMock(bi); + CallbackMock cm1 = new CallbackMock(); + scph1.initialize(); + scph1.setCallback(cm1); + + SolcastPlaneHandler scph2 = new SolcastPlaneMock(bi); + CallbackMock cm2 = new CallbackMock(); + scph2.initialize(); + scph2.setCallback(cm2); + + // simulate trigger of refresh job + scbh.getData(); + + TimeSeries ts1 = cm.getTimeSeries("solarforecast:sc-site:bridge:average#energy-estimate"); + TimeSeries ts2 = cm2.getTimeSeries("solarforecast:sc-plane:thing:average#energy-estimate"); + assertEquals(336, ts1.size(), "TimeSeries size"); + assertEquals(336, ts2.size(), "TimeSeries size"); + + Iterator iter1 = ts1.getStates().iterator(); + Iterator iter2 = ts2.getStates().iterator(); + while (iter1.hasNext()) { + TimeSeries.Entry e1 = iter1.next(); + TimeSeries.Entry e2 = iter2.next(); + assertEquals("kWh", ((QuantityType) e1.state()).getUnit().toString(), "Power Unit"); + assertEquals("kWh", ((QuantityType) e2.state()).getUnit().toString(), "Power Unit"); + assertEquals(((QuantityType) e1.state()).doubleValue(), ((QuantityType) e2.state()).doubleValue() * 2, + 0.1, "Power Value"); + } + scbh.dispose(); + scph1.dispose(); + scph2.dispose(); + } + + @Test + void testSingleEnergyTimeSeries() { + BridgeImpl bi = new BridgeImpl(SolarForecastBindingConstants.SOLCAST_SITE, "bridge"); + SolcastBridgeHandler scbh = new SolcastBridgeHandler(bi, new TimeZP()); + bi.setHandler(scbh); + CallbackMock cm = new CallbackMock(); + scbh.setCallback(cm); + + SolcastPlaneHandler scph1 = new SolcastPlaneMock(bi); + CallbackMock cm1 = new CallbackMock(); + scph1.initialize(); + scph1.setCallback(cm1); + + // simulate trigger of refresh job + scbh.getData(); + + TimeSeries ts1 = cm.getTimeSeries("solarforecast:sc-site:bridge:average#energy-estimate"); + assertEquals(336, ts1.size(), "TimeSeries size"); + Iterator iter1 = ts1.getStates().iterator(); + while (iter1.hasNext()) { + TimeSeries.Entry e1 = iter1.next(); + assertEquals("kWh", ((QuantityType) e1.state()).getUnit().toString(), "Power Unit"); + } + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/TimeZP.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/TimeZP.java new file mode 100644 index 00000000000..44712a326f5 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/TimeZP.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast; + +import java.time.ZoneId; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.i18n.TimeZoneProvider; + +/** + * The {@link TimeZP} TimeZoneProvider for tests + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class TimeZP implements TimeZoneProvider { + + @Override + public ZoneId getTimeZone() { + return SolcastTest.TEST_ZONE; + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarPlaneMock.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarPlaneMock.java new file mode 100644 index 00000000000..7b4edfef804 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarPlaneMock.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast.internal.forecastsolar.handler; + +import static org.mockito.Mockito.mock; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.solarforecast.CallbackMock; +import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; +import org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarObject; +import org.openhab.core.library.types.PointType; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.internal.ThingImpl; + +/** + * The {@link ForecastSolarPlaneMock} mocks Plane Handler for solar.forecast + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class ForecastSolarPlaneMock extends ForecastSolarPlaneHandler { + + public ForecastSolarPlaneMock(ForecastSolarObject fso) { + super(new ThingImpl(SolarForecastBindingConstants.FORECAST_SOLAR_PLANE, new ThingUID("test", "plane")), + mock(HttpClient.class)); + super.setCallback(new CallbackMock()); + setLocation(PointType.valueOf("1.23,9.87")); + super.setForecast(fso); + } + + public void updateForecast(ForecastSolarObject fso) { + super.setForecast(fso); + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastPlaneMock.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastPlaneMock.java new file mode 100644 index 00000000000..b1b48a9778b --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastPlaneMock.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarforecast.internal.solcast.handler; + +import static org.mockito.Mockito.mock; + +import java.time.Instant; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.solarforecast.FileReader; +import org.openhab.binding.solarforecast.TimeZP; +import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; +import org.openhab.binding.solarforecast.internal.solcast.SolcastObject; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.internal.BridgeImpl; +import org.openhab.core.thing.internal.ThingImpl; + +/** + * The {@link SolcastPlaneMock} mocks Plane Handler for solcast + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class SolcastPlaneMock extends SolcastPlaneHandler { + Bridge bridge; + + // solarforecast:sc-site:bridge + public SolcastPlaneMock(BridgeImpl b) { + super(new ThingImpl(SolarForecastBindingConstants.SOLCAST_PLANE, + new ThingUID("solarforecast", "sc-plane", "thing")), mock(HttpClient.class)); + bridge = b; + } + + @Override + public @Nullable Bridge getBridge() { + return bridge; + } + + @Override + protected SolcastObject fetchData() { + forecast.ifPresent(forecastObject -> { + if (forecastObject.isExpired()) { + String content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + SolcastObject sco1 = new SolcastObject("sc-test", content, Instant.now().plusSeconds(3600), + new TimeZP()); + super.setForecast(sco1); + // new forecast + } else { + super.updateChannels(forecastObject); + } + }); + return forecast.get(); + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/result.json b/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/result.json new file mode 100644 index 00000000000..ccdb1a9c289 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/result.json @@ -0,0 +1,100 @@ +{ + "result": { + "watts": { + "2022-07-17 05:31:00": 0, + "2022-07-17 06:00:00": 615, + "2022-07-17 07:00:00": 1570, + "2022-07-17 08:00:00": 2913, + "2022-07-17 09:00:00": 4103, + "2022-07-17 10:00:00": 4874, + "2022-07-17 11:00:00": 5424, + "2022-07-17 12:00:00": 5895, + "2022-07-17 13:00:00": 6075, + "2022-07-17 14:00:00": 6399, + "2022-07-17 15:00:00": 6575, + "2022-07-17 16:00:00": 5986, + "2022-07-17 17:00:00": 5251, + "2022-07-17 18:00:00": 3956, + "2022-07-17 19:00:00": 2555, + "2022-07-17 20:00:00": 1260, + "2022-07-17 21:00:00": 379, + "2022-07-17 21:32:00": 0, + "2022-07-18 05:32:00": 0, + "2022-07-18 06:00:00": 567, + "2022-07-18 07:00:00": 1544, + "2022-07-18 08:00:00": 2754, + "2022-07-18 09:00:00": 3958, + "2022-07-18 10:00:00": 5085, + "2022-07-18 11:00:00": 6058, + "2022-07-18 12:00:00": 6698, + "2022-07-18 13:00:00": 7029, + "2022-07-18 14:00:00": 7054, + "2022-07-18 15:00:00": 6692, + "2022-07-18 16:00:00": 5978, + "2022-07-18 17:00:00": 4937, + "2022-07-18 18:00:00": 3698, + "2022-07-18 19:00:00": 2333, + "2022-07-18 20:00:00": 1078, + "2022-07-18 21:00:00": 320, + "2022-07-18 21:31:00": 0 + }, + "watt_hours": { + "2022-07-17 05:31:00": 0, + "2022-07-17 06:00:00": 149, + "2022-07-17 07:00:00": 1241, + "2022-07-17 08:00:00": 3483, + "2022-07-17 09:00:00": 6991, + "2022-07-17 10:00:00": 11479, + "2022-07-17 11:00:00": 16628, + "2022-07-17 12:00:00": 22288, + "2022-07-17 13:00:00": 28273, + "2022-07-17 14:00:00": 34510, + "2022-07-17 15:00:00": 40997, + "2022-07-17 16:00:00": 47277, + "2022-07-17 17:00:00": 52896, + "2022-07-17 18:00:00": 57499, + "2022-07-17 19:00:00": 60755, + "2022-07-17 20:00:00": 62662, + "2022-07-17 21:00:00": 63482, + "2022-07-17 21:32:00": 63583, + "2022-07-18 05:32:00": 0, + "2022-07-18 06:00:00": 132, + "2022-07-18 07:00:00": 1188, + "2022-07-18 08:00:00": 3337, + "2022-07-18 09:00:00": 6693, + "2022-07-18 10:00:00": 11214, + "2022-07-18 11:00:00": 16786, + "2022-07-18 12:00:00": 23164, + "2022-07-18 13:00:00": 30027, + "2022-07-18 14:00:00": 37069, + "2022-07-18 15:00:00": 43942, + "2022-07-18 16:00:00": 50277, + "2022-07-18 17:00:00": 55734, + "2022-07-18 18:00:00": 60052, + "2022-07-18 19:00:00": 63067, + "2022-07-18 20:00:00": 64773, + "2022-07-18 21:00:00": 65472, + "2022-07-18 21:31:00": 65554 + }, + "watt_hours_day": { + "2022-07-17": 63583, + "2022-07-18": 65554 + } + }, + "message": { + "code": 0, + "type": "success", + "text": "", + "info": { + "latitude": 54.321, + "longitude": 8.765, + "place": "Whereever", + "timezone": "Europe/Berlin" + }, + "ratelimit": { + "period": 3600, + "limit": 12, + "remaining": 10 + } + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/resultNextDay.json b/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/resultNextDay.json new file mode 100644 index 00000000000..412612f378d --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/resultNextDay.json @@ -0,0 +1,100 @@ +{ + "result": { + "watts": { + "2022-07-19 05:31:00": 0, + "2022-07-19 06:00:00": 615, + "2022-07-19 07:00:00": 1570, + "2022-07-19 08:00:00": 2913, + "2022-07-19 09:00:00": 4103, + "2022-07-19 10:00:00": 4874, + "2022-07-19 11:00:00": 5424, + "2022-07-19 12:00:00": 5895, + "2022-07-19 13:00:00": 6075, + "2022-07-19 14:00:00": 6399, + "2022-07-19 15:00:00": 6575, + "2022-07-19 16:00:00": 5986, + "2022-07-19 17:00:00": 5251, + "2022-07-19 18:00:00": 3956, + "2022-07-19 19:00:00": 2555, + "2022-07-19 20:00:00": 1260, + "2022-07-19 21:00:00": 379, + "2022-07-19 21:32:00": 0, + "2022-07-18 05:32:00": 0, + "2022-07-18 06:00:00": 567, + "2022-07-18 07:00:00": 1544, + "2022-07-18 08:00:00": 2754, + "2022-07-18 09:00:00": 3958, + "2022-07-18 10:00:00": 5085, + "2022-07-18 11:00:00": 6058, + "2022-07-18 12:00:00": 6698, + "2022-07-18 13:00:00": 7029, + "2022-07-18 14:00:00": 7054, + "2022-07-18 15:00:00": 6692, + "2022-07-18 16:00:00": 5978, + "2022-07-18 17:00:00": 4937, + "2022-07-18 18:00:00": 3698, + "2022-07-18 19:00:00": 2333, + "2022-07-18 20:00:00": 1078, + "2022-07-18 21:00:00": 320, + "2022-07-18 21:31:00": 0 + }, + "watt_hours": { + "2022-07-19 05:31:00": 0, + "2022-07-19 06:00:00": 149, + "2022-07-19 07:00:00": 1241, + "2022-07-19 08:00:00": 3483, + "2022-07-19 09:00:00": 6991, + "2022-07-19 10:00:00": 11479, + "2022-07-19 11:00:00": 16628, + "2022-07-19 12:00:00": 22288, + "2022-07-19 13:00:00": 28273, + "2022-07-19 14:00:00": 34510, + "2022-07-19 15:00:00": 40997, + "2022-07-19 16:00:00": 47277, + "2022-07-19 17:00:00": 52896, + "2022-07-19 18:00:00": 57499, + "2022-07-19 19:00:00": 60755, + "2022-07-19 20:00:00": 62662, + "2022-07-19 21:00:00": 63482, + "2022-07-19 21:32:00": 63583, + "2022-07-18 05:32:00": 0, + "2022-07-18 06:00:00": 132, + "2022-07-18 07:00:00": 1188, + "2022-07-18 08:00:00": 3337, + "2022-07-18 09:00:00": 6693, + "2022-07-18 10:00:00": 11214, + "2022-07-18 11:00:00": 16786, + "2022-07-18 12:00:00": 23164, + "2022-07-18 13:00:00": 30027, + "2022-07-18 14:00:00": 37069, + "2022-07-18 15:00:00": 43942, + "2022-07-18 16:00:00": 50277, + "2022-07-18 17:00:00": 55734, + "2022-07-18 18:00:00": 60052, + "2022-07-18 19:00:00": 63067, + "2022-07-18 20:00:00": 64773, + "2022-07-18 21:00:00": 65472, + "2022-07-18 21:31:00": 65554 + }, + "watt_hours_day": { + "2022-07-19": 63583, + "2022-07-18": 65554 + } + }, + "message": { + "code": 0, + "type": "success", + "text": "", + "info": { + "latitude": 54.321, + "longitude": 8.765, + "place": "Whereever", + "timezone": "Europe/Berlin" + }, + "ratelimit": { + "period": 3600, + "limit": 12, + "remaining": 10 + } + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.solarforecast/src/test/resources/solcast/estimated-actuals.json b/bundles/org.openhab.binding.solarforecast/src/test/resources/solcast/estimated-actuals.json new file mode 100644 index 00000000000..83857b305ce --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/resources/solcast/estimated-actuals.json @@ -0,0 +1,1684 @@ +{ + "estimated_actuals": [ + { + "pv_estimate": 0, + "period_end": "2022-07-17T21:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-17T20:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-17T20:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0132, + "period_end": "2022-07-17T19:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0848, + "period_end": "2022-07-17T19:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.2543, + "period_end": "2022-07-17T18:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.4465, + "period_end": "2022-07-17T18:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.6164, + "period_end": "2022-07-17T17:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.8581, + "period_end": "2022-07-17T17:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.0839, + "period_end": "2022-07-17T16:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.2834, + "period_end": "2022-07-17T16:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.5031, + "period_end": "2022-07-17T15:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.724, + "period_end": "2022-07-17T15:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9318, + "period_end": "2022-07-17T14:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.1015, + "period_end": "2022-07-17T14:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.2374, + "period_end": "2022-07-17T13:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3656, + "period_end": "2022-07-17T13:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.4651, + "period_end": "2022-07-17T12:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.5378, + "period_end": "2022-07-17T12:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.6181, + "period_end": "2022-07-17T11:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.6061, + "period_end": "2022-07-17T11:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.6213, + "period_end": "2022-07-17T10:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.602, + "period_end": "2022-07-17T10:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.5438, + "period_end": "2022-07-17T09:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.4719, + "period_end": "2022-07-17T09:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3766, + "period_end": "2022-07-17T08:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.2365, + "period_end": "2022-07-17T08:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.0613, + "period_end": "2022-07-17T07:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.8614, + "period_end": "2022-07-17T07:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.6401, + "period_end": "2022-07-17T06:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.3848, + "period_end": "2022-07-17T06:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.0663, + "period_end": "2022-07-17T05:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.7772, + "period_end": "2022-07-17T05:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.4252, + "period_end": "2022-07-17T04:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0262, + "period_end": "2022-07-17T04:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-17T03:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-17T03:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-17T02:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-17T02:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-17T01:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-17T01:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-17T00:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-17T00:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-16T23:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-16T23:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-16T22:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-16T22:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-16T21:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-16T21:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-16T20:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-16T20:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0332, + "period_end": "2022-07-16T19:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.1097, + "period_end": "2022-07-16T19:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.2983, + "period_end": "2022-07-16T18:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.47, + "period_end": "2022-07-16T18:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.6658, + "period_end": "2022-07-16T17:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.9006, + "period_end": "2022-07-16T17:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.1604, + "period_end": "2022-07-16T16:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.357, + "period_end": "2022-07-16T16:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.564, + "period_end": "2022-07-16T15:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.7801, + "period_end": "2022-07-16T15:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9812, + "period_end": "2022-07-16T14:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9825, + "period_end": "2022-07-16T14:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9112, + "period_end": "2022-07-16T13:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.077, + "period_end": "2022-07-16T13:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9073, + "period_end": "2022-07-16T12:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.4129, + "period_end": "2022-07-16T12:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.5011, + "period_end": "2022-07-16T11:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.8266, + "period_end": "2022-07-16T11:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.0153, + "period_end": "2022-07-16T10:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.1025, + "period_end": "2022-07-16T10:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.0439, + "period_end": "2022-07-16T09:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.5081, + "period_end": "2022-07-16T09:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3942, + "period_end": "2022-07-16T08:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.4576, + "period_end": "2022-07-16T08:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.6787, + "period_end": "2022-07-16T07:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.1379, + "period_end": "2022-07-16T07:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.4369, + "period_end": "2022-07-16T06:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.9787, + "period_end": "2022-07-16T06:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.724, + "period_end": "2022-07-16T05:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.22, + "period_end": "2022-07-16T05:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.3797, + "period_end": "2022-07-16T04:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0256, + "period_end": "2022-07-16T04:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-16T03:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-16T03:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-16T02:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-16T02:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-16T01:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-16T01:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-16T00:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-16T00:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-15T23:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-15T23:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-15T22:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-15T22:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-15T21:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-15T21:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-15T20:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0046, + "period_end": "2022-07-15T20:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0345, + "period_end": "2022-07-15T19:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.1346, + "period_end": "2022-07-15T19:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.3021, + "period_end": "2022-07-15T18:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.4937, + "period_end": "2022-07-15T18:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.6943, + "period_end": "2022-07-15T17:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.8941, + "period_end": "2022-07-15T17:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.1308, + "period_end": "2022-07-15T16:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.3697, + "period_end": "2022-07-15T16:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.5757, + "period_end": "2022-07-15T15:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.7271, + "period_end": "2022-07-15T15:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9182, + "period_end": "2022-07-15T14:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.1814, + "period_end": "2022-07-15T14:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.2006, + "period_end": "2022-07-15T13:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.2107, + "period_end": "2022-07-15T13:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.1333, + "period_end": "2022-07-15T12:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.369, + "period_end": "2022-07-15T12:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.0206, + "period_end": "2022-07-15T11:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.0481, + "period_end": "2022-07-15T11:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.329, + "period_end": "2022-07-15T10:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.7864, + "period_end": "2022-07-15T10:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.1888, + "period_end": "2022-07-15T09:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3456, + "period_end": "2022-07-15T09:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.4461, + "period_end": "2022-07-15T08:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.0576, + "period_end": "2022-07-15T08:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.2638, + "period_end": "2022-07-15T07:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.8807, + "period_end": "2022-07-15T07:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.7287, + "period_end": "2022-07-15T06:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.2221, + "period_end": "2022-07-15T06:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.1837, + "period_end": "2022-07-15T05:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0757, + "period_end": "2022-07-15T05:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0979, + "period_end": "2022-07-15T04:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0307, + "period_end": "2022-07-15T04:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-15T03:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-15T03:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-15T02:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-15T02:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-15T01:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-15T01:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-15T00:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-15T00:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-14T23:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-14T23:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-14T22:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-14T22:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-14T21:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-14T21:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-14T20:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0023, + "period_end": "2022-07-14T20:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0342, + "period_end": "2022-07-14T19:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.1368, + "period_end": "2022-07-14T19:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.3008, + "period_end": "2022-07-14T18:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.3319, + "period_end": "2022-07-14T18:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.7022, + "period_end": "2022-07-14T17:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.9083, + "period_end": "2022-07-14T17:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.1591, + "period_end": "2022-07-14T16:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.3839, + "period_end": "2022-07-14T16:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.549, + "period_end": "2022-07-14T15:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.6209, + "period_end": "2022-07-14T15:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9147, + "period_end": "2022-07-14T14:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.7257, + "period_end": "2022-07-14T14:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.0642, + "period_end": "2022-07-14T13:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.1923, + "period_end": "2022-07-14T13:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.6164, + "period_end": "2022-07-14T12:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9841, + "period_end": "2022-07-14T12:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3529, + "period_end": "2022-07-14T11:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.4802, + "period_end": "2022-07-14T11:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.5034, + "period_end": "2022-07-14T10:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.5874, + "period_end": "2022-07-14T10:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.6305, + "period_end": "2022-07-14T09:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.4497, + "period_end": "2022-07-14T09:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.0006, + "period_end": "2022-07-14T08:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.5463, + "period_end": "2022-07-14T08:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.4454, + "period_end": "2022-07-14T07:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.6282, + "period_end": "2022-07-14T07:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.4334, + "period_end": "2022-07-14T06:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.4264, + "period_end": "2022-07-14T06:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.2235, + "period_end": "2022-07-14T05:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0836, + "period_end": "2022-07-14T05:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0494, + "period_end": "2022-07-14T04:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0068, + "period_end": "2022-07-14T04:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-14T03:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-14T03:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-14T02:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-14T02:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-14T01:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-14T01:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-14T00:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-14T00:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-13T23:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-13T23:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-13T22:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-13T22:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-13T21:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-13T21:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-13T20:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-13T20:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0203, + "period_end": "2022-07-13T19:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0942, + "period_end": "2022-07-13T19:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.2893, + "period_end": "2022-07-13T18:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.3287, + "period_end": "2022-07-13T18:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.6298, + "period_end": "2022-07-13T17:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.8687, + "period_end": "2022-07-13T17:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.0799, + "period_end": "2022-07-13T16:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.1941, + "period_end": "2022-07-13T16:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.4071, + "period_end": "2022-07-13T15:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.642, + "period_end": "2022-07-13T15:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.8272, + "period_end": "2022-07-13T14:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9812, + "period_end": "2022-07-13T14:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.2045, + "period_end": "2022-07-13T13:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3269, + "period_end": "2022-07-13T13:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.1805, + "period_end": "2022-07-13T12:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3253, + "period_end": "2022-07-13T12:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.2855, + "period_end": "2022-07-13T11:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.4151, + "period_end": "2022-07-13T11:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.8633, + "period_end": "2022-07-13T10:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.548, + "period_end": "2022-07-13T10:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.4838, + "period_end": "2022-07-13T09:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.7732, + "period_end": "2022-07-13T09:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.1873, + "period_end": "2022-07-13T08:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.8801, + "period_end": "2022-07-13T08:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.7869, + "period_end": "2022-07-13T07:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.7447, + "period_end": "2022-07-13T07:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.7483, + "period_end": "2022-07-13T06:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.3534, + "period_end": "2022-07-13T06:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.1224, + "period_end": "2022-07-13T05:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.1822, + "period_end": "2022-07-13T05:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0288, + "period_end": "2022-07-13T04:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0044, + "period_end": "2022-07-13T04:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-13T03:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-13T03:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-13T02:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-13T02:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-13T01:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-13T01:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-13T00:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-13T00:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-12T23:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-12T23:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-12T22:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-12T22:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-12T21:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-12T21:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-12T20:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-12T20:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.025, + "period_end": "2022-07-12T19:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0942, + "period_end": "2022-07-12T19:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.181, + "period_end": "2022-07-12T18:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.4866, + "period_end": "2022-07-12T18:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.6711, + "period_end": "2022-07-12T17:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.898, + "period_end": "2022-07-12T17:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.1145, + "period_end": "2022-07-12T16:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.3165, + "period_end": "2022-07-12T16:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.507, + "period_end": "2022-07-12T15:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.7555, + "period_end": "2022-07-12T15:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9616, + "period_end": "2022-07-12T14:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.1316, + "period_end": "2022-07-12T14:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.2636, + "period_end": "2022-07-12T13:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3862, + "period_end": "2022-07-12T13:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.499, + "period_end": "2022-07-12T12:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.584, + "period_end": "2022-07-12T12:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.6534, + "period_end": "2022-07-12T11:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.6568, + "period_end": "2022-07-12T11:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.6734, + "period_end": "2022-07-12T10:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.6413, + "period_end": "2022-07-12T10:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.6053, + "period_end": "2022-07-12T09:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.2459, + "period_end": "2022-07-12T09:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.2619, + "period_end": "2022-07-12T08:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.073, + "period_end": "2022-07-12T08:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.1316, + "period_end": "2022-07-12T07:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9143, + "period_end": "2022-07-12T07:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.7024, + "period_end": "2022-07-12T06:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.453, + "period_end": "2022-07-12T06:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.2017, + "period_end": "2022-07-12T05:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.8564, + "period_end": "2022-07-12T05:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.4738, + "period_end": "2022-07-12T04:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0769, + "period_end": "2022-07-12T04:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-12T03:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-12T03:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-12T02:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-12T02:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-12T01:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-12T01:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-12T00:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-12T00:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-11T23:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-11T23:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-11T22:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-11T22:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-11T21:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-11T21:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-11T20:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0045, + "period_end": "2022-07-11T20:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0443, + "period_end": "2022-07-11T19:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.1255, + "period_end": "2022-07-11T19:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.29, + "period_end": "2022-07-11T18:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.4389, + "period_end": "2022-07-11T18:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.6142, + "period_end": "2022-07-11T17:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.5635, + "period_end": "2022-07-11T17:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.8217, + "period_end": "2022-07-11T16:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.0935, + "period_end": "2022-07-11T16:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.3116, + "period_end": "2022-07-11T15:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.3947, + "period_end": "2022-07-11T15:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.6082, + "period_end": "2022-07-11T14:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.3857, + "period_end": "2022-07-11T14:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.9869, + "period_end": "2022-07-11T13:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.574, + "period_end": "2022-07-11T13:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.566, + "period_end": "2022-07-11T12:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.5114, + "period_end": "2022-07-11T12:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.7266, + "period_end": "2022-07-11T11:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.3484, + "period_end": "2022-07-11T11:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.2986, + "period_end": "2022-07-11T10:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.1635, + "period_end": "2022-07-11T10:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.3318, + "period_end": "2022-07-11T09:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.2608, + "period_end": "2022-07-11T09:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.2389, + "period_end": "2022-07-11T08:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.0139, + "period_end": "2022-07-11T08:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.1048, + "period_end": "2022-07-11T07:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.6094, + "period_end": "2022-07-11T07:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.6392, + "period_end": "2022-07-11T06:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.3935, + "period_end": "2022-07-11T06:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.0654, + "period_end": "2022-07-11T05:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.7801, + "period_end": "2022-07-11T05:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.3273, + "period_end": "2022-07-11T04:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0323, + "period_end": "2022-07-11T04:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-11T03:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-11T03:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-11T02:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-11T02:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-11T01:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-11T01:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-11T00:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-11T00:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-10T23:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-10T23:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-10T22:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-10T22:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "period_end": "2022-07-10T21:30:00.0000000Z", + "period": "PT30M" + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.solarforecast/src/test/resources/solcast/forecasts.json b/bundles/org.openhab.binding.solarforecast/src/test/resources/solcast/forecasts.json new file mode 100644 index 00000000000..9fa129cc477 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/resources/solcast/forecasts.json @@ -0,0 +1,2356 @@ +{ + "forecasts": [ + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-17T21:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-17T22:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-17T22:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-17T23:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-17T23:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-18T00:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-18T00:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-18T01:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-18T01:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-18T02:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-18T02:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-18T03:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-18T03:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0205, + "pv_estimate10": 0.0047, + "pv_estimate90": 0.0205, + "period_end": "2022-07-18T04:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.1416, + "pv_estimate10": 0.0579, + "pv_estimate90": 0.1848, + "period_end": "2022-07-18T04:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.4478, + "pv_estimate10": 0.1449, + "pv_estimate90": 0.5472, + "period_end": "2022-07-18T05:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.763, + "pv_estimate10": 0.3284, + "pv_estimate90": 0.8842, + "period_end": "2022-07-18T05:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.1367, + "pv_estimate10": 0.5292, + "pv_estimate90": 1.2464, + "period_end": "2022-07-18T06:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.4044, + "pv_estimate10": 0.7642, + "pv_estimate90": 1.5202, + "period_end": "2022-07-18T06:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.6632, + "pv_estimate10": 1.0131, + "pv_estimate90": 1.7651, + "period_end": "2022-07-18T07:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.8667, + "pv_estimate10": 1.2179, + "pv_estimate90": 1.9681, + "period_end": "2022-07-18T07:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.0729, + "pv_estimate10": 1.4322, + "pv_estimate90": 2.1579, + "period_end": "2022-07-18T08:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.2377, + "pv_estimate10": 1.5748, + "pv_estimate90": 2.2838, + "period_end": "2022-07-18T08:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3516, + "pv_estimate10": 1.7452, + "pv_estimate90": 2.4013, + "period_end": "2022-07-18T09:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.4295, + "pv_estimate10": 1.8484, + "pv_estimate90": 2.4794, + "period_end": "2022-07-18T09:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.5136, + "pv_estimate10": 1.9304, + "pv_estimate90": 2.5415, + "period_end": "2022-07-18T10:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.5295, + "pv_estimate10": 2.0067, + "pv_estimate90": 2.5558, + "period_end": "2022-07-18T10:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.526, + "pv_estimate10": 2.0308, + "pv_estimate90": 2.5485, + "period_end": "2022-07-18T11:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.4879, + "pv_estimate10": 2.0368, + "pv_estimate90": 2.5133, + "period_end": "2022-07-18T11:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.4092, + "pv_estimate10": 2.0135, + "pv_estimate90": 2.4482, + "period_end": "2022-07-18T12:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3309, + "pv_estimate10": 1.9633, + "pv_estimate90": 2.3677, + "period_end": "2022-07-18T12:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.1984, + "pv_estimate10": 1.8494, + "pv_estimate90": 2.2333, + "period_end": "2022-07-18T13:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.0416, + "pv_estimate10": 1.7461, + "pv_estimate90": 2.1, + "period_end": "2022-07-18T13:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9076, + "pv_estimate10": 1.6195, + "pv_estimate90": 1.9674, + "period_end": "2022-07-18T14:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.7416, + "pv_estimate10": 1.4758, + "pv_estimate90": 1.7931, + "period_end": "2022-07-18T14:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.5414, + "pv_estimate10": 1.3132, + "pv_estimate90": 1.5823, + "period_end": "2022-07-18T15:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.3683, + "pv_estimate10": 1.1483, + "pv_estimate90": 1.3963, + "period_end": "2022-07-18T15:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.1603, + "pv_estimate10": 0.956, + "pv_estimate90": 1.1803, + "period_end": "2022-07-18T16:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.9527, + "pv_estimate10": 0.7762, + "pv_estimate90": 0.9654, + "period_end": "2022-07-18T16:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.7705, + "pv_estimate10": 0.5919, + "pv_estimate90": 0.7733, + "period_end": "2022-07-18T17:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.5673, + "pv_estimate10": 0.3992, + "pv_estimate90": 0.5678, + "period_end": "2022-07-18T17:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.3588, + "pv_estimate10": 0.2221, + "pv_estimate90": 0.37674, + "period_end": "2022-07-18T18:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.1948, + "pv_estimate10": 0.0952, + "pv_estimate90": 0.1999, + "period_end": "2022-07-18T18:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0654, + "pv_estimate10": 0.0423, + "pv_estimate90": 0.0676, + "period_end": "2022-07-18T19:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0118, + "pv_estimate10": 0.0084, + "pv_estimate90": 0.0118, + "period_end": "2022-07-18T19:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-18T20:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-18T20:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-18T21:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-18T21:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-18T22:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-18T22:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-18T23:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-18T23:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-19T00:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-19T00:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-19T01:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-19T01:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-19T02:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-19T02:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-19T03:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-19T03:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0175, + "pv_estimate10": 0.0045, + "pv_estimate90": 0.0175, + "period_end": "2022-07-19T04:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.1377, + "pv_estimate10": 0.0561, + "pv_estimate90": 0.1377, + "period_end": "2022-07-19T04:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.4737, + "pv_estimate10": 0.1767, + "pv_estimate90": 0.4737, + "period_end": "2022-07-19T05:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.792, + "pv_estimate10": 0.3811, + "pv_estimate90": 0.792, + "period_end": "2022-07-19T05:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.1438, + "pv_estimate10": 0.6405, + "pv_estimate90": 1.1438, + "period_end": "2022-07-19T06:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.4346, + "pv_estimate10": 0.8964, + "pv_estimate90": 1.4346, + "period_end": "2022-07-19T06:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.6679, + "pv_estimate10": 1.1527, + "pv_estimate90": 1.6679, + "period_end": "2022-07-19T07:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.8955, + "pv_estimate10": 1.3956, + "pv_estimate90": 1.8955, + "period_end": "2022-07-19T07:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.0584, + "pv_estimate10": 1.6084, + "pv_estimate90": 2.0584, + "period_end": "2022-07-19T08:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.1963, + "pv_estimate10": 1.7982, + "pv_estimate90": 2.1963, + "period_end": "2022-07-19T08:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3135, + "pv_estimate10": 1.9441, + "pv_estimate90": 2.3135, + "period_end": "2022-07-19T09:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.393, + "pv_estimate10": 2.0729, + "pv_estimate90": 2.393, + "period_end": "2022-07-19T09:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.4412, + "pv_estimate10": 2.1543, + "pv_estimate90": 2.4412, + "period_end": "2022-07-19T10:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.4754, + "pv_estimate10": 2.2173, + "pv_estimate90": 2.4754, + "period_end": "2022-07-19T10:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.4695, + "pv_estimate10": 2.2363, + "pv_estimate90": 2.4695, + "period_end": "2022-07-19T11:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.4306, + "pv_estimate10": 2.2238, + "pv_estimate90": 2.4306, + "period_end": "2022-07-19T11:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3763, + "pv_estimate10": 2.1976, + "pv_estimate90": 2.3763, + "period_end": "2022-07-19T12:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3003, + "pv_estimate10": 2.1378, + "pv_estimate90": 2.3003, + "period_end": "2022-07-19T12:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.1886, + "pv_estimate10": 2.0286, + "pv_estimate90": 2.1886, + "period_end": "2022-07-19T13:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.06, + "pv_estimate10": 1.9223, + "pv_estimate90": 2.06, + "period_end": "2022-07-19T13:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9249, + "pv_estimate10": 1.8002, + "pv_estimate90": 1.9249, + "period_end": "2022-07-19T14:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.7487, + "pv_estimate10": 1.6508, + "pv_estimate90": 1.7487, + "period_end": "2022-07-19T14:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.557, + "pv_estimate10": 1.4728, + "pv_estimate90": 1.557, + "period_end": "2022-07-19T15:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.3751, + "pv_estimate10": 1.3098, + "pv_estimate90": 1.3751, + "period_end": "2022-07-19T15:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.1584, + "pv_estimate10": 1.1127, + "pv_estimate90": 1.1584, + "period_end": "2022-07-19T16:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.9441, + "pv_estimate10": 0.9165, + "pv_estimate90": 0.9441, + "period_end": "2022-07-19T16:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.7338, + "pv_estimate10": 0.7171, + "pv_estimate90": 0.7338, + "period_end": "2022-07-19T17:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.5643, + "pv_estimate10": 0.5355, + "pv_estimate90": 0.5643, + "period_end": "2022-07-19T17:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.355, + "pv_estimate10": 0.3264, + "pv_estimate90": 0.355, + "period_end": "2022-07-19T18:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.2006, + "pv_estimate10": 0.1561, + "pv_estimate90": 0.2006, + "period_end": "2022-07-19T18:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0642, + "pv_estimate10": 0.056, + "pv_estimate90": 0.0642, + "period_end": "2022-07-19T19:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0095, + "pv_estimate10": 0.0062, + "pv_estimate90": 0.0095, + "period_end": "2022-07-19T19:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-19T20:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-19T20:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-19T21:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-19T21:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-19T22:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-19T22:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-19T23:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-19T23:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-20T00:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-20T00:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-20T01:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-20T01:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-20T02:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-20T02:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-20T03:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-20T03:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0044, + "pv_estimate10": 0.0022, + "pv_estimate90": 0.0151, + "period_end": "2022-07-20T04:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.1128, + "pv_estimate10": 0.0329, + "pv_estimate90": 0.1553, + "period_end": "2022-07-20T04:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.3939, + "pv_estimate10": 0.0762, + "pv_estimate90": 0.4737, + "period_end": "2022-07-20T05:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.7242, + "pv_estimate10": 0.1319, + "pv_estimate90": 0.8376, + "period_end": "2022-07-20T05:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.9885, + "pv_estimate10": 0.2423, + "pv_estimate90": 1.1318, + "period_end": "2022-07-20T06:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.2297, + "pv_estimate10": 0.36, + "pv_estimate90": 1.4031, + "period_end": "2022-07-20T06:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.4211, + "pv_estimate10": 0.4615, + "pv_estimate90": 1.6512, + "period_end": "2022-07-20T07:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.5682, + "pv_estimate10": 0.5595, + "pv_estimate90": 1.8406, + "period_end": "2022-07-20T07:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.6963, + "pv_estimate10": 0.628, + "pv_estimate90": 2.0071, + "period_end": "2022-07-20T08:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.8038, + "pv_estimate10": 0.6912, + "pv_estimate90": 2.1486, + "period_end": "2022-07-20T08:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.867, + "pv_estimate10": 0.691, + "pv_estimate90": 2.2611, + "period_end": "2022-07-20T09:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9107, + "pv_estimate10": 0.707, + "pv_estimate90": 2.3226, + "period_end": "2022-07-20T09:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9349, + "pv_estimate10": 0.719, + "pv_estimate90": 2.3591, + "period_end": "2022-07-20T10:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9591, + "pv_estimate10": 0.7227, + "pv_estimate90": 2.3784, + "period_end": "2022-07-20T10:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9951, + "pv_estimate10": 0.7658, + "pv_estimate90": 2.3608, + "period_end": "2022-07-20T11:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.0016, + "pv_estimate10": 0.7767, + "pv_estimate90": 2.3226, + "period_end": "2022-07-20T11:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9624, + "pv_estimate10": 0.765, + "pv_estimate90": 2.2519, + "period_end": "2022-07-20T12:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.927, + "pv_estimate10": 0.7802, + "pv_estimate90": 2.187, + "period_end": "2022-07-20T12:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.876, + "pv_estimate10": 0.784, + "pv_estimate90": 2.0918, + "period_end": "2022-07-20T13:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.7972, + "pv_estimate10": 0.7834, + "pv_estimate90": 1.9873, + "period_end": "2022-07-20T13:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.6934, + "pv_estimate10": 0.7207, + "pv_estimate90": 1.8705, + "period_end": "2022-07-20T14:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.573, + "pv_estimate10": 0.693, + "pv_estimate90": 1.7139, + "period_end": "2022-07-20T14:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.4334, + "pv_estimate10": 0.6639, + "pv_estimate90": 1.5257, + "period_end": "2022-07-20T15:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.2773, + "pv_estimate10": 0.5927, + "pv_estimate90": 1.3469, + "period_end": "2022-07-20T15:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.077, + "pv_estimate10": 0.4745, + "pv_estimate90": 1.1327, + "period_end": "2022-07-20T16:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.8892, + "pv_estimate10": 0.3671, + "pv_estimate90": 0.9373, + "period_end": "2022-07-20T16:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.6825, + "pv_estimate10": 0.2454, + "pv_estimate90": 0.7374, + "period_end": "2022-07-20T17:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.4909, + "pv_estimate10": 0.1358, + "pv_estimate90": 0.5488, + "period_end": "2022-07-20T17:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.2984, + "pv_estimate10": 0.0778, + "pv_estimate90": 0.341, + "period_end": "2022-07-20T18:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.1269, + "pv_estimate10": 0.044, + "pv_estimate90": 0.1543, + "period_end": "2022-07-20T18:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0543, + "pv_estimate10": 0.0192, + "pv_estimate90": 0.0638, + "period_end": "2022-07-20T19:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0072, + "pv_estimate10": 0.0022, + "pv_estimate90": 0.0079, + "period_end": "2022-07-20T19:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-20T20:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-20T20:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-20T21:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-20T21:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-20T22:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-20T22:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-20T23:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-20T23:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-21T00:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-21T00:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-21T01:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-21T01:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-21T02:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-21T02:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-21T03:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-21T03:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0022, + "pv_estimate10": 0, + "pv_estimate90": 0.0022, + "period_end": "2022-07-21T04:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.1015, + "pv_estimate10": 0.0179, + "pv_estimate90": 0.1911, + "period_end": "2022-07-21T04:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.295, + "pv_estimate10": 0.0471, + "pv_estimate90": 0.4675, + "period_end": "2022-07-21T05:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.6214, + "pv_estimate10": 0.0978, + "pv_estimate90": 0.8657, + "period_end": "2022-07-21T05:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.9143, + "pv_estimate10": 0.1984, + "pv_estimate90": 1.1813, + "period_end": "2022-07-21T06:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.1982, + "pv_estimate10": 0.3472, + "pv_estimate90": 1.4691, + "period_end": "2022-07-21T06:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.5092, + "pv_estimate10": 0.5175, + "pv_estimate90": 1.7602, + "period_end": "2022-07-21T07:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.7137, + "pv_estimate10": 0.6504, + "pv_estimate90": 1.9701, + "period_end": "2022-07-21T07:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9236, + "pv_estimate10": 0.8177, + "pv_estimate90": 2.1465, + "period_end": "2022-07-21T08:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.071, + "pv_estimate10": 0.9283, + "pv_estimate90": 2.3015, + "period_end": "2022-07-21T08:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.2137, + "pv_estimate10": 1.0682, + "pv_estimate90": 2.4064, + "period_end": "2022-07-21T09:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3307, + "pv_estimate10": 1.179, + "pv_estimate90": 2.5079, + "period_end": "2022-07-21T09:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3836, + "pv_estimate10": 1.267, + "pv_estimate90": 2.5587, + "period_end": "2022-07-21T10:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.406, + "pv_estimate10": 1.2955, + "pv_estimate90": 2.5943, + "period_end": "2022-07-21T10:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3884, + "pv_estimate10": 1.2957, + "pv_estimate90": 2.5844, + "period_end": "2022-07-21T11:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3529, + "pv_estimate10": 1.2832, + "pv_estimate90": 2.5529, + "period_end": "2022-07-21T11:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.2804, + "pv_estimate10": 1.2464, + "pv_estimate90": 2.4864, + "period_end": "2022-07-21T12:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.2065, + "pv_estimate10": 1.23, + "pv_estimate90": 2.4041, + "period_end": "2022-07-21T12:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.1312, + "pv_estimate10": 1.2279, + "pv_estimate90": 2.3012, + "period_end": "2022-07-21T13:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.0178, + "pv_estimate10": 1.2028, + "pv_estimate90": 2.1646, + "period_end": "2022-07-21T13:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.8701, + "pv_estimate10": 1.1297, + "pv_estimate90": 1.9989, + "period_end": "2022-07-21T14:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.7169, + "pv_estimate10": 1.0696, + "pv_estimate90": 1.82, + "period_end": "2022-07-21T14:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.5509, + "pv_estimate10": 0.9652, + "pv_estimate90": 1.6333, + "period_end": "2022-07-21T15:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.3622, + "pv_estimate10": 0.8778, + "pv_estimate90": 1.421, + "period_end": "2022-07-21T15:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.1869, + "pv_estimate10": 0.7675, + "pv_estimate90": 1.2202, + "period_end": "2022-07-21T16:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.9759, + "pv_estimate10": 0.6284, + "pv_estimate90": 0.9923, + "period_end": "2022-07-21T16:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.7675, + "pv_estimate10": 0.4705, + "pv_estimate90": 0.7693, + "period_end": "2022-07-21T17:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.5842, + "pv_estimate10": 0.3289, + "pv_estimate90": 0.6134100000000001, + "period_end": "2022-07-21T17:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.3788, + "pv_estimate10": 0.1871, + "pv_estimate90": 0.39774000000000004, + "period_end": "2022-07-21T18:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.1769, + "pv_estimate10": 0.0833, + "pv_estimate90": 0.18574500000000002, + "period_end": "2022-07-21T18:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0654, + "pv_estimate10": 0.0353, + "pv_estimate90": 0.0682, + "period_end": "2022-07-21T19:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0073, + "pv_estimate10": 0.0044, + "pv_estimate90": 0.0073, + "period_end": "2022-07-21T19:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-21T20:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-21T20:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-21T21:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-21T21:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-21T22:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-21T22:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-21T23:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-21T23:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-22T00:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-22T00:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-22T01:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-22T01:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-22T02:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-22T02:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-22T03:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-22T03:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-22T04:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0916, + "pv_estimate10": 0.0183, + "pv_estimate90": 0.1886, + "period_end": "2022-07-22T04:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.2989, + "pv_estimate10": 0.0481, + "pv_estimate90": 0.4564, + "period_end": "2022-07-22T05:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.6014, + "pv_estimate10": 0.0885, + "pv_estimate90": 0.8581, + "period_end": "2022-07-22T05:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.9027, + "pv_estimate10": 0.1654, + "pv_estimate90": 1.1849, + "period_end": "2022-07-22T06:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.2082, + "pv_estimate10": 0.2747, + "pv_estimate90": 1.5032, + "period_end": "2022-07-22T06:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.4825, + "pv_estimate10": 0.4286, + "pv_estimate90": 1.7619, + "period_end": "2022-07-22T07:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.6896, + "pv_estimate10": 0.5904, + "pv_estimate90": 1.9707, + "period_end": "2022-07-22T07:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9098, + "pv_estimate10": 0.7387, + "pv_estimate90": 2.1499, + "period_end": "2022-07-22T08:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.0837, + "pv_estimate10": 0.864, + "pv_estimate90": 2.3044, + "period_end": "2022-07-22T08:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.1977, + "pv_estimate10": 1.0058, + "pv_estimate90": 2.408, + "period_end": "2022-07-22T09:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3147, + "pv_estimate10": 1.1181, + "pv_estimate90": 2.5101, + "period_end": "2022-07-22T09:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3448, + "pv_estimate10": 1.1903, + "pv_estimate90": 2.5683, + "period_end": "2022-07-22T10:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3972, + "pv_estimate10": 1.2428, + "pv_estimate90": 2.6017, + "period_end": "2022-07-22T10:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3859, + "pv_estimate10": 1.2758, + "pv_estimate90": 2.6074, + "period_end": "2022-07-22T11:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3501, + "pv_estimate10": 1.2875, + "pv_estimate90": 2.5663, + "period_end": "2022-07-22T11:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.2958, + "pv_estimate10": 1.2599, + "pv_estimate90": 2.5085, + "period_end": "2022-07-22T12:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.2433, + "pv_estimate10": 1.2452, + "pv_estimate90": 2.437, + "period_end": "2022-07-22T12:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.1632, + "pv_estimate10": 1.2148, + "pv_estimate90": 2.3408, + "period_end": "2022-07-22T13:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.0674, + "pv_estimate10": 1.1698, + "pv_estimate90": 2.2236, + "period_end": "2022-07-22T13:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9279, + "pv_estimate10": 1.0698, + "pv_estimate90": 2.0602, + "period_end": "2022-07-22T14:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.7788, + "pv_estimate10": 0.9934, + "pv_estimate90": 1.8858, + "period_end": "2022-07-22T14:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.617, + "pv_estimate10": 0.8803, + "pv_estimate90": 1.7043, + "period_end": "2022-07-22T15:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.4098, + "pv_estimate10": 0.7462, + "pv_estimate90": 1.4715, + "period_end": "2022-07-22T15:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.2308, + "pv_estimate10": 0.6129, + "pv_estimate90": 1.2703, + "period_end": "2022-07-22T16:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.009, + "pv_estimate10": 0.4701, + "pv_estimate90": 1.032, + "period_end": "2022-07-22T16:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.7849, + "pv_estimate10": 0.3106, + "pv_estimate90": 0.7979, + "period_end": "2022-07-22T17:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.5561, + "pv_estimate10": 0.1804, + "pv_estimate90": 0.5645, + "period_end": "2022-07-22T17:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.3568, + "pv_estimate10": 0.0847, + "pv_estimate90": 0.3891, + "period_end": "2022-07-22T18:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.1571, + "pv_estimate10": 0.0435, + "pv_estimate90": 0.1792, + "period_end": "2022-07-22T18:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0584, + "pv_estimate10": 0.016, + "pv_estimate90": 0.0691, + "period_end": "2022-07-22T19:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0046, + "pv_estimate10": 0, + "pv_estimate90": 0.0052, + "period_end": "2022-07-22T19:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-22T20:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-22T20:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-22T21:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-22T21:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-22T22:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-22T22:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-22T23:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-22T23:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-23T00:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-23T00:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-23T01:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-23T01:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-23T02:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-23T02:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-23T03:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-23T03:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-23T04:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0745, + "pv_estimate10": 0.0095, + "pv_estimate90": 0.1473, + "period_end": "2022-07-23T04:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.205, + "pv_estimate10": 0.0234, + "pv_estimate90": 0.4975, + "period_end": "2022-07-23T05:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.4228, + "pv_estimate10": 0.0421, + "pv_estimate90": 0.831, + "period_end": "2022-07-23T05:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.6554, + "pv_estimate10": 0.0671, + "pv_estimate90": 1.1687, + "period_end": "2022-07-23T06:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.9, + "pv_estimate10": 0.0995, + "pv_estimate90": 1.4997, + "period_end": "2022-07-23T06:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.1658, + "pv_estimate10": 0.1753, + "pv_estimate90": 1.7737, + "period_end": "2022-07-23T07:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.3921, + "pv_estimate10": 0.2519, + "pv_estimate90": 1.989, + "period_end": "2022-07-23T07:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.5882, + "pv_estimate10": 0.3003, + "pv_estimate90": 2.191, + "period_end": "2022-07-23T08:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.7774, + "pv_estimate10": 0.3709, + "pv_estimate90": 2.3467, + "period_end": "2022-07-23T08:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9103, + "pv_estimate10": 0.4432, + "pv_estimate90": 2.4766, + "period_end": "2022-07-23T09:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.0696, + "pv_estimate10": 0.5417, + "pv_estimate90": 2.5662, + "period_end": "2022-07-23T09:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.2036, + "pv_estimate10": 0.6428, + "pv_estimate90": 2.6162, + "period_end": "2022-07-23T10:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.2956, + "pv_estimate10": 0.742, + "pv_estimate90": 2.638, + "period_end": "2022-07-23T10:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3641, + "pv_estimate10": 0.8636, + "pv_estimate90": 2.6384, + "period_end": "2022-07-23T11:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3786, + "pv_estimate10": 0.9431, + "pv_estimate90": 2.6029, + "period_end": "2022-07-23T11:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3756, + "pv_estimate10": 1.0408, + "pv_estimate90": 2.5349, + "period_end": "2022-07-23T12:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3078, + "pv_estimate10": 1.083, + "pv_estimate90": 2.4555, + "period_end": "2022-07-23T12:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.2021, + "pv_estimate10": 1.02, + "pv_estimate90": 2.3478, + "period_end": "2022-07-23T13:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.0661, + "pv_estimate10": 0.9478, + "pv_estimate90": 2.2098, + "period_end": "2022-07-23T13:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9176, + "pv_estimate10": 0.8644, + "pv_estimate90": 2.0456, + "period_end": "2022-07-23T14:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.7544, + "pv_estimate10": 0.7708, + "pv_estimate90": 1.864, + "period_end": "2022-07-23T14:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.5797, + "pv_estimate10": 0.6449, + "pv_estimate90": 1.67, + "period_end": "2022-07-23T15:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.3794, + "pv_estimate10": 0.5578, + "pv_estimate90": 1.4415, + "period_end": "2022-07-23T15:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.1748, + "pv_estimate10": 0.4676, + "pv_estimate90": 1.2103, + "period_end": "2022-07-23T16:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.9969, + "pv_estimate10": 0.3775, + "pv_estimate90": 1.0136, + "period_end": "2022-07-23T16:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.7737, + "pv_estimate10": 0.267, + "pv_estimate90": 0.7737, + "period_end": "2022-07-23T17:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.5574, + "pv_estimate10": 0.1618, + "pv_estimate90": 0.5852700000000001, + "period_end": "2022-07-23T17:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.3731, + "pv_estimate10": 0.0868, + "pv_estimate90": 0.3801, + "period_end": "2022-07-23T18:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.1728, + "pv_estimate10": 0.0444, + "pv_estimate90": 0.1776, + "period_end": "2022-07-23T18:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0637, + "pv_estimate10": 0.0177, + "pv_estimate90": 0.067, + "period_end": "2022-07-23T19:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0045, + "pv_estimate10": 0.0022, + "pv_estimate90": 0.0051, + "period_end": "2022-07-23T19:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-23T20:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-23T20:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-23T21:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-23T21:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-23T22:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-23T22:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-23T23:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-23T23:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-24T00:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-24T00:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-24T01:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-24T01:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-24T02:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-24T02:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-24T03:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-24T03:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-24T04:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.1045, + "pv_estimate10": 0.0139, + "pv_estimate90": 0.1166, + "period_end": "2022-07-24T04:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.425, + "pv_estimate10": 0.037, + "pv_estimate90": 0.4824, + "period_end": "2022-07-24T05:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.7432, + "pv_estimate10": 0.0756, + "pv_estimate90": 0.8193, + "period_end": "2022-07-24T05:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.1067, + "pv_estimate10": 0.1335, + "pv_estimate90": 1.1952, + "period_end": "2022-07-24T06:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.4001, + "pv_estimate10": 0.2525, + "pv_estimate90": 1.4846, + "period_end": "2022-07-24T06:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.6791, + "pv_estimate10": 0.4098, + "pv_estimate90": 1.7452, + "period_end": "2022-07-24T07:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.8959, + "pv_estimate10": 0.5464, + "pv_estimate90": 1.9626, + "period_end": "2022-07-24T07:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.0809, + "pv_estimate10": 0.6923, + "pv_estimate90": 2.1505, + "period_end": "2022-07-24T08:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.2587, + "pv_estimate10": 0.794, + "pv_estimate90": 2.3058, + "period_end": "2022-07-24T08:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.402, + "pv_estimate10": 0.9349, + "pv_estimate90": 2.4313, + "period_end": "2022-07-24T09:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.4872, + "pv_estimate10": 1.0086, + "pv_estimate90": 2.5121, + "period_end": "2022-07-24T09:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.5515, + "pv_estimate10": 1.1335, + "pv_estimate90": 2.5799, + "period_end": "2022-07-24T10:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.5741, + "pv_estimate10": 1.1814, + "pv_estimate90": 2.6013, + "period_end": "2022-07-24T10:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.5783, + "pv_estimate10": 1.2452, + "pv_estimate90": 2.6042, + "period_end": "2022-07-24T11:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.5354, + "pv_estimate10": 1.261, + "pv_estimate90": 2.5604, + "period_end": "2022-07-24T11:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.4904, + "pv_estimate10": 1.2898, + "pv_estimate90": 2.5113, + "period_end": "2022-07-24T12:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.3935, + "pv_estimate10": 1.2728, + "pv_estimate90": 2.416, + "period_end": "2022-07-24T12:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.2599, + "pv_estimate10": 1.257, + "pv_estimate90": 2.2968, + "period_end": "2022-07-24T13:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 2.141, + "pv_estimate10": 1.1996, + "pv_estimate90": 2.1749, + "period_end": "2022-07-24T13:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.9726, + "pv_estimate10": 1.126, + "pv_estimate90": 2.0039, + "period_end": "2022-07-24T14:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.7724, + "pv_estimate10": 0.9872, + "pv_estimate90": 1.8281, + "period_end": "2022-07-24T14:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.578, + "pv_estimate10": 0.8206, + "pv_estimate90": 1.638, + "period_end": "2022-07-24T15:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.3572, + "pv_estimate10": 0.6504, + "pv_estimate90": 1.4139, + "period_end": "2022-07-24T15:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 1.1495, + "pv_estimate10": 0.4931, + "pv_estimate90": 1.1906, + "period_end": "2022-07-24T16:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.9617, + "pv_estimate10": 0.3544, + "pv_estimate90": 0.9953, + "period_end": "2022-07-24T16:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.7468, + "pv_estimate10": 0.2231, + "pv_estimate90": 0.7637, + "period_end": "2022-07-24T17:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.5141, + "pv_estimate10": 0.1067, + "pv_estimate90": 0.5419, + "period_end": "2022-07-24T17:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.3009, + "pv_estimate10": 0.0587, + "pv_estimate90": 0.331, + "period_end": "2022-07-24T18:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.1232, + "pv_estimate10": 0.0307, + "pv_estimate90": 0.174, + "period_end": "2022-07-24T18:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0467, + "pv_estimate10": 0.011, + "pv_estimate90": 0.0648, + "period_end": "2022-07-24T19:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0.0022, + "pv_estimate10": 0, + "pv_estimate90": 0.0028, + "period_end": "2022-07-24T19:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-24T20:00:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-24T20:30:00.0000000Z", + "period": "PT30M" + }, + { + "pv_estimate": 0, + "pv_estimate10": 0, + "pv_estimate90": 0, + "period_end": "2022-07-24T21:00:00.0000000Z", + "period": "PT30M" + } + ] +} \ No newline at end of file diff --git a/bundles/pom.xml b/bundles/pom.xml index 0fa1f20f9dd..0db4af22a90 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -363,6 +363,7 @@ org.openhab.binding.sncf org.openhab.binding.snmp org.openhab.binding.solaredge + org.openhab.binding.solarforecast org.openhab.binding.solarlog org.openhab.binding.solarmax org.openhab.binding.solarwatt