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 96a0264b85..3db66110ec 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 0000000000..38d625e349 --- /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 0000000000..1de5035db8 --- /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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 v=_=?5y7 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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�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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 0000000000..ecd0e93a85 --- /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 0000000000..9237c11c88 --- /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 0000000000..7b4ba46b50 --- /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 0000000000..06c8c856cf --- /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 0000000000..9597ab2fdb --- /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 0000000000..b6d37bb269 --- /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 0000000000..c794ebb1c3 --- /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 0000000000..0163cf2bb5 --- /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 0000000000..de15a50c06 --- /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 0000000000..89f1fd1208 --- /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 0000000000..ed2da9a384 --- /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 0000000000..487d93e86d --- /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 0000000000..a15f617fdc --- /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 0000000000..f55b807eb9 --- /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 0000000000..667c0e77ff --- /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 0000000000..248b718854 --- /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 0000000000..1a2d2c1de8 --- /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 0000000000..de5fe18eab --- /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 0000000000..89c46564cf --- /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 0000000000..44844a6db2 --- /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 0000000000..faaa013112 --- /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 0000000000..3e413fcb32 --- /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 0000000000..081ac44e09 --- /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 0000000000..d17426c82e --- /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 0000000000..956eec83c0 --- /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 0000000000..a2fe132236 --- /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 0000000000..8f00a6c3f5 --- /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 0000000000..b6302715b9 --- /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 0000000000..f37bd94d41 --- /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 0000000000..0e7c2b91f1 --- /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 0000000000..6ca53734dc --- /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 0000000000..e5c61debd8 --- /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 0000000000..3427c90e0a --- /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 0000000000..c549cc8fe1 --- /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 0000000000..aab7c4418d --- /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 0000000000..a47b9d9c38 --- /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 0000000000..1ae5126798 --- /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 0000000000..608bc02ebe --- /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 0000000000..a6606d54f3 --- /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 0000000000..44712a326f --- /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 0000000000..7b4edfef80 --- /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 0000000000..b1b48a9778 --- /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 0000000000..ccdb1a9c28 --- /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 0000000000..412612f378 --- /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 0000000000..83857b305c --- /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 0000000000..9fa129cc47 --- /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 0fa1f20f9d..0db4af22a9 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 -- 2.47.3