From 22615c6fdf9d6920626e198452b9fe76fb642a0f Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 19 Jul 2022 11:27:56 +0200 Subject: [PATCH 001/111] initial commit Signed-off-by: Bernd Weymann --- .../org.openhab.binding.solarforecast/NOTICE | 13 + .../README.md | 76 + .../org.openhab.binding.solarforecast/pom.xml | 25 + .../src/main/feature/feature.xml | 9 + .../internal/ForecastObject.java | 104 + .../SolarForecastBindingConstants.java | 47 + .../internal/SolarForecastBridgeHandler.java | 113 + .../internal/SolarForecastConfiguration.java | 31 + .../internal/SolarForecastHandlerFactory.java | 72 + .../internal/SolarForecastPlaneHandler.java | 128 + .../SolarForecastSinglePlaneHandler.java | 77 + .../main/resources/OH-INF/binding/binding.xml | 9 + .../resources/OH-INF/config/bridge-config.xml | 24 + .../OH-INF/config/thing-part-config.xml | 21 + .../OH-INF/config/thing-single-config.xml | 36 + .../OH-INF/i18n/solarforecast_xx.properties | 21 + .../OH-INF/thing/bridge-type-multi.xml | 20 + .../resources/OH-INF/thing/channel-types.xml | 32 + .../OH-INF/thing/thing-type-part.xml | 25 + .../OH-INF/thing/thing-type-single.xml | 22 + .../binding/solarforecast/FileReader.java | 48 + .../solarforecast/ForecastSolarTest.java | 113 + .../test/resources/forecastsolar/result.json | 100 + .../solarcast/estimated-actuals.json | 1684 ++++++++++++ .../test/resources/solarcast/forecasts.json | 2356 +++++++++++++++++ 25 files changed, 5206 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/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/ForecastObject.java 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/SolarForecastBridgeHandler.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastConfiguration.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/SolarForecastPlaneHandler.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastSinglePlaneHandler.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/bridge-config.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/thing-part-config.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/thing-single-config.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast_xx.properties create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/bridge-type-multi.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/thing-type-part.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/thing-type-single.xml 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/resources/forecastsolar/result.json create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/resources/solarcast/estimated-actuals.json create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/resources/solarcast/forecasts.json diff --git a/bundles/org.openhab.binding.solarforecast/NOTICE b/bundles/org.openhab.binding.solarforecast/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /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 0000000000000..5b2fca0c53655 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -0,0 +1,76 @@ +# SolarForecast Binding + +_Give some details about what this binding is meant for - a protocol, system, specific device._ + +_If possible, provide some resources like pictures (only PNG is supported currently), a video, etc. to give an impression of what can be done with this binding._ +_You can place such resources into a `doc` folder next to this README.md._ + +_Put each sentence in a separate line to improve readability of diffs._ + +## Supported Things + +_Please describe the different supported things / devices including their ThingTypeUID within this section._ +_Which different types are supported, which models were tested etc.?_ +_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._ + +- `bridge`: Short description of the Bridge, if any +- `sample`: Short description of the Thing with the ThingTypeUID `sample` + +## Discovery + +_Describe the available auto-discovery features here._ +_Mention for what it works and what needs to be kept in mind when using it._ + +## Binding Configuration + +_If your binding requires or supports general configuration settings, please create a folder ```cfg``` and place the configuration file ```.cfg``` inside it._ +_In this section, you should link to this file and provide some information about the options._ +_The file could e.g. look like:_ + +``` +# Configuration for the SolarForecast Binding +# +# Default secret key for the pairing of the SolarForecast Thing. +# It has to be between 10-40 (alphanumeric) characters. +# This may be changed by the user for security reasons. +secret=openHABSecret +``` + +_Note that it is planned to generate some part of this based on the information that is available within ```src/main/resources/OH-INF/binding``` of your binding._ + +_If your binding does not offer any generic configurations, you can remove this section completely._ + +## Thing Configuration + +_Describe what is needed to manually configure a thing, either through the UI or via a thing-file._ +_This should be mainly about its mandatory and optional configuration parameters._ + +_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._ + +### `sample` Thing Configuration + +| Name | Type | Description | Default | Required | Advanced | +|-----------------|---------|---------------------------------------|---------|----------|----------| +| hostname | text | Hostname or IP address of the device | N/A | yes | no | +| password | text | Password to access the device | N/A | yes | no | +| refreshInterval | integer | Interval the device is polled in sec. | 600 | no | yes | + +## Channels + +_Here you should provide information about available channel types, what their meaning is and how they can be used._ + +_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._ + +| Channel | Type | Read/Write | Description | +|---------|--------|------------|-----------------------------| +| control | Switch | RW | This is the control channel | + +## Full Example + +_Provide a full usage example based on textual configuration files._ +_*.things, *.items examples are mandatory as textual configuration is well used by many users._ +_*.sitemap examples are optional._ + +## Any custom content here! + +_Feel free to add additional sections for whatever you think should also be mentioned about your binding!_ diff --git a/bundles/org.openhab.binding.solarforecast/pom.xml b/bundles/org.openhab.binding.solarforecast/pom.xml new file mode 100644 index 0000000000000..ad01bddcd0be3 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/pom.xml @@ -0,0 +1,25 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.4.0-SNAPSHOT + + + org.openhab.binding.solarforecast + + openHAB Add-ons :: Bundles :: SolarForecast Binding + + + + org.json + json + 20220320 + + + + 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 0000000000000..9237c11c88db7 --- /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/ForecastObject.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/ForecastObject.java new file mode 100644 index 0000000000000..0d48bb6ffd031 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/ForecastObject.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2010-2022 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.time.LocalDateTime; +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.TreeMap; + +import javax.measure.quantity.Energy; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; + +/** + * The {@link ForecastObject} holds complete data for forecast + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class ForecastObject { + private final TreeMap dataMap = new TreeMap(); + private boolean valid = false; + private int constructionHour; + + public ForecastObject(String content, LocalDateTime now) { + constructionHour = now.getHour(); + JSONObject contentJson = new JSONObject(content); + JSONObject resultJson = contentJson.getJSONObject("result"); + JSONObject wattsJson = resultJson.getJSONObject("watt_hours"); + Iterator iter = wattsJson.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 + LocalDateTime ldt = LocalDateTime.parse(dateStr.replace(" ", "T")); + if (ldt.getDayOfMonth() == now.getDayOfMonth()) { + dataMap.put(ldt, wattsJson.getDouble(dateStr)); + } + } + valid = true; + } + + public ForecastObject() { + } + + public boolean isValid() { + return valid && constructionHour == LocalDateTime.now().getHour() && !dataMap.isEmpty(); + } + + public QuantityType getCurrentValue(LocalDateTime now) { + Entry f = dataMap.floorEntry(now); + Entry c = dataMap.ceilingEntry(now); + if (f != null) { + if (c != null) { + // we're during suntime! + double production = c.getValue() - f.getValue(); + int interpolation = now.getMinute() - f.getKey().getMinute(); + double interpolationProduction = production * interpolation / 60; + double actualProduction = f.getValue() + interpolationProduction; + return QuantityType.valueOf(Math.round(actualProduction) / 1000.0, Units.KILOWATT_HOUR); + } else { + // sun is down + return QuantityType.valueOf(Math.round(f.getValue()) / 1000.0, Units.KILOWATT_HOUR); + } + } else { + // no floor - sun not rised yet + return QuantityType.valueOf(0, Units.KILOWATT_HOUR); + } + } + + public QuantityType getDayTotal() { + if (dataMap.isEmpty()) { + return QuantityType.valueOf(0, Units.KILOWATT_HOUR); + } + return QuantityType.valueOf(Math.round(dataMap.lastEntry().getValue()) / 1000.0, Units.KILOWATT_HOUR); + } + + public QuantityType getRemainingProduction(LocalDateTime now) { + if (dataMap.isEmpty()) { + return QuantityType.valueOf(0, Units.KILOWATT_HOUR); + } + return QuantityType.valueOf( + Math.round(dataMap.lastEntry().getValue() - getCurrentValue(now).doubleValue()) / 1000.0, + Units.KILOWATT_HOUR); + } + + @Override + public String toString() { + return "Hour: " + constructionHour + ", Valid: " + valid + ", Data:" + dataMap; + } +} 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 0000000000000..663b4a51c99e5 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastBindingConstants.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2022 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"; + + public static final ThingTypeUID FORECAST_SOLAR_SINGLE_STRING = new ThingTypeUID(BINDING_ID, "single"); + public static final ThingTypeUID FORECAST_SOLAR_MULTI_STRING = new ThingTypeUID(BINDING_ID, "multi"); + public static final ThingTypeUID FORECAST_SOLAR_PART_STRING = new ThingTypeUID(BINDING_ID, "part"); + public static final Set SUPPORTED_THING_SET = Set.of(FORECAST_SOLAR_SINGLE_STRING, + FORECAST_SOLAR_MULTI_STRING, FORECAST_SOLAR_PART_STRING); + + public static final String CHANNEL_TODAY = "today"; + public static final String CHANNEL_ACTUAL = "actual"; + public static final String CHANNEL_REMAINING = "remaining"; + public static final String CHANNEL_TOMORROW = "tomorrow"; + public static final String CHANNEL_RAW = "raw"; + + public static final String BASE_URL = "https://api.forecast.solar/estimate/"; + + public static final String SLASH = "/"; + public static final String EMPTY = ""; +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastBridgeHandler.java new file mode 100644 index 0000000000000..4a0f869ed76c1 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastBridgeHandler.java @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2010-2022 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.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import javax.measure.quantity.Energy; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +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.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SolarForecastBridgeHandler} is a non active handler instance. It will be triggerer by the bridge. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class SolarForecastBridgeHandler extends BaseBridgeHandler { + + private final Logger logger = LoggerFactory.getLogger(SolarForecastBridgeHandler.class); + + private List parts = new ArrayList(); + private Optional> refreshJob = Optional.empty(); + private @Nullable SolarForecastConfiguration config; + + public SolarForecastBridgeHandler(Bridge bridge, HttpClient httpClient, PointType location) { + super(bridge); + } + + @Override + public void initialize() { + config = getConfigAs(SolarForecastConfiguration.class); + startSchedule(config.refreshInterval); + updateStatus(ThingStatus.ONLINE); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } + + private void startSchedule(int interval) { + refreshJob.ifPresentOrElse(job -> { + if (job.isCancelled()) { + refreshJob = Optional + .of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES)); + } // else - scheduler is already running! + }, () -> { + refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES)); + }); + } + + private void getData() { + LocalDateTime now = LocalDateTime.now(); + QuantityType today = QuantityType.valueOf(0, Units.KILOWATT_HOUR); + QuantityType actual = QuantityType.valueOf(0, Units.KILOWATT_HOUR); + QuantityType remain = QuantityType.valueOf(0, Units.KILOWATT_HOUR); + for (Iterator iterator = parts.iterator(); iterator.hasNext();) { + ForecastObject fo = iterator.next().fetchData(); + if (fo.isValid()) { + today = today.add(fo.getDayTotal()); + actual = actual.add(fo.getCurrentValue(now)); + remain = remain.add(fo.getRemainingProduction(now)); + } else { + logger.info("Fetched data not valid {}", fo.toString()); + } + } + updateState(CHANNEL_TODAY, today); + updateState(CHANNEL_REMAINING, remain); + updateState(CHANNEL_TODAY, today); + } + + @Override + public void dispose() { + refreshJob.ifPresent(job -> job.cancel(true)); + } + + synchronized void addPlane(SolarForecastPlaneHandler sfph) { + parts.add(sfph); + } + + synchronized void removePlane(SolarForecastPlaneHandler sfph) { + parts.remove(sfph); + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastConfiguration.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastConfiguration.java new file mode 100644 index 0000000000000..a645acc08d9de --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastConfiguration.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2022 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; + +/** + * The {@link SolarForecastConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class SolarForecastConfiguration { + + public String location = "0.0,0.0"; + public int declination = -1; + public int azimuth = 360; + public int kwp = 0; + public int refreshInterval = -1; + public String apiKey = SolarForecastBindingConstants.EMPTY; +} 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 0000000000000..badfa2ccd15e9 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastHandlerFactory.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2010-2022 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 org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.core.i18n.LocationProvider; +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 HttpClient httpClient; + private final PointType location; + + @Activate + public SolarForecastHandlerFactory(final @Reference HttpClientFactory hcf, final @Reference LocationProvider lp) { + httpClient = hcf.getCommonHttpClient(); + PointType pt = lp.getLocation(); + if (pt != null) { + location = pt; + } else { + location = PointType.valueOf("0.0,0.0"); + } + } + + @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_MULTI_STRING.equals(thingTypeUID)) { + return new SolarForecastBridgeHandler((Bridge) thing, httpClient, location); + } else if (FORECAST_SOLAR_PART_STRING.equals(thingTypeUID)) { + return new SolarForecastPlaneHandler(thing, httpClient, location); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastPlaneHandler.java new file mode 100644 index 0000000000000..db0bf0b1ba06e --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastPlaneHandler.java @@ -0,0 +1,128 @@ +/** + * Copyright (c) 2010-2022 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.time.LocalDateTime; +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.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.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SolarForecastPlaneHandler} is a non active handler instance. It will be triggerer by the bridge. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class SolarForecastPlaneHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(SolarForecastPlaneHandler.class); + private final HttpClient httpClient; + private final PointType location; + private ForecastObject forecast = new ForecastObject(); + private Optional config = Optional.empty(); + + public SolarForecastPlaneHandler(Thing thing, HttpClient hc, PointType loc) { + super(thing); + httpClient = hc; + location = loc; + } + + @Override + public void initialize() { + SolarForecastConfiguration c = getConfigAs(SolarForecastConfiguration.class); + config = Optional.of(c); + Bridge bridge = getBridge(); + if (bridge != null) { + BridgeHandler handler = bridge.getHandler(); + if (handler != null) { + if (handler instanceof SolarForecastBridgeHandler) { + ((SolarForecastBridgeHandler) handler).addPlane(this); + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Wrong Handler " + handler); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, "BridgeHandler not found"); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, "Bridge not set"); + } + } + + @Override + public void dispose() { + super.dispose(); + if (thing instanceof SolarForecastBridgeHandler) { + ((SolarForecastBridgeHandler) thing).removePlane(this); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + // TODO Auto-generated method stub + } + + /** + * https://doc.forecast.solar/doku.php?id=api:estimate + */ + ForecastObject fetchData() { + if (!forecast.isValid()) { + // https://api.forecast.solar/estimate/:lat/:lon/:dec/:az/:kwp + String url = BASE_URL + location.getLatitude() + SLASH + location.getLongitude() + SLASH + + config.get().declination + SLASH + config.get().azimuth + SLASH + config.get().kwp; + logger.info("Call {}", url); + try { + ContentResponse cr = httpClient.GET(url); + if (cr.getStatus() == 200) { + forecast = new ForecastObject(cr.getContentAsString(), LocalDateTime.now()); + logger.info("Fetched data {}", forecast.toString()); + updateChannels(forecast); + updateState(new ChannelUID(thing.getUID(), CHANNEL_RAW), + StringType.valueOf(cr.getContentAsString())); + } + } catch (InterruptedException | ExecutionException | TimeoutException e) { + logger.info("Call {} failed {}", url, e.getMessage()); + } + return new ForecastObject(); + } + // return old forecast - forecast object will interpolate values + return forecast; + } + + private void updateChannels(ForecastObject f) { + updateState(new ChannelUID(thing.getUID(), SolarForecastBindingConstants.CHANNEL_ACTUAL), + f.getCurrentValue(LocalDateTime.now())); + updateState(new ChannelUID(thing.getUID(), SolarForecastBindingConstants.CHANNEL_REMAINING), + f.getRemainingProduction(LocalDateTime.now())); + updateState(new ChannelUID(thing.getUID(), SolarForecastBindingConstants.CHANNEL_TODAY), f.getDayTotal()); + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastSinglePlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastSinglePlaneHandler.java new file mode 100644 index 0000000000000..eb2e861301a04 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastSinglePlaneHandler.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2010-2022 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.Optional; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.core.library.types.PointType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SolarForecastBridgeHandler} is a non active handler instance. It will be triggerer by the bridge. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class SolarForecastSinglePlaneHandler extends SolarForecastPlaneHandler { + + private final Logger logger = LoggerFactory.getLogger(SolarForecastSinglePlaneHandler.class); + + private Optional> refreshJob = Optional.empty(); + private @Nullable SolarForecastConfiguration config; + + public SolarForecastSinglePlaneHandler(Thing thing, HttpClient httpClient, PointType location) { + super(thing, httpClient, location); + } + + @Override + public void initialize() { + config = getConfigAs(SolarForecastConfiguration.class); + startSchedule(config.refreshInterval); + updateStatus(ThingStatus.ONLINE); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } + + private void startSchedule(int interval) { + refreshJob.ifPresentOrElse(job -> { + if (job.isCancelled()) { + refreshJob = Optional + .of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES)); + } // else - scheduler is already running! + }, () -> { + refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES)); + }); + } + + private void getData() { + super.fetchData(); + } + + @Override + public void dispose() { + refreshJob.ifPresent(job -> job.cancel(true)); + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..57efb9f07be2c --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,9 @@ + + + + SolarForecast Binding + Solar Forecast for your location + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/bridge-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/bridge-config.xml new file mode 100644 index 0000000000000..4761f3bb519e0 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/bridge-config.xml @@ -0,0 +1,24 @@ + + + + + + location + + Location of Photovoltaic system + AUTODETECT + + + + Data refresh rate of forecast data + 1 + + + + If you have a paid subscription plan + + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/thing-part-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/thing-part-config.xml new file mode 100644 index 0000000000000..188c9a1e7ae09 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/thing-part-config.xml @@ -0,0 +1,21 @@ + + + + + + + 0 for horizontal till 90 for vertical declination + + + + -180 = north, -90 = east, 0 = south, 90 = west, 180 = north + + + + Installed Module power of this plane + + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/thing-single-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/thing-single-config.xml new file mode 100644 index 0000000000000..50e825f88b2de --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/thing-single-config.xml @@ -0,0 +1,36 @@ + + + + + + location + + Location of Photovoltaic system + AUTODETECT + + + + 0 for horizontal till 90 for vertical declination + + + + -180 = north, -90 = east, 0 = south, 90 = west, 180 = north + + + + Installed Module power of this plane + + + + Data refresh rate of forecast data + 1 + + + + If you have a paid subscription plan + + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast_xx.properties b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast_xx.properties new file mode 100644 index 0000000000000..15492a5f0cd07 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast_xx.properties @@ -0,0 +1,21 @@ +# FIXME: please substitute the xx with a proper locale, ie. de +# FIXME: please do not add the file to the repo if you add or change no content +# binding +binding.solarforecast.name = +binding.solarforecast.description = + +# thing types +thing-type.solarforecast.sample.label = +thing-type.solarforecast.sample.description = + +# thing type config description +thing-type.config.solarforecast.sample.hostname.label = +thing-type.config.solarforecast.sample.hostname.description = +thing-type.config.solarforecast.sample.password.label = +thing-type.config.solarforecast.sample.password.description = +thing-type.config.solarforecast.sample.refreshInterval.label = +thing-type.config.solarforecast.sample.refreshInterval.description = + +# channel types +channel-type.solarforecast.sample-channel.label = +channel-type.solarforecast.sample-channel.description = diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/bridge-type-multi.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/bridge-type-multi.xml new file mode 100644 index 0000000000000..64a9ef08329ed --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/bridge-type-multi.xml @@ -0,0 +1,20 @@ + + + + + + Bridge holding multiple photovoltaic planes + + + + + + + + + + + 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 0000000000000..77f1fd8237fa3 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/channel-types.xml @@ -0,0 +1,32 @@ + + + + + Number:Energy + + Todays forecast in total + + + Number:Energy + + Forecast of todays remaining production + + + Number:Energy + + Forecast till now (actual time) + + + Number:Energy + + Tomorrows forecast in total + + + String + + Plain JSON response without conversions + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/thing-type-part.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/thing-type-part.xml new file mode 100644 index 0000000000000..7f93bfb6fc29e --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/thing-type-part.xml @@ -0,0 +1,25 @@ + + + + + + + + + + PV Plane as part of Multi Plane Bridge + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/thing-type-single.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/thing-type-single.xml new file mode 100644 index 0000000000000..348dc260e376d --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/thing-type-single.xml @@ -0,0 +1,22 @@ + + + + + + + One PV plane attached to your inverter + + + + + + + + + + + + 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 0000000000000..fefde673c218d --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/FileReader.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2022 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.assertEquals; + +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 + assertEquals(filename, SolarForecastBindingConstants.EMPTY, "Read failute " + 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 0000000000000..b42144f82720f --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/ForecastSolarTest.java @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2010-2022 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.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.TreeMap; + +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.ForecastObject; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; + +/** + * The {@link ForecastSolarTest} tests responses from forecast solar website + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +class ForecastSolarTest { + public static final String DATE_INPUT_PATTERN_STRING = "yyyy-MM-dd'T'HH:mm:ss"; + public static final DateTimeFormatter DATE_INPUT_PATTERN = DateTimeFormatter.ofPattern(DATE_INPUT_PATTERN_STRING); + + @Test + void test() { + System.out.println("Test"); + String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); + JSONObject contentJson = new JSONObject(content); + JSONObject resultJson = contentJson.getJSONObject("result"); + JSONObject wattsJson = resultJson.getJSONObject("watt_hours"); + Iterator iter = wattsJson.keys(); + TreeMap m = new TreeMap(); + while (iter.hasNext()) { + String dateStr = iter.next(); + LocalDateTime ldt = LocalDateTime.parse(dateStr.replace(" ", "T")); + if (ldt.getDayOfMonth() == 17) { + m.put(ldt, wattsJson.getInt(dateStr)); + } + // Date d = new Date(dateStr.replace(" ", "T")); + } + System.out.println(m); + // LocalDateTime now = LocalDateTime.now(); + LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 23); + System.out.println(now); + Entry f = m.floorEntry(now); + System.out.println(f); + Entry c = m.ceilingEntry(now); + System.out.println(c); + if (f != null) { + if (c != null) { + // we're during suntime! + System.out.println("Floor " + f + " Ceiling " + c); + int production = c.getValue() - f.getValue(); + int interpolation = now.getMinute() - f.getKey().getMinute(); + int interpolationProduction = production * interpolation / 60; + System.out + .println("Minutes to interpolate " + interpolation + " Production " + interpolationProduction); + } + } + } + + @Test + void testForecastObject() { + System.out.println("Test FO"); + String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); + LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 23); + ForecastObject fo = new ForecastObject(content, now); + System.out.println(fo.getCurrentValue(now)); + } + + @Test + void testForecastSum() { + System.out.println("Test FO"); + String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); + LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 23); + ForecastObject fo = new ForecastObject(content, now); + System.out.println(fo.getCurrentValue(now)); + QuantityType actual = QuantityType.valueOf(0, Units.KILOWATT_HOUR); + System.out.println(actual + " / " + fo.getCurrentValue(now)); + actual = actual.add(fo.getCurrentValue(now)); + System.out.println(actual + " / " + fo.getCurrentValue(now)); + actual = actual.add(fo.getCurrentValue(now)); + System.out.println(actual + " / " + fo.getCurrentValue(now)); + } + + @Test + void testErrorCases() { + ForecastObject fo = new ForecastObject(); + assertFalse(fo.isValid()); + LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 23); + fo.getCurrentValue(now); + fo.getDayTotal(); + fo.getRemainingProduction(now); + } +} 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 0000000000000..8f4ab6b4dc8c0 --- /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": 50.556, + "longitude": 8.4956, + "place": "35585 Wetzlar, Lahn-Dill-Kreis, Hessen, DE", + "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/solarcast/estimated-actuals.json b/bundles/org.openhab.binding.solarforecast/src/test/resources/solarcast/estimated-actuals.json new file mode 100644 index 0000000000000..83857b305cef6 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/resources/solarcast/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/solarcast/forecasts.json b/bundles/org.openhab.binding.solarforecast/src/test/resources/solarcast/forecasts.json new file mode 100644 index 0000000000000..9fa129cc4771a --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/resources/solarcast/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 From 130eab0fe1fd81098b400cd77d6f815a2dee9791 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 19 Jul 2022 22:06:28 +0200 Subject: [PATCH 002/111] redesign forecast and location bugfix Signed-off-by: Bernd Weymann --- .../internal/ForecastObject.java | 84 +++++++++++------- .../SolarForecastBindingConstants.java | 1 + .../internal/SolarForecastBridgeHandler.java | 46 ++++++---- .../internal/SolarForecastHandlerFactory.java | 6 +- .../internal/SolarForecastPlaneHandler.java | 31 ++++--- .../SolarForecastSinglePlaneHandler.java | 19 ++-- .../resources/OH-INF/thing/channel-types.xml | 4 + .../solarforecast/ForecastSolarTest.java | 86 ++++++++----------- 8 files changed, 160 insertions(+), 117 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/ForecastObject.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/ForecastObject.java index 0d48bb6ffd031..02147052bde72 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/ForecastObject.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/ForecastObject.java @@ -12,17 +12,19 @@ */ package org.openhab.binding.solarforecast.internal; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.Iterator; import java.util.Map.Entry; +import java.util.Optional; import java.util.TreeMap; -import javax.measure.quantity.Energy; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.json.JSONObject; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.Units; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; /** * The {@link ForecastObject} holds complete data for forecast @@ -31,36 +33,44 @@ */ @NonNullByDefault public class ForecastObject { + private static final double UNDEF = -1; private final TreeMap dataMap = new TreeMap(); + private Optional rawData = Optional.empty(); private boolean valid = false; private int constructionHour; + public ForecastObject() { + } + public ForecastObject(String content, LocalDateTime now) { constructionHour = now.getHour(); - JSONObject contentJson = new JSONObject(content); - JSONObject resultJson = contentJson.getJSONObject("result"); - JSONObject wattsJson = resultJson.getJSONObject("watt_hours"); - Iterator iter = wattsJson.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 - LocalDateTime ldt = LocalDateTime.parse(dateStr.replace(" ", "T")); - if (ldt.getDayOfMonth() == now.getDayOfMonth()) { - dataMap.put(ldt, wattsJson.getDouble(dateStr)); + if (!content.equals(SolarForecastBindingConstants.EMPTY)) { + rawData = Optional.of(content); + JSONObject contentJson = new JSONObject(content); + JSONObject resultJson = contentJson.getJSONObject("result"); + JSONObject wattsJson = resultJson.getJSONObject("watt_hours"); + Iterator iter = wattsJson.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 + LocalDateTime ldt = LocalDateTime.parse(dateStr.replace(" ", "T")); + if (ldt.getDayOfMonth() == now.getDayOfMonth()) { + dataMap.put(ldt, wattsJson.getDouble(dateStr)); + } } + valid = true; } - valid = true; - } - - public ForecastObject() { } public boolean isValid() { return valid && constructionHour == LocalDateTime.now().getHour() && !dataMap.isEmpty(); } - public QuantityType getCurrentValue(LocalDateTime now) { + public double getActualValue(LocalDateTime now) { + if (dataMap.isEmpty()) { + return UNDEF; + } Entry f = dataMap.floorEntry(now); Entry c = dataMap.ceilingEntry(now); if (f != null) { @@ -70,31 +80,45 @@ public QuantityType getCurrentValue(LocalDateTime now) { int interpolation = now.getMinute() - f.getKey().getMinute(); double interpolationProduction = production * interpolation / 60; double actualProduction = f.getValue() + interpolationProduction; - return QuantityType.valueOf(Math.round(actualProduction) / 1000.0, Units.KILOWATT_HOUR); + return Math.round(actualProduction) / 1000.0; } else { // sun is down - return QuantityType.valueOf(Math.round(f.getValue()) / 1000.0, Units.KILOWATT_HOUR); + return Math.round(f.getValue()) / 1000.0; } } else { // no floor - sun not rised yet - return QuantityType.valueOf(0, Units.KILOWATT_HOUR); + return 0; } } - public QuantityType getDayTotal() { - if (dataMap.isEmpty()) { - return QuantityType.valueOf(0, Units.KILOWATT_HOUR); + public double getDayTotal(LocalDateTime now, int offset) { + if (rawData.isEmpty()) { + return -1; + } + LocalDate ld = now.plusDays(offset).toLocalDate(); + JSONObject contentJson = new JSONObject(rawData.get()); + JSONObject resultJson = contentJson.getJSONObject("result"); + JSONObject wattsDay = resultJson.getJSONObject("watt_hours_day"); + + if (wattsDay.has(ld.toString())) { + return Math.round(wattsDay.getDouble(ld.toString())) / 1000.0; } - return QuantityType.valueOf(Math.round(dataMap.lastEntry().getValue()) / 1000.0, Units.KILOWATT_HOUR); + return UNDEF; } - public QuantityType getRemainingProduction(LocalDateTime now) { + public double getRemainingProduction(LocalDateTime now) { if (dataMap.isEmpty()) { - return QuantityType.valueOf(0, Units.KILOWATT_HOUR); + return UNDEF; + } + return getDayTotal(now, 0) - getActualValue(now); + } + + public static State getStateObject(double d) { + if (d < 0) { + return UnDefType.UNDEF; + } else { + return QuantityType.valueOf(d, Units.KILOWATT_HOUR); } - return QuantityType.valueOf( - Math.round(dataMap.lastEntry().getValue() - getCurrentValue(now).doubleValue()) / 1000.0, - Units.KILOWATT_HOUR); } @Override 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 index 663b4a51c99e5..8feef9dfcce82 100644 --- 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 @@ -42,6 +42,7 @@ public class SolarForecastBindingConstants { public static final String BASE_URL = "https://api.forecast.solar/estimate/"; + public static final String AUTODETECT = "AUTODETECT"; public static final String SLASH = "/"; public static final String EMPTY = ""; } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastBridgeHandler.java index 4a0f869ed76c1..b85c472e1ced2 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastBridgeHandler.java @@ -22,14 +22,10 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import javax.measure.quantity.Energy; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.jetty.client.HttpClient; +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.library.unit.Units; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ThingStatus; @@ -47,19 +43,27 @@ public class SolarForecastBridgeHandler extends BaseBridgeHandler { private final Logger logger = LoggerFactory.getLogger(SolarForecastBridgeHandler.class); + private final PointType homeLocation; private List parts = new ArrayList(); private Optional> refreshJob = Optional.empty(); private @Nullable SolarForecastConfiguration config; - public SolarForecastBridgeHandler(Bridge bridge, HttpClient httpClient, PointType location) { + public SolarForecastBridgeHandler(Bridge bridge, PointType location) { super(bridge); + homeLocation = location; } @Override public void initialize() { config = getConfigAs(SolarForecastConfiguration.class); startSchedule(config.refreshInterval); + if (config.location.equals(SolarForecastBindingConstants.AUTODETECT)) { + Configuration editConfig = editConfiguration(); + editConfig.put("location", homeLocation.toString()); + updateConfiguration(editConfig); + config = getConfigAs(SolarForecastConfiguration.class); + } updateStatus(ThingStatus.ONLINE); } @@ -78,24 +82,30 @@ private void startSchedule(int interval) { }); } - private void getData() { + /** + * Get data for all planes. Protect parts map from being modified during update + */ + private synchronized void getData() { LocalDateTime now = LocalDateTime.now(); - QuantityType today = QuantityType.valueOf(0, Units.KILOWATT_HOUR); - QuantityType actual = QuantityType.valueOf(0, Units.KILOWATT_HOUR); - QuantityType remain = QuantityType.valueOf(0, Units.KILOWATT_HOUR); + double todaySum = 0; + double tomorrowSum = 0; + double actualSum = 0; + double remainSum = 0; for (Iterator iterator = parts.iterator(); iterator.hasNext();) { ForecastObject fo = iterator.next().fetchData(); if (fo.isValid()) { - today = today.add(fo.getDayTotal()); - actual = actual.add(fo.getCurrentValue(now)); - remain = remain.add(fo.getRemainingProduction(now)); + todaySum += fo.getDayTotal(now, 0); + tomorrowSum = fo.getDayTotal(now, 1); + actualSum = fo.getActualValue(now); + remainSum = fo.getRemainingProduction(now); } else { logger.info("Fetched data not valid {}", fo.toString()); } } - updateState(CHANNEL_TODAY, today); - updateState(CHANNEL_REMAINING, remain); - updateState(CHANNEL_TODAY, today); + updateState(CHANNEL_TODAY, ForecastObject.getStateObject(todaySum)); + updateState(CHANNEL_TOMORROW, ForecastObject.getStateObject(tomorrowSum)); + updateState(CHANNEL_REMAINING, ForecastObject.getStateObject(remainSum)); + updateState(CHANNEL_ACTUAL, ForecastObject.getStateObject(actualSum)); } @Override @@ -110,4 +120,8 @@ synchronized void addPlane(SolarForecastPlaneHandler sfph) { synchronized void removePlane(SolarForecastPlaneHandler sfph) { parts.remove(sfph); } + + public PointType getLocation() { + return PointType.valueOf(config.location); + } } 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 index badfa2ccd15e9..e55b1f3f79fb2 100644 --- 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 @@ -63,9 +63,11 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { protected @Nullable ThingHandler createHandler(Thing thing) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (FORECAST_SOLAR_MULTI_STRING.equals(thingTypeUID)) { - return new SolarForecastBridgeHandler((Bridge) thing, httpClient, location); + return new SolarForecastBridgeHandler((Bridge) thing, location); } else if (FORECAST_SOLAR_PART_STRING.equals(thingTypeUID)) { - return new SolarForecastPlaneHandler(thing, httpClient, location); + return new SolarForecastPlaneHandler(thing, httpClient); + } else if (FORECAST_SOLAR_SINGLE_STRING.equals(thingTypeUID)) { + return new SolarForecastSinglePlaneHandler(thing, httpClient, location); } return null; } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastPlaneHandler.java index db0bf0b1ba06e..b386081b856fc 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastPlaneHandler.java @@ -32,6 +32,7 @@ import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.thing.binding.BridgeHandler; import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,14 +46,13 @@ public class SolarForecastPlaneHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(SolarForecastPlaneHandler.class); private final HttpClient httpClient; - private final PointType location; private ForecastObject forecast = new ForecastObject(); private Optional config = Optional.empty(); + private Optional location = Optional.empty(); - public SolarForecastPlaneHandler(Thing thing, HttpClient hc, PointType loc) { + public SolarForecastPlaneHandler(Thing thing, HttpClient hc) { super(thing); httpClient = hc; - location = loc; } @Override @@ -65,6 +65,7 @@ public void initialize() { if (handler != null) { if (handler instanceof SolarForecastBridgeHandler) { ((SolarForecastBridgeHandler) handler).addPlane(this); + location = Optional.of(((SolarForecastBridgeHandler) handler).getLocation()); updateStatus(ThingStatus.ONLINE); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, @@ -88,16 +89,18 @@ public void dispose() { @Override public void handleCommand(ChannelUID channelUID, Command command) { - // TODO Auto-generated method stub + if (command instanceof RefreshType) { + fetchData(); + } } /** * https://doc.forecast.solar/doku.php?id=api:estimate */ ForecastObject fetchData() { - if (!forecast.isValid()) { + if (!forecast.isValid() && location.isPresent()) { // https://api.forecast.solar/estimate/:lat/:lon/:dec/:az/:kwp - String url = BASE_URL + location.getLatitude() + SLASH + location.getLongitude() + SLASH + String url = BASE_URL + location.get().getLatitude() + SLASH + location.get().getLongitude() + SLASH + config.get().declination + SLASH + config.get().azimuth + SLASH + config.get().kwp; logger.info("Call {}", url); try { @@ -112,17 +115,23 @@ ForecastObject fetchData() { } catch (InterruptedException | ExecutionException | TimeoutException e) { logger.info("Call {} failed {}", url, e.getMessage()); } - return new ForecastObject(); } - // return old forecast - forecast object will interpolate values + updateChannels(forecast); return forecast; } private void updateChannels(ForecastObject f) { updateState(new ChannelUID(thing.getUID(), SolarForecastBindingConstants.CHANNEL_ACTUAL), - f.getCurrentValue(LocalDateTime.now())); + ForecastObject.getStateObject(f.getActualValue(LocalDateTime.now()))); updateState(new ChannelUID(thing.getUID(), SolarForecastBindingConstants.CHANNEL_REMAINING), - f.getRemainingProduction(LocalDateTime.now())); - updateState(new ChannelUID(thing.getUID(), SolarForecastBindingConstants.CHANNEL_TODAY), f.getDayTotal()); + ForecastObject.getStateObject(f.getRemainingProduction(LocalDateTime.now()))); + updateState(new ChannelUID(thing.getUID(), SolarForecastBindingConstants.CHANNEL_TODAY), + ForecastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 0))); + updateState(new ChannelUID(thing.getUID(), SolarForecastBindingConstants.CHANNEL_TOMORROW), + ForecastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 1))); + } + + protected void setLocation(PointType loc) { + location = Optional.of(loc); } } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastSinglePlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastSinglePlaneHandler.java index eb2e861301a04..d6d274a46ae7c 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastSinglePlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastSinglePlaneHandler.java @@ -19,13 +19,12 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; +import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.PointType; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.types.Command; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link SolarForecastBridgeHandler} is a non active handler instance. It will be triggerer by the bridge. @@ -34,20 +33,28 @@ */ @NonNullByDefault public class SolarForecastSinglePlaneHandler extends SolarForecastPlaneHandler { - - private final Logger logger = LoggerFactory.getLogger(SolarForecastSinglePlaneHandler.class); - private Optional> refreshJob = Optional.empty(); + private PointType homeLocation; private @Nullable SolarForecastConfiguration config; public SolarForecastSinglePlaneHandler(Thing thing, HttpClient httpClient, PointType location) { - super(thing, httpClient, location); + super(thing, httpClient); + homeLocation = location; } @Override public void initialize() { config = getConfigAs(SolarForecastConfiguration.class); startSchedule(config.refreshInterval); + if (config.location.equals(SolarForecastBindingConstants.AUTODETECT)) { + Configuration editConfig = editConfiguration(); + editConfig.put("location", homeLocation.toString()); + updateConfiguration(editConfig); + super.setLocation(homeLocation); + config = getConfigAs(SolarForecastConfiguration.class); + } else { + super.setLocation(PointType.valueOf(config.location)); + } updateStatus(ThingStatus.ONLINE); } 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 index 77f1fd8237fa3..f020d6a448ca0 100644 --- 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 @@ -8,21 +8,25 @@ Number:Energy Todays forecast in total + Number:Energy Forecast of todays remaining production + Number:Energy Forecast till now (actual time) + Number:Energy Tomorrows forecast in total + String 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 index b42144f82720f..e0fa761d8209c 100644 --- 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 @@ -12,22 +12,20 @@ */ package org.openhab.binding.solarforecast; -import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.*; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import java.util.Iterator; -import java.util.Map.Entry; -import java.util.TreeMap; 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.ForecastObject; +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.types.State; /** * The {@link ForecastSolarTest} tests responses from forecast solar website @@ -40,65 +38,43 @@ class ForecastSolarTest { public static final DateTimeFormatter DATE_INPUT_PATTERN = DateTimeFormatter.ofPattern(DATE_INPUT_PATTERN_STRING); @Test - void test() { - System.out.println("Test"); + void testForecastObject() { String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); - JSONObject contentJson = new JSONObject(content); - JSONObject resultJson = contentJson.getJSONObject("result"); - JSONObject wattsJson = resultJson.getJSONObject("watt_hours"); - Iterator iter = wattsJson.keys(); - TreeMap m = new TreeMap(); - while (iter.hasNext()) { - String dateStr = iter.next(); - LocalDateTime ldt = LocalDateTime.parse(dateStr.replace(" ", "T")); - if (ldt.getDayOfMonth() == 17) { - m.put(ldt, wattsJson.getInt(dateStr)); - } - // Date d = new Date(dateStr.replace(" ", "T")); - } - System.out.println(m); - // LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 23); - System.out.println(now); - Entry f = m.floorEntry(now); - System.out.println(f); - Entry c = m.ceilingEntry(now); - System.out.println(c); - if (f != null) { - if (c != null) { - // we're during suntime! - System.out.println("Floor " + f + " Ceiling " + c); - int production = c.getValue() - f.getValue(); - int interpolation = now.getMinute() - f.getKey().getMinute(); - int interpolationProduction = production * interpolation / 60; - System.out - .println("Minutes to interpolate " + interpolation + " Production " + interpolationProduction); - } - } + ForecastObject fo = new ForecastObject(content, now); + assertEquals(49.431, fo.getActualValue(now), 0.001, "Current Production"); + assertEquals(14.152, fo.getRemainingProduction(now), 0.001, "Current Production"); + assertEquals(fo.getDayTotal(now, 0), fo.getActualValue(now) + fo.getRemainingProduction(now), 0.001, + "Total production"); + assertEquals(fo.getDayTotal(now, 0), fo.getActualValue(now) + fo.getRemainingProduction(now), 0.001, + "Total production"); } @Test - void testForecastObject() { - System.out.println("Test FO"); + void testInterpolation() { String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); - LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 23); + LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 0); ForecastObject fo = new ForecastObject(content, now); - System.out.println(fo.getCurrentValue(now)); + double previousValue = 0; + for (int i = 0; i < 60; i++) { + now = now.plusMinutes(1); + assertTrue(previousValue < fo.getActualValue(now)); + previousValue = fo.getActualValue(now); + } } @Test void testForecastSum() { - System.out.println("Test FO"); String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 23); ForecastObject fo = new ForecastObject(content, now); - System.out.println(fo.getCurrentValue(now)); QuantityType actual = QuantityType.valueOf(0, Units.KILOWATT_HOUR); - System.out.println(actual + " / " + fo.getCurrentValue(now)); - actual = actual.add(fo.getCurrentValue(now)); - System.out.println(actual + " / " + fo.getCurrentValue(now)); - actual = actual.add(fo.getCurrentValue(now)); - System.out.println(actual + " / " + fo.getCurrentValue(now)); + State st = ForecastObject.getStateObject(fo.getActualValue(now)); + assertTrue(st instanceof QuantityType); + actual = actual.add((QuantityType) st); + assertEquals(49.431, actual.floatValue(), 0.001, "Current Production"); + actual = actual.add((QuantityType) st); + assertEquals(98.862, actual.floatValue(), 0.001, "Doubled Current Production"); } @Test @@ -106,8 +82,14 @@ void testErrorCases() { ForecastObject fo = new ForecastObject(); assertFalse(fo.isValid()); LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 23); - fo.getCurrentValue(now); - fo.getDayTotal(); - fo.getRemainingProduction(now); + assertEquals(-1.0, fo.getActualValue(now), 0.001, "Actual Production"); + assertEquals(-1.0, fo.getDayTotal(now, 0), 0.001, "Today Production"); + assertEquals(-1.0, fo.getRemainingProduction(now), 0.001, "Remaining Production"); + } + + @Test + void testLocation() { + PointType pt = PointType.valueOf("1.234,9.876"); + System.out.println(pt); } } From a07d739c2f8a802a208435befcceffe3762120f3 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Thu, 21 Jul 2022 15:52:25 +0200 Subject: [PATCH 003/111] bugfixes after testing Signed-off-by: Bernd Weymann --- .../README.md | 2 +- .../internal/SolarForecastBridgeHandler.java | 13 ++-- .../internal/SolarForecastConfiguration.java | 9 ++- .../internal/SolarForecastPlaneHandler.java | 71 +++++++++++++------ .../SolarForecastSinglePlaneHandler.java | 9 +-- .../OH-INF/config/thing-part-config.xml | 2 +- .../OH-INF/config/thing-single-config.xml | 2 +- .../resources/OH-INF/thing/channel-types.xml | 2 +- .../OH-INF/thing/thing-type-single.xml | 1 - 9 files changed, 76 insertions(+), 35 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 5b2fca0c53655..49b583b891caf 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -1,4 +1,4 @@ -# SolarForecast Binding + SolarForecast Binding _Give some details about what this binding is meant for - a protocol, system, specific device._ diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastBridgeHandler.java index b85c472e1ced2..607f38798a427 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastBridgeHandler.java @@ -86,18 +86,23 @@ private void startSchedule(int interval) { * Get data for all planes. Protect parts map from being modified during update */ private synchronized void getData() { + if (parts.isEmpty()) { + logger.info("No plane defined yet"); + return; + } LocalDateTime now = LocalDateTime.now(); double todaySum = 0; double tomorrowSum = 0; double actualSum = 0; double remainSum = 0; for (Iterator iterator = parts.iterator(); iterator.hasNext();) { - ForecastObject fo = iterator.next().fetchData(); + SolarForecastPlaneHandler sfph = iterator.next(); + ForecastObject fo = sfph.fetchData(); if (fo.isValid()) { todaySum += fo.getDayTotal(now, 0); - tomorrowSum = fo.getDayTotal(now, 1); - actualSum = fo.getActualValue(now); - remainSum = fo.getRemainingProduction(now); + tomorrowSum += fo.getDayTotal(now, 1); + actualSum += fo.getActualValue(now); + remainSum += fo.getRemainingProduction(now); } else { logger.info("Fetched data not valid {}", fo.toString()); } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastConfiguration.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastConfiguration.java index a645acc08d9de..561d833c9a197 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastConfiguration.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastConfiguration.java @@ -21,11 +21,16 @@ */ @NonNullByDefault public class SolarForecastConfiguration { - public String location = "0.0,0.0"; public int declination = -1; public int azimuth = 360; - public int kwp = 0; + public double kwp = 0; public int refreshInterval = -1; public String apiKey = SolarForecastBindingConstants.EMPTY; + + @Override + public String toString() { + return "Loc " + location + " Dec " + declination + " Azi " + azimuth + " KWP " + kwp + " Ref " + + refreshInterval; + } } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastPlaneHandler.java index b386081b856fc..2d31a19574b4e 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastPlaneHandler.java @@ -43,9 +43,9 @@ */ @NonNullByDefault public class SolarForecastPlaneHandler extends BaseThingHandler { - private final Logger logger = LoggerFactory.getLogger(SolarForecastPlaneHandler.class); private final HttpClient httpClient; + private ForecastObject forecast = new ForecastObject(); private Optional config = Optional.empty(); private Optional location = Optional.empty(); @@ -82,13 +82,20 @@ public void initialize() { @Override public void dispose() { super.dispose(); - if (thing instanceof SolarForecastBridgeHandler) { - ((SolarForecastBridgeHandler) thing).removePlane(this); + Bridge bridge = getBridge(); + if (bridge != null) { + BridgeHandler handler = bridge.getHandler(); + if (handler != null) { + if (handler instanceof SolarForecastBridgeHandler) { + ((SolarForecastBridgeHandler) handler).removePlane(this); + } + } } } @Override public void handleCommand(ChannelUID channelUID, Command command) { + logger.info("Handle command {} for channel {}", channelUID, command); if (command instanceof RefreshType) { fetchData(); } @@ -98,25 +105,33 @@ public void handleCommand(ChannelUID channelUID, Command command) { * https://doc.forecast.solar/doku.php?id=api:estimate */ ForecastObject fetchData() { - if (!forecast.isValid() && location.isPresent()) { - // https://api.forecast.solar/estimate/:lat/:lon/:dec/:az/:kwp - String url = BASE_URL + location.get().getLatitude() + SLASH + location.get().getLongitude() + SLASH - + config.get().declination + SLASH + config.get().azimuth + SLASH + config.get().kwp; - logger.info("Call {}", url); - try { - ContentResponse cr = httpClient.GET(url); - if (cr.getStatus() == 200) { - forecast = new ForecastObject(cr.getContentAsString(), LocalDateTime.now()); - logger.info("Fetched data {}", forecast.toString()); - updateChannels(forecast); - updateState(new ChannelUID(thing.getUID(), CHANNEL_RAW), - StringType.valueOf(cr.getContentAsString())); + if (location.isPresent()) { + if (!forecast.isValid()) { + // https://api.forecast.solar/estimate/:lat/:lon/:dec/:az/:kwp + String url = BASE_URL + location.get().getLatitude() + SLASH + location.get().getLongitude() + SLASH + + config.get().declination + SLASH + config.get().azimuth + SLASH + config.get().kwp; + logger.info("{} Call {}", thing.getLabel(), url); + try { + ContentResponse cr = httpClient.GET(url); + if (cr.getStatus() == 200) { + forecast = new ForecastObject(cr.getContentAsString(), LocalDateTime.now()); + logger.info("{} Fetched data {}", thing.getLabel(), forecast.toString()); + updateChannels(forecast); + updateState(new ChannelUID(thing.getUID(), CHANNEL_RAW), + StringType.valueOf(cr.getContentAsString())); + } else { + logger.info("{} Call {} failed {}", thing.getLabel(), url, cr.getStatus()); + } + } catch (InterruptedException | ExecutionException | TimeoutException e) { + logger.info("{} Call {} failed {}", thing.getLabel(), url, e.getMessage()); } - } catch (InterruptedException | ExecutionException | TimeoutException e) { - logger.info("Call {} failed {}", url, e.getMessage()); + } else { + logger.info("{} use available forecast {}", thing.getLabel(), forecast); } + updateChannels(forecast); + } else { + logger.info("{} Location not present", thing.getLabel()); } - updateChannels(forecast); return forecast; } @@ -131,7 +146,23 @@ private void updateChannels(ForecastObject f) { ForecastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 1))); } - protected void setLocation(PointType loc) { + /** + * Used by Bridge to set location directly + * + * @param loc + */ + void setLocation(PointType loc) { location = Optional.of(loc); } + + /** + * Used by SinglePlaneHandler to submit config data + * + * @param c + */ + protected void setConfig(SolarForecastConfiguration c) { + logger.info("Config {}", c); + config = Optional.of(c); + location = Optional.of(PointType.valueOf(c.location)); + } } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastSinglePlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastSinglePlaneHandler.java index d6d274a46ae7c..eb72333fd5ff9 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastSinglePlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastSinglePlaneHandler.java @@ -45,21 +45,22 @@ public SolarForecastSinglePlaneHandler(Thing thing, HttpClient httpClient, Point @Override public void initialize() { config = getConfigAs(SolarForecastConfiguration.class); - startSchedule(config.refreshInterval); if (config.location.equals(SolarForecastBindingConstants.AUTODETECT)) { Configuration editConfig = editConfiguration(); editConfig.put("location", homeLocation.toString()); updateConfiguration(editConfig); - super.setLocation(homeLocation); config = getConfigAs(SolarForecastConfiguration.class); - } else { - super.setLocation(PointType.valueOf(config.location)); } + if (config != null) { + super.setConfig(config); + } + startSchedule(config.refreshInterval); updateStatus(ThingStatus.ONLINE); } @Override public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); } private void startSchedule(int interval) { diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/thing-part-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/thing-part-config.xml index 188c9a1e7ae09..bd45f43e2dd8a 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/thing-part-config.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/thing-part-config.xml @@ -13,7 +13,7 @@ -180 = north, -90 = east, 0 = south, 90 = west, 180 = north - + Installed Module power of this plane diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/thing-single-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/thing-single-config.xml index 50e825f88b2de..f9e582882a463 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/thing-single-config.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/thing-single-config.xml @@ -19,7 +19,7 @@ -180 = north, -90 = east, 0 = south, 90 = west, 180 = north - + Installed Module power of this plane 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 index f020d6a448ca0..219449a0dec9f 100644 --- 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 @@ -28,7 +28,7 @@ Tomorrows forecast in total - + String Plain JSON response without conversions diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/thing-type-single.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/thing-type-single.xml index 348dc260e376d..58f7dc2620607 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/thing-type-single.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/thing-type-single.xml @@ -5,7 +5,6 @@ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - One PV plane attached to your inverter From b6068625a1a42420f9d0684698531235449535ce Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sun, 24 Jul 2022 12:26:22 +0200 Subject: [PATCH 004/111] add solcast service Signed-off-by: Bernd Weymann --- .../SolarForecastBindingConstants.java | 32 ++- .../internal/SolarForecastHandlerFactory.java | 18 +- .../ForecastSolarBridgeHandler.java} | 70 +++-- .../ForecastSolarConfiguration.java} | 7 +- .../forecastsolar/ForecastSolarConstants.java | 27 ++ .../ForecastSolarObject.java} | 36 ++- .../ForecastSolarPlaneHandler.java} | 96 ++++--- .../ForecastSolarSinglePlaneHandler.java} | 23 +- .../solcast/SolcastBridgeHandler.java | 169 +++++++++++++ .../solcast/SolcastConfiguration.java | 27 ++ .../internal/solcast/SolcastConstants.java | 27 ++ .../internal/solcast/SolcastObject.java | 239 ++++++++++++++++++ .../internal/solcast/SolcastPlaneHandler.java | 168 ++++++++++++ .../solcast/SolcastSinglePlaneHandler.java | 73 ++++++ ...bridge-config.xml => fs-bridge-config.xml} | 7 +- ...rt-config.xml => fs-plane-part-config.xml} | 7 +- ...-config.xml => fs-plane-single-config.xml} | 4 +- .../OH-INF/config/sc-plane-part-config.xml | 18 ++ .../OH-INF/config/sc-plane-single-config.xml | 18 ++ .../resources/OH-INF/thing/channel-types.xml | 120 ++++++++- ...ype-multi.xml => fs-bridge-type-multi.xml} | 8 +- ...{thing-type-part.xml => fs-plane-part.xml} | 10 +- ...ng-type-single.xml => fs-plane-single.xml} | 6 +- .../OH-INF/thing/sc-bridge-type-multi.xml | 35 +++ .../OH-INF/thing/sc-thing-type-part.xml | 42 +++ .../OH-INF/thing/sc-thing-type-single.xml | 38 +++ .../solarforecast/ForecastSolarTest.java | 20 +- .../binding/solarforecast/SolcastTest.java | 111 ++++++++ .../estimated-actuals.json | 0 .../{solarcast => solcast}/forecasts.json | 0 30 files changed, 1317 insertions(+), 139 deletions(-) rename bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/{SolarForecastBridgeHandler.java => forecastsolar/ForecastSolarBridgeHandler.java} (60%) rename bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/{SolarForecastConfiguration.java => forecastsolar/ForecastSolarConfiguration.java} (79%) create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarConstants.java rename bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/{ForecastObject.java => forecastsolar/ForecastSolarObject.java} (76%) rename bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/{SolarForecastPlaneHandler.java => forecastsolar/ForecastSolarPlaneHandler.java} (53%) rename bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/{SolarForecastSinglePlaneHandler.java => forecastsolar/ForecastSolarSinglePlaneHandler.java} (78%) create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastConfiguration.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/SolcastPlaneHandler.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastSinglePlaneHandler.java rename bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/{bridge-config.xml => fs-bridge-config.xml} (73%) rename bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/{thing-part-config.xml => fs-plane-part-config.xml} (78%) rename bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/{thing-single-config.xml => fs-plane-single-config.xml} (94%) create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-part-config.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-single-config.xml rename bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/{bridge-type-multi.xml => fs-bridge-type-multi.xml} (75%) rename bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/{thing-type-part.xml => fs-plane-part.xml} (75%) rename bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/{thing-type-single.xml => fs-plane-single.xml} (78%) create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-bridge-type-multi.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-thing-type-part.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-thing-type-single.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastTest.java rename bundles/org.openhab.binding.solarforecast/src/test/resources/{solarcast => solcast}/estimated-actuals.json (100%) rename bundles/org.openhab.binding.solarforecast/src/test/resources/{solarcast => solcast}/forecasts.json (100%) 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 index 8feef9dfcce82..ef5d91e25a15c 100644 --- 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 @@ -28,19 +28,39 @@ public class SolarForecastBindingConstants { private static final String BINDING_ID = "solarforecast"; - public static final ThingTypeUID FORECAST_SOLAR_SINGLE_STRING = new ThingTypeUID(BINDING_ID, "single"); - public static final ThingTypeUID FORECAST_SOLAR_MULTI_STRING = new ThingTypeUID(BINDING_ID, "multi"); - public static final ThingTypeUID FORECAST_SOLAR_PART_STRING = new ThingTypeUID(BINDING_ID, "part"); + public static final ThingTypeUID FORECAST_SOLAR_SINGLE_STRING = new ThingTypeUID(BINDING_ID, "fs-single"); + public static final ThingTypeUID FORECAST_SOLAR_MULTI_STRING = new ThingTypeUID(BINDING_ID, "fs-multi"); + public static final ThingTypeUID FORECAST_SOLAR_PART_STRING = new ThingTypeUID(BINDING_ID, "fs-part"); + public static final ThingTypeUID SOLCAST_BRIDGE_STRING = new ThingTypeUID(BINDING_ID, "sc-multi"); + public static final ThingTypeUID SOLCAST_PART_STRING = new ThingTypeUID(BINDING_ID, "sc-part"); + public static final ThingTypeUID SOLCAST_SINGLE_STRING = new ThingTypeUID(BINDING_ID, "sc-single"); public static final Set SUPPORTED_THING_SET = Set.of(FORECAST_SOLAR_SINGLE_STRING, - FORECAST_SOLAR_MULTI_STRING, FORECAST_SOLAR_PART_STRING); + FORECAST_SOLAR_MULTI_STRING, FORECAST_SOLAR_PART_STRING, SOLCAST_BRIDGE_STRING, SOLCAST_PART_STRING, + SOLCAST_SINGLE_STRING); public static final String CHANNEL_TODAY = "today"; public static final String CHANNEL_ACTUAL = "actual"; public static final String CHANNEL_REMAINING = "remaining"; public static final String CHANNEL_TOMORROW = "tomorrow"; - public static final String CHANNEL_RAW = "raw"; + public static final String CHANNEL_TOMORROW_LOW = "tomorrow-low"; + public static final String CHANNEL_TOMORROW_HIGH = "tomorrow-high"; + public static final String CHANNEL_DAY2 = "day2"; + public static final String CHANNEL_DAY2_LOW = "day2-low"; + public static final String CHANNEL_DAY2_HIGH = "day2-high"; + public static final String CHANNEL_DAY3 = "day3"; + public static final String CHANNEL_DAY3_LOW = "day3-low"; + public static final String CHANNEL_DAY3_HIGH = "day3-high"; + public static final String CHANNEL_DAY4 = "day4"; + public static final String CHANNEL_DAY4_LOW = "day4-low"; + public static final String CHANNEL_DAY4_HIGH = "day4-high"; + public static final String CHANNEL_DAY5 = "day5"; + public static final String CHANNEL_DAY5_LOW = "day5-low"; + public static final String CHANNEL_DAY5_HIGH = "day5-high"; + public static final String CHANNEL_DAY6 = "day6"; + public static final String CHANNEL_DAY6_LOW = "day6-low"; + public static final String CHANNEL_DAY6_HIGH = "day6-high"; - public static final String BASE_URL = "https://api.forecast.solar/estimate/"; + public static final String CHANNEL_RAW = "raw"; public static final String AUTODETECT = "AUTODETECT"; public static final String SLASH = "/"; 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 index e55b1f3f79fb2..965a3e55f9351 100644 --- 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 @@ -17,6 +17,12 @@ 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.ForecastSolarBridgeHandler; +import org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarPlaneHandler; +import org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarSinglePlaneHandler; +import org.openhab.binding.solarforecast.internal.solcast.SolcastBridgeHandler; +import org.openhab.binding.solarforecast.internal.solcast.SolcastPlaneHandler; +import org.openhab.binding.solarforecast.internal.solcast.SolcastSinglePlaneHandler; import org.openhab.core.i18n.LocationProvider; import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.library.types.PointType; @@ -63,11 +69,17 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { protected @Nullable ThingHandler createHandler(Thing thing) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (FORECAST_SOLAR_MULTI_STRING.equals(thingTypeUID)) { - return new SolarForecastBridgeHandler((Bridge) thing, location); + return new ForecastSolarBridgeHandler((Bridge) thing, location); } else if (FORECAST_SOLAR_PART_STRING.equals(thingTypeUID)) { - return new SolarForecastPlaneHandler(thing, httpClient); + return new ForecastSolarPlaneHandler(thing, httpClient); } else if (FORECAST_SOLAR_SINGLE_STRING.equals(thingTypeUID)) { - return new SolarForecastSinglePlaneHandler(thing, httpClient, location); + return new ForecastSolarSinglePlaneHandler(thing, httpClient, location); + } else if (SOLCAST_BRIDGE_STRING.equals(thingTypeUID)) { + return new SolcastBridgeHandler((Bridge) thing); + } else if (SOLCAST_PART_STRING.equals(thingTypeUID)) { + return new SolcastPlaneHandler(thing, httpClient); + } else if (SOLCAST_SINGLE_STRING.equals(thingTypeUID)) { + return new SolcastSinglePlaneHandler(thing, httpClient); } return null; } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java similarity index 60% rename from bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastBridgeHandler.java rename to bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java index 607f38798a427..7fc7660379809 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.solarforecast.internal; +package org.openhab.binding.solarforecast.internal.forecastsolar; import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; @@ -23,7 +23,8 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; +import org.openhab.binding.solarforecast.internal.solcast.SolcastObject; import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.PointType; import org.openhab.core.thing.Bridge; @@ -35,36 +36,38 @@ import org.slf4j.LoggerFactory; /** - * The {@link SolarForecastBridgeHandler} is a non active handler instance. It will be triggerer by the bridge. + * The {@link ForecastSolarBridgeHandler} is a non active handler instance. It will be triggerer by the bridge. * * @author Bernd Weymann - Initial contribution */ @NonNullByDefault -public class SolarForecastBridgeHandler extends BaseBridgeHandler { +public class ForecastSolarBridgeHandler extends BaseBridgeHandler { - private final Logger logger = LoggerFactory.getLogger(SolarForecastBridgeHandler.class); + private final Logger logger = LoggerFactory.getLogger(ForecastSolarBridgeHandler.class); private final PointType homeLocation; - private List parts = new ArrayList(); + private List parts = new ArrayList(); + private Optional configuration = Optional.empty(); private Optional> refreshJob = Optional.empty(); - private @Nullable SolarForecastConfiguration config; - public SolarForecastBridgeHandler(Bridge bridge, PointType location) { + public ForecastSolarBridgeHandler(Bridge bridge, PointType location) { super(bridge); homeLocation = location; } @Override public void initialize() { - config = getConfigAs(SolarForecastConfiguration.class); - startSchedule(config.refreshInterval); + ForecastSolarConfiguration config = getConfigAs(ForecastSolarConfiguration.class); if (config.location.equals(SolarForecastBindingConstants.AUTODETECT)) { Configuration editConfig = editConfiguration(); editConfig.put("location", homeLocation.toString()); updateConfiguration(editConfig); - config = getConfigAs(SolarForecastConfiguration.class); + config = getConfigAs(ForecastSolarConfiguration.class); } + configuration = Optional.of(config); updateStatus(ThingStatus.ONLINE); + getData(); + startSchedule(1); } @Override @@ -91,26 +94,33 @@ private synchronized void getData() { return; } LocalDateTime now = LocalDateTime.now(); - double todaySum = 0; - double tomorrowSum = 0; double actualSum = 0; double remainSum = 0; - for (Iterator iterator = parts.iterator(); iterator.hasNext();) { - SolarForecastPlaneHandler sfph = iterator.next(); - ForecastObject fo = sfph.fetchData(); + double todaySum = 0; + double tomorrowSum = 0; + double day2Sum = 0; + double day3Sum = 0; + for (Iterator iterator = parts.iterator(); iterator.hasNext();) { + ForecastSolarPlaneHandler sfph = iterator.next(); + ForecastSolarObject fo = sfph.fetchData(); if (fo.isValid()) { - todaySum += fo.getDayTotal(now, 0); - tomorrowSum += fo.getDayTotal(now, 1); actualSum += fo.getActualValue(now); remainSum += fo.getRemainingProduction(now); + todaySum += fo.getDayTotal(now, 0); + tomorrowSum += fo.getDayTotal(now, 1); + day2Sum += fo.getDayTotal(now, 2); + day3Sum += fo.getDayTotal(now, 3); } else { logger.info("Fetched data not valid {}", fo.toString()); } } - updateState(CHANNEL_TODAY, ForecastObject.getStateObject(todaySum)); - updateState(CHANNEL_TOMORROW, ForecastObject.getStateObject(tomorrowSum)); - updateState(CHANNEL_REMAINING, ForecastObject.getStateObject(remainSum)); - updateState(CHANNEL_ACTUAL, ForecastObject.getStateObject(actualSum)); + logger.info("Remain: {}", remainSum); + updateState(CHANNEL_REMAINING, SolcastObject.getStateObject(remainSum)); + updateState(CHANNEL_ACTUAL, SolcastObject.getStateObject(actualSum)); + updateState(CHANNEL_TODAY, SolcastObject.getStateObject(todaySum)); + updateState(CHANNEL_TOMORROW, SolcastObject.getStateObject(tomorrowSum)); + updateState(CHANNEL_DAY2, SolcastObject.getStateObject(day2Sum)); + updateState(CHANNEL_DAY3, SolcastObject.getStateObject(day3Sum)); } @Override @@ -118,15 +128,19 @@ public void dispose() { refreshJob.ifPresent(job -> job.cancel(true)); } - synchronized void addPlane(SolarForecastPlaneHandler sfph) { + synchronized void addPlane(ForecastSolarPlaneHandler sfph) { parts.add(sfph); + // update passive PV plane with necessary data + if (configuration.isPresent()) { + sfph.setLocation(new PointType(configuration.get().location)); + if (!EMPTY.equals(configuration.get().apiKey)) { + sfph.setApiKey(configuration.get().apiKey); + } + } + getData(); } - synchronized void removePlane(SolarForecastPlaneHandler sfph) { + synchronized void removePlane(ForecastSolarPlaneHandler sfph) { parts.remove(sfph); } - - public PointType getLocation() { - return PointType.valueOf(config.location); - } } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastConfiguration.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarConfiguration.java similarity index 79% rename from bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastConfiguration.java rename to bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarConfiguration.java index 561d833c9a197..c4ecca2dbb86a 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastConfiguration.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarConfiguration.java @@ -10,17 +10,18 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.solarforecast.internal; +package org.openhab.binding.solarforecast.internal.forecastsolar; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; /** - * The {@link SolarForecastConfiguration} class contains fields mapping thing configuration parameters. + * The {@link ForecastSolarConfiguration} class contains fields mapping thing configuration parameters. * * @author Bernd Weymann - Initial contribution */ @NonNullByDefault -public class SolarForecastConfiguration { +public class ForecastSolarConfiguration { public String location = "0.0,0.0"; public int declination = -1; public int azimuth = 360; diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarConstants.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarConstants.java new file mode 100644 index 0000000000000..afcbd9511493c --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarConstants.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2022 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 org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ForecastSolarConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class ForecastSolarConstants { + + public static final String BASE_URL = "https://api.forecast.solar/"; +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/ForecastObject.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarObject.java similarity index 76% rename from bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/ForecastObject.java rename to bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarObject.java index 02147052bde72..4fb43f286920e 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/ForecastObject.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarObject.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.solarforecast.internal; +package org.openhab.binding.solarforecast.internal.forecastsolar; import java.time.LocalDate; import java.time.LocalDateTime; @@ -21,29 +21,34 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.json.JSONObject; +import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.Units; import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** - * The {@link ForecastObject} holds complete data for forecast + * The {@link ForecastSolarObject} holds complete data for forecast * * @author Bernd Weymann - Initial contribution */ @NonNullByDefault -public class ForecastObject { +public class ForecastSolarObject { + private final Logger logger = LoggerFactory.getLogger(ForecastSolarObject.class); private static final double UNDEF = -1; private final TreeMap dataMap = new TreeMap(); private Optional rawData = Optional.empty(); private boolean valid = false; - private int constructionHour; + private LocalDateTime expirationDateTime; - public ForecastObject() { + public ForecastSolarObject() { + expirationDateTime = LocalDateTime.now(); } - public ForecastObject(String content, LocalDateTime now) { - constructionHour = now.getHour(); + public ForecastSolarObject(String content, LocalDateTime now, LocalDateTime expirationDate) { + expirationDateTime = expirationDate; if (!content.equals(SolarForecastBindingConstants.EMPTY)) { rawData = Optional.of(content); JSONObject contentJson = new JSONObject(content); @@ -64,7 +69,20 @@ public ForecastObject(String content, LocalDateTime now) { } public boolean isValid() { - return valid && constructionHour == LocalDateTime.now().getHour() && !dataMap.isEmpty(); + if (valid) { + if (!dataMap.isEmpty()) { + if (expirationDateTime.isAfter(LocalDateTime.now())) { + return true; + } else { + logger.info("Forecast data expired"); + } + } else { + logger.info("Empty data map"); + } + } else { + logger.info("No Forecast data available"); + } + return false; } public double getActualValue(LocalDateTime now) { @@ -123,6 +141,6 @@ public static State getStateObject(double d) { @Override public String toString() { - return "Hour: " + constructionHour + ", Valid: " + valid + ", Data:" + dataMap; + return "Expiration: " + expirationDateTime + ", Valid: " + valid + ", Data:" + dataMap; } } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java similarity index 53% rename from bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastPlaneHandler.java rename to bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java index 2d31a19574b4e..4fb953e64f600 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java @@ -10,9 +10,10 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.solarforecast.internal; +package org.openhab.binding.solarforecast.internal.forecastsolar; import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; +import static org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarConstants.BASE_URL; import java.time.LocalDateTime; import java.util.Optional; @@ -22,6 +23,7 @@ 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.solcast.SolcastObject; import org.openhab.core.library.types.PointType; import org.openhab.core.library.types.StringType; import org.openhab.core.thing.Bridge; @@ -37,35 +39,37 @@ import org.slf4j.LoggerFactory; /** - * The {@link SolarForecastPlaneHandler} is a non active handler instance. It will be triggerer by the bridge. + * The {@link ForecastSolarPlaneHandler} is a non active handler instance. It will be triggerer by the bridge. * * @author Bernd Weymann - Initial contribution */ @NonNullByDefault -public class SolarForecastPlaneHandler extends BaseThingHandler { - private final Logger logger = LoggerFactory.getLogger(SolarForecastPlaneHandler.class); +public class ForecastSolarPlaneHandler extends BaseThingHandler { + private final Logger logger = LoggerFactory.getLogger(ForecastSolarPlaneHandler.class); private final HttpClient httpClient; - private ForecastObject forecast = new ForecastObject(); - private Optional config = Optional.empty(); + private Optional configuration = Optional.empty(); + private Optional bridgeHandler = Optional.empty(); private Optional location = Optional.empty(); + private Optional apiKey = Optional.empty(); + private ForecastSolarObject forecast = new ForecastSolarObject(); - public SolarForecastPlaneHandler(Thing thing, HttpClient hc) { + public ForecastSolarPlaneHandler(Thing thing, HttpClient hc) { super(thing); httpClient = hc; } @Override public void initialize() { - SolarForecastConfiguration c = getConfigAs(SolarForecastConfiguration.class); - config = Optional.of(c); + ForecastSolarConfiguration c = getConfigAs(ForecastSolarConfiguration.class); + configuration = Optional.of(c); Bridge bridge = getBridge(); if (bridge != null) { BridgeHandler handler = bridge.getHandler(); if (handler != null) { - if (handler instanceof SolarForecastBridgeHandler) { - ((SolarForecastBridgeHandler) handler).addPlane(this); - location = Optional.of(((SolarForecastBridgeHandler) handler).getLocation()); + if (handler instanceof ForecastSolarBridgeHandler) { + bridgeHandler = Optional.of((ForecastSolarBridgeHandler) handler); + bridgeHandler.get().addPlane(this); updateStatus(ThingStatus.ONLINE); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, @@ -82,14 +86,8 @@ public void initialize() { @Override public void dispose() { super.dispose(); - Bridge bridge = getBridge(); - if (bridge != null) { - BridgeHandler handler = bridge.getHandler(); - if (handler != null) { - if (handler instanceof SolarForecastBridgeHandler) { - ((SolarForecastBridgeHandler) handler).removePlane(this); - } - } + if (bridgeHandler.isPresent()) { + bridgeHandler.get().removePlane(this); } } @@ -104,21 +102,33 @@ public void handleCommand(ChannelUID channelUID, Command command) { /** * https://doc.forecast.solar/doku.php?id=api:estimate */ - ForecastObject fetchData() { + protected ForecastSolarObject fetchData() { if (location.isPresent()) { if (!forecast.isValid()) { - // https://api.forecast.solar/estimate/:lat/:lon/:dec/:az/:kwp - String url = BASE_URL + location.get().getLatitude() + SLASH + location.get().getLongitude() + SLASH - + config.get().declination + SLASH + config.get().azimuth + SLASH + config.get().kwp; + String url; + if (apiKey.isEmpty()) { + // use public API + // https://api.forecast.solar/estimate/:lat/:lon/:dec/:az/:kwp + url = BASE_URL + "estimate/" + location.get().getLatitude() + SLASH + location.get().getLongitude() + + SLASH + configuration.get().declination + SLASH + configuration.get().azimuth + SLASH + + configuration.get().kwp; + } else { + // use paid API + // https://api.forecast.solar/:apikey/estimate/:lat/:lon/:dec/:az/:kwp + url = BASE_URL + apiKey.get() + "/estimate/" + location.get().getLatitude() + SLASH + + location.get().getLongitude() + SLASH + configuration.get().declination + SLASH + + configuration.get().azimuth + SLASH + configuration.get().kwp; + } logger.info("{} Call {}", thing.getLabel(), url); try { ContentResponse cr = httpClient.GET(url); if (cr.getStatus() == 200) { - forecast = new ForecastObject(cr.getContentAsString(), LocalDateTime.now()); + forecast = new ForecastSolarObject(cr.getContentAsString(), LocalDateTime.now(), + LocalDateTime.now().plusMinutes(configuration.get().refreshInterval)); logger.info("{} Fetched data {}", thing.getLabel(), forecast.toString()); + logger.info("{} Valid after creation? {}", thing.getLabel(), forecast.isValid()); updateChannels(forecast); - updateState(new ChannelUID(thing.getUID(), CHANNEL_RAW), - StringType.valueOf(cr.getContentAsString())); + updateState(CHANNEL_RAW, StringType.valueOf(cr.getContentAsString())); } else { logger.info("{} Call {} failed {}", thing.getLabel(), url, cr.getStatus()); } @@ -135,15 +145,13 @@ ForecastObject fetchData() { return forecast; } - private void updateChannels(ForecastObject f) { - updateState(new ChannelUID(thing.getUID(), SolarForecastBindingConstants.CHANNEL_ACTUAL), - ForecastObject.getStateObject(f.getActualValue(LocalDateTime.now()))); - updateState(new ChannelUID(thing.getUID(), SolarForecastBindingConstants.CHANNEL_REMAINING), - ForecastObject.getStateObject(f.getRemainingProduction(LocalDateTime.now()))); - updateState(new ChannelUID(thing.getUID(), SolarForecastBindingConstants.CHANNEL_TODAY), - ForecastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 0))); - updateState(new ChannelUID(thing.getUID(), SolarForecastBindingConstants.CHANNEL_TOMORROW), - ForecastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 1))); + private void updateChannels(ForecastSolarObject f) { + updateState(CHANNEL_ACTUAL, SolcastObject.getStateObject(f.getActualValue(LocalDateTime.now()))); + updateState(CHANNEL_REMAINING, SolcastObject.getStateObject(f.getRemainingProduction(LocalDateTime.now()))); + updateState(CHANNEL_TODAY, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 0))); + updateState(CHANNEL_TOMORROW, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 1))); + updateState(CHANNEL_DAY2, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 2))); + updateState(CHANNEL_DAY3, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 3))); } /** @@ -155,14 +163,26 @@ void setLocation(PointType loc) { location = Optional.of(loc); } + /** + * Used by Bridge to set location directly + * + * @param loc + */ + void setApiKey(String key) { + apiKey = Optional.of(key); + } + /** * Used by SinglePlaneHandler to submit config data * * @param c */ - protected void setConfig(SolarForecastConfiguration c) { + protected void setConfig(ForecastSolarConfiguration c) { logger.info("Config {}", c); - config = Optional.of(c); + configuration = Optional.of(c); location = Optional.of(PointType.valueOf(c.location)); + if (!EMPTY.equals(c.apiKey)) { + apiKey = Optional.of(c.apiKey); + } } } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastSinglePlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarSinglePlaneHandler.java similarity index 78% rename from bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastSinglePlaneHandler.java rename to bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarSinglePlaneHandler.java index eb72333fd5ff9..88d35885e8530 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastSinglePlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarSinglePlaneHandler.java @@ -10,15 +10,15 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.solarforecast.internal; +package org.openhab.binding.solarforecast.internal.forecastsolar; import java.util.Optional; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.PointType; import org.openhab.core.thing.ChannelUID; @@ -27,34 +27,32 @@ import org.openhab.core.types.Command; /** - * The {@link SolarForecastBridgeHandler} is a non active handler instance. It will be triggerer by the bridge. + * The {@link ForecastSolarBridgeHandler} is a non active handler instance. It will be triggerer by the bridge. * * @author Bernd Weymann - Initial contribution */ @NonNullByDefault -public class SolarForecastSinglePlaneHandler extends SolarForecastPlaneHandler { +public class ForecastSolarSinglePlaneHandler extends ForecastSolarPlaneHandler { private Optional> refreshJob = Optional.empty(); private PointType homeLocation; - private @Nullable SolarForecastConfiguration config; - public SolarForecastSinglePlaneHandler(Thing thing, HttpClient httpClient, PointType location) { + public ForecastSolarSinglePlaneHandler(Thing thing, HttpClient httpClient, PointType location) { super(thing, httpClient); homeLocation = location; } @Override public void initialize() { - config = getConfigAs(SolarForecastConfiguration.class); + ForecastSolarConfiguration config = getConfigAs(ForecastSolarConfiguration.class); + // adapt to home location if AUTODETECT selected if (config.location.equals(SolarForecastBindingConstants.AUTODETECT)) { Configuration editConfig = editConfiguration(); editConfig.put("location", homeLocation.toString()); updateConfiguration(editConfig); - config = getConfigAs(SolarForecastConfiguration.class); + config = getConfigAs(ForecastSolarConfiguration.class); } - if (config != null) { - super.setConfig(config); - } - startSchedule(config.refreshInterval); + super.setConfig(config); + startSchedule(1); updateStatus(ThingStatus.ONLINE); } @@ -80,6 +78,7 @@ private void getData() { @Override public void dispose() { + super.dispose(); refreshJob.ifPresent(job -> job.cancel(true)); } } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java new file mode 100644 index 0000000000000..58b88df594774 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java @@ -0,0 +1,169 @@ +/** + * Copyright (c) 2010-2022 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 static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SolcastBridgeHandler} is a non active handler instance. It will be triggerer by the bridge. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class SolcastBridgeHandler extends BaseBridgeHandler { + + private final Logger logger = LoggerFactory.getLogger(SolcastBridgeHandler.class); + private List parts = new ArrayList(); + private Optional> refreshJob = Optional.empty(); + + public SolcastBridgeHandler(Bridge bridge) { + super(bridge); + } + + @Override + public void initialize() { + // nothing to initialze - simply start scheduling + updateStatus(ThingStatus.ONLINE); + getData(); + startSchedule(1); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } + + private void startSchedule(int interval) { + refreshJob.ifPresentOrElse(job -> { + if (job.isCancelled()) { + refreshJob = Optional + .of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES)); + } // else - scheduler is already running! + }, () -> { + refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES)); + }); + } + + /** + * Get data for all planes. Protect parts map from being modified during update + */ + private synchronized void getData() { + if (parts.isEmpty()) { + logger.info("No plane defined yet"); + return; + } + LocalDateTime now = LocalDateTime.now(); + double actualSum = 0; + double remainSum = 0; + double todaySum = 0; + double tomorrowSum = 0; + double tomorrowSumLow = 0; + double tomorrowSumHigh = 0; + double day2Sum = 0; + double day2SumLow = 0; + double day2SumHigh = 0; + double day3Sum = 0; + double day3SumLow = 0; + double day3SumHigh = 0; + double day4Sum = 0; + double day4SumLow = 0; + double day4SumHigh = 0; + double day5Sum = 0; + double day5SumLow = 0; + double day5SumHigh = 0; + double day6Sum = 0; + double day6SumLow = 0; + double day6SumHigh = 0; + + for (Iterator iterator = parts.iterator(); iterator.hasNext();) { + SolcastPlaneHandler sfph = iterator.next(); + SolcastObject fo = sfph.fetchData(); + if (fo.isValid()) { + actualSum += fo.getActualValue(now); + remainSum += fo.getRemainingProduction(now); + todaySum += fo.getDayTotal(now, 0); + tomorrowSum += fo.getDayTotal(now, 1); + tomorrowSumLow += fo.getPessimisticDayTotal(now, 1); + tomorrowSumHigh += fo.getOptimisticDayTotal(now, 1); + day2Sum += fo.getDayTotal(now, 2); + day2SumLow += fo.getPessimisticDayTotal(now, 2); + day2SumHigh += fo.getOptimisticDayTotal(now, 2); + day3Sum += fo.getDayTotal(now, 3); + day3SumLow += fo.getPessimisticDayTotal(now, 3); + day3SumHigh += fo.getOptimisticDayTotal(now, 3); + day4Sum += fo.getDayTotal(now, 4); + day4SumLow += fo.getPessimisticDayTotal(now, 4); + day4SumHigh += fo.getOptimisticDayTotal(now, 4); + day5Sum += fo.getDayTotal(now, 5); + day5SumLow += fo.getPessimisticDayTotal(now, 5); + day5SumHigh += fo.getOptimisticDayTotal(now, 5); + day6Sum += fo.getDayTotal(now, 6); + day6SumLow += fo.getPessimisticDayTotal(now, 6); + day6SumHigh += fo.getOptimisticDayTotal(now, 6); + } else { + logger.info("Fetched data not valid {}", fo.toString()); + } + } + updateState(CHANNEL_ACTUAL, SolcastObject.getStateObject(actualSum)); + updateState(CHANNEL_REMAINING, SolcastObject.getStateObject(remainSum)); + updateState(CHANNEL_TODAY, SolcastObject.getStateObject(todaySum)); + updateState(CHANNEL_TOMORROW, SolcastObject.getStateObject(tomorrowSum)); + updateState(CHANNEL_TOMORROW_HIGH, SolcastObject.getStateObject(tomorrowSumHigh)); + updateState(CHANNEL_TOMORROW_LOW, SolcastObject.getStateObject(tomorrowSumLow)); + updateState(CHANNEL_DAY2, SolcastObject.getStateObject(day2Sum)); + updateState(CHANNEL_DAY2_HIGH, SolcastObject.getStateObject(day2SumHigh)); + updateState(CHANNEL_DAY2_LOW, SolcastObject.getStateObject(day2SumLow)); + updateState(CHANNEL_DAY3, SolcastObject.getStateObject(day3Sum)); + updateState(CHANNEL_DAY3_HIGH, SolcastObject.getStateObject(day3SumHigh)); + updateState(CHANNEL_DAY3_LOW, SolcastObject.getStateObject(day3SumLow)); + updateState(CHANNEL_DAY4, SolcastObject.getStateObject(day4Sum)); + updateState(CHANNEL_DAY4_HIGH, SolcastObject.getStateObject(day4SumHigh)); + updateState(CHANNEL_DAY4_LOW, SolcastObject.getStateObject(day4SumLow)); + updateState(CHANNEL_DAY5, SolcastObject.getStateObject(day5Sum)); + updateState(CHANNEL_DAY5_HIGH, SolcastObject.getStateObject(day5SumHigh)); + updateState(CHANNEL_DAY5_LOW, SolcastObject.getStateObject(day5SumLow)); + updateState(CHANNEL_DAY6, SolcastObject.getStateObject(day6Sum)); + updateState(CHANNEL_DAY6_HIGH, SolcastObject.getStateObject(day6SumHigh)); + updateState(CHANNEL_DAY6_LOW, SolcastObject.getStateObject(day6SumLow)); + } + + @Override + public void dispose() { + refreshJob.ifPresent(job -> job.cancel(true)); + } + + synchronized void addPlane(SolcastPlaneHandler sph) { + parts.add(sph); + getData(); + } + + synchronized void removePlane(SolcastPlaneHandler sph) { + parts.remove(sph); + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastConfiguration.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastConfiguration.java new file mode 100644 index 0000000000000..3c1fbda6d01fd --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastConfiguration.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2022 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 org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; + +/** + * The {@link SolcastConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class SolcastConfiguration { + public int refreshInterval = -1; + public String resourceId = SolarForecastBindingConstants.EMPTY; +} 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 0000000000000..48062d7ccaa0e --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastConstants.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2022 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 org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link SolcastConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class SolcastConstants { + public static final String FORECAST_URL = "https://api.solcast.com.au/rooftop_sites/%s/forecasts?format=json"; + public static final String CURRENT_ESTIMATE_URL = "https://api.solcast.com.au/rooftop_sites/%s/estimated_actuals?format=json"; +} 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 0000000000000..371b5fb6be66c --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastObject.java @@ -0,0 +1,239 @@ +/** + * Copyright (c) 2010-2022 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.LocalDate; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONArray; +import org.json.JSONObject; +import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +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 { + private final Logger logger = LoggerFactory.getLogger(SolcastObject.class); + private static final double UNDEF = -1; + private final Map> dataMap = new HashMap>(); + private final Map> optimisticDataMap = new HashMap>(); + private final Map> pessimisticDataMap = new HashMap>(); + private Optional rawData = Optional.of(new JSONObject()); + private LocalDateTime expirationDateTime; + private boolean valid = false; + + public SolcastObject() { + // invalid forecast object + expirationDateTime = LocalDateTime.now(); + } + + public SolcastObject(String content, LocalDateTime ldt) { + expirationDateTime = ldt; + add(content); + } + + public void join(String content) { + add(content); + } + + private void add(String content) { + if (!content.equals(SolarForecastBindingConstants.EMPTY)) { + valid = true; + JSONObject contentJson = new JSONObject(content); + JSONArray resultJsonArray; + if (contentJson.has("forecasts")) { + resultJsonArray = contentJson.getJSONArray("forecasts"); + rawData.get().put("forecasts", resultJsonArray); + + } else { + resultJsonArray = contentJson.getJSONArray("estimated_actuals"); + rawData.get().put("estimated_actuals", resultJsonArray); + } + for (int i = 0; i < resultJsonArray.length(); i++) { + JSONObject jo = resultJsonArray.getJSONObject(i); + String periodEnd = jo.getString("period_end"); + LocalDate ld = LocalDate.parse(periodEnd.substring(0, periodEnd.indexOf("T"))); + TreeMap forecastMap = dataMap.get(ld); + if (forecastMap == null) { + forecastMap = new TreeMap(); + LocalDateTime ldt = LocalDateTime.parse(periodEnd.substring(0, periodEnd.lastIndexOf("."))); + forecastMap.put(ldt, jo.getDouble("pv_estimate")); + dataMap.put(ld, forecastMap); + } else { + LocalDateTime ldt = LocalDateTime.parse(periodEnd.substring(0, periodEnd.lastIndexOf("."))); + forecastMap.put(ldt, jo.getDouble("pv_estimate")); + dataMap.put(ld, forecastMap); + } + if (jo.has("pv_estimate10")) { + TreeMap pessimisticForecastMap = pessimisticDataMap.get(ld); + if (pessimisticForecastMap == null) { + pessimisticForecastMap = new TreeMap(); + LocalDateTime ldt = LocalDateTime.parse(periodEnd.substring(0, periodEnd.lastIndexOf("."))); + pessimisticForecastMap.put(ldt, jo.getDouble("pv_estimate10")); + pessimisticDataMap.put(ld, pessimisticForecastMap); + } else { + LocalDateTime ldt = LocalDateTime.parse(periodEnd.substring(0, periodEnd.lastIndexOf("."))); + pessimisticForecastMap.put(ldt, jo.getDouble("pv_estimate10")); + pessimisticDataMap.put(ld, pessimisticForecastMap); + } + } + if (jo.has("pv_estimate90")) { + TreeMap optimisticForecastMap = optimisticDataMap.get(ld); + if (optimisticForecastMap == null) { + optimisticForecastMap = new TreeMap(); + LocalDateTime ldt = LocalDateTime.parse(periodEnd.substring(0, periodEnd.lastIndexOf("."))); + optimisticForecastMap.put(ldt, jo.getDouble("pv_estimate90")); + optimisticDataMap.put(ld, optimisticForecastMap); + } else { + LocalDateTime ldt = LocalDateTime.parse(periodEnd.substring(0, periodEnd.lastIndexOf("."))); + optimisticForecastMap.put(ldt, jo.getDouble("pv_estimate90")); + optimisticDataMap.put(ld, optimisticForecastMap); + } + } + } + } + } + + public boolean isValid() { + if (valid) { + if (!dataMap.isEmpty()) { + if (expirationDateTime.isAfter(LocalDateTime.now())) { + return true; + } else { + logger.info("Forecast data expired"); + } + } else { + logger.info("Empty data map"); + } + } else { + logger.info("No Forecast data available"); + } + return false; + } + + public double getActualValue(LocalDateTime now) { + if (dataMap.isEmpty()) { + return UNDEF; + } + LocalDate ld = now.toLocalDate(); + TreeMap dtm = dataMap.get(ld); + if (dtm == null) { + return UNDEF; + } + double forecastValue = 0; + Set keySet = dtm.keySet(); + for (LocalDateTime key : keySet) { + if (key.isBefore(now)) { + forecastValue += dtm.get(key); + } + } + + Entry f = dtm.floorEntry(now); + Entry c = dtm.ceilingEntry(now); + if (f != null) { + if (c != null) { + // we're during suntime! + double production = c.getValue(); + int interpolation = now.getMinute() - f.getKey().getMinute(); + double interpolationProduction = production * interpolation / 60; + forecastValue += interpolationProduction; + return forecastValue; + } else { + // sun is down + return forecastValue; + } + } else { + // no floor - sun not rised yet + return 0; + } + } + + public double getDayTotal(LocalDateTime now, int offset) { + LocalDate ld = now.plusDays(offset).toLocalDate(); + TreeMap dtm = dataMap.get(ld); + if (dtm != null) { + return getTotalValue(dtm); + } else { + return 0; + } + } + + public double getOptimisticDayTotal(LocalDateTime now, int offset) { + LocalDate ld = now.plusDays(offset).toLocalDate(); + TreeMap dtm = optimisticDataMap.get(ld); + if (dtm != null) { + return getTotalValue(dtm); + } else { + return 0; + } + } + + public double getPessimisticDayTotal(LocalDateTime now, int offset) { + LocalDate ld = now.plusDays(offset).toLocalDate(); + TreeMap dtm = pessimisticDataMap.get(ld); + if (dtm != null) { + return getTotalValue(dtm); + } else { + return 0; + } + } + + private double getTotalValue(TreeMap map) { + double forecastValue = 0; + Set keySet = map.keySet(); + for (LocalDateTime key : keySet) { + forecastValue += map.get(key); + } + return forecastValue; + } + + public double getRemainingProduction(LocalDateTime now) { + if (dataMap.isEmpty()) { + return UNDEF; + } + return getDayTotal(now, 0) - getActualValue(now); + } + + public static State getStateObject(double d) { + if (d < 0) { + return UnDefType.UNDEF; + } else { + return QuantityType.valueOf(d, Units.KILOWATT_HOUR); + } + } + + @Override + public String toString() { + return "Expiration: " + expirationDateTime + ", Valid: " + valid + ", Data:" + dataMap; + } + + public String getRaw() { + return rawData.get().toString(); + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java new file mode 100644 index 0000000000000..3bfa4e22e4b94 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java @@ -0,0 +1,168 @@ +/** + * Copyright (c) 2010-2022 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 static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; +import static org.openhab.binding.solarforecast.internal.solcast.SolcastConstants.*; + +import java.time.LocalDateTime; +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.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.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 { + private final Logger logger = LoggerFactory.getLogger(SolcastPlaneHandler.class); + private final HttpClient httpClient; + + private Optional configuration = Optional.empty(); + private Optional bridgeHandler = Optional.empty(); + private SolcastObject forecast = new SolcastObject(); + + public SolcastPlaneHandler(Thing thing, HttpClient hc) { + super(thing); + httpClient = hc; + } + + @Override + public void initialize() { + SolcastConfiguration c = getConfigAs(SolcastConfiguration.class); + configuration = Optional.of(c); + Bridge bridge = getBridge(); + if (bridge != null) { + BridgeHandler handler = bridge.getHandler(); + if (handler != null) { + if (handler instanceof SolcastBridgeHandler) { + bridgeHandler = Optional.of((SolcastBridgeHandler) handler); + bridgeHandler.get().addPlane(this); + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Wrong Handler " + handler); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, "BridgeHandler not found"); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, "Bridge not set"); + } + } + + @Override + public void dispose() { + super.dispose(); + if (bridgeHandler.isPresent()) { + bridgeHandler.get().removePlane(this); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.info("Handle command {} for channel {}", channelUID, command); + if (command instanceof RefreshType) { + fetchData(); + } + } + + /** + * https://doc.forecast.solar/doku.php?id=api:estimate + */ + protected SolcastObject fetchData() { + if (!forecast.isValid()) { + // get etsimate + String forecastUrl = String.format(FORECAST_URL, configuration.get().resourceId); + String currentEstimateUrl = String.format(CURRENT_ESTIMATE_URL, configuration.get().resourceId); + logger.trace("{} Call {}", thing.getLabel(), currentEstimateUrl); + try { + ContentResponse crEstimate = httpClient.GET(currentEstimateUrl); + if (crEstimate.getStatus() == 200) { + forecast = new SolcastObject(crEstimate.getContentAsString(), + LocalDateTime.now().plusMinutes(configuration.get().refreshInterval)); + logger.info("{} Fetched data {}", thing.getLabel(), forecast.toString()); + // get forecast + logger.info("{} Call {}", thing.getLabel(), forecastUrl); + ContentResponse crForecast = httpClient.GET(forecastUrl); + if (crForecast.getStatus() == 200) { + forecast.join(crForecast.getContentAsString()); + logger.info("{} Fetched data {}", thing.getLabel(), forecast.toString()); + updateChannels(forecast); + updateState(CHANNEL_RAW, StringType.valueOf(forecast.getRaw())); + } else { + logger.info("{} Call {} failed {}", thing.getLabel(), forecastUrl, crForecast.getStatus()); + } + updateChannels(forecast); + updateState(CHANNEL_RAW, StringType.valueOf(forecast.getRaw())); + } else { + logger.info("{} Call {} failed {}", thing.getLabel(), currentEstimateUrl, crEstimate.getStatus()); + } + } catch (InterruptedException | ExecutionException | TimeoutException e) { + logger.info("{} Call {} failed {}", thing.getLabel(), currentEstimateUrl, e.getMessage()); + } + } else { + logger.info("{} use available forecast {}", thing.getLabel(), forecast); + } + updateChannels(forecast); + return forecast; + } + + private void updateChannels(SolcastObject f) { + updateState(CHANNEL_ACTUAL, SolcastObject.getStateObject(f.getActualValue(LocalDateTime.now()))); + updateState(CHANNEL_REMAINING, SolcastObject.getStateObject(f.getRemainingProduction(LocalDateTime.now()))); + updateState(CHANNEL_TODAY, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 0))); + updateState(CHANNEL_TOMORROW, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 1))); + updateState(CHANNEL_TOMORROW_HIGH, + SolcastObject.getStateObject(f.getOptimisticDayTotal(LocalDateTime.now(), 1))); + updateState(CHANNEL_TOMORROW_LOW, + SolcastObject.getStateObject(f.getPessimisticDayTotal(LocalDateTime.now(), 1))); + updateState(CHANNEL_DAY2, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 2))); + updateState(CHANNEL_DAY2_HIGH, SolcastObject.getStateObject(f.getOptimisticDayTotal(LocalDateTime.now(), 2))); + updateState(CHANNEL_DAY2_LOW, SolcastObject.getStateObject(f.getPessimisticDayTotal(LocalDateTime.now(), 2))); + updateState(CHANNEL_DAY3, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 3))); + updateState(CHANNEL_DAY3_HIGH, SolcastObject.getStateObject(f.getOptimisticDayTotal(LocalDateTime.now(), 3))); + updateState(CHANNEL_DAY3_LOW, SolcastObject.getStateObject(f.getPessimisticDayTotal(LocalDateTime.now(), 3))); + updateState(CHANNEL_DAY4, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 4))); + updateState(CHANNEL_DAY4_HIGH, SolcastObject.getStateObject(f.getOptimisticDayTotal(LocalDateTime.now(), 4))); + updateState(CHANNEL_DAY4_LOW, SolcastObject.getStateObject(f.getPessimisticDayTotal(LocalDateTime.now(), 4))); + updateState(CHANNEL_DAY5, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 5))); + updateState(CHANNEL_DAY5_HIGH, SolcastObject.getStateObject(f.getOptimisticDayTotal(LocalDateTime.now(), 5))); + updateState(CHANNEL_DAY5_LOW, SolcastObject.getStateObject(f.getPessimisticDayTotal(LocalDateTime.now(), 5))); + updateState(CHANNEL_DAY6, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 6))); + updateState(CHANNEL_DAY6_HIGH, SolcastObject.getStateObject(f.getOptimisticDayTotal(LocalDateTime.now(), 6))); + updateState(CHANNEL_DAY6_LOW, SolcastObject.getStateObject(f.getPessimisticDayTotal(LocalDateTime.now(), 6))); + } + + public void setConfig(SolcastConfiguration config) { + configuration = Optional.of(config); + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastSinglePlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastSinglePlaneHandler.java new file mode 100644 index 0000000000000..562872b11ee50 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastSinglePlaneHandler.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2022 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.util.Optional; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarBridgeHandler; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.types.Command; + +/** + * The {@link ForecastSolarBridgeHandler} is a non active handler instance. It will be triggerer by the bridge. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class SolcastSinglePlaneHandler extends SolcastPlaneHandler { + private Optional> refreshJob = Optional.empty(); + + public SolcastSinglePlaneHandler(Thing thing, HttpClient httpClient) { + super(thing, httpClient); + } + + @Override + public void initialize() { + SolcastConfiguration config = getConfigAs(SolcastConfiguration.class); + super.setConfig(config); + startSchedule(1); + updateStatus(ThingStatus.ONLINE); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); + } + + private void startSchedule(int interval) { + refreshJob.ifPresentOrElse(job -> { + if (job.isCancelled()) { + refreshJob = Optional + .of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES)); + } // else - scheduler is already running! + }, () -> { + refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES)); + }); + } + + private void getData() { + super.fetchData(); + } + + @Override + public void dispose() { + super.dispose(); + refreshJob.ifPresent(job -> job.cancel(true)); + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/bridge-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-bridge-config.xml similarity index 73% rename from bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/bridge-config.xml rename to bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-bridge-config.xml index 4761f3bb519e0..81bb63f2c05d0 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/bridge-config.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-bridge-config.xml @@ -4,18 +4,13 @@ xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> - + location Location of Photovoltaic system AUTODETECT - - - Data refresh rate of forecast data - 1 - If you have a paid subscription plan diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/thing-part-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-part-config.xml similarity index 78% rename from bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/thing-part-config.xml rename to bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-part-config.xml index bd45f43e2dd8a..e4d1202376bf7 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/thing-part-config.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-part-config.xml @@ -4,7 +4,12 @@ xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> - + + + + Data refresh rate of forecast data + 30 + 0 for horizontal till 90 for vertical declination diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/thing-single-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-single-config.xml similarity index 94% rename from bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/thing-single-config.xml rename to bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-single-config.xml index f9e582882a463..f77872def4b79 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/thing-single-config.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-single-config.xml @@ -4,7 +4,7 @@ xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> - + location @@ -26,7 +26,7 @@ Data refresh rate of forecast data - 1 + 15 diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-part-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-part-config.xml new file mode 100644 index 0000000000000..0fc62e867ab83 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-part-config.xml @@ -0,0 +1,18 @@ + + + + + + + Data refresh rate of forecast data + 120 + + + + Resource Id from Solcast configuration + + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-single-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-single-config.xml new file mode 100644 index 0000000000000..d82a37f3c5132 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-single-config.xml @@ -0,0 +1,18 @@ + + + + + + + Data refresh rate of forecast data + 60 + + + + Resource Id from Solcast configuration + + + 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 index 219449a0dec9f..38a2220940d58 100644 --- 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 @@ -4,12 +4,6 @@ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - - Number:Energy - - Todays forecast in total - - Number:Energy @@ -22,10 +16,10 @@ Forecast till now (actual time) - + Number:Energy - - Tomorrows forecast in total + + Todays forecast in total @@ -33,4 +27,112 @@ Plain JSON response without conversions + + Number:Energy + + Tomorrows forecast in total + + + + Number:Energy + + Tomorrows pessimistic forecast + + + + Number:Energy + + Tomorrows optimistic forecast + + + + Number:Energy + + Day after tomorrow estimation + + + + Number:Energy + + Day after tomorrow pessimistic estimation + + + + Number:Energy + + Day after tomorrow optimistic estimation + + + + Number:Energy + + Day 3 estimation + + + + Number:Energy + + Day 3 pessimistic estimation + + + + Number:Energy + + Day 3 optimistic estimation + + + + Number:Energy + + Day 4 estimation + + + + Number:Energy + + Day 4 pessimistic estimation + + + + Number:Energy + + Day 4 optimistic estimation + + + + Number:Energy + + Day 5 estimation + + + + Number:Energy + + Day 5 pessimistic estimation + + + + Number:Energy + + Day 5 optimistic estimation + + + + Number:Energy + + Day 3 estimation + + + + Number:Energy + + Day 6 pessimistic estimation + + + + Number:Energy + + Day 6 optimistic estimation + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/bridge-type-multi.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-bridge-type-multi.xml similarity index 75% rename from bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/bridge-type-multi.xml rename to bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-bridge-type-multi.xml index 64a9ef08329ed..76f8ce19186aa 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/bridge-type-multi.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-bridge-type-multi.xml @@ -4,8 +4,8 @@ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - - + + Bridge holding multiple photovoltaic planes @@ -13,8 +13,10 @@ + + - + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/thing-type-part.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-plane-part.xml similarity index 75% rename from bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/thing-type-part.xml rename to bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-plane-part.xml index 7f93bfb6fc29e..13c98766396bd 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/thing-type-part.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-plane-part.xml @@ -4,12 +4,12 @@ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - + - + - + PV Plane as part of Multi Plane Bridge @@ -17,9 +17,11 @@ + + - + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/thing-type-single.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-plane-single.xml similarity index 78% rename from bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/thing-type-single.xml rename to bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-plane-single.xml index 58f7dc2620607..578dfc9a04f86 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/thing-type-single.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-plane-single.xml @@ -5,7 +5,7 @@ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - + One PV plane attached to your inverter @@ -13,9 +13,11 @@ + + - + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-bridge-type-multi.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-bridge-type-multi.xml new file mode 100644 index 0000000000000..e1e07b9d521df --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-bridge-type-multi.xml @@ -0,0 +1,35 @@ + + + + + + Bridge holding multiple photovoltaic planes + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-thing-type-part.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-thing-type-part.xml new file mode 100644 index 0000000000000..4693b1ec23783 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-thing-type-part.xml @@ -0,0 +1,42 @@ + + + + + + + + + + PV Plane as part of Multi Plane Bridge + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-thing-type-single.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-thing-type-single.xml new file mode 100644 index 0000000000000..262c676b28915 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-thing-type-single.xml @@ -0,0 +1,38 @@ + + + + + + PV Plane as part of Multi Plane Bridge + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 index e0fa761d8209c..b5bc05496298b 100644 --- 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 @@ -21,8 +21,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.Test; -import org.openhab.binding.solarforecast.internal.ForecastObject; -import org.openhab.core.library.types.PointType; +import org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarObject; +import org.openhab.binding.solarforecast.internal.solcast.SolcastObject; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.Units; import org.openhab.core.types.State; @@ -41,7 +41,7 @@ class ForecastSolarTest { void testForecastObject() { String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 23); - ForecastObject fo = new ForecastObject(content, now); + ForecastSolarObject fo = new ForecastSolarObject(content, now, now); assertEquals(49.431, fo.getActualValue(now), 0.001, "Current Production"); assertEquals(14.152, fo.getRemainingProduction(now), 0.001, "Current Production"); assertEquals(fo.getDayTotal(now, 0), fo.getActualValue(now) + fo.getRemainingProduction(now), 0.001, @@ -54,7 +54,7 @@ void testForecastObject() { void testInterpolation() { String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 0); - ForecastObject fo = new ForecastObject(content, now); + ForecastSolarObject fo = new ForecastSolarObject(content, now, now); double previousValue = 0; for (int i = 0; i < 60; i++) { now = now.plusMinutes(1); @@ -67,9 +67,9 @@ void testInterpolation() { void testForecastSum() { String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 23); - ForecastObject fo = new ForecastObject(content, now); + ForecastSolarObject fo = new ForecastSolarObject(content, now, now); QuantityType actual = QuantityType.valueOf(0, Units.KILOWATT_HOUR); - State st = ForecastObject.getStateObject(fo.getActualValue(now)); + State st = SolcastObject.getStateObject(fo.getActualValue(now)); assertTrue(st instanceof QuantityType); actual = actual.add((QuantityType) st); assertEquals(49.431, actual.floatValue(), 0.001, "Current Production"); @@ -79,17 +79,11 @@ void testForecastSum() { @Test void testErrorCases() { - ForecastObject fo = new ForecastObject(); + ForecastSolarObject fo = new ForecastSolarObject(); assertFalse(fo.isValid()); LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 23); assertEquals(-1.0, fo.getActualValue(now), 0.001, "Actual Production"); assertEquals(-1.0, fo.getDayTotal(now, 0), 0.001, "Today Production"); assertEquals(-1.0, fo.getRemainingProduction(now), 0.001, "Remaining Production"); } - - @Test - void testLocation() { - PointType pt = PointType.valueOf("1.234,9.876"); - System.out.println(pt); - } } 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 0000000000000..56938f52aaeb3 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastTest.java @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2010-2022 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.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.junit.jupiter.api.Test; +import org.openhab.binding.solarforecast.internal.solcast.SolcastObject; + +/** + * The {@link SolcastTest} tests responses from forecast solar website + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +class SolcastTest { + public static final String DATE_INPUT_PATTERN_STRING = "yyyy-MM-dd'T'HH:mm:ss"; + public static final DateTimeFormatter DATE_INPUT_PATTERN = DateTimeFormatter.ofPattern(DATE_INPUT_PATTERN_STRING); + + @Test + void testForecastObject() { + String dateTime = "2022-07-17T22:00:00.0000000Z"; + System.out.println(dateTime.substring(0, dateTime.lastIndexOf("."))); + dateTime = dateTime.substring(0, dateTime.lastIndexOf("T")); + LocalDate ld = LocalDate.parse(dateTime); + System.out.println(ld); + String content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 23); + SolcastObject scfo = new SolcastObject(content, now); + // System.out.println(scfo); + } + + @Test + void testForecastTreeMap() { + String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); + LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 23); + SolcastObject scfo = new SolcastObject(content, now); + // System.out.println(scfo); + System.out.println(scfo.getDayTotal(now, 0)); + System.out.println(scfo.getActualValue(now)); + } + + @Test + void testJoin() { + String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); + LocalDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23); + SolcastObject scfo = new SolcastObject(content, now); + System.out.println(scfo.getActualValue(now)); + content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + scfo.join(content); + System.out.println(scfo.getActualValue(now)); + System.out.println(scfo.getDayTotal(now, 0)); + } + + @Test + void testOptimisticPessimistic() { + String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); + LocalDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23); + SolcastObject scfo = new SolcastObject(content, now); + System.out.println(scfo.getActualValue(now)); + content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + scfo.join(content); + System.out.println("Forecast med " + scfo.getDayTotal(now, 2)); + System.out.println("Forecast pes " + scfo.getPessimisticDayTotal(now, 2)); + System.out.println("Forecast opt " + scfo.getOptimisticDayTotal(now, 2)); + } + + @Test + void testInavlid() { + String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); + LocalDateTime now = LocalDateTime.now(); + SolcastObject scfo = new SolcastObject(content, now); + System.out.println(scfo.getActualValue(now)); + content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + scfo.join(content); + System.out.println(scfo.getActualValue(now)); + System.out.println(scfo.getDayTotal(now, 0)); + } + + @Test + void testRawChannel() { + String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); + LocalDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23); + SolcastObject sco = new SolcastObject(content, now); + System.out.println(sco.getActualValue(now)); + content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + sco.join(content); + System.out.println("SCO Raw " + sco.getRaw()); + } + + @Test + void testJSONObject() { + JSONObject jo = new JSONObject(); + jo.put("a", 1); + System.out.println(jo); + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/test/resources/solarcast/estimated-actuals.json b/bundles/org.openhab.binding.solarforecast/src/test/resources/solcast/estimated-actuals.json similarity index 100% rename from bundles/org.openhab.binding.solarforecast/src/test/resources/solarcast/estimated-actuals.json rename to bundles/org.openhab.binding.solarforecast/src/test/resources/solcast/estimated-actuals.json diff --git a/bundles/org.openhab.binding.solarforecast/src/test/resources/solarcast/forecasts.json b/bundles/org.openhab.binding.solarforecast/src/test/resources/solcast/forecasts.json similarity index 100% rename from bundles/org.openhab.binding.solarforecast/src/test/resources/solarcast/forecasts.json rename to bundles/org.openhab.binding.solarforecast/src/test/resources/solcast/forecasts.json From 39556fe09857487842018524b34ca80cbdbe32fa Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sun, 24 Jul 2022 15:57:52 +0200 Subject: [PATCH 005/111] solcast Bearer auth Signed-off-by: Bernd Weymann --- .../ForecastSolarBridgeHandler.java | 1 - .../ForecastSolarPlaneHandler.java | 5 +- .../solcast/SolcastBridgeHandler.java | 23 +- .../solcast/SolcastConfiguration.java | 1 + .../internal/solcast/SolcastConstants.java | 1 + .../internal/solcast/SolcastObject.java | 8 +- .../internal/solcast/SolcastPlaneHandler.java | 23 +- .../OH-INF/config/sc-bridge-config.xml | 13 + .../resources/OH-INF/thing/channel-types.xml | 4 +- .../OH-INF/thing/sc-bridge-type-multi.xml | 1 + .../binding/solarforecast/SolcastTest.java | 17 +- .../src/test/resources/solcast/NE.json | 4023 +++++++++++++++++ 12 files changed, 4097 insertions(+), 23 deletions(-) create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-bridge-config.xml create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/resources/solcast/NE.json diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java index 7fc7660379809..6046d465a9bb2 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java @@ -114,7 +114,6 @@ private synchronized void getData() { logger.info("Fetched data not valid {}", fo.toString()); } } - logger.info("Remain: {}", remainSum); updateState(CHANNEL_REMAINING, SolcastObject.getStateObject(remainSum)); updateState(CHANNEL_ACTUAL, SolcastObject.getStateObject(actualSum)); updateState(CHANNEL_TODAY, SolcastObject.getStateObject(todaySum)); diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java index 4fb953e64f600..527ce77a35453 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java @@ -125,8 +125,7 @@ protected ForecastSolarObject fetchData() { if (cr.getStatus() == 200) { forecast = new ForecastSolarObject(cr.getContentAsString(), LocalDateTime.now(), LocalDateTime.now().plusMinutes(configuration.get().refreshInterval)); - logger.info("{} Fetched data {}", thing.getLabel(), forecast.toString()); - logger.info("{} Valid after creation? {}", thing.getLabel(), forecast.isValid()); + logger.debug("{} Fetched data {}", thing.getLabel(), forecast.toString()); updateChannels(forecast); updateState(CHANNEL_RAW, StringType.valueOf(cr.getContentAsString())); } else { @@ -136,7 +135,7 @@ protected ForecastSolarObject fetchData() { logger.info("{} Call {} failed {}", thing.getLabel(), url, e.getMessage()); } } else { - logger.info("{} use available forecast {}", thing.getLabel(), forecast); + logger.debug("{} use available forecast {}", thing.getLabel(), forecast); } updateChannels(forecast); } else { diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java index 58b88df594774..5ce02c9e0e880 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java @@ -26,6 +26,7 @@ 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.types.Command; import org.slf4j.Logger; @@ -41,6 +42,7 @@ public class SolcastBridgeHandler extends BaseBridgeHandler { private final Logger logger = LoggerFactory.getLogger(SolcastBridgeHandler.class); private List parts = new ArrayList(); + private Optional configuration = Optional.empty(); private Optional> refreshJob = Optional.empty(); public SolcastBridgeHandler(Bridge bridge) { @@ -49,10 +51,16 @@ public SolcastBridgeHandler(Bridge bridge) { @Override public void initialize() { - // nothing to initialze - simply start scheduling - updateStatus(ThingStatus.ONLINE); - getData(); - startSchedule(1); + SolcastConfiguration config = getConfigAs(SolcastConfiguration.class); + configuration = Optional.of(config); + if (!EMPTY.equals(config.apiKey)) { + updateStatus(ThingStatus.ONLINE); + getData(); + startSchedule(1); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "API Key is mandatory"); + logger.info("API Key missing"); + } } @Override @@ -166,4 +174,11 @@ synchronized void addPlane(SolcastPlaneHandler sph) { synchronized void removePlane(SolcastPlaneHandler sph) { parts.remove(sph); } + + String getApiKey() { + if (configuration.isPresent()) { + return configuration.get().apiKey; + } + return EMPTY; + } } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastConfiguration.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastConfiguration.java index 3c1fbda6d01fd..2e0712ee51de0 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastConfiguration.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastConfiguration.java @@ -23,5 +23,6 @@ @NonNullByDefault public class SolcastConfiguration { public int refreshInterval = -1; + public String apiKey = SolarForecastBindingConstants.EMPTY; public String resourceId = SolarForecastBindingConstants.EMPTY; } 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 index 48062d7ccaa0e..5066a867105d9 100644 --- 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 @@ -24,4 +24,5 @@ public class SolcastConstants { public static final String FORECAST_URL = "https://api.solcast.com.au/rooftop_sites/%s/forecasts?format=json"; public static final String CURRENT_ESTIMATE_URL = "https://api.solcast.com.au/rooftop_sites/%s/estimated_actuals?format=json"; + public static final String BEARER = "Bearer "; } 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 index 371b5fb6be66c..5bbef9a0ac3f7 100644 --- 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 @@ -150,7 +150,9 @@ public double getActualValue(LocalDateTime now) { Set keySet = dtm.keySet(); for (LocalDateTime key : keySet) { if (key.isBefore(now)) { - forecastValue += dtm.get(key); + // value are reported in PT30M = 30 minutes interval with kw value + // for kw/h it's half the value + forecastValue += dtm.get(key) / 2; } } @@ -208,7 +210,9 @@ private double getTotalValue(TreeMap map) { double forecastValue = 0; Set keySet = map.keySet(); for (LocalDateTime key : keySet) { - forecastValue += map.get(key); + // value are reported in PT30M = 30 minutes interval with kw value + // for kw/h it's half the value + forecastValue += map.get(key) / 2; } return forecastValue; } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java index 3bfa4e22e4b94..38bf7c86239bd 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java @@ -23,6 +23,8 @@ 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.core.library.types.StringType; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; @@ -100,22 +102,28 @@ public void handleCommand(ChannelUID channelUID, Command command) { */ protected SolcastObject fetchData() { if (!forecast.isValid()) { - // get etsimate String forecastUrl = String.format(FORECAST_URL, configuration.get().resourceId); String currentEstimateUrl = String.format(CURRENT_ESTIMATE_URL, configuration.get().resourceId); - logger.trace("{} Call {}", thing.getLabel(), currentEstimateUrl); + logger.info("{} Call {}", thing.getLabel(), currentEstimateUrl); try { - ContentResponse crEstimate = httpClient.GET(currentEstimateUrl); + // get actual estimate + Request estimateRequest = httpClient.newRequest(currentEstimateUrl); + estimateRequest.header(HttpHeader.AUTHORIZATION, BEARER + bridgeHandler.get().getApiKey()); + ContentResponse crEstimate = estimateRequest.send(); if (crEstimate.getStatus() == 200) { forecast = new SolcastObject(crEstimate.getContentAsString(), LocalDateTime.now().plusMinutes(configuration.get().refreshInterval)); - logger.info("{} Fetched data {}", thing.getLabel(), forecast.toString()); + logger.trace("{} Fetched data {}", thing.getLabel(), forecast.toString()); + // get forecast logger.info("{} Call {}", thing.getLabel(), forecastUrl); - ContentResponse crForecast = httpClient.GET(forecastUrl); + Request forecastRequest = httpClient.newRequest(forecastUrl); + forecastRequest.header(HttpHeader.AUTHORIZATION, BEARER + bridgeHandler.get().getApiKey()); + ContentResponse crForecast = forecastRequest.send(); + if (crForecast.getStatus() == 200) { forecast.join(crForecast.getContentAsString()); - logger.info("{} Fetched data {}", thing.getLabel(), forecast.toString()); + logger.trace("{} Fetched data {}", thing.getLabel(), forecast.toString()); updateChannels(forecast); updateState(CHANNEL_RAW, StringType.valueOf(forecast.getRaw())); } else { @@ -130,7 +138,7 @@ protected SolcastObject fetchData() { logger.info("{} Call {} failed {}", thing.getLabel(), currentEstimateUrl, e.getMessage()); } } else { - logger.info("{} use available forecast {}", thing.getLabel(), forecast); + logger.debug("{} use available forecast {}", thing.getLabel(), forecast); } updateChannels(forecast); return forecast; @@ -160,6 +168,7 @@ private void updateChannels(SolcastObject f) { updateState(CHANNEL_DAY6, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 6))); updateState(CHANNEL_DAY6_HIGH, SolcastObject.getStateObject(f.getOptimisticDayTotal(LocalDateTime.now(), 6))); updateState(CHANNEL_DAY6_LOW, SolcastObject.getStateObject(f.getPessimisticDayTotal(LocalDateTime.now(), 6))); + updateState(CHANNEL_RAW, StringType.valueOf(forecast.getRaw())); } public void setConfig(SolcastConfiguration config) { diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-bridge-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-bridge-config.xml new file mode 100644 index 0000000000000..58291b19f1915 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-bridge-config.xml @@ -0,0 +1,13 @@ + + + + + + + API Key from your subscription + + + 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 index 38a2220940d58..3a782931ac0aa 100644 --- 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 @@ -119,8 +119,8 @@ Number:Energy - - Day 3 estimation + + Day 6 estimation diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-bridge-type-multi.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-bridge-type-multi.xml index e1e07b9d521df..5278ec47feaee 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-bridge-type-multi.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-bridge-type-multi.xml @@ -31,5 +31,6 @@ + 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 index 56938f52aaeb3..598f7efe2e3ba 100644 --- 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 @@ -103,9 +103,18 @@ void testRawChannel() { } @Test - void testJSONObject() { - JSONObject jo = new JSONObject(); - jo.put("a", 1); - System.out.println(jo); + void testRoofNE() { + String content = FileReader.readFileInString("src/test/resources/solcast/NE.json"); + LocalDateTime now = LocalDateTime.now(); + JSONObject act = new JSONObject(content); + JSONObject actual = new JSONObject(); + actual.put("estimated_actuals", act.getJSONArray("estimated_actuals")); + SolcastObject scfo = new SolcastObject(actual.toString(), now); + System.out.println(scfo.getActualValue(now)); + + JSONObject forecasts = new JSONObject(); + forecasts.put("forecasts", act.getJSONArray("forecasts")); + scfo.join(forecasts.toString()); + System.out.println(scfo.getActualValue(now)); } } diff --git a/bundles/org.openhab.binding.solarforecast/src/test/resources/solcast/NE.json b/bundles/org.openhab.binding.solarforecast/src/test/resources/solcast/NE.json new file mode 100644 index 0000000000000..71272e07ef153 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/resources/solcast/NE.json @@ -0,0 +1,4023 @@ +{ + "estimated_actuals": [ + { + "period_end": "2022-07-24T12:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.3467 + }, + { + "period_end": "2022-07-24T12:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.4376 + }, + { + "period_end": "2022-07-24T11:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.4888 + }, + { + "period_end": "2022-07-24T11:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.4877 + }, + { + "period_end": "2022-07-24T10:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.487 + }, + { + "period_end": "2022-07-24T10:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.4675 + }, + { + "period_end": "2022-07-24T09:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.4038 + }, + { + "period_end": "2022-07-24T09:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.3233 + }, + { + "period_end": "2022-07-24T08:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.2117 + }, + { + "period_end": "2022-07-24T08:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.0763 + }, + { + "period_end": "2022-07-24T07:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.8913 + }, + { + "period_end": "2022-07-24T07:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.6715 + }, + { + "period_end": "2022-07-24T06:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.1027 + }, + { + "period_end": "2022-07-24T06:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.7972 + }, + { + "period_end": "2022-07-24T05:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.5099 + }, + { + "period_end": "2022-07-24T05:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.2235 + }, + { + "period_end": "2022-07-24T04:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0654 + }, + { + "period_end": "2022-07-24T04:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0023 + }, + { + "period_end": "2022-07-24T03:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-24T03:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-24T02:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-24T02:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-24T01:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-24T01:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-24T00:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-24T00:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-23T23:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-23T23:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-23T22:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-23T22:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-23T21:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-23T21:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-23T20:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-23T20:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-23T19:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.014 + }, + { + "period_end": "2022-07-23T19:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0657 + }, + { + "period_end": "2022-07-23T18:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.1625 + }, + { + "period_end": "2022-07-23T18:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.3704 + }, + { + "period_end": "2022-07-23T17:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.5914 + }, + { + "period_end": "2022-07-23T17:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.7602 + }, + { + "period_end": "2022-07-23T16:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.9932 + }, + { + "period_end": "2022-07-23T16:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.1795 + }, + { + "period_end": "2022-07-23T15:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.3805 + }, + { + "period_end": "2022-07-23T15:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.5629 + }, + { + "period_end": "2022-07-23T14:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.7041 + }, + { + "period_end": "2022-07-23T14:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.7898 + }, + { + "period_end": "2022-07-23T13:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.0421 + }, + { + "period_end": "2022-07-23T13:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.0732 + }, + { + "period_end": "2022-07-23T12:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.1193 + }, + { + "period_end": "2022-07-23T12:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.1525 + }, + { + "period_end": "2022-07-23T11:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.1135 + }, + { + "period_end": "2022-07-23T11:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.2488 + }, + { + "period_end": "2022-07-23T10:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.2079 + }, + { + "period_end": "2022-07-23T10:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.2887 + }, + { + "period_end": "2022-07-23T09:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.3472 + }, + { + "period_end": "2022-07-23T09:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.2443 + }, + { + "period_end": "2022-07-23T08:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.1171 + }, + { + "period_end": "2022-07-23T08:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.6259 + }, + { + "period_end": "2022-07-23T07:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.035 + }, + { + "period_end": "2022-07-23T07:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.1865 + }, + { + "period_end": "2022-07-23T06:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.0569 + }, + { + "period_end": "2022-07-23T06:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.6812 + }, + { + "period_end": "2022-07-23T05:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.3647 + }, + { + "period_end": "2022-07-23T05:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.1986 + }, + { + "period_end": "2022-07-23T04:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0159 + }, + { + "period_end": "2022-07-23T04:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-23T03:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-23T03:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-23T02:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-23T02:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-23T01:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-23T01:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-23T00:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-23T00:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-22T23:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-22T23:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-22T22:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-22T22:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-22T21:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-22T21:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-22T20:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-22T20:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-22T19:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-22T19:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.044 + }, + { + "period_end": "2022-07-22T18:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0729 + }, + { + "period_end": "2022-07-22T18:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.2943 + }, + { + "period_end": "2022-07-22T17:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.5305 + }, + { + "period_end": "2022-07-22T17:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.7256 + }, + { + "period_end": "2022-07-22T16:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.9587 + }, + { + "period_end": "2022-07-22T16:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.1872 + }, + { + "period_end": "2022-07-22T15:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.4061 + }, + { + "period_end": "2022-07-22T15:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.6205 + }, + { + "period_end": "2022-07-22T14:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.7181 + }, + { + "period_end": "2022-07-22T14:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.8627 + }, + { + "period_end": "2022-07-22T13:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.9427 + }, + { + "period_end": "2022-07-22T13:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.1803 + }, + { + "period_end": "2022-07-22T12:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.3676 + }, + { + "period_end": "2022-07-22T12:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.445 + }, + { + "period_end": "2022-07-22T11:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.5272 + }, + { + "period_end": "2022-07-22T11:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.5222 + }, + { + "period_end": "2022-07-22T10:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.5315 + }, + { + "period_end": "2022-07-22T10:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.5122 + }, + { + "period_end": "2022-07-22T09:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.4772 + }, + { + "period_end": "2022-07-22T09:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.3909 + }, + { + "period_end": "2022-07-22T08:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.2789 + }, + { + "period_end": "2022-07-22T08:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.9704 + }, + { + "period_end": "2022-07-22T07:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.6186 + }, + { + "period_end": "2022-07-22T07:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.1127 + }, + { + "period_end": "2022-07-22T06:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.4484 + }, + { + "period_end": "2022-07-22T06:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.3004 + }, + { + "period_end": "2022-07-22T05:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.1597 + }, + { + "period_end": "2022-07-22T05:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0836 + }, + { + "period_end": "2022-07-22T04:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0748 + }, + { + "period_end": "2022-07-22T04:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0023 + }, + { + "period_end": "2022-07-22T03:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-22T03:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-22T02:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-22T02:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-22T01:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-22T01:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-22T00:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-22T00:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-21T23:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-21T23:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-21T22:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-21T22:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-21T21:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-21T21:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-21T20:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-21T20:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-21T19:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0141 + }, + { + "period_end": "2022-07-21T19:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0659 + }, + { + "period_end": "2022-07-21T18:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.1362 + }, + { + "period_end": "2022-07-21T18:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.2017 + }, + { + "period_end": "2022-07-21T17:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.2276 + }, + { + "period_end": "2022-07-21T17:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.3903 + }, + { + "period_end": "2022-07-21T16:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.513 + }, + { + "period_end": "2022-07-21T16:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.4306 + }, + { + "period_end": "2022-07-21T15:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.4328 + }, + { + "period_end": "2022-07-21T15:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.71 + }, + { + "period_end": "2022-07-21T14:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.6313 + }, + { + "period_end": "2022-07-21T14:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.0995 + }, + { + "period_end": "2022-07-21T13:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.3991 + }, + { + "period_end": "2022-07-21T13:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.7188 + }, + { + "period_end": "2022-07-21T12:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.3481 + }, + { + "period_end": "2022-07-21T12:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.1533 + }, + { + "period_end": "2022-07-21T11:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.2648 + }, + { + "period_end": "2022-07-21T11:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.8648 + }, + { + "period_end": "2022-07-21T10:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.2882 + }, + { + "period_end": "2022-07-21T10:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.2078 + }, + { + "period_end": "2022-07-21T09:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.0309 + }, + { + "period_end": "2022-07-21T09:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.0067 + }, + { + "period_end": "2022-07-21T08:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.1591 + }, + { + "period_end": "2022-07-21T08:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.4075 + }, + { + "period_end": "2022-07-21T07:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.178 + }, + { + "period_end": "2022-07-21T07:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.2128 + }, + { + "period_end": "2022-07-21T06:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.5251 + }, + { + "period_end": "2022-07-21T06:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.7555 + }, + { + "period_end": "2022-07-21T05:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.5044 + }, + { + "period_end": "2022-07-21T05:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0906 + }, + { + "period_end": "2022-07-21T04:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0266 + }, + { + "period_end": "2022-07-21T04:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-21T03:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-21T03:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-21T02:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-21T02:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-21T01:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-21T01:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-21T00:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-21T00:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-20T23:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-20T23:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-20T22:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-20T22:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-20T21:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-20T21:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-20T20:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-20T20:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-20T19:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0105 + }, + { + "period_end": "2022-07-20T19:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0417 + }, + { + "period_end": "2022-07-20T18:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0801 + }, + { + "period_end": "2022-07-20T18:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0801 + }, + { + "period_end": "2022-07-20T17:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.4001 + }, + { + "period_end": "2022-07-20T17:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.4913 + }, + { + "period_end": "2022-07-20T16:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.7275 + }, + { + "period_end": "2022-07-20T16:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.1724 + }, + { + "period_end": "2022-07-20T15:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.3813 + }, + { + "period_end": "2022-07-20T15:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.5826 + }, + { + "period_end": "2022-07-20T14:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.7688 + }, + { + "period_end": "2022-07-20T14:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.9328 + }, + { + "period_end": "2022-07-20T13:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.0633 + }, + { + "period_end": "2022-07-20T13:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.1855 + }, + { + "period_end": "2022-07-20T12:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.2809 + }, + { + "period_end": "2022-07-20T12:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.3597 + }, + { + "period_end": "2022-07-20T11:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.4296 + }, + { + "period_end": "2022-07-20T11:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.4367 + }, + { + "period_end": "2022-07-20T10:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.2416 + }, + { + "period_end": "2022-07-20T10:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.0957 + }, + { + "period_end": "2022-07-20T09:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.3884 + }, + { + "period_end": "2022-07-20T09:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.3178 + }, + { + "period_end": "2022-07-20T08:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.2075 + }, + { + "period_end": "2022-07-20T08:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.0753 + }, + { + "period_end": "2022-07-20T06:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.5011 + }, + { + "period_end": "2022-07-20T06:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.2381 + }, + { + "period_end": "2022-07-20T05:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.9805 + }, + { + "period_end": "2022-07-20T05:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.574 + }, + { + "period_end": "2022-07-20T04:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.2516 + }, + { + "period_end": "2022-07-20T04:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0192 + }, + { + "period_end": "2022-07-20T03:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-20T03:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-20T02:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-20T02:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-20T01:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-20T01:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-20T00:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-20T00:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-19T23:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-19T23:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-19T22:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-19T22:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-19T21:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-19T21:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-19T20:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-19T20:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-19T19:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0231 + }, + { + "period_end": "2022-07-19T19:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0899 + }, + { + "period_end": "2022-07-19T18:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.2433 + }, + { + "period_end": "2022-07-19T18:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.4328 + }, + { + "period_end": "2022-07-19T17:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.6136 + }, + { + "period_end": "2022-07-19T17:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.8177 + }, + { + "period_end": "2022-07-19T16:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.0034 + }, + { + "period_end": "2022-07-19T16:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.2156 + }, + { + "period_end": "2022-07-19T15:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.4317 + }, + { + "period_end": "2022-07-19T15:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.6306 + }, + { + "period_end": "2022-07-19T14:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.8022 + }, + { + "period_end": "2022-07-19T14:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.9595 + }, + { + "period_end": "2022-07-19T13:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.0827 + }, + { + "period_end": "2022-07-19T13:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.1966 + }, + { + "period_end": "2022-07-19T12:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.2923 + }, + { + "period_end": "2022-07-19T12:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.351 + }, + { + "period_end": "2022-07-19T11:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.4274 + }, + { + "period_end": "2022-07-19T11:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.4127 + }, + { + "period_end": "2022-07-19T10:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.4196 + }, + { + "period_end": "2022-07-19T10:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.389 + }, + { + "period_end": "2022-07-19T09:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.3427 + }, + { + "period_end": "2022-07-19T09:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.2588 + }, + { + "period_end": "2022-07-19T08:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.1485 + }, + { + "period_end": "2022-07-19T08:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.0083 + }, + { + "period_end": "2022-07-19T07:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.8397 + }, + { + "period_end": "2022-07-19T07:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.6237 + }, + { + "period_end": "2022-07-19T06:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.3969 + }, + { + "period_end": "2022-07-19T06:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.1083 + }, + { + "period_end": "2022-07-19T05:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.7846 + }, + { + "period_end": "2022-07-19T05:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.5003 + }, + { + "period_end": "2022-07-19T04:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.1598 + }, + { + "period_end": "2022-07-19T04:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0173 + }, + { + "period_end": "2022-07-19T03:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-19T03:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-19T02:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-19T02:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-19T01:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-19T01:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-19T00:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-19T00:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-18T23:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-18T23:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-18T22:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-18T22:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-18T21:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-18T21:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-18T20:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-18T20:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-18T19:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0237 + }, + { + "period_end": "2022-07-18T19:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.093 + }, + { + "period_end": "2022-07-18T18:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.2511 + }, + { + "period_end": "2022-07-18T18:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.4379 + }, + { + "period_end": "2022-07-18T17:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.6213 + }, + { + "period_end": "2022-07-18T17:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.8362 + }, + { + "period_end": "2022-07-18T16:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.0591 + }, + { + "period_end": "2022-07-18T16:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.2492 + }, + { + "period_end": "2022-07-18T15:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.4614 + }, + { + "period_end": "2022-07-18T15:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.669 + }, + { + "period_end": "2022-07-18T14:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.8463 + }, + { + "period_end": "2022-07-18T14:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.0041 + }, + { + "period_end": "2022-07-18T13:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.1422 + }, + { + "period_end": "2022-07-18T13:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.2634 + }, + { + "period_end": "2022-07-18T12:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.3734 + }, + { + "period_end": "2022-07-18T12:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.4392 + }, + { + "period_end": "2022-07-18T11:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.5012 + }, + { + "period_end": "2022-07-18T11:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.4912 + }, + { + "period_end": "2022-07-18T10:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.5001 + }, + { + "period_end": "2022-07-18T10:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.4844 + }, + { + "period_end": "2022-07-18T09:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.4319 + }, + { + "period_end": "2022-07-18T09:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.3503 + }, + { + "period_end": "2022-07-18T08:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.2488 + }, + { + "period_end": "2022-07-18T08:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.1001 + }, + { + "period_end": "2022-07-18T07:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.911 + }, + { + "period_end": "2022-07-18T07:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.7075 + }, + { + "period_end": "2022-07-18T06:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.4574 + }, + { + "period_end": "2022-07-18T06:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.1741 + }, + { + "period_end": "2022-07-18T05:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.8316 + }, + { + "period_end": "2022-07-18T05:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.5323 + }, + { + "period_end": "2022-07-18T04:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.2142 + }, + { + "period_end": "2022-07-18T04:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0202 + }, + { + "period_end": "2022-07-18T03:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-18T03:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-18T02:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-18T02:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-18T01:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-18T01:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-18T00:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-18T00:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-17T23:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-17T23:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-17T22:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-17T22:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0 + }, + { + "period_end": "2022-07-17T21:30:00.0000000Z", + "period": "PT30M", + "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 + }, + { + "period_end": "2022-07-17T19:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0132 + }, + { + "period_end": "2022-07-17T19:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.0848 + }, + { + "period_end": "2022-07-17T18:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.2543 + }, + { + "period_end": "2022-07-17T18:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.4465 + }, + { + "period_end": "2022-07-17T17:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.6164 + }, + { + "period_end": "2022-07-17T17:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 0.8581 + }, + { + "period_end": "2022-07-17T16:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.0839 + }, + { + "period_end": "2022-07-17T16:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.2834 + }, + { + "period_end": "2022-07-17T15:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.5031 + }, + { + "period_end": "2022-07-17T15:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.724 + }, + { + "period_end": "2022-07-17T14:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 1.9318 + }, + { + "period_end": "2022-07-17T14:00:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.1015 + }, + { + "period_end": "2022-07-17T13:30:00.0000000Z", + "period": "PT30M", + "pv_estimate": 2.2374 + } + ], + "forecasts": [ + { + "period_end": "2022-07-24T13:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.1321, + "pv_estimate": 2.1321, + "pv_estimate10": 2.0481 + }, + { + "period_end": "2022-07-24T14:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.9722, + "pv_estimate": 1.9722, + "pv_estimate10": 1.8916 + }, + { + "period_end": "2022-07-24T14:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.8016, + "pv_estimate": 1.8016, + "pv_estimate10": 1.73 + }, + { + "period_end": "2022-07-24T15:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.6187, + "pv_estimate": 1.614, + "pv_estimate10": 1.5505 + }, + { + "period_end": "2022-07-24T15:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.4041, + "pv_estimate": 1.3883, + "pv_estimate10": 1.3572 + }, + { + "period_end": "2022-07-24T16:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.1857, + "pv_estimate": 1.1679, + "pv_estimate10": 1.1497 + }, + { + "period_end": "2022-07-24T16:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.9902, + "pv_estimate": 0.9767, + "pv_estimate10": 0.9619 + }, + { + "period_end": "2022-07-24T17:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.7437, + "pv_estimate": 0.7432, + "pv_estimate10": 0.704 + }, + { + "period_end": "2022-07-24T17:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.564795, + "pv_estimate": 0.5379, + "pv_estimate10": 0.5027 + }, + { + "period_end": "2022-07-24T18:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.35280000000000006, + "pv_estimate": 0.336, + "pv_estimate10": 0.3074 + }, + { + "period_end": "2022-07-24T18:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.17986500000000002, + "pv_estimate": 0.1713, + "pv_estimate10": 0.1369 + }, + { + "period_end": "2022-07-24T19:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.0642, + "pv_estimate": 0.0617, + "pv_estimate10": 0.0507 + }, + { + "period_end": "2022-07-24T19:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.00441, + "pv_estimate": 0.0042, + "pv_estimate10": 0.0021 + }, + { + "period_end": "2022-07-24T20:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-24T20:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-24T21:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-24T21:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-24T22:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-24T22:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-24T23:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-24T23:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-25T00:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-25T00:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-25T01:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-25T01:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-25T02:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-25T02:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-25T03:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-25T03:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-25T04:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-25T04:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.1608, + "pv_estimate": 0.1608, + "pv_estimate10": 0.0697 + }, + { + "period_end": "2022-07-25T05:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.5596, + "pv_estimate": 0.5596, + "pv_estimate10": 0.2133 + }, + { + "period_end": "2022-07-25T05:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.8647, + "pv_estimate": 0.8647, + "pv_estimate10": 0.4462 + }, + { + "period_end": "2022-07-25T06:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.2206, + "pv_estimate": 1.2206, + "pv_estimate10": 0.6935 + }, + { + "period_end": "2022-07-25T06:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.4745, + "pv_estimate": 1.4745, + "pv_estimate10": 0.934 + }, + { + "period_end": "2022-07-25T07:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.6943, + "pv_estimate": 1.6943, + "pv_estimate10": 1.1397 + }, + { + "period_end": "2022-07-25T07:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.9227, + "pv_estimate": 1.9227, + "pv_estimate10": 1.2972 + }, + { + "period_end": "2022-07-25T08:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.0759, + "pv_estimate": 2.0759, + "pv_estimate10": 1.4616 + }, + { + "period_end": "2022-07-25T08:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.2044, + "pv_estimate": 2.2044, + "pv_estimate10": 1.5608 + }, + { + "period_end": "2022-07-25T09:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.3179, + "pv_estimate": 2.3179, + "pv_estimate10": 1.682 + }, + { + "period_end": "2022-07-25T09:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.3743, + "pv_estimate": 2.3301, + "pv_estimate10": 1.6771 + }, + { + "period_end": "2022-07-25T10:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.4101, + "pv_estimate": 2.2845, + "pv_estimate10": 1.6089 + }, + { + "period_end": "2022-07-25T10:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.4287, + "pv_estimate": 2.2396, + "pv_estimate10": 1.5469 + }, + { + "period_end": "2022-07-25T11:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.4001, + "pv_estimate": 2.1727, + "pv_estimate10": 1.4196 + }, + { + "period_end": "2022-07-25T11:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.3508, + "pv_estimate": 2.0999, + "pv_estimate10": 1.2781 + }, + { + "period_end": "2022-07-25T12:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.2839, + "pv_estimate": 2.0046, + "pv_estimate10": 1.1158 + }, + { + "period_end": "2022-07-25T12:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.1987, + "pv_estimate": 1.8915, + "pv_estimate10": 0.9529 + }, + { + "period_end": "2022-07-25T13:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.0767, + "pv_estimate": 1.733, + "pv_estimate10": 0.7843 + }, + { + "period_end": "2022-07-25T13:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.9512, + "pv_estimate": 1.5699, + "pv_estimate10": 0.6383 + }, + { + "period_end": "2022-07-25T14:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.806, + "pv_estimate": 1.3873, + "pv_estimate10": 0.4946 + }, + { + "period_end": "2022-07-25T14:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.6276, + "pv_estimate": 1.1822, + "pv_estimate10": 0.3538 + }, + { + "period_end": "2022-07-25T15:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.4165, + "pv_estimate": 0.9462, + "pv_estimate10": 0.2048 + }, + { + "period_end": "2022-07-25T15:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.2499, + "pv_estimate": 0.8183, + "pv_estimate10": 0.1501 + }, + { + "period_end": "2022-07-25T16:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.0486, + "pv_estimate": 0.7015, + "pv_estimate10": 0.1378 + }, + { + "period_end": "2022-07-25T16:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.8492, + "pv_estimate": 0.5834, + "pv_estimate10": 0.1175 + }, + { + "period_end": "2022-07-25T17:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.6776, + "pv_estimate": 0.4387, + "pv_estimate10": 0.0889 + }, + { + "period_end": "2022-07-25T17:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.4669, + "pv_estimate": 0.2939, + "pv_estimate10": 0.0678 + }, + { + "period_end": "2022-07-25T18:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.2729, + "pv_estimate": 0.1554, + "pv_estimate10": 0.0411 + }, + { + "period_end": "2022-07-25T18:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.1183, + "pv_estimate": 0.0747, + "pv_estimate10": 0.023 + }, + { + "period_end": "2022-07-25T19:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.0454, + "pv_estimate": 0.0318, + "pv_estimate10": 0.0085 + }, + { + "period_end": "2022-07-25T19:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.0022, + "pv_estimate": 0.0022, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-25T20:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-25T20:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-25T21:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-25T21:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-25T22:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-25T22:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-25T23:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-25T23:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-26T00:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-26T00:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-26T01:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-26T01:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-26T02:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-26T02:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-26T03:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-26T03:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-26T04:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-26T04:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.1876, + "pv_estimate": 0.1195, + "pv_estimate10": 0.0229 + }, + { + "period_end": "2022-07-26T05:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.6319, + "pv_estimate": 0.46, + "pv_estimate10": 0.0641 + }, + { + "period_end": "2022-07-26T05:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.0401, + "pv_estimate": 0.8309, + "pv_estimate10": 0.1385 + }, + { + "period_end": "2022-07-26T06:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.337, + "pv_estimate": 1.1301, + "pv_estimate10": 0.3017 + }, + { + "period_end": "2022-07-26T06:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.5961, + "pv_estimate": 1.3265, + "pv_estimate10": 0.4624 + }, + { + "period_end": "2022-07-26T07:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.8278, + "pv_estimate": 1.4362, + "pv_estimate10": 0.5508 + }, + { + "period_end": "2022-07-26T07:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.0533, + "pv_estimate": 1.5659, + "pv_estimate10": 0.6205 + }, + { + "period_end": "2022-07-26T08:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.2208, + "pv_estimate": 1.6314, + "pv_estimate10": 0.6814 + }, + { + "period_end": "2022-07-26T08:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.3663, + "pv_estimate": 1.7112, + "pv_estimate10": 0.7082 + }, + { + "period_end": "2022-07-26T09:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.4823, + "pv_estimate": 1.7301, + "pv_estimate10": 0.6835 + }, + { + "period_end": "2022-07-26T09:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.5539, + "pv_estimate": 1.8156, + "pv_estimate10": 0.7121 + }, + { + "period_end": "2022-07-26T10:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.6156, + "pv_estimate": 1.9505, + "pv_estimate10": 0.8481 + }, + { + "period_end": "2022-07-26T10:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.6448, + "pv_estimate": 2.0697, + "pv_estimate10": 0.9466 + }, + { + "period_end": "2022-07-26T11:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.636, + "pv_estimate": 2.1259, + "pv_estimate10": 1.0131 + }, + { + "period_end": "2022-07-26T11:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.6058, + "pv_estimate": 2.1694, + "pv_estimate10": 1.0897 + }, + { + "period_end": "2022-07-26T12:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.5458, + "pv_estimate": 2.1759, + "pv_estimate10": 1.1808 + }, + { + "period_end": "2022-07-26T12:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.4696, + "pv_estimate": 2.1602, + "pv_estimate10": 1.2341 + }, + { + "period_end": "2022-07-26T13:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.3542, + "pv_estimate": 2.1008, + "pv_estimate10": 1.2619 + }, + { + "period_end": "2022-07-26T13:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.204, + "pv_estimate": 2.0034, + "pv_estimate10": 1.2487 + }, + { + "period_end": "2022-07-26T14:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.0528, + "pv_estimate": 1.8902, + "pv_estimate10": 1.2335 + }, + { + "period_end": "2022-07-26T14:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.8771, + "pv_estimate": 1.7498, + "pv_estimate10": 1.1984 + }, + { + "period_end": "2022-07-26T15:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.6508, + "pv_estimate": 1.5584, + "pv_estimate10": 1.1046 + }, + { + "period_end": "2022-07-26T15:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.4533, + "pv_estimate": 1.3928, + "pv_estimate10": 1.0041 + }, + { + "period_end": "2022-07-26T16:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.2208, + "pv_estimate": 1.1784, + "pv_estimate10": 0.849 + }, + { + "period_end": "2022-07-26T16:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.9781, + "pv_estimate": 0.9574, + "pv_estimate10": 0.6839 + }, + { + "period_end": "2022-07-26T17:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.7789, + "pv_estimate": 0.7788, + "pv_estimate10": 0.5103 + }, + { + "period_end": "2022-07-26T17:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.59304, + "pv_estimate": 0.5648, + "pv_estimate10": 0.334 + }, + { + "period_end": "2022-07-26T18:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.367815, + "pv_estimate": 0.3503, + "pv_estimate10": 0.1904 + }, + { + "period_end": "2022-07-26T18:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.19026, + "pv_estimate": 0.1812, + "pv_estimate10": 0.0851 + }, + { + "period_end": "2022-07-26T19:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.05775, + "pv_estimate": 0.055, + "pv_estimate10": 0.0312 + }, + { + "period_end": "2022-07-26T19:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-26T20:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-26T20:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-26T21:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-26T21:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-26T22:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-26T22:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-26T23:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-26T23:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-27T00:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-27T00:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-27T01:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-27T01:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-27T02:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-27T02:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-27T03:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-27T03:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-27T04:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-27T04:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.132, + "pv_estimate": 0.0242, + "pv_estimate10": 0.0024 + }, + { + "period_end": "2022-07-27T05:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.4986, + "pv_estimate": 0.067, + "pv_estimate10": 0.0072 + }, + { + "period_end": "2022-07-27T05:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.9302, + "pv_estimate": 0.144, + "pv_estimate10": 0.0118 + }, + { + "period_end": "2022-07-27T06:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.2524, + "pv_estimate": 0.293, + "pv_estimate10": 0.0164 + }, + { + "period_end": "2022-07-27T06:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.5654, + "pv_estimate": 0.5137, + "pv_estimate10": 0.0347 + }, + { + "period_end": "2022-07-27T07:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.8768, + "pv_estimate": 0.8088, + "pv_estimate10": 0.0939 + }, + { + "period_end": "2022-07-27T07:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.0971, + "pv_estimate": 1.1373, + "pv_estimate10": 0.2415 + }, + { + "period_end": "2022-07-27T08:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.2741, + "pv_estimate": 1.4973, + "pv_estimate10": 0.4666 + }, + { + "period_end": "2022-07-27T08:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.4277, + "pv_estimate": 1.8229, + "pv_estimate10": 0.7225 + }, + { + "period_end": "2022-07-27T09:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.5326, + "pv_estimate": 2.0874, + "pv_estimate10": 0.9995 + }, + { + "period_end": "2022-07-27T09:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.6296, + "pv_estimate": 2.2142, + "pv_estimate10": 1.1417 + }, + { + "period_end": "2022-07-27T10:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.6676, + "pv_estimate": 2.2069, + "pv_estimate10": 1.1647 + }, + { + "period_end": "2022-07-27T10:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.6937, + "pv_estimate": 2.1865, + "pv_estimate10": 1.1483 + }, + { + "period_end": "2022-07-27T11:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.6851, + "pv_estimate": 2.1513, + "pv_estimate10": 1.0764 + }, + { + "period_end": "2022-07-27T11:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.6328, + "pv_estimate": 2.0606, + "pv_estimate10": 1.0213 + }, + { + "period_end": "2022-07-27T12:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.5289, + "pv_estimate": 1.9687, + "pv_estimate10": 0.918 + }, + { + "period_end": "2022-07-27T12:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.4397, + "pv_estimate": 1.884, + "pv_estimate10": 0.8708 + }, + { + "period_end": "2022-07-27T13:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.3524, + "pv_estimate": 1.8269, + "pv_estimate10": 0.8187 + }, + { + "period_end": "2022-07-27T13:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.2098, + "pv_estimate": 1.7439, + "pv_estimate10": 0.7745 + }, + { + "period_end": "2022-07-27T14:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.0459, + "pv_estimate": 1.6448, + "pv_estimate10": 0.7264 + }, + { + "period_end": "2022-07-27T14:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.8706, + "pv_estimate": 1.5134, + "pv_estimate10": 0.6651 + }, + { + "period_end": "2022-07-27T15:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.6721, + "pv_estimate": 1.381, + "pv_estimate10": 0.5686 + }, + { + "period_end": "2022-07-27T15:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.4681, + "pv_estimate": 1.2282, + "pv_estimate10": 0.503 + }, + { + "period_end": "2022-07-27T16:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.227, + "pv_estimate": 1.0871, + "pv_estimate10": 0.4683 + }, + { + "period_end": "2022-07-27T16:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.9844, + "pv_estimate": 0.9202, + "pv_estimate10": 0.3936 + }, + { + "period_end": "2022-07-27T17:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.7464, + "pv_estimate": 0.7328, + "pv_estimate10": 0.2994 + }, + { + "period_end": "2022-07-27T17:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.579075, + "pv_estimate": 0.5515, + "pv_estimate10": 0.2016 + }, + { + "period_end": "2022-07-27T18:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.37254000000000004, + "pv_estimate": 0.3548, + "pv_estimate10": 0.1097 + }, + { + "period_end": "2022-07-27T18:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.16149, + "pv_estimate": 0.1538, + "pv_estimate10": 0.0583 + }, + { + "period_end": "2022-07-27T19:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.056805, + "pv_estimate": 0.0541, + "pv_estimate10": 0.0224 + }, + { + "period_end": "2022-07-27T19:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-27T20:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-27T20:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-27T21:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-27T21:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-27T22:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-27T22:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-27T23:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-27T23:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-28T00:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-28T00:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-28T01:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-28T01:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-28T02:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-28T02:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-28T03:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-28T03:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-28T04:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-28T04:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.1211, + "pv_estimate": 0.1211, + "pv_estimate10": 0.0237 + }, + { + "period_end": "2022-07-28T05:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.4283, + "pv_estimate": 0.4283, + "pv_estimate10": 0.0722 + }, + { + "period_end": "2022-07-28T05:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.8702, + "pv_estimate": 0.8702, + "pv_estimate10": 0.1567 + }, + { + "period_end": "2022-07-28T06:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.193, + "pv_estimate": 1.193, + "pv_estimate10": 0.3118 + }, + { + "period_end": "2022-07-28T06:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.5327, + "pv_estimate": 1.5327, + "pv_estimate10": 0.4794 + }, + { + "period_end": "2022-07-28T07:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.7815, + "pv_estimate": 1.7815, + "pv_estimate10": 0.682 + }, + { + "period_end": "2022-07-28T07:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.9945, + "pv_estimate": 1.9945, + "pv_estimate10": 0.8507 + }, + { + "period_end": "2022-07-28T08:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.1703, + "pv_estimate": 2.1703, + "pv_estimate10": 0.9928 + }, + { + "period_end": "2022-07-28T08:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.3272, + "pv_estimate": 2.3272, + "pv_estimate10": 1.1446 + }, + { + "period_end": "2022-07-28T09:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.4358, + "pv_estimate": 2.4358, + "pv_estimate10": 1.2873 + }, + { + "period_end": "2022-07-28T09:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.5199, + "pv_estimate": 2.5199, + "pv_estimate10": 1.3766 + }, + { + "period_end": "2022-07-28T10:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.581, + "pv_estimate": 2.581, + "pv_estimate10": 1.4629 + }, + { + "period_end": "2022-07-28T10:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.5983, + "pv_estimate": 2.5983, + "pv_estimate10": 1.5442 + }, + { + "period_end": "2022-07-28T11:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.5861, + "pv_estimate": 2.5861, + "pv_estimate10": 1.5737 + }, + { + "period_end": "2022-07-28T11:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.5549, + "pv_estimate": 2.5549, + "pv_estimate10": 1.5883 + }, + { + "period_end": "2022-07-28T12:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.482, + "pv_estimate": 2.482, + "pv_estimate10": 1.5772 + }, + { + "period_end": "2022-07-28T12:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.3967, + "pv_estimate": 2.3763, + "pv_estimate10": 1.525 + }, + { + "period_end": "2022-07-28T13:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.2855, + "pv_estimate": 2.248, + "pv_estimate10": 1.4473 + }, + { + "period_end": "2022-07-28T13:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.145, + "pv_estimate": 2.0829, + "pv_estimate10": 1.3305 + }, + { + "period_end": "2022-07-28T14:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.9721, + "pv_estimate": 1.9024, + "pv_estimate10": 1.2069 + }, + { + "period_end": "2022-07-28T14:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.7877, + "pv_estimate": 1.7149, + "pv_estimate10": 1.065 + }, + { + "period_end": "2022-07-28T15:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.5979, + "pv_estimate": 1.5313, + "pv_estimate10": 0.9167 + }, + { + "period_end": "2022-07-28T15:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.3653, + "pv_estimate": 1.3108, + "pv_estimate10": 0.7387 + }, + { + "period_end": "2022-07-28T16:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.1699, + "pv_estimate": 1.1384, + "pv_estimate10": 0.5699 + }, + { + "period_end": "2022-07-28T16:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.935, + "pv_estimate": 0.9258, + "pv_estimate10": 0.3935 + }, + { + "period_end": "2022-07-28T17:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.709, + "pv_estimate": 0.7074, + "pv_estimate10": 0.2312 + }, + { + "period_end": "2022-07-28T17:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.485, + "pv_estimate": 0.485, + "pv_estimate10": 0.1024 + }, + { + "period_end": "2022-07-28T18:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.3199, + "pv_estimate": 0.2845, + "pv_estimate10": 0.0549 + }, + { + "period_end": "2022-07-28T18:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.1203, + "pv_estimate": 0.1, + "pv_estimate10": 0.0213 + }, + { + "period_end": "2022-07-28T19:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.0492, + "pv_estimate": 0.0343, + "pv_estimate10": 0.0065 + }, + { + "period_end": "2022-07-28T19:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-28T20:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-28T20:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-28T21:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-28T21:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-28T22:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-28T22:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-28T23:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-28T23:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-29T00:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-29T00:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-29T01:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-29T01:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-29T02:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-29T02:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-29T03:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-29T03:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-29T04:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-29T04:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.0903, + "pv_estimate": 0.0137, + "pv_estimate10": 0.0023 + }, + { + "period_end": "2022-07-29T05:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.4174, + "pv_estimate": 0.0458, + "pv_estimate10": 0.0046 + }, + { + "period_end": "2022-07-29T05:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.7295, + "pv_estimate": 0.0883, + "pv_estimate10": 0.0091 + }, + { + "period_end": "2022-07-29T06:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.0121, + "pv_estimate": 0.1785, + "pv_estimate10": 0.0134 + }, + { + "period_end": "2022-07-29T06:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.3596, + "pv_estimate": 0.3133, + "pv_estimate10": 0.0202 + }, + { + "period_end": "2022-07-29T07:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.6687, + "pv_estimate": 0.491, + "pv_estimate10": 0.0244 + }, + { + "period_end": "2022-07-29T07:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.9075, + "pv_estimate": 0.6666, + "pv_estimate10": 0.0311 + }, + { + "period_end": "2022-07-29T08:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.0894, + "pv_estimate": 0.8303, + "pv_estimate10": 0.0351 + }, + { + "period_end": "2022-07-29T08:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.2649, + "pv_estimate": 1.0202, + "pv_estimate10": 0.0395 + }, + { + "period_end": "2022-07-29T09:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.3779, + "pv_estimate": 1.1751, + "pv_estimate10": 0.0435 + }, + { + "period_end": "2022-07-29T09:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.461, + "pv_estimate": 1.3177, + "pv_estimate10": 0.0452 + }, + { + "period_end": "2022-07-29T10:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.5244, + "pv_estimate": 1.4392, + "pv_estimate10": 0.0495 + }, + { + "period_end": "2022-07-29T10:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.5453, + "pv_estimate": 1.5754, + "pv_estimate10": 0.0512 + }, + { + "period_end": "2022-07-29T11:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.545, + "pv_estimate": 1.6459, + "pv_estimate10": 0.0512 + }, + { + "period_end": "2022-07-29T11:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.5033, + "pv_estimate": 1.6883, + "pv_estimate10": 0.0549 + }, + { + "period_end": "2022-07-29T12:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.43, + "pv_estimate": 1.707, + "pv_estimate10": 0.0586 + }, + { + "period_end": "2022-07-29T12:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.3594, + "pv_estimate": 1.7087, + "pv_estimate10": 0.0613 + }, + { + "period_end": "2022-07-29T13:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.2445, + "pv_estimate": 1.6797, + "pv_estimate10": 0.0591 + }, + { + "period_end": "2022-07-29T13:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.1377, + "pv_estimate": 1.6333, + "pv_estimate10": 0.0725 + }, + { + "period_end": "2022-07-29T14:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.9722, + "pv_estimate": 1.5526, + "pv_estimate10": 0.084 + }, + { + "period_end": "2022-07-29T14:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.7984, + "pv_estimate": 1.4223, + "pv_estimate10": 0.1022 + }, + { + "period_end": "2022-07-29T15:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.6112, + "pv_estimate": 1.3074, + "pv_estimate10": 0.0944 + }, + { + "period_end": "2022-07-29T15:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.3821, + "pv_estimate": 1.133, + "pv_estimate10": 0.0944 + }, + { + "period_end": "2022-07-29T16:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.1828, + "pv_estimate": 0.9672, + "pv_estimate10": 0.0834 + }, + { + "period_end": "2022-07-29T16:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.9523, + "pv_estimate": 0.7787, + "pv_estimate10": 0.0754 + }, + { + "period_end": "2022-07-29T17:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.7209, + "pv_estimate": 0.587, + "pv_estimate10": 0.0599 + }, + { + "period_end": "2022-07-29T17:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.4914, + "pv_estimate": 0.3894, + "pv_estimate10": 0.0399 + }, + { + "period_end": "2022-07-29T18:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.327, + "pv_estimate": 0.2181, + "pv_estimate10": 0.0222 + }, + { + "period_end": "2022-07-29T18:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.1263, + "pv_estimate": 0.0866, + "pv_estimate10": 0.0112 + }, + { + "period_end": "2022-07-29T19:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.0474, + "pv_estimate": 0.029, + "pv_estimate10": 0.0022 + }, + { + "period_end": "2022-07-29T19:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-29T20:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-29T20:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-29T21:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-29T21:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-29T22:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-29T22:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-29T23:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-29T23:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-30T00:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-30T00:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-30T01:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-30T01:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-30T02:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-30T02:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-30T03:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-30T03:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-30T04:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-30T04:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.0758, + "pv_estimate": 0.0206, + "pv_estimate10": 0.0023 + }, + { + "period_end": "2022-07-30T05:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.4099, + "pv_estimate": 0.0706, + "pv_estimate10": 0.0045 + }, + { + "period_end": "2022-07-30T05:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.7547, + "pv_estimate": 0.1613, + "pv_estimate10": 0.0091 + }, + { + "period_end": "2022-07-30T06:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.1231, + "pv_estimate": 0.3232, + "pv_estimate10": 0.0157 + }, + { + "period_end": "2022-07-30T06:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.416, + "pv_estimate": 0.5442, + "pv_estimate10": 0.0288 + }, + { + "period_end": "2022-07-30T07:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.6691, + "pv_estimate": 0.7798, + "pv_estimate10": 0.0527 + }, + { + "period_end": "2022-07-30T07:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.8862, + "pv_estimate": 1.0171, + "pv_estimate10": 0.0826 + }, + { + "period_end": "2022-07-30T08:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.0773, + "pv_estimate": 1.2267, + "pv_estimate10": 0.1229 + }, + { + "period_end": "2022-07-30T08:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.2186, + "pv_estimate": 1.4355, + "pv_estimate10": 0.2023 + }, + { + "period_end": "2022-07-30T09:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.345, + "pv_estimate": 1.633, + "pv_estimate10": 0.3015 + }, + { + "period_end": "2022-07-30T09:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.4218, + "pv_estimate": 1.7818, + "pv_estimate10": 0.3909 + }, + { + "period_end": "2022-07-30T10:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.4923, + "pv_estimate": 1.8994, + "pv_estimate10": 0.4771 + }, + { + "period_end": "2022-07-30T10:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.5126, + "pv_estimate": 2.0092, + "pv_estimate10": 0.5639 + }, + { + "period_end": "2022-07-30T11:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.5177, + "pv_estimate": 2.0631, + "pv_estimate10": 0.6123 + }, + { + "period_end": "2022-07-30T11:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.4786, + "pv_estimate": 2.089, + "pv_estimate10": 0.6733 + }, + { + "period_end": "2022-07-30T12:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.4081, + "pv_estimate": 2.0861, + "pv_estimate10": 0.7334 + }, + { + "period_end": "2022-07-30T12:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.3253, + "pv_estimate": 2.0441, + "pv_estimate10": 0.7562 + }, + { + "period_end": "2022-07-30T13:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.2143, + "pv_estimate": 1.9712, + "pv_estimate10": 0.756 + }, + { + "period_end": "2022-07-30T13:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.0767, + "pv_estimate": 1.8678, + "pv_estimate10": 0.7281 + }, + { + "period_end": "2022-07-30T14:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.9265, + "pv_estimate": 1.747, + "pv_estimate10": 0.6843 + }, + { + "period_end": "2022-07-30T14:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.7378, + "pv_estimate": 1.5933, + "pv_estimate10": 0.6253 + }, + { + "period_end": "2022-07-30T15:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.5505, + "pv_estimate": 1.4346, + "pv_estimate10": 0.5533 + }, + { + "period_end": "2022-07-30T15:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.3349, + "pv_estimate": 1.2546, + "pv_estimate10": 0.4644 + }, + { + "period_end": "2022-07-30T16:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.111, + "pv_estimate": 1.0587, + "pv_estimate10": 0.3831 + }, + { + "period_end": "2022-07-30T16:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.9177, + "pv_estimate": 0.8771, + "pv_estimate10": 0.2864 + }, + { + "period_end": "2022-07-30T17:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.6899, + "pv_estimate": 0.662, + "pv_estimate10": 0.1838 + }, + { + "period_end": "2022-07-30T17:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.4735, + "pv_estimate": 0.4494, + "pv_estimate10": 0.0944 + }, + { + "period_end": "2022-07-30T18:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.2742, + "pv_estimate": 0.2546, + "pv_estimate10": 0.0565 + }, + { + "period_end": "2022-07-30T18:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.1182, + "pv_estimate": 0.097, + "pv_estimate10": 0.0253 + }, + { + "period_end": "2022-07-30T19:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.0365, + "pv_estimate": 0.0336, + "pv_estimate10": 0.0085 + }, + { + "period_end": "2022-07-30T19:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-30T20:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-30T20:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-30T21:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-30T21:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-30T22:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-30T22:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-30T23:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-30T23:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-31T00:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-31T00:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-31T01:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-31T01:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-31T02:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-31T02:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-31T03:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-31T03:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-31T04:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0, + "pv_estimate": 0, + "pv_estimate10": 0 + }, + { + "period_end": "2022-07-31T04:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.06, + "pv_estimate": 0.0413, + "pv_estimate10": 0.009 + }, + { + "period_end": "2022-07-31T05:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.3783, + "pv_estimate": 0.1838, + "pv_estimate10": 0.0355 + }, + { + "period_end": "2022-07-31T05:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 0.7147, + "pv_estimate": 0.4491, + "pv_estimate10": 0.0838 + }, + { + "period_end": "2022-07-31T06:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.1041, + "pv_estimate": 0.7898, + "pv_estimate10": 0.1673 + }, + { + "period_end": "2022-07-31T06:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.3838, + "pv_estimate": 1.0963, + "pv_estimate10": 0.3141 + }, + { + "period_end": "2022-07-31T07:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.6292, + "pv_estimate": 1.3641, + "pv_estimate10": 0.4846 + }, + { + "period_end": "2022-07-31T07:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 1.8644, + "pv_estimate": 1.589, + "pv_estimate10": 0.6206 + }, + { + "period_end": "2022-07-31T08:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.0284, + "pv_estimate": 1.772, + "pv_estimate10": 0.7664 + }, + { + "period_end": "2022-07-31T08:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.17, + "pv_estimate": 1.9404, + "pv_estimate10": 0.8994 + }, + { + "period_end": "2022-07-31T09:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.2885, + "pv_estimate": 2.0701, + "pv_estimate10": 1.0292 + }, + { + "period_end": "2022-07-31T09:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.3646, + "pv_estimate": 2.1733, + "pv_estimate10": 1.1289 + }, + { + "period_end": "2022-07-31T10:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.4348, + "pv_estimate": 2.2607, + "pv_estimate10": 1.2208 + }, + { + "period_end": "2022-07-31T10:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.4527, + "pv_estimate": 2.2881, + "pv_estimate10": 1.2942 + }, + { + "period_end": "2022-07-31T11:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.444, + "pv_estimate": 2.2938, + "pv_estimate10": 1.3481 + }, + { + "period_end": "2022-07-31T11:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.4172, + "pv_estimate": 2.2856, + "pv_estimate10": 1.3635 + }, + { + "period_end": "2022-07-31T12:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.3487, + "pv_estimate": 2.2342, + "pv_estimate10": 1.3687 + }, + { + "period_end": "2022-07-31T12:30:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.2764, + "pv_estimate": 2.159, + "pv_estimate10": 1.3267 + }, + { + "period_end": "2022-07-31T13:00:00.0000000Z", + "period": "PT30M", + "pv_estimate90": 2.1679, + "pv_estimate": 2.0549, + "pv_estimate10": 1.2353 + } + ] +} From 660ec9e640149c513b5525d356f989a9d715c2af Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 26 Jul 2022 01:54:06 +0200 Subject: [PATCH 006/111] remove single plane handlers Signed-off-by: Bernd Weymann --- .../README.md | 150 ++++++++++++------ .../SolarForecastBindingConstants.java | 7 +- .../internal/SolarForecastHandlerFactory.java | 6 - ... => ForecastSolarBridgeConfiguration.java} | 12 +- .../ForecastSolarBridgeHandler.java | 8 +- .../ForecastSolarPlaneConfiguration.java | 33 ++++ .../ForecastSolarPlaneHandler.java | 18 +-- .../ForecastSolarSinglePlaneHandler.java | 84 ---------- .../solcast/SolcastBridgeConfiguration.java | 27 ++++ .../solcast/SolcastBridgeHandler.java | 6 +- .../internal/solcast/SolcastObject.java | 11 +- ...on.java => SolcastPlaneConfiguration.java} | 5 +- .../internal/solcast/SolcastPlaneHandler.java | 8 +- .../solcast/SolcastSinglePlaneHandler.java | 73 --------- .../OH-INF/config/fs-bridge-config.xml | 5 + .../OH-INF/config/fs-plane-part-config.xml | 2 +- .../OH-INF/config/fs-plane-single-config.xml | 36 ----- .../OH-INF/config/sc-bridge-config.xml | 5 + .../OH-INF/config/sc-plane-part-config.xml | 6 +- .../OH-INF/config/sc-plane-single-config.xml | 18 --- .../resources/OH-INF/thing/channel-types.xml | 2 +- .../OH-INF/thing/fs-bridge-type-multi.xml | 2 +- .../OH-INF/thing/fs-plane-single.xml | 23 --- .../OH-INF/thing/sc-bridge-type-multi.xml | 2 +- .../OH-INF/thing/sc-thing-type-single.xml | 38 ----- 25 files changed, 208 insertions(+), 379 deletions(-) rename bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/{ForecastSolarConfiguration.java => ForecastSolarBridgeConfiguration.java} (66%) create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneConfiguration.java delete mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarSinglePlaneHandler.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeConfiguration.java rename bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/{SolcastConfiguration.java => SolcastPlaneConfiguration.java} (79%) delete mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastSinglePlaneHandler.java delete mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-single-config.xml delete mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-single-config.xml delete mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-plane-single.xml delete mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-thing-type-single.xml diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 49b583b891caf..2704b7ed26743 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -1,69 +1,130 @@ - SolarForecast Binding +SolarForecast Binding -_Give some details about what this binding is meant for - a protocol, system, specific device._ +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 yor energy planning. -_If possible, provide some resources like pictures (only PNG is supported currently), a video, etc. to give an impression of what can be done with this binding._ -_You can place such resources into a `doc` folder next to this README.md._ +Supported Services + +- [Solcast](https://solcast.com/) + - [Hobbyist Plan](https://toolkit.solcast.com.au/register/hobbyist) +- [Forecast.Solar](https://forecast.solar/) -_Put each sentence in a separate line to improve readability of diffs._ ## Supported Things -_Please describe the different supported things / devices including their ThingTypeUID within this section._ -_Which different types are supported, which models were tested etc.?_ -_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._ +Each service needs one `Bridge` for your location and 1+ Photovaltaic Plane `Things` + +| Name | Thing Type ID | +|------------------------------|---------------| +| Solcast Plane Bridge | sc-multi | +| Solcast PV Plane | sc-plane | +| Forecast Solar Plane Bridge | fs-multi | +| Forecast Solar PV Plane | fs-plane | + +## Solcast Configuration + +[Solcast service](https://solcast.com/) requires a personal registration with an email 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. +After configuration the necessary information is available. + +### Solcast Bridge Configuration + +| Name | Type | Description | Default | Required | +|------------------------|---------|---------------------------------------|---------|----------| +| apiKey | text | API Key | N/A | yes | +| channelRefreshInterval | integer | Channel Refresh Interval in minutes | 1 | yes | + +`apiKey` can be obtained in your [Account Settings](https://toolkit.solcast.com.au/account) + + +### Solcast Plane Configuration -- `bridge`: Short description of the Bridge, if any -- `sample`: Short description of the Thing with the ThingTypeUID `sample` +| Name | Type | Description | Default | Required | +|-----------------|---------|---------------------------------------|---------|----------| +| resourceId | text | Resource Id of Solcast rooftop site | N/A | yes | +| refreshInterval | integer | Forecast Refresh Interval in minutes | 120 | yes | -## Discovery +`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've 25 free calls per day, each plane needs 2 call per update a refresh interval of 120 minutes will result in 24 call per day. +Nore: `channelRefreshInterval` from [Bridge Configuration](#solcast-bridge-configuration) will calculate intermediate values withput requesting new forecast data. -_Describe the available auto-discovery features here._ -_Mention for what it works and what needs to be kept in mind when using it._ -## Binding Configuration +## Solcast Channels -_If your binding requires or supports general configuration settings, please create a folder ```cfg``` and place the configuration file ```.cfg``` inside it._ -_In this section, you should link to this file and provide some information about the options._ -_The file could e.g. look like:_ +Each Plane Thing reports their specific values including a `raw` channel holding json content. +The Bridge sums up all `Plane Thing` values and provides the total forecast for your home location. -``` -# Configuration for the SolarForecast Binding -# -# Default secret key for the pairing of the SolarForecast Thing. -# It has to be between 10-40 (alphanumeric) characters. -# This may be changed by the user for security reasons. -secret=openHABSecret -``` +Channels are covering todays actual data with current, remaining and todays total prediction. +Forecasts are delivered up to 6 days in advance including -_Note that it is planned to generate some part of this based on the information that is available within ```src/main/resources/OH-INF/binding``` of your binding._ +- a pessismitic scenario: 10th percentile +- an optimistic scenario: 90th percentile -_If your binding does not offer any generic configurations, you can remove this section completely._ -## Thing Configuration +| Channel | Type | Description | +|-------------------------|---------------|-----------------------------------------| +| actual-channel | Number:Energy | Todays forecast till now | +| remaining-channel | Number:Energy | Forecast of todays remaining production | +| today-channel | Number:Energy | Todays forecast in total | +| tomorrow-channel | Number:Energy | Tomorrows forecast in total | +| tomorrow-low-channel | Number:Energy | Tomorrows pessimistic forecast | +| tomorrow-high-channel | Number:Energy | Tomorrows optimistic forecast | +| day`X`-channel | Number:Energy | Day `X` forecast in total | +| day`X`-low-channel | Number:Energy | Day `X` pessimistic forecast | +| day`X`-high-channel | Number:Energy | Day `X` optimistic forecast | +| raw | String | Plain JSON response without conversions | -_Describe what is needed to manually configure a thing, either through the UI or via a thing-file._ -_This should be mainly about its mandatory and optional configuration parameters._ -_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._ +## ForecastSolar Configuration -### `sample` Thing Configuration +[ForecastSolar service](https://forecast.solar/) provides a public free plan. -| Name | Type | Description | Default | Required | Advanced | -|-----------------|---------|---------------------------------------|---------|----------|----------| -| hostname | text | Hostname or IP address of the device | N/A | yes | no | -| password | text | Password to access the device | N/A | yes | no | -| refreshInterval | integer | Interval the device is polled in sec. | 600 | no | yes | +### ForecastSolar Bridge Configuration -## Channels +| Name | Type | Description | Default | Required | +|------------------------|---------|---------------------------------------|--------------|----------| +| location | text | Location of Photovoltaic system | AUTODETECT | yes | +| channelRefreshInterval | integer | Channel Refresh Interval in minutes | 1 | yes | +| apiKey | text | API Key | N/A | no | -_Here you should provide information about available channel types, what their meaning is and how they can be used._ +`location` defines latitufe,longitude values of your PV system. +In case of autodetect the location configured in openHAB is obtained. +`apiKey` can be given in case you subscribed to a paid plan -_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._ -| Channel | Type | Read/Write | Description | -|---------|--------|------------|-----------------------------| -| control | Switch | RW | This is the control channel | +### ForecastSolar Plane Configuration + +| Name | Type | Description | Default | Required | +|-----------------|---------|------------------------------------------------------------------------------|---------|----------| +| refreshInterval | integer | Forecast Refresh Interval in minutes | 30 | yes | +| declination | integer | Plane Declination - 0 for horizontal till 90 for vertical declination | N/A | yes | +| azimuth | integer | Plane Azimuth - -180 = north, -90 = east, 0 = south, 90 = west, 180 = north | N/A | yes | +| kwp | decimal | Installed Kilowatt Peak | N/A | yes | + +`refreshInterval` of forecast data needs to respect the throttling of the ForecastSolar service. +There're 12 calls per hour allowed from your caller IP address so for 2 planes lowest possible refresh rate is 10 minutes. +Nore: `channelRefreshInterval` from [Bridge Configuration](#forecastsolar-bridge-configuration) will calculate intermediate values withput requesting new forecast data. + + +## ForecastSolar Channels + +Each Plane Thing reports their specific values including a `raw` channel holding json content. +The Bridge sums up all `Plane Thing` values and provides the total forecast for your home location. + +Channels are covering todays actual data with current, remaining and todays total prediction. +Forecasts are delivered up to 3 days for paid personal plans. + +| Channel | Type | Description | +|-------------------------|---------------|-----------------------------------------| +| actual-channel | Number:Energy | Todays forecast till now | +| remaining-channel | Number:Energy | Forecast of todays remaining production | +| today-channel | Number:Energy | Todays forecast in total | +| tomorrow-channel | Number:Energy | Tomorrows forecast in total | +| day`X`-channel | Number:Energy | Day `X` forecast in total | +| raw | String | Plain JSON response without conversions | ## Full Example @@ -71,6 +132,3 @@ _Provide a full usage example based on textual configuration files._ _*.things, *.items examples are mandatory as textual configuration is well used by many users._ _*.sitemap examples are optional._ -## Any custom content here! - -_Feel free to add additional sections for whatever you think should also be mentioned about your binding!_ 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 index ef5d91e25a15c..015b610865e30 100644 --- 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 @@ -28,15 +28,12 @@ public class SolarForecastBindingConstants { private static final String BINDING_ID = "solarforecast"; - public static final ThingTypeUID FORECAST_SOLAR_SINGLE_STRING = new ThingTypeUID(BINDING_ID, "fs-single"); public static final ThingTypeUID FORECAST_SOLAR_MULTI_STRING = new ThingTypeUID(BINDING_ID, "fs-multi"); public static final ThingTypeUID FORECAST_SOLAR_PART_STRING = new ThingTypeUID(BINDING_ID, "fs-part"); public static final ThingTypeUID SOLCAST_BRIDGE_STRING = new ThingTypeUID(BINDING_ID, "sc-multi"); public static final ThingTypeUID SOLCAST_PART_STRING = new ThingTypeUID(BINDING_ID, "sc-part"); - public static final ThingTypeUID SOLCAST_SINGLE_STRING = new ThingTypeUID(BINDING_ID, "sc-single"); - public static final Set SUPPORTED_THING_SET = Set.of(FORECAST_SOLAR_SINGLE_STRING, - FORECAST_SOLAR_MULTI_STRING, FORECAST_SOLAR_PART_STRING, SOLCAST_BRIDGE_STRING, SOLCAST_PART_STRING, - SOLCAST_SINGLE_STRING); + public static final Set SUPPORTED_THING_SET = Set.of(FORECAST_SOLAR_MULTI_STRING, + FORECAST_SOLAR_PART_STRING, SOLCAST_BRIDGE_STRING, SOLCAST_PART_STRING); public static final String CHANNEL_TODAY = "today"; public static final String CHANNEL_ACTUAL = "actual"; 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 index 965a3e55f9351..d495cd4801403 100644 --- 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 @@ -19,10 +19,8 @@ import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarBridgeHandler; import org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarPlaneHandler; -import org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarSinglePlaneHandler; import org.openhab.binding.solarforecast.internal.solcast.SolcastBridgeHandler; import org.openhab.binding.solarforecast.internal.solcast.SolcastPlaneHandler; -import org.openhab.binding.solarforecast.internal.solcast.SolcastSinglePlaneHandler; import org.openhab.core.i18n.LocationProvider; import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.library.types.PointType; @@ -72,14 +70,10 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { return new ForecastSolarBridgeHandler((Bridge) thing, location); } else if (FORECAST_SOLAR_PART_STRING.equals(thingTypeUID)) { return new ForecastSolarPlaneHandler(thing, httpClient); - } else if (FORECAST_SOLAR_SINGLE_STRING.equals(thingTypeUID)) { - return new ForecastSolarSinglePlaneHandler(thing, httpClient, location); } else if (SOLCAST_BRIDGE_STRING.equals(thingTypeUID)) { return new SolcastBridgeHandler((Bridge) thing); } else if (SOLCAST_PART_STRING.equals(thingTypeUID)) { return new SolcastPlaneHandler(thing, httpClient); - } else if (SOLCAST_SINGLE_STRING.equals(thingTypeUID)) { - return new SolcastSinglePlaneHandler(thing, httpClient); } return null; } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarConfiguration.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeConfiguration.java similarity index 66% rename from bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarConfiguration.java rename to bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeConfiguration.java index c4ecca2dbb86a..17dbe49d4a8e3 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarConfiguration.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeConfiguration.java @@ -16,22 +16,18 @@ import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; /** - * The {@link ForecastSolarConfiguration} class contains fields mapping thing configuration parameters. + * The {@link ForecastSolarBridgeConfiguration} class contains fields mapping thing configuration parameters. * * @author Bernd Weymann - Initial contribution */ @NonNullByDefault -public class ForecastSolarConfiguration { +public class ForecastSolarBridgeConfiguration { public String location = "0.0,0.0"; - public int declination = -1; - public int azimuth = 360; - public double kwp = 0; - public int refreshInterval = -1; + public int channelRefreshInterval = -1; public String apiKey = SolarForecastBindingConstants.EMPTY; @Override public String toString() { - return "Loc " + location + " Dec " + declination + " Azi " + azimuth + " KWP " + kwp + " Ref " - + refreshInterval; + return "Loc " + location + " Ref " + channelRefreshInterval; } } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java index 6046d465a9bb2..c82ddc93bd078 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java @@ -47,7 +47,7 @@ public class ForecastSolarBridgeHandler extends BaseBridgeHandler { private final PointType homeLocation; private List parts = new ArrayList(); - private Optional configuration = Optional.empty(); + private Optional configuration = Optional.empty(); private Optional> refreshJob = Optional.empty(); public ForecastSolarBridgeHandler(Bridge bridge, PointType location) { @@ -57,17 +57,17 @@ public ForecastSolarBridgeHandler(Bridge bridge, PointType location) { @Override public void initialize() { - ForecastSolarConfiguration config = getConfigAs(ForecastSolarConfiguration.class); + ForecastSolarBridgeConfiguration config = getConfigAs(ForecastSolarBridgeConfiguration.class); if (config.location.equals(SolarForecastBindingConstants.AUTODETECT)) { Configuration editConfig = editConfiguration(); editConfig.put("location", homeLocation.toString()); updateConfiguration(editConfig); - config = getConfigAs(ForecastSolarConfiguration.class); + config = getConfigAs(ForecastSolarBridgeConfiguration.class); } configuration = Optional.of(config); updateStatus(ThingStatus.ONLINE); getData(); - startSchedule(1); + startSchedule(configuration.get().channelRefreshInterval); } @Override diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneConfiguration.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneConfiguration.java new file mode 100644 index 0000000000000..eeb53e3246c48 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneConfiguration.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2022 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 org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * 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 = 360; + public double kwp = 0; + public int refreshInterval = -1; + + @Override + public String toString() { + return " Dec " + declination + " Azi " + azimuth + " KWP " + kwp + " Ref " + refreshInterval; + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java index 527ce77a35453..5a596b1fd4e23 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java @@ -48,7 +48,7 @@ public class ForecastSolarPlaneHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(ForecastSolarPlaneHandler.class); private final HttpClient httpClient; - private Optional configuration = Optional.empty(); + private Optional configuration = Optional.empty(); private Optional bridgeHandler = Optional.empty(); private Optional location = Optional.empty(); private Optional apiKey = Optional.empty(); @@ -61,7 +61,7 @@ public ForecastSolarPlaneHandler(Thing thing, HttpClient hc) { @Override public void initialize() { - ForecastSolarConfiguration c = getConfigAs(ForecastSolarConfiguration.class); + ForecastSolarPlaneConfiguration c = getConfigAs(ForecastSolarPlaneConfiguration.class); configuration = Optional.of(c); Bridge bridge = getBridge(); if (bridge != null) { @@ -170,18 +170,4 @@ void setLocation(PointType loc) { void setApiKey(String key) { apiKey = Optional.of(key); } - - /** - * Used by SinglePlaneHandler to submit config data - * - * @param c - */ - protected void setConfig(ForecastSolarConfiguration c) { - logger.info("Config {}", c); - configuration = Optional.of(c); - location = Optional.of(PointType.valueOf(c.location)); - if (!EMPTY.equals(c.apiKey)) { - apiKey = Optional.of(c.apiKey); - } - } } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarSinglePlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarSinglePlaneHandler.java deleted file mode 100644 index 88d35885e8530..0000000000000 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarSinglePlaneHandler.java +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Copyright (c) 2010-2022 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.util.Optional; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jetty.client.HttpClient; -import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; -import org.openhab.core.config.core.Configuration; -import org.openhab.core.library.types.PointType; -import org.openhab.core.thing.ChannelUID; -import org.openhab.core.thing.Thing; -import org.openhab.core.thing.ThingStatus; -import org.openhab.core.types.Command; - -/** - * The {@link ForecastSolarBridgeHandler} is a non active handler instance. It will be triggerer by the bridge. - * - * @author Bernd Weymann - Initial contribution - */ -@NonNullByDefault -public class ForecastSolarSinglePlaneHandler extends ForecastSolarPlaneHandler { - private Optional> refreshJob = Optional.empty(); - private PointType homeLocation; - - public ForecastSolarSinglePlaneHandler(Thing thing, HttpClient httpClient, PointType location) { - super(thing, httpClient); - homeLocation = location; - } - - @Override - public void initialize() { - ForecastSolarConfiguration config = getConfigAs(ForecastSolarConfiguration.class); - // adapt to home location if AUTODETECT selected - if (config.location.equals(SolarForecastBindingConstants.AUTODETECT)) { - Configuration editConfig = editConfiguration(); - editConfig.put("location", homeLocation.toString()); - updateConfiguration(editConfig); - config = getConfigAs(ForecastSolarConfiguration.class); - } - super.setConfig(config); - startSchedule(1); - updateStatus(ThingStatus.ONLINE); - } - - @Override - public void handleCommand(ChannelUID channelUID, Command command) { - super.handleCommand(channelUID, command); - } - - private void startSchedule(int interval) { - refreshJob.ifPresentOrElse(job -> { - if (job.isCancelled()) { - refreshJob = Optional - .of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES)); - } // else - scheduler is already running! - }, () -> { - refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES)); - }); - } - - private void getData() { - super.fetchData(); - } - - @Override - public void dispose() { - super.dispose(); - refreshJob.ifPresent(job -> job.cancel(true)); - } -} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeConfiguration.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeConfiguration.java new file mode 100644 index 0000000000000..a7531c8c9d4b1 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeConfiguration.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2022 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 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 int channelRefreshInterval = -1; + public String apiKey = SolarForecastBindingConstants.EMPTY; +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java index 5ce02c9e0e880..c09541a3330f3 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java @@ -42,7 +42,7 @@ public class SolcastBridgeHandler extends BaseBridgeHandler { private final Logger logger = LoggerFactory.getLogger(SolcastBridgeHandler.class); private List parts = new ArrayList(); - private Optional configuration = Optional.empty(); + private Optional configuration = Optional.empty(); private Optional> refreshJob = Optional.empty(); public SolcastBridgeHandler(Bridge bridge) { @@ -51,12 +51,12 @@ public SolcastBridgeHandler(Bridge bridge) { @Override public void initialize() { - SolcastConfiguration config = getConfigAs(SolcastConfiguration.class); + SolcastBridgeConfiguration config = getConfigAs(SolcastBridgeConfiguration.class); configuration = Optional.of(config); if (!EMPTY.equals(config.apiKey)) { updateStatus(ThingStatus.ONLINE); getData(); - startSchedule(1); + startSchedule(configuration.get().channelRefreshInterval); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "API Key is mandatory"); logger.info("API Key missing"); 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 index 5bbef9a0ac3f7..997c38e8a8a17 100644 --- 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 @@ -70,7 +70,6 @@ private void add(String content) { if (contentJson.has("forecasts")) { resultJsonArray = contentJson.getJSONArray("forecasts"); rawData.get().put("forecasts", resultJsonArray); - } else { resultJsonArray = contentJson.getJSONArray("estimated_actuals"); rawData.get().put("estimated_actuals", resultJsonArray); @@ -152,7 +151,10 @@ public double getActualValue(LocalDateTime now) { if (key.isBefore(now)) { // value are reported in PT30M = 30 minutes interval with kw value // for kw/h it's half the value - forecastValue += dtm.get(key) / 2; + Double addedValue = dtm.get(key); + if (addedValue != null) { + forecastValue += addedValue.doubleValue() / 2; + } } } @@ -212,7 +214,10 @@ private double getTotalValue(TreeMap map) { for (LocalDateTime key : keySet) { // value are reported in PT30M = 30 minutes interval with kw value // for kw/h it's half the value - forecastValue += map.get(key) / 2; + Double addedValue = map.get(key); + if (addedValue != null) { + forecastValue += addedValue.doubleValue() / 2; + } } return forecastValue; } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastConfiguration.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneConfiguration.java similarity index 79% rename from bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastConfiguration.java rename to bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneConfiguration.java index 2e0712ee51de0..fabfe26efe46c 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastConfiguration.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneConfiguration.java @@ -16,13 +16,12 @@ import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; /** - * The {@link SolcastConfiguration} class contains fields mapping thing configuration parameters. + * The {@link SolcastPlaneConfiguration} class contains fields mapping thing configuration parameters. * * @author Bernd Weymann - Initial contribution */ @NonNullByDefault -public class SolcastConfiguration { +public class SolcastPlaneConfiguration { public int refreshInterval = -1; - public String apiKey = SolarForecastBindingConstants.EMPTY; public String resourceId = SolarForecastBindingConstants.EMPTY; } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java index 38bf7c86239bd..1789c7b5f0f8c 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java @@ -48,7 +48,7 @@ public class SolcastPlaneHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(SolcastPlaneHandler.class); private final HttpClient httpClient; - private Optional configuration = Optional.empty(); + private Optional configuration = Optional.empty(); private Optional bridgeHandler = Optional.empty(); private SolcastObject forecast = new SolcastObject(); @@ -59,7 +59,7 @@ public SolcastPlaneHandler(Thing thing, HttpClient hc) { @Override public void initialize() { - SolcastConfiguration c = getConfigAs(SolcastConfiguration.class); + SolcastPlaneConfiguration c = getConfigAs(SolcastPlaneConfiguration.class); configuration = Optional.of(c); Bridge bridge = getBridge(); if (bridge != null) { @@ -170,8 +170,4 @@ private void updateChannels(SolcastObject f) { updateState(CHANNEL_DAY6_LOW, SolcastObject.getStateObject(f.getPessimisticDayTotal(LocalDateTime.now(), 6))); updateState(CHANNEL_RAW, StringType.valueOf(forecast.getRaw())); } - - public void setConfig(SolcastConfiguration config) { - configuration = Optional.of(config); - } } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastSinglePlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastSinglePlaneHandler.java deleted file mode 100644 index 562872b11ee50..0000000000000 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastSinglePlaneHandler.java +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (c) 2010-2022 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.util.Optional; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jetty.client.HttpClient; -import org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarBridgeHandler; -import org.openhab.core.thing.ChannelUID; -import org.openhab.core.thing.Thing; -import org.openhab.core.thing.ThingStatus; -import org.openhab.core.types.Command; - -/** - * The {@link ForecastSolarBridgeHandler} is a non active handler instance. It will be triggerer by the bridge. - * - * @author Bernd Weymann - Initial contribution - */ -@NonNullByDefault -public class SolcastSinglePlaneHandler extends SolcastPlaneHandler { - private Optional> refreshJob = Optional.empty(); - - public SolcastSinglePlaneHandler(Thing thing, HttpClient httpClient) { - super(thing, httpClient); - } - - @Override - public void initialize() { - SolcastConfiguration config = getConfigAs(SolcastConfiguration.class); - super.setConfig(config); - startSchedule(1); - updateStatus(ThingStatus.ONLINE); - } - - @Override - public void handleCommand(ChannelUID channelUID, Command command) { - super.handleCommand(channelUID, command); - } - - private void startSchedule(int interval) { - refreshJob.ifPresentOrElse(job -> { - if (job.isCancelled()) { - refreshJob = Optional - .of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES)); - } // else - scheduler is already running! - }, () -> { - refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES)); - }); - } - - private void getData() { - super.fetchData(); - } - - @Override - public void dispose() { - super.dispose(); - refreshJob.ifPresent(job -> job.cancel(true)); - } -} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-bridge-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-bridge-config.xml index 81bb63f2c05d0..365bf2c21a4dd 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-bridge-config.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-bridge-config.xml @@ -15,5 +15,10 @@ If you have a paid subscription plan + + + Refresh rate of channel data + 1 + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-part-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-part-config.xml index e4d1202376bf7..aebcdf969665a 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-part-config.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-part-config.xml @@ -6,7 +6,7 @@ - + Data refresh rate of forecast data 30 diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-single-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-single-config.xml deleted file mode 100644 index f77872def4b79..0000000000000 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-single-config.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - location - - Location of Photovoltaic system - AUTODETECT - - - - 0 for horizontal till 90 for vertical declination - - - - -180 = north, -90 = east, 0 = south, 90 = west, 180 = north - - - - Installed Module power of this plane - - - - Data refresh rate of forecast data - 15 - - - - If you have a paid subscription plan - - - diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-bridge-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-bridge-config.xml index 58291b19f1915..eb1123a6c60ca 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-bridge-config.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-bridge-config.xml @@ -9,5 +9,10 @@ API Key from your subscription + + + Refresh rate of channel data + 1 + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-part-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-part-config.xml index 0fc62e867ab83..8d68dc145af55 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-part-config.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-part-config.xml @@ -6,13 +6,13 @@ - + Data refresh rate of forecast data 120 - - Resource Id from Solcast configuration + + Resource Id of Solcast rooftop site diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-single-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-single-config.xml deleted file mode 100644 index d82a37f3c5132..0000000000000 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-single-config.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - Data refresh rate of forecast data - 60 - - - - Resource Id from Solcast configuration - - - 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 index 3a782931ac0aa..ccd8da4e78fcd 100644 --- 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 @@ -13,7 +13,7 @@ Number:Energy - Forecast till now (actual time) + Todays forecast till now diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-bridge-type-multi.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-bridge-type-multi.xml index 76f8ce19186aa..d08533824897b 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-bridge-type-multi.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-bridge-type-multi.xml @@ -5,7 +5,7 @@ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - + Bridge holding multiple photovoltaic planes diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-plane-single.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-plane-single.xml deleted file mode 100644 index 578dfc9a04f86..0000000000000 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-plane-single.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - One PV plane attached to your inverter - - - - - - - - - - - - - - diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-bridge-type-multi.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-bridge-type-multi.xml index 5278ec47feaee..8997c9c0d1c65 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-bridge-type-multi.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-bridge-type-multi.xml @@ -5,7 +5,7 @@ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - + Bridge holding multiple photovoltaic planes diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-thing-type-single.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-thing-type-single.xml deleted file mode 100644 index 262c676b28915..0000000000000 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-thing-type-single.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - PV Plane as part of Multi Plane Bridge - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 406ea193e528d1b3df58fc34934304e76df1b78d Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 26 Jul 2022 10:30:26 +0200 Subject: [PATCH 007/111] readme updates Signed-off-by: Bernd Weymann --- .../README.md | 6 +- .../internal/solcast/SolcastObject.java | 4 +- .../binding/solarforecast/SolcastTest.java | 60 +- .../src/test/resources/solcast/NE.json | 4023 ----------------- 4 files changed, 23 insertions(+), 4070 deletions(-) delete mode 100644 bundles/org.openhab.binding.solarforecast/src/test/resources/solcast/NE.json diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 2704b7ed26743..7ce0e4cb8cc76 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -1,4 +1,4 @@ -SolarForecast Binding +# 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. @@ -49,7 +49,7 @@ After configuration the necessary information is available. `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've 25 free calls per day, each plane needs 2 call per update a refresh interval of 120 minutes will result in 24 call per day. -Nore: `channelRefreshInterval` from [Bridge Configuration](#solcast-bridge-configuration) will calculate intermediate values withput requesting new forecast data. +Nore: `channelRefreshInterval` from [Bridge Configuration](#solcast-bridge-configuration) will calculate intermediate values without requesting new forecast data. ## Solcast Channels @@ -106,7 +106,7 @@ In case of autodetect the location configured in openHAB is obtained. `refreshInterval` of forecast data needs to respect the throttling of the ForecastSolar service. There're 12 calls per hour allowed from your caller IP address so for 2 planes lowest possible refresh rate is 10 minutes. -Nore: `channelRefreshInterval` from [Bridge Configuration](#forecastsolar-bridge-configuration) will calculate intermediate values withput requesting new forecast data. +Nore: `channelRefreshInterval` from [Bridge Configuration](#forecastsolar-bridge-configuration) will calculate intermediate values without requesting new forecast data. ## ForecastSolar Channels 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 index 997c38e8a8a17..e023e986b83ad 100644 --- 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 @@ -184,7 +184,7 @@ public double getDayTotal(LocalDateTime now, int offset) { if (dtm != null) { return getTotalValue(dtm); } else { - return 0; + return -1; } } @@ -194,7 +194,7 @@ public double getOptimisticDayTotal(LocalDateTime now, int offset) { if (dtm != null) { return getTotalValue(dtm); } else { - return 0; + return -1; } } 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 index 598f7efe2e3ba..e12a27226208b 100644 --- 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 @@ -12,9 +12,9 @@ */ package org.openhab.binding.solarforecast; -import java.time.LocalDate; +import static org.junit.jupiter.api.Assertions.*; + import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import org.eclipse.jdt.annotation.NonNullByDefault; import org.json.JSONObject; @@ -28,20 +28,13 @@ */ @NonNullByDefault class SolcastTest { - public static final String DATE_INPUT_PATTERN_STRING = "yyyy-MM-dd'T'HH:mm:ss"; - public static final DateTimeFormatter DATE_INPUT_PATTERN = DateTimeFormatter.ofPattern(DATE_INPUT_PATTERN_STRING); @Test void testForecastObject() { - String dateTime = "2022-07-17T22:00:00.0000000Z"; - System.out.println(dateTime.substring(0, dateTime.lastIndexOf("."))); - dateTime = dateTime.substring(0, dateTime.lastIndexOf("T")); - LocalDate ld = LocalDate.parse(dateTime); - System.out.println(ld); String content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); - LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 23); + LocalDateTime now = LocalDateTime.of(2022, 7, 23, 16, 23); SolcastObject scfo = new SolcastObject(content, now); - // System.out.println(scfo); + assertEquals(19.462, scfo.getActualValue(now), 0.001, "Actual estimation"); } @Test @@ -49,9 +42,8 @@ void testForecastTreeMap() { String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 23); SolcastObject scfo = new SolcastObject(content, now); - // System.out.println(scfo); - System.out.println(scfo.getDayTotal(now, 0)); - System.out.println(scfo.getActualValue(now)); + assertEquals(25.413, scfo.getDayTotal(now, 0), 0.001, "Day total"); + assertEquals(24.150, scfo.getActualValue(now), 0.001, "Actual estimation"); } @Test @@ -59,11 +51,11 @@ void testJoin() { String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); LocalDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23); SolcastObject scfo = new SolcastObject(content, now); - System.out.println(scfo.getActualValue(now)); + assertEquals(-1.0, scfo.getActualValue(now), 0.01, "Invalid"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); - System.out.println(scfo.getActualValue(now)); - System.out.println(scfo.getDayTotal(now, 0)); + assertEquals(22.011, scfo.getActualValue(now), 0.01, "Actual data"); + assertEquals(23.107, scfo.getDayTotal(now, 0), 0.01, "Today data"); } @Test @@ -71,12 +63,11 @@ void testOptimisticPessimistic() { String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); LocalDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23); SolcastObject scfo = new SolcastObject(content, now); - System.out.println(scfo.getActualValue(now)); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); - System.out.println("Forecast med " + scfo.getDayTotal(now, 2)); - System.out.println("Forecast pes " + scfo.getPessimisticDayTotal(now, 2)); - System.out.println("Forecast opt " + scfo.getOptimisticDayTotal(now, 2)); + assertEquals(19.389, scfo.getDayTotal(now, 2), 0.001, "Estimation"); + assertEquals(7.358, scfo.getPessimisticDayTotal(now, 2), 0.001, "Estimation"); + assertEquals(22.283, scfo.getOptimisticDayTotal(now, 2), 0.001, "Estimation"); } @Test @@ -84,11 +75,11 @@ void testInavlid() { String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); LocalDateTime now = LocalDateTime.now(); SolcastObject scfo = new SolcastObject(content, now); - System.out.println(scfo.getActualValue(now)); + assertEquals(-1.0, scfo.getActualValue(now), 0.01, "Data available - day not in"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); - System.out.println(scfo.getActualValue(now)); - System.out.println(scfo.getDayTotal(now, 0)); + assertEquals(-1.0, scfo.getActualValue(now), 0.01, "Data available after merge - day not in"); + assertEquals(-1.0, scfo.getDayTotal(now, 0), 0.01, "Data available after merge - day not in"); } @Test @@ -96,25 +87,10 @@ void testRawChannel() { String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); LocalDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23); SolcastObject sco = new SolcastObject(content, now); - System.out.println(sco.getActualValue(now)); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); sco.join(content); - System.out.println("SCO Raw " + sco.getRaw()); - } - - @Test - void testRoofNE() { - String content = FileReader.readFileInString("src/test/resources/solcast/NE.json"); - LocalDateTime now = LocalDateTime.now(); - JSONObject act = new JSONObject(content); - JSONObject actual = new JSONObject(); - actual.put("estimated_actuals", act.getJSONArray("estimated_actuals")); - SolcastObject scfo = new SolcastObject(actual.toString(), now); - System.out.println(scfo.getActualValue(now)); - - JSONObject forecasts = new JSONObject(); - forecasts.put("forecasts", act.getJSONArray("forecasts")); - scfo.join(forecasts.toString()); - System.out.println(scfo.getActualValue(now)); + JSONObject joined = new JSONObject(sco.getRaw()); + assertTrue(joined.has("forecasts"), "Forecasts available"); + assertTrue(joined.has("estimated_actuals"), "Actual data available"); } } diff --git a/bundles/org.openhab.binding.solarforecast/src/test/resources/solcast/NE.json b/bundles/org.openhab.binding.solarforecast/src/test/resources/solcast/NE.json deleted file mode 100644 index 71272e07ef153..0000000000000 --- a/bundles/org.openhab.binding.solarforecast/src/test/resources/solcast/NE.json +++ /dev/null @@ -1,4023 +0,0 @@ -{ - "estimated_actuals": [ - { - "period_end": "2022-07-24T12:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.3467 - }, - { - "period_end": "2022-07-24T12:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.4376 - }, - { - "period_end": "2022-07-24T11:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.4888 - }, - { - "period_end": "2022-07-24T11:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.4877 - }, - { - "period_end": "2022-07-24T10:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.487 - }, - { - "period_end": "2022-07-24T10:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.4675 - }, - { - "period_end": "2022-07-24T09:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.4038 - }, - { - "period_end": "2022-07-24T09:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.3233 - }, - { - "period_end": "2022-07-24T08:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.2117 - }, - { - "period_end": "2022-07-24T08:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.0763 - }, - { - "period_end": "2022-07-24T07:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.8913 - }, - { - "period_end": "2022-07-24T07:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.6715 - }, - { - "period_end": "2022-07-24T06:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.1027 - }, - { - "period_end": "2022-07-24T06:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.7972 - }, - { - "period_end": "2022-07-24T05:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.5099 - }, - { - "period_end": "2022-07-24T05:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.2235 - }, - { - "period_end": "2022-07-24T04:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0654 - }, - { - "period_end": "2022-07-24T04:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0023 - }, - { - "period_end": "2022-07-24T03:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-24T03:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-24T02:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-24T02:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-24T01:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-24T01:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-24T00:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-24T00:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-23T23:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-23T23:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-23T22:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-23T22:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-23T21:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-23T21:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-23T20:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-23T20:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-23T19:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.014 - }, - { - "period_end": "2022-07-23T19:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0657 - }, - { - "period_end": "2022-07-23T18:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.1625 - }, - { - "period_end": "2022-07-23T18:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.3704 - }, - { - "period_end": "2022-07-23T17:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.5914 - }, - { - "period_end": "2022-07-23T17:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.7602 - }, - { - "period_end": "2022-07-23T16:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.9932 - }, - { - "period_end": "2022-07-23T16:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.1795 - }, - { - "period_end": "2022-07-23T15:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.3805 - }, - { - "period_end": "2022-07-23T15:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.5629 - }, - { - "period_end": "2022-07-23T14:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.7041 - }, - { - "period_end": "2022-07-23T14:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.7898 - }, - { - "period_end": "2022-07-23T13:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.0421 - }, - { - "period_end": "2022-07-23T13:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.0732 - }, - { - "period_end": "2022-07-23T12:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.1193 - }, - { - "period_end": "2022-07-23T12:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.1525 - }, - { - "period_end": "2022-07-23T11:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.1135 - }, - { - "period_end": "2022-07-23T11:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.2488 - }, - { - "period_end": "2022-07-23T10:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.2079 - }, - { - "period_end": "2022-07-23T10:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.2887 - }, - { - "period_end": "2022-07-23T09:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.3472 - }, - { - "period_end": "2022-07-23T09:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.2443 - }, - { - "period_end": "2022-07-23T08:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.1171 - }, - { - "period_end": "2022-07-23T08:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.6259 - }, - { - "period_end": "2022-07-23T07:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.035 - }, - { - "period_end": "2022-07-23T07:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.1865 - }, - { - "period_end": "2022-07-23T06:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.0569 - }, - { - "period_end": "2022-07-23T06:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.6812 - }, - { - "period_end": "2022-07-23T05:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.3647 - }, - { - "period_end": "2022-07-23T05:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.1986 - }, - { - "period_end": "2022-07-23T04:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0159 - }, - { - "period_end": "2022-07-23T04:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-23T03:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-23T03:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-23T02:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-23T02:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-23T01:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-23T01:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-23T00:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-23T00:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-22T23:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-22T23:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-22T22:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-22T22:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-22T21:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-22T21:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-22T20:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-22T20:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-22T19:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-22T19:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.044 - }, - { - "period_end": "2022-07-22T18:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0729 - }, - { - "period_end": "2022-07-22T18:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.2943 - }, - { - "period_end": "2022-07-22T17:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.5305 - }, - { - "period_end": "2022-07-22T17:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.7256 - }, - { - "period_end": "2022-07-22T16:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.9587 - }, - { - "period_end": "2022-07-22T16:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.1872 - }, - { - "period_end": "2022-07-22T15:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.4061 - }, - { - "period_end": "2022-07-22T15:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.6205 - }, - { - "period_end": "2022-07-22T14:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.7181 - }, - { - "period_end": "2022-07-22T14:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.8627 - }, - { - "period_end": "2022-07-22T13:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.9427 - }, - { - "period_end": "2022-07-22T13:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.1803 - }, - { - "period_end": "2022-07-22T12:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.3676 - }, - { - "period_end": "2022-07-22T12:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.445 - }, - { - "period_end": "2022-07-22T11:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.5272 - }, - { - "period_end": "2022-07-22T11:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.5222 - }, - { - "period_end": "2022-07-22T10:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.5315 - }, - { - "period_end": "2022-07-22T10:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.5122 - }, - { - "period_end": "2022-07-22T09:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.4772 - }, - { - "period_end": "2022-07-22T09:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.3909 - }, - { - "period_end": "2022-07-22T08:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.2789 - }, - { - "period_end": "2022-07-22T08:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.9704 - }, - { - "period_end": "2022-07-22T07:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.6186 - }, - { - "period_end": "2022-07-22T07:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.1127 - }, - { - "period_end": "2022-07-22T06:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.4484 - }, - { - "period_end": "2022-07-22T06:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.3004 - }, - { - "period_end": "2022-07-22T05:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.1597 - }, - { - "period_end": "2022-07-22T05:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0836 - }, - { - "period_end": "2022-07-22T04:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0748 - }, - { - "period_end": "2022-07-22T04:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0023 - }, - { - "period_end": "2022-07-22T03:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-22T03:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-22T02:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-22T02:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-22T01:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-22T01:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-22T00:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-22T00:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-21T23:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-21T23:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-21T22:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-21T22:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-21T21:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-21T21:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-21T20:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-21T20:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-21T19:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0141 - }, - { - "period_end": "2022-07-21T19:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0659 - }, - { - "period_end": "2022-07-21T18:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.1362 - }, - { - "period_end": "2022-07-21T18:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.2017 - }, - { - "period_end": "2022-07-21T17:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.2276 - }, - { - "period_end": "2022-07-21T17:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.3903 - }, - { - "period_end": "2022-07-21T16:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.513 - }, - { - "period_end": "2022-07-21T16:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.4306 - }, - { - "period_end": "2022-07-21T15:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.4328 - }, - { - "period_end": "2022-07-21T15:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.71 - }, - { - "period_end": "2022-07-21T14:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.6313 - }, - { - "period_end": "2022-07-21T14:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.0995 - }, - { - "period_end": "2022-07-21T13:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.3991 - }, - { - "period_end": "2022-07-21T13:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.7188 - }, - { - "period_end": "2022-07-21T12:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.3481 - }, - { - "period_end": "2022-07-21T12:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.1533 - }, - { - "period_end": "2022-07-21T11:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.2648 - }, - { - "period_end": "2022-07-21T11:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.8648 - }, - { - "period_end": "2022-07-21T10:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.2882 - }, - { - "period_end": "2022-07-21T10:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.2078 - }, - { - "period_end": "2022-07-21T09:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.0309 - }, - { - "period_end": "2022-07-21T09:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.0067 - }, - { - "period_end": "2022-07-21T08:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.1591 - }, - { - "period_end": "2022-07-21T08:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.4075 - }, - { - "period_end": "2022-07-21T07:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.178 - }, - { - "period_end": "2022-07-21T07:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.2128 - }, - { - "period_end": "2022-07-21T06:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.5251 - }, - { - "period_end": "2022-07-21T06:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.7555 - }, - { - "period_end": "2022-07-21T05:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.5044 - }, - { - "period_end": "2022-07-21T05:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0906 - }, - { - "period_end": "2022-07-21T04:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0266 - }, - { - "period_end": "2022-07-21T04:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-21T03:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-21T03:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-21T02:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-21T02:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-21T01:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-21T01:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-21T00:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-21T00:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-20T23:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-20T23:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-20T22:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-20T22:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-20T21:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-20T21:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-20T20:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-20T20:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-20T19:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0105 - }, - { - "period_end": "2022-07-20T19:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0417 - }, - { - "period_end": "2022-07-20T18:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0801 - }, - { - "period_end": "2022-07-20T18:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0801 - }, - { - "period_end": "2022-07-20T17:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.4001 - }, - { - "period_end": "2022-07-20T17:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.4913 - }, - { - "period_end": "2022-07-20T16:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.7275 - }, - { - "period_end": "2022-07-20T16:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.1724 - }, - { - "period_end": "2022-07-20T15:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.3813 - }, - { - "period_end": "2022-07-20T15:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.5826 - }, - { - "period_end": "2022-07-20T14:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.7688 - }, - { - "period_end": "2022-07-20T14:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.9328 - }, - { - "period_end": "2022-07-20T13:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.0633 - }, - { - "period_end": "2022-07-20T13:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.1855 - }, - { - "period_end": "2022-07-20T12:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.2809 - }, - { - "period_end": "2022-07-20T12:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.3597 - }, - { - "period_end": "2022-07-20T11:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.4296 - }, - { - "period_end": "2022-07-20T11:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.4367 - }, - { - "period_end": "2022-07-20T10:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.2416 - }, - { - "period_end": "2022-07-20T10:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.0957 - }, - { - "period_end": "2022-07-20T09:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.3884 - }, - { - "period_end": "2022-07-20T09:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.3178 - }, - { - "period_end": "2022-07-20T08:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.2075 - }, - { - "period_end": "2022-07-20T08:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.0753 - }, - { - "period_end": "2022-07-20T06:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.5011 - }, - { - "period_end": "2022-07-20T06:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.2381 - }, - { - "period_end": "2022-07-20T05:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.9805 - }, - { - "period_end": "2022-07-20T05:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.574 - }, - { - "period_end": "2022-07-20T04:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.2516 - }, - { - "period_end": "2022-07-20T04:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0192 - }, - { - "period_end": "2022-07-20T03:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-20T03:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-20T02:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-20T02:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-20T01:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-20T01:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-20T00:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-20T00:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-19T23:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-19T23:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-19T22:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-19T22:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-19T21:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-19T21:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-19T20:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-19T20:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-19T19:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0231 - }, - { - "period_end": "2022-07-19T19:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0899 - }, - { - "period_end": "2022-07-19T18:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.2433 - }, - { - "period_end": "2022-07-19T18:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.4328 - }, - { - "period_end": "2022-07-19T17:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.6136 - }, - { - "period_end": "2022-07-19T17:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.8177 - }, - { - "period_end": "2022-07-19T16:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.0034 - }, - { - "period_end": "2022-07-19T16:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.2156 - }, - { - "period_end": "2022-07-19T15:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.4317 - }, - { - "period_end": "2022-07-19T15:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.6306 - }, - { - "period_end": "2022-07-19T14:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.8022 - }, - { - "period_end": "2022-07-19T14:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.9595 - }, - { - "period_end": "2022-07-19T13:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.0827 - }, - { - "period_end": "2022-07-19T13:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.1966 - }, - { - "period_end": "2022-07-19T12:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.2923 - }, - { - "period_end": "2022-07-19T12:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.351 - }, - { - "period_end": "2022-07-19T11:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.4274 - }, - { - "period_end": "2022-07-19T11:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.4127 - }, - { - "period_end": "2022-07-19T10:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.4196 - }, - { - "period_end": "2022-07-19T10:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.389 - }, - { - "period_end": "2022-07-19T09:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.3427 - }, - { - "period_end": "2022-07-19T09:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.2588 - }, - { - "period_end": "2022-07-19T08:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.1485 - }, - { - "period_end": "2022-07-19T08:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.0083 - }, - { - "period_end": "2022-07-19T07:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.8397 - }, - { - "period_end": "2022-07-19T07:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.6237 - }, - { - "period_end": "2022-07-19T06:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.3969 - }, - { - "period_end": "2022-07-19T06:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.1083 - }, - { - "period_end": "2022-07-19T05:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.7846 - }, - { - "period_end": "2022-07-19T05:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.5003 - }, - { - "period_end": "2022-07-19T04:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.1598 - }, - { - "period_end": "2022-07-19T04:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0173 - }, - { - "period_end": "2022-07-19T03:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-19T03:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-19T02:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-19T02:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-19T01:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-19T01:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-19T00:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-19T00:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-18T23:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-18T23:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-18T22:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-18T22:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-18T21:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-18T21:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-18T20:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-18T20:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-18T19:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0237 - }, - { - "period_end": "2022-07-18T19:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.093 - }, - { - "period_end": "2022-07-18T18:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.2511 - }, - { - "period_end": "2022-07-18T18:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.4379 - }, - { - "period_end": "2022-07-18T17:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.6213 - }, - { - "period_end": "2022-07-18T17:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.8362 - }, - { - "period_end": "2022-07-18T16:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.0591 - }, - { - "period_end": "2022-07-18T16:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.2492 - }, - { - "period_end": "2022-07-18T15:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.4614 - }, - { - "period_end": "2022-07-18T15:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.669 - }, - { - "period_end": "2022-07-18T14:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.8463 - }, - { - "period_end": "2022-07-18T14:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.0041 - }, - { - "period_end": "2022-07-18T13:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.1422 - }, - { - "period_end": "2022-07-18T13:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.2634 - }, - { - "period_end": "2022-07-18T12:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.3734 - }, - { - "period_end": "2022-07-18T12:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.4392 - }, - { - "period_end": "2022-07-18T11:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.5012 - }, - { - "period_end": "2022-07-18T11:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.4912 - }, - { - "period_end": "2022-07-18T10:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.5001 - }, - { - "period_end": "2022-07-18T10:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.4844 - }, - { - "period_end": "2022-07-18T09:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.4319 - }, - { - "period_end": "2022-07-18T09:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.3503 - }, - { - "period_end": "2022-07-18T08:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.2488 - }, - { - "period_end": "2022-07-18T08:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.1001 - }, - { - "period_end": "2022-07-18T07:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.911 - }, - { - "period_end": "2022-07-18T07:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.7075 - }, - { - "period_end": "2022-07-18T06:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.4574 - }, - { - "period_end": "2022-07-18T06:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.1741 - }, - { - "period_end": "2022-07-18T05:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.8316 - }, - { - "period_end": "2022-07-18T05:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.5323 - }, - { - "period_end": "2022-07-18T04:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.2142 - }, - { - "period_end": "2022-07-18T04:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0202 - }, - { - "period_end": "2022-07-18T03:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-18T03:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-18T02:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-18T02:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-18T01:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-18T01:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-18T00:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-18T00:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-17T23:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-17T23:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-17T22:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-17T22:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0 - }, - { - "period_end": "2022-07-17T21:30:00.0000000Z", - "period": "PT30M", - "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 - }, - { - "period_end": "2022-07-17T19:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0132 - }, - { - "period_end": "2022-07-17T19:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.0848 - }, - { - "period_end": "2022-07-17T18:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.2543 - }, - { - "period_end": "2022-07-17T18:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.4465 - }, - { - "period_end": "2022-07-17T17:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.6164 - }, - { - "period_end": "2022-07-17T17:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 0.8581 - }, - { - "period_end": "2022-07-17T16:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.0839 - }, - { - "period_end": "2022-07-17T16:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.2834 - }, - { - "period_end": "2022-07-17T15:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.5031 - }, - { - "period_end": "2022-07-17T15:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.724 - }, - { - "period_end": "2022-07-17T14:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 1.9318 - }, - { - "period_end": "2022-07-17T14:00:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.1015 - }, - { - "period_end": "2022-07-17T13:30:00.0000000Z", - "period": "PT30M", - "pv_estimate": 2.2374 - } - ], - "forecasts": [ - { - "period_end": "2022-07-24T13:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.1321, - "pv_estimate": 2.1321, - "pv_estimate10": 2.0481 - }, - { - "period_end": "2022-07-24T14:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.9722, - "pv_estimate": 1.9722, - "pv_estimate10": 1.8916 - }, - { - "period_end": "2022-07-24T14:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.8016, - "pv_estimate": 1.8016, - "pv_estimate10": 1.73 - }, - { - "period_end": "2022-07-24T15:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.6187, - "pv_estimate": 1.614, - "pv_estimate10": 1.5505 - }, - { - "period_end": "2022-07-24T15:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.4041, - "pv_estimate": 1.3883, - "pv_estimate10": 1.3572 - }, - { - "period_end": "2022-07-24T16:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.1857, - "pv_estimate": 1.1679, - "pv_estimate10": 1.1497 - }, - { - "period_end": "2022-07-24T16:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.9902, - "pv_estimate": 0.9767, - "pv_estimate10": 0.9619 - }, - { - "period_end": "2022-07-24T17:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.7437, - "pv_estimate": 0.7432, - "pv_estimate10": 0.704 - }, - { - "period_end": "2022-07-24T17:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.564795, - "pv_estimate": 0.5379, - "pv_estimate10": 0.5027 - }, - { - "period_end": "2022-07-24T18:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.35280000000000006, - "pv_estimate": 0.336, - "pv_estimate10": 0.3074 - }, - { - "period_end": "2022-07-24T18:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.17986500000000002, - "pv_estimate": 0.1713, - "pv_estimate10": 0.1369 - }, - { - "period_end": "2022-07-24T19:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.0642, - "pv_estimate": 0.0617, - "pv_estimate10": 0.0507 - }, - { - "period_end": "2022-07-24T19:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.00441, - "pv_estimate": 0.0042, - "pv_estimate10": 0.0021 - }, - { - "period_end": "2022-07-24T20:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-24T20:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-24T21:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-24T21:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-24T22:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-24T22:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-24T23:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-24T23:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-25T00:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-25T00:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-25T01:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-25T01:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-25T02:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-25T02:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-25T03:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-25T03:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-25T04:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-25T04:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.1608, - "pv_estimate": 0.1608, - "pv_estimate10": 0.0697 - }, - { - "period_end": "2022-07-25T05:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.5596, - "pv_estimate": 0.5596, - "pv_estimate10": 0.2133 - }, - { - "period_end": "2022-07-25T05:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.8647, - "pv_estimate": 0.8647, - "pv_estimate10": 0.4462 - }, - { - "period_end": "2022-07-25T06:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.2206, - "pv_estimate": 1.2206, - "pv_estimate10": 0.6935 - }, - { - "period_end": "2022-07-25T06:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.4745, - "pv_estimate": 1.4745, - "pv_estimate10": 0.934 - }, - { - "period_end": "2022-07-25T07:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.6943, - "pv_estimate": 1.6943, - "pv_estimate10": 1.1397 - }, - { - "period_end": "2022-07-25T07:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.9227, - "pv_estimate": 1.9227, - "pv_estimate10": 1.2972 - }, - { - "period_end": "2022-07-25T08:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.0759, - "pv_estimate": 2.0759, - "pv_estimate10": 1.4616 - }, - { - "period_end": "2022-07-25T08:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.2044, - "pv_estimate": 2.2044, - "pv_estimate10": 1.5608 - }, - { - "period_end": "2022-07-25T09:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.3179, - "pv_estimate": 2.3179, - "pv_estimate10": 1.682 - }, - { - "period_end": "2022-07-25T09:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.3743, - "pv_estimate": 2.3301, - "pv_estimate10": 1.6771 - }, - { - "period_end": "2022-07-25T10:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.4101, - "pv_estimate": 2.2845, - "pv_estimate10": 1.6089 - }, - { - "period_end": "2022-07-25T10:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.4287, - "pv_estimate": 2.2396, - "pv_estimate10": 1.5469 - }, - { - "period_end": "2022-07-25T11:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.4001, - "pv_estimate": 2.1727, - "pv_estimate10": 1.4196 - }, - { - "period_end": "2022-07-25T11:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.3508, - "pv_estimate": 2.0999, - "pv_estimate10": 1.2781 - }, - { - "period_end": "2022-07-25T12:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.2839, - "pv_estimate": 2.0046, - "pv_estimate10": 1.1158 - }, - { - "period_end": "2022-07-25T12:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.1987, - "pv_estimate": 1.8915, - "pv_estimate10": 0.9529 - }, - { - "period_end": "2022-07-25T13:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.0767, - "pv_estimate": 1.733, - "pv_estimate10": 0.7843 - }, - { - "period_end": "2022-07-25T13:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.9512, - "pv_estimate": 1.5699, - "pv_estimate10": 0.6383 - }, - { - "period_end": "2022-07-25T14:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.806, - "pv_estimate": 1.3873, - "pv_estimate10": 0.4946 - }, - { - "period_end": "2022-07-25T14:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.6276, - "pv_estimate": 1.1822, - "pv_estimate10": 0.3538 - }, - { - "period_end": "2022-07-25T15:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.4165, - "pv_estimate": 0.9462, - "pv_estimate10": 0.2048 - }, - { - "period_end": "2022-07-25T15:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.2499, - "pv_estimate": 0.8183, - "pv_estimate10": 0.1501 - }, - { - "period_end": "2022-07-25T16:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.0486, - "pv_estimate": 0.7015, - "pv_estimate10": 0.1378 - }, - { - "period_end": "2022-07-25T16:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.8492, - "pv_estimate": 0.5834, - "pv_estimate10": 0.1175 - }, - { - "period_end": "2022-07-25T17:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.6776, - "pv_estimate": 0.4387, - "pv_estimate10": 0.0889 - }, - { - "period_end": "2022-07-25T17:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.4669, - "pv_estimate": 0.2939, - "pv_estimate10": 0.0678 - }, - { - "period_end": "2022-07-25T18:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.2729, - "pv_estimate": 0.1554, - "pv_estimate10": 0.0411 - }, - { - "period_end": "2022-07-25T18:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.1183, - "pv_estimate": 0.0747, - "pv_estimate10": 0.023 - }, - { - "period_end": "2022-07-25T19:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.0454, - "pv_estimate": 0.0318, - "pv_estimate10": 0.0085 - }, - { - "period_end": "2022-07-25T19:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.0022, - "pv_estimate": 0.0022, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-25T20:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-25T20:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-25T21:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-25T21:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-25T22:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-25T22:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-25T23:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-25T23:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-26T00:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-26T00:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-26T01:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-26T01:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-26T02:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-26T02:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-26T03:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-26T03:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-26T04:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-26T04:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.1876, - "pv_estimate": 0.1195, - "pv_estimate10": 0.0229 - }, - { - "period_end": "2022-07-26T05:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.6319, - "pv_estimate": 0.46, - "pv_estimate10": 0.0641 - }, - { - "period_end": "2022-07-26T05:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.0401, - "pv_estimate": 0.8309, - "pv_estimate10": 0.1385 - }, - { - "period_end": "2022-07-26T06:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.337, - "pv_estimate": 1.1301, - "pv_estimate10": 0.3017 - }, - { - "period_end": "2022-07-26T06:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.5961, - "pv_estimate": 1.3265, - "pv_estimate10": 0.4624 - }, - { - "period_end": "2022-07-26T07:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.8278, - "pv_estimate": 1.4362, - "pv_estimate10": 0.5508 - }, - { - "period_end": "2022-07-26T07:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.0533, - "pv_estimate": 1.5659, - "pv_estimate10": 0.6205 - }, - { - "period_end": "2022-07-26T08:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.2208, - "pv_estimate": 1.6314, - "pv_estimate10": 0.6814 - }, - { - "period_end": "2022-07-26T08:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.3663, - "pv_estimate": 1.7112, - "pv_estimate10": 0.7082 - }, - { - "period_end": "2022-07-26T09:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.4823, - "pv_estimate": 1.7301, - "pv_estimate10": 0.6835 - }, - { - "period_end": "2022-07-26T09:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.5539, - "pv_estimate": 1.8156, - "pv_estimate10": 0.7121 - }, - { - "period_end": "2022-07-26T10:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.6156, - "pv_estimate": 1.9505, - "pv_estimate10": 0.8481 - }, - { - "period_end": "2022-07-26T10:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.6448, - "pv_estimate": 2.0697, - "pv_estimate10": 0.9466 - }, - { - "period_end": "2022-07-26T11:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.636, - "pv_estimate": 2.1259, - "pv_estimate10": 1.0131 - }, - { - "period_end": "2022-07-26T11:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.6058, - "pv_estimate": 2.1694, - "pv_estimate10": 1.0897 - }, - { - "period_end": "2022-07-26T12:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.5458, - "pv_estimate": 2.1759, - "pv_estimate10": 1.1808 - }, - { - "period_end": "2022-07-26T12:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.4696, - "pv_estimate": 2.1602, - "pv_estimate10": 1.2341 - }, - { - "period_end": "2022-07-26T13:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.3542, - "pv_estimate": 2.1008, - "pv_estimate10": 1.2619 - }, - { - "period_end": "2022-07-26T13:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.204, - "pv_estimate": 2.0034, - "pv_estimate10": 1.2487 - }, - { - "period_end": "2022-07-26T14:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.0528, - "pv_estimate": 1.8902, - "pv_estimate10": 1.2335 - }, - { - "period_end": "2022-07-26T14:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.8771, - "pv_estimate": 1.7498, - "pv_estimate10": 1.1984 - }, - { - "period_end": "2022-07-26T15:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.6508, - "pv_estimate": 1.5584, - "pv_estimate10": 1.1046 - }, - { - "period_end": "2022-07-26T15:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.4533, - "pv_estimate": 1.3928, - "pv_estimate10": 1.0041 - }, - { - "period_end": "2022-07-26T16:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.2208, - "pv_estimate": 1.1784, - "pv_estimate10": 0.849 - }, - { - "period_end": "2022-07-26T16:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.9781, - "pv_estimate": 0.9574, - "pv_estimate10": 0.6839 - }, - { - "period_end": "2022-07-26T17:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.7789, - "pv_estimate": 0.7788, - "pv_estimate10": 0.5103 - }, - { - "period_end": "2022-07-26T17:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.59304, - "pv_estimate": 0.5648, - "pv_estimate10": 0.334 - }, - { - "period_end": "2022-07-26T18:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.367815, - "pv_estimate": 0.3503, - "pv_estimate10": 0.1904 - }, - { - "period_end": "2022-07-26T18:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.19026, - "pv_estimate": 0.1812, - "pv_estimate10": 0.0851 - }, - { - "period_end": "2022-07-26T19:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.05775, - "pv_estimate": 0.055, - "pv_estimate10": 0.0312 - }, - { - "period_end": "2022-07-26T19:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-26T20:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-26T20:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-26T21:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-26T21:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-26T22:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-26T22:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-26T23:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-26T23:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-27T00:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-27T00:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-27T01:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-27T01:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-27T02:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-27T02:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-27T03:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-27T03:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-27T04:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-27T04:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.132, - "pv_estimate": 0.0242, - "pv_estimate10": 0.0024 - }, - { - "period_end": "2022-07-27T05:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.4986, - "pv_estimate": 0.067, - "pv_estimate10": 0.0072 - }, - { - "period_end": "2022-07-27T05:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.9302, - "pv_estimate": 0.144, - "pv_estimate10": 0.0118 - }, - { - "period_end": "2022-07-27T06:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.2524, - "pv_estimate": 0.293, - "pv_estimate10": 0.0164 - }, - { - "period_end": "2022-07-27T06:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.5654, - "pv_estimate": 0.5137, - "pv_estimate10": 0.0347 - }, - { - "period_end": "2022-07-27T07:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.8768, - "pv_estimate": 0.8088, - "pv_estimate10": 0.0939 - }, - { - "period_end": "2022-07-27T07:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.0971, - "pv_estimate": 1.1373, - "pv_estimate10": 0.2415 - }, - { - "period_end": "2022-07-27T08:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.2741, - "pv_estimate": 1.4973, - "pv_estimate10": 0.4666 - }, - { - "period_end": "2022-07-27T08:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.4277, - "pv_estimate": 1.8229, - "pv_estimate10": 0.7225 - }, - { - "period_end": "2022-07-27T09:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.5326, - "pv_estimate": 2.0874, - "pv_estimate10": 0.9995 - }, - { - "period_end": "2022-07-27T09:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.6296, - "pv_estimate": 2.2142, - "pv_estimate10": 1.1417 - }, - { - "period_end": "2022-07-27T10:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.6676, - "pv_estimate": 2.2069, - "pv_estimate10": 1.1647 - }, - { - "period_end": "2022-07-27T10:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.6937, - "pv_estimate": 2.1865, - "pv_estimate10": 1.1483 - }, - { - "period_end": "2022-07-27T11:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.6851, - "pv_estimate": 2.1513, - "pv_estimate10": 1.0764 - }, - { - "period_end": "2022-07-27T11:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.6328, - "pv_estimate": 2.0606, - "pv_estimate10": 1.0213 - }, - { - "period_end": "2022-07-27T12:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.5289, - "pv_estimate": 1.9687, - "pv_estimate10": 0.918 - }, - { - "period_end": "2022-07-27T12:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.4397, - "pv_estimate": 1.884, - "pv_estimate10": 0.8708 - }, - { - "period_end": "2022-07-27T13:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.3524, - "pv_estimate": 1.8269, - "pv_estimate10": 0.8187 - }, - { - "period_end": "2022-07-27T13:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.2098, - "pv_estimate": 1.7439, - "pv_estimate10": 0.7745 - }, - { - "period_end": "2022-07-27T14:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.0459, - "pv_estimate": 1.6448, - "pv_estimate10": 0.7264 - }, - { - "period_end": "2022-07-27T14:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.8706, - "pv_estimate": 1.5134, - "pv_estimate10": 0.6651 - }, - { - "period_end": "2022-07-27T15:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.6721, - "pv_estimate": 1.381, - "pv_estimate10": 0.5686 - }, - { - "period_end": "2022-07-27T15:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.4681, - "pv_estimate": 1.2282, - "pv_estimate10": 0.503 - }, - { - "period_end": "2022-07-27T16:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.227, - "pv_estimate": 1.0871, - "pv_estimate10": 0.4683 - }, - { - "period_end": "2022-07-27T16:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.9844, - "pv_estimate": 0.9202, - "pv_estimate10": 0.3936 - }, - { - "period_end": "2022-07-27T17:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.7464, - "pv_estimate": 0.7328, - "pv_estimate10": 0.2994 - }, - { - "period_end": "2022-07-27T17:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.579075, - "pv_estimate": 0.5515, - "pv_estimate10": 0.2016 - }, - { - "period_end": "2022-07-27T18:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.37254000000000004, - "pv_estimate": 0.3548, - "pv_estimate10": 0.1097 - }, - { - "period_end": "2022-07-27T18:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.16149, - "pv_estimate": 0.1538, - "pv_estimate10": 0.0583 - }, - { - "period_end": "2022-07-27T19:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.056805, - "pv_estimate": 0.0541, - "pv_estimate10": 0.0224 - }, - { - "period_end": "2022-07-27T19:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-27T20:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-27T20:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-27T21:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-27T21:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-27T22:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-27T22:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-27T23:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-27T23:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-28T00:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-28T00:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-28T01:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-28T01:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-28T02:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-28T02:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-28T03:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-28T03:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-28T04:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-28T04:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.1211, - "pv_estimate": 0.1211, - "pv_estimate10": 0.0237 - }, - { - "period_end": "2022-07-28T05:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.4283, - "pv_estimate": 0.4283, - "pv_estimate10": 0.0722 - }, - { - "period_end": "2022-07-28T05:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.8702, - "pv_estimate": 0.8702, - "pv_estimate10": 0.1567 - }, - { - "period_end": "2022-07-28T06:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.193, - "pv_estimate": 1.193, - "pv_estimate10": 0.3118 - }, - { - "period_end": "2022-07-28T06:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.5327, - "pv_estimate": 1.5327, - "pv_estimate10": 0.4794 - }, - { - "period_end": "2022-07-28T07:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.7815, - "pv_estimate": 1.7815, - "pv_estimate10": 0.682 - }, - { - "period_end": "2022-07-28T07:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.9945, - "pv_estimate": 1.9945, - "pv_estimate10": 0.8507 - }, - { - "period_end": "2022-07-28T08:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.1703, - "pv_estimate": 2.1703, - "pv_estimate10": 0.9928 - }, - { - "period_end": "2022-07-28T08:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.3272, - "pv_estimate": 2.3272, - "pv_estimate10": 1.1446 - }, - { - "period_end": "2022-07-28T09:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.4358, - "pv_estimate": 2.4358, - "pv_estimate10": 1.2873 - }, - { - "period_end": "2022-07-28T09:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.5199, - "pv_estimate": 2.5199, - "pv_estimate10": 1.3766 - }, - { - "period_end": "2022-07-28T10:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.581, - "pv_estimate": 2.581, - "pv_estimate10": 1.4629 - }, - { - "period_end": "2022-07-28T10:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.5983, - "pv_estimate": 2.5983, - "pv_estimate10": 1.5442 - }, - { - "period_end": "2022-07-28T11:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.5861, - "pv_estimate": 2.5861, - "pv_estimate10": 1.5737 - }, - { - "period_end": "2022-07-28T11:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.5549, - "pv_estimate": 2.5549, - "pv_estimate10": 1.5883 - }, - { - "period_end": "2022-07-28T12:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.482, - "pv_estimate": 2.482, - "pv_estimate10": 1.5772 - }, - { - "period_end": "2022-07-28T12:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.3967, - "pv_estimate": 2.3763, - "pv_estimate10": 1.525 - }, - { - "period_end": "2022-07-28T13:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.2855, - "pv_estimate": 2.248, - "pv_estimate10": 1.4473 - }, - { - "period_end": "2022-07-28T13:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.145, - "pv_estimate": 2.0829, - "pv_estimate10": 1.3305 - }, - { - "period_end": "2022-07-28T14:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.9721, - "pv_estimate": 1.9024, - "pv_estimate10": 1.2069 - }, - { - "period_end": "2022-07-28T14:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.7877, - "pv_estimate": 1.7149, - "pv_estimate10": 1.065 - }, - { - "period_end": "2022-07-28T15:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.5979, - "pv_estimate": 1.5313, - "pv_estimate10": 0.9167 - }, - { - "period_end": "2022-07-28T15:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.3653, - "pv_estimate": 1.3108, - "pv_estimate10": 0.7387 - }, - { - "period_end": "2022-07-28T16:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.1699, - "pv_estimate": 1.1384, - "pv_estimate10": 0.5699 - }, - { - "period_end": "2022-07-28T16:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.935, - "pv_estimate": 0.9258, - "pv_estimate10": 0.3935 - }, - { - "period_end": "2022-07-28T17:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.709, - "pv_estimate": 0.7074, - "pv_estimate10": 0.2312 - }, - { - "period_end": "2022-07-28T17:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.485, - "pv_estimate": 0.485, - "pv_estimate10": 0.1024 - }, - { - "period_end": "2022-07-28T18:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.3199, - "pv_estimate": 0.2845, - "pv_estimate10": 0.0549 - }, - { - "period_end": "2022-07-28T18:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.1203, - "pv_estimate": 0.1, - "pv_estimate10": 0.0213 - }, - { - "period_end": "2022-07-28T19:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.0492, - "pv_estimate": 0.0343, - "pv_estimate10": 0.0065 - }, - { - "period_end": "2022-07-28T19:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-28T20:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-28T20:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-28T21:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-28T21:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-28T22:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-28T22:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-28T23:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-28T23:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-29T00:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-29T00:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-29T01:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-29T01:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-29T02:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-29T02:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-29T03:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-29T03:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-29T04:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-29T04:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.0903, - "pv_estimate": 0.0137, - "pv_estimate10": 0.0023 - }, - { - "period_end": "2022-07-29T05:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.4174, - "pv_estimate": 0.0458, - "pv_estimate10": 0.0046 - }, - { - "period_end": "2022-07-29T05:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.7295, - "pv_estimate": 0.0883, - "pv_estimate10": 0.0091 - }, - { - "period_end": "2022-07-29T06:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.0121, - "pv_estimate": 0.1785, - "pv_estimate10": 0.0134 - }, - { - "period_end": "2022-07-29T06:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.3596, - "pv_estimate": 0.3133, - "pv_estimate10": 0.0202 - }, - { - "period_end": "2022-07-29T07:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.6687, - "pv_estimate": 0.491, - "pv_estimate10": 0.0244 - }, - { - "period_end": "2022-07-29T07:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.9075, - "pv_estimate": 0.6666, - "pv_estimate10": 0.0311 - }, - { - "period_end": "2022-07-29T08:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.0894, - "pv_estimate": 0.8303, - "pv_estimate10": 0.0351 - }, - { - "period_end": "2022-07-29T08:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.2649, - "pv_estimate": 1.0202, - "pv_estimate10": 0.0395 - }, - { - "period_end": "2022-07-29T09:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.3779, - "pv_estimate": 1.1751, - "pv_estimate10": 0.0435 - }, - { - "period_end": "2022-07-29T09:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.461, - "pv_estimate": 1.3177, - "pv_estimate10": 0.0452 - }, - { - "period_end": "2022-07-29T10:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.5244, - "pv_estimate": 1.4392, - "pv_estimate10": 0.0495 - }, - { - "period_end": "2022-07-29T10:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.5453, - "pv_estimate": 1.5754, - "pv_estimate10": 0.0512 - }, - { - "period_end": "2022-07-29T11:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.545, - "pv_estimate": 1.6459, - "pv_estimate10": 0.0512 - }, - { - "period_end": "2022-07-29T11:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.5033, - "pv_estimate": 1.6883, - "pv_estimate10": 0.0549 - }, - { - "period_end": "2022-07-29T12:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.43, - "pv_estimate": 1.707, - "pv_estimate10": 0.0586 - }, - { - "period_end": "2022-07-29T12:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.3594, - "pv_estimate": 1.7087, - "pv_estimate10": 0.0613 - }, - { - "period_end": "2022-07-29T13:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.2445, - "pv_estimate": 1.6797, - "pv_estimate10": 0.0591 - }, - { - "period_end": "2022-07-29T13:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.1377, - "pv_estimate": 1.6333, - "pv_estimate10": 0.0725 - }, - { - "period_end": "2022-07-29T14:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.9722, - "pv_estimate": 1.5526, - "pv_estimate10": 0.084 - }, - { - "period_end": "2022-07-29T14:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.7984, - "pv_estimate": 1.4223, - "pv_estimate10": 0.1022 - }, - { - "period_end": "2022-07-29T15:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.6112, - "pv_estimate": 1.3074, - "pv_estimate10": 0.0944 - }, - { - "period_end": "2022-07-29T15:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.3821, - "pv_estimate": 1.133, - "pv_estimate10": 0.0944 - }, - { - "period_end": "2022-07-29T16:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.1828, - "pv_estimate": 0.9672, - "pv_estimate10": 0.0834 - }, - { - "period_end": "2022-07-29T16:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.9523, - "pv_estimate": 0.7787, - "pv_estimate10": 0.0754 - }, - { - "period_end": "2022-07-29T17:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.7209, - "pv_estimate": 0.587, - "pv_estimate10": 0.0599 - }, - { - "period_end": "2022-07-29T17:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.4914, - "pv_estimate": 0.3894, - "pv_estimate10": 0.0399 - }, - { - "period_end": "2022-07-29T18:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.327, - "pv_estimate": 0.2181, - "pv_estimate10": 0.0222 - }, - { - "period_end": "2022-07-29T18:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.1263, - "pv_estimate": 0.0866, - "pv_estimate10": 0.0112 - }, - { - "period_end": "2022-07-29T19:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.0474, - "pv_estimate": 0.029, - "pv_estimate10": 0.0022 - }, - { - "period_end": "2022-07-29T19:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-29T20:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-29T20:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-29T21:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-29T21:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-29T22:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-29T22:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-29T23:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-29T23:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-30T00:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-30T00:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-30T01:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-30T01:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-30T02:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-30T02:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-30T03:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-30T03:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-30T04:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-30T04:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.0758, - "pv_estimate": 0.0206, - "pv_estimate10": 0.0023 - }, - { - "period_end": "2022-07-30T05:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.4099, - "pv_estimate": 0.0706, - "pv_estimate10": 0.0045 - }, - { - "period_end": "2022-07-30T05:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.7547, - "pv_estimate": 0.1613, - "pv_estimate10": 0.0091 - }, - { - "period_end": "2022-07-30T06:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.1231, - "pv_estimate": 0.3232, - "pv_estimate10": 0.0157 - }, - { - "period_end": "2022-07-30T06:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.416, - "pv_estimate": 0.5442, - "pv_estimate10": 0.0288 - }, - { - "period_end": "2022-07-30T07:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.6691, - "pv_estimate": 0.7798, - "pv_estimate10": 0.0527 - }, - { - "period_end": "2022-07-30T07:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.8862, - "pv_estimate": 1.0171, - "pv_estimate10": 0.0826 - }, - { - "period_end": "2022-07-30T08:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.0773, - "pv_estimate": 1.2267, - "pv_estimate10": 0.1229 - }, - { - "period_end": "2022-07-30T08:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.2186, - "pv_estimate": 1.4355, - "pv_estimate10": 0.2023 - }, - { - "period_end": "2022-07-30T09:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.345, - "pv_estimate": 1.633, - "pv_estimate10": 0.3015 - }, - { - "period_end": "2022-07-30T09:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.4218, - "pv_estimate": 1.7818, - "pv_estimate10": 0.3909 - }, - { - "period_end": "2022-07-30T10:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.4923, - "pv_estimate": 1.8994, - "pv_estimate10": 0.4771 - }, - { - "period_end": "2022-07-30T10:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.5126, - "pv_estimate": 2.0092, - "pv_estimate10": 0.5639 - }, - { - "period_end": "2022-07-30T11:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.5177, - "pv_estimate": 2.0631, - "pv_estimate10": 0.6123 - }, - { - "period_end": "2022-07-30T11:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.4786, - "pv_estimate": 2.089, - "pv_estimate10": 0.6733 - }, - { - "period_end": "2022-07-30T12:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.4081, - "pv_estimate": 2.0861, - "pv_estimate10": 0.7334 - }, - { - "period_end": "2022-07-30T12:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.3253, - "pv_estimate": 2.0441, - "pv_estimate10": 0.7562 - }, - { - "period_end": "2022-07-30T13:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.2143, - "pv_estimate": 1.9712, - "pv_estimate10": 0.756 - }, - { - "period_end": "2022-07-30T13:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.0767, - "pv_estimate": 1.8678, - "pv_estimate10": 0.7281 - }, - { - "period_end": "2022-07-30T14:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.9265, - "pv_estimate": 1.747, - "pv_estimate10": 0.6843 - }, - { - "period_end": "2022-07-30T14:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.7378, - "pv_estimate": 1.5933, - "pv_estimate10": 0.6253 - }, - { - "period_end": "2022-07-30T15:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.5505, - "pv_estimate": 1.4346, - "pv_estimate10": 0.5533 - }, - { - "period_end": "2022-07-30T15:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.3349, - "pv_estimate": 1.2546, - "pv_estimate10": 0.4644 - }, - { - "period_end": "2022-07-30T16:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.111, - "pv_estimate": 1.0587, - "pv_estimate10": 0.3831 - }, - { - "period_end": "2022-07-30T16:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.9177, - "pv_estimate": 0.8771, - "pv_estimate10": 0.2864 - }, - { - "period_end": "2022-07-30T17:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.6899, - "pv_estimate": 0.662, - "pv_estimate10": 0.1838 - }, - { - "period_end": "2022-07-30T17:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.4735, - "pv_estimate": 0.4494, - "pv_estimate10": 0.0944 - }, - { - "period_end": "2022-07-30T18:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.2742, - "pv_estimate": 0.2546, - "pv_estimate10": 0.0565 - }, - { - "period_end": "2022-07-30T18:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.1182, - "pv_estimate": 0.097, - "pv_estimate10": 0.0253 - }, - { - "period_end": "2022-07-30T19:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.0365, - "pv_estimate": 0.0336, - "pv_estimate10": 0.0085 - }, - { - "period_end": "2022-07-30T19:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-30T20:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-30T20:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-30T21:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-30T21:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-30T22:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-30T22:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-30T23:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-30T23:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-31T00:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-31T00:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-31T01:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-31T01:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-31T02:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-31T02:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-31T03:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-31T03:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-31T04:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0, - "pv_estimate": 0, - "pv_estimate10": 0 - }, - { - "period_end": "2022-07-31T04:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.06, - "pv_estimate": 0.0413, - "pv_estimate10": 0.009 - }, - { - "period_end": "2022-07-31T05:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.3783, - "pv_estimate": 0.1838, - "pv_estimate10": 0.0355 - }, - { - "period_end": "2022-07-31T05:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 0.7147, - "pv_estimate": 0.4491, - "pv_estimate10": 0.0838 - }, - { - "period_end": "2022-07-31T06:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.1041, - "pv_estimate": 0.7898, - "pv_estimate10": 0.1673 - }, - { - "period_end": "2022-07-31T06:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.3838, - "pv_estimate": 1.0963, - "pv_estimate10": 0.3141 - }, - { - "period_end": "2022-07-31T07:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.6292, - "pv_estimate": 1.3641, - "pv_estimate10": 0.4846 - }, - { - "period_end": "2022-07-31T07:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 1.8644, - "pv_estimate": 1.589, - "pv_estimate10": 0.6206 - }, - { - "period_end": "2022-07-31T08:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.0284, - "pv_estimate": 1.772, - "pv_estimate10": 0.7664 - }, - { - "period_end": "2022-07-31T08:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.17, - "pv_estimate": 1.9404, - "pv_estimate10": 0.8994 - }, - { - "period_end": "2022-07-31T09:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.2885, - "pv_estimate": 2.0701, - "pv_estimate10": 1.0292 - }, - { - "period_end": "2022-07-31T09:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.3646, - "pv_estimate": 2.1733, - "pv_estimate10": 1.1289 - }, - { - "period_end": "2022-07-31T10:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.4348, - "pv_estimate": 2.2607, - "pv_estimate10": 1.2208 - }, - { - "period_end": "2022-07-31T10:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.4527, - "pv_estimate": 2.2881, - "pv_estimate10": 1.2942 - }, - { - "period_end": "2022-07-31T11:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.444, - "pv_estimate": 2.2938, - "pv_estimate10": 1.3481 - }, - { - "period_end": "2022-07-31T11:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.4172, - "pv_estimate": 2.2856, - "pv_estimate10": 1.3635 - }, - { - "period_end": "2022-07-31T12:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.3487, - "pv_estimate": 2.2342, - "pv_estimate10": 1.3687 - }, - { - "period_end": "2022-07-31T12:30:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.2764, - "pv_estimate": 2.159, - "pv_estimate10": 1.3267 - }, - { - "period_end": "2022-07-31T13:00:00.0000000Z", - "period": "PT30M", - "pv_estimate90": 2.1679, - "pv_estimate": 2.0549, - "pv_estimate10": 1.2353 - } - ] -} From c3b494d36ef9295ad8dc526fb72c70e07a5f9d33 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Thu, 28 Jul 2022 11:18:56 +0200 Subject: [PATCH 008/111] alpha version for marketplace Signed-off-by: Bernd Weymann --- .../README.md | 113 +++++++++++------- .../SolarForecastBindingConstants.java | 14 +-- .../ForecastSolarBridgeHandler.java | 8 +- .../forecastsolar/ForecastSolarObject.java | 6 +- .../ForecastSolarPlaneHandler.java | 4 +- .../solcast/SolcastBridgeHandler.java | 20 ++-- .../internal/solcast/SolcastObject.java | 6 +- .../internal/solcast/SolcastPlaneHandler.java | 12 +- ...ne-part-config.xml => fs-plane-config.xml} | 2 +- ...s-bridge-config.xml => fs-site-config.xml} | 2 +- ...ne-part-config.xml => sc-plane-config.xml} | 2 +- ...c-bridge-config.xml => sc-site-config.xml} | 2 +- .../resources/OH-INF/thing/channel-types.xml | 6 +- .../{fs-plane-part.xml => fs-plane-type.xml} | 10 +- ...bridge-type-multi.xml => fs-site-type.xml} | 12 +- ...-thing-type-part.xml => sc-plane-type.xml} | 12 +- ...bridge-type-multi.xml => sc-site-type.xml} | 14 +-- bundles/pom.xml | 1 + 18 files changed, 139 insertions(+), 107 deletions(-) rename bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/{fs-plane-part-config.xml => fs-plane-config.xml} (95%) rename bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/{fs-bridge-config.xml => fs-site-config.xml} (94%) rename bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/{sc-plane-part-config.xml => sc-plane-config.xml} (93%) rename bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/{sc-bridge-config.xml => sc-site-config.xml} (92%) rename bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/{fs-plane-part.xml => fs-plane-type.xml} (82%) rename bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/{fs-bridge-type-multi.xml => fs-site-type.xml} (71%) rename bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/{sc-thing-type-part.xml => sc-plane-type.xml} (83%) rename bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/{sc-bridge-type-multi.xml => sc-site-type.xml} (77%) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 7ce0e4cb8cc76..5dbbdd1a1e77c 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -2,25 +2,26 @@ 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 yor energy planning. +Look ahead the next days in order to identify surplus / shortages in your energy planning. Supported Services - [Solcast](https://solcast.com/) - - [Hobbyist Plan](https://toolkit.solcast.com.au/register/hobbyist) + - 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 ## Supported Things -Each service needs one `Bridge` for your location and 1+ Photovaltaic Plane `Things` +Each service needs one `Bridge` for your location and 1+ Photovoltaic Plane `Things` -| Name | Thing Type ID | -|------------------------------|---------------| -| Solcast Plane Bridge | sc-multi | -| Solcast PV Plane | sc-plane | -| Forecast Solar Plane Bridge | fs-multi | -| Forecast Solar PV Plane | fs-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 @@ -47,40 +48,42 @@ After configuration the necessary information is available. | refreshInterval | integer | Forecast Refresh Interval in minutes | 120 | yes | `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've 25 free calls per day, each plane needs 2 call per update a refresh interval of 120 minutes will result in 24 call per day. -Nore: `channelRefreshInterval` from [Bridge Configuration](#solcast-bridge-configuration) will calculate intermediate values without requesting new forecast data. + +Note: `channelRefreshInterval` from [Bridge Configuration](#solcast-bridge-configuration) will calculate intermediate values without requesting new forecast data. ## Solcast Channels Each Plane Thing reports their specific values including a `raw` channel holding json content. -The Bridge sums up all `Plane Thing` values and provides the total forecast for your home location. +The Bridge sums up all attched `sc-pLane` values and provides the total forecast for your home location. -Channels are covering todays actual data with current, remaining and todays total prediction. +Channels are covering today's actual data with current, remaining and today's total prediction. Forecasts are delivered up to 6 days in advance including -- a pessismitic scenario: 10th percentile +- a pessimistic scenario: 10th percentile - an optimistic scenario: 90th percentile +Day*X* channels are referring to forecasts plus *X* days: 1 = tomorrow, 2 = day after tomorrow, ... + | Channel | Type | Description | |-------------------------|---------------|-----------------------------------------| -| actual-channel | Number:Energy | Todays forecast till now | -| remaining-channel | Number:Energy | Forecast of todays remaining production | -| today-channel | Number:Energy | Todays forecast in total | -| tomorrow-channel | Number:Energy | Tomorrows forecast in total | -| tomorrow-low-channel | Number:Energy | Tomorrows pessimistic forecast | -| tomorrow-high-channel | Number:Energy | Tomorrows optimistic forecast | -| day`X`-channel | Number:Energy | Day `X` forecast in total | -| day`X`-low-channel | Number:Energy | Day `X` pessimistic forecast | -| day`X`-high-channel | Number:Energy | Day `X` optimistic forecast | +| actual-channel | Number:Energy | Today's forecast till now | +| remaining-channel | Number:Energy | Forecast of today's remaining production | +| today-channel | Number:Energy | Today's forecast in total | +| day*X*-channel | Number:Energy | Day *X* forecast in total | +| day*X*-low-channel | Number:Energy | Day *X* pessimistic forecast | +| day*X*-high-channel | Number:Energy | Day *X* optimistic forecast | | raw | String | Plain JSON response without conversions | ## ForecastSolar Configuration -[ForecastSolar service](https://forecast.solar/) provides a public free plan. +[ForecastSolar service](https://forecast.solar/) provides a [public free](https://forecast.solar/#accounts) plan. +You can try this out without any registration or other pre-conditions. ### ForecastSolar Bridge Configuration @@ -90,8 +93,9 @@ Forecasts are delivered up to 6 days in advance including | channelRefreshInterval | integer | Channel Refresh Interval in minutes | 1 | yes | | apiKey | text | API Key | N/A | no | -`location` defines latitufe,longitude values of your PV system. -In case of autodetect the location configured in openHAB is obtained. +`location` defines latitude, longitude values of your PV system. +In case of auto-detect the location configured in openHAB is obtained. + `apiKey` can be given in case you subscribed to a paid plan @@ -100,13 +104,14 @@ In case of autodetect the location configured in openHAB is obtained. | Name | Type | Description | Default | Required | |-----------------|---------|------------------------------------------------------------------------------|---------|----------| | refreshInterval | integer | Forecast Refresh Interval in minutes | 30 | yes | -| declination | integer | Plane Declination - 0 for horizontal till 90 for vertical declination | N/A | yes | -| azimuth | integer | Plane Azimuth - -180 = north, -90 = east, 0 = south, 90 = west, 180 = north | N/A | yes | +| declination | integer | Plane Declination: 0 for horizontal till 90 for vertical declination | N/A | yes | +| azimuth | integer | Plane Azimuth: -180 = north, -90 = east, 0 = south, 90 = west, 180 = north | N/A | yes | | kwp | decimal | Installed Kilowatt Peak | N/A | yes | `refreshInterval` of forecast data needs to respect the throttling of the ForecastSolar service. -There're 12 calls per hour allowed from your caller IP address so for 2 planes lowest possible refresh rate is 10 minutes. -Nore: `channelRefreshInterval` from [Bridge Configuration](#forecastsolar-bridge-configuration) will calculate intermediate values without requesting new forecast data. +12 calls per hour allowed from your caller IP address so for 2 planes lowest possible refresh rate is 10 minutes. + +Note: `channelRefreshInterval` from [Bridge Configuration](#forecastsolar-bridge-configuration) will calculate intermediate values without requesting new forecast data. ## ForecastSolar Channels @@ -114,21 +119,49 @@ Nore: `channelRefreshInterval` from [Bridge Configuration](#forecastsolar-bridge Each Plane Thing reports their specific values including a `raw` channel holding json content. The Bridge sums up all `Plane Thing` values and provides the total forecast for your home location. -Channels are covering todays actual data with current, remaining and todays total prediction. +Channels are covering todays actual data with current, remaining and today's total prediction. Forecasts are delivered up to 3 days for paid personal plans. +Day*X* channels are referring to forecasts plus *X* days: 1 = tomorrow, 2 = day after tomorrow, ... + + | Channel | Type | Description | |-------------------------|---------------|-----------------------------------------| -| actual-channel | Number:Energy | Todays forecast till now | -| remaining-channel | Number:Energy | Forecast of todays remaining production | -| today-channel | Number:Energy | Todays forecast in total | -| tomorrow-channel | Number:Energy | Tomorrows forecast in total | -| day`X`-channel | Number:Energy | Day `X` forecast in total | +| actual-channel | Number:Energy | Today's forecast till now | +| remaining-channel | Number:Energy | Forecast of today's remaining production | +| today-channel | Number:Energy | Today's forecast in total | +| day*X*-channel | Number:Energy | Day *X* forecast in total | | raw | String | Plain JSON response without conversions | -## Full Example - -_Provide a full usage example based on textual configuration files._ -_*.things, *.items examples are mandatory as textual configuration is well used by many users._ -_*.sitemap examples are optional._ +## 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 + +```` +Bridge solarforecast:fs-site:homeSite "ForecastSolar Home" [ location="54.321,8.976", channelRefreshInterval="1"] { + Thing fs-plane homeSouthWest "ForecastSolar Home South-West" [ refreshInterval=10, azimuth=45, declination=35, kwp=5.5] + Thing fs-plane homeNorthEast "ForecastSolar Home North-East" [ refreshInterval=10, azimuth=-145, declination=35, kwp=4.425] +} +```` +### Items file + +```` +Number:Energy ForecastSolarHome_Actual "Actual Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-site:homeSite:actual" } +Number:Energy ForecastSolarHome_Remaining "Remaining Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-site:homeSite:remaining" } +Number:Energy ForecastSolarHome_Today "Today Total Forecast [%3.f %unit%]" {channel="solarforecast:fs-site:homeSite:today" } +Number:Energy ForecastSolarHome_Day1 "Tomorrow Total Forecast [%3.f %unit%]" {channel="solarforecast:fs-site:homeSite:day1" } + +Number:Energy ForecastSolarHome_Actual_NE "Actual NE Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:actual" } +Number:Energy ForecastSolarHome_Remaining_NE "Remaining NE Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:remaining" } +Number:Energy ForecastSolarHome_Today_NE "Total NE Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:today" } +Number:Energy ForecastSolarHome_Day_NE "Tomorrow NE Forecast [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:day1" } + +Number:Energy ForecastSolarHome_Actual_SW "Actual SW Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:actual" } +Number:Energy ForecastSolarHome_Remaining_SW "Remaining SW Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:remaining" } +Number:Energy ForecastSolarHome_Today_SW "Total SW Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:today" } +Number:Energy ForecastSolarHome_Day_SW "Tomorrow SW Forecast [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:day1" } +```` 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 index 015b610865e30..8f03a73a1c366 100644 --- 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 @@ -28,19 +28,19 @@ public class SolarForecastBindingConstants { private static final String BINDING_ID = "solarforecast"; - public static final ThingTypeUID FORECAST_SOLAR_MULTI_STRING = new ThingTypeUID(BINDING_ID, "fs-multi"); - public static final ThingTypeUID FORECAST_SOLAR_PART_STRING = new ThingTypeUID(BINDING_ID, "fs-part"); - public static final ThingTypeUID SOLCAST_BRIDGE_STRING = new ThingTypeUID(BINDING_ID, "sc-multi"); - public static final ThingTypeUID SOLCAST_PART_STRING = new ThingTypeUID(BINDING_ID, "sc-part"); + public static final ThingTypeUID FORECAST_SOLAR_MULTI_STRING = new ThingTypeUID(BINDING_ID, "fs-site"); + public static final ThingTypeUID FORECAST_SOLAR_PART_STRING = new ThingTypeUID(BINDING_ID, "fs-plane"); + public static final ThingTypeUID SOLCAST_BRIDGE_STRING = new ThingTypeUID(BINDING_ID, "sc-site"); + public static final ThingTypeUID SOLCAST_PART_STRING = new ThingTypeUID(BINDING_ID, "sc-plane"); public static final Set SUPPORTED_THING_SET = Set.of(FORECAST_SOLAR_MULTI_STRING, FORECAST_SOLAR_PART_STRING, SOLCAST_BRIDGE_STRING, SOLCAST_PART_STRING); public static final String CHANNEL_TODAY = "today"; public static final String CHANNEL_ACTUAL = "actual"; public static final String CHANNEL_REMAINING = "remaining"; - public static final String CHANNEL_TOMORROW = "tomorrow"; - public static final String CHANNEL_TOMORROW_LOW = "tomorrow-low"; - public static final String CHANNEL_TOMORROW_HIGH = "tomorrow-high"; + public static final String CHANNEL_DAY1 = "day1"; + public static final String CHANNEL_DAY1_LOW = "day1-low"; + public static final String CHANNEL_DAY1_HIGH = "day1-high"; public static final String CHANNEL_DAY2 = "day2"; public static final String CHANNEL_DAY2_LOW = "day2-low"; public static final String CHANNEL_DAY2_HIGH = "day2-high"; diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java index c82ddc93bd078..873e7c2d7590e 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java @@ -90,14 +90,14 @@ private void startSchedule(int interval) { */ private synchronized void getData() { if (parts.isEmpty()) { - logger.info("No plane defined yet"); + logger.debug("No PV plane defined yet"); return; } LocalDateTime now = LocalDateTime.now(); double actualSum = 0; double remainSum = 0; double todaySum = 0; - double tomorrowSum = 0; + double day1Sum = 0; double day2Sum = 0; double day3Sum = 0; for (Iterator iterator = parts.iterator(); iterator.hasNext();) { @@ -107,7 +107,7 @@ private synchronized void getData() { actualSum += fo.getActualValue(now); remainSum += fo.getRemainingProduction(now); todaySum += fo.getDayTotal(now, 0); - tomorrowSum += fo.getDayTotal(now, 1); + day1Sum += fo.getDayTotal(now, 1); day2Sum += fo.getDayTotal(now, 2); day3Sum += fo.getDayTotal(now, 3); } else { @@ -117,7 +117,7 @@ private synchronized void getData() { updateState(CHANNEL_REMAINING, SolcastObject.getStateObject(remainSum)); updateState(CHANNEL_ACTUAL, SolcastObject.getStateObject(actualSum)); updateState(CHANNEL_TODAY, SolcastObject.getStateObject(todaySum)); - updateState(CHANNEL_TOMORROW, SolcastObject.getStateObject(tomorrowSum)); + updateState(CHANNEL_DAY1, SolcastObject.getStateObject(day1Sum)); updateState(CHANNEL_DAY2, SolcastObject.getStateObject(day2Sum)); updateState(CHANNEL_DAY3, SolcastObject.getStateObject(day3Sum)); } 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 index 4fb43f286920e..702846fbebe7b 100644 --- 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 @@ -74,13 +74,13 @@ public boolean isValid() { if (expirationDateTime.isAfter(LocalDateTime.now())) { return true; } else { - logger.info("Forecast data expired"); + logger.debug("Forecast data expired"); } } else { - logger.info("Empty data map"); + logger.debug("Empty data map"); } } else { - logger.info("No Forecast data available"); + logger.debug("No Forecast data available"); } return false; } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java index 5a596b1fd4e23..6f5ec98ee737b 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java @@ -119,7 +119,7 @@ protected ForecastSolarObject fetchData() { + location.get().getLongitude() + SLASH + configuration.get().declination + SLASH + configuration.get().azimuth + SLASH + configuration.get().kwp; } - logger.info("{} Call {}", thing.getLabel(), url); + logger.debug("{} Call {}", thing.getLabel(), url); try { ContentResponse cr = httpClient.GET(url); if (cr.getStatus() == 200) { @@ -148,7 +148,7 @@ private void updateChannels(ForecastSolarObject f) { updateState(CHANNEL_ACTUAL, SolcastObject.getStateObject(f.getActualValue(LocalDateTime.now()))); updateState(CHANNEL_REMAINING, SolcastObject.getStateObject(f.getRemainingProduction(LocalDateTime.now()))); updateState(CHANNEL_TODAY, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 0))); - updateState(CHANNEL_TOMORROW, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 1))); + updateState(CHANNEL_DAY1, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 1))); updateState(CHANNEL_DAY2, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 2))); updateState(CHANNEL_DAY3, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 3))); } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java index c09541a3330f3..d4b93ec251f92 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java @@ -83,16 +83,16 @@ private void startSchedule(int interval) { */ private synchronized void getData() { if (parts.isEmpty()) { - logger.info("No plane defined yet"); + logger.debug("No PV plane defined yet"); return; } LocalDateTime now = LocalDateTime.now(); double actualSum = 0; double remainSum = 0; double todaySum = 0; - double tomorrowSum = 0; - double tomorrowSumLow = 0; - double tomorrowSumHigh = 0; + double day1Sum = 0; + double day1SumLow = 0; + double day1SumHigh = 0; double day2Sum = 0; double day2SumLow = 0; double day2SumHigh = 0; @@ -116,9 +116,9 @@ private synchronized void getData() { actualSum += fo.getActualValue(now); remainSum += fo.getRemainingProduction(now); todaySum += fo.getDayTotal(now, 0); - tomorrowSum += fo.getDayTotal(now, 1); - tomorrowSumLow += fo.getPessimisticDayTotal(now, 1); - tomorrowSumHigh += fo.getOptimisticDayTotal(now, 1); + day1Sum += fo.getDayTotal(now, 1); + day1SumLow += fo.getPessimisticDayTotal(now, 1); + day1SumHigh += fo.getOptimisticDayTotal(now, 1); day2Sum += fo.getDayTotal(now, 2); day2SumLow += fo.getPessimisticDayTotal(now, 2); day2SumHigh += fo.getOptimisticDayTotal(now, 2); @@ -141,9 +141,9 @@ private synchronized void getData() { updateState(CHANNEL_ACTUAL, SolcastObject.getStateObject(actualSum)); updateState(CHANNEL_REMAINING, SolcastObject.getStateObject(remainSum)); updateState(CHANNEL_TODAY, SolcastObject.getStateObject(todaySum)); - updateState(CHANNEL_TOMORROW, SolcastObject.getStateObject(tomorrowSum)); - updateState(CHANNEL_TOMORROW_HIGH, SolcastObject.getStateObject(tomorrowSumHigh)); - updateState(CHANNEL_TOMORROW_LOW, SolcastObject.getStateObject(tomorrowSumLow)); + updateState(CHANNEL_DAY1, SolcastObject.getStateObject(day1Sum)); + updateState(CHANNEL_DAY1_HIGH, SolcastObject.getStateObject(day1SumHigh)); + updateState(CHANNEL_DAY1_LOW, SolcastObject.getStateObject(day1SumLow)); updateState(CHANNEL_DAY2, SolcastObject.getStateObject(day2Sum)); updateState(CHANNEL_DAY2_HIGH, SolcastObject.getStateObject(day2SumHigh)); updateState(CHANNEL_DAY2_LOW, SolcastObject.getStateObject(day2SumLow)); 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 index e023e986b83ad..f0867f819ff09 100644 --- 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 @@ -125,13 +125,13 @@ public boolean isValid() { if (expirationDateTime.isAfter(LocalDateTime.now())) { return true; } else { - logger.info("Forecast data expired"); + logger.debug("Forecast data expired"); } } else { - logger.info("Empty data map"); + logger.debug("Empty data map"); } } else { - logger.info("No Forecast data available"); + logger.debug("No Forecast data available"); } return false; } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java index 1789c7b5f0f8c..e365e2aebe129 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java @@ -104,7 +104,7 @@ protected SolcastObject fetchData() { if (!forecast.isValid()) { String forecastUrl = String.format(FORECAST_URL, configuration.get().resourceId); String currentEstimateUrl = String.format(CURRENT_ESTIMATE_URL, configuration.get().resourceId); - logger.info("{} Call {}", thing.getLabel(), currentEstimateUrl); + logger.debug("{} Call {}", thing.getLabel(), currentEstimateUrl); try { // get actual estimate Request estimateRequest = httpClient.newRequest(currentEstimateUrl); @@ -116,7 +116,7 @@ protected SolcastObject fetchData() { logger.trace("{} Fetched data {}", thing.getLabel(), forecast.toString()); // get forecast - logger.info("{} Call {}", thing.getLabel(), forecastUrl); + logger.debug("{} Call {}", thing.getLabel(), forecastUrl); Request forecastRequest = httpClient.newRequest(forecastUrl); forecastRequest.header(HttpHeader.AUTHORIZATION, BEARER + bridgeHandler.get().getApiKey()); ContentResponse crForecast = forecastRequest.send(); @@ -148,11 +148,9 @@ private void updateChannels(SolcastObject f) { updateState(CHANNEL_ACTUAL, SolcastObject.getStateObject(f.getActualValue(LocalDateTime.now()))); updateState(CHANNEL_REMAINING, SolcastObject.getStateObject(f.getRemainingProduction(LocalDateTime.now()))); updateState(CHANNEL_TODAY, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 0))); - updateState(CHANNEL_TOMORROW, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 1))); - updateState(CHANNEL_TOMORROW_HIGH, - SolcastObject.getStateObject(f.getOptimisticDayTotal(LocalDateTime.now(), 1))); - updateState(CHANNEL_TOMORROW_LOW, - SolcastObject.getStateObject(f.getPessimisticDayTotal(LocalDateTime.now(), 1))); + updateState(CHANNEL_DAY1, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 1))); + updateState(CHANNEL_DAY1_HIGH, SolcastObject.getStateObject(f.getOptimisticDayTotal(LocalDateTime.now(), 1))); + updateState(CHANNEL_DAY1_LOW, SolcastObject.getStateObject(f.getPessimisticDayTotal(LocalDateTime.now(), 1))); updateState(CHANNEL_DAY2, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 2))); updateState(CHANNEL_DAY2_HIGH, SolcastObject.getStateObject(f.getOptimisticDayTotal(LocalDateTime.now(), 2))); updateState(CHANNEL_DAY2_LOW, SolcastObject.getStateObject(f.getPessimisticDayTotal(LocalDateTime.now(), 2))); diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-part-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-config.xml similarity index 95% rename from bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-part-config.xml rename to bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-config.xml index aebcdf969665a..fc1c8fb73acaf 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-part-config.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-plane-config.xml @@ -4,7 +4,7 @@ xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> - + Data refresh rate of forecast data diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-bridge-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-site-config.xml similarity index 94% rename from bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-bridge-config.xml rename to bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-site-config.xml index 365bf2c21a4dd..e09df41e88410 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-bridge-config.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/fs-site-config.xml @@ -4,7 +4,7 @@ xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> - + location diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-part-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-config.xml similarity index 93% rename from bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-part-config.xml rename to bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-config.xml index 8d68dc145af55..d9cfdccfc0d42 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-part-config.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-config.xml @@ -4,7 +4,7 @@ xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> - + Data refresh rate of forecast data diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-bridge-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-site-config.xml similarity index 92% rename from bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-bridge-config.xml rename to bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-site-config.xml index eb1123a6c60ca..f76ad5970f5b5 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-bridge-config.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-site-config.xml @@ -4,7 +4,7 @@ xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> - + API Key from your subscription 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 index ccd8da4e78fcd..4545c01fffdce 100644 --- 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 @@ -27,19 +27,19 @@ Plain JSON response without conversions - + Number:Energy Tomorrows forecast in total - + Number:Energy Tomorrows pessimistic forecast - + Number:Energy Tomorrows optimistic forecast diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-plane-part.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-plane-type.xml similarity index 82% rename from bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-plane-part.xml rename to bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-plane-type.xml index 13c98766396bd..dbc5ba6f323ad 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-plane-part.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-plane-type.xml @@ -4,24 +4,24 @@ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - + - + PV Plane as part of Multi Plane Bridge - - + + - + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-bridge-type-multi.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-site-type.xml similarity index 71% rename from bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-bridge-type-multi.xml rename to bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-site-type.xml index d08533824897b..8260153ae291d 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-bridge-type-multi.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/fs-site-type.xml @@ -4,19 +4,19 @@ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - - - Bridge holding multiple photovoltaic planes + + + Site location for Forecast Solar - - + + - + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-thing-type-part.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-plane-type.xml similarity index 83% rename from bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-thing-type-part.xml rename to bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-plane-type.xml index 4693b1ec23783..020064cd85fda 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-thing-type-part.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-plane-type.xml @@ -4,9 +4,9 @@ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - + - + @@ -16,9 +16,9 @@ - - - + + + @@ -37,6 +37,6 @@ - + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-bridge-type-multi.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-site-type.xml similarity index 77% rename from bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-bridge-type-multi.xml rename to bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-site-type.xml index 8997c9c0d1c65..2b84e8cf7c27a 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-bridge-type-multi.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/sc-site-type.xml @@ -4,17 +4,17 @@ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - - - Bridge holding multiple photovoltaic planes + + + Solcast service site definition - - - + + + @@ -31,6 +31,6 @@ - + diff --git a/bundles/pom.xml b/bundles/pom.xml index 14f345d1dfb22..07dfe46b2bd64 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -356,6 +356,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 From 845d4a7c639f266e95a4459d6176521f20eb0826 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Thu, 28 Jul 2022 12:11:08 +0200 Subject: [PATCH 009/111] avoid zero reporting if forecast update fails Signed-off-by: Bernd Weymann --- .../README.md | 6 ++- .../ForecastSolarBridgeHandler.java | 16 +++---- .../forecastsolar/ForecastSolarObject.java | 2 +- .../solcast/SolcastBridgeHandler.java | 46 +++++++++---------- .../solarforecast/ForecastSolarTest.java | 1 + 5 files changed, 33 insertions(+), 38 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 5dbbdd1a1e77c..b5187b774ff92 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -50,7 +50,7 @@ After configuration the necessary information is available. `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've 25 free calls per day, each plane needs 2 call per update a refresh interval of 120 minutes will result in 24 call per day. +If you've 25 free calls per day, each plane needs 2 calls per update a refresh interval of 120 minutes will result in 24 call per day. Note: `channelRefreshInterval` from [Bridge Configuration](#solcast-bridge-configuration) will calculate intermediate values without requesting new forecast data. @@ -83,7 +83,8 @@ Day*X* channels are referring to forecasts plus *X* days: 1 = tomorrow, 2 = day ## ForecastSolar Configuration [ForecastSolar service](https://forecast.solar/) provides a [public free](https://forecast.solar/#accounts) plan. -You can try this out without any registration or other pre-conditions. +You can try it without any registration or other pre-conditions. + ### ForecastSolar Bridge Configuration @@ -146,6 +147,7 @@ Bridge solarforecast:fs-site:homeSite "ForecastSolar Home" [ location="54.321, Thing fs-plane homeNorthEast "ForecastSolar Home North-East" [ refreshInterval=10, azimuth=-145, declination=35, kwp=4.425] } ```` + ### Items file ```` diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java index 873e7c2d7590e..6d54b573469c3 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java @@ -103,16 +103,12 @@ private synchronized void getData() { for (Iterator iterator = parts.iterator(); iterator.hasNext();) { ForecastSolarPlaneHandler sfph = iterator.next(); ForecastSolarObject fo = sfph.fetchData(); - if (fo.isValid()) { - actualSum += fo.getActualValue(now); - remainSum += fo.getRemainingProduction(now); - todaySum += fo.getDayTotal(now, 0); - day1Sum += fo.getDayTotal(now, 1); - day2Sum += fo.getDayTotal(now, 2); - day3Sum += fo.getDayTotal(now, 3); - } else { - logger.info("Fetched data not valid {}", fo.toString()); - } + actualSum += fo.getActualValue(now); + remainSum += fo.getRemainingProduction(now); + todaySum += fo.getDayTotal(now, 0); + day1Sum += fo.getDayTotal(now, 1); + day2Sum += fo.getDayTotal(now, 2); + day3Sum += fo.getDayTotal(now, 3); } updateState(CHANNEL_REMAINING, SolcastObject.getStateObject(remainSum)); updateState(CHANNEL_ACTUAL, SolcastObject.getStateObject(actualSum)); 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 index 702846fbebe7b..5feb0216f208a 100644 --- 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 @@ -111,7 +111,7 @@ public double getActualValue(LocalDateTime now) { public double getDayTotal(LocalDateTime now, int offset) { if (rawData.isEmpty()) { - return -1; + return UNDEF; } LocalDate ld = now.plusDays(offset).toLocalDate(); JSONObject contentJson = new JSONObject(rawData.get()); diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java index d4b93ec251f92..19405a0ae29a3 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java @@ -112,31 +112,27 @@ private synchronized void getData() { for (Iterator iterator = parts.iterator(); iterator.hasNext();) { SolcastPlaneHandler sfph = iterator.next(); SolcastObject fo = sfph.fetchData(); - if (fo.isValid()) { - actualSum += fo.getActualValue(now); - remainSum += fo.getRemainingProduction(now); - todaySum += fo.getDayTotal(now, 0); - day1Sum += fo.getDayTotal(now, 1); - day1SumLow += fo.getPessimisticDayTotal(now, 1); - day1SumHigh += fo.getOptimisticDayTotal(now, 1); - day2Sum += fo.getDayTotal(now, 2); - day2SumLow += fo.getPessimisticDayTotal(now, 2); - day2SumHigh += fo.getOptimisticDayTotal(now, 2); - day3Sum += fo.getDayTotal(now, 3); - day3SumLow += fo.getPessimisticDayTotal(now, 3); - day3SumHigh += fo.getOptimisticDayTotal(now, 3); - day4Sum += fo.getDayTotal(now, 4); - day4SumLow += fo.getPessimisticDayTotal(now, 4); - day4SumHigh += fo.getOptimisticDayTotal(now, 4); - day5Sum += fo.getDayTotal(now, 5); - day5SumLow += fo.getPessimisticDayTotal(now, 5); - day5SumHigh += fo.getOptimisticDayTotal(now, 5); - day6Sum += fo.getDayTotal(now, 6); - day6SumLow += fo.getPessimisticDayTotal(now, 6); - day6SumHigh += fo.getOptimisticDayTotal(now, 6); - } else { - logger.info("Fetched data not valid {}", fo.toString()); - } + actualSum += fo.getActualValue(now); + remainSum += fo.getRemainingProduction(now); + todaySum += fo.getDayTotal(now, 0); + day1Sum += fo.getDayTotal(now, 1); + day1SumLow += fo.getPessimisticDayTotal(now, 1); + day1SumHigh += fo.getOptimisticDayTotal(now, 1); + day2Sum += fo.getDayTotal(now, 2); + day2SumLow += fo.getPessimisticDayTotal(now, 2); + day2SumHigh += fo.getOptimisticDayTotal(now, 2); + day3Sum += fo.getDayTotal(now, 3); + day3SumLow += fo.getPessimisticDayTotal(now, 3); + day3SumHigh += fo.getOptimisticDayTotal(now, 3); + day4Sum += fo.getDayTotal(now, 4); + day4SumLow += fo.getPessimisticDayTotal(now, 4); + day4SumHigh += fo.getOptimisticDayTotal(now, 4); + day5Sum += fo.getDayTotal(now, 5); + day5SumLow += fo.getPessimisticDayTotal(now, 5); + day5SumHigh += fo.getOptimisticDayTotal(now, 5); + day6Sum += fo.getDayTotal(now, 6); + day6SumLow += fo.getPessimisticDayTotal(now, 6); + day6SumHigh += fo.getOptimisticDayTotal(now, 6); } updateState(CHANNEL_ACTUAL, SolcastObject.getStateObject(actualSum)); updateState(CHANNEL_REMAINING, SolcastObject.getStateObject(remainSum)); 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 index b5bc05496298b..5418b9f5e90ae 100644 --- 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 @@ -85,5 +85,6 @@ void testErrorCases() { assertEquals(-1.0, fo.getActualValue(now), 0.001, "Actual Production"); assertEquals(-1.0, fo.getDayTotal(now, 0), 0.001, "Today Production"); assertEquals(-1.0, fo.getRemainingProduction(now), 0.001, "Remaining Production"); + assertEquals(-1.0, fo.getDayTotal(now, 1), 0.001, "Tomorrow Production"); } } From 8c5d3443e13d5ca9c1ed43e46ac77403fea6e63c Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Thu, 28 Jul 2022 12:37:07 +0200 Subject: [PATCH 010/111] minor readme changes Signed-off-by: Bernd Weymann --- bundles/org.openhab.binding.solarforecast/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index b5187b774ff92..f5e72c0e2a4c8 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -14,7 +14,7 @@ Supported Services ## Supported Things -Each service needs one `Bridge` for your location and 1+ Photovoltaic Plane `Things` +Each service needs one `site` for your location and 1+ Photovoltaic `plane`. | Name | Thing Type ID | |-----------------------------------|---------------| @@ -50,15 +50,15 @@ After configuration the necessary information is available. `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've 25 free calls per day, each plane needs 2 calls per update a refresh interval of 120 minutes will result in 24 call per day. +If you've 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. Note: `channelRefreshInterval` from [Bridge Configuration](#solcast-bridge-configuration) will calculate intermediate values without requesting new forecast data. ## Solcast Channels -Each Plane Thing reports their specific values including a `raw` channel holding json content. -The Bridge sums up all attched `sc-pLane` values and provides the total forecast for your home location. +Each `sc-plane` reports it's own values including a `raw` channel holding json content. +The `sc-site` bridge sums up all attached `sc-plane` values and provides the 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 including @@ -117,8 +117,8 @@ Note: `channelRefreshInterval` from [Bridge Configuration](#forecastsolar-bridge ## ForecastSolar Channels -Each Plane Thing reports their specific values including a `raw` channel holding json content. -The Bridge sums up all `Plane Thing` values and provides the total forecast for your home location. +Each `fs-plane` reports it's own values including a `raw` 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 todays actual data with current, remaining and today's total prediction. Forecasts are delivered up to 3 days for paid personal plans. From db3e62251f0a61b7e33d31dec1af1e0b3fc4e5af Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 2 Aug 2022 01:54:55 +0200 Subject: [PATCH 011/111] add solcast tuning measures Signed-off-by: Bernd Weymann --- .../README.md | 12 ++- .../internal/SolarForecastHandlerFactory.java | 18 ++++- .../ForecastSolarPlaneHandler.java | 7 +- .../internal/solcast/SolcastConstants.java | 2 + .../solcast/SolcastPlaneConfiguration.java | 3 +- .../internal/solcast/SolcastPlaneHandler.java | 79 ++++++++++++++++++- .../OH-INF/config/sc-plane-config.xml | 6 ++ 7 files changed, 115 insertions(+), 12 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index f5e72c0e2a4c8..efd43ab2993e7 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -42,10 +42,11 @@ After configuration the necessary information is available. ### Solcast Plane Configuration -| Name | Type | Description | Default | Required | -|-----------------|---------|---------------------------------------|---------|----------| -| resourceId | text | Resource Id of Solcast rooftop site | N/A | yes | -| refreshInterval | integer | Forecast Refresh Interval in minutes | 120 | yes | +| Name | Type | Description | Default | Required | +|-----------------|---------|--------------------------------------------------------|---------|----------| +| resourceId | text | Resource Id of Solcast rooftop site | N/A | yes | +| refreshInterval | integer | Forecast Refresh Interval in minutes | 120 | yes | +| powerItem | text | Power item from your solar inverter for this rooftop | N/A | no | `resourceId` for each plane can be obtained in your [Rooftop Sites](https://toolkit.solcast.com.au/rooftop-sites) @@ -54,6 +55,9 @@ If you've 25 free calls per day, each plane needs 2 calls per update a refresh i Note: `channelRefreshInterval` from [Bridge Configuration](#solcast-bridge-configuration) will calculate intermediate values without requesting new forecast data. +`powerItem` shall reflect the power in kW for this specific rooftop. +It's an optional setting and the [measure is sent to Solcast API in order to tune the forecast](https://legacy-docs.solcast.com.au/#measurements-rooftop-site) in the future. +If you don't want to sent measures to Solcast leave this configuration item empty. ## Solcast Channels 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 index d495cd4801403..aa3ae3181a262 100644 --- 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 @@ -14,6 +14,8 @@ 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; @@ -24,6 +26,9 @@ import org.openhab.core.i18n.LocationProvider; import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.library.types.PointType; +import org.openhab.core.persistence.PersistenceService; +import org.openhab.core.persistence.PersistenceServiceRegistry; +import org.openhab.core.persistence.QueryablePersistenceService; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingTypeUID; @@ -43,12 +48,13 @@ @NonNullByDefault @Component(configurationPid = "binding.solarforecast", service = ThingHandlerFactory.class) public class SolarForecastHandlerFactory extends BaseThingHandlerFactory { - + private Optional qps = Optional.empty(); private final HttpClient httpClient; private final PointType location; @Activate - public SolarForecastHandlerFactory(final @Reference HttpClientFactory hcf, final @Reference LocationProvider lp) { + public SolarForecastHandlerFactory(final @Reference HttpClientFactory hcf, final @Reference LocationProvider lp, + final @Reference PersistenceServiceRegistry psr) { httpClient = hcf.getCommonHttpClient(); PointType pt = lp.getLocation(); if (pt != null) { @@ -56,6 +62,12 @@ public SolarForecastHandlerFactory(final @Reference HttpClientFactory hcf, final } else { location = PointType.valueOf("0.0,0.0"); } + + PersistenceService s = psr.getDefault(); + if (!(s instanceof QueryablePersistenceService)) { + } else { + qps = Optional.of((QueryablePersistenceService) s); + } } @Override @@ -73,7 +85,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { } else if (SOLCAST_BRIDGE_STRING.equals(thingTypeUID)) { return new SolcastBridgeHandler((Bridge) thing); } else if (SOLCAST_PART_STRING.equals(thingTypeUID)) { - return new SolcastPlaneHandler(thing, httpClient); + return new SolcastPlaneHandler(thing, httpClient, qps); } return null; } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java index 6f5ec98ee737b..7315050626c19 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java @@ -53,6 +53,7 @@ public class ForecastSolarPlaneHandler extends BaseThingHandler { private Optional location = Optional.empty(); private Optional apiKey = Optional.empty(); private ForecastSolarObject forecast = new ForecastSolarObject(); + private int failureCounter = 0; public ForecastSolarPlaneHandler(Thing thing, HttpClient hc) { super(thing); @@ -126,13 +127,15 @@ protected ForecastSolarObject fetchData() { forecast = new ForecastSolarObject(cr.getContentAsString(), LocalDateTime.now(), LocalDateTime.now().plusMinutes(configuration.get().refreshInterval)); logger.debug("{} Fetched data {}", thing.getLabel(), forecast.toString()); + logger.info("{} {} HTTP errors since last successful update", thing.getLabel(), failureCounter); + failureCounter = 0; updateChannels(forecast); updateState(CHANNEL_RAW, StringType.valueOf(cr.getContentAsString())); } else { - logger.info("{} Call {} failed {}", thing.getLabel(), url, cr.getStatus()); + logger.debug("{} Call {} failed {}", thing.getLabel(), url, cr.getStatus()); } } catch (InterruptedException | ExecutionException | TimeoutException e) { - logger.info("{} Call {} failed {}", thing.getLabel(), url, e.getMessage()); + logger.debug("{} Call {} failed {}", thing.getLabel(), url, e.getMessage()); } } else { logger.debug("{} use available forecast {}", thing.getLabel(), 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 index 5066a867105d9..9b4352d67cea7 100644 --- 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 @@ -24,5 +24,7 @@ public class SolcastConstants { public static final String FORECAST_URL = "https://api.solcast.com.au/rooftop_sites/%s/forecasts?format=json"; public static final String CURRENT_ESTIMATE_URL = "https://api.solcast.com.au/rooftop_sites/%s/estimated_actuals?format=json"; + public static final String MEASUREMENT_URL = "https://api.solcast.com.au/rooftop_sites/%s/measurements?format=json"; + public static final String BEARER = "Bearer "; } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneConfiguration.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneConfiguration.java index fabfe26efe46c..b2a5bc4437431 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneConfiguration.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneConfiguration.java @@ -22,6 +22,7 @@ */ @NonNullByDefault public class SolcastPlaneConfiguration { - public int refreshInterval = -1; public String resourceId = SolarForecastBindingConstants.EMPTY; + public String powerItem = SolarForecastBindingConstants.EMPTY; + public int refreshInterval = -1; } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java index e365e2aebe129..db0ec9ca13ff1 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java @@ -16,6 +16,8 @@ import static org.openhab.binding.solarforecast.internal.solcast.SolcastConstants.*; import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; @@ -24,8 +26,14 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; import org.eclipse.jetty.http.HttpHeader; +import org.json.JSONObject; +import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.StringType; +import org.openhab.core.persistence.FilterCriteria; +import org.openhab.core.persistence.HistoricItem; +import org.openhab.core.persistence.QueryablePersistenceService; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; @@ -45,16 +53,20 @@ */ @NonNullByDefault public class SolcastPlaneHandler extends BaseThingHandler { + private static final int MEASURE_INTERVAL_MIN = 15; + private static final int MEASURE_OFFSET_MIN = 5; private final Logger logger = LoggerFactory.getLogger(SolcastPlaneHandler.class); private final HttpClient httpClient; - private Optional configuration = Optional.empty(); private Optional bridgeHandler = Optional.empty(); private SolcastObject forecast = new SolcastObject(); + private Optional persistenceService; + private ZonedDateTime nextMeasurement = ZonedDateTime.now(); - public SolcastPlaneHandler(Thing thing, HttpClient hc) { + public SolcastPlaneHandler(Thing thing, HttpClient hc, Optional qps) { super(thing); httpClient = hc; + persistenceService = qps; } @Override @@ -141,9 +153,72 @@ protected SolcastObject fetchData() { logger.debug("{} use available forecast {}", thing.getLabel(), forecast); } updateChannels(forecast); + if (ZonedDateTime.now().isAfter(nextMeasurement)) { + nextMeasurement = ZonedDateTime.now().plusMinutes(MEASURE_INTERVAL_MIN); + sendMeasure(); + } return forecast; } + /** + * https://legacy-docs.solcast.com.au/#measurements-rooftop-site + */ + private void sendMeasure() { + if (persistenceService.isPresent() && !EMPTY.equals(configuration.get().powerItem)) { + logger.info("Get item {}", configuration.get().powerItem); + ZonedDateTime now = ZonedDateTime.now(); + ZonedDateTime beginPeriodDT = now.minusMinutes(MEASURE_INTERVAL_MIN + MEASURE_OFFSET_MIN); + ZonedDateTime endPeriodDT = now.minusMinutes(MEASURE_OFFSET_MIN); + FilterCriteria fc = new FilterCriteria(); + fc.setBeginDate(beginPeriodDT); + fc.setEndDate(endPeriodDT); + fc.setItemName(configuration.get().powerItem); + Iterable historicItems = persistenceService.get().query(fc); + int count = 0; + double total = 0; + for (HistoricItem historicItem : historicItems) { + // logger.info("Found {} item with average {} power", historicItem.getTimestamp(), + // historicItem.getState().toFullString()); + DecimalType dt = historicItem.getState().as(DecimalType.class); + if (dt != null) { + total += dt.doubleValue(); + } + count++; + } + double power = Math.round(total * 1000.0 / count) / 1000.0; + if (power > 0.001) { + logger.info("Found {} items with average {} power", count, total / count); + JSONObject measureObject = new JSONObject(); + JSONObject measure = new JSONObject(); + measure.put("period_end", endPeriodDT.format(DateTimeFormatter.ISO_INSTANT)); + measure.put("period", "PT" + MEASURE_INTERVAL_MIN + "M"); + measure.put("total_power", power); + measureObject.put("measurement", measure); + logger.info("Send {}", measureObject.toString()); + + String measureUrl = String.format(MEASUREMENT_URL, configuration.get().resourceId); + Request request = httpClient.POST(measureUrl); + request.header(HttpHeader.AUTHORIZATION, BEARER + bridgeHandler.get().getApiKey()); + request.header(HttpHeader.CONTENT_TYPE, "application/json"); + request.content(new StringContentProvider(measureObject.toString()), "application/json"); + try { + ContentResponse crMeasure = request.send(); + if (crMeasure.getStatus() == 200) { + logger.info("{} Call {} finished {}", thing.getLabel(), measureUrl, + crMeasure.getContentAsString()); + } else { + logger.info("{} Call {} failed {} - {}", thing.getLabel(), measureUrl, crMeasure.getStatus(), + crMeasure.getContentAsString()); + } + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logger.info("{} Call {} failed {}", thing.getLabel(), measureUrl, e.getMessage()); + } + } + } else { + logger.info("Persistence empty"); + } + } + private void updateChannels(SolcastObject f) { updateState(CHANNEL_ACTUAL, SolcastObject.getStateObject(f.getActualValue(LocalDateTime.now()))); updateState(CHANNEL_REMAINING, SolcastObject.getStateObject(f.getRemainingProduction(LocalDateTime.now()))); 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 index d9cfdccfc0d42..286a956df9bed 100644 --- 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 @@ -14,5 +14,11 @@ Resource Id of Solcast rooftop site + + item + + Power item from your solar inverter for this rooftop + + From 11eb89a7047bebde07ab9f87ece288f76d287a8c Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Thu, 4 Aug 2022 01:17:51 +0200 Subject: [PATCH 012/111] timeframe measurements Signed-off-by: Bernd Weymann --- .../README.md | 70 ++++++----- .../SolarForecastBindingConstants.java | 3 +- .../internal/SolarForecastHandlerFactory.java | 13 +- .../ForecastSolarPlaneHandler.java | 1 + .../internal/solcast/SolcastConstants.java | 9 ++ .../solcast/SolcastPlaneConfiguration.java | 1 + .../internal/solcast/SolcastPlaneHandler.java | 116 +++++++++++++++--- .../OH-INF/config/fs-site-config.xml | 2 +- .../OH-INF/config/sc-plane-config.xml | 15 ++- .../resources/OH-INF/thing/channel-types.xml | 5 + .../resources/OH-INF/thing/sc-plane-type.xml | 1 + .../binding/solarforecast/SolcastTest.java | 39 ++++++ 12 files changed, 226 insertions(+), 49 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index efd43ab2993e7..08cc2024ce6ee 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -30,6 +30,20 @@ A free version for your personal home PV system is available in [Hobbyist Plan]( You need to configure your Home Photovoltaic System within the web interface. After configuration the necessary information is available. +### Solcast Tuning + +You've the opportunity to [send your own measurements back to Solcast API](https://legacy-docs.solcast.com.au/#measurements-rooftop-site). +This data is used internally to improve the forecast for your specific site. +Configuration and channels can be set after checking the *Show advanced* checkbox. +You need an item which reports the electric power for the specific rooftop. +If this item isn't set no measures will be sent. +As described in [Solcast Rooftop Measurement](https://legacy-docs.solcast.com.au/#measurements-rooftop-site) check in beforehand if your measures are *sane*. + +- item is delivering good values and they are stored in persistence +- time settings in openHAB are correct in order to so measurements are matching to the measure time frame + +After the measurement is sent the `raw-tuning` is reporting the result. + ### Solcast Bridge Configuration | Name | Type | Description | Default | Required | @@ -42,11 +56,12 @@ After configuration the necessary information is available. ### Solcast Plane Configuration -| Name | Type | Description | Default | Required | -|-----------------|---------|--------------------------------------------------------|---------|----------| -| resourceId | text | Resource Id of Solcast rooftop site | N/A | yes | -| refreshInterval | integer | Forecast Refresh Interval in minutes | 120 | yes | -| powerItem | text | Power item from your solar inverter for this rooftop | N/A | no | +| 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 | +| powerItem | text | Power item from your solar inverter for this rooftop | N/A | no | yes | +| powerUnit | text | Unit selection of the powerItem | auto-detect | no | yes | `resourceId` for each plane can be obtained in your [Rooftop Sites](https://toolkit.solcast.com.au/rooftop-sites) @@ -55,10 +70,14 @@ If you've 25 free calls per day, each plane needs 2 calls per update a refresh i Note: `channelRefreshInterval` from [Bridge Configuration](#solcast-bridge-configuration) will calculate intermediate values without requesting new forecast data. -`powerItem` shall reflect the power in kW for this specific rooftop. +`powerItem` shall reflect the power for this specific rooftop. It's an optional setting and the [measure is sent to Solcast API in order to tune the forecast](https://legacy-docs.solcast.com.au/#measurements-rooftop-site) in the future. If you don't want to sent measures to Solcast leave this configuration item empty. +`powerUnit' is set to `auto-detect`. +In case the `powerItem` is delivering a valid `QuantityType` state this setting is fine. +If the item delivers a raw number without unit please select `powerUnit` accordingly if item state is Watt or Kilowatt unit. + ## Solcast Channels Each `sc-plane` reports it's own values including a `raw` channel holding json content. @@ -72,29 +91,27 @@ Forecasts are delivered up to 6 days in advance including Day*X* channels are referring to forecasts plus *X* days: 1 = tomorrow, 2 = day after tomorrow, ... - -| Channel | Type | Description | -|-------------------------|---------------|-----------------------------------------| -| actual-channel | Number:Energy | Today's forecast till now | -| remaining-channel | Number:Energy | Forecast of today's remaining production | -| today-channel | Number:Energy | Today's forecast in total | -| day*X*-channel | Number:Energy | Day *X* forecast in total | -| day*X*-low-channel | Number:Energy | Day *X* pessimistic forecast | -| day*X*-high-channel | Number:Energy | Day *X* optimistic forecast | -| raw | String | Plain JSON response without conversions | - +| Channel | Type | Description | Advanced | +|-------------------------|---------------|------------------------------------------|----------| +| actual-channel | Number:Energy | Today's forecast till now | no | +| remaining-channel | Number:Energy | Forecast of today's remaining production | no | +| today-channel | Number:Energy | Today's forecast in total | no | +| day*X*-channel | Number:Energy | Day *X* forecast in total | no | +| day*X*-low-channel | Number:Energy | Day *X* pessimistic forecast | no | +| day*X*-high-channel | Number:Energy | Day *X* optimistic forecast | no | +| raw | String | Plain JSON response without conversions | no | +| raw-tuning | String | JSON response from tuning call | 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 pre-conditions. - ### ForecastSolar Bridge Configuration | Name | Type | Description | Default | Required | |------------------------|---------|---------------------------------------|--------------|----------| -| location | text | Location of Photovoltaic system | AUTODETECT | yes | +| location | text | Location of Photovoltaic system | auto-detect | yes | | channelRefreshInterval | integer | Channel Refresh Interval in minutes | 1 | yes | | apiKey | text | API Key | N/A | no | @@ -103,14 +120,13 @@ In case of auto-detect 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 | |-----------------|---------|------------------------------------------------------------------------------|---------|----------| | refreshInterval | integer | Forecast Refresh Interval in minutes | 30 | yes | -| declination | integer | Plane Declination: 0 for horizontal till 90 for vertical declination | N/A | yes | -| azimuth | integer | Plane Azimuth: -180 = north, -90 = east, 0 = south, 90 = west, 180 = north | N/A | yes | +| declination | integer | Plane Declination: 0 for horizontal till 90 for vertical declination | N/A | yes | +| azimuth | integer | Plane Azimuth: -180 = north, -90 = east, 0 = south, 90 = west, 180 = north | N/A | yes | | kwp | decimal | Installed Kilowatt Peak | N/A | yes | `refreshInterval` of forecast data needs to respect the throttling of the ForecastSolar service. @@ -118,7 +134,6 @@ In case of auto-detect the location configured in openHAB is obtained. Note: `channelRefreshInterval` from [Bridge Configuration](#forecastsolar-bridge-configuration) will calculate intermediate values without requesting new forecast data. - ## ForecastSolar Channels Each `fs-plane` reports it's own values including a `raw` channel holding json content. @@ -129,14 +144,13 @@ Forecasts are delivered up to 3 days for paid personal plans. Day*X* channels are referring to forecasts plus *X* days: 1 = tomorrow, 2 = day after tomorrow, ... - -| Channel | Type | Description | -|-------------------------|---------------|-----------------------------------------| +| Channel | Type | Description | +|-------------------------|---------------|------------------------------------------| | actual-channel | Number:Energy | Today's forecast till now | | remaining-channel | Number:Energy | Forecast of today's remaining production | | today-channel | Number:Energy | Today's forecast in total | -| day*X*-channel | Number:Energy | Day *X* forecast in total | -| raw | String | Plain JSON response without conversions | +| day*X*-channel | Number:Energy | Day *X* forecast in total | +| raw | String | Plain JSON response without conversions | ## Example 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 index 8f03a73a1c366..0a8d95a029e72 100644 --- 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 @@ -58,8 +58,9 @@ public class SolarForecastBindingConstants { public static final String CHANNEL_DAY6_HIGH = "day6-high"; public static final String CHANNEL_RAW = "raw"; + public static final String CHANNEL_RAW_TUNING = "raw-tuning"; - public static final String AUTODETECT = "AUTODETECT"; + public static final String AUTODETECT = "auto-detect"; public static final String SLASH = "/"; public static final String EMPTY = ""; } 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 index aa3ae3181a262..9eac102a66b3d 100644 --- 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 @@ -24,7 +24,9 @@ import org.openhab.binding.solarforecast.internal.solcast.SolcastBridgeHandler; import org.openhab.binding.solarforecast.internal.solcast.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.items.ItemRegistry; import org.openhab.core.library.types.PointType; import org.openhab.core.persistence.PersistenceService; import org.openhab.core.persistence.PersistenceServiceRegistry; @@ -48,13 +50,16 @@ @NonNullByDefault @Component(configurationPid = "binding.solarforecast", service = ThingHandlerFactory.class) public class SolarForecastHandlerFactory extends BaseThingHandlerFactory { - private Optional qps = Optional.empty(); + private final ItemRegistry itemRegistry; private final HttpClient httpClient; private final PointType location; + private Optional qps = Optional.empty(); + @Activate public SolarForecastHandlerFactory(final @Reference HttpClientFactory hcf, final @Reference LocationProvider lp, - final @Reference PersistenceServiceRegistry psr) { + final @Reference PersistenceServiceRegistry psr, final @Reference ItemRegistry ir, + final @Reference TimeZoneProvider tzp) { httpClient = hcf.getCommonHttpClient(); PointType pt = lp.getLocation(); if (pt != null) { @@ -68,6 +73,8 @@ public SolarForecastHandlerFactory(final @Reference HttpClientFactory hcf, final } else { qps = Optional.of((QueryablePersistenceService) s); } + itemRegistry = ir; + tzp.getTimeZone(); } @Override @@ -85,7 +92,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { } else if (SOLCAST_BRIDGE_STRING.equals(thingTypeUID)) { return new SolcastBridgeHandler((Bridge) thing); } else if (SOLCAST_PART_STRING.equals(thingTypeUID)) { - return new SolcastPlaneHandler(thing, httpClient, qps); + return new SolcastPlaneHandler(thing, httpClient, qps, itemRegistry); } return null; } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java index 7315050626c19..14483770c3b70 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java @@ -133,6 +133,7 @@ protected ForecastSolarObject fetchData() { updateState(CHANNEL_RAW, StringType.valueOf(cr.getContentAsString())); } else { logger.debug("{} Call {} failed {}", thing.getLabel(), url, cr.getStatus()); + failureCounter++; } } catch (InterruptedException | ExecutionException | TimeoutException e) { logger.debug("{} Call {} failed {}", thing.getLabel(), url, e.getMessage()); 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 index 9b4352d67cea7..33a6ccb0beff6 100644 --- 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 @@ -12,7 +12,12 @@ */ 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, which are @@ -27,4 +32,8 @@ public class SolcastConstants { public static final String MEASUREMENT_URL = "https://api.solcast.com.au/rooftop_sites/%s/measurements?format=json"; public static final String BEARER = "Bearer "; + + public static final Unit KILOWATT_UNIT = MetricPrefix.KILO(Units.WATT); + + public static final double UNDEF_DOUBLE = -1.0; } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneConfiguration.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneConfiguration.java index b2a5bc4437431..55476d4881794 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneConfiguration.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneConfiguration.java @@ -25,4 +25,5 @@ public class SolcastPlaneConfiguration { public String resourceId = SolarForecastBindingConstants.EMPTY; public String powerItem = SolarForecastBindingConstants.EMPTY; public int refreshInterval = -1; + public String powerUnit = SolarForecastBindingConstants.AUTODETECT; } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java index db0ec9ca13ff1..a0fb8148c59fd 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java @@ -22,6 +22,8 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; +import javax.measure.Unit; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; @@ -29,8 +31,12 @@ import org.eclipse.jetty.client.util.StringContentProvider; import org.eclipse.jetty.http.HttpHeader; import org.json.JSONObject; +import org.openhab.core.items.Item; +import org.openhab.core.items.ItemRegistry; import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.Units; import org.openhab.core.persistence.FilterCriteria; import org.openhab.core.persistence.HistoricItem; import org.openhab.core.persistence.QueryablePersistenceService; @@ -43,6 +49,8 @@ import org.openhab.core.thing.binding.BridgeHandler; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,25 +62,71 @@ @NonNullByDefault public class SolcastPlaneHandler extends BaseThingHandler { private static final int MEASURE_INTERVAL_MIN = 15; - private static final int MEASURE_OFFSET_MIN = 5; private final Logger logger = LoggerFactory.getLogger(SolcastPlaneHandler.class); private final HttpClient httpClient; + private final ItemRegistry itemRegistry; private Optional configuration = Optional.empty(); private Optional bridgeHandler = Optional.empty(); - private SolcastObject forecast = new SolcastObject(); + private Optional powerItem = Optional.empty(); private Optional persistenceService; - private ZonedDateTime nextMeasurement = ZonedDateTime.now(); + private SolcastObject forecast = new SolcastObject(); + private ZonedDateTime nextMeasurement; - public SolcastPlaneHandler(Thing thing, HttpClient hc, Optional qps) { + public SolcastPlaneHandler(Thing thing, HttpClient hc, Optional qps, ItemRegistry ir) { super(thing); httpClient = hc; persistenceService = qps; + itemRegistry = ir; + nextMeasurement = getNextTimeframe(ZonedDateTime.now()); + } + + /** + * Get time frames in 15 minutes intervals + * + * @return + */ + public static ZonedDateTime getNextTimeframe(ZonedDateTime now) { + ZonedDateTime nextTime; + int quarter = now.getMinute() / 15; + switch (quarter) { + case 0: + nextTime = now.withMinute(15).withSecond(0).withNano(0); + break; + case 1: + nextTime = now.withMinute(30).withSecond(0).withNano(0); + break; + case 2: + nextTime = now.withMinute(45).withSecond(0).withNano(0); + break; + case 3: + nextTime = now.withMinute(0).withSecond(0).withNano(0).plusHours(1); + break; + default: + nextTime = now; + break; + } + return nextTime; } @Override public void initialize() { SolcastPlaneConfiguration c = getConfigAs(SolcastPlaneConfiguration.class); configuration = Optional.of(c); + + // initialize Power Item + if (!EMPTY.equals(c.powerItem)) { + // power item configured + Item item = itemRegistry.get(c.powerItem); + if (item != null) { + powerItem = Optional.of(item); + } else { + logger.info("Item {} not found", c.powerItem); + } + } else { + logger.info("No Power item configured"); + } + + // connect Bridge & Status Bridge bridge = getBridge(); if (bridge != null) { BridgeHandler handler = bridge.getHandler(); @@ -154,7 +208,6 @@ protected SolcastObject fetchData() { } updateChannels(forecast); if (ZonedDateTime.now().isAfter(nextMeasurement)) { - nextMeasurement = ZonedDateTime.now().plusMinutes(MEASURE_INTERVAL_MIN); sendMeasure(); } return forecast; @@ -164,11 +217,11 @@ protected SolcastObject fetchData() { * https://legacy-docs.solcast.com.au/#measurements-rooftop-site */ private void sendMeasure() { - if (persistenceService.isPresent() && !EMPTY.equals(configuration.get().powerItem)) { + State updateState = UnDefType.UNDEF; + if (persistenceService.isPresent() && powerItem.isPresent()) { logger.info("Get item {}", configuration.get().powerItem); - ZonedDateTime now = ZonedDateTime.now(); - ZonedDateTime beginPeriodDT = now.minusMinutes(MEASURE_INTERVAL_MIN + MEASURE_OFFSET_MIN); - ZonedDateTime endPeriodDT = now.minusMinutes(MEASURE_OFFSET_MIN); + ZonedDateTime beginPeriodDT = nextMeasurement.minusMinutes(MEASURE_INTERVAL_MIN); + ZonedDateTime endPeriodDT = nextMeasurement; FilterCriteria fc = new FilterCriteria(); fc.setBeginDate(beginPeriodDT); fc.setEndDate(endPeriodDT); @@ -184,9 +237,41 @@ private void sendMeasure() { total += dt.doubleValue(); } count++; + + } + double power = total / count; + + // detect unit + if (AUTODETECT.equals(configuration.get().powerUnit)) { + State state = powerItem.get().getState(); + if (state instanceof QuantityType) { + Unit unitDetected = ((QuantityType) state).getUnit(); + if (Units.WATT.toString().equals(unitDetected.toString())) { + // scale to kW necessary, keep 3 digits after comma + power = Math.round(power) / 1000.0; + } else if (KILOWATT_UNIT.toString().equals(unitDetected.toString())) { + // just round and keep 3 digits after comma + power = Math.round(power * 1000.0) / 1000.0; + } else { + logger.info("No Power unit detected - result is {}", unitDetected.toString()); + power = UNDEF_DOUBLE; + } + } else { + logger.info("No autodetection for State class {} possible", state.getClass()); + power = UNDEF_DOUBLE; + } + } else if (Units.WATT.toString().equals(configuration.get().powerUnit)) { + // scale to kW necessary, keep 3 digits after comma + power = Math.round(power) / 1000.0; + } else if (KILOWATT_UNIT.toString().equals(configuration.get().powerUnit)) { + // just round and keep 3 digits after comma + power = Math.round(power * 1000.0) / 1000.0; + } else { + logger.info("No Unit conversion possible for {}", configuration.get().powerUnit); + power = UNDEF_DOUBLE; } - double power = Math.round(total * 1000.0 / count) / 1000.0; - if (power > 0.001) { + + if (power >= 0) { logger.info("Found {} items with average {} power", count, total / count); JSONObject measureObject = new JSONObject(); JSONObject measure = new JSONObject(); @@ -199,13 +284,14 @@ private void sendMeasure() { String measureUrl = String.format(MEASUREMENT_URL, configuration.get().resourceId); Request request = httpClient.POST(measureUrl); request.header(HttpHeader.AUTHORIZATION, BEARER + bridgeHandler.get().getApiKey()); + request.content(new StringContentProvider(measureObject.toString())); request.header(HttpHeader.CONTENT_TYPE, "application/json"); - request.content(new StringContentProvider(measureObject.toString()), "application/json"); try { ContentResponse crMeasure = request.send(); if (crMeasure.getStatus() == 200) { logger.info("{} Call {} finished {}", thing.getLabel(), measureUrl, crMeasure.getContentAsString()); + updateState = StringType.valueOf(crMeasure.getContentAsString()); } else { logger.info("{} Call {} failed {} - {}", thing.getLabel(), measureUrl, crMeasure.getStatus(), crMeasure.getContentAsString()); @@ -213,10 +299,12 @@ private void sendMeasure() { } catch (InterruptedException | TimeoutException | ExecutionException e) { logger.info("{} Call {} failed {}", thing.getLabel(), measureUrl, e.getMessage()); } + } else { + logger.info("Persistence empty"); } - } else { - logger.info("Persistence empty"); } + updateState(CHANNEL_RAW_TUNING, updateState); + nextMeasurement = getNextTimeframe(ZonedDateTime.now()); } private void updateChannels(SolcastObject f) { 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 index e09df41e88410..ce5678ebe447b 100644 --- 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 @@ -9,7 +9,7 @@ location Location of Photovoltaic system - AUTODETECT + auto-detect 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 index 286a956df9bed..ec1e9477595aa 100644 --- 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 @@ -16,9 +16,20 @@ item - + Power item from your solar inverter for this rooftop + true + + + + Unit delivered by power item + + + + + + auto-detect + true - 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 index 4545c01fffdce..b39f85893d976 100644 --- 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 @@ -27,6 +27,11 @@ Plain JSON response without conversions + + String + + Plain JSON response from tuning call + Number:Energy 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 index 020064cd85fda..ad75557973acb 100644 --- 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 @@ -35,6 +35,7 @@ + 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 index e12a27226208b..e2cf693edad08 100644 --- 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 @@ -15,11 +15,17 @@ import static org.junit.jupiter.api.Assertions.*; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import org.eclipse.jdt.annotation.NonNullByDefault; import org.json.JSONObject; import org.junit.jupiter.api.Test; +import org.openhab.binding.solarforecast.internal.solcast.SolcastConstants; import org.openhab.binding.solarforecast.internal.solcast.SolcastObject; +import org.openhab.binding.solarforecast.internal.solcast.SolcastPlaneHandler; +import org.openhab.core.library.unit.Units; /** * The {@link SolcastTest} tests responses from forecast solar website @@ -93,4 +99,37 @@ void testRawChannel() { 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() { + ZonedDateTime zdt = ZonedDateTime.of(2022, 7, 22, 17, 3, 10, 345, ZoneId.systemDefault()); + assertEquals("17:15", SolcastPlaneHandler.getNextTimeframe(zdt).toLocalTime().toString(), "Q1"); + zdt = zdt.plusMinutes(20); + assertEquals("17:30", SolcastPlaneHandler.getNextTimeframe(zdt).toLocalTime().toString(), "Q2"); + zdt = zdt.plusMinutes(3); + assertEquals("17:30", SolcastPlaneHandler.getNextTimeframe(zdt).toLocalTime().toString(), "Q2"); + zdt = zdt.plusMinutes(5); + assertEquals("17:45", SolcastPlaneHandler.getNextTimeframe(zdt).toLocalTime().toString(), "Q3"); + zdt = zdt.plusMinutes(25); + assertEquals("18:00", SolcastPlaneHandler.getNextTimeframe(zdt).toLocalTime().toString(), "Q4"); + zdt = zdt.plusMinutes(6); + assertEquals("18:15", SolcastPlaneHandler.getNextTimeframe(zdt).toLocalTime().toString(), "Q4"); + System.out.println(zdt.format(DateTimeFormatter.ISO_INSTANT)); + // System.out.println(zdt.toLocalDateTime().format(DateTimeFormatter.ISO_INSTANT)); + System.out.println(DateTimeFormatter.ISO_INSTANT.format(zdt.toOffsetDateTime())); + System.out.println(zdt.toOffsetDateTime()); + + DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-M-dd'T'HH:mm:ss.SSSSSSS z"); + String utcTimeString = "2022-07-17T19:30:00.0000000Z"; + LocalDateTime ldt = LocalDateTime.parse(utcTimeString, DateTimeFormatter.ISO_DATE_TIME); + ZonedDateTime zdt2 = ZonedDateTime.parse(utcTimeString, DateTimeFormatter.ISO_DATE_TIME); + System.out.println(zdt2); + + } } From 2649437788be5de6e3f175f58a4c3b1a0e8adc83 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Thu, 4 Aug 2022 13:33:00 +0200 Subject: [PATCH 013/111] switch solcast service to ZonedDateTime with openHAB ZoneId Signed-off-by: Bernd Weymann --- .../internal/SolarForecastHandlerFactory.java | 5 +- .../solcast/SolcastBridgeHandler.java | 4 +- .../internal/solcast/SolcastConstants.java | 3 + .../internal/solcast/SolcastObject.java | 88 +++++++++---------- .../internal/solcast/SolcastPlaneHandler.java | 54 ++++++------ .../binding/solarforecast/SolcastTest.java | 85 +++++++++++++----- 6 files changed, 144 insertions(+), 95 deletions(-) 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 index 9eac102a66b3d..7864c57466098 100644 --- 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 @@ -22,6 +22,7 @@ import org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarBridgeHandler; import org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarPlaneHandler; import org.openhab.binding.solarforecast.internal.solcast.SolcastBridgeHandler; +import org.openhab.binding.solarforecast.internal.solcast.SolcastConstants; import org.openhab.binding.solarforecast.internal.solcast.SolcastPlaneHandler; import org.openhab.core.i18n.LocationProvider; import org.openhab.core.i18n.TimeZoneProvider; @@ -60,6 +61,8 @@ public class SolarForecastHandlerFactory extends BaseThingHandlerFactory { public SolarForecastHandlerFactory(final @Reference HttpClientFactory hcf, final @Reference LocationProvider lp, final @Reference PersistenceServiceRegistry psr, final @Reference ItemRegistry ir, final @Reference TimeZoneProvider tzp) { + itemRegistry = ir; + SolcastConstants.zonedId = tzp.getTimeZone(); httpClient = hcf.getCommonHttpClient(); PointType pt = lp.getLocation(); if (pt != null) { @@ -73,8 +76,6 @@ public SolarForecastHandlerFactory(final @Reference HttpClientFactory hcf, final } else { qps = Optional.of((QueryablePersistenceService) s); } - itemRegistry = ir; - tzp.getTimeZone(); } @Override diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java index 19405a0ae29a3..721be3d7cff5f 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java @@ -14,7 +14,7 @@ import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; -import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -86,7 +86,7 @@ private synchronized void getData() { logger.debug("No PV plane defined yet"); return; } - LocalDateTime now = LocalDateTime.now(); + ZonedDateTime now = ZonedDateTime.now(SolcastConstants.zonedId); double actualSum = 0; double remainSum = 0; double todaySum = 0; 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 index 33a6ccb0beff6..e7fdf4c2b2b5a 100644 --- 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 @@ -12,6 +12,8 @@ */ package org.openhab.binding.solarforecast.internal.solcast; +import java.time.ZoneId; + import javax.measure.Unit; import javax.measure.quantity.Power; @@ -34,6 +36,7 @@ public class SolcastConstants { public static final String BEARER = "Bearer "; public static final Unit KILOWATT_UNIT = MetricPrefix.KILO(Units.WATT); + public static ZoneId zonedId = ZoneId.systemDefault(); public static final double UNDEF_DOUBLE = -1.0; } 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 index f0867f819ff09..b7bc3994412d9 100644 --- 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 @@ -12,8 +12,9 @@ */ package org.openhab.binding.solarforecast.internal.solcast; +import java.time.Instant; import java.time.LocalDate; -import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; @@ -41,20 +42,20 @@ public class SolcastObject { private final Logger logger = LoggerFactory.getLogger(SolcastObject.class); private static final double UNDEF = -1; - private final Map> dataMap = new HashMap>(); - private final Map> optimisticDataMap = new HashMap>(); - private final Map> pessimisticDataMap = new HashMap>(); + private final Map> dataMap = new HashMap>(); + private final Map> optimisticDataMap = new HashMap>(); + private final Map> pessimisticDataMap = new HashMap>(); private Optional rawData = Optional.of(new JSONObject()); - private LocalDateTime expirationDateTime; + private ZonedDateTime expirationDateTime; private boolean valid = false; public SolcastObject() { // invalid forecast object - expirationDateTime = LocalDateTime.now(); + expirationDateTime = ZonedDateTime.now(SolcastConstants.zonedId); } - public SolcastObject(String content, LocalDateTime ldt) { - expirationDateTime = ldt; + public SolcastObject(String content, ZonedDateTime zdt) { + expirationDateTime = zdt; add(content); } @@ -77,41 +78,35 @@ private void add(String content) { for (int i = 0; i < resultJsonArray.length(); i++) { JSONObject jo = resultJsonArray.getJSONObject(i); String periodEnd = jo.getString("period_end"); - LocalDate ld = LocalDate.parse(periodEnd.substring(0, periodEnd.indexOf("T"))); - TreeMap forecastMap = dataMap.get(ld); + LocalDate ld = getZdtFromUTC(periodEnd).toLocalDate(); + TreeMap forecastMap = dataMap.get(ld); if (forecastMap == null) { - forecastMap = new TreeMap(); - LocalDateTime ldt = LocalDateTime.parse(periodEnd.substring(0, periodEnd.lastIndexOf("."))); - forecastMap.put(ldt, jo.getDouble("pv_estimate")); + forecastMap = new TreeMap(); + forecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate")); dataMap.put(ld, forecastMap); } else { - LocalDateTime ldt = LocalDateTime.parse(periodEnd.substring(0, periodEnd.lastIndexOf("."))); - forecastMap.put(ldt, jo.getDouble("pv_estimate")); + forecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate")); dataMap.put(ld, forecastMap); } if (jo.has("pv_estimate10")) { - TreeMap pessimisticForecastMap = pessimisticDataMap.get(ld); + TreeMap pessimisticForecastMap = pessimisticDataMap.get(ld); if (pessimisticForecastMap == null) { - pessimisticForecastMap = new TreeMap(); - LocalDateTime ldt = LocalDateTime.parse(periodEnd.substring(0, periodEnd.lastIndexOf("."))); - pessimisticForecastMap.put(ldt, jo.getDouble("pv_estimate10")); + pessimisticForecastMap = new TreeMap(); + pessimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate10")); pessimisticDataMap.put(ld, pessimisticForecastMap); } else { - LocalDateTime ldt = LocalDateTime.parse(periodEnd.substring(0, periodEnd.lastIndexOf("."))); - pessimisticForecastMap.put(ldt, jo.getDouble("pv_estimate10")); + pessimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate10")); pessimisticDataMap.put(ld, pessimisticForecastMap); } } if (jo.has("pv_estimate90")) { - TreeMap optimisticForecastMap = optimisticDataMap.get(ld); + TreeMap optimisticForecastMap = optimisticDataMap.get(ld); if (optimisticForecastMap == null) { - optimisticForecastMap = new TreeMap(); - LocalDateTime ldt = LocalDateTime.parse(periodEnd.substring(0, periodEnd.lastIndexOf("."))); - optimisticForecastMap.put(ldt, jo.getDouble("pv_estimate90")); + optimisticForecastMap = new TreeMap(); + optimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate90")); optimisticDataMap.put(ld, optimisticForecastMap); } else { - LocalDateTime ldt = LocalDateTime.parse(periodEnd.substring(0, periodEnd.lastIndexOf("."))); - optimisticForecastMap.put(ldt, jo.getDouble("pv_estimate90")); + optimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate90")); optimisticDataMap.put(ld, optimisticForecastMap); } } @@ -122,7 +117,7 @@ private void add(String content) { public boolean isValid() { if (valid) { if (!dataMap.isEmpty()) { - if (expirationDateTime.isAfter(LocalDateTime.now())) { + if (expirationDateTime.isAfter(ZonedDateTime.now(SolcastConstants.zonedId))) { return true; } else { logger.debug("Forecast data expired"); @@ -136,18 +131,18 @@ public boolean isValid() { return false; } - public double getActualValue(LocalDateTime now) { + public double getActualValue(ZonedDateTime now) { if (dataMap.isEmpty()) { return UNDEF; } LocalDate ld = now.toLocalDate(); - TreeMap dtm = dataMap.get(ld); + TreeMap dtm = dataMap.get(ld); if (dtm == null) { return UNDEF; } double forecastValue = 0; - Set keySet = dtm.keySet(); - for (LocalDateTime key : keySet) { + Set keySet = dtm.keySet(); + for (ZonedDateTime key : keySet) { if (key.isBefore(now)) { // value are reported in PT30M = 30 minutes interval with kw value // for kw/h it's half the value @@ -158,8 +153,8 @@ public double getActualValue(LocalDateTime now) { } } - Entry f = dtm.floorEntry(now); - Entry c = dtm.ceilingEntry(now); + Entry f = dtm.floorEntry(now); + Entry c = dtm.ceilingEntry(now); if (f != null) { if (c != null) { // we're during suntime! @@ -178,9 +173,9 @@ public double getActualValue(LocalDateTime now) { } } - public double getDayTotal(LocalDateTime now, int offset) { + public double getDayTotal(ZonedDateTime now, int offset) { LocalDate ld = now.plusDays(offset).toLocalDate(); - TreeMap dtm = dataMap.get(ld); + TreeMap dtm = dataMap.get(ld); if (dtm != null) { return getTotalValue(dtm); } else { @@ -188,9 +183,9 @@ public double getDayTotal(LocalDateTime now, int offset) { } } - public double getOptimisticDayTotal(LocalDateTime now, int offset) { + public double getOptimisticDayTotal(ZonedDateTime now, int offset) { LocalDate ld = now.plusDays(offset).toLocalDate(); - TreeMap dtm = optimisticDataMap.get(ld); + TreeMap dtm = optimisticDataMap.get(ld); if (dtm != null) { return getTotalValue(dtm); } else { @@ -198,9 +193,9 @@ public double getOptimisticDayTotal(LocalDateTime now, int offset) { } } - public double getPessimisticDayTotal(LocalDateTime now, int offset) { + public double getPessimisticDayTotal(ZonedDateTime now, int offset) { LocalDate ld = now.plusDays(offset).toLocalDate(); - TreeMap dtm = pessimisticDataMap.get(ld); + TreeMap dtm = pessimisticDataMap.get(ld); if (dtm != null) { return getTotalValue(dtm); } else { @@ -208,10 +203,10 @@ public double getPessimisticDayTotal(LocalDateTime now, int offset) { } } - private double getTotalValue(TreeMap map) { + private double getTotalValue(TreeMap map) { double forecastValue = 0; - Set keySet = map.keySet(); - for (LocalDateTime key : keySet) { + Set keySet = map.keySet(); + for (ZonedDateTime key : keySet) { // value are reported in PT30M = 30 minutes interval with kw value // for kw/h it's half the value Double addedValue = map.get(key); @@ -222,7 +217,7 @@ private double getTotalValue(TreeMap map) { return forecastValue; } - public double getRemainingProduction(LocalDateTime now) { + public double getRemainingProduction(ZonedDateTime now) { if (dataMap.isEmpty()) { return UNDEF; } @@ -245,4 +240,9 @@ public String toString() { public String getRaw() { return rawData.get().toString(); } + + public static ZonedDateTime getZdtFromUTC(String utc) { + Instant timestamp = Instant.parse(utc); + return timestamp.atZone(SolcastConstants.zonedId); + } } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java index a0fb8148c59fd..c3fcf807dc94a 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java @@ -15,7 +15,6 @@ import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; import static org.openhab.binding.solarforecast.internal.solcast.SolcastConstants.*; -import java.time.LocalDateTime; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.Optional; @@ -77,7 +76,7 @@ public SolcastPlaneHandler(Thing thing, HttpClient hc, Optional Date: Fri, 5 Aug 2022 20:42:14 +0200 Subject: [PATCH 014/111] add actual-power channel Signed-off-by: Bernd Weymann --- .../README.md | 30 +++++---- .../SolarForecastBindingConstants.java | 1 + .../internal/SolarForecastHandlerFactory.java | 8 ++- .../ForecastSolarBridgeHandler.java | 18 ++--- .../forecastsolar/ForecastSolarObject.java | 62 ++++++++++------- .../ForecastSolarPlaneHandler.java | 16 +++-- .../solcast/SolcastBridgeHandler.java | 46 +++++++------ .../internal/solcast/SolcastObject.java | 66 +++++++++++-------- .../internal/solcast/SolcastPlaneHandler.java | 44 +++++++------ .../resources/OH-INF/thing/channel-types.xml | 10 ++- .../resources/OH-INF/thing/fs-plane-type.xml | 1 + .../resources/OH-INF/thing/fs-site-type.xml | 1 + .../resources/OH-INF/thing/sc-plane-type.xml | 1 + .../resources/OH-INF/thing/sc-site-type.xml | 1 + .../solarforecast/ForecastSolarTest.java | 20 +++++- .../binding/solarforecast/SolcastTest.java | 29 ++++++++ .../test/resources/forecastsolar/result.json | 6 +- 17 files changed, 231 insertions(+), 129 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 08cc2024ce6ee..3094b775fb19e 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -93,13 +93,14 @@ Day*X* channels are referring to forecasts plus *X* days: 1 = tomorrow, 2 = day | Channel | Type | Description | Advanced | |-------------------------|---------------|------------------------------------------|----------| -| actual-channel | Number:Energy | Today's forecast till now | no | -| remaining-channel | Number:Energy | Forecast of today's remaining production | no | -| today-channel | Number:Energy | Today's forecast in total | no | -| day*X*-channel | Number:Energy | Day *X* forecast in total | no | -| day*X*-low-channel | Number:Energy | Day *X* pessimistic forecast | no | -| day*X*-high-channel | Number:Energy | Day *X* optimistic forecast | no | -| raw | String | Plain JSON response without conversions | no | +| actual | Number:Energy | Today's forecast till now | no | +| actual-power | Number:Power | Predicted power in this moment | no | +| remaining | Number:Energy | Forecast of today's remaining production | no | +| today | Number:Energy | Today's forecast in total | no | +| day*X* | Number:Energy | Day *X* forecast in total | no | +| day*X*-low | Number:Energy | Day *X* pessimistic forecast | no | +| day*X*-high | Number:Energy | Day *X* optimistic forecast | no | +| raw | String | Plain JSON response without conversions | yes | | raw-tuning | String | JSON response from tuning call | yes | ## ForecastSolar Configuration @@ -144,13 +145,14 @@ Forecasts are delivered up to 3 days for paid personal plans. Day*X* channels are referring to forecasts plus *X* days: 1 = tomorrow, 2 = day after tomorrow, ... -| Channel | Type | Description | -|-------------------------|---------------|------------------------------------------| -| actual-channel | Number:Energy | Today's forecast till now | -| remaining-channel | Number:Energy | Forecast of today's remaining production | -| today-channel | Number:Energy | Today's forecast in total | -| day*X*-channel | Number:Energy | Day *X* forecast in total | -| raw | String | Plain JSON response without conversions | +| Channel | Type | Description | Advanced | +|-------------------------|---------------|------------------------------------------|----------| +| actual | Number:Energy | Today's forecast till now | no | +| actual-power | Number:Power | Predicted power in this moment | no | +| remaining | Number:Energy | Forecast of today's remaining production | no | +| today | Number:Energy | Today's forecast in total | no | +| day*X* | Number:Energy | Day *X* forecast in total | no | +| raw | String | Plain JSON response without conversions | yes | ## Example 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 index 0a8d95a029e72..3510970fa6016 100644 --- 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 @@ -37,6 +37,7 @@ public class SolarForecastBindingConstants { public static final String CHANNEL_TODAY = "today"; public static final String CHANNEL_ACTUAL = "actual"; + public static final String CHANNEL_ACTUAL_POWER = "actual-power"; public static final String CHANNEL_REMAINING = "remaining"; public static final String CHANNEL_DAY1 = "day1"; public static final String CHANNEL_DAY1_LOW = "day1-low"; 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 index 7864c57466098..b56722914e9d9 100644 --- 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 @@ -41,6 +41,8 @@ import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link SolarForecastHandlerFactory} is responsible for creating things and thing @@ -51,6 +53,7 @@ @NonNullByDefault @Component(configurationPid = "binding.solarforecast", service = ThingHandlerFactory.class) public class SolarForecastHandlerFactory extends BaseThingHandlerFactory { + private final Logger logger = LoggerFactory.getLogger(SolarForecastHandlerFactory.class); private final ItemRegistry itemRegistry; private final HttpClient httpClient; private final PointType location; @@ -72,9 +75,10 @@ public SolarForecastHandlerFactory(final @Reference HttpClientFactory hcf, final } PersistenceService s = psr.getDefault(); - if (!(s instanceof QueryablePersistenceService)) { - } else { + if (s instanceof QueryablePersistenceService) { qps = Optional.of((QueryablePersistenceService) s); + } else { + logger.info("Persistence {} cannot be queried. Feature Solcast Tuninng will not work", s); } } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java index 6d54b573469c3..ff56fe2c131c0 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java @@ -24,7 +24,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; -import org.openhab.binding.solarforecast.internal.solcast.SolcastObject; +import org.openhab.binding.solarforecast.internal.Utils; import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.PointType; import org.openhab.core.thing.Bridge; @@ -42,7 +42,6 @@ */ @NonNullByDefault public class ForecastSolarBridgeHandler extends BaseBridgeHandler { - private final Logger logger = LoggerFactory.getLogger(ForecastSolarBridgeHandler.class); private final PointType homeLocation; @@ -95,6 +94,7 @@ private synchronized void getData() { } LocalDateTime now = LocalDateTime.now(); double actualSum = 0; + double actualPowerSum = 0; double remainSum = 0; double todaySum = 0; double day1Sum = 0; @@ -104,18 +104,20 @@ private synchronized void getData() { ForecastSolarPlaneHandler sfph = iterator.next(); ForecastSolarObject fo = sfph.fetchData(); actualSum += fo.getActualValue(now); + actualPowerSum += fo.getActualPowerValue(now); remainSum += fo.getRemainingProduction(now); todaySum += fo.getDayTotal(now, 0); day1Sum += fo.getDayTotal(now, 1); day2Sum += fo.getDayTotal(now, 2); day3Sum += fo.getDayTotal(now, 3); } - updateState(CHANNEL_REMAINING, SolcastObject.getStateObject(remainSum)); - updateState(CHANNEL_ACTUAL, SolcastObject.getStateObject(actualSum)); - updateState(CHANNEL_TODAY, SolcastObject.getStateObject(todaySum)); - updateState(CHANNEL_DAY1, SolcastObject.getStateObject(day1Sum)); - updateState(CHANNEL_DAY2, SolcastObject.getStateObject(day2Sum)); - updateState(CHANNEL_DAY3, SolcastObject.getStateObject(day3Sum)); + updateState(CHANNEL_ACTUAL, Utils.getEnergyState(actualSum)); + updateState(CHANNEL_ACTUAL_POWER, Utils.getPowerState(actualPowerSum)); + updateState(CHANNEL_REMAINING, Utils.getEnergyState(remainSum)); + updateState(CHANNEL_TODAY, Utils.getEnergyState(todaySum)); + updateState(CHANNEL_DAY1, Utils.getEnergyState(day1Sum)); + updateState(CHANNEL_DAY2, Utils.getEnergyState(day2Sum)); + updateState(CHANNEL_DAY3, Utils.getEnergyState(day3Sum)); } @Override 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 index 5feb0216f208a..86856ec23c437 100644 --- 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 @@ -22,10 +22,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.json.JSONObject; import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; -import org.openhab.core.library.types.QuantityType; -import org.openhab.core.library.unit.Units; -import org.openhab.core.types.State; -import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,7 +34,8 @@ public class ForecastSolarObject { private final Logger logger = LoggerFactory.getLogger(ForecastSolarObject.class); private static final double UNDEF = -1; - private final TreeMap dataMap = new TreeMap(); + private final TreeMap wattHourMap = new TreeMap(); + private final TreeMap wattMap = new TreeMap(); private Optional rawData = Optional.empty(); private boolean valid = false; private LocalDateTime expirationDateTime; @@ -53,15 +50,17 @@ public ForecastSolarObject(String content, LocalDateTime now, LocalDateTime expi rawData = Optional.of(content); JSONObject contentJson = new JSONObject(content); JSONObject resultJson = contentJson.getJSONObject("result"); - JSONObject wattsJson = resultJson.getJSONObject("watt_hours"); - Iterator iter = wattsJson.keys(); + JSONObject wattHourJson = resultJson.getJSONObject("watt_hours"); + JSONObject wattJson = resultJson.getJSONObject("watts"); + 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 LocalDateTime ldt = LocalDateTime.parse(dateStr.replace(" ", "T")); if (ldt.getDayOfMonth() == now.getDayOfMonth()) { - dataMap.put(ldt, wattsJson.getDouble(dateStr)); + wattHourMap.put(ldt, wattHourJson.getDouble(dateStr)); + wattMap.put(ldt, wattJson.getDouble(dateStr)); } } valid = true; @@ -70,7 +69,7 @@ public ForecastSolarObject(String content, LocalDateTime now, LocalDateTime expi public boolean isValid() { if (valid) { - if (!dataMap.isEmpty()) { + if (!wattHourMap.isEmpty()) { if (expirationDateTime.isAfter(LocalDateTime.now())) { return true; } else { @@ -86,11 +85,11 @@ public boolean isValid() { } public double getActualValue(LocalDateTime now) { - if (dataMap.isEmpty()) { + if (wattHourMap.isEmpty()) { return UNDEF; } - Entry f = dataMap.floorEntry(now); - Entry c = dataMap.ceilingEntry(now); + Entry f = wattHourMap.floorEntry(now); + Entry c = wattHourMap.ceilingEntry(now); if (f != null) { if (c != null) { // we're during suntime! @@ -109,6 +108,33 @@ public double getActualValue(LocalDateTime now) { } } + public double getActualPowerValue(LocalDateTime now) { + if (wattMap.isEmpty()) { + return UNDEF; + } + double actualPowerValue = 0; + Entry f = wattMap.floorEntry(now); + Entry c = wattMap.ceilingEntry(now); + if (f != null) { + if (c != null) { + // we're during suntime! + double powerCeiling = c.getValue(); + double powerFloor = f.getValue(); + // calculate in minutes from floor to now, e.g. 20 minutes + // => take 2/3 of floor and 1/3 of ceiling + double interpolation = (now.getMinute() - f.getKey().getMinute()) / 60.0; + actualPowerValue = ((1 - interpolation) * powerFloor) + (interpolation * powerCeiling); + return Math.round(actualPowerValue) / 1000.0; + } else { + // sun is down + return 0; + } + } else { + // no floor - sun not rised yet + return 0; + } + } + public double getDayTotal(LocalDateTime now, int offset) { if (rawData.isEmpty()) { return UNDEF; @@ -125,22 +151,14 @@ public double getDayTotal(LocalDateTime now, int offset) { } public double getRemainingProduction(LocalDateTime now) { - if (dataMap.isEmpty()) { + if (wattHourMap.isEmpty()) { return UNDEF; } return getDayTotal(now, 0) - getActualValue(now); } - public static State getStateObject(double d) { - if (d < 0) { - return UnDefType.UNDEF; - } else { - return QuantityType.valueOf(d, Units.KILOWATT_HOUR); - } - } - @Override public String toString() { - return "Expiration: " + expirationDateTime + ", Valid: " + valid + ", Data:" + dataMap; + return "Expiration: " + expirationDateTime + ", Valid: " + valid + ", Data:" + wattHourMap; } } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java index 14483770c3b70..7da726efecfbb 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java @@ -23,7 +23,7 @@ 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.solcast.SolcastObject; +import org.openhab.binding.solarforecast.internal.Utils; import org.openhab.core.library.types.PointType; import org.openhab.core.library.types.StringType; import org.openhab.core.thing.Bridge; @@ -149,12 +149,14 @@ protected ForecastSolarObject fetchData() { } private void updateChannels(ForecastSolarObject f) { - updateState(CHANNEL_ACTUAL, SolcastObject.getStateObject(f.getActualValue(LocalDateTime.now()))); - updateState(CHANNEL_REMAINING, SolcastObject.getStateObject(f.getRemainingProduction(LocalDateTime.now()))); - updateState(CHANNEL_TODAY, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 0))); - updateState(CHANNEL_DAY1, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 1))); - updateState(CHANNEL_DAY2, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 2))); - updateState(CHANNEL_DAY3, SolcastObject.getStateObject(f.getDayTotal(LocalDateTime.now(), 3))); + LocalDateTime now = LocalDateTime.now(); + updateState(CHANNEL_ACTUAL, Utils.getEnergyState(f.getActualValue(now))); + updateState(CHANNEL_ACTUAL_POWER, Utils.getPowerState(f.getActualPowerValue(now))); + updateState(CHANNEL_REMAINING, Utils.getEnergyState(f.getRemainingProduction(now))); + updateState(CHANNEL_TODAY, Utils.getEnergyState(f.getDayTotal(now, 0))); + updateState(CHANNEL_DAY1, Utils.getEnergyState(f.getDayTotal(now, 1))); + updateState(CHANNEL_DAY2, Utils.getEnergyState(f.getDayTotal(now, 2))); + updateState(CHANNEL_DAY3, Utils.getEnergyState(f.getDayTotal(now, 3))); } /** diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java index 721be3d7cff5f..b0fd605689147 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java @@ -23,6 +23,7 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.solarforecast.internal.Utils; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ThingStatus; @@ -88,6 +89,7 @@ private synchronized void getData() { } ZonedDateTime now = ZonedDateTime.now(SolcastConstants.zonedId); double actualSum = 0; + double actualPowerSum = 0; double remainSum = 0; double todaySum = 0; double day1Sum = 0; @@ -113,6 +115,7 @@ private synchronized void getData() { SolcastPlaneHandler sfph = iterator.next(); SolcastObject fo = sfph.fetchData(); actualSum += fo.getActualValue(now); + actualPowerSum += fo.getActualPowerValue(now); remainSum += fo.getRemainingProduction(now); todaySum += fo.getDayTotal(now, 0); day1Sum += fo.getDayTotal(now, 1); @@ -134,27 +137,28 @@ private synchronized void getData() { day6SumLow += fo.getPessimisticDayTotal(now, 6); day6SumHigh += fo.getOptimisticDayTotal(now, 6); } - updateState(CHANNEL_ACTUAL, SolcastObject.getStateObject(actualSum)); - updateState(CHANNEL_REMAINING, SolcastObject.getStateObject(remainSum)); - updateState(CHANNEL_TODAY, SolcastObject.getStateObject(todaySum)); - updateState(CHANNEL_DAY1, SolcastObject.getStateObject(day1Sum)); - updateState(CHANNEL_DAY1_HIGH, SolcastObject.getStateObject(day1SumHigh)); - updateState(CHANNEL_DAY1_LOW, SolcastObject.getStateObject(day1SumLow)); - updateState(CHANNEL_DAY2, SolcastObject.getStateObject(day2Sum)); - updateState(CHANNEL_DAY2_HIGH, SolcastObject.getStateObject(day2SumHigh)); - updateState(CHANNEL_DAY2_LOW, SolcastObject.getStateObject(day2SumLow)); - updateState(CHANNEL_DAY3, SolcastObject.getStateObject(day3Sum)); - updateState(CHANNEL_DAY3_HIGH, SolcastObject.getStateObject(day3SumHigh)); - updateState(CHANNEL_DAY3_LOW, SolcastObject.getStateObject(day3SumLow)); - updateState(CHANNEL_DAY4, SolcastObject.getStateObject(day4Sum)); - updateState(CHANNEL_DAY4_HIGH, SolcastObject.getStateObject(day4SumHigh)); - updateState(CHANNEL_DAY4_LOW, SolcastObject.getStateObject(day4SumLow)); - updateState(CHANNEL_DAY5, SolcastObject.getStateObject(day5Sum)); - updateState(CHANNEL_DAY5_HIGH, SolcastObject.getStateObject(day5SumHigh)); - updateState(CHANNEL_DAY5_LOW, SolcastObject.getStateObject(day5SumLow)); - updateState(CHANNEL_DAY6, SolcastObject.getStateObject(day6Sum)); - updateState(CHANNEL_DAY6_HIGH, SolcastObject.getStateObject(day6SumHigh)); - updateState(CHANNEL_DAY6_LOW, SolcastObject.getStateObject(day6SumLow)); + updateState(CHANNEL_ACTUAL, Utils.getEnergyState(actualSum)); + updateState(CHANNEL_ACTUAL_POWER, Utils.getPowerState(actualPowerSum)); + updateState(CHANNEL_REMAINING, Utils.getEnergyState(remainSum)); + updateState(CHANNEL_TODAY, Utils.getEnergyState(todaySum)); + updateState(CHANNEL_DAY1, Utils.getEnergyState(day1Sum)); + updateState(CHANNEL_DAY1_HIGH, Utils.getEnergyState(day1SumHigh)); + updateState(CHANNEL_DAY1_LOW, Utils.getEnergyState(day1SumLow)); + updateState(CHANNEL_DAY2, Utils.getEnergyState(day2Sum)); + updateState(CHANNEL_DAY2_HIGH, Utils.getEnergyState(day2SumHigh)); + updateState(CHANNEL_DAY2_LOW, Utils.getEnergyState(day2SumLow)); + updateState(CHANNEL_DAY3, Utils.getEnergyState(day3Sum)); + updateState(CHANNEL_DAY3_HIGH, Utils.getEnergyState(day3SumHigh)); + updateState(CHANNEL_DAY3_LOW, Utils.getEnergyState(day3SumLow)); + updateState(CHANNEL_DAY4, Utils.getEnergyState(day4Sum)); + updateState(CHANNEL_DAY4_HIGH, Utils.getEnergyState(day4SumHigh)); + updateState(CHANNEL_DAY4_LOW, Utils.getEnergyState(day4SumLow)); + updateState(CHANNEL_DAY5, Utils.getEnergyState(day5Sum)); + updateState(CHANNEL_DAY5_HIGH, Utils.getEnergyState(day5SumHigh)); + updateState(CHANNEL_DAY5_LOW, Utils.getEnergyState(day5SumLow)); + updateState(CHANNEL_DAY6, Utils.getEnergyState(day6Sum)); + updateState(CHANNEL_DAY6_HIGH, Utils.getEnergyState(day6SumHigh)); + updateState(CHANNEL_DAY6_LOW, Utils.getEnergyState(day6SumLow)); } @Override 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 index b7bc3994412d9..9ff9fa9fb6404 100644 --- 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 @@ -26,10 +26,6 @@ import org.json.JSONArray; import org.json.JSONObject; import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; -import org.openhab.core.library.types.QuantityType; -import org.openhab.core.library.unit.Units; -import org.openhab.core.types.State; -import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -82,33 +78,25 @@ private void add(String content) { TreeMap forecastMap = dataMap.get(ld); if (forecastMap == null) { forecastMap = new TreeMap(); - forecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate")); - dataMap.put(ld, forecastMap); - } else { - forecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate")); - dataMap.put(ld, forecastMap); } + forecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate")); + dataMap.put(ld, forecastMap); + if (jo.has("pv_estimate10")) { TreeMap pessimisticForecastMap = pessimisticDataMap.get(ld); if (pessimisticForecastMap == null) { pessimisticForecastMap = new TreeMap(); - pessimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate10")); - pessimisticDataMap.put(ld, pessimisticForecastMap); - } else { - pessimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate10")); - pessimisticDataMap.put(ld, pessimisticForecastMap); } + pessimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate10")); + pessimisticDataMap.put(ld, pessimisticForecastMap); } if (jo.has("pv_estimate90")) { TreeMap optimisticForecastMap = optimisticDataMap.get(ld); if (optimisticForecastMap == null) { optimisticForecastMap = new TreeMap(); - optimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate90")); - optimisticDataMap.put(ld, optimisticForecastMap); - } else { - optimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate90")); - optimisticDataMap.put(ld, optimisticForecastMap); } + optimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate90")); + optimisticDataMap.put(ld, optimisticForecastMap); } } } @@ -173,6 +161,38 @@ public double getActualValue(ZonedDateTime now) { } } + public double getActualPowerValue(ZonedDateTime now) { + if (dataMap.isEmpty()) { + return UNDEF; + } + LocalDate ld = now.toLocalDate(); + TreeMap dtm = dataMap.get(ld); + if (dtm == null) { + return UNDEF; + } + double actualPowerValue = 0; + Entry f = dtm.floorEntry(now); + Entry c = dtm.ceilingEntry(now); + if (f != null) { + if (c != null) { + // we're during suntime! + double powerCeiling = c.getValue(); + double powerFloor = f.getValue(); + // calculate in minutes from floor to now, e.g. 20 minutes + // => take 2/3 of floor and 1/3 of ceiling + double interpolation = (now.getMinute() - f.getKey().getMinute()) / 60.0; + actualPowerValue = ((1 - interpolation) * powerFloor) + (interpolation * powerCeiling); + return Math.round(actualPowerValue * 1000) / 1000.0; + } else { + // sun is down + return 0; + } + } else { + // no floor - sun not rised yet + return 0; + } + } + public double getDayTotal(ZonedDateTime now, int offset) { LocalDate ld = now.plusDays(offset).toLocalDate(); TreeMap dtm = dataMap.get(ld); @@ -224,14 +244,6 @@ public double getRemainingProduction(ZonedDateTime now) { return getDayTotal(now, 0) - getActualValue(now); } - public static State getStateObject(double d) { - if (d < 0) { - return UnDefType.UNDEF; - } else { - return QuantityType.valueOf(d, Units.KILOWATT_HOUR); - } - } - @Override public String toString() { return "Expiration: " + expirationDateTime + ", Valid: " + valid + ", Data:" + dataMap; diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java index c3fcf807dc94a..732d43528c95a 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java @@ -30,6 +30,7 @@ import org.eclipse.jetty.client.util.StringContentProvider; import org.eclipse.jetty.http.HttpHeader; import org.json.JSONObject; +import org.openhab.binding.solarforecast.internal.Utils; import org.openhab.core.items.Item; import org.openhab.core.items.ItemRegistry; import org.openhab.core.library.types.DecimalType; @@ -308,27 +309,28 @@ private void sendMeasure() { private void updateChannels(SolcastObject f) { ZonedDateTime now = ZonedDateTime.now(SolcastConstants.zonedId); - updateState(CHANNEL_ACTUAL, SolcastObject.getStateObject(f.getActualValue(now))); - updateState(CHANNEL_REMAINING, SolcastObject.getStateObject(f.getRemainingProduction(now))); - updateState(CHANNEL_TODAY, SolcastObject.getStateObject(f.getDayTotal(now, 0))); - updateState(CHANNEL_DAY1, SolcastObject.getStateObject(f.getDayTotal(now, 1))); - updateState(CHANNEL_DAY1_HIGH, SolcastObject.getStateObject(f.getOptimisticDayTotal(now, 1))); - updateState(CHANNEL_DAY1_LOW, SolcastObject.getStateObject(f.getPessimisticDayTotal(now, 1))); - updateState(CHANNEL_DAY2, SolcastObject.getStateObject(f.getDayTotal(now, 2))); - updateState(CHANNEL_DAY2_HIGH, SolcastObject.getStateObject(f.getOptimisticDayTotal(now, 2))); - updateState(CHANNEL_DAY2_LOW, SolcastObject.getStateObject(f.getPessimisticDayTotal(now, 2))); - updateState(CHANNEL_DAY3, SolcastObject.getStateObject(f.getDayTotal(now, 3))); - updateState(CHANNEL_DAY3_HIGH, SolcastObject.getStateObject(f.getOptimisticDayTotal(now, 3))); - updateState(CHANNEL_DAY3_LOW, SolcastObject.getStateObject(f.getPessimisticDayTotal(now, 3))); - updateState(CHANNEL_DAY4, SolcastObject.getStateObject(f.getDayTotal(now, 4))); - updateState(CHANNEL_DAY4_HIGH, SolcastObject.getStateObject(f.getOptimisticDayTotal(now, 4))); - updateState(CHANNEL_DAY4_LOW, SolcastObject.getStateObject(f.getPessimisticDayTotal(now, 4))); - updateState(CHANNEL_DAY5, SolcastObject.getStateObject(f.getDayTotal(now, 5))); - updateState(CHANNEL_DAY5_HIGH, SolcastObject.getStateObject(f.getOptimisticDayTotal(now, 5))); - updateState(CHANNEL_DAY5_LOW, SolcastObject.getStateObject(f.getPessimisticDayTotal(now, 5))); - updateState(CHANNEL_DAY6, SolcastObject.getStateObject(f.getDayTotal(now, 6))); - updateState(CHANNEL_DAY6_HIGH, SolcastObject.getStateObject(f.getOptimisticDayTotal(now, 6))); - updateState(CHANNEL_DAY6_LOW, SolcastObject.getStateObject(f.getPessimisticDayTotal(now, 6))); + updateState(CHANNEL_ACTUAL, Utils.getEnergyState(f.getActualValue(now))); + updateState(CHANNEL_ACTUAL_POWER, Utils.getEnergyState(f.getActualPowerValue(now))); + updateState(CHANNEL_REMAINING, Utils.getEnergyState(f.getRemainingProduction(now))); + updateState(CHANNEL_TODAY, Utils.getEnergyState(f.getDayTotal(now, 0))); + updateState(CHANNEL_DAY1, Utils.getEnergyState(f.getDayTotal(now, 1))); + updateState(CHANNEL_DAY1_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(now, 1))); + updateState(CHANNEL_DAY1_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(now, 1))); + updateState(CHANNEL_DAY2, Utils.getEnergyState(f.getDayTotal(now, 2))); + updateState(CHANNEL_DAY2_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(now, 2))); + updateState(CHANNEL_DAY2_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(now, 2))); + updateState(CHANNEL_DAY3, Utils.getEnergyState(f.getDayTotal(now, 3))); + updateState(CHANNEL_DAY3_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(now, 3))); + updateState(CHANNEL_DAY3_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(now, 3))); + updateState(CHANNEL_DAY4, Utils.getEnergyState(f.getDayTotal(now, 4))); + updateState(CHANNEL_DAY4_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(now, 4))); + updateState(CHANNEL_DAY4_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(now, 4))); + updateState(CHANNEL_DAY5, Utils.getEnergyState(f.getDayTotal(now, 5))); + updateState(CHANNEL_DAY5_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(now, 5))); + updateState(CHANNEL_DAY5_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(now, 5))); + updateState(CHANNEL_DAY6, Utils.getEnergyState(f.getDayTotal(now, 6))); + updateState(CHANNEL_DAY6_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(now, 6))); + updateState(CHANNEL_DAY6_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(now, 6))); updateState(CHANNEL_RAW, StringType.valueOf(forecast.getRaw())); } } 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 index b39f85893d976..569f55f65ecb9 100644 --- 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 @@ -12,8 +12,14 @@ Number:Energy - - Todays forecast till now + + Todays production forecast till now + + + + Number:Power + + Predicted power in this moment 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 index dbc5ba6f323ad..687ab2d153257 100644 --- 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 @@ -14,6 +14,7 @@ + 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 index 8260153ae291d..e5639712e7760 100644 --- 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 @@ -10,6 +10,7 @@ + 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 index ad75557973acb..199dec4839d7d 100644 --- 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 @@ -14,6 +14,7 @@ + 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 index 2b84e8cf7c27a..82bf320ba0dd1 100644 --- 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 @@ -10,6 +10,7 @@ + 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 index 5418b9f5e90ae..d20bcd66d3165 100644 --- 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 @@ -21,8 +21,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.Test; +import org.openhab.binding.solarforecast.internal.Utils; import org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarObject; -import org.openhab.binding.solarforecast.internal.solcast.SolcastObject; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.Units; import org.openhab.core.types.State; @@ -50,6 +50,22 @@ void testForecastObject() { "Total production"); } + @Test + void testActualPower() { + String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); + LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 23); + ForecastSolarObject fo = new ForecastSolarObject(content, now, now); + assertEquals(5.704, fo.getActualPowerValue(now), 0.001, "Actual estimation"); + + // todo: test this - date out of scope shall return undef + // LocalDateTime ldt = LocalDateTime.of(2022, 7, 23, 0, 5); + LocalDateTime ldt = LocalDateTime.of(2022, 7, 17, 0, 5); + for (int i = 0; i < 96; i++) { + ldt = ldt.plusMinutes(15); + System.out.println(ldt + " " + fo.getActualPowerValue(ldt)); + } + } + @Test void testInterpolation() { String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); @@ -69,7 +85,7 @@ void testForecastSum() { LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 23); ForecastSolarObject fo = new ForecastSolarObject(content, now, now); QuantityType actual = QuantityType.valueOf(0, Units.KILOWATT_HOUR); - State st = SolcastObject.getStateObject(fo.getActualValue(now)); + State st = Utils.getEnergyState(fo.getActualValue(now)); assertTrue(st instanceof QuantityType); actual = actual.add((QuantityType) st); assertEquals(49.431, actual.floatValue(), 0.001, "Current Production"); 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 index 593b13c3d8777..ce4ed9d15096c 100644 --- 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 @@ -43,6 +43,35 @@ void testForecastObject() { assertEquals(16.809, scfo.getActualValue(now), 0.001, "Actual estimation"); } + /** + * { + * "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" + */ + @Test + void testActualPower() { + String content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + ZonedDateTime now = LocalDateTime.of(2022, 7, 23, 16, 23).atZone(ZoneId.systemDefault()); + SolcastObject scfo = new SolcastObject(content, now); + assertEquals(1.855, scfo.getActualPowerValue(now), 0.001, "Actual estimation"); + + ZonedDateTime zdt = LocalDateTime.of(2022, 7, 23, 0, 5).atZone(ZoneId.systemDefault()); + for (int i = 0; i < 96; i++) { + zdt = zdt.plusMinutes(15); + System.out.println(zdt + " " + scfo.getActualPowerValue(zdt)); + } + } + /** * Data from TreeMap for manual validation * 2022-07-17T04:30+02:00[Europe/Berlin]=0.0, 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 index 8f4ab6b4dc8c0..ccdb1a9c28983 100644 --- a/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/result.json +++ b/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/result.json @@ -86,9 +86,9 @@ "type": "success", "text": "", "info": { - "latitude": 50.556, - "longitude": 8.4956, - "place": "35585 Wetzlar, Lahn-Dill-Kreis, Hessen, DE", + "latitude": 54.321, + "longitude": 8.765, + "place": "Whereever", "timezone": "Europe/Berlin" }, "ratelimit": { From 23de30c6a0afb1f4076026d0b845c96d4d091656 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sat, 6 Aug 2022 15:52:54 +0200 Subject: [PATCH 015/111] feature: add Actions Signed-off-by: Bernd Weymann --- .../README.md | 85 +++++++ .../solarforecast/internal/SolarForecast.java | 67 ++++++ .../internal/SolarForecastActions.java | 220 ++++++++++++++++++ .../internal/SolarForecastProvider.java | 33 +++ .../binding/solarforecast/internal/Utils.java | 45 ++++ .../ForecastSolarBridgeHandler.java | 24 +- .../forecastsolar/ForecastSolarObject.java | 106 +++++++-- .../ForecastSolarPlaneHandler.java | 28 ++- .../solcast/SolcastBridgeHandler.java | 22 +- .../internal/solcast/SolcastObject.java | 82 ++++++- .../internal/solcast/SolcastPlaneHandler.java | 30 ++- .../OH-INF/i18n/solarforecast.properties | 102 ++++++++ .../OH-INF/i18n/solarforecast_xx.properties | 21 -- .../solarforecast/ForecastSolarTest.java | 16 ++ .../binding/solarforecast/SolcastTest.java | 20 +- 15 files changed, 845 insertions(+), 56 deletions(-) create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecast.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastActions.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastProvider.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/Utils.java create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast.properties delete mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast_xx.properties diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 3094b775fb19e..39903917c4e5b 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -154,6 +154,63 @@ Day*X* channels are referring to forecasts plus *X* days: 1 = tomorrow, 2 = day | day*X* | Number:Energy | Day *X* forecast in total | no | | raw | 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. +While 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` thing for specific plane values or `xx-site` thing which delivers the sum of all attached planes. + +### Get Forecast Begin + +```` +LocalDateTime getForecastBegin() +```` + +Returns `LocalDateTime` of the earliest possible forecast data available. +It's located in the past, e.g. Solcast provides data from the last 7 days. +`LocalDateTime.MIN` is returned in case of now forecast data is available. + +### Get Forecast End + +```` +LocalDateTime getForecastEnd() +```` + +Returns `LocalDateTime` of the latest possible forecast data available. +`LocalDateTime.MAX` is returned in case of now forecast data is available. + +### Get Power + +```` +State getPower(LocalDateTime dateTime) +```` + +Returns `QuantityType` at the given `dateTime`. +Respect `getForecastBegin` and `getForecastEnd` to avoid ambigouos values. +Check for `UndefType.UNDEF` in case of an errors. + +### Get Day + +```` +State getDay(LocalDate localDate) +```` + +Returns `QuantityType` at the given `localDate`. +Respect `getForecastBegin` and `getForecastEnd` to avoid ambigouos values. +Check for `UndefType.UNDEF` in case of an errors. + +### Get Energy + +```` +State State getEnergy(LocalDateTime begin, LocalDateTime end) +```` + +Returns `QuantityType` between the timestamps `begin` and `end`. +Respect `getForecastBegin` and `getForecastEnd` to avoid ambigouos values. +Check for `UndefType.UNDEF` in case of an errors. + + ## Example Example is based on Forecast.Solar service without any registration. @@ -187,3 +244,31 @@ Number:Energy ForecastSolarHome_Today_SW "Total SW Forecast Toda Number:Energy ForecastSolarHome_Day_SW "Tomorrow SW Forecast [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:day1" } ```` +### Actions rule + +```` +rule "Forecast Solar Actions" + when + Time cron "0 0 23 * * ?" // trigger whatever you like + then + // get Actions for specific fs-site + val solarforecastActions = getActions("solarforecast","solarforecast:fs-site:homeSite") + + // get earliest and latest forecast dates + val beginDT = solarforecastActions.getForecastBegin + val endDT = solarforecastActions.getForecastEnd + logInfo("SF Tests","Begin: "+ beginDT+" End: "+endDT) + + // get daily forecast for tomorrow + val fcToday = solarforecastActions.getDay(LocalDateTime.now.plusDays(1)) + logInfo("SF Tests","Forecast tomorrow: "+ fcToday.toString) + + // get power forecast in one hour + val currentPower = solarforecastActions.getPower(LocalDateTime.now.plusHours(1)) + logInfo("SF Tests","Hour+1 Power: "+ currentPower.toString) + + // get total energy forecast from now till 2 days ahead + val twoDaysForecast = solarforecastActions.getEnergy(LocalDateTime.now,LocalDateTime.now.plusDays(2)) + logInfo("SF Tests","Forecast 2 Days: "+ twoDaysForecast.toString) +end +```` \ No newline at end of file diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecast.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecast.java new file mode 100644 index 0000000000000..c0eebe7d58e97 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecast.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2010-2022 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.time.LocalDate; +import java.time.LocalDateTime; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.types.State; + +/** + * The {@link SolarForecast} Interface needed for Actions + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public interface SolarForecast { + + /** + * Returns electric energy production for one day + * + * @param dateString + * @return QuaatityType in kW/h + */ + public State getDay(LocalDate localDate); + + /** + * Returns electric energy between two timestamps + * + * @param dateTimeFrom + * @param dateTimeTo + * @return QuantityType in kW/h + */ + public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDateTimeEnd); + + /** + * Returns electric power at one specific timepoint + * + * @param dateTimeString + * @return QuantityType in kW + */ + public State getPower(LocalDateTime localDateTime); + + /** + * Get the first date and time of forecast data + * + * @return your localized date time + */ + public LocalDateTime getForecastBegin(); + + /** + * Get the last date and time of forecast data + * + * @return your localized date time + */ + public LocalDateTime getForecastEnd(); +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastActions.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastActions.java new file mode 100644 index 0000000000000..3e23d37957866 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastActions.java @@ -0,0 +1,220 @@ +/** + * Copyright (c) 2010-2022 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.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +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.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.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ActionsS to query forecast objects + * + * @author Bernd Weymann - Initial contribution + */ +@ThingActionsScope(name = "solarforecast") +@NonNullByDefault +public class SolarForecastActions implements ThingActions { + private static final Logger logger = LoggerFactory.getLogger(SolarForecastActions.class); + private Optional thingHandler = Optional.empty(); + + @RuleAction(label = "@text/actionDayLabel", description = "@text/actionDayDesc") + public State getDay( + @ActionInput(name = "localDate", label = "@text/actionInputDayLabel", description = "@text/actionInputDayDesc") LocalDate localDate) { + logger.info("getDay"); + if (thingHandler.isPresent()) { + List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); + logger.info("Found {} SolarForecast entries", l.size()); + if (!l.isEmpty()) { + QuantityType measure = QuantityType.valueOf(0, Units.KILOWATT_HOUR); + for (Iterator iterator = l.iterator(); iterator.hasNext();) { + SolarForecast solarForecast = iterator.next(); + State s = solarForecast.getDay(localDate); + logger.info("Found measure {}", s); + if (s instanceof QuantityType) { + measure = measure.add((QuantityType) s); + } else { + // break in case of failure getting values to avoid ambiguous values + logger.info("Found measure {} - break", s); + return UnDefType.UNDEF; + } + } + return measure; + } + } + return UnDefType.UNDEF; + } + + @RuleAction(label = "@text/actionPowerLabel", description = "@text/actionPowerDesc") + public State getPower( + @ActionInput(name = "localDateTime", label = "@text/actionInputDateTimeLabel", description = "@text/actionInputDateTimeDesc") LocalDateTime localDateTime) { + if (thingHandler.isPresent()) { + List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); + logger.info("Found {} SolarForecast entries", l.size()); + if (!l.isEmpty()) { + QuantityType measure = QuantityType.valueOf(0, MetricPrefix.KILO(Units.WATT)); + for (Iterator iterator = l.iterator(); iterator.hasNext();) { + SolarForecast solarForecast = iterator.next(); + State s = solarForecast.getPower(localDateTime); + logger.info("Found measure {}", s); + if (s instanceof QuantityType) { + measure = measure.add((QuantityType) s); + } else { + // break in case of failure getting values to avoid ambiguous values + logger.info("Found measure {} - break", s); + return UnDefType.UNDEF; + } + } + return measure; + } + } + return UnDefType.UNDEF; + } + + @RuleAction(label = "@text/actionEnergyLabel", description = "@text/actionEnergyDesc") + public State getEnergy( + @ActionInput(name = "localDateTimeBegin", label = "@text/actionInputDateTimeBeginLabel", description = "@text/actionInputDateTimeBeginDesc") LocalDateTime localDateTimeBegin, + @ActionInput(name = "localDateTimeEnd", label = "@text/actionInputDateTimeEndLabel", description = "@text/actionInputDateTimeEndDesc") LocalDateTime localDateTimeEnd) { + if (thingHandler.isPresent()) { + List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); + logger.info("Found {} SolarForecast entries", l.size()); + if (!l.isEmpty()) { + QuantityType measure = QuantityType.valueOf(0, Units.KILOWATT_HOUR); + for (Iterator iterator = l.iterator(); iterator.hasNext();) { + SolarForecast solarForecast = iterator.next(); + State s = solarForecast.getEnergy(localDateTimeBegin, localDateTimeEnd); + logger.info("Found measure {}", s); + if (s instanceof QuantityType) { + measure = measure.add((QuantityType) s); + } else { + // break in case of failure getting values to avoid ambiguous values + logger.info("Found measure {} - break", s); + return UnDefType.UNDEF; + } + } + return measure; + } + } + return UnDefType.UNDEF; + } + + @RuleAction(label = "@text/actionForecastBeginLabel", description = "@text/actionForecastBeginDesc") + public LocalDateTime getForecastBegin() { + logger.info("getForecastBegin"); + LocalDateTime returnLdt = LocalDateTime.MIN; + if (thingHandler.isPresent()) { + List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); + logger.info("Found {} SolarForecast entries", l.size()); + if (!l.isEmpty()) { + for (Iterator iterator = l.iterator(); iterator.hasNext();) { + SolarForecast solarForecast = iterator.next(); + LocalDateTime forecastLdt = solarForecast.getForecastBegin(); + logger.info("Found {}", forecastLdt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + // break in case of failure getting values to avoid ambiguous values + if (forecastLdt.equals(LocalDateTime.MIN)) { + return LocalDateTime.MIN; + } + // take latest possible timestamp to avoid ambiguous values + if (forecastLdt.isAfter(returnLdt)) { + returnLdt = forecastLdt; + logger.info("Set {}", forecastLdt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + } + } + return returnLdt; + } + } else { + logger.info("Thinghandler missing"); + } + return LocalDateTime.MIN; + } + + @RuleAction(label = "@text/actionForecastEndLabel", description = "@text/actionForecastEndDesc") + public LocalDateTime getForecastEnd() { + LocalDateTime returnLdt = LocalDateTime.MAX; + if (thingHandler.isPresent()) { + List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); + if (!l.isEmpty()) { + for (Iterator iterator = l.iterator(); iterator.hasNext();) { + SolarForecast solarForecast = iterator.next(); + LocalDateTime forecastLdt = solarForecast.getForecastEnd(); + // break in case of failure getting values to avoid ambiguous values + if (forecastLdt.equals(LocalDateTime.MIN)) { + return LocalDateTime.MAX; + } + // take earliest possible timestamp to avoid ambiguous values + if (forecastLdt.isBefore(returnLdt)) { + returnLdt = forecastLdt; + } + } + return returnLdt; + } + } else { + logger.info("Thinghandler missing"); + } + return LocalDateTime.MAX; + } + + public static State getDay(ThingActions actions, LocalDate ld) { + return ((SolarForecastActions) actions).getDay(ld); + } + + public static State getPower(ThingActions actions, LocalDateTime dateTime) { + return ((SolarForecastActions) actions).getPower(dateTime); + } + + public static State getEnergy(ThingActions actions, LocalDateTime begin, LocalDateTime end) { + return ((SolarForecastActions) actions).getEnergy(begin, end); + } + + public static LocalDateTime getForecastBegin(ThingActions actions) { + return ((SolarForecastActions) actions).getForecastBegin(); + } + + public static LocalDateTime getForecastEnd(ThingActions actions) { + return ((SolarForecastActions) actions).getForecastEnd(); + } + + @Override + public void setThingHandler(ThingHandler handler) { + logger.info("ThingHandler {} set", handler.toString()); + 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/SolarForecastProvider.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastProvider.java new file mode 100644 index 0000000000000..5ce42439801f6 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastProvider.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2022 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.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 + */ + public List getSolarForecasts(); +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/Utils.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/Utils.java new file mode 100644 index 0000000000000..fc7042b9c8f4c --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/Utils.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2022 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 javax.measure.MetricPrefix; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * The {@link Utils} Some helpers for all + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class Utils { + public static State getEnergyState(double d) { + if (d < 0) { + return UnDefType.UNDEF; + } else { + return QuantityType.valueOf(d, Units.KILOWATT_HOUR); + } + } + + public static State getPowerState(double d) { + if (d < 0) { + return UnDefType.UNDEF; + } else { + return QuantityType.valueOf(d, MetricPrefix.KILO(Units.WATT)); + } + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java index ff56fe2c131c0..8c1ce5ffd779d 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java @@ -16,6 +16,8 @@ import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Optional; @@ -23,7 +25,10 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.solarforecast.internal.SolarForecast; +import org.openhab.binding.solarforecast.internal.SolarForecastActions; import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; +import org.openhab.binding.solarforecast.internal.SolarForecastProvider; import org.openhab.binding.solarforecast.internal.Utils; import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.PointType; @@ -31,6 +36,7 @@ import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.types.Command; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,7 +47,7 @@ * @author Bernd Weymann - Initial contribution */ @NonNullByDefault -public class ForecastSolarBridgeHandler extends BaseBridgeHandler { +public class ForecastSolarBridgeHandler extends BaseBridgeHandler implements SolarForecastProvider { private final Logger logger = LoggerFactory.getLogger(ForecastSolarBridgeHandler.class); private final PointType homeLocation; @@ -54,6 +60,11 @@ public ForecastSolarBridgeHandler(Bridge bridge, PointType location) { homeLocation = location; } + @Override + public Collection> getServices() { + return Collections.singleton(SolarForecastActions.class); + } + @Override public void initialize() { ForecastSolarBridgeConfiguration config = getConfigAs(ForecastSolarBridgeConfiguration.class); @@ -140,4 +151,15 @@ synchronized void addPlane(ForecastSolarPlaneHandler sfph) { synchronized void removePlane(ForecastSolarPlaneHandler sfph) { parts.remove(sfph); } + + @Override + public synchronized List getSolarForecasts() { + List l = new ArrayList(); + parts.forEach(entry -> { + l.addAll(entry.getSolarForecasts()); + logger.info("{} Forecast added", entry.getSolarForecasts().size()); + + }); + return l; + } } 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 index 86856ec23c437..6773708ebdbef 100644 --- 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 @@ -21,7 +21,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.json.JSONObject; +import org.openhab.binding.solarforecast.internal.SolarForecast; import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; +import org.openhab.binding.solarforecast.internal.Utils; +import org.openhab.core.types.State; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,7 +34,7 @@ * @author Bernd Weymann - Initial contribution */ @NonNullByDefault -public class ForecastSolarObject { +public class ForecastSolarObject implements SolarForecast { private final Logger logger = LoggerFactory.getLogger(ForecastSolarObject.class); private static final double UNDEF = -1; private final TreeMap wattHourMap = new TreeMap(); @@ -58,10 +61,8 @@ public ForecastSolarObject(String content, LocalDateTime now, LocalDateTime expi String dateStr = iter.next(); // convert date time into machine readable format LocalDateTime ldt = LocalDateTime.parse(dateStr.replace(" ", "T")); - if (ldt.getDayOfMonth() == now.getDayOfMonth()) { - wattHourMap.put(ldt, wattHourJson.getDouble(dateStr)); - wattMap.put(ldt, wattJson.getDouble(dateStr)); - } + wattHourMap.put(ldt, wattHourJson.getDouble(dateStr)); + wattMap.put(ldt, wattJson.getDouble(dateStr)); } valid = true; } @@ -91,16 +92,26 @@ public double getActualValue(LocalDateTime now) { Entry f = wattHourMap.floorEntry(now); Entry c = wattHourMap.ceilingEntry(now); if (f != null) { - if (c != null) { - // we're during suntime! - double production = c.getValue() - f.getValue(); - int interpolation = now.getMinute() - f.getKey().getMinute(); - double interpolationProduction = production * interpolation / 60; - double actualProduction = f.getValue() + interpolationProduction; - return Math.round(actualProduction) / 1000.0; + if (f.getKey().toLocalDate().equals(now.toLocalDate())) { + if (c != null) { + if (c.getKey().toLocalDate().equals(now.toLocalDate())) { + // we're during suntime! + double production = c.getValue() - f.getValue(); + int interpolation = now.getMinute() - f.getKey().getMinute(); + double interpolationProduction = production * interpolation / 60; + double actualProduction = f.getValue() + interpolationProduction; + return Math.round(actualProduction) / 1000.0; + } else { + // ceiling from wrong date + return Math.round(f.getValue()) / 1000.0; + } + } else { + // sun is down + return Math.round(f.getValue()) / 1000.0; + } } else { - // sun is down - return Math.round(f.getValue()) / 1000.0; + // floor from wrong date + return 0; } } else { // no floor - sun not rised yet @@ -135,11 +146,10 @@ public double getActualPowerValue(LocalDateTime now) { } } - public double getDayTotal(LocalDateTime now, int offset) { + private double getDayTotal(LocalDate ld) { if (rawData.isEmpty()) { return UNDEF; } - LocalDate ld = now.plusDays(offset).toLocalDate(); JSONObject contentJson = new JSONObject(rawData.get()); JSONObject resultJson = contentJson.getJSONObject("result"); JSONObject wattsDay = resultJson.getJSONObject("watt_hours_day"); @@ -150,6 +160,14 @@ public double getDayTotal(LocalDateTime now, int offset) { return UNDEF; } + public double getDayTotal(LocalDateTime now, int offset) { + if (rawData.isEmpty()) { + return UNDEF; + } + LocalDate ld = now.plusDays(offset).toLocalDate(); + return getDayTotal(ld); + } + public double getRemainingProduction(LocalDateTime now) { if (wattHourMap.isEmpty()) { return UNDEF; @@ -161,4 +179,60 @@ public double getRemainingProduction(LocalDateTime now) { public String toString() { return "Expiration: " + expirationDateTime + ", Valid: " + valid + ", Data:" + wattHourMap; } + + // SolarForecast Interface + @Override + public State getDay(LocalDate localDate) { + double measure = getDayTotal(localDate); + logger.info("Deliver measure {}", measure); + return Utils.getEnergyState(measure); + } + + @Override + public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDateTimeEnd) { + LocalDate beginDate = localDateTimeBegin.toLocalDate(); + LocalDate endDate = localDateTimeEnd.toLocalDate(); + double measure = UNDEF; + if (beginDate.equals(endDate)) { + measure = getDayTotal(localDateTimeEnd, 0) - getActualValue(localDateTimeBegin) + - getRemainingProduction(localDateTimeEnd); + } else { + measure = getRemainingProduction(localDateTimeBegin); + beginDate = beginDate.plusDays(1); + while (beginDate.isBefore(endDate)) { + double day = getDayTotal(beginDate); + if (day > 0) { + measure += day; + } + beginDate = beginDate.plusDays(1); + } + measure += getActualValue(localDateTimeEnd); + } + return Utils.getEnergyState(Math.round(measure * 1000) / 1000.0); + } + + @Override + public State getPower(LocalDateTime localDateTime) { + double measure = getActualPowerValue(localDateTime); + logger.info("Deliver measure {}", measure); + return Utils.getPowerState(measure); + } + + @Override + public LocalDateTime getForecastBegin() { + if (!wattHourMap.isEmpty()) { + LocalDateTime ldt = wattHourMap.firstEntry().getKey(); + return ldt; + } + return LocalDateTime.MIN; + } + + @Override + public LocalDateTime getForecastEnd() { + if (!wattHourMap.isEmpty()) { + LocalDateTime ldt = wattHourMap.lastEntry().getKey(); + return ldt; + } + return LocalDateTime.MIN; + } } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java index 7da726efecfbb..e759c4c68fa82 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java @@ -16,6 +16,9 @@ import static org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarConstants.BASE_URL; import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; @@ -23,6 +26,9 @@ 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.SolarForecast; +import org.openhab.binding.solarforecast.internal.SolarForecastActions; +import org.openhab.binding.solarforecast.internal.SolarForecastProvider; import org.openhab.binding.solarforecast.internal.Utils; import org.openhab.core.library.types.PointType; import org.openhab.core.library.types.StringType; @@ -33,6 +39,7 @@ 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; @@ -44,7 +51,7 @@ * @author Bernd Weymann - Initial contribution */ @NonNullByDefault -public class ForecastSolarPlaneHandler extends BaseThingHandler { +public class ForecastSolarPlaneHandler extends BaseThingHandler implements SolarForecastProvider { private final Logger logger = LoggerFactory.getLogger(ForecastSolarPlaneHandler.class); private final HttpClient httpClient; @@ -60,6 +67,11 @@ public ForecastSolarPlaneHandler(Thing thing, HttpClient hc) { httpClient = hc; } + @Override + public Collection> getServices() { + return Collections.singleton(SolarForecastActions.class); + } + @Override public void initialize() { ForecastSolarPlaneConfiguration c = getConfigAs(ForecastSolarPlaneConfiguration.class); @@ -124,8 +136,10 @@ protected ForecastSolarObject fetchData() { try { ContentResponse cr = httpClient.GET(url); if (cr.getStatus() == 200) { - forecast = new ForecastSolarObject(cr.getContentAsString(), LocalDateTime.now(), + ForecastSolarObject localForecast = new ForecastSolarObject(cr.getContentAsString(), + LocalDateTime.now(), LocalDateTime.now().plusMinutes(configuration.get().refreshInterval)); + setForecast(localForecast); logger.debug("{} Fetched data {}", thing.getLabel(), forecast.toString()); logger.info("{} {} HTTP errors since last successful update", thing.getLabel(), failureCounter); failureCounter = 0; @@ -176,4 +190,14 @@ void setLocation(PointType loc) { void setApiKey(String key) { apiKey = Optional.of(key); } + + private synchronized void setForecast(ForecastSolarObject f) { + logger.info("Forecast added - valid? {}", f.isValid()); + forecast = f; + } + + @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/SolcastBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java index b0fd605689147..3ed664b6baeeb 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java @@ -16,6 +16,8 @@ import java.time.ZonedDateTime; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Optional; @@ -23,12 +25,16 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.solarforecast.internal.SolarForecast; +import org.openhab.binding.solarforecast.internal.SolarForecastActions; +import org.openhab.binding.solarforecast.internal.SolarForecastProvider; import org.openhab.binding.solarforecast.internal.Utils; 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.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,7 +45,7 @@ * @author Bernd Weymann - Initial contribution */ @NonNullByDefault -public class SolcastBridgeHandler extends BaseBridgeHandler { +public class SolcastBridgeHandler extends BaseBridgeHandler implements SolarForecastProvider { private final Logger logger = LoggerFactory.getLogger(SolcastBridgeHandler.class); private List parts = new ArrayList(); @@ -50,6 +56,11 @@ public SolcastBridgeHandler(Bridge bridge) { super(bridge); } + @Override + public Collection> getServices() { + return Collections.singleton(SolarForecastActions.class); + } + @Override public void initialize() { SolcastBridgeConfiguration config = getConfigAs(SolcastBridgeConfiguration.class); @@ -181,4 +192,13 @@ String getApiKey() { } return EMPTY; } + + @Override + public synchronized List getSolarForecasts() { + List l = new ArrayList(); + parts.forEach(entry -> { + l.addAll(entry.getSolarForecasts()); + }); + return l; + } } 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 index 9ff9fa9fb6404..2a1f11961a470 100644 --- 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 @@ -14,9 +14,8 @@ import java.time.Instant; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.ZonedDateTime; -import java.util.HashMap; -import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.Set; @@ -25,7 +24,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.json.JSONArray; import org.json.JSONObject; +import org.openhab.binding.solarforecast.internal.SolarForecast; import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; +import org.openhab.binding.solarforecast.internal.Utils; +import org.openhab.core.types.State; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,12 +37,12 @@ * @author Bernd Weymann - Initial contribution */ @NonNullByDefault -public class SolcastObject { +public class SolcastObject implements SolarForecast { private final Logger logger = LoggerFactory.getLogger(SolcastObject.class); private static final double UNDEF = -1; - private final Map> dataMap = new HashMap>(); - private final Map> optimisticDataMap = new HashMap>(); - private final Map> pessimisticDataMap = new HashMap>(); + private final TreeMap> dataMap = new TreeMap>(); + private final TreeMap> optimisticDataMap = new TreeMap>(); + private final TreeMap> pessimisticDataMap = new TreeMap>(); private Optional rawData = Optional.of(new JSONObject()); private ZonedDateTime expirationDateTime; private boolean valid = false; @@ -195,11 +197,15 @@ public double getActualPowerValue(ZonedDateTime now) { public double getDayTotal(ZonedDateTime now, int offset) { LocalDate ld = now.plusDays(offset).toLocalDate(); - TreeMap dtm = dataMap.get(ld); + return getDayTotal(ld, offset); + } + + private double getDayTotal(LocalDate now, int offset) { + TreeMap dtm = dataMap.get(now.plusDays(offset)); if (dtm != null) { return getTotalValue(dtm); } else { - return -1; + return UNDEF; } } @@ -209,7 +215,7 @@ public double getOptimisticDayTotal(ZonedDateTime now, int offset) { if (dtm != null) { return getTotalValue(dtm); } else { - return -1; + return UNDEF; } } @@ -219,7 +225,7 @@ public double getPessimisticDayTotal(ZonedDateTime now, int offset) { if (dtm != null) { return getTotalValue(dtm); } else { - return 0; + return UNDEF; } } @@ -257,4 +263,60 @@ public static ZonedDateTime getZdtFromUTC(String utc) { Instant timestamp = Instant.parse(utc); return timestamp.atZone(SolcastConstants.zonedId); } + + // SolarForecast Interface + @Override + public State getDay(LocalDate localDate) { + double measure = getDayTotal(localDate, 0); + return Utils.getEnergyState(measure); + } + + @Override + public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDateTimeEnd) { + ZonedDateTime zdtBegin = localDateTimeBegin.atZone(SolcastConstants.zonedId); + ZonedDateTime zdtEnd = localDateTimeEnd.atZone(SolcastConstants.zonedId); + LocalDate beginDate = zdtBegin.toLocalDate(); + LocalDate endDate = zdtEnd.toLocalDate(); + double measure = UNDEF; + if (beginDate.equals(endDate)) { + measure = getDayTotal(zdtEnd, 0) - getActualValue(zdtBegin) - getRemainingProduction(zdtEnd); + } else { + measure = getRemainingProduction(zdtBegin); + beginDate = beginDate.plusDays(1); + while (beginDate.isBefore(endDate)) { + double day = getDayTotal(beginDate, 0); + if (day > 0) { + measure += day; + } + beginDate = beginDate.plusDays(1); + } + measure += getActualValue(zdtEnd); + } + return Utils.getEnergyState(Math.round(measure * 1000) / 1000.0); + } + + @Override + public State getPower(LocalDateTime localDateTime) { + ZonedDateTime zdt = localDateTime.atZone(SolcastConstants.zonedId); + double measure = getActualPowerValue(zdt); + return Utils.getPowerState(measure); + } + + @Override + public LocalDateTime getForecastBegin() { + if (!dataMap.isEmpty()) { + ZonedDateTime zdt = dataMap.firstEntry().getValue().firstEntry().getKey(); + return zdt.toLocalDateTime(); + } + return LocalDateTime.MIN; + } + + @Override + public LocalDateTime getForecastEnd() { + if (!dataMap.isEmpty()) { + ZonedDateTime zdt = dataMap.lastEntry().getValue().lastEntry().getKey(); + return zdt.toLocalDateTime(); + } + return LocalDateTime.MIN; + } } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java index 732d43528c95a..03b8816cb8571 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java @@ -17,6 +17,9 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; @@ -30,6 +33,9 @@ import org.eclipse.jetty.client.util.StringContentProvider; import org.eclipse.jetty.http.HttpHeader; import org.json.JSONObject; +import org.openhab.binding.solarforecast.internal.SolarForecast; +import org.openhab.binding.solarforecast.internal.SolarForecastActions; +import org.openhab.binding.solarforecast.internal.SolarForecastProvider; import org.openhab.binding.solarforecast.internal.Utils; import org.openhab.core.items.Item; import org.openhab.core.items.ItemRegistry; @@ -47,6 +53,7 @@ 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.openhab.core.types.State; @@ -60,7 +67,7 @@ * @author Bernd Weymann - Initial contribution */ @NonNullByDefault -public class SolcastPlaneHandler extends BaseThingHandler { +public class SolcastPlaneHandler extends BaseThingHandler implements SolarForecastProvider { private static final int MEASURE_INTERVAL_MIN = 15; private final Logger logger = LoggerFactory.getLogger(SolcastPlaneHandler.class); private final HttpClient httpClient; @@ -80,6 +87,11 @@ public SolcastPlaneHandler(Thing thing, HttpClient hc, Optional> getServices() { + return Collections.singleton(SolarForecastActions.class); + } + /** * Get time frames in 15 minutes intervals * @@ -177,9 +189,9 @@ protected SolcastObject fetchData() { estimateRequest.header(HttpHeader.AUTHORIZATION, BEARER + bridgeHandler.get().getApiKey()); ContentResponse crEstimate = estimateRequest.send(); if (crEstimate.getStatus() == 200) { - forecast = new SolcastObject(crEstimate.getContentAsString(), ZonedDateTime + SolcastObject localForecast = new SolcastObject(crEstimate.getContentAsString(), ZonedDateTime .now(SolcastConstants.zonedId).plusMinutes(configuration.get().refreshInterval)); - logger.trace("{} Fetched data {}", thing.getLabel(), forecast.toString()); + logger.trace("{} Fetched data {}", thing.getLabel(), localForecast.toString()); // get forecast logger.debug("{} Call {}", thing.getLabel(), forecastUrl); @@ -188,7 +200,8 @@ protected SolcastObject fetchData() { ContentResponse crForecast = forecastRequest.send(); if (crForecast.getStatus() == 200) { - forecast.join(crForecast.getContentAsString()); + localForecast.join(crForecast.getContentAsString()); + setForecast(localForecast); logger.trace("{} Fetched data {}", thing.getLabel(), forecast.toString()); updateChannels(forecast); updateState(CHANNEL_RAW, StringType.valueOf(forecast.getRaw())); @@ -333,4 +346,13 @@ private void updateChannels(SolcastObject f) { updateState(CHANNEL_DAY6_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(now, 6))); updateState(CHANNEL_RAW, StringType.valueOf(forecast.getRaw())); } + + private synchronized void setForecast(SolcastObject f) { + forecast = f; + } + + @Override + public synchronized List getSolarForecasts() { + return List.of(forecast); + } } 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 0000000000000..462207fca3fc5 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast.properties @@ -0,0 +1,102 @@ +# binding + +binding.solarforecast.name = SolarForecast Binding +binding.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.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.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 +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.channelRefreshInterval.label = Channel Refresh Interval +thing-type.config.solarforecast.fs-site.channelRefreshInterval.description = Refresh rate of channel data +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.powerItem.label = Power Item +thing-type.config.solarforecast.sc-plane.powerItem.description = Power item from your solar inverter for this rooftop +thing-type.config.solarforecast.sc-plane.powerUnit.label = Power Item Unit +thing-type.config.solarforecast.sc-plane.powerUnit.description = Unit delivered by power item +thing-type.config.solarforecast.sc-plane.powerUnit.option.auto-detect = auto-detect +thing-type.config.solarforecast.sc-plane.powerUnit.option.kW = Kilowatt +thing-type.config.solarforecast.sc-plane.powerUnit.option.W = Watt +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 +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.channelRefreshInterval.label = Channel Refresh Interval +thing-type.config.solarforecast.sc-site.channelRefreshInterval.description = Refresh rate of channel data + +# channel types + +channel-type.solarforecast.actual-channel.label = Actual Energy Forecast +channel-type.solarforecast.actual-channel.description = Todays production forecast till now +channel-type.solarforecast.actual-power-channel.label = Actual Power Forecast +channel-type.solarforecast.actual-power-channel.description = Predicted power in this moment +channel-type.solarforecast.day1-channel.label = Forecast Tomorrow +channel-type.solarforecast.day1-channel.description = Tomorrows forecast in total +channel-type.solarforecast.day1-high-channel.label = Forecast Tomorrow High +channel-type.solarforecast.day1-high-channel.description = Tomorrows optimistic forecast +channel-type.solarforecast.day1-low-channel.label = Forecast Tomorrow Low +channel-type.solarforecast.day1-low-channel.description = Tomorrows pessimistic forecast +channel-type.solarforecast.day2-channel.label = Day 2 Estimation +channel-type.solarforecast.day2-channel.description = Day after tomorrow estimation +channel-type.solarforecast.day2-high-channel.label = Day 2 High Estimation +channel-type.solarforecast.day2-high-channel.description = Day after tomorrow optimistic estimation +channel-type.solarforecast.day2-low-channel.label = Day 2 Low Estimation +channel-type.solarforecast.day2-low-channel.description = Day after tomorrow pessimistic estimation +channel-type.solarforecast.day3-channel.label = Day 3 Estimation +channel-type.solarforecast.day3-channel.description = Day 3 estimation +channel-type.solarforecast.day3-high-channel.label = Day 3 High Estimation +channel-type.solarforecast.day3-high-channel.description = Day 3 optimistic estimation +channel-type.solarforecast.day3-low-channel.label = Day 3 Low Estimation +channel-type.solarforecast.day3-low-channel.description = Day 3 pessimistic estimation +channel-type.solarforecast.day4-channel.label = Day 4 Estimation +channel-type.solarforecast.day4-channel.description = Day 4 estimation +channel-type.solarforecast.day4-high-channel.label = Day 4 High Estimation +channel-type.solarforecast.day4-high-channel.description = Day 4 optimistic estimation +channel-type.solarforecast.day4-low-channel.label = Day 4 Low Estimation +channel-type.solarforecast.day4-low-channel.description = Day 4 pessimistic estimation +channel-type.solarforecast.day5-channel.label = Day 5 Estimation +channel-type.solarforecast.day5-channel.description = Day 5 estimation +channel-type.solarforecast.day5-high-channel.label = Day 5 High Estimation +channel-type.solarforecast.day5-high-channel.description = Day 5 optimistic estimation +channel-type.solarforecast.day5-low-channel.label = Day 5 Low Estimation +channel-type.solarforecast.day5-low-channel.description = Day 5 pessimistic estimation +channel-type.solarforecast.day6-channel.label = Day 6 Estimation +channel-type.solarforecast.day6-channel.description = Day 6 estimation +channel-type.solarforecast.day6-high-channel.label = Day 6 High Estimation +channel-type.solarforecast.day6-high-channel.description = Day 6 optimistic estimation +channel-type.solarforecast.day6-low-channel.label = Day 6 Low Estimation +channel-type.solarforecast.day6-low-channel.description = Day 6 pessimistic estimation +channel-type.solarforecast.raw-channel.label = Raw JSON Response +channel-type.solarforecast.raw-channel.description = Plain JSON response without conversions +channel-type.solarforecast.raw-tuning-channel.label = Raw JSON Tuning Response +channel-type.solarforecast.raw-tuning-channel.description = Plain JSON response from tuning call +channel-type.solarforecast.remaining-channel.label = Remaining Production Today +channel-type.solarforecast.remaining-channel.description = Forecast of todays remaining production +channel-type.solarforecast.today-channel.label = Forecast Today +channel-type.solarforecast.today-channel.description = Todays forecast in total + +# thing actions +actionDayLabel = Daily Energy Production +actionDayDesc = Get energy production for complete day diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast_xx.properties b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast_xx.properties deleted file mode 100644 index 15492a5f0cd07..0000000000000 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast_xx.properties +++ /dev/null @@ -1,21 +0,0 @@ -# FIXME: please substitute the xx with a proper locale, ie. de -# FIXME: please do not add the file to the repo if you add or change no content -# binding -binding.solarforecast.name = -binding.solarforecast.description = - -# thing types -thing-type.solarforecast.sample.label = -thing-type.solarforecast.sample.description = - -# thing type config description -thing-type.config.solarforecast.sample.hostname.label = -thing-type.config.solarforecast.sample.hostname.description = -thing-type.config.solarforecast.sample.password.label = -thing-type.config.solarforecast.sample.password.description = -thing-type.config.solarforecast.sample.refreshInterval.label = -thing-type.config.solarforecast.sample.refreshInterval.description = - -# channel types -channel-type.solarforecast.sample-channel.label = -channel-type.solarforecast.sample-channel.description = 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 index d20bcd66d3165..5ccd866335895 100644 --- 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 @@ -103,4 +103,20 @@ void testErrorCases() { assertEquals(-1.0, fo.getRemainingProduction(now), 0.001, "Remaining Production"); assertEquals(-1.0, fo.getDayTotal(now, 1), 0.001, "Tomorrow Production"); } + + @Test + void testActions() { + String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); + LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 23); + ForecastSolarObject fo = new ForecastSolarObject(content, now, now); + System.out.println(fo.getForecastBegin()); + System.out.println(fo.getForecastEnd()); + System.out.println(fo.getDay(now.toLocalDate())); + System.out.println(fo.getDay(now.toLocalDate())); + System.out.println(fo.getPower(now)); + System.out.println(fo.getEnergy(now, now.plusDays(2))); + System.out.println(fo.getEnergy(now, now.plusMinutes(120))); + System.out.println(fo.getEnergy(now, now.plusDays(20))); + System.out.println(fo.getEnergy(now, now.plusDays(20))); + } } 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 index ce4ed9d15096c..3be2527dc44aa 100644 --- 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 @@ -41,6 +41,7 @@ void testForecastObject() { ZonedDateTime now = LocalDateTime.of(2022, 7, 23, 16, 23).atZone(ZoneId.systemDefault()); SolcastObject scfo = new SolcastObject(content, now); assertEquals(16.809, scfo.getActualValue(now), 0.001, "Actual estimation"); + System.out.println(scfo.getDayTotal(now, -1)); } /** @@ -138,6 +139,23 @@ void testJoin() { 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(ZoneId.systemDefault()); + SolcastObject scfo = new SolcastObject(content, now); + assertEquals(-1.0, scfo.getActualValue(now), 0.01, "Invalid"); + content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + scfo.join(content); + System.out.println(scfo.getForecastBegin()); + System.out.println(scfo.getForecastEnd()); + System.out.println(scfo.getDay(now.toLocalDate())); + System.out.println(scfo.getPower(now.toLocalDateTime())); + System.out.println(scfo.getEnergy(now.toLocalDateTime(), now.plusDays(2).toLocalDateTime())); + System.out.println(scfo.getEnergy(now.toLocalDateTime(), now.plusMinutes(20).toLocalDateTime())); + System.out.println(scfo.getEnergy(now.toLocalDateTime(), now.plusDays(20).toLocalDateTime())); + } + @Test void testOptimisticPessimistic() { String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); @@ -145,7 +163,7 @@ void testOptimisticPessimistic() { SolcastObject scfo = new SolcastObject(content, now); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); - assertEquals(19.389, scfo.getDayTotal(now, 2), 0.001, "Estimation"); + assertEquals(21.96, scfo.getDayTotal(now, 2), 0.001, "Estimation"); assertEquals(7.358, scfo.getPessimisticDayTotal(now, 2), 0.001, "Estimation"); assertEquals(22.283, scfo.getOptimisticDayTotal(now, 2), 0.001, "Estimation"); } From b88cc59d86b22f6cb027e62ba0095db3e71a2a5f Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sun, 7 Aug 2022 17:47:24 +0200 Subject: [PATCH 016/111] trace adaptions Signed-off-by: Bernd Weymann --- .../README.md | 71 +++++++++++-------- .../internal/SolarForecastActions.java | 32 ++++----- .../binding/solarforecast/internal/Utils.java | 36 +++++++++- .../ForecastSolarBridgeHandler.java | 15 +++- .../forecastsolar/ForecastSolarObject.java | 16 ++--- .../ForecastSolarPlaneHandler.java | 11 +-- .../solcast/SolcastBridgeHandler.java | 30 +++++--- .../internal/solcast/SolcastObject.java | 4 +- .../internal/solcast/SolcastPlaneHandler.java | 52 ++++---------- .../binding/solarforecast/SolcastTest.java | 14 ++-- 10 files changed, 155 insertions(+), 126 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 39903917c4e5b..ee6dd260ede7e 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -163,7 +163,7 @@ You can execute this for each `xx-plane` thing for specific plane values or `xx- ### Get Forecast Begin -```` +````java LocalDateTime getForecastBegin() ```` @@ -173,7 +173,7 @@ It's located in the past, e.g. Solcast provides data from the last 7 days. ### Get Forecast End -```` +````java LocalDateTime getForecastEnd() ```` @@ -182,33 +182,33 @@ Returns `LocalDateTime` of the latest possible forecast data available. ### Get Power -```` +````java State getPower(LocalDateTime dateTime) ```` Returns `QuantityType` at the given `dateTime`. Respect `getForecastBegin` and `getForecastEnd` to avoid ambigouos values. -Check for `UndefType.UNDEF` in case of an errors. +Check for `UndefType.UNDEF` in case of errors. ### Get Day -```` +````java State getDay(LocalDate localDate) ```` Returns `QuantityType` at the given `localDate`. Respect `getForecastBegin` and `getForecastEnd` to avoid ambigouos values. -Check for `UndefType.UNDEF` in case of an errors. +Check for `UndefType.UNDEF` in case of errors. ### Get Energy -```` +````java State State getEnergy(LocalDateTime begin, LocalDateTime end) ```` Returns `QuantityType` between the timestamps `begin` and `end`. Respect `getForecastBegin` and `getForecastEnd` to avoid ambigouos values. -Check for `UndefType.UNDEF` in case of an errors. +Check for `UndefType.UNDEF` in case of errors. ## Example @@ -228,20 +228,23 @@ Bridge solarforecast:fs-site:homeSite "ForecastSolar Home" [ location="54.321, ### Items file ```` -Number:Energy ForecastSolarHome_Actual "Actual Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-site:homeSite:actual" } -Number:Energy ForecastSolarHome_Remaining "Remaining Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-site:homeSite:remaining" } -Number:Energy ForecastSolarHome_Today "Today Total Forecast [%3.f %unit%]" {channel="solarforecast:fs-site:homeSite:today" } -Number:Energy ForecastSolarHome_Day1 "Tomorrow Total Forecast [%3.f %unit%]" {channel="solarforecast:fs-site:homeSite:day1" } - -Number:Energy ForecastSolarHome_Actual_NE "Actual NE Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:actual" } -Number:Energy ForecastSolarHome_Remaining_NE "Remaining NE Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:remaining" } -Number:Energy ForecastSolarHome_Today_NE "Total NE Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:today" } -Number:Energy ForecastSolarHome_Day_NE "Tomorrow NE Forecast [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:day1" } - -Number:Energy ForecastSolarHome_Actual_SW "Actual SW Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:actual" } -Number:Energy ForecastSolarHome_Remaining_SW "Remaining SW Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:remaining" } -Number:Energy ForecastSolarHome_Today_SW "Total SW Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:today" } -Number:Energy ForecastSolarHome_Day_SW "Tomorrow SW Forecast [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:day1" } +Number:Energy ForecastSolarHome_Actual "Actual Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-site:homeSite:actual" } +Number:Power ForecastSolarHome_Actual_Power "Actual Power Forecast [%3.f %unit%]" {channel="solarforecast:fs-site:homeSite:actual-power" } +Number:Energy ForecastSolarHome_Remaining "Remaining Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-site:homeSite:remaining" } +Number:Energy ForecastSolarHome_Today "Today Total Forecast [%3.f %unit%]" {channel="solarforecast:fs-site:homeSite:today" } +Number:Energy ForecastSolarHome_Day1 "Tomorrow Total Forecast [%3.f %unit%]" {channel="solarforecast:fs-site:homeSite:day1" } + +Number:Energy ForecastSolarHome_Actual_NE "Actual NE Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:actual" } +Number:Power ForecastSolarHome_Actual_Power_NE "Actual NE Power Forecast [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:actual-power" } +Number:Energy ForecastSolarHome_Remaining_NE "Remaining NE Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:remaining" } +Number:Energy ForecastSolarHome_Today_NE "Total NE Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:today" } +Number:Energy ForecastSolarHome_Day_NE "Tomorrow NE Forecast [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:day1" } + +Number:Energy ForecastSolarHome_Actual_SW "Actual SW Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:actual" } +Number:Power ForecastSolarHome_Actual_Power_SW "Actual SW Power Forecast [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:actual-power" } +Number:Energy ForecastSolarHome_Remaining_SW "Remaining SW Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:remaining" } +Number:Energy ForecastSolarHome_Today_SW "Total SW Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:today" } +Number:Energy ForecastSolarHome_Day_SW "Tomorrow SW Forecast [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:day1" } ```` ### Actions rule @@ -253,22 +256,28 @@ rule "Forecast Solar Actions" then // get Actions for specific fs-site val solarforecastActions = getActions("solarforecast","solarforecast:fs-site:homeSite") - + // get earliest and latest forecast dates val beginDT = solarforecastActions.getForecastBegin val endDT = solarforecastActions.getForecastEnd logInfo("SF Tests","Begin: "+ beginDT+" End: "+endDT) - // get daily forecast for tomorrow - val fcToday = solarforecastActions.getDay(LocalDateTime.now.plusDays(1)) - logInfo("SF Tests","Forecast tomorrow: "+ fcToday.toString) + // get forecast for tomorrow + val fcTomorrowState = solarforecastActions.getDay(LocalDate.now.plusDays(1)) + logInfo("SF Tests","Forecast tomorrow state: "+ fcTomorrowState.toString) + val fcToTomorrowDouble = (fcTomorrowState as Number).doubleValue + logInfo("SF Tests","Forecast tomorrow value: "+ fcToTomorrowDouble) // get power forecast in one hour - val currentPower = solarforecastActions.getPower(LocalDateTime.now.plusHours(1)) - logInfo("SF Tests","Hour+1 Power: "+ currentPower.toString) + val hourPlusOnePowerState = solarforecastActions.getPower(LocalDateTime.now.plusHours(1)) + logInfo("SF Tests","Hour+1 power state: "+ hourPlusOnePowerState.toString) + val hourPlusOnePowerValue = (hourPlusOnePowerState as Number).doubleValue + logInfo("SF Tests","Hour+1 power value: "+ hourPlusOnePowerValue) // get total energy forecast from now till 2 days ahead - val twoDaysForecast = solarforecastActions.getEnergy(LocalDateTime.now,LocalDateTime.now.plusDays(2)) - logInfo("SF Tests","Forecast 2 Days: "+ twoDaysForecast.toString) + val twoDaysForecastFromNowState = solarforecastActions.getEnergy(LocalDateTime.now,LocalDateTime.now.plusDays(2)) + logInfo("SF Tests","Forecast 2 days state: "+ twoDaysForecastFromNowState.toString) + val twoDaysForecastFromNowValue = (twoDaysForecastFromNowState as Number).doubleValue + logInfo("SF Tests","Forecast 2 days value: "+ twoDaysForecastFromNowValue) end -```` \ No newline at end of file +```` diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastActions.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastActions.java index 3e23d37957866..01fdfafd7c71c 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastActions.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastActions.java @@ -51,21 +51,20 @@ public class SolarForecastActions implements ThingActions { @RuleAction(label = "@text/actionDayLabel", description = "@text/actionDayDesc") public State getDay( @ActionInput(name = "localDate", label = "@text/actionInputDayLabel", description = "@text/actionInputDayDesc") LocalDate localDate) { - logger.info("getDay"); if (thingHandler.isPresent()) { List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); - logger.info("Found {} SolarForecast entries", l.size()); + logger.trace("Found {} SolarForecast entries", l.size()); if (!l.isEmpty()) { QuantityType measure = QuantityType.valueOf(0, Units.KILOWATT_HOUR); for (Iterator iterator = l.iterator(); iterator.hasNext();) { SolarForecast solarForecast = iterator.next(); State s = solarForecast.getDay(localDate); - logger.info("Found measure {}", s); + logger.trace("Found measure {}", s); if (s instanceof QuantityType) { measure = measure.add((QuantityType) s); } else { // break in case of failure getting values to avoid ambiguous values - logger.info("Found measure {} - break", s); + logger.debug("Found measure {} - break", s); return UnDefType.UNDEF; } } @@ -80,18 +79,18 @@ public State getPower( @ActionInput(name = "localDateTime", label = "@text/actionInputDateTimeLabel", description = "@text/actionInputDateTimeDesc") LocalDateTime localDateTime) { if (thingHandler.isPresent()) { List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); - logger.info("Found {} SolarForecast entries", l.size()); + logger.trace("Found {} SolarForecast entries", l.size()); if (!l.isEmpty()) { QuantityType measure = QuantityType.valueOf(0, MetricPrefix.KILO(Units.WATT)); for (Iterator iterator = l.iterator(); iterator.hasNext();) { SolarForecast solarForecast = iterator.next(); State s = solarForecast.getPower(localDateTime); - logger.info("Found measure {}", s); + logger.trace("Found measure {}", s); if (s instanceof QuantityType) { measure = measure.add((QuantityType) s); } else { // break in case of failure getting values to avoid ambiguous values - logger.info("Found measure {} - break", s); + logger.debug("Found measure {} - break", s); return UnDefType.UNDEF; } } @@ -107,18 +106,18 @@ public State getEnergy( @ActionInput(name = "localDateTimeEnd", label = "@text/actionInputDateTimeEndLabel", description = "@text/actionInputDateTimeEndDesc") LocalDateTime localDateTimeEnd) { if (thingHandler.isPresent()) { List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); - logger.info("Found {} SolarForecast entries", l.size()); + logger.trace("Found {} SolarForecast entries", l.size()); if (!l.isEmpty()) { QuantityType measure = QuantityType.valueOf(0, Units.KILOWATT_HOUR); for (Iterator iterator = l.iterator(); iterator.hasNext();) { SolarForecast solarForecast = iterator.next(); State s = solarForecast.getEnergy(localDateTimeBegin, localDateTimeEnd); - logger.info("Found measure {}", s); + logger.trace("Found measure {}", s); if (s instanceof QuantityType) { measure = measure.add((QuantityType) s); } else { // break in case of failure getting values to avoid ambiguous values - logger.info("Found measure {} - break", s); + logger.debug("Found measure {} - break", s); return UnDefType.UNDEF; } } @@ -130,16 +129,15 @@ public State getEnergy( @RuleAction(label = "@text/actionForecastBeginLabel", description = "@text/actionForecastBeginDesc") public LocalDateTime getForecastBegin() { - logger.info("getForecastBegin"); LocalDateTime returnLdt = LocalDateTime.MIN; if (thingHandler.isPresent()) { List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); - logger.info("Found {} SolarForecast entries", l.size()); + logger.trace("Found {} SolarForecast entries", l.size()); if (!l.isEmpty()) { for (Iterator iterator = l.iterator(); iterator.hasNext();) { SolarForecast solarForecast = iterator.next(); LocalDateTime forecastLdt = solarForecast.getForecastBegin(); - logger.info("Found {}", forecastLdt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + logger.trace("Found {}", forecastLdt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); // break in case of failure getting values to avoid ambiguous values if (forecastLdt.equals(LocalDateTime.MIN)) { return LocalDateTime.MIN; @@ -147,13 +145,11 @@ public LocalDateTime getForecastBegin() { // take latest possible timestamp to avoid ambiguous values if (forecastLdt.isAfter(returnLdt)) { returnLdt = forecastLdt; - logger.info("Set {}", forecastLdt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + logger.trace("Set {}", forecastLdt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); } } return returnLdt; } - } else { - logger.info("Thinghandler missing"); } return LocalDateTime.MIN; } @@ -178,8 +174,6 @@ public LocalDateTime getForecastEnd() { } return returnLdt; } - } else { - logger.info("Thinghandler missing"); } return LocalDateTime.MAX; } @@ -206,7 +200,7 @@ public static LocalDateTime getForecastEnd(ThingActions actions) { @Override public void setThingHandler(ThingHandler handler) { - logger.info("ThingHandler {} set", handler.toString()); + logger.trace("ThingHandler {} set", handler.toString()); thingHandler = Optional.of(handler); } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/Utils.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/Utils.java index fc7042b9c8f4c..b97c01c90e7db 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/Utils.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/Utils.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.solarforecast.internal; +import java.time.ZonedDateTime; + import javax.measure.MetricPrefix; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -21,7 +23,7 @@ import org.openhab.core.types.UnDefType; /** - * The {@link Utils} Some helpers for all + * The {@link Utils} Helpers for Solcast and ForecastSolar * * @author Bernd Weymann - Initial contribution */ @@ -31,7 +33,7 @@ public static State getEnergyState(double d) { if (d < 0) { return UnDefType.UNDEF; } else { - return QuantityType.valueOf(d, Units.KILOWATT_HOUR); + return QuantityType.valueOf(Math.round(d * 1000) / 1000.0, Units.KILOWATT_HOUR); } } @@ -39,7 +41,35 @@ public static State getPowerState(double d) { if (d < 0) { return UnDefType.UNDEF; } else { - return QuantityType.valueOf(d, MetricPrefix.KILO(Units.WATT)); + return QuantityType.valueOf(Math.round(d * 1000) / 1000.0, MetricPrefix.KILO(Units.WATT)); + } + } + + /** + * Get time frames in 15 minutes intervals + * + * @return + */ + public static ZonedDateTime getNextTimeframe(ZonedDateTime now) { + ZonedDateTime nextTime; + int quarter = now.getMinute() / 15; + switch (quarter) { + case 0: + nextTime = now.withMinute(15).withSecond(0).withNano(0); + break; + case 1: + nextTime = now.withMinute(30).withSecond(0).withNano(0); + break; + case 2: + nextTime = now.withMinute(45).withSecond(0).withNano(0); + break; + case 3: + nextTime = now.withMinute(0).withSecond(0).withNano(0).plusHours(1); + break; + default: + nextTime = now; + break; } + return nextTime; } } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java index 8c1ce5ffd779d..662aeb15ff532 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java @@ -58,6 +58,7 @@ public class ForecastSolarBridgeHandler extends BaseBridgeHandler implements Sol public ForecastSolarBridgeHandler(Bridge bridge, PointType location) { super(bridge); homeLocation = location; + logger.debug("{} Constructor", bridge.getLabel()); } @Override @@ -67,6 +68,7 @@ public Collection> getServices() { @Override public void initialize() { + logger.debug("{} initialize", thing.getLabel()); ForecastSolarBridgeConfiguration config = getConfigAs(ForecastSolarBridgeConfiguration.class); if (config.location.equals(SolarForecastBindingConstants.AUTODETECT)) { Configuration editConfig = editConfiguration(); @@ -85,13 +87,19 @@ public void handleCommand(ChannelUID channelUID, Command command) { } private void startSchedule(int interval) { + /** + * Interval given in minutes so seconds needs multiplier. Wait for 10 seconds until attached planes are created + * and registered. If now waiting time is defined user will see some glitches e.g. after restart if no or not + * all planes are initialized + */ refreshJob.ifPresentOrElse(job -> { if (job.isCancelled()) { refreshJob = Optional - .of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES)); + .of(scheduler.scheduleWithFixedDelay(this::getData, 10, interval * 60, TimeUnit.SECONDS)); } // else - scheduler is already running! }, () -> { - refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES)); + refreshJob = Optional + .of(scheduler.scheduleWithFixedDelay(this::getData, 10, interval * 60, TimeUnit.SECONDS)); }); } @@ -99,6 +107,7 @@ private void startSchedule(int interval) { * Get data for all planes. Protect parts map from being modified during update */ private synchronized void getData() { + logger.debug("{} getData for {} planes", thing.getLabel(), parts.size()); if (parts.isEmpty()) { logger.debug("No PV plane defined yet"); return; @@ -157,7 +166,7 @@ public synchronized List getSolarForecasts() { List l = new ArrayList(); parts.forEach(entry -> { l.addAll(entry.getSolarForecasts()); - logger.info("{} Forecast added", entry.getSolarForecasts().size()); + logger.trace("Actions: {} forecast added", entry.getSolarForecasts().size()); }); return l; 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 index 6773708ebdbef..ec00aa8625b55 100644 --- 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 @@ -100,14 +100,14 @@ public double getActualValue(LocalDateTime now) { int interpolation = now.getMinute() - f.getKey().getMinute(); double interpolationProduction = production * interpolation / 60; double actualProduction = f.getValue() + interpolationProduction; - return Math.round(actualProduction) / 1000.0; + return actualProduction / 1000.0; } else { // ceiling from wrong date - return Math.round(f.getValue()) / 1000.0; + return f.getValue() / 1000.0; } } else { // sun is down - return Math.round(f.getValue()) / 1000.0; + return f.getValue() / 1000.0; } } else { // floor from wrong date @@ -135,7 +135,7 @@ public double getActualPowerValue(LocalDateTime now) { // => take 2/3 of floor and 1/3 of ceiling double interpolation = (now.getMinute() - f.getKey().getMinute()) / 60.0; actualPowerValue = ((1 - interpolation) * powerFloor) + (interpolation * powerCeiling); - return Math.round(actualPowerValue) / 1000.0; + return actualPowerValue / 1000.0; } else { // sun is down return 0; @@ -155,7 +155,7 @@ private double getDayTotal(LocalDate ld) { JSONObject wattsDay = resultJson.getJSONObject("watt_hours_day"); if (wattsDay.has(ld.toString())) { - return Math.round(wattsDay.getDouble(ld.toString())) / 1000.0; + return wattsDay.getDouble(ld.toString()) / 1000.0; } return UNDEF; } @@ -184,7 +184,7 @@ public String toString() { @Override public State getDay(LocalDate localDate) { double measure = getDayTotal(localDate); - logger.info("Deliver measure {}", measure); + logger.trace("Actions: deliver measure {}", measure); return Utils.getEnergyState(measure); } @@ -208,13 +208,13 @@ public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDate } measure += getActualValue(localDateTimeEnd); } - return Utils.getEnergyState(Math.round(measure * 1000) / 1000.0); + return Utils.getEnergyState(measure); } @Override public State getPower(LocalDateTime localDateTime) { double measure = getActualPowerValue(localDateTime); - logger.info("Deliver measure {}", measure); + logger.trace("Actions: deliver measure {}", measure); return Utils.getPowerState(measure); } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java index e759c4c68fa82..c1728c23c4f46 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java @@ -65,6 +65,7 @@ public class ForecastSolarPlaneHandler extends BaseThingHandler implements Solar public ForecastSolarPlaneHandler(Thing thing, HttpClient hc) { super(thing); httpClient = hc; + logger.debug("{} Constructor", thing.getLabel()); } @Override @@ -74,6 +75,7 @@ public Collection> getServices() { @Override public void initialize() { + logger.debug("{} initialize", thing.getLabel()); ForecastSolarPlaneConfiguration c = getConfigAs(ForecastSolarPlaneConfiguration.class); configuration = Optional.of(c); Bridge bridge = getBridge(); @@ -106,7 +108,7 @@ public void dispose() { @Override public void handleCommand(ChannelUID channelUID, Command command) { - logger.info("Handle command {} for channel {}", channelUID, command); + logger.trace("Handle command {} for channel {}", channelUID, command); if (command instanceof RefreshType) { fetchData(); } @@ -116,6 +118,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { * https://doc.forecast.solar/doku.php?id=api:estimate */ protected ForecastSolarObject fetchData() { + logger.debug("{} fetch data", thing.getLabel()); if (location.isPresent()) { if (!forecast.isValid()) { String url; @@ -141,9 +144,9 @@ protected ForecastSolarObject fetchData() { LocalDateTime.now().plusMinutes(configuration.get().refreshInterval)); setForecast(localForecast); logger.debug("{} Fetched data {}", thing.getLabel(), forecast.toString()); - logger.info("{} {} HTTP errors since last successful update", thing.getLabel(), failureCounter); + logger.debug("{} {} HTTP errors since last successful update", thing.getLabel(), + failureCounter); failureCounter = 0; - updateChannels(forecast); updateState(CHANNEL_RAW, StringType.valueOf(cr.getContentAsString())); } else { logger.debug("{} Call {} failed {}", thing.getLabel(), url, cr.getStatus()); @@ -192,7 +195,7 @@ void setApiKey(String key) { } private synchronized void setForecast(ForecastSolarObject f) { - logger.info("Forecast added - valid? {}", f.isValid()); + logger.debug("Forecast set - valid? {}", f.isValid()); forecast = f; } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java index 3ed664b6baeeb..4ca354debe2e6 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java @@ -36,6 +36,7 @@ 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.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,6 +55,7 @@ public class SolcastBridgeHandler extends BaseBridgeHandler implements SolarFore public SolcastBridgeHandler(Bridge bridge) { super(bridge); + logger.debug("{} Constructor", bridge.getLabel()); } @Override @@ -63,11 +65,11 @@ public Collection> getServices() { @Override public void initialize() { + logger.debug("{} initialize", thing.getLabel()); SolcastBridgeConfiguration config = getConfigAs(SolcastBridgeConfiguration.class); configuration = Optional.of(config); if (!EMPTY.equals(config.apiKey)) { updateStatus(ThingStatus.ONLINE); - getData(); startSchedule(configuration.get().channelRefreshInterval); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "API Key is mandatory"); @@ -77,23 +79,39 @@ public void initialize() { @Override public void handleCommand(ChannelUID channelUID, Command command) { + logger.trace("Handle command {} for channel {}", channelUID, command); + if (command instanceof RefreshType) { + getData(); + } } private void startSchedule(int interval) { + /** + * Interval given in minutes so seconds needs multiplier. Wait for 10 seconds until attached planes are created + * and registered. If now waiting time is defined user will see some glitches e.g. after restart if no or not + * all planes are initialized + */ refreshJob.ifPresentOrElse(job -> { if (job.isCancelled()) { refreshJob = Optional - .of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES)); + .of(scheduler.scheduleWithFixedDelay(this::getData, 10, interval * 60, TimeUnit.SECONDS)); } // else - scheduler is already running! }, () -> { - refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES)); + refreshJob = Optional + .of(scheduler.scheduleWithFixedDelay(this::getData, 10, interval * 60, TimeUnit.SECONDS)); }); } + @Override + public void dispose() { + refreshJob.ifPresent(job -> job.cancel(true)); + } + /** * Get data for all planes. Protect parts map from being modified during update */ private synchronized void getData() { + logger.debug("{} getData for {} planes", thing.getLabel(), parts.size()); if (parts.isEmpty()) { logger.debug("No PV plane defined yet"); return; @@ -172,14 +190,8 @@ private synchronized void getData() { updateState(CHANNEL_DAY6_LOW, Utils.getEnergyState(day6SumLow)); } - @Override - public void dispose() { - refreshJob.ifPresent(job -> job.cancel(true)); - } - synchronized void addPlane(SolcastPlaneHandler sph) { parts.add(sph); - getData(); } synchronized void removePlane(SolcastPlaneHandler sph) { 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 index 2a1f11961a470..38e0c897dd397 100644 --- 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 @@ -184,7 +184,7 @@ public double getActualPowerValue(ZonedDateTime now) { // => take 2/3 of floor and 1/3 of ceiling double interpolation = (now.getMinute() - f.getKey().getMinute()) / 60.0; actualPowerValue = ((1 - interpolation) * powerFloor) + (interpolation * powerCeiling); - return Math.round(actualPowerValue * 1000) / 1000.0; + return actualPowerValue; } else { // sun is down return 0; @@ -292,7 +292,7 @@ public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDate } measure += getActualValue(zdtEnd); } - return Utils.getEnergyState(Math.round(measure * 1000) / 1000.0); + return Utils.getEnergyState(measure); } @Override diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java index 03b8816cb8571..51654d324e1a6 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java @@ -84,7 +84,8 @@ public SolcastPlaneHandler(Thing thing, HttpClient hc, Optional> getServices() { return Collections.singleton(SolarForecastActions.class); } - /** - * Get time frames in 15 minutes intervals - * - * @return - */ - public static ZonedDateTime getNextTimeframe(ZonedDateTime now) { - ZonedDateTime nextTime; - int quarter = now.getMinute() / 15; - switch (quarter) { - case 0: - nextTime = now.withMinute(15).withSecond(0).withNano(0); - break; - case 1: - nextTime = now.withMinute(30).withSecond(0).withNano(0); - break; - case 2: - nextTime = now.withMinute(45).withSecond(0).withNano(0); - break; - case 3: - nextTime = now.withMinute(0).withSecond(0).withNano(0).plusHours(1); - break; - default: - nextTime = now; - break; - } - return nextTime; - } - @Override public void initialize() { + logger.debug("{} initialize", thing.getLabel()); SolcastPlaneConfiguration c = getConfigAs(SolcastPlaneConfiguration.class); configuration = Optional.of(c); @@ -135,7 +109,7 @@ public void initialize() { logger.info("Item {} not found", c.powerItem); } } else { - logger.info("No Power item configured"); + logger.debug("No Power item configured"); } // connect Bridge & Status @@ -169,7 +143,7 @@ public void dispose() { @Override public void handleCommand(ChannelUID channelUID, Command command) { - logger.info("Handle command {} for channel {}", channelUID, command); + logger.trace("Handle command {} for channel {}", channelUID, command); if (command instanceof RefreshType) { fetchData(); } @@ -179,6 +153,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { * https://doc.forecast.solar/doku.php?id=api:estimate */ protected SolcastObject fetchData() { + logger.debug("{} fetch data", thing.getLabel()); if (!forecast.isValid()) { String forecastUrl = String.format(FORECAST_URL, configuration.get().resourceId); String currentEstimateUrl = String.format(CURRENT_ESTIMATE_URL, configuration.get().resourceId); @@ -202,14 +177,11 @@ protected SolcastObject fetchData() { if (crForecast.getStatus() == 200) { localForecast.join(crForecast.getContentAsString()); setForecast(localForecast); - logger.trace("{} Fetched data {}", thing.getLabel(), forecast.toString()); - updateChannels(forecast); updateState(CHANNEL_RAW, StringType.valueOf(forecast.getRaw())); + logger.trace("{} Fetched data {}", thing.getLabel(), forecast.toString()); } else { logger.info("{} Call {} failed {}", thing.getLabel(), forecastUrl, crForecast.getStatus()); } - updateChannels(forecast); - updateState(CHANNEL_RAW, StringType.valueOf(forecast.getRaw())); } else { logger.info("{} Call {} failed {}", thing.getLabel(), currentEstimateUrl, crEstimate.getStatus()); } @@ -232,7 +204,7 @@ protected SolcastObject fetchData() { private void sendMeasure() { State updateState = UnDefType.UNDEF; if (persistenceService.isPresent() && powerItem.isPresent()) { - logger.info("Get item {}", configuration.get().powerItem); + logger.debug("Get item {}", configuration.get().powerItem); ZonedDateTime beginPeriodDT = nextMeasurement.minusMinutes(MEASURE_INTERVAL_MIN); ZonedDateTime endPeriodDT = nextMeasurement; FilterCriteria fc = new FilterCriteria(); @@ -285,14 +257,14 @@ private void sendMeasure() { } if (power >= 0) { - logger.info("Found {} items with average {} power", count, total / count); + logger.debug("Found {} items with average {} power", count, total / count); JSONObject measureObject = new JSONObject(); JSONObject measure = new JSONObject(); measure.put("period_end", endPeriodDT.format(DateTimeFormatter.ISO_INSTANT)); measure.put("period", "PT" + MEASURE_INTERVAL_MIN + "M"); measure.put("total_power", power); measureObject.put("measurement", measure); - logger.info("Send {}", measureObject.toString()); + logger.debug("Send {}", measureObject.toString()); String measureUrl = String.format(MEASUREMENT_URL, configuration.get().resourceId); Request request = httpClient.POST(measureUrl); @@ -317,7 +289,7 @@ private void sendMeasure() { } } updateState(CHANNEL_RAW_TUNING, updateState); - nextMeasurement = getNextTimeframe(ZonedDateTime.now(SolcastConstants.zonedId)); + nextMeasurement = Utils.getNextTimeframe(ZonedDateTime.now(SolcastConstants.zonedId)); } private void updateChannels(SolcastObject f) { @@ -344,10 +316,10 @@ private void updateChannels(SolcastObject f) { updateState(CHANNEL_DAY6, Utils.getEnergyState(f.getDayTotal(now, 6))); updateState(CHANNEL_DAY6_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(now, 6))); updateState(CHANNEL_DAY6_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(now, 6))); - updateState(CHANNEL_RAW, StringType.valueOf(forecast.getRaw())); } private synchronized void setForecast(SolcastObject f) { + logger.debug("{} Forecast set", thing.getLabel()); forecast = f; } 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 index 3be2527dc44aa..559caab7b21d9 100644 --- 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 @@ -22,9 +22,9 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.json.JSONObject; import org.junit.jupiter.api.Test; +import org.openhab.binding.solarforecast.internal.Utils; import org.openhab.binding.solarforecast.internal.solcast.SolcastConstants; import org.openhab.binding.solarforecast.internal.solcast.SolcastObject; -import org.openhab.binding.solarforecast.internal.solcast.SolcastPlaneHandler; import org.openhab.core.library.unit.Units; /** @@ -201,17 +201,17 @@ void testUnitDetection() { @Test void testTimeframes() { ZonedDateTime zdt = ZonedDateTime.of(2022, 7, 22, 17, 3, 10, 345, ZoneId.systemDefault()); - assertEquals("17:15", SolcastPlaneHandler.getNextTimeframe(zdt).toLocalTime().toString(), "Q1"); + assertEquals("17:15", Utils.getNextTimeframe(zdt).toLocalTime().toString(), "Q1"); zdt = zdt.plusMinutes(20); - assertEquals("17:30", SolcastPlaneHandler.getNextTimeframe(zdt).toLocalTime().toString(), "Q2"); + assertEquals("17:30", Utils.getNextTimeframe(zdt).toLocalTime().toString(), "Q2"); zdt = zdt.plusMinutes(3); - assertEquals("17:30", SolcastPlaneHandler.getNextTimeframe(zdt).toLocalTime().toString(), "Q2"); + assertEquals("17:30", Utils.getNextTimeframe(zdt).toLocalTime().toString(), "Q2"); zdt = zdt.plusMinutes(5); - assertEquals("17:45", SolcastPlaneHandler.getNextTimeframe(zdt).toLocalTime().toString(), "Q3"); + assertEquals("17:45", Utils.getNextTimeframe(zdt).toLocalTime().toString(), "Q3"); zdt = zdt.plusMinutes(25); - assertEquals("18:00", SolcastPlaneHandler.getNextTimeframe(zdt).toLocalTime().toString(), "Q4"); + assertEquals("18:00", Utils.getNextTimeframe(zdt).toLocalTime().toString(), "Q4"); zdt = zdt.plusMinutes(6); - assertEquals("18:15", SolcastPlaneHandler.getNextTimeframe(zdt).toLocalTime().toString(), "Q4"); + assertEquals("18:15", Utils.getNextTimeframe(zdt).toLocalTime().toString(), "Q4"); } @Test From f22133dcb4fe142bf885890cc2d694a4ed33c82c Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sun, 7 Aug 2022 18:04:12 +0200 Subject: [PATCH 017/111] readme corrections Signed-off-by: Bernd Weymann --- .../README.md | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index ee6dd260ede7e..7538e2fe78e21 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -14,7 +14,7 @@ Supported Services ## Supported Things -Each service needs one `site` for your location and 1+ Photovoltaic `plane`. +Each service needs one `xx-site` for your location and at least one photovoltaic `xx-plane`. | Name | Thing Type ID | |-----------------------------------|---------------| @@ -27,7 +27,7 @@ Each service needs one `site` for your location and 1+ Photovoltaic `plane`. [Solcast service](https://solcast.com/) requires a personal registration with an email 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. +You need to configure your home photovoltaic system within the web interface. After configuration the necessary information is available. ### Solcast Tuning @@ -42,7 +42,7 @@ As described in [Solcast Rooftop Measurement](https://legacy-docs.solcast.com.au - item is delivering good values and they are stored in persistence - time settings in openHAB are correct in order to so measurements are matching to the measure time frame -After the measurement is sent the `raw-tuning` is reporting the result. +After the measurement is sent the `raw-tuning` channel is reporting the result. ### Solcast Bridge Configuration @@ -74,7 +74,7 @@ Note: `channelRefreshInterval` from [Bridge Configuration](#solcast-bridge-confi It's an optional setting and the [measure is sent to Solcast API in order to tune the forecast](https://legacy-docs.solcast.com.au/#measurements-rooftop-site) in the future. If you don't want to sent measures to Solcast leave this configuration item empty. -`powerUnit' is set to `auto-detect`. +`powerUnit` is set to `auto-detect`. In case the `powerItem` is delivering a valid `QuantityType` state this setting is fine. If the item delivers a raw number without unit please select `powerUnit` accordingly if item state is Watt or Kilowatt unit. @@ -159,7 +159,7 @@ Day*X* channels are referring to forecasts plus *X* days: 1 = tomorrow, 2 = day All things `sc-site`, `sc-plane`, `fs-site` and `fs-plane` are providing the same Actions. While 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` thing for specific plane values or `xx-site` thing which delivers the sum of all attached planes. +You can execute this for each `xx-plane` for specific plane values or `xx-site` to sum up all attached planes. ### Get Forecast Begin @@ -169,7 +169,7 @@ LocalDateTime getForecastBegin() Returns `LocalDateTime` of the earliest possible forecast data available. It's located in the past, e.g. Solcast provides data from the last 7 days. -`LocalDateTime.MIN` is returned in case of now forecast data is available. +`LocalDateTime.MIN` is returned in case of no forecast data is available. ### Get Forecast End @@ -178,7 +178,7 @@ LocalDateTime getForecastEnd() ```` Returns `LocalDateTime` of the latest possible forecast data available. -`LocalDateTime.MAX` is returned in case of now forecast data is available. +`LocalDateTime.MAX` is returned in case of no forecast data is available. ### Get Power @@ -187,7 +187,7 @@ State getPower(LocalDateTime dateTime) ```` Returns `QuantityType` at the given `dateTime`. -Respect `getForecastBegin` and `getForecastEnd` to avoid ambigouos values. +Respect `getForecastBegin` and `getForecastEnd` to get a valid value. Check for `UndefType.UNDEF` in case of errors. ### Get Day @@ -203,7 +203,7 @@ Check for `UndefType.UNDEF` in case of errors. ### Get Energy ````java -State State getEnergy(LocalDateTime begin, LocalDateTime end) +State getEnergy(LocalDateTime begin, LocalDateTime end) ```` Returns `QuantityType` between the timestamps `begin` and `end`. @@ -281,3 +281,15 @@ rule "Forecast Solar Actions" logInfo("SF Tests","Forecast 2 days value: "+ twoDaysForecastFromNowValue) end ```` + +shall produce following output + +```` +2022-08-07 18:02:19.874 [INFO ] [g.openhab.core.model.script.SF Tests] - Begin: 2022-07-31T18:30 End: 2022-08-14T18:00 +2022-08-07 18:02:19.878 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast tomorrow state: 55.999 kWh +2022-08-07 18:02:19.880 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast tomorrow value: 55.999 +2022-08-07 18:02:19.884 [INFO ] [g.openhab.core.model.script.SF Tests] - Hour+1 power state: 2.497 kW +2022-08-07 18:02:19.886 [INFO ] [g.openhab.core.model.script.SF Tests] - Hour+1 power value: 2.497 +2022-08-07 18:02:19.891 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast 2 days state: 112.483 kWh +2022-08-07 18:02:19.892 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast 2 days value: 112.483 +```` From 9a26bd61beea5c54b6d5ef63927c694d889cb7e8 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sun, 7 Aug 2022 18:16:22 +0200 Subject: [PATCH 018/111] readme corrections Signed-off-by: Bernd Weymann --- .../solarforecast/internal/solcast/SolcastPlaneHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java index 51654d324e1a6..03fa153c49c21 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java @@ -274,7 +274,7 @@ private void sendMeasure() { try { ContentResponse crMeasure = request.send(); if (crMeasure.getStatus() == 200) { - logger.info("{} Call {} finished {}", thing.getLabel(), measureUrl, + logger.debug("{} Call {} finished {}", thing.getLabel(), measureUrl, crMeasure.getContentAsString()); updateState = StringType.valueOf(crMeasure.getContentAsString()); } else { From 9bbbce3120ab299893c5ff92ee1da7f0276b858f Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Mon, 8 Aug 2022 13:16:09 +0200 Subject: [PATCH 019/111] rework forecast solar with testing of corner cases Signed-off-by: Bernd Weymann --- .../ForecastSolarBridgeHandler.java | 8 +- .../forecastsolar/ForecastSolarObject.java | 135 ++++++++------- .../ForecastSolarPlaneHandler.java | 10 +- .../solarforecast/ForecastSolarTest.java | 154 +++++++++++++----- 4 files changed, 197 insertions(+), 110 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java index 662aeb15ff532..78cb100fd7203 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java @@ -126,10 +126,10 @@ private synchronized void getData() { actualSum += fo.getActualValue(now); actualPowerSum += fo.getActualPowerValue(now); remainSum += fo.getRemainingProduction(now); - todaySum += fo.getDayTotal(now, 0); - day1Sum += fo.getDayTotal(now, 1); - day2Sum += fo.getDayTotal(now, 2); - day3Sum += fo.getDayTotal(now, 3); + todaySum += fo.getDayTotal(now.toLocalDate()); + day1Sum += fo.getDayTotal(now.plusDays(1).toLocalDate()); + day2Sum += fo.getDayTotal(now.plusDays(2).toLocalDate()); + day3Sum += fo.getDayTotal(now.plusDays(3).toLocalDate()); } updateState(CHANNEL_ACTUAL, Utils.getEnergyState(actualSum)); updateState(CHANNEL_ACTUAL_POWER, Utils.getPowerState(actualPowerSum)); 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 index ec00aa8625b55..051bb22ff5b6d 100644 --- 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 @@ -14,6 +14,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.Iterator; import java.util.Map.Entry; import java.util.Optional; @@ -35,10 +36,13 @@ */ @NonNullByDefault public class ForecastSolarObject implements SolarForecast { - private final Logger logger = LoggerFactory.getLogger(ForecastSolarObject.class); private static final double UNDEF = -1; + + 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 Optional rawData = Optional.empty(); private boolean valid = false; private LocalDateTime expirationDateTime; @@ -47,7 +51,7 @@ public ForecastSolarObject() { expirationDateTime = LocalDateTime.now(); } - public ForecastSolarObject(String content, LocalDateTime now, LocalDateTime expirationDate) { + public ForecastSolarObject(String content, LocalDateTime expirationDate) { expirationDateTime = expirationDate; if (!content.equals(SolarForecastBindingConstants.EMPTY)) { rawData = Optional.of(content); @@ -60,7 +64,7 @@ public ForecastSolarObject(String content, LocalDateTime now, LocalDateTime expi while (iter.hasNext()) { String dateStr = iter.next(); // convert date time into machine readable format - LocalDateTime ldt = LocalDateTime.parse(dateStr.replace(" ", "T")); + LocalDateTime ldt = LocalDateTime.parse(dateStr, dateInputFormatter); wattHourMap.put(ldt, wattHourJson.getDouble(dateStr)); wattMap.put(ldt, wattJson.getDouble(dateStr)); } @@ -85,68 +89,87 @@ public boolean isValid() { return false; } - public double getActualValue(LocalDateTime now) { + public double getActualValue(LocalDateTime queryDateTime) { if (wattHourMap.isEmpty()) { return UNDEF; } - Entry f = wattHourMap.floorEntry(now); - Entry c = wattHourMap.ceilingEntry(now); - if (f != null) { - if (f.getKey().toLocalDate().equals(now.toLocalDate())) { - if (c != null) { - if (c.getKey().toLocalDate().equals(now.toLocalDate())) { - // we're during suntime! - double production = c.getValue() - f.getValue(); - int interpolation = now.getMinute() - f.getKey().getMinute(); - double interpolationProduction = production * interpolation / 60; - double actualProduction = f.getValue() + interpolationProduction; - return actualProduction / 1000.0; - } else { - // ceiling from wrong date - return f.getValue() / 1000.0; - } + 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 + return UNDEF; + } + } 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 + return UNDEF; + } + } else { + // 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(); + int interpolation = queryDateTime.getMinute() - f.getKey().getMinute(); + double interpolationProduction = production * interpolation / 60; + double actualProduction = f.getValue() + interpolationProduction; + return actualProduction / 1000.0; } else { - // sun is down + // ceiling from wrong date, but floor is valid return f.getValue() / 1000.0; } } else { - // floor from wrong date + // floor invalid - ceiling not reached return 0; } - } else { - // no floor - sun not rised yet - return 0; } } - public double getActualPowerValue(LocalDateTime now) { + public double getActualPowerValue(LocalDateTime queryDateTime) { if (wattMap.isEmpty()) { return UNDEF; } double actualPowerValue = 0; - Entry f = wattMap.floorEntry(now); - Entry c = wattMap.ceilingEntry(now); - if (f != null) { - if (c != null) { - // we're during suntime! - double powerCeiling = c.getValue(); - double powerFloor = f.getValue(); - // calculate in minutes from floor to now, e.g. 20 minutes - // => take 2/3 of floor and 1/3 of ceiling - double interpolation = (now.getMinute() - f.getKey().getMinute()) / 60.0; - actualPowerValue = ((1 - interpolation) * powerFloor) + (interpolation * powerCeiling); - return actualPowerValue / 1000.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 { - // sun is down + // floor date doesn't fit + return UNDEF; + } + } 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 + return UNDEF; } } else { - // no floor - sun not rised yet - return 0; + // we're during suntime! + double powerCeiling = c.getValue(); + double powerFloor = f.getValue(); + // calculate in minutes from floor to now, e.g. 20 minutes + // => take 2/3 of floor and 1/3 of ceiling + double interpolation = (queryDateTime.getMinute() - f.getKey().getMinute()) / 60.0; + actualPowerValue = ((1 - interpolation) * powerFloor) + (interpolation * powerCeiling); + return actualPowerValue / 1000.0; } } - private double getDayTotal(LocalDate ld) { + public double getDayTotal(LocalDate queryDate) { if (rawData.isEmpty()) { return UNDEF; } @@ -154,25 +177,22 @@ private double getDayTotal(LocalDate ld) { JSONObject resultJson = contentJson.getJSONObject("result"); JSONObject wattsDay = resultJson.getJSONObject("watt_hours_day"); - if (wattsDay.has(ld.toString())) { - return wattsDay.getDouble(ld.toString()) / 1000.0; + if (wattsDay.has(queryDate.toString())) { + return wattsDay.getDouble(queryDate.toString()) / 1000.0; } return UNDEF; } - public double getDayTotal(LocalDateTime now, int offset) { - if (rawData.isEmpty()) { + public double getRemainingProduction(LocalDateTime queryDateTime) { + if (wattHourMap.isEmpty()) { return UNDEF; } - LocalDate ld = now.plusDays(offset).toLocalDate(); - return getDayTotal(ld); - } - - public double getRemainingProduction(LocalDateTime now) { - if (wattHourMap.isEmpty()) { + double daily = getDayTotal(queryDateTime.toLocalDate()); + double actual = getActualValue(queryDateTime); + if (daily < 0 || actual < 0) { return UNDEF; } - return getDayTotal(now, 0) - getActualValue(now); + return daily - actual; } @Override @@ -194,19 +214,22 @@ public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDate LocalDate endDate = localDateTimeEnd.toLocalDate(); double measure = UNDEF; if (beginDate.equals(endDate)) { - measure = getDayTotal(localDateTimeEnd, 0) - getActualValue(localDateTimeBegin) + measure = getDayTotal(beginDate) - getActualValue(localDateTimeBegin) - getRemainingProduction(localDateTimeEnd); } else { measure = getRemainingProduction(localDateTimeBegin); beginDate = beginDate.plusDays(1); - while (beginDate.isBefore(endDate)) { + while (beginDate.isBefore(endDate) && measure >= 0) { double day = getDayTotal(beginDate); if (day > 0) { measure += day; } beginDate = beginDate.plusDays(1); } - measure += getActualValue(localDateTimeEnd); + double lastDay = getActualValue(localDateTimeEnd); + if (lastDay >= 0) { + measure += getActualValue(localDateTimeEnd); + } } return Utils.getEnergyState(measure); } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java index c1728c23c4f46..5bb4de69d083b 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java @@ -140,7 +140,7 @@ protected ForecastSolarObject fetchData() { ContentResponse cr = httpClient.GET(url); if (cr.getStatus() == 200) { ForecastSolarObject localForecast = new ForecastSolarObject(cr.getContentAsString(), - LocalDateTime.now(), + LocalDateTime.now().plusMinutes(configuration.get().refreshInterval)); setForecast(localForecast); logger.debug("{} Fetched data {}", thing.getLabel(), forecast.toString()); @@ -170,10 +170,10 @@ private void updateChannels(ForecastSolarObject f) { updateState(CHANNEL_ACTUAL, Utils.getEnergyState(f.getActualValue(now))); updateState(CHANNEL_ACTUAL_POWER, Utils.getPowerState(f.getActualPowerValue(now))); updateState(CHANNEL_REMAINING, Utils.getEnergyState(f.getRemainingProduction(now))); - updateState(CHANNEL_TODAY, Utils.getEnergyState(f.getDayTotal(now, 0))); - updateState(CHANNEL_DAY1, Utils.getEnergyState(f.getDayTotal(now, 1))); - updateState(CHANNEL_DAY2, Utils.getEnergyState(f.getDayTotal(now, 2))); - updateState(CHANNEL_DAY3, Utils.getEnergyState(f.getDayTotal(now, 3))); + updateState(CHANNEL_TODAY, Utils.getEnergyState(f.getDayTotal(now.toLocalDate()))); + updateState(CHANNEL_DAY1, Utils.getEnergyState(f.getDayTotal(now.plusDays(1).toLocalDate()))); + updateState(CHANNEL_DAY2, Utils.getEnergyState(f.getDayTotal(now.plusDays(2).toLocalDate()))); + updateState(CHANNEL_DAY3, Utils.getEnergyState(f.getDayTotal(now.plusDays(3).toLocalDate()))); } /** 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 index 5ccd866335895..10039997ba21a 100644 --- 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 @@ -28,7 +28,7 @@ import org.openhab.core.types.State; /** - * The {@link ForecastSolarTest} tests responses from forecast solar website + * The {@link ForecastSolarTest} tests responses from forecast solar object * * @author Bernd Weymann - Initial contribution */ @@ -40,52 +40,68 @@ class ForecastSolarTest { @Test void testForecastObject() { String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); - LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 23); - ForecastSolarObject fo = new ForecastSolarObject(content, now, now); - assertEquals(49.431, fo.getActualValue(now), 0.001, "Current Production"); - assertEquals(14.152, fo.getRemainingProduction(now), 0.001, "Current Production"); - assertEquals(fo.getDayTotal(now, 0), fo.getActualValue(now) + fo.getRemainingProduction(now), 0.001, - "Total production"); - assertEquals(fo.getDayTotal(now, 0), fo.getActualValue(now) + fo.getRemainingProduction(now), 0.001, - "Total production"); + LocalDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 17, 00); + ForecastSolarObject fo = new ForecastSolarObject(content, queryDateTime); + + // "2022-07-17 21:32:00": 63583, + assertEquals(63.583, fo.getDayTotal(queryDateTime.toLocalDate()), 0.001, "Total production"); + // "2022-07-17 17:00:00": 52896, + assertEquals(52.896, fo.getActualValue(queryDateTime), 0.001, "Current Production"); + // 63583 - 52896 = 10687 + assertEquals(10.687, fo.getRemainingProduction(queryDateTime), 0.001, "Current Production"); + // sum cross check + assertEquals(fo.getDayTotal(queryDateTime.toLocalDate()), + fo.getActualValue(queryDateTime) + fo.getRemainingProduction(queryDateTime), 0.001, + "actual + remain = total"); + + queryDateTime = LocalDateTime.of(2022, 7, 18, 19, 00); + // "2022-07-18 19:00:00": 63067, + assertEquals(63.067, fo.getActualValue(queryDateTime), 0.001, "Actual production"); + // "2022-07-18 21:31:00": 65554 + assertEquals(65.554, fo.getDayTotal(queryDateTime.toLocalDate()), 0.001, "Total production"); } @Test void testActualPower() { String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); - LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 23); - ForecastSolarObject fo = new ForecastSolarObject(content, now, now); - assertEquals(5.704, fo.getActualPowerValue(now), 0.001, "Actual estimation"); - - // todo: test this - date out of scope shall return undef - // LocalDateTime ldt = LocalDateTime.of(2022, 7, 23, 0, 5); - LocalDateTime ldt = LocalDateTime.of(2022, 7, 17, 0, 5); - for (int i = 0; i < 96; i++) { - ldt = ldt.plusMinutes(15); - System.out.println(ldt + " " + fo.getActualPowerValue(ldt)); - } + LocalDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 10, 00); + ForecastSolarObject fo = new ForecastSolarObject(content, queryDateTime); + // "2022-07-17 10:00:00": 4874, + assertEquals(4.874, fo.getActualPowerValue(queryDateTime), 0.001, "Actual estimation"); + + queryDateTime = LocalDateTime.of(2022, 7, 18, 14, 00); + // "2022-07-18 14:00:00": 7054, + assertEquals(7.054, fo.getActualPowerValue(queryDateTime), 0.001, "Actual estimation"); } @Test void testInterpolation() { String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); - LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 0); - ForecastSolarObject fo = new ForecastSolarObject(content, now, now); + LocalDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 0); + ForecastSolarObject fo = new ForecastSolarObject(content, queryDateTime); + + // test steady value increase double previousValue = 0; for (int i = 0; i < 60; i++) { - now = now.plusMinutes(1); - assertTrue(previousValue < fo.getActualValue(now)); - previousValue = fo.getActualValue(now); + queryDateTime = queryDateTime.plusMinutes(1); + assertTrue(previousValue < fo.getActualValue(queryDateTime)); + previousValue = fo.getActualValue(queryDateTime); } + + queryDateTime = LocalDateTime.of(2022, 7, 18, 6, 23); + // "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.getActualValue(queryDateTime), 0.002, "Actual estimation"); } @Test void testForecastSum() { String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); - LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 23); - ForecastSolarObject fo = new ForecastSolarObject(content, now, now); + LocalDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 23); + ForecastSolarObject fo = new ForecastSolarObject(content, queryDateTime); QuantityType actual = QuantityType.valueOf(0, Units.KILOWATT_HOUR); - State st = Utils.getEnergyState(fo.getActualValue(now)); + State st = Utils.getEnergyState(fo.getActualValue(queryDateTime)); assertTrue(st instanceof QuantityType); actual = actual.add((QuantityType) st); assertEquals(49.431, actual.floatValue(), 0.001, "Current Production"); @@ -94,29 +110,77 @@ void testForecastSum() { } @Test - void testErrorCases() { + void testCornerCases() { + // invalid object ForecastSolarObject fo = new ForecastSolarObject(); assertFalse(fo.isValid()); - LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 23); - assertEquals(-1.0, fo.getActualValue(now), 0.001, "Actual Production"); - assertEquals(-1.0, fo.getDayTotal(now, 0), 0.001, "Today Production"); - assertEquals(-1.0, fo.getRemainingProduction(now), 0.001, "Remaining Production"); - assertEquals(-1.0, fo.getDayTotal(now, 1), 0.001, "Tomorrow Production"); + LocalDateTime query = LocalDateTime.of(2022, 7, 17, 16, 23); + assertEquals(-1.0, fo.getActualValue(query), 0.001, "Actual Production"); + assertEquals(-1.0, fo.getDayTotal(query.toLocalDate()), 0.001, "Today Production"); + assertEquals(-1.0, fo.getRemainingProduction(query), 0.001, "Remaining Production"); + assertEquals(-1.0, fo.getDayTotal(query.plusDays(1).toLocalDate()), 0.001, "Tomorrow Production"); + + // 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); + fo = new ForecastSolarObject(content, query); + assertEquals(-1.0, fo.getDayTotal(query.toLocalDate()), 0.001, "Actual out of scope"); + assertEquals(-1.0, fo.getActualValue(query), 0.001, "Actual out of scope"); + assertEquals(-1.0, fo.getRemainingProduction(query), 0.001, "Remain out of scope"); + assertEquals(-1.0, fo.getActualPowerValue(query), 0.001, "Remain out of scope"); + + // one minute later we reach a valid date + query = query.plusMinutes(1); + assertEquals(63.583, fo.getDayTotal(query.toLocalDate()), 0.001, "Actual out of scope"); + assertEquals(0.0, fo.getActualValue(query), 0.001, "Actual out of scope"); + assertEquals(63.583, fo.getRemainingProduction(query), 0.001, "Remain out of scope"); + assertEquals(0.0, fo.getActualPowerValue(query), 0.001, "Remain out of scope"); + + // valid object - query date one day too late + query = LocalDateTime.of(2022, 7, 19, 0, 0); + assertEquals(-1.0, fo.getDayTotal(query.toLocalDate()), 0.001, "Actual out of scope"); + assertEquals(-1.0, fo.getActualValue(query), 0.001, "Actual out of scope"); + assertEquals(-1.0, fo.getRemainingProduction(query), 0.001, "Remain out of scope"); + assertEquals(-1.0, fo.getActualPowerValue(query), 0.001, "Remain out of scope"); + + // one minute earlier we reach a valid date + query = query.minusMinutes(1); + assertEquals(65.554, fo.getDayTotal(query.toLocalDate()), 0.001, "Actual out of scope"); + assertEquals(65.554, fo.getActualValue(query), 0.001, "Actual out of scope"); + assertEquals(0.0, fo.getRemainingProduction(query), 0.001, "Remain out of scope"); + assertEquals(0.0, fo.getActualPowerValue(query), 0.001, "Remain out of scope"); + + // test times between 2 dates + query = LocalDateTime.of(2022, 7, 17, 23, 59); + assertEquals(63.583, fo.getDayTotal(query.toLocalDate()), 0.001, "Actual out of scope"); + assertEquals(63.583, fo.getActualValue(query), 0.001, "Actual out of scope"); + assertEquals(0.0, fo.getRemainingProduction(query), 0.001, "Remain out of scope"); + assertEquals(0.0, fo.getActualPowerValue(query), 0.001, "Remain out of scope"); + query = query.plusMinutes(1); + assertEquals(65.554, fo.getDayTotal(query.toLocalDate()), 0.001, "Actual out of scope"); + assertEquals(0.0, fo.getActualValue(query), 0.001, "Actual out of scope"); + assertEquals(65.554, fo.getRemainingProduction(query), 0.001, "Remain out of scope"); + assertEquals(0.0, fo.getActualPowerValue(query), 0.001, "Remain out of scope"); } @Test void testActions() { String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); - LocalDateTime now = LocalDateTime.of(2022, 7, 17, 16, 23); - ForecastSolarObject fo = new ForecastSolarObject(content, now, now); - System.out.println(fo.getForecastBegin()); - System.out.println(fo.getForecastEnd()); - System.out.println(fo.getDay(now.toLocalDate())); - System.out.println(fo.getDay(now.toLocalDate())); - System.out.println(fo.getPower(now)); - System.out.println(fo.getEnergy(now, now.plusDays(2))); - System.out.println(fo.getEnergy(now, now.plusMinutes(120))); - System.out.println(fo.getEnergy(now, now.plusDays(20))); - System.out.println(fo.getEnergy(now, now.plusDays(20))); + LocalDateTime query = LocalDateTime.of(2022, 7, 17, 16, 23); + ForecastSolarObject fo = new ForecastSolarObject(content, query); + assertEquals("2022-07-17T05:31:00", fo.getForecastBegin().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), + "Forecast begin"); + assertEquals("2022-07-18T21:31:00", fo.getForecastEnd().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), + "Forecast end"); + assertEquals(QuantityType.valueOf(63.583, Units.KILOWATT_HOUR).toString(), + fo.getDay(query.toLocalDate()).toFullString(), "Actual out of scope"); + + query = LocalDateTime.of(2022, 7, 17, 0, 0); + // "watt_hours_day": { + // "2022-07-17": 63583, + // "2022-07-18": 65554 + // } + assertEquals(QuantityType.valueOf(129.137, Units.KILOWATT_HOUR).toString(), + fo.getEnergy(query, query.plusDays(2)).toFullString(), "Actual out of scope"); } } From e75fe7a54eda1a0414677abceab916a9241c6d9a Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Mon, 8 Aug 2022 20:54:38 +0200 Subject: [PATCH 020/111] solcast bugfixes and test enhancement Signed-off-by: Bernd Weymann --- .../forecastsolar/ForecastSolarObject.java | 2 +- .../solcast/SolcastBridgeHandler.java | 40 +-- .../internal/solcast/SolcastObject.java | 165 +++++++----- .../internal/solcast/SolcastPlaneHandler.java | 40 +-- .../solarforecast/ForecastSolarTest.java | 82 +++--- .../binding/solarforecast/SolcastTest.java | 247 +++++++++++++----- 6 files changed, 373 insertions(+), 203 deletions(-) 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 index 051bb22ff5b6d..7f211490e6167 100644 --- 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 @@ -228,7 +228,7 @@ public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDate } double lastDay = getActualValue(localDateTimeEnd); if (lastDay >= 0) { - measure += getActualValue(localDateTimeEnd); + measure += lastDay; } } return Utils.getEnergyState(measure); diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java index 4ca354debe2e6..c5929c9d27b95 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java @@ -14,6 +14,7 @@ import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; +import java.time.LocalDate; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collection; @@ -146,25 +147,26 @@ private synchronized void getData() { actualSum += fo.getActualValue(now); actualPowerSum += fo.getActualPowerValue(now); remainSum += fo.getRemainingProduction(now); - todaySum += fo.getDayTotal(now, 0); - day1Sum += fo.getDayTotal(now, 1); - day1SumLow += fo.getPessimisticDayTotal(now, 1); - day1SumHigh += fo.getOptimisticDayTotal(now, 1); - day2Sum += fo.getDayTotal(now, 2); - day2SumLow += fo.getPessimisticDayTotal(now, 2); - day2SumHigh += fo.getOptimisticDayTotal(now, 2); - day3Sum += fo.getDayTotal(now, 3); - day3SumLow += fo.getPessimisticDayTotal(now, 3); - day3SumHigh += fo.getOptimisticDayTotal(now, 3); - day4Sum += fo.getDayTotal(now, 4); - day4SumLow += fo.getPessimisticDayTotal(now, 4); - day4SumHigh += fo.getOptimisticDayTotal(now, 4); - day5Sum += fo.getDayTotal(now, 5); - day5SumLow += fo.getPessimisticDayTotal(now, 5); - day5SumHigh += fo.getOptimisticDayTotal(now, 5); - day6Sum += fo.getDayTotal(now, 6); - day6SumLow += fo.getPessimisticDayTotal(now, 6); - day6SumHigh += fo.getOptimisticDayTotal(now, 6); + LocalDate nowDate = now.toLocalDate(); + todaySum += fo.getDayTotal(nowDate); + day1Sum += fo.getDayTotal(nowDate.plusDays(1)); + day1SumLow += fo.getPessimisticDayTotal(nowDate.plusDays(1)); + day1SumHigh += fo.getOptimisticDayTotal(nowDate.plusDays(1)); + day2Sum += fo.getDayTotal(nowDate.plusDays(2)); + day2SumLow += fo.getPessimisticDayTotal(nowDate.plusDays(2)); + day2SumHigh += fo.getOptimisticDayTotal(nowDate.plusDays(2)); + day3Sum += fo.getDayTotal(nowDate.plusDays(3)); + day3SumLow += fo.getPessimisticDayTotal(nowDate.plusDays(3)); + day3SumHigh += fo.getOptimisticDayTotal(nowDate.plusDays(3)); + day4Sum += fo.getDayTotal(nowDate.plusDays(4)); + day4SumLow += fo.getPessimisticDayTotal(nowDate.plusDays(4)); + day4SumHigh += fo.getOptimisticDayTotal(nowDate.plusDays(4)); + day5Sum += fo.getDayTotal(nowDate.plusDays(5)); + day5SumLow += fo.getPessimisticDayTotal(nowDate.plusDays(5)); + day5SumHigh += fo.getOptimisticDayTotal(nowDate.plusDays(5)); + day6Sum += fo.getDayTotal(nowDate.plusDays(6)); + day6SumLow += fo.getPessimisticDayTotal(nowDate.plusDays(6)); + day6SumHigh += fo.getOptimisticDayTotal(nowDate.plusDays(6)); } updateState(CHANNEL_ACTUAL, Utils.getEnergyState(actualSum)); updateState(CHANNEL_ACTUAL_POWER, Utils.getPowerState(actualPowerSum)); 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 index 38e0c897dd397..d614c1d6b7605 100644 --- 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 @@ -40,7 +40,7 @@ public class SolcastObject implements SolarForecast { private final Logger logger = LoggerFactory.getLogger(SolcastObject.class); private static final double UNDEF = -1; - private final TreeMap> dataMap = new TreeMap>(); + private final TreeMap> estimationDataMap = new TreeMap>(); private final TreeMap> optimisticDataMap = new TreeMap>(); private final TreeMap> pessimisticDataMap = new TreeMap>(); private Optional rawData = Optional.of(new JSONObject()); @@ -52,8 +52,8 @@ public SolcastObject() { expirationDateTime = ZonedDateTime.now(SolcastConstants.zonedId); } - public SolcastObject(String content, ZonedDateTime zdt) { - expirationDateTime = zdt; + public SolcastObject(String content, ZonedDateTime expiration) { + expirationDateTime = expiration; add(content); } @@ -66,6 +66,8 @@ private void add(String content) { valid = true; JSONObject contentJson = new JSONObject(content); JSONArray resultJsonArray; + + // prepare data for raw channel if (contentJson.has("forecasts")) { resultJsonArray = contentJson.getJSONArray("forecasts"); rawData.get().put("forecasts", resultJsonArray); @@ -73,32 +75,41 @@ private void add(String content) { resultJsonArray = contentJson.getJSONArray("estimated_actuals"); rawData.get().put("estimated_actuals", resultJsonArray); } + + // sort data into TreeMaps for (int i = 0; i < resultJsonArray.length(); i++) { JSONObject jo = resultJsonArray.getJSONObject(i); String periodEnd = jo.getString("period_end"); LocalDate ld = getZdtFromUTC(periodEnd).toLocalDate(); - TreeMap forecastMap = dataMap.get(ld); + TreeMap forecastMap = estimationDataMap.get(ld); if (forecastMap == null) { forecastMap = new TreeMap(); + estimationDataMap.put(ld, forecastMap); } forecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate")); - dataMap.put(ld, forecastMap); + // fill pessimistic values + TreeMap pessimisticForecastMap = pessimisticDataMap.get(ld); + if (pessimisticForecastMap == null) { + pessimisticForecastMap = new TreeMap(); + pessimisticDataMap.put(ld, pessimisticForecastMap); + } if (jo.has("pv_estimate10")) { - TreeMap pessimisticForecastMap = pessimisticDataMap.get(ld); - if (pessimisticForecastMap == null) { - pessimisticForecastMap = new TreeMap(); - } pessimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate10")); - pessimisticDataMap.put(ld, pessimisticForecastMap); + } else { + pessimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate")); + } + + // fill optimistic values + TreeMap optimisticForecastMap = optimisticDataMap.get(ld); + if (optimisticForecastMap == null) { + optimisticForecastMap = new TreeMap(); + optimisticDataMap.put(ld, optimisticForecastMap); } if (jo.has("pv_estimate90")) { - TreeMap optimisticForecastMap = optimisticDataMap.get(ld); - if (optimisticForecastMap == null) { - optimisticForecastMap = new TreeMap(); - } optimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate90")); - optimisticDataMap.put(ld, optimisticForecastMap); + } else { + optimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate")); } } } @@ -106,7 +117,7 @@ private void add(String content) { public boolean isValid() { if (valid) { - if (!dataMap.isEmpty()) { + if (!estimationDataMap.isEmpty()) { if (expirationDateTime.isAfter(ZonedDateTime.now(SolcastConstants.zonedId))) { return true; } else { @@ -121,19 +132,19 @@ public boolean isValid() { return false; } - public double getActualValue(ZonedDateTime now) { - if (dataMap.isEmpty()) { + public double getActualValue(ZonedDateTime query) { + if (estimationDataMap.isEmpty()) { return UNDEF; } - LocalDate ld = now.toLocalDate(); - TreeMap dtm = dataMap.get(ld); + LocalDate ld = query.toLocalDate(); + TreeMap dtm = estimationDataMap.get(ld); if (dtm == null) { return UNDEF; } double forecastValue = 0; Set keySet = dtm.keySet(); for (ZonedDateTime key : keySet) { - if (key.isBefore(now)) { + if (key.isBefore(query) || key.isEqual(query)) { // value are reported in PT30M = 30 minutes interval with kw value // for kw/h it's half the value Double addedValue = dtm.get(key); @@ -143,13 +154,13 @@ public double getActualValue(ZonedDateTime now) { } } - Entry f = dtm.floorEntry(now); - Entry c = dtm.ceilingEntry(now); + Entry f = dtm.floorEntry(query); + Entry c = dtm.ceilingEntry(query); if (f != null) { if (c != null) { // we're during suntime! double production = c.getValue(); - int interpolation = now.getMinute() - f.getKey().getMinute(); + int interpolation = query.getMinute() - f.getKey().getMinute(); double interpolationProduction = production * interpolation / 60; forecastValue += interpolationProduction; return forecastValue; @@ -163,18 +174,50 @@ public double getActualValue(ZonedDateTime now) { } } - public double getActualPowerValue(ZonedDateTime now) { - if (dataMap.isEmpty()) { + /** + * Get power values + */ + + public double getActualPowerValue(ZonedDateTime query) { + if (estimationDataMap.isEmpty()) { + return UNDEF; + } + LocalDate ld = query.toLocalDate(); + TreeMap dtm = estimationDataMap.get(ld); + if (dtm == null) { + return UNDEF; + } + return getPowerFromTreemap(dtm, query); + } + + public double getOptimisticPowerValue(ZonedDateTime query) { + if (optimisticDataMap.isEmpty()) { return UNDEF; } - LocalDate ld = now.toLocalDate(); - TreeMap dtm = dataMap.get(ld); + LocalDate ld = query.toLocalDate(); + TreeMap dtm = optimisticDataMap.get(ld); if (dtm == null) { return UNDEF; } + return getPowerFromTreemap(dtm, query); + } + + public double getPessimisticPowerValue(ZonedDateTime query) { + if (pessimisticDataMap.isEmpty()) { + return UNDEF; + } + LocalDate ld = query.toLocalDate(); + TreeMap dtm = pessimisticDataMap.get(ld); + if (dtm == null) { + return UNDEF; + } + return getPowerFromTreemap(dtm, query); + } + + private double getPowerFromTreemap(TreeMap dtm, ZonedDateTime query) { double actualPowerValue = 0; - Entry f = dtm.floorEntry(now); - Entry c = dtm.ceilingEntry(now); + Entry f = dtm.floorEntry(query); + Entry c = dtm.ceilingEntry(query); if (f != null) { if (c != null) { // we're during suntime! @@ -182,7 +225,7 @@ public double getActualPowerValue(ZonedDateTime now) { double powerFloor = f.getValue(); // calculate in minutes from floor to now, e.g. 20 minutes // => take 2/3 of floor and 1/3 of ceiling - double interpolation = (now.getMinute() - f.getKey().getMinute()) / 60.0; + double interpolation = (query.getMinute() - f.getKey().getMinute()) / 60.0; actualPowerValue = ((1 - interpolation) * powerFloor) + (interpolation * powerCeiling); return actualPowerValue; } else { @@ -195,23 +238,23 @@ public double getActualPowerValue(ZonedDateTime now) { } } - public double getDayTotal(ZonedDateTime now, int offset) { - LocalDate ld = now.plusDays(offset).toLocalDate(); - return getDayTotal(ld, offset); - } + /** + * Daily totals + */ - private double getDayTotal(LocalDate now, int offset) { - TreeMap dtm = dataMap.get(now.plusDays(offset)); + public double getDayTotal(LocalDate query) { + TreeMap dtm = estimationDataMap.get(query); if (dtm != null) { + // JSONObject jot = new JSONObject(dtm); + // System.out.println(jot); return getTotalValue(dtm); } else { return UNDEF; } } - public double getOptimisticDayTotal(ZonedDateTime now, int offset) { - LocalDate ld = now.plusDays(offset).toLocalDate(); - TreeMap dtm = optimisticDataMap.get(ld); + public double getOptimisticDayTotal(LocalDate query) { + TreeMap dtm = optimisticDataMap.get(query); if (dtm != null) { return getTotalValue(dtm); } else { @@ -219,9 +262,8 @@ public double getOptimisticDayTotal(ZonedDateTime now, int offset) { } } - public double getPessimisticDayTotal(ZonedDateTime now, int offset) { - LocalDate ld = now.plusDays(offset).toLocalDate(); - TreeMap dtm = pessimisticDataMap.get(ld); + public double getPessimisticDayTotal(LocalDate query) { + TreeMap dtm = pessimisticDataMap.get(query); if (dtm != null) { return getTotalValue(dtm); } else { @@ -237,22 +279,22 @@ private double getTotalValue(TreeMap map) { // for kw/h it's half the value Double addedValue = map.get(key); if (addedValue != null) { - forecastValue += addedValue.doubleValue() / 2; + forecastValue += addedValue.doubleValue() / 2.0; } } return forecastValue; } - public double getRemainingProduction(ZonedDateTime now) { - if (dataMap.isEmpty()) { + public double getRemainingProduction(ZonedDateTime query) { + if (estimationDataMap.isEmpty()) { return UNDEF; } - return getDayTotal(now, 0) - getActualValue(now); + return getDayTotal(query.toLocalDate()) - getActualValue(query); } @Override public String toString() { - return "Expiration: " + expirationDateTime + ", Valid: " + valid + ", Data:" + dataMap; + return "Expiration: " + expirationDateTime + ", Valid: " + valid + ", Data:" + estimationDataMap; } public String getRaw() { @@ -264,10 +306,12 @@ public static ZonedDateTime getZdtFromUTC(String utc) { return timestamp.atZone(SolcastConstants.zonedId); } - // SolarForecast Interface + /** + * SolarForecast Interface + */ @Override public State getDay(LocalDate localDate) { - double measure = getDayTotal(localDate, 0); + double measure = getDayTotal(localDate); return Utils.getEnergyState(measure); } @@ -278,19 +322,22 @@ public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDate LocalDate beginDate = zdtBegin.toLocalDate(); LocalDate endDate = zdtEnd.toLocalDate(); double measure = UNDEF; - if (beginDate.equals(endDate)) { - measure = getDayTotal(zdtEnd, 0) - getActualValue(zdtBegin) - getRemainingProduction(zdtEnd); + if (beginDate.isEqual(endDate)) { + measure = getDayTotal(zdtEnd.toLocalDate()) - getActualValue(zdtBegin) - getRemainingProduction(zdtEnd); } else { measure = getRemainingProduction(zdtBegin); beginDate = beginDate.plusDays(1); - while (beginDate.isBefore(endDate)) { - double day = getDayTotal(beginDate, 0); + while (beginDate.isBefore(endDate) && measure >= 0) { + double day = getDayTotal(beginDate); if (day > 0) { measure += day; } beginDate = beginDate.plusDays(1); } - measure += getActualValue(zdtEnd); + double lastDay = getActualValue(zdtEnd); + if (lastDay >= 0) { + measure += lastDay; + } } return Utils.getEnergyState(measure); } @@ -304,8 +351,8 @@ public State getPower(LocalDateTime localDateTime) { @Override public LocalDateTime getForecastBegin() { - if (!dataMap.isEmpty()) { - ZonedDateTime zdt = dataMap.firstEntry().getValue().firstEntry().getKey(); + if (!estimationDataMap.isEmpty()) { + ZonedDateTime zdt = estimationDataMap.firstEntry().getValue().firstEntry().getKey(); return zdt.toLocalDateTime(); } return LocalDateTime.MIN; @@ -313,8 +360,8 @@ public LocalDateTime getForecastBegin() { @Override public LocalDateTime getForecastEnd() { - if (!dataMap.isEmpty()) { - ZonedDateTime zdt = dataMap.lastEntry().getValue().lastEntry().getKey(); + if (!estimationDataMap.isEmpty()) { + ZonedDateTime zdt = estimationDataMap.lastEntry().getValue().lastEntry().getKey(); return zdt.toLocalDateTime(); } return LocalDateTime.MIN; diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java index 03fa153c49c21..57a96210a964a 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java @@ -15,6 +15,7 @@ import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; import static org.openhab.binding.solarforecast.internal.solcast.SolcastConstants.*; +import java.time.LocalDate; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.Collection; @@ -297,25 +298,26 @@ private void updateChannels(SolcastObject f) { updateState(CHANNEL_ACTUAL, Utils.getEnergyState(f.getActualValue(now))); updateState(CHANNEL_ACTUAL_POWER, Utils.getEnergyState(f.getActualPowerValue(now))); updateState(CHANNEL_REMAINING, Utils.getEnergyState(f.getRemainingProduction(now))); - updateState(CHANNEL_TODAY, Utils.getEnergyState(f.getDayTotal(now, 0))); - updateState(CHANNEL_DAY1, Utils.getEnergyState(f.getDayTotal(now, 1))); - updateState(CHANNEL_DAY1_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(now, 1))); - updateState(CHANNEL_DAY1_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(now, 1))); - updateState(CHANNEL_DAY2, Utils.getEnergyState(f.getDayTotal(now, 2))); - updateState(CHANNEL_DAY2_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(now, 2))); - updateState(CHANNEL_DAY2_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(now, 2))); - updateState(CHANNEL_DAY3, Utils.getEnergyState(f.getDayTotal(now, 3))); - updateState(CHANNEL_DAY3_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(now, 3))); - updateState(CHANNEL_DAY3_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(now, 3))); - updateState(CHANNEL_DAY4, Utils.getEnergyState(f.getDayTotal(now, 4))); - updateState(CHANNEL_DAY4_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(now, 4))); - updateState(CHANNEL_DAY4_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(now, 4))); - updateState(CHANNEL_DAY5, Utils.getEnergyState(f.getDayTotal(now, 5))); - updateState(CHANNEL_DAY5_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(now, 5))); - updateState(CHANNEL_DAY5_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(now, 5))); - updateState(CHANNEL_DAY6, Utils.getEnergyState(f.getDayTotal(now, 6))); - updateState(CHANNEL_DAY6_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(now, 6))); - updateState(CHANNEL_DAY6_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(now, 6))); + LocalDate nowDate = now.toLocalDate(); + updateState(CHANNEL_TODAY, Utils.getEnergyState(f.getDayTotal(nowDate))); + updateState(CHANNEL_DAY1, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(1)))); + updateState(CHANNEL_DAY1_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(nowDate.plusDays(1)))); + updateState(CHANNEL_DAY1_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(nowDate.plusDays(1)))); + updateState(CHANNEL_DAY2, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(2)))); + updateState(CHANNEL_DAY2_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(nowDate.plusDays(2)))); + updateState(CHANNEL_DAY2_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(nowDate.plusDays(2)))); + updateState(CHANNEL_DAY3, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(3)))); + updateState(CHANNEL_DAY3_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(nowDate.plusDays(3)))); + updateState(CHANNEL_DAY3_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(nowDate.plusDays(3)))); + updateState(CHANNEL_DAY4, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(4)))); + updateState(CHANNEL_DAY4_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(nowDate.plusDays(4)))); + updateState(CHANNEL_DAY4_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(nowDate.plusDays(4)))); + updateState(CHANNEL_DAY5, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(5)))); + updateState(CHANNEL_DAY5_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(nowDate.plusDays(5)))); + updateState(CHANNEL_DAY5_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(nowDate.plusDays(5)))); + updateState(CHANNEL_DAY6, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(6)))); + updateState(CHANNEL_DAY6_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(nowDate.plusDays(6)))); + updateState(CHANNEL_DAY6_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(nowDate.plusDays(6)))); } private synchronized void setForecast(SolcastObject f) { 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 index 10039997ba21a..0df5ccc1a3e1b 100644 --- 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 @@ -34,8 +34,10 @@ */ @NonNullByDefault class ForecastSolarTest { - public static final String DATE_INPUT_PATTERN_STRING = "yyyy-MM-dd'T'HH:mm:ss"; - public static final DateTimeFormatter DATE_INPUT_PATTERN = DateTimeFormatter.ofPattern(DATE_INPUT_PATTERN_STRING); + // double comparison tolerance = 1 Watt + private static final double TOLERANCE = 0.001; + private static final String DATE_INPUT_PATTERN_STRING = "yyyy-MM-dd'T'HH:mm:ss"; + private static final DateTimeFormatter DATE_INPUT_PATTERN = DateTimeFormatter.ofPattern(DATE_INPUT_PATTERN_STRING); @Test void testForecastObject() { @@ -44,21 +46,21 @@ void testForecastObject() { ForecastSolarObject fo = new ForecastSolarObject(content, queryDateTime); // "2022-07-17 21:32:00": 63583, - assertEquals(63.583, fo.getDayTotal(queryDateTime.toLocalDate()), 0.001, "Total production"); + assertEquals(63.583, fo.getDayTotal(queryDateTime.toLocalDate()), TOLERANCE, "Total production"); // "2022-07-17 17:00:00": 52896, - assertEquals(52.896, fo.getActualValue(queryDateTime), 0.001, "Current Production"); + assertEquals(52.896, fo.getActualValue(queryDateTime), TOLERANCE, "Current Production"); // 63583 - 52896 = 10687 - assertEquals(10.687, fo.getRemainingProduction(queryDateTime), 0.001, "Current Production"); + assertEquals(10.687, fo.getRemainingProduction(queryDateTime), TOLERANCE, "Current Production"); // sum cross check assertEquals(fo.getDayTotal(queryDateTime.toLocalDate()), - fo.getActualValue(queryDateTime) + fo.getRemainingProduction(queryDateTime), 0.001, + fo.getActualValue(queryDateTime) + fo.getRemainingProduction(queryDateTime), TOLERANCE, "actual + remain = total"); queryDateTime = LocalDateTime.of(2022, 7, 18, 19, 00); // "2022-07-18 19:00:00": 63067, - assertEquals(63.067, fo.getActualValue(queryDateTime), 0.001, "Actual production"); + assertEquals(63.067, fo.getActualValue(queryDateTime), TOLERANCE, "Actual production"); // "2022-07-18 21:31:00": 65554 - assertEquals(65.554, fo.getDayTotal(queryDateTime.toLocalDate()), 0.001, "Total production"); + assertEquals(65.554, fo.getDayTotal(queryDateTime.toLocalDate()), TOLERANCE, "Total production"); } @Test @@ -67,11 +69,11 @@ void testActualPower() { LocalDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 10, 00); ForecastSolarObject fo = new ForecastSolarObject(content, queryDateTime); // "2022-07-17 10:00:00": 4874, - assertEquals(4.874, fo.getActualPowerValue(queryDateTime), 0.001, "Actual estimation"); + assertEquals(4.874, fo.getActualPowerValue(queryDateTime), TOLERANCE, "Actual estimation"); queryDateTime = LocalDateTime.of(2022, 7, 18, 14, 00); // "2022-07-18 14:00:00": 7054, - assertEquals(7.054, fo.getActualPowerValue(queryDateTime), 0.001, "Actual estimation"); + assertEquals(7.054, fo.getActualPowerValue(queryDateTime), TOLERANCE, "Actual estimation"); } @Test @@ -104,9 +106,9 @@ void testForecastSum() { State st = Utils.getEnergyState(fo.getActualValue(queryDateTime)); assertTrue(st instanceof QuantityType); actual = actual.add((QuantityType) st); - assertEquals(49.431, actual.floatValue(), 0.001, "Current Production"); + assertEquals(49.431, actual.floatValue(), TOLERANCE, "Current Production"); actual = actual.add((QuantityType) st); - assertEquals(98.862, actual.floatValue(), 0.001, "Doubled Current Production"); + assertEquals(98.862, actual.floatValue(), TOLERANCE, "Doubled Current Production"); } @Test @@ -115,52 +117,52 @@ void testCornerCases() { ForecastSolarObject fo = new ForecastSolarObject(); assertFalse(fo.isValid()); LocalDateTime query = LocalDateTime.of(2022, 7, 17, 16, 23); - assertEquals(-1.0, fo.getActualValue(query), 0.001, "Actual Production"); - assertEquals(-1.0, fo.getDayTotal(query.toLocalDate()), 0.001, "Today Production"); - assertEquals(-1.0, fo.getRemainingProduction(query), 0.001, "Remaining Production"); - assertEquals(-1.0, fo.getDayTotal(query.plusDays(1).toLocalDate()), 0.001, "Tomorrow Production"); + assertEquals(-1.0, fo.getActualValue(query), TOLERANCE, "Actual Production"); + assertEquals(-1.0, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Today Production"); + assertEquals(-1.0, fo.getRemainingProduction(query), TOLERANCE, "Remaining Production"); + assertEquals(-1.0, fo.getDayTotal(query.plusDays(1).toLocalDate()), TOLERANCE, "Tomorrow Production"); // 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); fo = new ForecastSolarObject(content, query); - assertEquals(-1.0, fo.getDayTotal(query.toLocalDate()), 0.001, "Actual out of scope"); - assertEquals(-1.0, fo.getActualValue(query), 0.001, "Actual out of scope"); - assertEquals(-1.0, fo.getRemainingProduction(query), 0.001, "Remain out of scope"); - assertEquals(-1.0, fo.getActualPowerValue(query), 0.001, "Remain out of scope"); + assertEquals(-1.0, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope"); + assertEquals(-1.0, fo.getActualValue(query), TOLERANCE, "Actual out of scope"); + assertEquals(-1.0, fo.getRemainingProduction(query), TOLERANCE, "Remain out of scope"); + assertEquals(-1.0, fo.getActualPowerValue(query), TOLERANCE, "Remain out of scope"); // one minute later we reach a valid date query = query.plusMinutes(1); - assertEquals(63.583, fo.getDayTotal(query.toLocalDate()), 0.001, "Actual out of scope"); - assertEquals(0.0, fo.getActualValue(query), 0.001, "Actual out of scope"); - assertEquals(63.583, fo.getRemainingProduction(query), 0.001, "Remain out of scope"); - assertEquals(0.0, fo.getActualPowerValue(query), 0.001, "Remain out of scope"); + assertEquals(63.583, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope"); + assertEquals(0.0, fo.getActualValue(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); - assertEquals(-1.0, fo.getDayTotal(query.toLocalDate()), 0.001, "Actual out of scope"); - assertEquals(-1.0, fo.getActualValue(query), 0.001, "Actual out of scope"); - assertEquals(-1.0, fo.getRemainingProduction(query), 0.001, "Remain out of scope"); - assertEquals(-1.0, fo.getActualPowerValue(query), 0.001, "Remain out of scope"); + assertEquals(-1.0, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope"); + assertEquals(-1.0, fo.getActualValue(query), TOLERANCE, "Actual out of scope"); + assertEquals(-1.0, fo.getRemainingProduction(query), TOLERANCE, "Remain out of scope"); + assertEquals(-1.0, fo.getActualPowerValue(query), TOLERANCE, "Remain out of scope"); // one minute earlier we reach a valid date query = query.minusMinutes(1); - assertEquals(65.554, fo.getDayTotal(query.toLocalDate()), 0.001, "Actual out of scope"); - assertEquals(65.554, fo.getActualValue(query), 0.001, "Actual out of scope"); - assertEquals(0.0, fo.getRemainingProduction(query), 0.001, "Remain out of scope"); - assertEquals(0.0, fo.getActualPowerValue(query), 0.001, "Remain out of scope"); + assertEquals(65.554, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope"); + assertEquals(65.554, fo.getActualValue(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); - assertEquals(63.583, fo.getDayTotal(query.toLocalDate()), 0.001, "Actual out of scope"); - assertEquals(63.583, fo.getActualValue(query), 0.001, "Actual out of scope"); - assertEquals(0.0, fo.getRemainingProduction(query), 0.001, "Remain out of scope"); - assertEquals(0.0, fo.getActualPowerValue(query), 0.001, "Remain out of scope"); + assertEquals(63.583, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope"); + assertEquals(63.583, fo.getActualValue(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()), 0.001, "Actual out of scope"); - assertEquals(0.0, fo.getActualValue(query), 0.001, "Actual out of scope"); - assertEquals(65.554, fo.getRemainingProduction(query), 0.001, "Remain out of scope"); - assertEquals(0.0, fo.getActualPowerValue(query), 0.001, "Remain out of scope"); + assertEquals(65.554, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope"); + assertEquals(0.0, fo.getActualValue(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 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 index 559caab7b21d9..8f5326117b81e 100644 --- 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 @@ -25,6 +25,7 @@ import org.openhab.binding.solarforecast.internal.Utils; import org.openhab.binding.solarforecast.internal.solcast.SolcastConstants; import org.openhab.binding.solarforecast.internal.solcast.SolcastObject; +import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.Units; /** @@ -34,43 +35,147 @@ */ @NonNullByDefault class SolcastTest { + private static final ZoneId TEST_ZONE = ZoneId.of("Europe/Berlin"); + // double comparison tolerance = 1 Watt + private static final double TOLERANCE = 0.001; + /** + * "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, 23, 16, 23).atZone(ZoneId.systemDefault()); + ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 0, 0).atZone(TEST_ZONE); SolcastObject scfo = new SolcastObject(content, now); - assertEquals(16.809, scfo.getActualValue(now), 0.001, "Actual estimation"); - System.out.println(scfo.getDayTotal(now, -1)); + 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()); + double actual = scfo.getActualValue(now); + double remain = scfo.getRemainingProduction(now); + 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), TOLERANCE, "Begin of day power"); + for (int i = 0; i < 47; i++) { + now = now.plusMinutes(30); + double power = scfo.getActualPowerValue(now) / 2.0; + actual += power; + assertEquals(actual, scfo.getActualValue(now), TOLERANCE, "Actual at " + now); + remain -= power; + assertEquals(remain, scfo.getRemainingProduction(now), TOLERANCE, "Remain at " + now); + assertEquals(dayTotal, actual + remain, TOLERANCE, "Total sum at " + now); + } } - /** - * { - * "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" - */ @Test - void testActualPower() { + void testPower() { String content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); - ZonedDateTime now = LocalDateTime.of(2022, 7, 23, 16, 23).atZone(ZoneId.systemDefault()); + ZonedDateTime now = LocalDateTime.of(2022, 7, 23, 16, 00).atZone(TEST_ZONE); SolcastObject scfo = new SolcastObject(content, now); - assertEquals(1.855, scfo.getActualPowerValue(now), 0.001, "Actual estimation"); + content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); + scfo.join(content); - ZonedDateTime zdt = LocalDateTime.of(2022, 7, 23, 0, 5).atZone(ZoneId.systemDefault()); - for (int i = 0; i < 96; i++) { - zdt = zdt.plusMinutes(15); - System.out.println(zdt + " " + scfo.getActualPowerValue(zdt)); - } + /** + * { + * "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), TOLERANCE, "Estimate power " + now); + assertEquals(1.754, scfo.getActualPowerValue(now.plusMinutes(30)), TOLERANCE, + "Estimate power " + now.plusMinutes(30)); + + assertEquals(2.046, scfo.getOptimisticPowerValue(now), TOLERANCE, "Optimistic power " + now); + assertEquals(1.864, scfo.getOptimisticPowerValue(now.plusMinutes(30)), TOLERANCE, + "Optimistic power " + now.plusMinutes(30)); + + assertEquals(0.864, scfo.getPessimisticPowerValue(now), TOLERANCE, "Pessimistic power " + now); + assertEquals(0.771, scfo.getPessimisticPowerValue(now.plusMinutes(30)), 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), TOLERANCE, "Estimate power " + past); + assertEquals(1.724, scfo.getActualPowerValue(past.plusMinutes(30)), TOLERANCE, + "Estimate power " + now.plusMinutes(30)); + + assertEquals(1.932, scfo.getOptimisticPowerValue(past), TOLERANCE, "Optimistic power " + past); + assertEquals(1.724, scfo.getOptimisticPowerValue(past.plusMinutes(30)), TOLERANCE, + "Optimistic power " + past.plusMinutes(30)); + + assertEquals(1.932, scfo.getPessimisticPowerValue(past), TOLERANCE, "Pessimistic power " + past); + assertEquals(1.724, scfo.getPessimisticPowerValue(past.plusMinutes(30)), TOLERANCE, + "Pessimistic power " + past.plusMinutes(30)); } /** @@ -80,7 +185,7 @@ void testActualPower() { * 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: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, @@ -98,7 +203,7 @@ void testActualPower() { * 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: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, @@ -113,27 +218,27 @@ void testActualPower() { * 2022-07-17T22:00+02:00[Europe/Berlin]=0.0, * 2022-07-17T22:30+02:00[Europe/Berlin]=0.0 * - * <= 41,0262 + * <<< = 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, 16, 23).atZone(ZoneId.systemDefault()); + ZonedDateTime now = LocalDateTime.of(2022, 7, 17, 7, 0).atZone(TEST_ZONE); SolcastObject scfo = new SolcastObject(content, now); - assertEquals(25.413, scfo.getDayTotal(now, 0), 0.001, "Day total"); - assertEquals(21.254, scfo.getActualValue(now), 0.001, "Actual estimation"); + assertEquals(0.614, scfo.getActualValue(now), TOLERANCE, "Actual estimation"); + assertEquals(25.413, scfo.getDayTotal(now.toLocalDate()), 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(ZoneId.systemDefault()); + ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE); SolcastObject scfo = new SolcastObject(content, now); assertEquals(-1.0, scfo.getActualValue(now), 0.01, "Invalid"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); assertEquals(19.408, scfo.getActualValue(now), 0.01, "Actual data"); - assertEquals(23.107, scfo.getDayTotal(now, 0), 0.01, "Today data"); + assertEquals(23.107, scfo.getDayTotal(now.toLocalDate()), 0.01, "Today data"); JSONObject rawJson = new JSONObject(scfo.getRaw()); assertTrue(rawJson.has("forecasts")); assertTrue(rawJson.has("estimated_actuals")); @@ -142,48 +247,76 @@ void testJoin() { @Test void testActions() { String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); - ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(ZoneId.systemDefault()); + ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE); SolcastObject scfo = new SolcastObject(content, now); assertEquals(-1.0, scfo.getActualValue(now), 0.01, "Invalid"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); - System.out.println(scfo.getForecastBegin()); - System.out.println(scfo.getForecastEnd()); - System.out.println(scfo.getDay(now.toLocalDate())); - System.out.println(scfo.getPower(now.toLocalDateTime())); - System.out.println(scfo.getEnergy(now.toLocalDateTime(), now.plusDays(2).toLocalDateTime())); - System.out.println(scfo.getEnergy(now.toLocalDateTime(), now.plusMinutes(20).toLocalDateTime())); - System.out.println(scfo.getEnergy(now.toLocalDateTime(), now.plusDays(20).toLocalDateTime())); + + assertEquals("2022-07-10T23:30", scfo.getForecastBegin().toString(), "Forecast begin"); + assertEquals("2022-07-24T23:00", scfo.getForecastEnd().toString(), "Forecast end"); + // test daily forecasts + cumulated getEnergy + double totalEnergy = 0; + LocalDateTime ldtStartTime = LocalDateTime.of(2022, 7, 18, 0, 0); + ZonedDateTime zdtStartTime = LocalDateTime.of(2022, 7, 18, 0, 0).atZone(TEST_ZONE); + for (int i = 0; i < 7; i++) { + double day = scfo.getDayTotal(zdtStartTime.toLocalDate().plusDays(i)); + QuantityType qt = (QuantityType) scfo.getDay(ldtStartTime.toLocalDate().plusDays(i)); + QuantityType eqt = (QuantityType) scfo.getEnergy(ldtStartTime.plusDays(i), ldtStartTime.plusDays(i + 1)); + // System.out.println("Day: " + day + " ADay: " + qt.doubleValue() + " EDay: " + eqt.doubleValue()); + totalEnergy += qt.doubleValue(); + + qt = (QuantityType) scfo.getEnergy(ldtStartTime, ldtStartTime.plusDays(i + 1)); + // System.out.println("Total: " + qt.doubleValue()); + assertEquals(totalEnergy, qt.doubleValue(), i * TOLERANCE, "Total " + i + " days forecast"); + // System.out.println(i + " done"); + } } @Test void testOptimisticPessimistic() { String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); - ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(ZoneId.systemDefault()); + ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE); SolcastObject scfo = new SolcastObject(content, now); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); - assertEquals(21.96, scfo.getDayTotal(now, 2), 0.001, "Estimation"); - assertEquals(7.358, scfo.getPessimisticDayTotal(now, 2), 0.001, "Estimation"); - assertEquals(22.283, scfo.getOptimisticDayTotal(now, 2), 0.001, "Estimation"); + assertEquals(19.389, scfo.getDayTotal(now.toLocalDate().plusDays(2)), TOLERANCE, "Estimation"); + assertEquals(7.358, scfo.getPessimisticDayTotal(now.toLocalDate().plusDays(2)), TOLERANCE, "Estimation"); + assertEquals(22.283, scfo.getOptimisticDayTotal(now.toLocalDate().plusDays(2)), TOLERANCE, "Estimation"); } @Test void testInavlid() { String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); - ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault()); + ZonedDateTime now = ZonedDateTime.now(TEST_ZONE); SolcastObject scfo = new SolcastObject(content, now); assertEquals(-1.0, scfo.getActualValue(now), 0.01, "Data available - day not in"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); assertEquals(-1.0, scfo.getActualValue(now), 0.01, "Data available after merge - day not in"); - assertEquals(-1.0, scfo.getDayTotal(now, 0), 0.01, "Data available after merge - day not in"); + assertEquals(-1.0, scfo.getDayTotal(now.toLocalDate()), 0.01, "Data available after merge - day not in"); + } + + @Test + void testTimeframes() { + ZonedDateTime zdt = ZonedDateTime.of(2022, 7, 22, 17, 3, 10, 345, TEST_ZONE); + assertEquals("17:15", Utils.getNextTimeframe(zdt).toLocalTime().toString(), "Q1"); + zdt = zdt.plusMinutes(20); + assertEquals("17:30", Utils.getNextTimeframe(zdt).toLocalTime().toString(), "Q2"); + zdt = zdt.plusMinutes(3); + assertEquals("17:30", Utils.getNextTimeframe(zdt).toLocalTime().toString(), "Q2"); + zdt = zdt.plusMinutes(5); + assertEquals("17:45", Utils.getNextTimeframe(zdt).toLocalTime().toString(), "Q3"); + zdt = zdt.plusMinutes(25); + assertEquals("18:00", Utils.getNextTimeframe(zdt).toLocalTime().toString(), "Q4"); + zdt = zdt.plusMinutes(6); + assertEquals("18:15", Utils.getNextTimeframe(zdt).toLocalTime().toString(), "Q4"); } @Test void testRawChannel() { String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); - ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(ZoneId.systemDefault()); + ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE); SolcastObject sco = new SolcastObject(content, now); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); sco.join(content); @@ -198,22 +331,6 @@ void testUnitDetection() { assertEquals("W", Units.WATT.toString(), "Watt"); } - @Test - void testTimeframes() { - ZonedDateTime zdt = ZonedDateTime.of(2022, 7, 22, 17, 3, 10, 345, ZoneId.systemDefault()); - assertEquals("17:15", Utils.getNextTimeframe(zdt).toLocalTime().toString(), "Q1"); - zdt = zdt.plusMinutes(20); - assertEquals("17:30", Utils.getNextTimeframe(zdt).toLocalTime().toString(), "Q2"); - zdt = zdt.plusMinutes(3); - assertEquals("17:30", Utils.getNextTimeframe(zdt).toLocalTime().toString(), "Q2"); - zdt = zdt.plusMinutes(5); - assertEquals("17:45", Utils.getNextTimeframe(zdt).toLocalTime().toString(), "Q3"); - zdt = zdt.plusMinutes(25); - assertEquals("18:00", Utils.getNextTimeframe(zdt).toLocalTime().toString(), "Q4"); - zdt = zdt.plusMinutes(6); - assertEquals("18:15", Utils.getNextTimeframe(zdt).toLocalTime().toString(), "Q4"); - } - @Test void testTimes() { String utcTimeString = "2022-07-17T19:30:00.0000000Z"; From 270c446cbe7a8bd207553c837eff717e3ba360a8 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 9 Aug 2022 16:12:03 +0200 Subject: [PATCH 021/111] solcast actions providing optimistic and pessimistic values Signed-off-by: Bernd Weymann --- .../internal/{ => actions}/SolarForecast.java | 31 ++- .../{ => actions}/SolarForecastActions.java | 5 +- .../{ => actions}/SolarForecastProvider.java | 2 +- .../ForecastSolarBridgeHandler.java | 6 +- .../forecastsolar/ForecastSolarObject.java | 25 +- .../ForecastSolarPlaneHandler.java | 6 +- .../solcast/SolcastBridgeHandler.java | 51 +++-- .../internal/solcast/SolcastObject.java | 215 +++++++++++------- .../internal/solcast/SolcastPlaneHandler.java | 51 +++-- .../solarforecast/ForecastSolarTest.java | 8 +- .../binding/solarforecast/SolcastTest.java | 89 +++++--- 11 files changed, 295 insertions(+), 194 deletions(-) rename bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/{ => actions}/SolarForecast.java (59%) rename bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/{ => actions}/SolarForecastActions.java (98%) rename bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/{ => actions}/SolarForecastProvider.java (93%) diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecast.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecast.java similarity index 59% rename from bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecast.java rename to bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecast.java index c0eebe7d58e97..e9ad0cc256678 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecast.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecast.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.solarforecast.internal; +package org.openhab.binding.solarforecast.internal.actions; import java.time.LocalDate; import java.time.LocalDateTime; @@ -25,31 +25,42 @@ */ @NonNullByDefault public interface SolarForecast { + /** + * 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 dateString - * @return QuaatityType in kW/h + * @param localDate + * @param args possible arguments from this interface + * @return QuantityType in kW/h */ - public State getDay(LocalDate localDate); + public State getDay(LocalDate localDate, String... args); /** * Returns electric energy between two timestamps * - * @param dateTimeFrom - * @param dateTimeTo + * @param localDateTimeBegin + * @param localDateTimeEnd + * @param args possible arguments from this interface * @return QuantityType in kW/h */ - public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDateTimeEnd); + public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDateTimeEnd, String... args); /** - * Returns electric power at one specific timepoint + * Returns electric power at one specific point of time * - * @param dateTimeString + * @param localDateTime + * @param args possible arguments from this interface * @return QuantityType in kW */ - public State getPower(LocalDateTime localDateTime); + public State getPower(LocalDateTime localDateTime, String... args); /** * Get the first date and time of forecast data diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastActions.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecastActions.java similarity index 98% rename from bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastActions.java rename to bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecastActions.java index 01fdfafd7c71c..6a454c7a9f1e8 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastActions.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecastActions.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.solarforecast.internal; +package org.openhab.binding.solarforecast.internal.actions; import java.time.LocalDate; import java.time.LocalDateTime; @@ -50,7 +50,8 @@ public class SolarForecastActions implements ThingActions { @RuleAction(label = "@text/actionDayLabel", description = "@text/actionDayDesc") public State getDay( - @ActionInput(name = "localDate", label = "@text/actionInputDayLabel", description = "@text/actionInputDayDesc") LocalDate localDate) { + @ActionInput(name = "localDate", label = "@text/actionInputDayLabel", description = "@text/actionInputDayDesc") LocalDate localDate, + String... args) { if (thingHandler.isPresent()) { List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); logger.trace("Found {} SolarForecast entries", l.size()); diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastProvider.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecastProvider.java similarity index 93% rename from bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastProvider.java rename to bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecastProvider.java index 5ce42439801f6..9f18c3d11b7b1 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastProvider.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecastProvider.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.solarforecast.internal; +package org.openhab.binding.solarforecast.internal.actions; import java.util.List; diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java index 78cb100fd7203..6bd98906e18bc 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java @@ -25,11 +25,11 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.solarforecast.internal.SolarForecast; -import org.openhab.binding.solarforecast.internal.SolarForecastActions; import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; -import org.openhab.binding.solarforecast.internal.SolarForecastProvider; import org.openhab.binding.solarforecast.internal.Utils; +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.core.config.core.Configuration; import org.openhab.core.library.types.PointType; import org.openhab.core.thing.Bridge; 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 index 7f211490e6167..f5a2f563ddc28 100644 --- 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 @@ -22,10 +22,11 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.json.JSONObject; -import org.openhab.binding.solarforecast.internal.SolarForecast; import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; import org.openhab.binding.solarforecast.internal.Utils; +import org.openhab.binding.solarforecast.internal.actions.SolarForecast; import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -200,16 +201,26 @@ public String toString() { return "Expiration: " + expirationDateTime + ", Valid: " + valid + ", Data:" + wattHourMap; } - // SolarForecast Interface + /** + * SolarForecast Interface + */ @Override - public State getDay(LocalDate localDate) { + public State getDay(LocalDate localDate, String... args) { + if (args.length > 0) { + logger.debug("ForecastSolar doesn't accept arguments"); + return UnDefType.UNDEF; + } double measure = getDayTotal(localDate); logger.trace("Actions: deliver measure {}", measure); return Utils.getEnergyState(measure); } @Override - public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDateTimeEnd) { + public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDateTimeEnd, String... args) { + if (args.length > 0) { + logger.debug("ForecastSolar doesn't accept arguments"); + return UnDefType.UNDEF; + } LocalDate beginDate = localDateTimeBegin.toLocalDate(); LocalDate endDate = localDateTimeEnd.toLocalDate(); double measure = UNDEF; @@ -235,7 +246,11 @@ public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDate } @Override - public State getPower(LocalDateTime localDateTime) { + public State getPower(LocalDateTime localDateTime, String... args) { + if (args.length > 0) { + logger.debug("ForecastSolar doesn't accept arguments"); + return UnDefType.UNDEF; + } double measure = getActualPowerValue(localDateTime); logger.trace("Actions: deliver measure {}", measure); return Utils.getPowerState(measure); diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java index 5bb4de69d083b..83d862a55d7d0 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java @@ -26,10 +26,10 @@ 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.SolarForecast; -import org.openhab.binding.solarforecast.internal.SolarForecastActions; -import org.openhab.binding.solarforecast.internal.SolarForecastProvider; import org.openhab.binding.solarforecast.internal.Utils; +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.core.library.types.PointType; import org.openhab.core.library.types.StringType; import org.openhab.core.thing.Bridge; diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java index c5929c9d27b95..bd0d4fbd571a1 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java @@ -26,10 +26,11 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.solarforecast.internal.SolarForecast; -import org.openhab.binding.solarforecast.internal.SolarForecastActions; -import org.openhab.binding.solarforecast.internal.SolarForecastProvider; import org.openhab.binding.solarforecast.internal.Utils; +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.QueryMode; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ThingStatus; @@ -144,29 +145,29 @@ private synchronized void getData() { for (Iterator iterator = parts.iterator(); iterator.hasNext();) { SolcastPlaneHandler sfph = iterator.next(); SolcastObject fo = sfph.fetchData(); - actualSum += fo.getActualValue(now); - actualPowerSum += fo.getActualPowerValue(now); - remainSum += fo.getRemainingProduction(now); + actualSum += fo.getActualValue(now, QueryMode.Estimation); + actualPowerSum += fo.getActualPowerValue(now, QueryMode.Estimation); + remainSum += fo.getRemainingProduction(now, QueryMode.Estimation); LocalDate nowDate = now.toLocalDate(); - todaySum += fo.getDayTotal(nowDate); - day1Sum += fo.getDayTotal(nowDate.plusDays(1)); - day1SumLow += fo.getPessimisticDayTotal(nowDate.plusDays(1)); - day1SumHigh += fo.getOptimisticDayTotal(nowDate.plusDays(1)); - day2Sum += fo.getDayTotal(nowDate.plusDays(2)); - day2SumLow += fo.getPessimisticDayTotal(nowDate.plusDays(2)); - day2SumHigh += fo.getOptimisticDayTotal(nowDate.plusDays(2)); - day3Sum += fo.getDayTotal(nowDate.plusDays(3)); - day3SumLow += fo.getPessimisticDayTotal(nowDate.plusDays(3)); - day3SumHigh += fo.getOptimisticDayTotal(nowDate.plusDays(3)); - day4Sum += fo.getDayTotal(nowDate.plusDays(4)); - day4SumLow += fo.getPessimisticDayTotal(nowDate.plusDays(4)); - day4SumHigh += fo.getOptimisticDayTotal(nowDate.plusDays(4)); - day5Sum += fo.getDayTotal(nowDate.plusDays(5)); - day5SumLow += fo.getPessimisticDayTotal(nowDate.plusDays(5)); - day5SumHigh += fo.getOptimisticDayTotal(nowDate.plusDays(5)); - day6Sum += fo.getDayTotal(nowDate.plusDays(6)); - day6SumLow += fo.getPessimisticDayTotal(nowDate.plusDays(6)); - day6SumHigh += fo.getOptimisticDayTotal(nowDate.plusDays(6)); + todaySum += fo.getDayTotal(nowDate, QueryMode.Estimation); + day1Sum += fo.getDayTotal(nowDate.plusDays(1), QueryMode.Estimation); + day1SumLow += fo.getDayTotal(nowDate.plusDays(1), QueryMode.Pessimistic); + day1SumHigh += fo.getDayTotal(nowDate.plusDays(1), QueryMode.Optimistic); + day2Sum += fo.getDayTotal(nowDate.plusDays(2), QueryMode.Estimation); + day2SumLow += fo.getDayTotal(nowDate.plusDays(2), QueryMode.Pessimistic); + day2SumHigh += fo.getDayTotal(nowDate.plusDays(2), QueryMode.Optimistic); + day3Sum += fo.getDayTotal(nowDate.plusDays(3), QueryMode.Estimation); + day3SumLow += fo.getDayTotal(nowDate.plusDays(3), QueryMode.Pessimistic); + day3SumHigh += fo.getDayTotal(nowDate.plusDays(3), QueryMode.Optimistic); + day4Sum += fo.getDayTotal(nowDate.plusDays(4), QueryMode.Estimation); + day4SumLow += fo.getDayTotal(nowDate.plusDays(4), QueryMode.Pessimistic); + day4SumHigh += fo.getDayTotal(nowDate.plusDays(4), QueryMode.Optimistic); + day5Sum += fo.getDayTotal(nowDate.plusDays(5), QueryMode.Estimation); + day5SumLow += fo.getDayTotal(nowDate.plusDays(5), QueryMode.Pessimistic); + day5SumHigh += fo.getDayTotal(nowDate.plusDays(5), QueryMode.Optimistic); + day6Sum += fo.getDayTotal(nowDate.plusDays(6), QueryMode.Estimation); + day6SumLow += fo.getDayTotal(nowDate.plusDays(6), QueryMode.Pessimistic); + day6SumHigh += fo.getDayTotal(nowDate.plusDays(6), QueryMode.Optimistic); } updateState(CHANNEL_ACTUAL, Utils.getEnergyState(actualSum)); updateState(CHANNEL_ACTUAL_POWER, Utils.getPowerState(actualPowerSum)); 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 index d614c1d6b7605..12b4c3163829d 100644 --- 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 @@ -16,6 +16,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZonedDateTime; +import java.util.Arrays; import java.util.Map.Entry; import java.util.Optional; import java.util.Set; @@ -24,10 +25,11 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.json.JSONArray; import org.json.JSONObject; -import org.openhab.binding.solarforecast.internal.SolarForecast; import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; import org.openhab.binding.solarforecast.internal.Utils; +import org.openhab.binding.solarforecast.internal.actions.SolarForecast; import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,15 +40,36 @@ */ @NonNullByDefault public class SolcastObject implements SolarForecast { - private final Logger logger = LoggerFactory.getLogger(SolcastObject.class); private static final double UNDEF = -1; + 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 Optional rawData = Optional.of(new JSONObject()); private ZonedDateTime expirationDateTime; private boolean valid = false; + public enum QueryMode { + Estimation("Estimation"), + Optimistic(SolarForecast.OPTIMISTIC), + Pessimistic(SolarForecast.PESSIMISTIC), + Error("Error"); + + String modeDescirption; + + QueryMode(String description) { + modeDescirption = description; + } + + @Override + public String toString() { + return modeDescirption; + } + } + public SolcastObject() { // invalid forecast object expirationDateTime = ZonedDateTime.now(SolcastConstants.zonedId); @@ -132,13 +155,10 @@ public boolean isValid() { return false; } - public double getActualValue(ZonedDateTime query) { - if (estimationDataMap.isEmpty()) { - return UNDEF; - } + public double getActualValue(ZonedDateTime query, QueryMode mode) { LocalDate ld = query.toLocalDate(); - TreeMap dtm = estimationDataMap.get(ld); - if (dtm == null) { + TreeMap dtm = getDataMap(ld, mode); + if (dtm.isEmpty()) { return UNDEF; } double forecastValue = 0; @@ -178,43 +198,12 @@ public double getActualValue(ZonedDateTime query) { * Get power values */ - public double getActualPowerValue(ZonedDateTime query) { - if (estimationDataMap.isEmpty()) { - return UNDEF; - } + public double getActualPowerValue(ZonedDateTime query, QueryMode mode) { LocalDate ld = query.toLocalDate(); - TreeMap dtm = estimationDataMap.get(ld); - if (dtm == null) { + TreeMap dtm = getDataMap(ld, mode); + if (dtm.isEmpty()) { return UNDEF; } - return getPowerFromTreemap(dtm, query); - } - - public double getOptimisticPowerValue(ZonedDateTime query) { - if (optimisticDataMap.isEmpty()) { - return UNDEF; - } - LocalDate ld = query.toLocalDate(); - TreeMap dtm = optimisticDataMap.get(ld); - if (dtm == null) { - return UNDEF; - } - return getPowerFromTreemap(dtm, query); - } - - public double getPessimisticPowerValue(ZonedDateTime query) { - if (pessimisticDataMap.isEmpty()) { - return UNDEF; - } - LocalDate ld = query.toLocalDate(); - TreeMap dtm = pessimisticDataMap.get(ld); - if (dtm == null) { - return UNDEF; - } - return getPowerFromTreemap(dtm, query); - } - - private double getPowerFromTreemap(TreeMap dtm, ZonedDateTime query) { double actualPowerValue = 0; Entry f = dtm.floorEntry(query); Entry c = dtm.ceilingEntry(query); @@ -242,42 +231,17 @@ private double getPowerFromTreemap(TreeMap dtm, ZonedDate * Daily totals */ - public double getDayTotal(LocalDate query) { - TreeMap dtm = estimationDataMap.get(query); - if (dtm != null) { - // JSONObject jot = new JSONObject(dtm); - // System.out.println(jot); - return getTotalValue(dtm); - } else { - return UNDEF; - } - } - - public double getOptimisticDayTotal(LocalDate query) { - TreeMap dtm = optimisticDataMap.get(query); - if (dtm != null) { - return getTotalValue(dtm); - } else { - return UNDEF; - } - } - - public double getPessimisticDayTotal(LocalDate query) { - TreeMap dtm = pessimisticDataMap.get(query); - if (dtm != null) { - return getTotalValue(dtm); - } else { + public double getDayTotal(LocalDate query, QueryMode mode) { + TreeMap dtm = getDataMap(query, mode); + if (dtm.isEmpty()) { return UNDEF; } - } - - private double getTotalValue(TreeMap map) { double forecastValue = 0; - Set keySet = map.keySet(); + Set keySet = dtm.keySet(); for (ZonedDateTime key : keySet) { // value are reported in PT30M = 30 minutes interval with kw value // for kw/h it's half the value - Double addedValue = map.get(key); + Double addedValue = dtm.get(key); if (addedValue != null) { forecastValue += addedValue.doubleValue() / 2.0; } @@ -285,11 +249,13 @@ private double getTotalValue(TreeMap map) { return forecastValue; } - public double getRemainingProduction(ZonedDateTime query) { - if (estimationDataMap.isEmpty()) { + public double getRemainingProduction(ZonedDateTime query, QueryMode mode) { + LocalDate ld = query.toLocalDate(); + TreeMap dtm = getDataMap(ld, mode); + if (dtm.isEmpty()) { return UNDEF; } - return getDayTotal(query.toLocalDate()) - getActualValue(query); + return getDayTotal(query.toLocalDate(), mode) - getActualValue(query, mode); } @Override @@ -306,35 +272,83 @@ public static ZonedDateTime getZdtFromUTC(String utc) { return timestamp.atZone(SolcastConstants.zonedId); } + private TreeMap getDataMap(LocalDate ld, QueryMode mode) { + TreeMap returnMap = EMPTY_MAP; + switch (mode) { + case Estimation: + returnMap = estimationDataMap.get(ld); + break; + case Optimistic: + returnMap = optimisticDataMap.get(ld); + break; + case Pessimistic: + returnMap = pessimisticDataMap.get(ld); + break; + case Error: + // nothing to do + break; + default: + // nothing to do + break; + } + if (returnMap == null) { + return EMPTY_MAP; + } + return returnMap; + } + /** * SolarForecast Interface */ @Override - public State getDay(LocalDate localDate) { - double measure = getDayTotal(localDate); + public State getDay(LocalDate localDate, String... args) { + QueryMode mode = evalArguments(args); + if (mode.equals(QueryMode.Error)) { + return UnDefType.UNDEF; + } else if (mode.equals(QueryMode.Optimistic) || mode.equals(QueryMode.Pessimistic)) { + if (localDate.isBefore(LocalDate.now())) { + logger.debug("{} forecasts only available for future", mode); + return UnDefType.UNDEF; + } + } + double measure = getDayTotal(localDate, mode); return Utils.getEnergyState(measure); } @Override - public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDateTimeEnd) { + public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDateTimeEnd, String... args) { + if (localDateTimeEnd.isBefore(localDateTimeBegin)) { + logger.debug("End {} defined before Start {}", localDateTimeEnd, localDateTimeBegin); + return UnDefType.UNDEF; + } + QueryMode mode = evalArguments(args); + if (mode.equals(QueryMode.Error)) { + return UnDefType.UNDEF; + } else if (mode.equals(QueryMode.Optimistic) || mode.equals(QueryMode.Pessimistic)) { + if (localDateTimeEnd.isBefore(LocalDateTime.now())) { + logger.debug("{} forecasts only available for future", mode); + return UnDefType.UNDEF; + } + } ZonedDateTime zdtBegin = localDateTimeBegin.atZone(SolcastConstants.zonedId); ZonedDateTime zdtEnd = localDateTimeEnd.atZone(SolcastConstants.zonedId); LocalDate beginDate = zdtBegin.toLocalDate(); LocalDate endDate = zdtEnd.toLocalDate(); double measure = UNDEF; if (beginDate.isEqual(endDate)) { - measure = getDayTotal(zdtEnd.toLocalDate()) - getActualValue(zdtBegin) - getRemainingProduction(zdtEnd); + measure = getDayTotal(zdtEnd.toLocalDate(), mode) - getActualValue(zdtBegin, mode) + - getRemainingProduction(zdtEnd, mode); } else { - measure = getRemainingProduction(zdtBegin); + measure = getRemainingProduction(zdtBegin, mode); beginDate = beginDate.plusDays(1); while (beginDate.isBefore(endDate) && measure >= 0) { - double day = getDayTotal(beginDate); + double day = getDayTotal(beginDate, mode); if (day > 0) { measure += day; } beginDate = beginDate.plusDays(1); } - double lastDay = getActualValue(zdtEnd); + double lastDay = getActualValue(zdtEnd, mode); if (lastDay >= 0) { measure += lastDay; } @@ -343,9 +357,20 @@ public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDate } @Override - public State getPower(LocalDateTime localDateTime) { - ZonedDateTime zdt = localDateTime.atZone(SolcastConstants.zonedId); - double measure = getActualPowerValue(zdt); + public State getPower(LocalDateTime queryDateTime, String... args) { + // eliminate error cases and return immediately + QueryMode mode = evalArguments(args); + if (mode.equals(QueryMode.Error)) { + return UnDefType.UNDEF; + } else if (mode.equals(QueryMode.Optimistic) || mode.equals(QueryMode.Pessimistic)) { + if (queryDateTime.isBefore(LocalDateTime.now().minusMinutes(1))) { + logger.debug("{} forecasts only available for future", mode); + return UnDefType.UNDEF; + } + } + + ZonedDateTime zdt = queryDateTime.atZone(SolcastConstants.zonedId); + double measure = getActualPowerValue(zdt, mode); return Utils.getPowerState(measure); } @@ -366,4 +391,24 @@ public LocalDateTime getForecastEnd() { } return LocalDateTime.MIN; } + + private QueryMode evalArguments(String[] args) { + if (args.length > 0) { + if (args.length > 1) { + logger.debug("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 { + logger.debug("Argument {} not supported", args[0]); + return QueryMode.Error; + } + } else { + return QueryMode.Estimation; + } + } } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java index 57a96210a964a..013a8e0ccad11 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java @@ -34,10 +34,11 @@ import org.eclipse.jetty.client.util.StringContentProvider; import org.eclipse.jetty.http.HttpHeader; import org.json.JSONObject; -import org.openhab.binding.solarforecast.internal.SolarForecast; -import org.openhab.binding.solarforecast.internal.SolarForecastActions; -import org.openhab.binding.solarforecast.internal.SolarForecastProvider; import org.openhab.binding.solarforecast.internal.Utils; +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.QueryMode; import org.openhab.core.items.Item; import org.openhab.core.items.ItemRegistry; import org.openhab.core.library.types.DecimalType; @@ -295,29 +296,29 @@ private void sendMeasure() { private void updateChannels(SolcastObject f) { ZonedDateTime now = ZonedDateTime.now(SolcastConstants.zonedId); - updateState(CHANNEL_ACTUAL, Utils.getEnergyState(f.getActualValue(now))); - updateState(CHANNEL_ACTUAL_POWER, Utils.getEnergyState(f.getActualPowerValue(now))); - updateState(CHANNEL_REMAINING, Utils.getEnergyState(f.getRemainingProduction(now))); + updateState(CHANNEL_ACTUAL, Utils.getEnergyState(f.getActualValue(now, QueryMode.Estimation))); + updateState(CHANNEL_ACTUAL_POWER, Utils.getEnergyState(f.getActualPowerValue(now, QueryMode.Estimation))); + updateState(CHANNEL_REMAINING, Utils.getEnergyState(f.getRemainingProduction(now, QueryMode.Estimation))); LocalDate nowDate = now.toLocalDate(); - updateState(CHANNEL_TODAY, Utils.getEnergyState(f.getDayTotal(nowDate))); - updateState(CHANNEL_DAY1, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(1)))); - updateState(CHANNEL_DAY1_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(nowDate.plusDays(1)))); - updateState(CHANNEL_DAY1_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(nowDate.plusDays(1)))); - updateState(CHANNEL_DAY2, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(2)))); - updateState(CHANNEL_DAY2_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(nowDate.plusDays(2)))); - updateState(CHANNEL_DAY2_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(nowDate.plusDays(2)))); - updateState(CHANNEL_DAY3, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(3)))); - updateState(CHANNEL_DAY3_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(nowDate.plusDays(3)))); - updateState(CHANNEL_DAY3_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(nowDate.plusDays(3)))); - updateState(CHANNEL_DAY4, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(4)))); - updateState(CHANNEL_DAY4_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(nowDate.plusDays(4)))); - updateState(CHANNEL_DAY4_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(nowDate.plusDays(4)))); - updateState(CHANNEL_DAY5, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(5)))); - updateState(CHANNEL_DAY5_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(nowDate.plusDays(5)))); - updateState(CHANNEL_DAY5_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(nowDate.plusDays(5)))); - updateState(CHANNEL_DAY6, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(6)))); - updateState(CHANNEL_DAY6_HIGH, Utils.getEnergyState(f.getOptimisticDayTotal(nowDate.plusDays(6)))); - updateState(CHANNEL_DAY6_LOW, Utils.getEnergyState(f.getPessimisticDayTotal(nowDate.plusDays(6)))); + updateState(CHANNEL_TODAY, Utils.getEnergyState(f.getDayTotal(nowDate, QueryMode.Estimation))); + updateState(CHANNEL_DAY1, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(1), QueryMode.Estimation))); + updateState(CHANNEL_DAY1_HIGH, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(1), QueryMode.Optimistic))); + updateState(CHANNEL_DAY1_LOW, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(1), QueryMode.Pessimistic))); + updateState(CHANNEL_DAY2, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(2), QueryMode.Estimation))); + updateState(CHANNEL_DAY2_HIGH, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(2), QueryMode.Optimistic))); + updateState(CHANNEL_DAY2_LOW, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(2), QueryMode.Pessimistic))); + updateState(CHANNEL_DAY3, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(3), QueryMode.Estimation))); + updateState(CHANNEL_DAY3_HIGH, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(3), QueryMode.Optimistic))); + updateState(CHANNEL_DAY3_LOW, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(3), QueryMode.Pessimistic))); + updateState(CHANNEL_DAY4, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(4), QueryMode.Estimation))); + updateState(CHANNEL_DAY4_HIGH, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(4), QueryMode.Optimistic))); + updateState(CHANNEL_DAY4_LOW, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(4), QueryMode.Pessimistic))); + updateState(CHANNEL_DAY5, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(5), QueryMode.Estimation))); + updateState(CHANNEL_DAY5_HIGH, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(5), QueryMode.Optimistic))); + updateState(CHANNEL_DAY5_LOW, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(5), QueryMode.Pessimistic))); + updateState(CHANNEL_DAY6, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(6), QueryMode.Estimation))); + updateState(CHANNEL_DAY6_HIGH, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(6), QueryMode.Optimistic))); + updateState(CHANNEL_DAY6_LOW, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(6), QueryMode.Pessimistic))); } private synchronized void setForecast(SolcastObject f) { 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 index 0df5ccc1a3e1b..2019dfab50ca8 100644 --- 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 @@ -26,6 +26,7 @@ import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.Units; import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; /** * The {@link ForecastSolarTest} tests responses from forecast solar object @@ -36,15 +37,12 @@ class ForecastSolarTest { // double comparison tolerance = 1 Watt private static final double TOLERANCE = 0.001; - private static final String DATE_INPUT_PATTERN_STRING = "yyyy-MM-dd'T'HH:mm:ss"; - private static final DateTimeFormatter DATE_INPUT_PATTERN = DateTimeFormatter.ofPattern(DATE_INPUT_PATTERN_STRING); @Test void testForecastObject() { String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); LocalDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 17, 00); ForecastSolarObject fo = new ForecastSolarObject(content, queryDateTime); - // "2022-07-17 21:32:00": 63583, assertEquals(63.583, fo.getDayTotal(queryDateTime.toLocalDate()), TOLERANCE, "Total production"); // "2022-07-17 17:00:00": 52896, @@ -184,5 +182,9 @@ void testActions() { // } assertEquals(QuantityType.valueOf(129.137, Units.KILOWATT_HOUR).toString(), fo.getEnergy(query, query.plusDays(2)).toFullString(), "Actual out of scope"); + + assertEquals(UnDefType.UNDEF, fo.getDay(query.toLocalDate(), "optimistic")); + assertEquals(UnDefType.UNDEF, fo.getDay(query.toLocalDate(), "pessimistic")); + assertEquals(UnDefType.UNDEF, fo.getDay(query.toLocalDate(), "total", "rubbish")); } } 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 index 8f5326117b81e..7d712e37050aa 100644 --- 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 @@ -14,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.*; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; @@ -23,10 +24,13 @@ import org.json.JSONObject; import org.junit.jupiter.api.Test; import org.openhab.binding.solarforecast.internal.Utils; +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.core.library.types.QuantityType; import org.openhab.core.library.unit.Units; +import org.openhab.core.types.UnDefType; /** * The {@link SolcastTest} tests responses from forecast solar website @@ -98,20 +102,20 @@ void testForecastObject() { scfo.join(content); // test one day, step ahead in time and cross check channel values - double dayTotal = scfo.getDayTotal(now.toLocalDate()); - double actual = scfo.getActualValue(now); - double remain = scfo.getRemainingProduction(now); + double dayTotal = scfo.getDayTotal(now.toLocalDate(), QueryMode.Estimation); + double actual = scfo.getActualValue(now, QueryMode.Estimation); + double remain = scfo.getRemainingProduction(now, QueryMode.Estimation); 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), TOLERANCE, "Begin of day power"); + assertEquals(0.0, scfo.getActualPowerValue(now, QueryMode.Estimation), TOLERANCE, "Begin of day power"); for (int i = 0; i < 47; i++) { now = now.plusMinutes(30); - double power = scfo.getActualPowerValue(now) / 2.0; + double power = scfo.getActualPowerValue(now, QueryMode.Estimation) / 2.0; actual += power; - assertEquals(actual, scfo.getActualValue(now), TOLERANCE, "Actual at " + now); + assertEquals(actual, scfo.getActualValue(now, QueryMode.Estimation), TOLERANCE, "Actual at " + now); remain -= power; - assertEquals(remain, scfo.getRemainingProduction(now), TOLERANCE, "Remain at " + now); + assertEquals(remain, scfo.getRemainingProduction(now, QueryMode.Estimation), TOLERANCE, "Remain at " + now); assertEquals(dayTotal, actual + remain, TOLERANCE, "Total sum at " + now); } } @@ -139,16 +143,17 @@ void testPower() { * "period_end": "2022-07-23T14:30:00.0000000Z", * "period": "PT30M" */ - assertEquals(1.9176, scfo.getActualPowerValue(now), TOLERANCE, "Estimate power " + now); - assertEquals(1.754, scfo.getActualPowerValue(now.plusMinutes(30)), TOLERANCE, + assertEquals(1.9176, scfo.getActualPowerValue(now, QueryMode.Estimation), TOLERANCE, "Estimate power " + now); + assertEquals(1.754, scfo.getActualPowerValue(now.plusMinutes(30), QueryMode.Estimation), TOLERANCE, "Estimate power " + now.plusMinutes(30)); - assertEquals(2.046, scfo.getOptimisticPowerValue(now), TOLERANCE, "Optimistic power " + now); - assertEquals(1.864, scfo.getOptimisticPowerValue(now.plusMinutes(30)), TOLERANCE, + 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.getPessimisticPowerValue(now), TOLERANCE, "Pessimistic power " + now); - assertEquals(0.771, scfo.getPessimisticPowerValue(now.plusMinutes(30)), TOLERANCE, + 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)); /** @@ -165,16 +170,18 @@ void testPower() { **/ // 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), TOLERANCE, "Estimate power " + past); - assertEquals(1.724, scfo.getActualPowerValue(past.plusMinutes(30)), TOLERANCE, + assertEquals(1.932, scfo.getActualPowerValue(past, QueryMode.Estimation), TOLERANCE, "Estimate power " + past); + assertEquals(1.724, scfo.getActualPowerValue(past.plusMinutes(30), QueryMode.Estimation), TOLERANCE, "Estimate power " + now.plusMinutes(30)); - assertEquals(1.932, scfo.getOptimisticPowerValue(past), TOLERANCE, "Optimistic power " + past); - assertEquals(1.724, scfo.getOptimisticPowerValue(past.plusMinutes(30)), TOLERANCE, + 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.getPessimisticPowerValue(past), TOLERANCE, "Pessimistic power " + past); - assertEquals(1.724, scfo.getPessimisticPowerValue(past.plusMinutes(30)), TOLERANCE, + 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)); } @@ -225,8 +232,8 @@ 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(content, now); - assertEquals(0.614, scfo.getActualValue(now), TOLERANCE, "Actual estimation"); - assertEquals(25.413, scfo.getDayTotal(now.toLocalDate()), TOLERANCE, "Day total"); + assertEquals(0.614, scfo.getActualValue(now, QueryMode.Estimation), TOLERANCE, "Actual estimation"); + assertEquals(25.413, scfo.getDayTotal(now.toLocalDate(), QueryMode.Estimation), TOLERANCE, "Day total"); } @Test @@ -234,11 +241,11 @@ 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(content, now); - assertEquals(-1.0, scfo.getActualValue(now), 0.01, "Invalid"); + assertEquals(-1.0, scfo.getActualValue(now, QueryMode.Estimation), 0.01, "Invalid"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); - assertEquals(19.408, scfo.getActualValue(now), 0.01, "Actual data"); - assertEquals(23.107, scfo.getDayTotal(now.toLocalDate()), 0.01, "Today data"); + assertEquals(19.408, scfo.getActualValue(now, QueryMode.Estimation), 0.01, "Actual data"); + assertEquals(23.107, scfo.getDayTotal(now.toLocalDate(), QueryMode.Estimation), 0.01, "Today data"); JSONObject rawJson = new JSONObject(scfo.getRaw()); assertTrue(rawJson.has("forecasts")); assertTrue(rawJson.has("estimated_actuals")); @@ -249,7 +256,7 @@ 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(content, now); - assertEquals(-1.0, scfo.getActualValue(now), 0.01, "Invalid"); + assertEquals(-1.0, scfo.getActualValue(now, QueryMode.Estimation), 0.01, "Invalid"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); @@ -260,7 +267,7 @@ void testActions() { LocalDateTime ldtStartTime = LocalDateTime.of(2022, 7, 18, 0, 0); ZonedDateTime zdtStartTime = LocalDateTime.of(2022, 7, 18, 0, 0).atZone(TEST_ZONE); for (int i = 0; i < 7; i++) { - double day = scfo.getDayTotal(zdtStartTime.toLocalDate().plusDays(i)); + double day = scfo.getDayTotal(zdtStartTime.toLocalDate().plusDays(i), QueryMode.Estimation); QuantityType qt = (QuantityType) scfo.getDay(ldtStartTime.toLocalDate().plusDays(i)); QuantityType eqt = (QuantityType) scfo.getEnergy(ldtStartTime.plusDays(i), ldtStartTime.plusDays(i + 1)); // System.out.println("Day: " + day + " ADay: " + qt.doubleValue() + " EDay: " + eqt.doubleValue()); @@ -280,9 +287,25 @@ void testOptimisticPessimistic() { SolcastObject scfo = new SolcastObject(content, now); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); - assertEquals(19.389, scfo.getDayTotal(now.toLocalDate().plusDays(2)), TOLERANCE, "Estimation"); - assertEquals(7.358, scfo.getPessimisticDayTotal(now.toLocalDate().plusDays(2)), TOLERANCE, "Estimation"); - assertEquals(22.283, scfo.getOptimisticDayTotal(now.toLocalDate().plusDays(2)), TOLERANCE, "Estimation"); + assertEquals(19.389, scfo.getDayTotal(now.toLocalDate().plusDays(2), QueryMode.Estimation), 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"); + + // access in past shall be rejected + LocalDateTime past = LocalDateTime.now().minusMinutes(5); + assertEquals(UnDefType.UNDEF, scfo.getPower(past, SolarForecast.OPTIMISTIC), "Optimistic Power"); + assertEquals(UnDefType.UNDEF, scfo.getPower(past, SolarForecast.PESSIMISTIC), "Pessimistic Power"); + assertEquals(UnDefType.UNDEF, scfo.getPower(past, "total", "rubbish"), "Rubbish arguments"); + assertEquals(UnDefType.UNDEF, scfo.getPower(past.plusHours(2), "total", "rubbish"), "Rubbish arguments"); + assertEquals(UnDefType.UNDEF, scfo.getPower(past), "Normal Power"); + + LocalDate ld = LocalDate.of(2022, 7, 20); + System.out.println(scfo.getDayTotal(ld, QueryMode.Estimation)); + System.out.println(scfo.getDayTotal(ld, QueryMode.Optimistic)); + System.out.println(scfo.getDayTotal(ld, QueryMode.Pessimistic)); } @Test @@ -290,11 +313,13 @@ void testInavlid() { String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); ZonedDateTime now = ZonedDateTime.now(TEST_ZONE); SolcastObject scfo = new SolcastObject(content, now); - assertEquals(-1.0, scfo.getActualValue(now), 0.01, "Data available - day not in"); + assertEquals(-1.0, scfo.getActualValue(now, QueryMode.Estimation), 0.01, "Data available - day not in"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); - assertEquals(-1.0, scfo.getActualValue(now), 0.01, "Data available after merge - day not in"); - assertEquals(-1.0, scfo.getDayTotal(now.toLocalDate()), 0.01, "Data available after merge - day not in"); + assertEquals(-1.0, scfo.getActualValue(now, QueryMode.Estimation), 0.01, + "Data available after merge - day not in"); + assertEquals(-1.0, scfo.getDayTotal(now.toLocalDate(), QueryMode.Estimation), 0.01, + "Data available after merge - day not in"); } @Test From dacc4848e2618f0db7f4d4bd8ff2d7143b196e12 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 9 Aug 2022 18:01:46 +0200 Subject: [PATCH 022/111] change TimeZoneProvider handling Signed-off-by: Bernd Weymann --- .../internal/SolarForecastHandlerFactory.java | 8 ++--- .../solcast/SolcastBridgeHandler.java | 9 +++-- .../internal/solcast/SolcastConstants.java | 3 -- .../internal/solcast/SolcastObject.java | 22 +++++++----- .../internal/solcast/SolcastPlaneHandler.java | 20 +++++++---- .../binding/solarforecast/SolcastTest.java | 34 ++++++++++--------- .../openhab/binding/solarforecast/TimeZP.java | 32 +++++++++++++++++ 7 files changed, 86 insertions(+), 42 deletions(-) create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/TimeZP.java 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 index b56722914e9d9..0d360ccd122a3 100644 --- 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 @@ -22,7 +22,6 @@ import org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarBridgeHandler; import org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarPlaneHandler; import org.openhab.binding.solarforecast.internal.solcast.SolcastBridgeHandler; -import org.openhab.binding.solarforecast.internal.solcast.SolcastConstants; import org.openhab.binding.solarforecast.internal.solcast.SolcastPlaneHandler; import org.openhab.core.i18n.LocationProvider; import org.openhab.core.i18n.TimeZoneProvider; @@ -54,6 +53,7 @@ @Component(configurationPid = "binding.solarforecast", service = ThingHandlerFactory.class) public class SolarForecastHandlerFactory extends BaseThingHandlerFactory { private final Logger logger = LoggerFactory.getLogger(SolarForecastHandlerFactory.class); + private final TimeZoneProvider timeZoneProvider; private final ItemRegistry itemRegistry; private final HttpClient httpClient; private final PointType location; @@ -65,7 +65,7 @@ public SolarForecastHandlerFactory(final @Reference HttpClientFactory hcf, final final @Reference PersistenceServiceRegistry psr, final @Reference ItemRegistry ir, final @Reference TimeZoneProvider tzp) { itemRegistry = ir; - SolcastConstants.zonedId = tzp.getTimeZone(); + timeZoneProvider = tzp; httpClient = hcf.getCommonHttpClient(); PointType pt = lp.getLocation(); if (pt != null) { @@ -95,9 +95,9 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { } else if (FORECAST_SOLAR_PART_STRING.equals(thingTypeUID)) { return new ForecastSolarPlaneHandler(thing, httpClient); } else if (SOLCAST_BRIDGE_STRING.equals(thingTypeUID)) { - return new SolcastBridgeHandler((Bridge) thing); + return new SolcastBridgeHandler((Bridge) thing, timeZoneProvider); } else if (SOLCAST_PART_STRING.equals(thingTypeUID)) { - return new SolcastPlaneHandler(thing, httpClient, qps, itemRegistry); + return new SolcastPlaneHandler(thing, httpClient, qps, itemRegistry, timeZoneProvider); } return null; } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java index bd0d4fbd571a1..0820e73957fc2 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java @@ -31,6 +31,7 @@ import org.openhab.binding.solarforecast.internal.actions.SolarForecastActions; import org.openhab.binding.solarforecast.internal.actions.SolarForecastProvider; import org.openhab.binding.solarforecast.internal.solcast.SolcastObject.QueryMode; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ThingStatus; @@ -49,14 +50,16 @@ */ @NonNullByDefault public class SolcastBridgeHandler extends BaseBridgeHandler implements SolarForecastProvider { - private final Logger logger = LoggerFactory.getLogger(SolcastBridgeHandler.class); + private final TimeZoneProvider timeZoneProvider; + private List parts = new ArrayList(); private Optional configuration = Optional.empty(); private Optional> refreshJob = Optional.empty(); - public SolcastBridgeHandler(Bridge bridge) { + public SolcastBridgeHandler(Bridge bridge, TimeZoneProvider tzp) { super(bridge); + timeZoneProvider = tzp; logger.debug("{} Constructor", bridge.getLabel()); } @@ -118,7 +121,7 @@ private synchronized void getData() { logger.debug("No PV plane defined yet"); return; } - ZonedDateTime now = ZonedDateTime.now(SolcastConstants.zonedId); + ZonedDateTime now = ZonedDateTime.now(timeZoneProvider.getTimeZone()); double actualSum = 0; double actualPowerSum = 0; double remainSum = 0; 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 index e7fdf4c2b2b5a..33a6ccb0beff6 100644 --- 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 @@ -12,8 +12,6 @@ */ package org.openhab.binding.solarforecast.internal.solcast; -import java.time.ZoneId; - import javax.measure.Unit; import javax.measure.quantity.Power; @@ -36,7 +34,6 @@ public class SolcastConstants { public static final String BEARER = "Bearer "; public static final Unit KILOWATT_UNIT = MetricPrefix.KILO(Units.WATT); - public static ZoneId zonedId = ZoneId.systemDefault(); public static final double UNDEF_DOUBLE = -1.0; } 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 index 12b4c3163829d..9384a9a3e6e5e 100644 --- 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 @@ -28,6 +28,7 @@ import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; import org.openhab.binding.solarforecast.internal.Utils; import org.openhab.binding.solarforecast.internal.actions.SolarForecast; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; import org.slf4j.Logger; @@ -47,6 +48,7 @@ public class SolcastObject implements SolarForecast { private final TreeMap> estimationDataMap = new TreeMap>(); private final TreeMap> optimisticDataMap = new TreeMap>(); private final TreeMap> pessimisticDataMap = new TreeMap>(); + private final TimeZoneProvider timeZoneProvider; private Optional rawData = Optional.of(new JSONObject()); private ZonedDateTime expirationDateTime; @@ -70,13 +72,15 @@ public String toString() { } } - public SolcastObject() { + public SolcastObject(TimeZoneProvider tzp) { // invalid forecast object - expirationDateTime = ZonedDateTime.now(SolcastConstants.zonedId); + timeZoneProvider = tzp; + expirationDateTime = ZonedDateTime.now(timeZoneProvider.getTimeZone()); } - public SolcastObject(String content, ZonedDateTime expiration) { + public SolcastObject(String content, ZonedDateTime expiration, TimeZoneProvider tzp) { expirationDateTime = expiration; + timeZoneProvider = tzp; add(content); } @@ -141,7 +145,7 @@ private void add(String content) { public boolean isValid() { if (valid) { if (!estimationDataMap.isEmpty()) { - if (expirationDateTime.isAfter(ZonedDateTime.now(SolcastConstants.zonedId))) { + if (expirationDateTime.isAfter(ZonedDateTime.now(timeZoneProvider.getTimeZone()))) { return true; } else { logger.debug("Forecast data expired"); @@ -267,9 +271,9 @@ public String getRaw() { return rawData.get().toString(); } - public static ZonedDateTime getZdtFromUTC(String utc) { + public ZonedDateTime getZdtFromUTC(String utc) { Instant timestamp = Instant.parse(utc); - return timestamp.atZone(SolcastConstants.zonedId); + return timestamp.atZone(timeZoneProvider.getTimeZone()); } private TreeMap getDataMap(LocalDate ld, QueryMode mode) { @@ -330,8 +334,8 @@ public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDate return UnDefType.UNDEF; } } - ZonedDateTime zdtBegin = localDateTimeBegin.atZone(SolcastConstants.zonedId); - ZonedDateTime zdtEnd = localDateTimeEnd.atZone(SolcastConstants.zonedId); + ZonedDateTime zdtBegin = localDateTimeBegin.atZone(timeZoneProvider.getTimeZone()); + ZonedDateTime zdtEnd = localDateTimeEnd.atZone(timeZoneProvider.getTimeZone()); LocalDate beginDate = zdtBegin.toLocalDate(); LocalDate endDate = zdtEnd.toLocalDate(); double measure = UNDEF; @@ -369,7 +373,7 @@ public State getPower(LocalDateTime queryDateTime, String... args) { } } - ZonedDateTime zdt = queryDateTime.atZone(SolcastConstants.zonedId); + ZonedDateTime zdt = queryDateTime.atZone(timeZoneProvider.getTimeZone()); double measure = getActualPowerValue(zdt, mode); return Utils.getPowerState(measure); } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java index 013a8e0ccad11..8dca17c18a94f 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java @@ -39,6 +39,7 @@ import org.openhab.binding.solarforecast.internal.actions.SolarForecastActions; import org.openhab.binding.solarforecast.internal.actions.SolarForecastProvider; import org.openhab.binding.solarforecast.internal.solcast.SolcastObject.QueryMode; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.items.Item; import org.openhab.core.items.ItemRegistry; import org.openhab.core.library.types.DecimalType; @@ -74,19 +75,23 @@ public class SolcastPlaneHandler extends BaseThingHandler implements SolarForeca private final Logger logger = LoggerFactory.getLogger(SolcastPlaneHandler.class); private final HttpClient httpClient; private final ItemRegistry itemRegistry; + private final TimeZoneProvider timeZoneProvider; private Optional configuration = Optional.empty(); private Optional bridgeHandler = Optional.empty(); private Optional powerItem = Optional.empty(); private Optional persistenceService; - private SolcastObject forecast = new SolcastObject(); + private SolcastObject forecast; private ZonedDateTime nextMeasurement; - public SolcastPlaneHandler(Thing thing, HttpClient hc, Optional qps, ItemRegistry ir) { + public SolcastPlaneHandler(Thing thing, HttpClient hc, Optional qps, ItemRegistry ir, + TimeZoneProvider tzp) { super(thing); httpClient = hc; persistenceService = qps; itemRegistry = ir; - nextMeasurement = Utils.getNextTimeframe(ZonedDateTime.now(SolcastConstants.zonedId)); + timeZoneProvider = tzp; + forecast = new SolcastObject(timeZoneProvider); + nextMeasurement = Utils.getNextTimeframe(ZonedDateTime.now(timeZoneProvider.getTimeZone())); logger.debug("{} Constructor", thing.getLabel()); } @@ -167,7 +172,8 @@ protected SolcastObject fetchData() { ContentResponse crEstimate = estimateRequest.send(); if (crEstimate.getStatus() == 200) { SolcastObject localForecast = new SolcastObject(crEstimate.getContentAsString(), ZonedDateTime - .now(SolcastConstants.zonedId).plusMinutes(configuration.get().refreshInterval)); + .now(timeZoneProvider.getTimeZone()).plusMinutes(configuration.get().refreshInterval), + timeZoneProvider); logger.trace("{} Fetched data {}", thing.getLabel(), localForecast.toString()); // get forecast @@ -194,7 +200,7 @@ protected SolcastObject fetchData() { logger.debug("{} use available forecast {}", thing.getLabel(), forecast); } updateChannels(forecast); - if (ZonedDateTime.now(SolcastConstants.zonedId).isAfter(nextMeasurement)) { + if (ZonedDateTime.now(timeZoneProvider.getTimeZone()).isAfter(nextMeasurement)) { sendMeasure(); } return forecast; @@ -291,11 +297,11 @@ private void sendMeasure() { } } updateState(CHANNEL_RAW_TUNING, updateState); - nextMeasurement = Utils.getNextTimeframe(ZonedDateTime.now(SolcastConstants.zonedId)); + nextMeasurement = Utils.getNextTimeframe(ZonedDateTime.now(timeZoneProvider.getTimeZone())); } private void updateChannels(SolcastObject f) { - ZonedDateTime now = ZonedDateTime.now(SolcastConstants.zonedId); + ZonedDateTime now = ZonedDateTime.now(timeZoneProvider.getTimeZone()); updateState(CHANNEL_ACTUAL, Utils.getEnergyState(f.getActualValue(now, QueryMode.Estimation))); updateState(CHANNEL_ACTUAL_POWER, Utils.getEnergyState(f.getActualPowerValue(now, QueryMode.Estimation))); updateState(CHANNEL_REMAINING, Utils.getEnergyState(f.getRemainingProduction(now, QueryMode.Estimation))); 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 index 7d712e37050aa..6a20bbc3b3f51 100644 --- 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 @@ -14,7 +14,6 @@ import static org.junit.jupiter.api.Assertions.*; -import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; @@ -39,7 +38,8 @@ */ @NonNullByDefault class SolcastTest { - private static final ZoneId TEST_ZONE = ZoneId.of("Europe/Berlin"); + public static final ZoneId TEST_ZONE = ZoneId.of("Europe/Berlin"); + private static final TimeZP tzp = new TimeZP(); // double comparison tolerance = 1 Watt private static final double TOLERANCE = 0.001; @@ -97,7 +97,7 @@ class SolcastTest { 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(content, now); + SolcastObject scfo = new SolcastObject(content, now, tzp); content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); scfo.join(content); @@ -124,7 +124,7 @@ void testForecastObject() { 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(content, now); + SolcastObject scfo = new SolcastObject(content, now, tzp); content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); scfo.join(content); @@ -231,7 +231,7 @@ void testPower() { 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(content, now); + SolcastObject scfo = new SolcastObject(content, now, tzp); assertEquals(0.614, scfo.getActualValue(now, QueryMode.Estimation), TOLERANCE, "Actual estimation"); assertEquals(25.413, scfo.getDayTotal(now.toLocalDate(), QueryMode.Estimation), TOLERANCE, "Day total"); } @@ -240,7 +240,7 @@ void testForecastTreeMap() { 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(content, now); + SolcastObject scfo = new SolcastObject(content, now, tzp); assertEquals(-1.0, scfo.getActualValue(now, QueryMode.Estimation), 0.01, "Invalid"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); @@ -255,7 +255,7 @@ void testJoin() { 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(content, now); + SolcastObject scfo = new SolcastObject(content, now, tzp); assertEquals(-1.0, scfo.getActualValue(now, QueryMode.Estimation), 0.01, "Invalid"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); @@ -284,7 +284,7 @@ void testActions() { 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(content, now); + SolcastObject scfo = new SolcastObject(content, now, tzp); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); assertEquals(19.389, scfo.getDayTotal(now.toLocalDate().plusDays(2), QueryMode.Estimation), TOLERANCE, @@ -293,6 +293,12 @@ void testOptimisticPessimistic() { "Estimation"); assertEquals(22.283, scfo.getDayTotal(now.toLocalDate().plusDays(2), QueryMode.Optimistic), TOLERANCE, "Estimation"); + assertEquals(23.316, scfo.getDayTotal(now.toLocalDate().plusDays(6), QueryMode.Estimation), 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 LocalDateTime past = LocalDateTime.now().minusMinutes(5); @@ -301,18 +307,13 @@ void testOptimisticPessimistic() { assertEquals(UnDefType.UNDEF, scfo.getPower(past, "total", "rubbish"), "Rubbish arguments"); assertEquals(UnDefType.UNDEF, scfo.getPower(past.plusHours(2), "total", "rubbish"), "Rubbish arguments"); assertEquals(UnDefType.UNDEF, scfo.getPower(past), "Normal Power"); - - LocalDate ld = LocalDate.of(2022, 7, 20); - System.out.println(scfo.getDayTotal(ld, QueryMode.Estimation)); - System.out.println(scfo.getDayTotal(ld, QueryMode.Optimistic)); - System.out.println(scfo.getDayTotal(ld, QueryMode.Pessimistic)); } @Test void testInavlid() { String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); ZonedDateTime now = ZonedDateTime.now(TEST_ZONE); - SolcastObject scfo = new SolcastObject(content, now); + SolcastObject scfo = new SolcastObject(content, now, tzp); assertEquals(-1.0, scfo.getActualValue(now, QueryMode.Estimation), 0.01, "Data available - day not in"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); @@ -342,7 +343,7 @@ void testTimeframes() { 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(content, now); + SolcastObject sco = new SolcastObject(content, now, tzp); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); sco.join(content); JSONObject joined = new JSONObject(sco.getRaw()); @@ -359,7 +360,8 @@ void testUnitDetection() { @Test void testTimes() { String utcTimeString = "2022-07-17T19:30:00.0000000Z"; - ZonedDateTime zdt = SolcastObject.getZdtFromUTC(utcTimeString); + SolcastObject sco = new SolcastObject(tzp); + ZonedDateTime zdt = sco.getZdtFromUTC(utcTimeString); assertEquals("2022-07-17T21:30+02:00[Europe/Berlin]", zdt.toString(), "ZonedDateTime"); LocalDateTime ldt = zdt.toLocalDateTime(); assertEquals("2022-07-17T21:30", ldt.toString(), "LocalDateTime"); 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 0000000000000..a5af8e59bbef8 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/TimeZP.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2022 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; + } +} From da12ba997c966e517963aef650d37d58f3cf6f1b Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Wed, 10 Aug 2022 00:05:52 +0200 Subject: [PATCH 023/111] bugfix actions arguments Signed-off-by: Bernd Weymann --- .../README.md | 86 +++++++++++++++++-- .../actions/SolarForecastActions.java | 26 +++--- .../forecastsolar/ForecastSolarObject.java | 6 +- .../ForecastSolarPlaneHandler.java | 7 +- .../internal/solcast/SolcastObject.java | 12 +-- .../internal/solcast/SolcastPlaneHandler.java | 4 +- .../binding/solarforecast/SolcastTest.java | 29 ++++--- 7 files changed, 122 insertions(+), 48 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 7538e2fe78e21..1f61da59565fb 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -30,6 +30,9 @@ A free version for your personal home PV system is available in [Hobbyist Plan]( You need to configure your home photovoltaic system within the web interface. After configuration the necessary information is available. +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 Tuning You've the opportunity to [send your own measurements back to Solcast API](https://legacy-docs.solcast.com.au/#measurements-rooftop-site). @@ -161,10 +164,18 @@ All things `sc-site`, `sc-plane`, `fs-site` and `fs-plane` are providing the sam While 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. +Input for queries are `LocalDateTime` and `LocalDate` objects. +Double check your time zone in *openHAB - Settings - Regional Settings* which is crucial for calculation. + ### Get Forecast Begin ````java -LocalDateTime getForecastBegin() + /** + * Get the first date and time of forecast data + * + * @return your localized date time + */ + public LocalDateTime getForecastBegin(); ```` Returns `LocalDateTime` of the earliest possible forecast data available. @@ -174,7 +185,12 @@ It's located in the past, e.g. Solcast provides data from the last 7 days. ### Get Forecast End ````java -LocalDateTime getForecastEnd() + /** + * Get the last date and time of forecast data + * + * @return your localized date time + */ + public LocalDateTime getForecastEnd(); ```` Returns `LocalDateTime` of the latest possible forecast data available. @@ -183,33 +199,66 @@ Returns `LocalDateTime` of the latest possible forecast data available. ### Get Power ````java -State getPower(LocalDateTime dateTime) + /** + * Returns electric power at one specific point of time + * + * @param localDateTime + * @param args possible arguments from this interface + * @return QuantityType in kW + */ + public State getPower(LocalDateTime localDateTime, String... args); ```` -Returns `QuantityType` at the given `dateTime`. +Returns `QuantityType` at the given `localDateTime`. Respect `getForecastBegin` and `getForecastEnd` to get a valid value. Check for `UndefType.UNDEF` in case of errors. +Solcast things are supporting arguments. +Chosse ´optimistic´ or ´pessimistic´ to get values for a positive or negative future scenario. +For these scenarios `localDateTime` needs to be located between `now` and ´getForecastEnd´. + ### Get Day ````java -State getDay(LocalDate localDate) + /** + * Returns electric energy production for one day + * + * @param localDate + * @param args possible arguments from this interface + * @return QuantityType in kW/h + */ + public State getDay(LocalDate localDate, String... args); ```` Returns `QuantityType` at the given `localDate`. Respect `getForecastBegin` and `getForecastEnd` to avoid ambigouos values. Check for `UndefType.UNDEF` in case of errors. +Solcast things are supporting arguments. +Chosse ´optimistic´ or ´pessimistic´ to get values for a positive or negative future scenario. +For these scenarios `localDate` needs to be today and ´getForecastEnd´. + ### Get Energy ````java -State getEnergy(LocalDateTime begin, LocalDateTime end) + /** + * Returns electric energy between two timestamps + * + * @param localDateTimeBegin + * @param localDateTimeEnd + * @param args possible arguments from this interface + * @return QuantityType in kW/h + */ + public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDateTimeEnd, String... args); ```` -Returns `QuantityType` between the timestamps `begin` and `end`. +Returns `QuantityType` between the timestamps `localDateTimeBegin` and `localDateTimeEnd`. Respect `getForecastBegin` and `getForecastEnd` to avoid ambigouos values. Check for `UndefType.UNDEF` in case of errors. +Solcast things are supporting arguments. +Chosse ´optimistic´ or ´pessimistic´ to get values for a positive or negative future scenario. +For these scenarios `localDateTimeEnd` needs to be located between `now` and ´getForecastEnd´. ## Example @@ -279,6 +328,10 @@ rule "Forecast Solar Actions" logInfo("SF Tests","Forecast 2 days state: "+ twoDaysForecastFromNowState.toString) val twoDaysForecastFromNowValue = (twoDaysForecastFromNowState as Number).doubleValue logInfo("SF Tests","Forecast 2 days value: "+ twoDaysForecastFromNowValue) + + /* + val solarforecastActions = getActions("solarforecast","solarforecast:fs-site:homeSite") + */ end ```` @@ -293,3 +346,22 @@ shall produce following output 2022-08-07 18:02:19.891 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast 2 days state: 112.483 kWh 2022-08-07 18:02:19.892 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast 2 days value: 112.483 ```` + +Call with Arguments + +```` + val sixDayForecast = solarforecastActions.getEnergy(LocalDateTime.now,LocalDateTime.now.plusDays(6)) + logInfo("SF Tests","Forecast Estimate 6 days "+ sixDayForecast) + val sixDayOptimistic = solarforecastActions.getEnergy(LocalDateTime.now,LocalDateTime.now.plusDays(6),"optimistic") + logInfo("SF Tests","Forecast Optimist 6 days "+ sixDayOptimistic) + val sixDayPessimistic = solarforecastActions.getEnergy(LocalDateTime.now,LocalDateTime.now.plusDays(6),"pessimistic") + logInfo("SF Tests","Forecast Pessimist 6 days "+ sixDayPessimistic) +```` + +shall produce following output + +```` +2022-08-10 00:02:16.569 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast Estimate 6 days 309.424 kWh +2022-08-10 00:02:16.574 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast Optimist 6 days 319.827 kWh +2022-08-10 00:02:16.578 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast Pessimist 6 days 208.235 kWh +```` 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 index 6a454c7a9f1e8..e540f94596c19 100644 --- 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 @@ -45,7 +45,7 @@ @ThingActionsScope(name = "solarforecast") @NonNullByDefault public class SolarForecastActions implements ThingActions { - private static final Logger logger = LoggerFactory.getLogger(SolarForecastActions.class); + private final Logger logger = LoggerFactory.getLogger(SolarForecastActions.class); private Optional thingHandler = Optional.empty(); @RuleAction(label = "@text/actionDayLabel", description = "@text/actionDayDesc") @@ -59,7 +59,7 @@ public State getDay( QuantityType measure = QuantityType.valueOf(0, Units.KILOWATT_HOUR); for (Iterator iterator = l.iterator(); iterator.hasNext();) { SolarForecast solarForecast = iterator.next(); - State s = solarForecast.getDay(localDate); + State s = solarForecast.getDay(localDate, args); logger.trace("Found measure {}", s); if (s instanceof QuantityType) { measure = measure.add((QuantityType) s); @@ -77,7 +77,8 @@ public State getDay( @RuleAction(label = "@text/actionPowerLabel", description = "@text/actionPowerDesc") public State getPower( - @ActionInput(name = "localDateTime", label = "@text/actionInputDateTimeLabel", description = "@text/actionInputDateTimeDesc") LocalDateTime localDateTime) { + @ActionInput(name = "localDateTime", label = "@text/actionInputDateTimeLabel", description = "@text/actionInputDateTimeDesc") LocalDateTime localDateTime, + String... args) { if (thingHandler.isPresent()) { List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); logger.trace("Found {} SolarForecast entries", l.size()); @@ -85,7 +86,7 @@ public State getPower( QuantityType measure = QuantityType.valueOf(0, MetricPrefix.KILO(Units.WATT)); for (Iterator iterator = l.iterator(); iterator.hasNext();) { SolarForecast solarForecast = iterator.next(); - State s = solarForecast.getPower(localDateTime); + State s = solarForecast.getPower(localDateTime, args); logger.trace("Found measure {}", s); if (s instanceof QuantityType) { measure = measure.add((QuantityType) s); @@ -104,7 +105,8 @@ public State getPower( @RuleAction(label = "@text/actionEnergyLabel", description = "@text/actionEnergyDesc") public State getEnergy( @ActionInput(name = "localDateTimeBegin", label = "@text/actionInputDateTimeBeginLabel", description = "@text/actionInputDateTimeBeginDesc") LocalDateTime localDateTimeBegin, - @ActionInput(name = "localDateTimeEnd", label = "@text/actionInputDateTimeEndLabel", description = "@text/actionInputDateTimeEndDesc") LocalDateTime localDateTimeEnd) { + @ActionInput(name = "localDateTimeEnd", label = "@text/actionInputDateTimeEndLabel", description = "@text/actionInputDateTimeEndDesc") LocalDateTime localDateTimeEnd, + String... args) { if (thingHandler.isPresent()) { List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); logger.trace("Found {} SolarForecast entries", l.size()); @@ -112,7 +114,7 @@ public State getEnergy( QuantityType measure = QuantityType.valueOf(0, Units.KILOWATT_HOUR); for (Iterator iterator = l.iterator(); iterator.hasNext();) { SolarForecast solarForecast = iterator.next(); - State s = solarForecast.getEnergy(localDateTimeBegin, localDateTimeEnd); + State s = solarForecast.getEnergy(localDateTimeBegin, localDateTimeEnd, args); logger.trace("Found measure {}", s); if (s instanceof QuantityType) { measure = measure.add((QuantityType) s); @@ -179,16 +181,16 @@ public LocalDateTime getForecastEnd() { return LocalDateTime.MAX; } - public static State getDay(ThingActions actions, LocalDate ld) { - return ((SolarForecastActions) actions).getDay(ld); + public static State getDay(ThingActions actions, LocalDate ld, String... args) { + return ((SolarForecastActions) actions).getDay(ld, args); } - public static State getPower(ThingActions actions, LocalDateTime dateTime) { - return ((SolarForecastActions) actions).getPower(dateTime); + public static State getPower(ThingActions actions, LocalDateTime dateTime, String... args) { + return ((SolarForecastActions) actions).getPower(dateTime, args); } - public static State getEnergy(ThingActions actions, LocalDateTime begin, LocalDateTime end) { - return ((SolarForecastActions) actions).getEnergy(begin, end); + public static State getEnergy(ThingActions actions, LocalDateTime begin, LocalDateTime end, String... args) { + return ((SolarForecastActions) actions).getEnergy(begin, end, args); } public static LocalDateTime getForecastBegin(ThingActions actions) { 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 index f5a2f563ddc28..4d46781fdbbd1 100644 --- 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 @@ -207,7 +207,7 @@ public String toString() { @Override public State getDay(LocalDate localDate, String... args) { if (args.length > 0) { - logger.debug("ForecastSolar doesn't accept arguments"); + logger.info("ForecastSolar doesn't accept arguments"); return UnDefType.UNDEF; } double measure = getDayTotal(localDate); @@ -218,7 +218,7 @@ public State getDay(LocalDate localDate, String... args) { @Override public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDateTimeEnd, String... args) { if (args.length > 0) { - logger.debug("ForecastSolar doesn't accept arguments"); + logger.info("ForecastSolar doesn't accept arguments"); return UnDefType.UNDEF; } LocalDate beginDate = localDateTimeBegin.toLocalDate(); @@ -248,7 +248,7 @@ public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDate @Override public State getPower(LocalDateTime localDateTime, String... args) { if (args.length > 0) { - logger.debug("ForecastSolar doesn't accept arguments"); + logger.info("ForecastSolar doesn't accept arguments"); return UnDefType.UNDEF; } double measure = getActualPowerValue(localDateTime); diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java index 83d862a55d7d0..8ab2ba6217a38 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java @@ -143,9 +143,8 @@ protected ForecastSolarObject fetchData() { LocalDateTime.now().plusMinutes(configuration.get().refreshInterval)); setForecast(localForecast); - logger.debug("{} Fetched data {}", thing.getLabel(), forecast.toString()); - logger.debug("{} {} HTTP errors since last successful update", thing.getLabel(), - failureCounter); + logger.debug("Fetched new data. {} {} HTTP errors since last successful update", + thing.getLabel(), failureCounter); failureCounter = 0; updateState(CHANNEL_RAW, StringType.valueOf(cr.getContentAsString())); } else { @@ -156,7 +155,7 @@ protected ForecastSolarObject fetchData() { logger.debug("{} Call {} failed {}", thing.getLabel(), url, e.getMessage()); } } else { - logger.debug("{} use available forecast {}", thing.getLabel(), forecast); + logger.trace("{} use available forecast", thing.getLabel()); } updateChannels(forecast); } else { 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 index 9384a9a3e6e5e..69d5b0d3ca03f 100644 --- 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 @@ -311,7 +311,7 @@ public State getDay(LocalDate localDate, String... args) { return UnDefType.UNDEF; } else if (mode.equals(QueryMode.Optimistic) || mode.equals(QueryMode.Pessimistic)) { if (localDate.isBefore(LocalDate.now())) { - logger.debug("{} forecasts only available for future", mode); + logger.info("{} forecasts only available for future", mode); return UnDefType.UNDEF; } } @@ -322,7 +322,7 @@ public State getDay(LocalDate localDate, String... args) { @Override public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDateTimeEnd, String... args) { if (localDateTimeEnd.isBefore(localDateTimeBegin)) { - logger.debug("End {} defined before Start {}", localDateTimeEnd, localDateTimeBegin); + logger.info("End {} defined before Start {}", localDateTimeEnd, localDateTimeBegin); return UnDefType.UNDEF; } QueryMode mode = evalArguments(args); @@ -330,7 +330,7 @@ public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDate return UnDefType.UNDEF; } else if (mode.equals(QueryMode.Optimistic) || mode.equals(QueryMode.Pessimistic)) { if (localDateTimeEnd.isBefore(LocalDateTime.now())) { - logger.debug("{} forecasts only available for future", mode); + logger.info("{} forecasts only available for future", mode); return UnDefType.UNDEF; } } @@ -368,7 +368,7 @@ public State getPower(LocalDateTime queryDateTime, String... args) { return UnDefType.UNDEF; } else if (mode.equals(QueryMode.Optimistic) || mode.equals(QueryMode.Pessimistic)) { if (queryDateTime.isBefore(LocalDateTime.now().minusMinutes(1))) { - logger.debug("{} forecasts only available for future", mode); + logger.info("{} forecasts only available for future", mode); return UnDefType.UNDEF; } } @@ -399,7 +399,7 @@ public LocalDateTime getForecastEnd() { private QueryMode evalArguments(String[] args) { if (args.length > 0) { if (args.length > 1) { - logger.debug("Too many arguments {}", Arrays.toString(args)); + logger.info("Too many arguments {}", Arrays.toString(args)); return QueryMode.Error; } @@ -408,7 +408,7 @@ private QueryMode evalArguments(String[] args) { } else if (SolarForecast.PESSIMISTIC.equals(args[0])) { return QueryMode.Pessimistic; } else { - logger.debug("Argument {} not supported", args[0]); + logger.info("Argument {} not supported", args[0]); return QueryMode.Error; } } else { diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java index 8dca17c18a94f..89b3932d068ba 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java @@ -197,7 +197,7 @@ protected SolcastObject fetchData() { logger.info("{} Call {} failed {}", thing.getLabel(), currentEstimateUrl, e.getMessage()); } } else { - logger.debug("{} use available forecast {}", thing.getLabel(), forecast); + logger.trace("{} use available forecast", thing.getLabel()); } updateChannels(forecast); if (ZonedDateTime.now(timeZoneProvider.getTimeZone()).isAfter(nextMeasurement)) { @@ -303,7 +303,7 @@ private void sendMeasure() { private void updateChannels(SolcastObject f) { ZonedDateTime now = ZonedDateTime.now(timeZoneProvider.getTimeZone()); updateState(CHANNEL_ACTUAL, Utils.getEnergyState(f.getActualValue(now, QueryMode.Estimation))); - updateState(CHANNEL_ACTUAL_POWER, Utils.getEnergyState(f.getActualPowerValue(now, QueryMode.Estimation))); + updateState(CHANNEL_ACTUAL_POWER, Utils.getPowerState(f.getActualPowerValue(now, QueryMode.Estimation))); updateState(CHANNEL_REMAINING, Utils.getEnergyState(f.getRemainingProduction(now, QueryMode.Estimation))); LocalDate nowDate = now.toLocalDate(); updateState(CHANNEL_TODAY, Utils.getEnergyState(f.getDayTotal(nowDate, QueryMode.Estimation))); 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 index 6a20bbc3b3f51..0dca91643248c 100644 --- 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 @@ -39,7 +39,7 @@ @NonNullByDefault class SolcastTest { public static final ZoneId TEST_ZONE = ZoneId.of("Europe/Berlin"); - private static final TimeZP tzp = new TimeZP(); + private static final TimeZP TIMEZONEPROVIDER = new TimeZP(); // double comparison tolerance = 1 Watt private static final double TOLERANCE = 0.001; @@ -97,7 +97,7 @@ class SolcastTest { 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(content, now, tzp); + SolcastObject scfo = new SolcastObject(content, now, TIMEZONEPROVIDER); content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); scfo.join(content); @@ -124,7 +124,7 @@ void testForecastObject() { 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(content, now, tzp); + SolcastObject scfo = new SolcastObject(content, now, TIMEZONEPROVIDER); content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); scfo.join(content); @@ -231,7 +231,7 @@ void testPower() { 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(content, now, tzp); + SolcastObject scfo = new SolcastObject(content, now, TIMEZONEPROVIDER); assertEquals(0.614, scfo.getActualValue(now, QueryMode.Estimation), TOLERANCE, "Actual estimation"); assertEquals(25.413, scfo.getDayTotal(now.toLocalDate(), QueryMode.Estimation), TOLERANCE, "Day total"); } @@ -240,7 +240,7 @@ void testForecastTreeMap() { 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(content, now, tzp); + SolcastObject scfo = new SolcastObject(content, now, TIMEZONEPROVIDER); assertEquals(-1.0, scfo.getActualValue(now, QueryMode.Estimation), 0.01, "Invalid"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); @@ -255,7 +255,7 @@ void testJoin() { 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(content, now, tzp); + SolcastObject scfo = new SolcastObject(content, now, TIMEZONEPROVIDER); assertEquals(-1.0, scfo.getActualValue(now, QueryMode.Estimation), 0.01, "Invalid"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); @@ -265,18 +265,19 @@ void testActions() { // test daily forecasts + cumulated getEnergy double totalEnergy = 0; LocalDateTime ldtStartTime = LocalDateTime.of(2022, 7, 18, 0, 0); - ZonedDateTime zdtStartTime = LocalDateTime.of(2022, 7, 18, 0, 0).atZone(TEST_ZONE); for (int i = 0; i < 7; i++) { - double day = scfo.getDayTotal(zdtStartTime.toLocalDate().plusDays(i), QueryMode.Estimation); QuantityType qt = (QuantityType) scfo.getDay(ldtStartTime.toLocalDate().plusDays(i)); QuantityType eqt = (QuantityType) scfo.getEnergy(ldtStartTime.plusDays(i), ldtStartTime.plusDays(i + 1)); + + // check if energy calculation fits to daily query + assertEquals(qt.doubleValue(), eqt.doubleValue(), TOLERANCE, "Total " + i + " days forecast"); // System.out.println("Day: " + day + " ADay: " + qt.doubleValue() + " EDay: " + eqt.doubleValue()); totalEnergy += qt.doubleValue(); + // check if sum is fitting to total energy query qt = (QuantityType) scfo.getEnergy(ldtStartTime, ldtStartTime.plusDays(i + 1)); // System.out.println("Total: " + qt.doubleValue()); - assertEquals(totalEnergy, qt.doubleValue(), i * TOLERANCE, "Total " + i + " days forecast"); - // System.out.println(i + " done"); + assertEquals(totalEnergy, qt.doubleValue(), TOLERANCE * 2, "Total " + i + " days forecast"); } } @@ -284,7 +285,7 @@ void testActions() { 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(content, now, tzp); + SolcastObject scfo = new SolcastObject(content, now, TIMEZONEPROVIDER); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); assertEquals(19.389, scfo.getDayTotal(now.toLocalDate().plusDays(2), QueryMode.Estimation), TOLERANCE, @@ -313,7 +314,7 @@ void testOptimisticPessimistic() { void testInavlid() { String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); ZonedDateTime now = ZonedDateTime.now(TEST_ZONE); - SolcastObject scfo = new SolcastObject(content, now, tzp); + SolcastObject scfo = new SolcastObject(content, now, TIMEZONEPROVIDER); assertEquals(-1.0, scfo.getActualValue(now, QueryMode.Estimation), 0.01, "Data available - day not in"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); @@ -343,7 +344,7 @@ void testTimeframes() { 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(content, now, tzp); + SolcastObject sco = new SolcastObject(content, now, TIMEZONEPROVIDER); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); sco.join(content); JSONObject joined = new JSONObject(sco.getRaw()); @@ -360,7 +361,7 @@ void testUnitDetection() { @Test void testTimes() { String utcTimeString = "2022-07-17T19:30:00.0000000Z"; - SolcastObject sco = new SolcastObject(tzp); + SolcastObject sco = new SolcastObject(TIMEZONEPROVIDER); ZonedDateTime zdt = sco.getZdtFromUTC(utcTimeString); assertEquals("2022-07-17T21:30+02:00[Europe/Berlin]", zdt.toString(), "ZonedDateTime"); LocalDateTime ldt = zdt.toLocalDateTime(); From 22b7591184907a382f6b51233d41d14463b08354 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Wed, 10 Aug 2022 00:47:16 +0200 Subject: [PATCH 024/111] readme corrections Signed-off-by: Bernd Weymann --- .../README.md | 20 ++++++++++++------ .../doc/SolcastCumulated.png | Bin 0 -> 64475 bytes .../doc/SolcastPower.png | Bin 0 -> 83573 bytes 3 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 bundles/org.openhab.binding.solarforecast/doc/SolcastCumulated.png create mode 100644 bundles/org.openhab.binding.solarforecast/doc/SolcastPower.png diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 1f61da59565fb..caab5d220384e 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -11,6 +11,14 @@ Supported Services - [Forecast.Solar](https://forecast.solar/) - Public, Personal and Professional [plans](https://forecast.solar/#accounts) available +Display Forecast *Power values* and measures of *PV innverter* item + + + +Display added up values during the day of *Forecast* and *PV inverter* item. +Yellow line shows *Daily Total Forecast*. + + ## Supported Things @@ -214,8 +222,8 @@ Respect `getForecastBegin` and `getForecastEnd` to get a valid value. Check for `UndefType.UNDEF` in case of errors. Solcast things are supporting arguments. -Chosse ´optimistic´ or ´pessimistic´ to get values for a positive or negative future scenario. -For these scenarios `localDateTime` needs to be located between `now` and ´getForecastEnd´. +Choose `optimistic` or `pessimistic` to get values for a positive or negative future scenario. +For these scenarios `localDateTime` needs to be located between `now` and `getForecastEnd`. ### Get Day @@ -235,8 +243,8 @@ Respect `getForecastBegin` and `getForecastEnd` to avoid ambigouos values. Check for `UndefType.UNDEF` in case of errors. Solcast things are supporting arguments. -Chosse ´optimistic´ or ´pessimistic´ to get values for a positive or negative future scenario. -For these scenarios `localDate` needs to be today and ´getForecastEnd´. +Choose `optimistic` or `pessimistic` to get values for a positive or negative future scenario. +For these scenarios `localDate` needs to be between *today* and `getForecastEnd`. ### Get Energy @@ -257,8 +265,8 @@ Respect `getForecastBegin` and `getForecastEnd` to avoid ambigouos values. Check for `UndefType.UNDEF` in case of errors. Solcast things are supporting arguments. -Chosse ´optimistic´ or ´pessimistic´ to get values for a positive or negative future scenario. -For these scenarios `localDateTimeEnd` needs to be located between `now` and ´getForecastEnd´. +Choose `optimistic` or `pessimistic` to get values for a positive or negative future scenario. +For these scenarios `localDateTimeEnd` needs to be located between `now` and `getForecastEnd`. ## Example 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 zcmd432T;@P^Dm08T|w+1RYgFlq7(rE6#?l|LT^g1p?8RiigW=1=}lTFq4yA^_Z~V# zLhmiK5XgCg@9%%koO|caJ!kHjxtV$A{gQmkKKtx`cK5UU)$gr>6vZW)OJrnZ6w+^A zE0d9(yFo^FI_<(g;F|@)M^5m+Qx3{fugLPc=oY~rXG~wpza%3o2)RsncNYA8(e{m| z0~y(EX3~GBq?I3RkdYD7q+h>Ob=6y$xTu4M91}O&-AA|sI8yH3^{4sFVj-;}oo1fJ zC$GZ$6lOVU69Y zjajo)S2dBpm#&0o7*5k(Yr>q5fne^<0Z|^iir{+0eXr09Sr}t#yoPjL;wzs&BdkJU zDi}gU;=At6o=ku#MNUQXT z*Fz918k5C++1S_yzWn}GT&(JO7#$sLV`Jkm-{rMCVp?W1p{1ikclE06)E6pvEN{%k z8!QbC4b*lDR?`Q5uWDFMM4Rxt z!D>>vKg$iPcvpYU>D}YjmX@oSW87jtakmVG9vlp26q}rwII8`4nv08zO2_tNrF3e= zLQ23_@;k~pIs~Aa%w&g`8T}Nr{2gdCWQUCGkIn_h-q#jh}Fhl z#{TN{lQicKZeu4B>2174F$eMYP2C{F8jEHV&WfK)d)r&57{9PN+gk}oNmuVtr3Ttp zuRdpYD;fKJi;OJ)^7ec;rqapM#AFWa+TJ>bxH|?7q2ZGW)OVS1I^10yt8kbXz<4s{ z^6eqoH{~}lXd6Ye76`;Cpci9SvK<9Z%Q%3+u}rTyt$T^R(6SjqR5V_n%W@j zcssGB<&EHdo7+%a8#`jyGfUz6y0WI`;@sTa^mLKOo{i6M;4M7w>dv@1VH^Cka-9{Q z3ce55IkmUh%!nc$A7aloCR!ZfccOHiM{+ca8mRme=hwaWgxxZ99zR4~jumUlQpq^p zsHeoriXN^!waduFcAAP|_p1y9H<@nMbE0cSvLV;P6dRsG4O^TUJ$ zGP0y*S0R3d%|fJ5hx@f?Io;8&(|E6>gP;qkuGcinDp9xW#()J;L|AC`Tj@I_OBabF zUa?zZbEzl<&Q|f}(HMVUe~7^k9q(WpV<$J|R8l|b&=_iC?@9?qEh;9PL|^rrhXwI^ zLne8-7;@({{lsRU)(0M1(B}!Su$f8%BmFoJJD!Izcy9czt*zCqa!&j83mCL{f4a!Q zO!$+)tH6Y?Gc%6$6asRIqDKO#gU;y?F)m4dtKod_BX@3Y#Nk@i`Vi{a$;fDic!VM9 zI&|`x6uC_|1!bgJI8OzSb z){wZG;IXDzWD>N${jKa zZPsH~tg*z=Y8l3&WON=}UU!*$ybPMSpXJZkzA{oo#ijEQrIt@1gHV!@xxWgP-VS*+ zKX!UK@y?{seZg+zJ7|!<8b?-p&0NK+$k!}A5>LmoSw5kK1alET%y`Mx%J)JyIkhli_YeO+h$i0Vq#+@l{E)7&MOEfLV1H- zuWk6gieKsCQA3qK;-bYBq|K#FOrA@=oONM>>FiXd8&CD(#Bn67^hP2X*$X0%#Xx2% zCs-5JojZaKb8@V#8I+>?f6`-ZV#Hv_DvBZyP0c%ujNDDLQ3C@5p158q`^nDn3WpM{ zRa$7ZooP`Jt-z9Fr9J^mg*x6r7`}gh>GEYN4h`Ee>#_;=`LC-fc$#$B_q)y_@D zM7p;a@j!7?86bPSh=;R*K~k?70l$o3bYHmF>n{+o&b%^Kwho0I##(4ujTTE2K8lEl zu*%0g7`@D0{rK+PKl_{0XV0FcuU2ht2>7->guxJp3k(6k9xrAj_Vx8OxUkkgs!)}$ zZUn%^%h1qJ2&0%Dq1AX40)dnOEZ_AG+)8VR^4vkI z15b_56gylgDzO}j>6{xK)!MB0#4l#BDkjj;({mozqVQDpW}yNXfow}n4lJeZlEN8$Qp(P;F0&*oryK0ZDk z9-bR4azCHhr33`Dcf||Oraf(IZx{C5&wtp!9w3&n&yTFG1~#g$p+QYY=h=8my`^}$ zCs`azKX%B&j|xlA%j05V5+AhoOZ?!v`Rmt%gA#!CKY%-`-w?BK6Za5%K}Ke~>n|6x ze5I<5(BjTB>ee}?uARYE*UYRCI+{Kr`6^8DNPRXK+wK!b3-FS7$IZfNY&Kq)QQqe3 zgfx05CftNpx}P^O-Sv$qhwS|4qmtl$m@{%ZQ?`HL(dO!{n69hm+@4wBc47>uf_!fj zfb`+C|IU>W5d73rO zrGHmE#ml7Jy!FENC%SpHEZNfEFZD6SU)~yQuZ@oFu(b(}R2RyPTe&^At7iUGm{$2a zxn<>QmIxm)bBtdN4spWv7JpyGjl4Rvj3p+#yN4-X{roUF{pGf2&328JZ`u7}T3Qf= zRu+0wZ{51(?d@&v!1!*iBl^^-Q@~g>H8t%Uu!rj?ZZ0lo2Zw;bKti2AHI;w>FRvU3 z2{yT$L5yOe&!1OkW-c56Z&)d4ZlHL=dkyi}q~j+kl&pdzS!#^uQ*1hj*nw~?2%GdLkQ!}-_RFNz2S zm1>&VbTksa=&>(u+y8Z}kXZg}F=&?IO&#KBIhS}UUQtCbZiF;i~V*!l6}$Gr&`j5ZXx6N@28c17J0wEtBuRUPm8B^FvU(%zn(l7a=O zyu~S$Z`d4MSy{Qi@48uNpYlC(tVY$)aC%4|79SeA`NxN{jeDg!Uf4wq0tuqwYy16s zii4F-8YBgr4t;TCJb-$kRIBD%185xqPN5VmWBdb*aWYcn+7Nh@Urq>koI9fc4c3Qgc z30b3C7y$<|_ka_dw zD&i_5V`6mlBfVYVu3^yLkEAdMHU!JbL94#JgD>UY0WcmQi>2+gDCyK6I*Xy8n3E- zgq&x{%a<=f$mqPKu5)iK6lOw3)|1V(XD&3aCpxoL926lxACJ^>Wz#B9(EWTrJRP|H zN?)-+DUC&RR*}UI_pE0jUrV6$^@TF+3YrFqlaJQ;v*w-t_9WuA?pz%m}kB#O@x10pRmpZLJuPuf?es-~aToLQ!57D;4SMbh{?ik}`$d#R{^K%E`8*$1 z6Q!{13ST7$gaq;{+JcO?N>mq*t+{T!|3yGfmBdmk+z4U}dU9*tOtIY$9$$Hz_S6gL z`>tz`_lu()26D`?O1AIXihS*HQ9)j5qVwPNSEW#wCIs4%kT483ucFBSAHxF{+P zFZmd1PntDxQZ`%c+Q1nmwhFh}PyLBA=(_g78*2jV_Y%rTlcE<>K5gwf2otNzS<<3f z7LKjq|CLc0z8MWKh*5p#(seS&JAgGvQ-ywS#BJB}RS>PCLyMRD>zQeYJ&QC!s3}~ufB0ZxFz1VwpIN^BRGpYYbO5OV~YC~kn42wc0?DA*tVDz z7xPoWo7pAiw}b4wBhl6w`u5hvavW_u&)gSjMusoEP`oGj&v)B;FG_(`tK48ne@;7h z?|$>d`_>P@x7sP>@Acxeg~$8d_cDBC_|y*d6j5o$(X`+ZR@u&4T1nE>#``To=eBb6 z=P{YR&pdViQ1jFowMHEC4p1vCZ#k7n8rqL*r2A1PJGa}6ULUhRj*;9}iU^;aaFp$5 ze-p$=VQu=7j?D3&jIGl?Av=DDo#YjqzMI;-^eNC*FPU9_gj$hk7-ow(S7ElSGUL|) z`EkGEY*hASj2ueBlm7hn>1R^UA{bu~v3>b##S`D+P(n%0s8S(!{nSSe@$T3G=TR7q z#m`k%*?GhjVL4i3CpMxv_d@ydfceqiO>qwrnm`E({`2AZGl?fm3fIUzmsIxP^;t`b zFH(L%V5MZWHh_g(;OU2!2-#XUaYlW$l)X}>brwWbz$CE(})tV=s6)ONl&Wf zKM#Y!NC;2*sQv#3+fU^Azh!jk2uDYdzbvppH^~3V&-t_(UH?L}#3dyqMMXuGtHyqb z&q$WCCbg$k;1+&GnE7A6_w>hBBY0Mpxs{cwm!hq$?Z(E2t(~3H(W3h_{g+^j>O%Ya z4}jIAPwg5XFE38=8YPqD5hO17)p;lzZqr5PUuJnF;kUs;&{e6c-0XTfd}{9N z^j~%-RTrk$fBZ`&4~vAi{(|cnY&UxBfZy~`At;r9`Duf_QSC7_lU0o?0th*|y!YR4 zOZFeay4GN3brlJ+C%|LLM?aCEptt~7$05M!-saND#21PGE5|u{D0Cfmw5|BbA*f3T~oN9!=6$6$^oprQ`}z6#QZnjQIa@;@5mi+la&jFV(N9I5 zKL@#fl8r@T=fT@}Hw?Y7v#P49j`N6dl#abLE2cdK9UJy9t6w$LI@2;Xc39LcT7~sz z2ehk8Grh~N^mO=TQ+xi_P*=n<0(Fesn*@})AK<)xJW|0)8TcX^m5?ym({q)cJ{LcU17w`&Rtt&LKoO7DtEd?M?O=TPNGg^O8@ath z6BAJezdt!p(_RFXBLxM8f`S5k-I(+|IhZ;mN_o=U?7Rz4%&^WTB}^Hm`~W=*l-2x zRTUNO#RpO%eas&?Dn z+TLb_5jN|6DYM&=)5nMFu(gW$@$vCU4$ZA$!wE{pH^J_W9|Lh=ec|=KL2r?g4&I)h z%F4Q17I-2*NC%pg*qECO8(k)uP3_%H6+D+uvdtTV{3X7a&1u%E%1Q-l7+do-CM<(p z#*&8usDlu&Bg;DsNKi!sCOxaN$FeE#&Ngr`QbjdnQNpgCC#RqQd?dGWP>rz|2F@v!a~te8B$(O_QCs==k2aG!bsSSgC#;q z-e}x7)0HbRJw12Dq5CN2F zqT12OJ;5cOix>S+)oU(erMdbg7GKPAGbZ^syLf3DTEN0Zh4jTxWEHJSc1rKIqfQL+ zarw+VqotJDl#(!Yk#FG1j!oVE zBQ7p2!A6CI3VydX?uPj0ZTlJPlhu;HYLxf_Oy_cj4x4~0@G;GS_pbw8oP4OHn)L#o zfry&@8_*Gesm)GbURqu*DlAm(&q1zdg5Z0S=%iGwA?FK1dkquZ=BDfnrvZw=z@FoV zG8leD9ph)(d3Xwd&lClA#K(H}2bBTUfu3C|(y{#Ri2z55iTbrS6sw={8ts=_FLQdJ zzJ&~frca~ar)jK;bClZBz~CTANzgdnT(vRhvP3;Eso!(4m%Iq^yYJ=rE~I&*rnb37 zzxnxj0hF65q_nfc4lQx5p|JZXWrJZ?Q|5*bK_|fk8&}7;Ra#6$CO10*^cIVLqKN zUs!6Xy5iWnE}x50G|bU;PUz+%U{(rLjrw8iC7=-pl`I^ZWHU1?#t_0 z7^oRk$1^1)B#}dZ2BBE9lDa+HkCe|*TdryOri6{M+Mc`VUGVo-wBFvQqV$BAM7*T3 zAH7o76P#b@6}K1djpSSgh7DTT6-c`SzvEcG#5SsfwJP+ao+t^jXmY>PIbm`%hnBP1 z8xiqKC-Vz0xT|Ku7J(yJ4(6zOJq6&x7!Ora`e+42FMI-HxFf=H|NiLQex{NkJsmIb z1AD>?N%x2vWfpm}FEg33KW$HVnHP+f(7I4W7pA>`-c>JnNqgV*aFlM3@`9Ds0DvbI zkx?o80eQJ+Cy134G7@L|Sx>@#fF7)bDYeW*6gHczx3MV(~7ZFjq+(*)%hlekFixzhq@$$R0 zja0`QY+LMcUEDo(2L1hUp%||ogG@*eXXv%mV_ZffCg)(lKBwl(UtcVDbMT+-L+(Al zO-;SkCYDejcs${f4T66#is+S0o;>O;$nhq)GIiT`(-%m*N1+PJEYqpNb`!!LN_%NZ z2)}$21gRtZ!CUv{*3i!SeNGY*$agD>JWMdY{v+aGyKv~(#KE&OCSvfO>OAW=@>dU} zbWM&9Grj_I@}e9k&Q!JQXXS9HgQ1YxyN2=kzl!2WBDdmZk4m>)*M(;xJ&m zYCU}A#BkjGFpKkRvdPKsSi+|=aJnf9N8a^A7*BVMR|sO?47oG#Yqtr+xH1q&ha$c1 zH%Z7^4KR?VOGuxp2Lhb;af^`uxW4nF$B&H^N1f40E|2Fy;5)Qku0AiSdyuE6pXCnD zPuc^V4*2@d^z`yVi�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 literal 0 HcmV?d00001 From 77e0659496c5a937bf003598ca08e0b5a4a7150b Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Wed, 10 Aug 2022 00:48:19 +0200 Subject: [PATCH 025/111] readme corrections Signed-off-by: Bernd Weymann --- bundles/org.openhab.binding.solarforecast/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index caab5d220384e..b216602db8c64 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -13,12 +13,12 @@ Supported Services Display Forecast *Power values* and measures of *PV innverter* item - + Display added up values during the day of *Forecast* and *PV inverter* item. Yellow line shows *Daily Total Forecast*. - + ## Supported Things From 6b4619cbdf3e08474dd5bb3e4f07a29fedd1d560 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Wed, 10 Aug 2022 00:57:35 +0200 Subject: [PATCH 026/111] readme corrections Signed-off-by: Bernd Weymann --- .../org.openhab.binding.solarforecast/README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index b216602db8c64..59f19a6b5a2b6 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -336,10 +336,6 @@ rule "Forecast Solar Actions" logInfo("SF Tests","Forecast 2 days state: "+ twoDaysForecastFromNowState.toString) val twoDaysForecastFromNowValue = (twoDaysForecastFromNowState as Number).doubleValue logInfo("SF Tests","Forecast 2 days value: "+ twoDaysForecastFromNowValue) - - /* - val solarforecastActions = getActions("solarforecast","solarforecast:fs-site:homeSite") - */ end ```` @@ -355,15 +351,23 @@ shall produce following output 2022-08-07 18:02:19.892 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast 2 days value: 112.483 ```` -Call with Arguments +### Actions rule with Arguments + +Only Solcast is deliering `optimistic` and `pessimistic` scenario data. +If arguments are used on ForecastSolar `UNDEF` state is returned ```` +rule "Solcast Actions" + when + Time cron "0 0 23 * * ?" // trigger whatever you like + then val sixDayForecast = solarforecastActions.getEnergy(LocalDateTime.now,LocalDateTime.now.plusDays(6)) logInfo("SF Tests","Forecast Estimate 6 days "+ sixDayForecast) val sixDayOptimistic = solarforecastActions.getEnergy(LocalDateTime.now,LocalDateTime.now.plusDays(6),"optimistic") logInfo("SF Tests","Forecast Optimist 6 days "+ sixDayOptimistic) val sixDayPessimistic = solarforecastActions.getEnergy(LocalDateTime.now,LocalDateTime.now.plusDays(6),"pessimistic") logInfo("SF Tests","Forecast Pessimist 6 days "+ sixDayPessimistic) +end ```` shall produce following output From 49103329bdfa72ef5992d47524df4bc990af0f00 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sun, 14 Aug 2022 20:12:14 +0200 Subject: [PATCH 027/111] interpolation optimization Signed-off-by: Bernd Weymann --- .../ForecastSolarBridgeHandler.java | 3 +- .../forecastsolar/ForecastSolarObject.java | 2 +- .../ForecastSolarPlaneHandler.java | 2 +- .../solcast/SolcastBridgeHandler.java | 2 +- .../internal/solcast/SolcastObject.java | 72 ++++++++++++------- .../internal/solcast/SolcastPlaneHandler.java | 2 +- .../internal/{ => utils}/Utils.java | 2 +- .../solarforecast/ForecastSolarTest.java | 3 +- .../binding/solarforecast/SolcastTest.java | 59 +++++++++++++-- 9 files changed, 108 insertions(+), 39 deletions(-) rename bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/{ => utils}/Utils.java (97%) diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java index 6bd98906e18bc..9a34de4321926 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java @@ -26,10 +26,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; -import org.openhab.binding.solarforecast.internal.Utils; 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.utils.Utils; import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.PointType; import org.openhab.core.thing.Bridge; @@ -78,7 +78,6 @@ public void initialize() { } configuration = Optional.of(config); updateStatus(ThingStatus.ONLINE); - getData(); startSchedule(configuration.get().channelRefreshInterval); } 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 index 4d46781fdbbd1..d05293a43b699 100644 --- 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 @@ -23,8 +23,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.json.JSONObject; import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; -import org.openhab.binding.solarforecast.internal.Utils; import org.openhab.binding.solarforecast.internal.actions.SolarForecast; +import org.openhab.binding.solarforecast.internal.utils.Utils; import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; import org.slf4j.Logger; diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java index 8ab2ba6217a38..feb75d423c2a8 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java @@ -26,10 +26,10 @@ 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.Utils; 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.utils.Utils; import org.openhab.core.library.types.PointType; import org.openhab.core.library.types.StringType; import org.openhab.core.thing.Bridge; diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java index 0820e73957fc2..ecb4383747af4 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java @@ -26,11 +26,11 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.solarforecast.internal.Utils; 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.QueryMode; +import org.openhab.binding.solarforecast.internal.utils.Utils; import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; 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 index 69d5b0d3ca03f..6a941451ca990 100644 --- 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 @@ -26,8 +26,8 @@ import org.json.JSONArray; import org.json.JSONObject; import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; -import org.openhab.binding.solarforecast.internal.Utils; 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.types.State; import org.openhab.core.types.UnDefType; @@ -165,15 +165,23 @@ public double getActualValue(ZonedDateTime query, QueryMode mode) { if (dtm.isEmpty()) { return UNDEF; } + double previousEstimate = 0; double forecastValue = 0; Set keySet = dtm.keySet(); for (ZonedDateTime key : keySet) { - if (key.isBefore(query) || key.isEqual(query)) { + if (key.isBefore(query) || query.isEqual(key)) { // value are reported in PT30M = 30 minutes interval with kw value // for kw/h it's half the value - Double addedValue = dtm.get(key); - if (addedValue != null) { - forecastValue += addedValue.doubleValue() / 2; + Double endValue = dtm.get(key); + // production during period is half of previous and next value + if (endValue != null) { + double addedValue = (endValue.doubleValue() + previousEstimate) / 2.0 / 2.0; + // System.out.println( + // "End " + endValue.doubleValue() + " Prev: " + previousEstimate + " Add " + addedValue); + forecastValue += addedValue; + previousEstimate = endValue.doubleValue(); + } else { + logger.info("No estimation found for {}", key); } } } @@ -182,18 +190,22 @@ public double getActualValue(ZonedDateTime query, QueryMode mode) { Entry c = dtm.ceilingEntry(query); if (f != null) { if (c != null) { - // we're during suntime! - double production = c.getValue(); - int interpolation = query.getMinute() - f.getKey().getMinute(); - double interpolationProduction = production * interpolation / 60; - forecastValue += interpolationProduction; - return forecastValue; + if (c.getValue() > 0) { + int interpolation = query.getMinute() - f.getKey().getMinute(); + double interpolationProduction = getActualPowerValue(query, mode) * interpolation / 30.0 / 2.0; + forecastValue += interpolationProduction; + // System.out.println(interpolationProduction); + return forecastValue; + } else { + // if ceiling value is 0 there's no further production in this period + return forecastValue; + } } else { - // sun is down + // if ceiling is null we're at the very end of the day return forecastValue; } } else { - // no floor - sun not rised yet + // if floor is null we're at the very beginning of the day => 0 return 0; } } @@ -213,20 +225,24 @@ public double getActualPowerValue(ZonedDateTime query, QueryMode mode) { Entry c = dtm.ceilingEntry(query); if (f != null) { if (c != null) { - // we're during suntime! double powerCeiling = c.getValue(); - double powerFloor = f.getValue(); - // calculate in minutes from floor to now, e.g. 20 minutes - // => take 2/3 of floor and 1/3 of ceiling - double interpolation = (query.getMinute() - f.getKey().getMinute()) / 60.0; - actualPowerValue = ((1 - interpolation) * powerFloor) + (interpolation * powerCeiling); - return actualPowerValue; + 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 = (query.getMinute() - f.getKey().getMinute()) / 30.0; + actualPowerValue = ((1 - interpolation) * powerFloor) + (interpolation * powerCeiling); + return actualPowerValue; + } else { + // if power ceiling == 0 there's no production in this period + return 0; + } } else { - // sun is down + // if ceiling is null we're at the very end of this day => 0 return 0; } } else { - // no floor - sun not rised yet + // if floor is null we're at the very beginning of this day => 0 return 0; } } @@ -240,14 +256,20 @@ public double getDayTotal(LocalDate query, QueryMode mode) { if (dtm.isEmpty()) { return UNDEF; } + double previousEstimate = 0; double forecastValue = 0; Set keySet = dtm.keySet(); for (ZonedDateTime key : keySet) { // value are reported in PT30M = 30 minutes interval with kw value // for kw/h it's half the value - Double addedValue = dtm.get(key); - if (addedValue != null) { - forecastValue += addedValue.doubleValue() / 2.0; + Double endValue = dtm.get(key); + // production during period is half of previous and next value + if (endValue != null) { + double addedValue = (endValue.doubleValue() + previousEstimate) / 2.0 / 2.0; + forecastValue += addedValue; + previousEstimate = endValue.doubleValue(); + } else { + logger.info("No estimation found for {}", key); } } return forecastValue; diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java index 89b3932d068ba..3b86d310f97a6 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java @@ -34,11 +34,11 @@ import org.eclipse.jetty.client.util.StringContentProvider; import org.eclipse.jetty.http.HttpHeader; import org.json.JSONObject; -import org.openhab.binding.solarforecast.internal.Utils; 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.QueryMode; +import org.openhab.binding.solarforecast.internal.utils.Utils; import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.items.Item; import org.openhab.core.items.ItemRegistry; diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/Utils.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/utils/Utils.java similarity index 97% rename from bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/Utils.java rename to bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/utils/Utils.java index b97c01c90e7db..1bff9db13d83c 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/Utils.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/utils/Utils.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.solarforecast.internal; +package org.openhab.binding.solarforecast.internal.utils; import java.time.ZonedDateTime; 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 index 2019dfab50ca8..8d382bbd1309d 100644 --- 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 @@ -15,14 +15,15 @@ import static org.junit.jupiter.api.Assertions.*; import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import javax.measure.quantity.Energy; import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.Test; -import org.openhab.binding.solarforecast.internal.Utils; import org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarObject; +import org.openhab.binding.solarforecast.internal.utils.Utils; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.Units; import org.openhab.core.types.State; 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 index 0dca91643248c..6b10d366e1929 100644 --- 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 @@ -22,11 +22,11 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.json.JSONObject; import org.junit.jupiter.api.Test; -import org.openhab.binding.solarforecast.internal.Utils; 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.utils.Utils; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.Units; import org.openhab.core.types.UnDefType; @@ -100,7 +100,6 @@ void testForecastObject() { SolcastObject scfo = new SolcastObject(content, now, 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.Estimation); double actual = scfo.getActualValue(now, QueryMode.Estimation); @@ -109,14 +108,17 @@ void testForecastObject() { assertEquals(23.107, remain, TOLERANCE, "Begin of day remaining"); assertEquals(23.107, dayTotal, TOLERANCE, "Day total"); assertEquals(0.0, scfo.getActualPowerValue(now, QueryMode.Estimation), 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.Estimation) / 2.0; - actual += power; + double powerAddOn = ((power + previousPower) / 2.0); + actual += powerAddOn; assertEquals(actual, scfo.getActualValue(now, QueryMode.Estimation), TOLERANCE, "Actual at " + now); - remain -= power; + remain -= powerAddOn; assertEquals(remain, scfo.getRemainingProduction(now, QueryMode.Estimation), TOLERANCE, "Remain at " + now); assertEquals(dayTotal, actual + remain, TOLERANCE, "Total sum at " + now); + previousPower = power; } } @@ -232,7 +234,7 @@ 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(content, now, TIMEZONEPROVIDER); - assertEquals(0.614, scfo.getActualValue(now, QueryMode.Estimation), TOLERANCE, "Actual estimation"); + assertEquals(0.42, scfo.getActualValue(now, QueryMode.Estimation), TOLERANCE, "Actual estimation"); assertEquals(25.413, scfo.getDayTotal(now.toLocalDate(), QueryMode.Estimation), TOLERANCE, "Day total"); } @@ -244,7 +246,7 @@ void testJoin() { assertEquals(-1.0, scfo.getActualValue(now, QueryMode.Estimation), 0.01, "Invalid"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); - assertEquals(19.408, scfo.getActualValue(now, QueryMode.Estimation), 0.01, "Actual data"); + assertEquals(18.946, scfo.getActualValue(now, QueryMode.Estimation), 0.01, "Actual data"); assertEquals(23.107, scfo.getDayTotal(now.toLocalDate(), QueryMode.Estimation), 0.01, "Today data"); JSONObject rawJson = new JSONObject(scfo.getRaw()); assertTrue(rawJson.has("forecasts")); @@ -340,6 +342,51 @@ void testTimeframes() { assertEquals("18:15", Utils.getNextTimeframe(zdt).toLocalTime().toString(), "Q4"); } + @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(content, now, TIMEZONEPROVIDER); + content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + sco.join(content); + + double startValue = sco.getActualPowerValue(now, QueryMode.Estimation); + double endValue = sco.getActualPowerValue(now.plusMinutes(30), QueryMode.Estimation); + for (int i = 0; i < 31; i++) { + // System.out.println(i + " : " + sco.getActualPowerValue(now.plusMinutes(i), QueryMode.Estimation)); + double interpolation = i / 30.0; + double expected = ((1 - interpolation) * startValue) + (interpolation * endValue); + assertEquals(expected, sco.getActualPowerValue(now.plusMinutes(i), QueryMode.Estimation), 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(content, now, 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.getActualValue(now.plusMinutes(i), QueryMode.Estimation); + int hour = now.plusMinutes(i).getHour(); + int minute = now.plusMinutes(i).getMinute(); + double addOnExpected = sco.getActualPowerValue(now.plusMinutes(i), QueryMode.Estimation) / 60.0; + productionExpected += addOnExpected; + double diff = forecast - productionExpected; + maxDiff = Math.max(diff, maxDiff); + System.out.println(hour + ":" + minute + " : " + Math.round(forecast * 1000) / 1000.0 + " - " + + Math.round(productionExpected * 1000) / 1000.0 + " - " + Math.round(diff * 1000) / 1000.0); + // assertEquals(productionExpected, sco.getActualValue(now.plusMinutes(i), QueryMode.Estimation), + // 100 * TOLERANCE, "Step " + i); + } + System.out.println(maxDiff); + } + @Test void testRawChannel() { String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); From a131ab07d4d2c7f91a05e842e6742a9f7129c7c3 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Thu, 18 Aug 2022 11:35:16 +0200 Subject: [PATCH 028/111] json dependency correction Signed-off-by: Bernd Weymann --- bundles/org.openhab.binding.solarforecast/pom.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/pom.xml b/bundles/org.openhab.binding.solarforecast/pom.xml index ad01bddcd0be3..7edc69dd33f17 100644 --- a/bundles/org.openhab.binding.solarforecast/pom.xml +++ b/bundles/org.openhab.binding.solarforecast/pom.xml @@ -14,11 +14,12 @@ openHAB Add-ons :: Bundles :: SolarForecast Binding - + org.json json - 20220320 + 20180813 + compile From 86ebe2bfdf617d0c6e0fda90358c675719b9c681 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Thu, 18 Aug 2022 18:27:20 +0200 Subject: [PATCH 029/111] unit corrections Signed-off-by: Bernd Weymann --- .../README.md | 34 +++++++------- .../resources/OH-INF/thing/channel-types.xml | 44 +++++++++---------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 59f19a6b5a2b6..334bc10b249da 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -285,23 +285,23 @@ Bridge solarforecast:fs-site:homeSite "ForecastSolar Home" [ location="54.321, ### Items file ```` -Number:Energy ForecastSolarHome_Actual "Actual Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-site:homeSite:actual" } -Number:Power ForecastSolarHome_Actual_Power "Actual Power Forecast [%3.f %unit%]" {channel="solarforecast:fs-site:homeSite:actual-power" } -Number:Energy ForecastSolarHome_Remaining "Remaining Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-site:homeSite:remaining" } -Number:Energy ForecastSolarHome_Today "Today Total Forecast [%3.f %unit%]" {channel="solarforecast:fs-site:homeSite:today" } -Number:Energy ForecastSolarHome_Day1 "Tomorrow Total Forecast [%3.f %unit%]" {channel="solarforecast:fs-site:homeSite:day1" } - -Number:Energy ForecastSolarHome_Actual_NE "Actual NE Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:actual" } -Number:Power ForecastSolarHome_Actual_Power_NE "Actual NE Power Forecast [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:actual-power" } -Number:Energy ForecastSolarHome_Remaining_NE "Remaining NE Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:remaining" } -Number:Energy ForecastSolarHome_Today_NE "Total NE Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:today" } -Number:Energy ForecastSolarHome_Day_NE "Tomorrow NE Forecast [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:day1" } - -Number:Energy ForecastSolarHome_Actual_SW "Actual SW Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:actual" } -Number:Power ForecastSolarHome_Actual_Power_SW "Actual SW Power Forecast [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:actual-power" } -Number:Energy ForecastSolarHome_Remaining_SW "Remaining SW Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:remaining" } -Number:Energy ForecastSolarHome_Today_SW "Total SW Forecast Today [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:today" } -Number:Energy ForecastSolarHome_Day_SW "Tomorrow SW Forecast [%3.f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:day1" } +Number:Energy ForecastSolarHome_Actual "Actual Forecast Today [%.3f %unit%]" {channel="solarforecast:fs-site:homeSite:actual" } +Number:Power ForecastSolarHome_Actual_Power "Actual Power Forecast [%.3f %unit%]" {channel="solarforecast:fs-site:homeSite:actual-power" } +Number:Energy ForecastSolarHome_Remaining "Remaining Forecast Today [%.3f %unit%]" {channel="solarforecast:fs-site:homeSite:remaining" } +Number:Energy ForecastSolarHome_Today "Today Total Forecast [%.3f %unit%]" {channel="solarforecast:fs-site:homeSite:today" } +Number:Energy ForecastSolarHome_Day1 "Tomorrow Total Forecast [%.3f %unit%]" {channel="solarforecast:fs-site:homeSite:day1" } + +Number:Energy ForecastSolarHome_Actual_NE "Actual NE Forecast Today [%.3f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:actual" } +Number:Power ForecastSolarHome_Actual_Power_NE "Actual NE Power Forecast [%.3f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:actual-power" } +Number:Energy ForecastSolarHome_Remaining_NE "Remaining NE Forecast Today [%.3f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:remaining" } +Number:Energy ForecastSolarHome_Today_NE "Total NE Forecast Today [%.3f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:today" } +Number:Energy ForecastSolarHome_Day_NE "Tomorrow NE Forecast [%.3f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:day1" } + +Number:Energy ForecastSolarHome_Actual_SW "Actual SW Forecast Today [%.3f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:actual" } +Number:Power ForecastSolarHome_Actual_Power_SW "Actual SW Power Forecast [%.3f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:actual-power" } +Number:Energy ForecastSolarHome_Remaining_SW "Remaining SW Forecast Today [%.3f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:remaining" } +Number:Energy ForecastSolarHome_Today_SW "Total SW Forecast Today [%.3f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:today" } +Number:Energy ForecastSolarHome_Day_SW "Tomorrow SW Forecast [%.3f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:day1" } ```` ### Actions rule 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 index 569f55f65ecb9..eeaafe6314b7d 100644 --- 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 @@ -8,25 +8,25 @@ Number:Energy Forecast of todays remaining production - + Number:Energy Todays production forecast till now - + Number:Power Predicted power in this moment - + Number:Energy Todays forecast in total - + String @@ -42,108 +42,108 @@ Number:Energy Tomorrows forecast in total - + Number:Energy Tomorrows pessimistic forecast - + Number:Energy Tomorrows optimistic forecast - + Number:Energy Day after tomorrow estimation - + Number:Energy Day after tomorrow pessimistic estimation - + Number:Energy Day after tomorrow optimistic estimation - + Number:Energy Day 3 estimation - + Number:Energy Day 3 pessimistic estimation - + Number:Energy Day 3 optimistic estimation - + Number:Energy Day 4 estimation - + Number:Energy Day 4 pessimistic estimation - + Number:Energy Day 4 optimistic estimation - + Number:Energy Day 5 estimation - + Number:Energy Day 5 pessimistic estimation - + Number:Energy Day 5 optimistic estimation - + Number:Energy Day 6 estimation - + Number:Energy Day 6 pessimistic estimation - + Number:Energy Day 6 optimistic estimation - + From 1bd9a81387e53f53bfab3e1dc0ecdc9aba6e668a Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Thu, 18 Aug 2022 18:29:36 +0200 Subject: [PATCH 030/111] remove unused imports Signed-off-by: Bernd Weymann --- .../org/openhab/binding/solarforecast/ForecastSolarTest.java | 1 - 1 file changed, 1 deletion(-) 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 index 8d382bbd1309d..c8ca571d1dcde 100644 --- 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 @@ -15,7 +15,6 @@ import static org.junit.jupiter.api.Assertions.*; import java.time.LocalDateTime; -import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import javax.measure.quantity.Energy; From 967e84c1700dd39a3de2aa6c1bee9ce79c7af74f Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Wed, 24 Aug 2022 20:18:38 +0200 Subject: [PATCH 031/111] add bom dependency Signed-off-by: Bernd Weymann --- bom/openhab-addons/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 44ad3d72de5b1..c66370bc9c47d 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1606,6 +1606,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 From ef14a3d541e8f2e9bd2e52f1be9e8840343c3aa8 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sat, 10 Sep 2022 00:08:58 +0200 Subject: [PATCH 032/111] Stability Forecast.Solar Signed-off-by: Bernd Weymann --- .../forecastsolar/ForecastSolarObject.java | 41 +++++----- .../internal/solcast/SolcastObject.java | 75 ++++++++++--------- .../binding/solarforecast/SolcastTest.java | 19 ++++- 3 files changed, 81 insertions(+), 54 deletions(-) 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 index d05293a43b699..5889f2b2d899b 100644 --- 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 @@ -21,6 +21,7 @@ import java.util.TreeMap; 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.actions.SolarForecast; @@ -56,20 +57,24 @@ public ForecastSolarObject(String content, LocalDateTime expirationDate) { expirationDateTime = expirationDate; if (!content.equals(SolarForecastBindingConstants.EMPTY)) { rawData = Optional.of(content); - JSONObject contentJson = new JSONObject(content); - JSONObject resultJson = contentJson.getJSONObject("result"); - JSONObject wattHourJson = resultJson.getJSONObject("watt_hours"); - JSONObject wattJson = resultJson.getJSONObject("watts"); - 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 - LocalDateTime ldt = LocalDateTime.parse(dateStr, dateInputFormatter); - wattHourMap.put(ldt, wattHourJson.getDouble(dateStr)); - wattMap.put(ldt, wattJson.getDouble(dateStr)); + try { + JSONObject contentJson = new JSONObject(content); + JSONObject resultJson = contentJson.getJSONObject("result"); + JSONObject wattHourJson = resultJson.getJSONObject("watt_hours"); + JSONObject wattJson = resultJson.getJSONObject("watts"); + 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 + LocalDateTime ldt = LocalDateTime.parse(dateStr, dateInputFormatter); + wattHourMap.put(ldt, wattHourJson.getDouble(dateStr)); + wattMap.put(ldt, wattJson.getDouble(dateStr)); + } + valid = true; + } catch (JSONException je) { + logger.info("Error parsing JSON response {} - {}", content, je.getMessage()); } - valid = true; } } @@ -113,7 +118,7 @@ public double getActualValue(LocalDateTime queryDateTime) { // ceiling date doesn't fit return UNDEF; } - } else { + } else if (f != null && c != null) { // ceiling and floor available if (f.getKey().toLocalDate().equals(queryDateTime.toLocalDate())) { if (c.getKey().toLocalDate().equals(queryDateTime.toLocalDate())) { @@ -131,7 +136,8 @@ public double getActualValue(LocalDateTime queryDateTime) { // floor invalid - ceiling not reached return 0; } - } + } // else both null - this shall not happen + return UNDEF; } public double getActualPowerValue(LocalDateTime queryDateTime) { @@ -158,7 +164,7 @@ public double getActualPowerValue(LocalDateTime queryDateTime) { // ceiling date doesn't fit return UNDEF; } - } else { + } else if (f != null && c != null) { // we're during suntime! double powerCeiling = c.getValue(); double powerFloor = f.getValue(); @@ -167,7 +173,8 @@ public double getActualPowerValue(LocalDateTime queryDateTime) { double interpolation = (queryDateTime.getMinute() - f.getKey().getMinute()) / 60.0; actualPowerValue = ((1 - interpolation) * powerFloor) + (interpolation * powerCeiling); return actualPowerValue / 1000.0; - } + } // else both null - this shall not happen + return UNDEF; } public double getDayTotal(LocalDate queryDate) { 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 index 6a941451ca990..4bc428e8738b6 100644 --- 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 @@ -97,47 +97,53 @@ private void add(String content) { // prepare data for raw channel if (contentJson.has("forecasts")) { resultJsonArray = contentJson.getJSONArray("forecasts"); + addJSONArray(resultJsonArray); rawData.get().put("forecasts", resultJsonArray); - } else { + } + if (contentJson.has("estimated_actuals")) { resultJsonArray = contentJson.getJSONArray("estimated_actuals"); + addJSONArray(resultJsonArray); rawData.get().put("estimated_actuals", resultJsonArray); } - // sort data into TreeMaps - for (int i = 0; i < resultJsonArray.length(); i++) { - JSONObject jo = resultJsonArray.getJSONObject(i); - String periodEnd = jo.getString("period_end"); - LocalDate ld = getZdtFromUTC(periodEnd).toLocalDate(); - TreeMap forecastMap = estimationDataMap.get(ld); - if (forecastMap == null) { - forecastMap = new TreeMap(); - estimationDataMap.put(ld, forecastMap); - } - forecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate")); + } + } - // fill pessimistic values - TreeMap pessimisticForecastMap = pessimisticDataMap.get(ld); - if (pessimisticForecastMap == null) { - pessimisticForecastMap = new TreeMap(); - pessimisticDataMap.put(ld, pessimisticForecastMap); - } - if (jo.has("pv_estimate10")) { - pessimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate10")); - } else { - pessimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate")); - } + 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"); + LocalDate ld = getZdtFromUTC(periodEnd).toLocalDate(); + TreeMap forecastMap = estimationDataMap.get(ld); + if (forecastMap == null) { + forecastMap = new TreeMap(); + estimationDataMap.put(ld, forecastMap); + } + forecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate")); - // fill optimistic values - TreeMap optimisticForecastMap = optimisticDataMap.get(ld); - if (optimisticForecastMap == null) { - optimisticForecastMap = new TreeMap(); - optimisticDataMap.put(ld, optimisticForecastMap); - } - if (jo.has("pv_estimate90")) { - optimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate90")); - } else { - optimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate")); - } + // fill pessimistic values + TreeMap pessimisticForecastMap = pessimisticDataMap.get(ld); + if (pessimisticForecastMap == null) { + pessimisticForecastMap = new TreeMap(); + pessimisticDataMap.put(ld, pessimisticForecastMap); + } + if (jo.has("pv_estimate10")) { + pessimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate10")); + } else { + pessimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate")); + } + + // fill optimistic values + TreeMap optimisticForecastMap = optimisticDataMap.get(ld); + if (optimisticForecastMap == null) { + optimisticForecastMap = new TreeMap(); + optimisticDataMap.put(ld, optimisticForecastMap); + } + if (jo.has("pv_estimate90")) { + optimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate90")); + } else { + optimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate")); } } } @@ -266,6 +272,7 @@ public double getDayTotal(LocalDate query, QueryMode mode) { // production during period is half of previous and next value if (endValue != null) { double addedValue = (endValue.doubleValue() + previousEstimate) / 2.0 / 2.0; + System.out.println(key + " add " + addedValue); forecastValue += addedValue; previousEstimate = endValue.doubleValue(); } else { 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 index 6b10d366e1929..9e71b8f8c3c1b 100644 --- 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 @@ -14,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.*; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; @@ -381,10 +382,9 @@ void testEnergyInterpolation() { maxDiff = Math.max(diff, maxDiff); System.out.println(hour + ":" + minute + " : " + Math.round(forecast * 1000) / 1000.0 + " - " + Math.round(productionExpected * 1000) / 1000.0 + " - " + Math.round(diff * 1000) / 1000.0); - // assertEquals(productionExpected, sco.getActualValue(now.plusMinutes(i), QueryMode.Estimation), - // 100 * TOLERANCE, "Step " + i); + assertEquals(productionExpected, sco.getActualValue(now.plusMinutes(i), QueryMode.Estimation), + 100 * TOLERANCE, "Step " + i); } - System.out.println(maxDiff); } @Test @@ -399,6 +399,19 @@ void testRawChannel() { assertTrue(joined.has("estimated_actuals"), "Actual data available"); } + @Test + void testUpdates() { + String content = FileReader.readFileInString("src/test/resources/solcast/test.json"); + ZonedDateTime now = LocalDateTime.of(2022, 7, 18, 16, 23).atZone(TEST_ZONE); + SolcastObject sco = new SolcastObject(content, now, 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"); + System.out.println(sco.getDay(LocalDate.now())); + } + @Test void testUnitDetection() { assertEquals("kW", SolcastConstants.KILOWATT_UNIT.toString(), "Kilowatt"); From 6f4e2fb7d284bdb24c7413aab720d3f7d12719eb Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Fri, 16 Sep 2022 09:45:11 +0200 Subject: [PATCH 033/111] add damping feature Signed-off-by: Bernd Weymann --- .../ForecastSolarBridgeConfiguration.java | 1 + .../forecastsolar/ForecastSolarBridgeHandler.java | 3 --- .../ForecastSolarPlaneConfiguration.java | 2 ++ .../forecastsolar/ForecastSolarPlaneHandler.java | 7 ++++--- .../internal/solcast/SolcastBridgeHandler.java | 3 --- .../internal/solcast/SolcastObject.java | 3 +-- .../internal/solcast/SolcastPlaneHandler.java | 4 ---- .../resources/OH-INF/config/fs-plane-config.xml | 10 ++++++++++ .../main/resources/OH-INF/config/fs-site-config.xml | 4 ++++ .../openhab/binding/solarforecast/SolcastTest.java | 13 ++++++------- 10 files changed, 28 insertions(+), 22 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeConfiguration.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeConfiguration.java index 17dbe49d4a8e3..4614761ce2ee9 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeConfiguration.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeConfiguration.java @@ -25,6 +25,7 @@ public class ForecastSolarBridgeConfiguration { public String location = "0.0,0.0"; public int channelRefreshInterval = -1; public String apiKey = SolarForecastBindingConstants.EMPTY; + public double inverterKwp = Double.MAX_VALUE; @Override public String toString() { diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java index 9a34de4321926..7a63ef4901fbb 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java @@ -58,7 +58,6 @@ public class ForecastSolarBridgeHandler extends BaseBridgeHandler implements Sol public ForecastSolarBridgeHandler(Bridge bridge, PointType location) { super(bridge); homeLocation = location; - logger.debug("{} Constructor", bridge.getLabel()); } @Override @@ -68,7 +67,6 @@ public Collection> getServices() { @Override public void initialize() { - logger.debug("{} initialize", thing.getLabel()); ForecastSolarBridgeConfiguration config = getConfigAs(ForecastSolarBridgeConfiguration.class); if (config.location.equals(SolarForecastBindingConstants.AUTODETECT)) { Configuration editConfig = editConfiguration(); @@ -106,7 +104,6 @@ private void startSchedule(int interval) { * Get data for all planes. Protect parts map from being modified during update */ private synchronized void getData() { - logger.debug("{} getData for {} planes", thing.getLabel(), parts.size()); if (parts.isEmpty()) { logger.debug("No PV plane defined yet"); return; diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneConfiguration.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneConfiguration.java index eeb53e3246c48..edc0b3a92e28c 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneConfiguration.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneConfiguration.java @@ -25,6 +25,8 @@ public class ForecastSolarPlaneConfiguration { public int azimuth = 360; public double kwp = 0; public int refreshInterval = -1; + public double dampAM = 0.25; + public double dampPM = 0.25; @Override public String toString() { diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java index feb75d423c2a8..31805f19f37b6 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java @@ -118,7 +118,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { * https://doc.forecast.solar/doku.php?id=api:estimate */ protected ForecastSolarObject fetchData() { - logger.debug("{} fetch data", thing.getLabel()); if (location.isPresent()) { if (!forecast.isValid()) { String url; @@ -127,13 +126,15 @@ protected ForecastSolarObject fetchData() { // https://api.forecast.solar/estimate/:lat/:lon/:dec/:az/:kwp url = BASE_URL + "estimate/" + location.get().getLatitude() + SLASH + location.get().getLongitude() + SLASH + configuration.get().declination + SLASH + configuration.get().azimuth + SLASH - + configuration.get().kwp; + + configuration.get().kwp + "?damping=" + configuration.get().dampAM + "," + + configuration.get().dampPM; } else { // use paid API // https://api.forecast.solar/:apikey/estimate/:lat/:lon/:dec/:az/:kwp url = BASE_URL + apiKey.get() + "/estimate/" + location.get().getLatitude() + SLASH + location.get().getLongitude() + SLASH + configuration.get().declination + SLASH - + configuration.get().azimuth + SLASH + configuration.get().kwp; + + configuration.get().azimuth + SLASH + configuration.get().kwp + "?damping=" + + configuration.get().dampAM + "," + configuration.get().dampPM; } logger.debug("{} Call {}", thing.getLabel(), url); try { diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java index ecb4383747af4..660f530ca956c 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java @@ -60,7 +60,6 @@ public class SolcastBridgeHandler extends BaseBridgeHandler implements SolarFore public SolcastBridgeHandler(Bridge bridge, TimeZoneProvider tzp) { super(bridge); timeZoneProvider = tzp; - logger.debug("{} Constructor", bridge.getLabel()); } @Override @@ -70,7 +69,6 @@ public Collection> getServices() { @Override public void initialize() { - logger.debug("{} initialize", thing.getLabel()); SolcastBridgeConfiguration config = getConfigAs(SolcastBridgeConfiguration.class); configuration = Optional.of(config); if (!EMPTY.equals(config.apiKey)) { @@ -116,7 +114,6 @@ public void dispose() { * Get data for all planes. Protect parts map from being modified during update */ private synchronized void getData() { - logger.debug("{} getData for {} planes", thing.getLabel(), parts.size()); if (parts.isEmpty()) { logger.debug("No PV plane defined yet"); return; 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 index 4bc428e8738b6..c9935f0312a1d 100644 --- 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 @@ -105,7 +105,6 @@ private void add(String content) { addJSONArray(resultJsonArray); rawData.get().put("estimated_actuals", resultJsonArray); } - } } @@ -272,7 +271,7 @@ public double getDayTotal(LocalDate query, QueryMode mode) { // production during period is half of previous and next value if (endValue != null) { double addedValue = (endValue.doubleValue() + previousEstimate) / 2.0 / 2.0; - System.out.println(key + " add " + addedValue); + // System.out.println(key + " add " + addedValue); forecastValue += addedValue; previousEstimate = endValue.doubleValue(); } else { diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java index 3b86d310f97a6..804ef9d3e3b4f 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java @@ -156,11 +156,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } - /** - * https://doc.forecast.solar/doku.php?id=api:estimate - */ protected SolcastObject fetchData() { - logger.debug("{} fetch data", thing.getLabel()); if (!forecast.isValid()) { String forecastUrl = String.format(FORECAST_URL, configuration.get().resourceId); String currentEstimateUrl = String.format(CURRENT_ESTIMATE_URL, configuration.get().resourceId); 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 index fc1c8fb73acaf..41eca5d17c01f 100644 --- 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 @@ -22,5 +22,15 @@ Installed Module power of this plane + + + Damping factor of morning hours + 0.25 + + + + Damping factor of evening hours + 0.25 + 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 index ce5678ebe447b..e5c2e79eac8d1 100644 --- 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 @@ -20,5 +20,9 @@ Refresh rate of channel data 1 + + + Inverter maximum Kilowatt Peak capability + 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 index 9e71b8f8c3c1b..89adce8df54a9 100644 --- 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 @@ -14,7 +14,6 @@ import static org.junit.jupiter.api.Assertions.*; -import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; @@ -374,14 +373,14 @@ void testEnergyInterpolation() { double productionExpected = 0; for (int i = 0; i < 1000; i++) { double forecast = sco.getActualValue(now.plusMinutes(i), QueryMode.Estimation); - int hour = now.plusMinutes(i).getHour(); - int minute = now.plusMinutes(i).getMinute(); + // int hour = now.plusMinutes(i).getHour(); + // int minute = now.plusMinutes(i).getMinute(); double addOnExpected = sco.getActualPowerValue(now.plusMinutes(i), QueryMode.Estimation) / 60.0; productionExpected += addOnExpected; double diff = forecast - productionExpected; maxDiff = Math.max(diff, maxDiff); - System.out.println(hour + ":" + minute + " : " + Math.round(forecast * 1000) / 1000.0 + " - " - + Math.round(productionExpected * 1000) / 1000.0 + " - " + Math.round(diff * 1000) / 1000.0); + // System.out.println(hour + ":" + minute + " : " + Math.round(forecast * 1000) / 1000.0 + " - " + // + Math.round(productionExpected * 1000) / 1000.0 + " - " + Math.round(diff * 1000) / 1000.0); assertEquals(productionExpected, sco.getActualValue(now.plusMinutes(i), QueryMode.Estimation), 100 * TOLERANCE, "Step " + i); } @@ -401,7 +400,7 @@ void testRawChannel() { @Test void testUpdates() { - String content = FileReader.readFileInString("src/test/resources/solcast/test.json"); + 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(content, now, TIMEZONEPROVIDER); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); @@ -409,7 +408,7 @@ void testUpdates() { JSONObject joined = new JSONObject(sco.getRaw()); assertTrue(joined.has("forecasts"), "Forecasts available"); assertTrue(joined.has("estimated_actuals"), "Actual data available"); - System.out.println(sco.getDay(LocalDate.now())); + // System.out.println(sco.getDay(LocalDate.now())); } @Test From 5ec22acef1e427ca01807d9b4bb7a3c8755ee6b4 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Wed, 2 Nov 2022 23:36:16 +0100 Subject: [PATCH 034/111] add horizon config Signed-off-by: Bernd Weymann --- .../README.md | 5 + .../ForecastSolarPlaneConfiguration.java | 2 + .../ForecastSolarPlaneHandler.java | 4 + .../internal/solcast/SolcastConstants.java | 3 +- .../OH-INF/config/fs-plane-config.xml | 7 + .../solarforecast/ForecastSolarTest.java | 23 + .../src/test/resources/forecastsolar/horizon | 2 + .../forecastsolar/horizon_50.556_8.495.csv | 62 ++ .../forecastsolar/horizon_50.556_8.495.json | 674 ++++++++++++++++++ .../forecastsolar/resultWithHorizon.json | 102 +++ .../forecastsolar/resultWithoutHorizon.json | 102 +++ 11 files changed, 985 insertions(+), 1 deletion(-) create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon_50.556_8.495.csv create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon_50.556_8.495.json create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/resultWithHorizon.json create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/resultWithoutHorizon.json diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 334bc10b249da..29d9eb5e541f6 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -146,6 +146,11 @@ In case of auto-detect the location configured in openHAB is obtained. Note: `channelRefreshInterval` from [Bridge Configuration](#forecastsolar-bridge-configuration) will calculate intermediate values without requesting new forecast data. +https://doc.forecast.solar/doku.php?id=damping +https://doc.forecast.solar/doku.php?id=api +https://joint-research-centre.ec.europa.eu/pvgis-photovoltaic-geographical-information-system/getting-started-pvgis/pvgis-user-manual_en#ref-2-using-horizon-information +https://re.jrc.ec.europa.eu/pvg_tools/en/ + ## ForecastSolar Channels Each `fs-plane` reports it's own values including a `raw` channel holding json content. diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneConfiguration.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneConfiguration.java index edc0b3a92e28c..422d1d1d838ae 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneConfiguration.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneConfiguration.java @@ -13,6 +13,7 @@ package org.openhab.binding.solarforecast.internal.forecastsolar; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; /** * The {@link ForecastSolarPlaneConfiguration} class contains fields mapping thing configuration parameters. @@ -27,6 +28,7 @@ public class ForecastSolarPlaneConfiguration { public int refreshInterval = -1; public double dampAM = 0.25; public double dampPM = 0.25; + public String horizon = SolarForecastBindingConstants.EMPTY; @Override public String toString() { diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java index 31805f19f37b6..5fcf63c3a1f95 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java @@ -26,6 +26,7 @@ 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.actions.SolarForecast; import org.openhab.binding.solarforecast.internal.actions.SolarForecastActions; import org.openhab.binding.solarforecast.internal.actions.SolarForecastProvider; @@ -136,6 +137,9 @@ protected ForecastSolarObject fetchData() { + 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; + } logger.debug("{} Call {}", thing.getLabel(), url); try { ContentResponse cr = httpClient.GET(url); 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 index 33a6ccb0beff6..232b0a9ef6ac5 100644 --- 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 @@ -27,7 +27,8 @@ */ @NonNullByDefault public class SolcastConstants { - public static final String FORECAST_URL = "https://api.solcast.com.au/rooftop_sites/%s/forecasts?format=json"; + // public static final String FORECAST_URL = "https://api.solcast.com.au/rooftop_sites/%s/forecasts?format=json"; + public static final String FORECAST_URL = "https://api.solcast.com.au/rooftop_sites/%s/forecasts?format=json&hours=168"; public static final String CURRENT_ESTIMATE_URL = "https://api.solcast.com.au/rooftop_sites/%s/estimated_actuals?format=json"; public static final String MEASUREMENT_URL = "https://api.solcast.com.au/rooftop_sites/%s/measurements?format=json"; 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 index 41eca5d17c01f..bdac0112cfbc9 100644 --- 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 @@ -26,11 +26,18 @@ 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/test/java/org/openhab/binding/solarforecast/ForecastSolarTest.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/ForecastSolarTest.java index c8ca571d1dcde..4424c3966a37a 100644 --- 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 @@ -14,6 +14,13 @@ import static org.junit.jupiter.api.Assertions.*; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -187,4 +194,20 @@ void testActions() { assertEquals(UnDefType.UNDEF, fo.getDay(query.toLocalDate(), "pessimistic")); assertEquals(UnDefType.UNDEF, fo.getDay(query.toLocalDate(), "total", "rubbish")); } + + @Test + public void testHorizon() throws URISyntaxException, IOException, InterruptedException { + String url = "https://api.forecast.solar/estimate/50.55598767987004/8.49558522179684/12/-40/5.525?damping=0.25,0.25"; + String horizon = "2,2,2,2,1,1,3,3,4,3,3,4,3,3,3,3,4,5,7,5,4,2,2,2,2,1,1,1,1,1,2,2,2,2,1,2"; + + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder().uri(new URI(url)).GET().build(); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + System.out.println(response.body()); + url += "?horizon=" + horizon; + request = HttpRequest.newBuilder().uri(new URI(url)).GET().build(); + response = client.send(request, BodyHandlers.ofString()); + System.out.println(response.body()); + + } } diff --git a/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon b/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon new file mode 100644 index 0000000000000..72a3d7518de96 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon @@ -0,0 +1,2 @@ +2,2,2,2,2,1,1,3,3,3,4,3,3,3,4,3,3,3,3,4,3,4,5,7,7,5,4,3,2,2,2,3,2,1,1,0,1,1,1,2,2,2,2,2,2,1,2,2 +2,2,2,2,1,1,3,3,4,3,3,4,3,3,3,3,4,5,7,5,4,2,2,2,2,1,1,1,1,1,2,2,2,2,1,2 \ No newline at end of file diff --git a/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon_50.556_8.495.csv b/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon_50.556_8.495.csv new file mode 100644 index 0000000000000..4594f2b9633ed --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon_50.556_8.495.csv @@ -0,0 +1,62 @@ +Latitude (deg.): 50.556 +Longitude (deg.): 8.495 + +A H_hor A_sun(w) H_sun(w) A_sun(s) H_sun(s) +-180.0 1.5 -180.0 0.0 -180.0 0.0 +-172.5 1.5 -165.1 0.0 -172.9 0.0 +-165.0 1.5 -151.2 0.0 -165.8 0.0 +-157.5 1.5 -138.8 0.0 -158.8 0.0 +-150.0 1.5 -128.2 0.0 -152.1 0.0 +-142.5 1.1 -119.0 0.0 -145.6 0.0 +-135.0 1.1 -110.9 0.0 -139.3 0.0 +-127.5 2.7 -103.8 0.0 -133.2 0.0 +-120.0 3.1 -97.3 0.0 -127.4 0.9 +-112.5 3.4 -91.2 0.0 -121.7 4.8 +-105.0 3.8 -85.5 0.0 -116.2 9.0 +-97.5 3.4 -80.0 0.0 -110.8 13.4 +-90.0 3.4 -74.6 0.0 -105.4 17.9 +-82.5 3.4 -69.2 0.0 -100.0 22.5 +-75.0 3.8 -63.8 0.0 -94.5 27.3 +-67.5 3.1 -58.3 0.0 -88.8 32.0 +-60.0 3.1 -52.6 0.0 -82.7 36.8 +-52.5 3.1 -46.8 2.7 -76.2 41.5 +-45.0 3.1 -40.7 6.0 -69.1 46.0 +-37.5 3.8 -34.4 8.9 -61.0 50.3 +-30.0 3.4 -27.9 11.4 -51.8 54.3 +-22.5 3.8 -21.2 13.4 -41.2 57.8 +-15.0 5.3 -14.2 14.8 -28.8 60.5 +-7.5 6.9 -7.1 15.7 -14.9 62.3 +0.0 6.9 0.0 16.0 0.0 62.9 +7.5 5.3 7.1 15.7 14.9 62.3 +15.0 3.8 14.2 14.8 28.8 60.5 +22.5 3.1 21.2 13.4 41.2 57.8 +30.0 2.3 27.9 11.4 51.8 54.3 +37.5 2.3 34.4 8.9 61.0 50.3 +45.0 2.3 40.7 6.0 69.1 46.0 +52.5 2.7 46.8 2.7 76.2 41.5 +60.0 2.3 52.6 0.0 82.7 36.8 +67.5 1.1 58.3 0.0 88.8 32.0 +75.0 0.8 63.8 0.0 94.5 27.3 +82.5 0.4 69.2 0.0 100.0 22.5 +90.0 0.8 74.6 0.0 105.4 17.9 +97.5 1.1 80.0 0.0 110.8 13.4 +105.0 1.1 85.5 0.0 116.2 9.0 +112.5 1.5 91.2 0.0 121.7 4.8 +120.0 1.9 97.3 0.0 127.4 0.9 +127.5 1.9 103.8 0.0 133.2 0.0 +135.0 1.5 110.9 0.0 139.3 0.0 +142.5 1.5 119.0 0.0 145.6 0.0 +150.0 1.5 128.2 0.0 152.1 0.0 +157.5 1.1 138.8 0.0 158.8 0.0 +165.0 1.5 151.2 0.0 165.8 0.0 +172.5 1.9 165.1 0.0 172.9 0.0 +180.0 1.5 180.0 0.0 180.0 0.0 + +A: Azimuth (0 = S, 90 = W, -90 = E) (degree) +H_hor: Horizon height (degree) +A_sun(w): Sun azimuth in the winter solstice (Dec 21) (0 = S, 90 = W, -90 = E) (degree) +H_sun(w): Sun height in the winter solstice (Dec 21) (degree) +A_sun(s): Sun azimuth in the summer solstice (June 21) (0 = S, 90 = W, -90 = E) (degree) +H_sun(s): Sun height in the summer solstice (June 21) (degree) + +PVGIS (c) European Union, 2001-2022 \ No newline at end of file diff --git a/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon_50.556_8.495.json b/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon_50.556_8.495.json new file mode 100644 index 0000000000000..72558323b0c0d --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon_50.556_8.495.json @@ -0,0 +1,674 @@ +{ + "inputs": { + "location": { + "latitude": 50.556, + "longitude": 8.495, + "elevation": 149.0 + }, + "horizon_db": "DEM-calculated" + }, + "outputs": { + "horizon_profile": [ + { + "A": -180.0, + "H_hor": 1.5 + }, + { + "A": -172.5, + "H_hor": 1.5 + }, + { + "A": -165.0, + "H_hor": 1.5 + }, + { + "A": -157.5, + "H_hor": 1.5 + }, + { + "A": -150.0, + "H_hor": 1.5 + }, + { + "A": -142.5, + "H_hor": 1.1 + }, + { + "A": -135.0, + "H_hor": 1.1 + }, + { + "A": -127.5, + "H_hor": 2.7 + }, + { + "A": -120.0, + "H_hor": 3.1 + }, + { + "A": -112.5, + "H_hor": 3.4 + }, + { + "A": -105.0, + "H_hor": 3.8 + }, + { + "A": -97.5, + "H_hor": 3.4 + }, + { + "A": -90.0, + "H_hor": 3.4 + }, + { + "A": -82.5, + "H_hor": 3.4 + }, + { + "A": -75.0, + "H_hor": 3.8 + }, + { + "A": -67.5, + "H_hor": 3.1 + }, + { + "A": -60.0, + "H_hor": 3.1 + }, + { + "A": -52.5, + "H_hor": 3.1 + }, + { + "A": -45.0, + "H_hor": 3.1 + }, + { + "A": -37.5, + "H_hor": 3.8 + }, + { + "A": -30.0, + "H_hor": 3.4 + }, + { + "A": -22.5, + "H_hor": 3.8 + }, + { + "A": -15.0, + "H_hor": 5.3 + }, + { + "A": -7.5, + "H_hor": 6.9 + }, + { + "A": 0.0, + "H_hor": 6.9 + }, + { + "A": 7.5, + "H_hor": 5.3 + }, + { + "A": 15.0, + "H_hor": 3.8 + }, + { + "A": 22.5, + "H_hor": 3.1 + }, + { + "A": 30.0, + "H_hor": 2.3 + }, + { + "A": 37.5, + "H_hor": 2.3 + }, + { + "A": 45.0, + "H_hor": 2.3 + }, + { + "A": 52.5, + "H_hor": 2.7 + }, + { + "A": 60.0, + "H_hor": 2.3 + }, + { + "A": 67.5, + "H_hor": 1.1 + }, + { + "A": 75.0, + "H_hor": 0.8 + }, + { + "A": 82.5, + "H_hor": 0.4 + }, + { + "A": 90.0, + "H_hor": 0.8 + }, + { + "A": 97.5, + "H_hor": 1.1 + }, + { + "A": 105.0, + "H_hor": 1.1 + }, + { + "A": 112.5, + "H_hor": 1.5 + }, + { + "A": 120.0, + "H_hor": 1.9 + }, + { + "A": 127.5, + "H_hor": 1.9 + }, + { + "A": 135.0, + "H_hor": 1.5 + }, + { + "A": 142.5, + "H_hor": 1.5 + }, + { + "A": 150.0, + "H_hor": 1.5 + }, + { + "A": 157.5, + "H_hor": 1.1 + }, + { + "A": 165.0, + "H_hor": 1.5 + }, + { + "A": 172.5, + "H_hor": 1.9 + }, + { + "A": 180.0, + "H_hor": 1.5 + } + ], + "winter_solstice": [ + { + "A_sun(w)": -180.0, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": -165.1, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": -151.2, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": -138.8, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": -128.2, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": -119.0, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": -110.9, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": -103.8, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": -97.3, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": -91.2, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": -85.5, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": -80.0, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": -74.6, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": -69.2, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": -63.8, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": -58.3, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": -52.6, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": -46.8, + "H_sun(w)": 2.7 + }, + { + "A_sun(w)": -40.7, + "H_sun(w)": 6.0 + }, + { + "A_sun(w)": -34.4, + "H_sun(w)": 8.9 + }, + { + "A_sun(w)": -27.9, + "H_sun(w)": 11.4 + }, + { + "A_sun(w)": -21.2, + "H_sun(w)": 13.4 + }, + { + "A_sun(w)": -14.2, + "H_sun(w)": 14.8 + }, + { + "A_sun(w)": -7.1, + "H_sun(w)": 15.7 + }, + { + "A_sun(w)": 0.0, + "H_sun(w)": 16.0 + }, + { + "A_sun(w)": 7.1, + "H_sun(w)": 15.7 + }, + { + "A_sun(w)": 14.2, + "H_sun(w)": 14.8 + }, + { + "A_sun(w)": 21.2, + "H_sun(w)": 13.4 + }, + { + "A_sun(w)": 27.9, + "H_sun(w)": 11.4 + }, + { + "A_sun(w)": 34.4, + "H_sun(w)": 8.9 + }, + { + "A_sun(w)": 40.7, + "H_sun(w)": 6.0 + }, + { + "A_sun(w)": 46.8, + "H_sun(w)": 2.7 + }, + { + "A_sun(w)": 52.6, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": 58.3, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": 63.8, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": 69.2, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": 74.6, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": 80.0, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": 85.5, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": 91.2, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": 97.3, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": 103.8, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": 110.9, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": 119.0, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": 128.2, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": 138.8, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": 151.2, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": 165.1, + "H_sun(w)": 0.0 + }, + { + "A_sun(w)": 180.0, + "H_sun(w)": 0.0 + } + ], + "summer_solstice": [ + { + "A_sun(s)": -180.0, + "H_sun(s)": 0.0 + }, + { + "A_sun(s)": -172.9, + "H_sun(s)": 0.0 + }, + { + "A_sun(s)": -165.8, + "H_sun(s)": 0.0 + }, + { + "A_sun(s)": -158.8, + "H_sun(s)": 0.0 + }, + { + "A_sun(s)": -152.1, + "H_sun(s)": 0.0 + }, + { + "A_sun(s)": -145.6, + "H_sun(s)": 0.0 + }, + { + "A_sun(s)": -139.3, + "H_sun(s)": 0.0 + }, + { + "A_sun(s)": -133.2, + "H_sun(s)": 0.0 + }, + { + "A_sun(s)": -127.4, + "H_sun(s)": 0.9 + }, + { + "A_sun(s)": -121.7, + "H_sun(s)": 4.8 + }, + { + "A_sun(s)": -116.2, + "H_sun(s)": 9.0 + }, + { + "A_sun(s)": -110.8, + "H_sun(s)": 13.4 + }, + { + "A_sun(s)": -105.4, + "H_sun(s)": 17.9 + }, + { + "A_sun(s)": -100.0, + "H_sun(s)": 22.5 + }, + { + "A_sun(s)": -94.5, + "H_sun(s)": 27.3 + }, + { + "A_sun(s)": -88.8, + "H_sun(s)": 32.0 + }, + { + "A_sun(s)": -82.7, + "H_sun(s)": 36.8 + }, + { + "A_sun(s)": -76.2, + "H_sun(s)": 41.5 + }, + { + "A_sun(s)": -69.1, + "H_sun(s)": 46.0 + }, + { + "A_sun(s)": -61.0, + "H_sun(s)": 50.3 + }, + { + "A_sun(s)": -51.8, + "H_sun(s)": 54.3 + }, + { + "A_sun(s)": -41.2, + "H_sun(s)": 57.8 + }, + { + "A_sun(s)": -28.8, + "H_sun(s)": 60.5 + }, + { + "A_sun(s)": -14.9, + "H_sun(s)": 62.3 + }, + { + "A_sun(s)": 0.0, + "H_sun(s)": 62.9 + }, + { + "A_sun(s)": 14.9, + "H_sun(s)": 62.3 + }, + { + "A_sun(s)": 28.8, + "H_sun(s)": 60.5 + }, + { + "A_sun(s)": 41.2, + "H_sun(s)": 57.8 + }, + { + "A_sun(s)": 51.8, + "H_sun(s)": 54.3 + }, + { + "A_sun(s)": 61.0, + "H_sun(s)": 50.3 + }, + { + "A_sun(s)": 69.1, + "H_sun(s)": 46.0 + }, + { + "A_sun(s)": 76.2, + "H_sun(s)": 41.5 + }, + { + "A_sun(s)": 82.7, + "H_sun(s)": 36.8 + }, + { + "A_sun(s)": 88.8, + "H_sun(s)": 32.0 + }, + { + "A_sun(s)": 94.5, + "H_sun(s)": 27.3 + }, + { + "A_sun(s)": 100.0, + "H_sun(s)": 22.5 + }, + { + "A_sun(s)": 105.4, + "H_sun(s)": 17.9 + }, + { + "A_sun(s)": 110.8, + "H_sun(s)": 13.4 + }, + { + "A_sun(s)": 116.2, + "H_sun(s)": 9.0 + }, + { + "A_sun(s)": 121.7, + "H_sun(s)": 4.8 + }, + { + "A_sun(s)": 127.4, + "H_sun(s)": 0.9 + }, + { + "A_sun(s)": 133.2, + "H_sun(s)": 0.0 + }, + { + "A_sun(s)": 139.3, + "H_sun(s)": 0.0 + }, + { + "A_sun(s)": 145.6, + "H_sun(s)": 0.0 + }, + { + "A_sun(s)": 152.1, + "H_sun(s)": 0.0 + }, + { + "A_sun(s)": 158.8, + "H_sun(s)": 0.0 + }, + { + "A_sun(s)": 165.8, + "H_sun(s)": 0.0 + }, + { + "A_sun(s)": 172.9, + "H_sun(s)": 0.0 + }, + { + "A_sun(s)": 180.0, + "H_sun(s)": 0.0 + } + ] + }, + "meta": { + "inputs": { + "location": { + "description": "Selected location", + "variables": { + "latitude": { + "description": "Latitude", + "units": "decimal degree" + }, + "longitude": { + "description": "Longitude", + "units": "decimal degree" + }, + "elevation": { + "description": "Elevation", + "units": "m" + } + } + }, + "horizon_db": { + "description": "Source of horizon data" + } + }, + "outputs": { + "horizon_profile": { + "type": "series", + "description": "Horizon profile", + "variables": { + "A": { + "description": "Azimuth (0 = S, 90 = W, -90 = E)", + "units": "degree" + }, + "H_hor": { + "description": "Horizon height", + "units": "degree" + } + } + }, + "winter_solstice": { + "type": "series", + "description": "Winter solstice", + "variables": { + "A_sun(w)": { + "description": "Sun azimuth in the winter solstice (Dec 21) (0 = S, 90 = W, -90 = E)", + "units": "degree" + }, + "H_sun(w)": { + "description": "Sun height in the winter solstice (Dec 21)", + "units": "degree" + } + } + }, + "summer_solstice": { + "type": "series", + "description": "Summer solstice profile", + "variables": { + "A_sun(s)": { + "description": "Sun azimuth in the summer solstice (June 21) (0 = S, 90 = W, -90 = E)", + "units": "degree" + }, + "H_sun(s)": { + "description": "Sun height in the summer solstice (June 21)", + "units": "degree" + } + } + } + } + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/resultWithHorizon.json b/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/resultWithHorizon.json new file mode 100644 index 0000000000000..d0342b0b3d30e --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/resultWithHorizon.json @@ -0,0 +1,102 @@ +{ + "result": { + "watts": { + "2022-11-02 07:17:00": 0, + "2022-11-02 08:00:00": 271, + "2022-11-02 09:00:00": 1077, + "2022-11-02 10:00:00": 1945, + "2022-11-02 11:00:00": 2122, + "2022-11-02 12:00:00": 2403, + "2022-11-02 13:00:00": 2508, + "2022-11-02 14:00:00": 2149, + "2022-11-02 15:00:00": 1387, + "2022-11-02 16:00:00": 663, + "2022-11-02 17:00:00": 55, + "2022-11-02 17:02:00": 0, + "2022-11-03 07:18:00": 0, + "2022-11-03 08:00:00": 50, + "2022-11-03 09:00:00": 127, + "2022-11-03 10:00:00": 166, + "2022-11-03 11:00:00": 249, + "2022-11-03 12:00:00": 296, + "2022-11-03 13:00:00": 287, + "2022-11-03 14:00:00": 199, + "2022-11-03 15:00:00": 127, + "2022-11-03 16:00:00": 66, + "2022-11-03 17:00:00": 0 + }, + "watt_hours_period": { + "2022-11-02 07:17:00": 0, + "2022-11-02 08:00:00": 97, + "2022-11-02 09:00:00": 674, + "2022-11-02 10:00:00": 1511, + "2022-11-02 11:00:00": 2034, + "2022-11-02 12:00:00": 2263, + "2022-11-02 13:00:00": 2456, + "2022-11-02 14:00:00": 2329, + "2022-11-02 15:00:00": 1768, + "2022-11-02 16:00:00": 1025, + "2022-11-02 17:00:00": 359, + "2022-11-02 17:02:00": 1, + "2022-11-03 07:18:00": 0, + "2022-11-03 08:00:00": 18, + "2022-11-03 09:00:00": 89, + "2022-11-03 10:00:00": 147, + "2022-11-03 11:00:00": 208, + "2022-11-03 12:00:00": 273, + "2022-11-03 13:00:00": 292, + "2022-11-03 14:00:00": 243, + "2022-11-03 15:00:00": 163, + "2022-11-03 16:00:00": 97, + "2022-11-03 17:00:00": 33 + }, + "watt_hours": { + "2022-11-02 07:17:00": 0, + "2022-11-02 08:00:00": 97, + "2022-11-02 09:00:00": 771, + "2022-11-02 10:00:00": 2282, + "2022-11-02 11:00:00": 4316, + "2022-11-02 12:00:00": 6579, + "2022-11-02 13:00:00": 9035, + "2022-11-02 14:00:00": 11364, + "2022-11-02 15:00:00": 13132, + "2022-11-02 16:00:00": 14157, + "2022-11-02 17:00:00": 14516, + "2022-11-02 17:02:00": 14517, + "2022-11-03 07:18:00": 0, + "2022-11-03 08:00:00": 18, + "2022-11-03 09:00:00": 107, + "2022-11-03 10:00:00": 254, + "2022-11-03 11:00:00": 462, + "2022-11-03 12:00:00": 735, + "2022-11-03 13:00:00": 1027, + "2022-11-03 14:00:00": 1270, + "2022-11-03 15:00:00": 1433, + "2022-11-03 16:00:00": 1530, + "2022-11-03 17:00:00": 1563 + }, + "watt_hours_day": { + "2022-11-02": 14517, + "2022-11-03": 1563 + } + }, + "message": { + "code": 0, + "type": "success", + "text": "", + "info": { + "latitude": 50.556, + "longitude": 8.4956, + "distance": 0, + "place": "4b, Uferstra\u00dfe, Neustadt, Wetzlar, Lahn-Dill-Kreis, Hessen, 35576, Deutschland", + "timezone": "Europe/Berlin", + "time": "2022-11-02T23:27:03+01:00", + "time_utc": "2022-11-02T22:27:03+00:00" + }, + "ratelimit": { + "period": 3600, + "limit": 12, + "remaining": 5 + } + } +} diff --git a/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/resultWithoutHorizon.json b/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/resultWithoutHorizon.json new file mode 100644 index 0000000000000..295827a422dba --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/resultWithoutHorizon.json @@ -0,0 +1,102 @@ +{ + "result": { + "watts": { + "2022-11-02 07:17:00": 0, + "2022-11-02 08:00:00": 315, + "2022-11-02 09:00:00": 1122, + "2022-11-02 10:00:00": 1867, + "2022-11-02 11:00:00": 2006, + "2022-11-02 12:00:00": 2326, + "2022-11-02 13:00:00": 2528, + "2022-11-02 14:00:00": 2155, + "2022-11-02 15:00:00": 1365, + "2022-11-02 16:00:00": 597, + "2022-11-02 17:00:00": 17, + "2022-11-02 17:02:00": 0, + "2022-11-03 07:18:00": 0, + "2022-11-03 08:00:00": 33, + "2022-11-03 09:00:00": 88, + "2022-11-03 10:00:00": 155, + "2022-11-03 11:00:00": 265, + "2022-11-03 12:00:00": 398, + "2022-11-03 13:00:00": 398, + "2022-11-03 14:00:00": 265, + "2022-11-03 15:00:00": 127, + "2022-11-03 16:00:00": 39, + "2022-11-03 17:00:00": 0 + }, + "watt_hours_period": { + "2022-11-02 07:17:00": 0, + "2022-11-02 08:00:00": 113, + "2022-11-02 09:00:00": 719, + "2022-11-02 10:00:00": 1495, + "2022-11-02 11:00:00": 1937, + "2022-11-02 12:00:00": 2166, + "2022-11-02 13:00:00": 2427, + "2022-11-02 14:00:00": 2342, + "2022-11-02 15:00:00": 1760, + "2022-11-02 16:00:00": 981, + "2022-11-02 17:00:00": 307, + "2022-11-02 17:02:00": 0, + "2022-11-03 07:18:00": 0, + "2022-11-03 08:00:00": 12, + "2022-11-03 09:00:00": 61, + "2022-11-03 10:00:00": 122, + "2022-11-03 11:00:00": 210, + "2022-11-03 12:00:00": 332, + "2022-11-03 13:00:00": 398, + "2022-11-03 14:00:00": 332, + "2022-11-03 15:00:00": 196, + "2022-11-03 16:00:00": 83, + "2022-11-03 17:00:00": 20 + }, + "watt_hours": { + "2022-11-02 07:17:00": 0, + "2022-11-02 08:00:00": 113, + "2022-11-02 09:00:00": 832, + "2022-11-02 10:00:00": 2327, + "2022-11-02 11:00:00": 4264, + "2022-11-02 12:00:00": 6430, + "2022-11-02 13:00:00": 8857, + "2022-11-02 14:00:00": 11199, + "2022-11-02 15:00:00": 12959, + "2022-11-02 16:00:00": 13940, + "2022-11-02 17:00:00": 14247, + "2022-11-02 17:02:00": 14247, + "2022-11-03 07:18:00": 0, + "2022-11-03 08:00:00": 12, + "2022-11-03 09:00:00": 73, + "2022-11-03 10:00:00": 195, + "2022-11-03 11:00:00": 405, + "2022-11-03 12:00:00": 737, + "2022-11-03 13:00:00": 1135, + "2022-11-03 14:00:00": 1467, + "2022-11-03 15:00:00": 1663, + "2022-11-03 16:00:00": 1746, + "2022-11-03 17:00:00": 1766 + }, + "watt_hours_day": { + "2022-11-02": 14247, + "2022-11-03": 1766 + } + }, + "message": { + "code": 0, + "type": "success", + "text": "", + "info": { + "latitude": 50.556, + "longitude": 8.4956, + "distance": 0, + "place": "4b, Uferstra\u00dfe, Neustadt, Wetzlar, Lahn-Dill-Kreis, Hessen, 35576, Deutschland", + "timezone": "Europe/Berlin", + "time": "2022-11-02T23:27:02+01:00", + "time_utc": "2022-11-02T22:27:02+00:00" + }, + "ratelimit": { + "period": 3600, + "limit": 12, + "remaining": 6 + } + } +} From ef8d94c7f556100f22f84342808fb2f57e754033 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sat, 5 Nov 2022 17:24:25 +0100 Subject: [PATCH 035/111] readme adjustment Signed-off-by: Bernd Weymann --- .../README.md | 34 +++++++++++++------ .../ForecastSolarPlaneConfiguration.java | 4 +-- .../OH-INF/config/fs-plane-config.xml | 4 +-- .../solarforecast/ForecastSolarTest.java | 4 ++- .../test/resources/forecastsolar/result.txt | 4 +++ 5 files changed, 35 insertions(+), 15 deletions(-) create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/result.txt diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 29d9eb5e541f6..a2661e93337d2 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -134,22 +134,36 @@ In case of auto-detect the location configured in openHAB is obtained. ### ForecastSolar Plane Configuration -| Name | Type | Description | Default | Required | -|-----------------|---------|------------------------------------------------------------------------------|---------|----------| -| refreshInterval | integer | Forecast Refresh Interval in minutes | 30 | yes | -| declination | integer | Plane Declination: 0 for horizontal till 90 for vertical declination | N/A | yes | -| azimuth | integer | Plane Azimuth: -180 = north, -90 = east, 0 = south, 90 = west, 180 = north | N/A | yes | -| kwp | decimal | Installed Kilowatt Peak | N/A | yes | +| 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. Note: `channelRefreshInterval` from [Bridge Configuration](#forecastsolar-bridge-configuration) will calculate intermediate values without requesting new forecast data. -https://doc.forecast.solar/doku.php?id=damping -https://doc.forecast.solar/doku.php?id=api -https://joint-research-centre.ec.europa.eu/pvgis-photovoltaic-geographical-information-system/getting-started-pvgis/pvgis-user-manual_en#ref-2-using-horizon-information -https://re.jrc.ec.europa.eu/pvg_tools/en/ +#### 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 diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneConfiguration.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneConfiguration.java index 422d1d1d838ae..cadf9a3aaeb5b 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneConfiguration.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneConfiguration.java @@ -26,8 +26,8 @@ public class ForecastSolarPlaneConfiguration { public int azimuth = 360; public double kwp = 0; public int refreshInterval = -1; - public double dampAM = 0.25; - public double dampPM = 0.25; + public double dampAM = 0; + public double dampPM = 0; public String horizon = SolarForecastBindingConstants.EMPTY; @Override 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 index bdac0112cfbc9..f20d18c153840 100644 --- 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 @@ -22,13 +22,13 @@ Installed Module power of this plane - + Damping factor of morning hours 0.25 true - + Damping factor of evening hours 0.25 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 index 4424c3966a37a..31c10dee00fbb 100644 --- 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 @@ -197,16 +197,18 @@ void testActions() { @Test public void testHorizon() throws URISyntaxException, IOException, InterruptedException { - String url = "https://api.forecast.solar/estimate/50.55598767987004/8.49558522179684/12/-40/5.525?damping=0.25,0.25"; + String url = "https://api.forecast.solar/estimate/50.55598767987004/8.49558522179684/12/-40/5.525"; String horizon = "2,2,2,2,1,1,3,3,4,3,3,4,3,3,3,3,4,5,7,5,4,2,2,2,2,1,1,1,1,1,2,2,2,2,1,2"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder().uri(new URI(url)).GET().build(); HttpResponse response = client.send(request, BodyHandlers.ofString()); + System.out.println(url); System.out.println(response.body()); url += "?horizon=" + horizon; request = HttpRequest.newBuilder().uri(new URI(url)).GET().build(); response = client.send(request, BodyHandlers.ofString()); + System.out.println(url); System.out.println(response.body()); } diff --git a/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/result.txt b/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/result.txt new file mode 100644 index 0000000000000..73c0de3eeba47 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/result.txt @@ -0,0 +1,4 @@ +https://api.forecast.solar/estimate/50.55598767987004/8.49558522179684/12/-40/5.525 +{"result":{"watts":{"2022-11-05 07:22:00":0,"2022-11-05 08:00:00":520,"2022-11-05 09:00:00":1426,"2022-11-05 10:00:00":2226,"2022-11-05 11:00:00":2379,"2022-11-05 12:00:00":2613,"2022-11-05 13:00:00":2596,"2022-11-05 14:00:00":2321,"2022-11-05 15:00:00":1775,"2022-11-05 16:00:00":712,"2022-11-05 16:57:00":0,"2022-11-06 07:23:00":0,"2022-11-06 08:00:00":361,"2022-11-06 09:00:00":842,"2022-11-06 10:00:00":994,"2022-11-06 11:00:00":1061,"2022-11-06 12:00:00":968,"2022-11-06 13:00:00":901,"2022-11-06 14:00:00":874,"2022-11-06 15:00:00":454,"2022-11-06 16:00:00":113,"2022-11-06 16:55:00":0},"watt_hours_period":{"2022-11-05 07:22:00":0,"2022-11-05 08:00:00":165,"2022-11-05 09:00:00":973,"2022-11-05 10:00:00":1826,"2022-11-05 11:00:00":2303,"2022-11-05 12:00:00":2496,"2022-11-05 13:00:00":2605,"2022-11-05 14:00:00":2459,"2022-11-05 15:00:00":2048,"2022-11-05 16:00:00":1244,"2022-11-05 16:57:00":338,"2022-11-06 07:23:00":0,"2022-11-06 08:00:00":111,"2022-11-06 09:00:00":602,"2022-11-06 10:00:00":918,"2022-11-06 11:00:00":1028,"2022-11-06 12:00:00":1015,"2022-11-06 13:00:00":935,"2022-11-06 14:00:00":888,"2022-11-06 15:00:00":664,"2022-11-06 16:00:00":284,"2022-11-06 16:55:00":52},"watt_hours":{"2022-11-05 07:22:00":0,"2022-11-05 08:00:00":165,"2022-11-05 09:00:00":1138,"2022-11-05 10:00:00":2964,"2022-11-05 11:00:00":5267,"2022-11-05 12:00:00":7763,"2022-11-05 13:00:00":10368,"2022-11-05 14:00:00":12827,"2022-11-05 15:00:00":14875,"2022-11-05 16:00:00":16119,"2022-11-05 16:57:00":16457,"2022-11-06 07:23:00":0,"2022-11-06 08:00:00":111,"2022-11-06 09:00:00":713,"2022-11-06 10:00:00":1631,"2022-11-06 11:00:00":2659,"2022-11-06 12:00:00":3674,"2022-11-06 13:00:00":4609,"2022-11-06 14:00:00":5497,"2022-11-06 15:00:00":6161,"2022-11-06 16:00:00":6445,"2022-11-06 16:55:00":6497},"watt_hours_day":{"2022-11-05":16457,"2022-11-06":6497}},"message":{"code":0,"type":"success","text":"","info":{"latitude":50.556,"longitude":8.4956,"distance":0,"place":"4b, Uferstra\u00dfe, Neustadt, Wetzlar, Lahn-Dill-Kreis, Hessen, 35576, Deutschland","timezone":"Europe/Berlin","time":"2022-11-05T16:11:37+01:00","time_utc":"2022-11-05T15:11:37+00:00"},"ratelimit":{"period":3600,"limit":12,"remaining":9}}} +https://api.forecast.solar/estimate/50.55598767987004/8.49558522179684/12/-40/5.525?horizon=2,2,2,2,1,1,3,3,4,3,3,4,3,3,3,3,4,5,7,5,4,2,2,2,2,1,1,1,1,1,2,2,2,2,1,2 +{"result":{"watts":{"2022-11-05 07:22:00":0,"2022-11-05 08:00:00":1434,"2022-11-05 09:00:00":2215,"2022-11-05 10:00:00":2561,"2022-11-05 11:00:00":2588,"2022-11-05 12:00:00":2577,"2022-11-05 13:00:00":2345,"2022-11-05 14:00:00":1736,"2022-11-05 15:00:00":1026,"2022-11-05 16:00:00":305,"2022-11-05 16:57:00":0,"2022-11-06 07:23:00":0,"2022-11-06 08:00:00":1028,"2022-11-06 09:00:00":1262,"2022-11-06 10:00:00":1231,"2022-11-06 11:00:00":1201,"2022-11-06 12:00:00":1042,"2022-11-06 13:00:00":739,"2022-11-06 14:00:00":672,"2022-11-06 15:00:00":278,"2022-11-06 16:00:00":51,"2022-11-06 16:55:00":0},"watt_hours_period":{"2022-11-05 07:22:00":0,"2022-11-05 08:00:00":454,"2022-11-05 09:00:00":1825,"2022-11-05 10:00:00":2388,"2022-11-05 11:00:00":2575,"2022-11-05 12:00:00":2583,"2022-11-05 13:00:00":2461,"2022-11-05 14:00:00":2041,"2022-11-05 15:00:00":1381,"2022-11-05 16:00:00":666,"2022-11-05 16:57:00":145,"2022-11-06 07:23:00":0,"2022-11-06 08:00:00":317,"2022-11-06 09:00:00":1145,"2022-11-06 10:00:00":1247,"2022-11-06 11:00:00":1216,"2022-11-06 12:00:00":1122,"2022-11-06 13:00:00":891,"2022-11-06 14:00:00":706,"2022-11-06 15:00:00":475,"2022-11-06 16:00:00":165,"2022-11-06 16:55:00":23},"watt_hours":{"2022-11-05 07:22:00":0,"2022-11-05 08:00:00":454,"2022-11-05 09:00:00":2279,"2022-11-05 10:00:00":4667,"2022-11-05 11:00:00":7242,"2022-11-05 12:00:00":9825,"2022-11-05 13:00:00":12286,"2022-11-05 14:00:00":14327,"2022-11-05 15:00:00":15708,"2022-11-05 16:00:00":16374,"2022-11-05 16:57:00":16519,"2022-11-06 07:23:00":0,"2022-11-06 08:00:00":317,"2022-11-06 09:00:00":1462,"2022-11-06 10:00:00":2709,"2022-11-06 11:00:00":3925,"2022-11-06 12:00:00":5047,"2022-11-06 13:00:00":5938,"2022-11-06 14:00:00":6644,"2022-11-06 15:00:00":7119,"2022-11-06 16:00:00":7284,"2022-11-06 16:55:00":7307},"watt_hours_day":{"2022-11-05":16519,"2022-11-06":7307}},"message":{"code":0,"type":"success","text":"","info":{"latitude":50.556,"longitude":8.4956,"distance":0,"place":"4b, Uferstra\u00dfe, Neustadt, Wetzlar, Lahn-Dill-Kreis, Hessen, 35576, Deutschland","timezone":"Europe/Berlin","time":"2022-11-05T16:11:38+01:00","time_utc":"2022-11-05T15:11:38+00:00"},"ratelimit":{"period":3600,"limit":12,"remaining":8}}} From 6c14255f4e66d915480c097d03614c49505da717 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sun, 6 Nov 2022 22:48:57 +0100 Subject: [PATCH 036/111] spotless Signed-off-by: Bernd Weymann --- .../main/resources/OH-INF/config/fs-plane-config.xml | 12 ++++++------ .../binding/solarforecast/ForecastSolarTest.java | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) 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 index f20d18c153840..3bb50b0cf0f93 100644 --- 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 @@ -32,12 +32,12 @@ Damping factor of evening hours 0.25 - true - - - - Horizon definition as comma separated integer values - true + true + + + + Horizon definition as comma separated integer values + true 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 index 31c10dee00fbb..89594479428bf 100644 --- 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 @@ -210,6 +210,5 @@ public void testHorizon() throws URISyntaxException, IOException, InterruptedExc response = client.send(request, BodyHandlers.ofString()); System.out.println(url); System.out.println(response.body()); - } } From f9fd4367a06efc7623c4d7ee293205acba0a1cfd Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 6 Jun 2023 20:20:33 +0200 Subject: [PATCH 037/111] packages for config and handlers Signed-off-by: Bernd Weymann --- .../internal/SolarForecastHandlerFactory.java | 8 ++++---- .../{ => config}/ForecastSolarBridgeConfiguration.java | 2 +- .../{ => config}/ForecastSolarPlaneConfiguration.java | 2 +- .../{ => handler}/ForecastSolarBridgeHandler.java | 4 +++- .../{ => handler}/ForecastSolarPlaneHandler.java | 4 +++- .../solcast/{ => config}/SolcastBridgeConfiguration.java | 2 +- .../solcast/{ => config}/SolcastPlaneConfiguration.java | 2 +- .../solcast/{ => handler}/SolcastBridgeHandler.java | 4 +++- .../solcast/{ => handler}/SolcastPlaneHandler.java | 4 +++- 9 files changed, 20 insertions(+), 12 deletions(-) rename bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/{ => config}/ForecastSolarBridgeConfiguration.java (99%) rename bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/{ => config}/ForecastSolarPlaneConfiguration.java (99%) rename bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/{ => handler}/ForecastSolarBridgeHandler.java (97%) rename bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/{ => handler}/ForecastSolarPlaneHandler.java (97%) rename bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/{ => config}/SolcastBridgeConfiguration.java (92%) rename bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/{ => config}/SolcastPlaneConfiguration.java (93%) rename bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/{ => handler}/SolcastBridgeHandler.java (97%) rename bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/{ => handler}/SolcastPlaneHandler.java (98%) 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 index 0d360ccd122a3..73c676991438a 100644 --- 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 @@ -19,10 +19,10 @@ 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.ForecastSolarBridgeHandler; -import org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarPlaneHandler; -import org.openhab.binding.solarforecast.internal.solcast.SolcastBridgeHandler; -import org.openhab.binding.solarforecast.internal.solcast.SolcastPlaneHandler; +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; diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeConfiguration.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/config/ForecastSolarBridgeConfiguration.java similarity index 99% rename from bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeConfiguration.java rename to bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/config/ForecastSolarBridgeConfiguration.java index 4614761ce2ee9..ffef1cca86add 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeConfiguration.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/config/ForecastSolarBridgeConfiguration.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.solarforecast.internal.forecastsolar; +package org.openhab.binding.solarforecast.internal.forecastsolar.config; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneConfiguration.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/config/ForecastSolarPlaneConfiguration.java similarity index 99% rename from bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneConfiguration.java rename to bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/config/ForecastSolarPlaneConfiguration.java index cadf9a3aaeb5b..0bb37cc808855 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneConfiguration.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/config/ForecastSolarPlaneConfiguration.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.solarforecast.internal.forecastsolar; +package org.openhab.binding.solarforecast.internal.forecastsolar.config; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarBridgeHandler.java similarity index 97% rename from bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java rename to bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarBridgeHandler.java index 7a63ef4901fbb..e8aa060b76cba 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarBridgeHandler.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.solarforecast.internal.forecastsolar; +package org.openhab.binding.solarforecast.internal.forecastsolar.handler; import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; @@ -29,6 +29,8 @@ 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.utils.Utils; import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.PointType; diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarPlaneHandler.java similarity index 97% rename from bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java rename to bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarPlaneHandler.java index 5fcf63c3a1f95..fe0775ea9354d 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarPlaneHandler.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.solarforecast.internal.forecastsolar; +package org.openhab.binding.solarforecast.internal.forecastsolar.handler; import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; import static org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarConstants.BASE_URL; @@ -30,6 +30,8 @@ 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.utils.Utils; import org.openhab.core.library.types.PointType; import org.openhab.core.library.types.StringType; diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeConfiguration.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/config/SolcastBridgeConfiguration.java similarity index 92% rename from bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeConfiguration.java rename to bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/config/SolcastBridgeConfiguration.java index a7531c8c9d4b1..4a97f850af98c 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeConfiguration.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/config/SolcastBridgeConfiguration.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.solarforecast.internal.solcast; +package org.openhab.binding.solarforecast.internal.solcast.config; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneConfiguration.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/config/SolcastPlaneConfiguration.java similarity index 93% rename from bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneConfiguration.java rename to bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/config/SolcastPlaneConfiguration.java index 55476d4881794..7128619770c43 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneConfiguration.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/config/SolcastPlaneConfiguration.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.solarforecast.internal.solcast; +package org.openhab.binding.solarforecast.internal.solcast.config; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastBridgeHandler.java similarity index 97% rename from bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java rename to bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastBridgeHandler.java index 660f530ca956c..298835b511b8b 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastBridgeHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastBridgeHandler.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.solarforecast.internal.solcast; +package org.openhab.binding.solarforecast.internal.solcast.handler; import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; @@ -29,7 +29,9 @@ 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.thing.Bridge; diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastPlaneHandler.java similarity index 98% rename from bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java rename to bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastPlaneHandler.java index 804ef9d3e3b4f..7f70e12c6db86 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastPlaneHandler.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.solarforecast.internal.solcast; +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.*; @@ -37,7 +37,9 @@ 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.i18n.TimeZoneProvider; import org.openhab.core.items.Item; From 058ae60d5b6d094eb17056bd97236f93e1af5ead Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 6 Jun 2023 20:43:46 +0200 Subject: [PATCH 038/111] updateStatuss using text keys Signed-off-by: Bernd Weymann --- .../forecastsolar/handler/ForecastSolarPlaneHandler.java | 9 ++++++--- .../internal/solcast/handler/SolcastBridgeHandler.java | 5 ++--- .../internal/solcast/handler/SolcastPlaneHandler.java | 9 ++++++--- .../main/resources/OH-INF/i18n/solarforecast.properties | 6 ++++++ 4 files changed, 20 insertions(+), 9 deletions(-) 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 index fe0775ea9354d..c35cf9ef5e834 100644 --- 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 @@ -91,13 +91,16 @@ public void initialize() { updateStatus(ThingStatus.ONLINE); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Wrong Handler " + handler); + "@text/solarforecast.site.status.wrong-handler" + " [\"" + handler + "\"]"); + } } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, "BridgeHandler not found"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "@text/solarforecast.site.status.bridge-handler-not-found"); } } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, "Bridge not set"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "@text/solarforecast.site.status.bridge-missing"); } } 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 index 298835b511b8b..37911219cbe76 100644 --- 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 @@ -77,14 +77,13 @@ public void initialize() { updateStatus(ThingStatus.ONLINE); startSchedule(configuration.get().channelRefreshInterval); } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "API Key is mandatory"); - logger.info("API Key missing"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/solarforecast.site.status.api-key-missing"); } } @Override public void handleCommand(ChannelUID channelUID, Command command) { - logger.trace("Handle command {} for channel {}", channelUID, command); if (command instanceof RefreshType) { getData(); } 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 index 7f70e12c6db86..5d186566892fe 100644 --- 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 @@ -132,13 +132,16 @@ public void initialize() { updateStatus(ThingStatus.ONLINE); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Wrong Handler " + handler); + "@text/solarforecast.site.status.wrong-handler" + " [\"" + handler + "\"]"); + } } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, "BridgeHandler not found"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "@text/solarforecast.site.status.bridge-handler-not-found"); } } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, "Bridge not set"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "@text/solarforecast.site.status.bridge-missing"); } } 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 index 462207fca3fc5..0bd79ad8b68ce 100644 --- 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 @@ -97,6 +97,12 @@ channel-type.solarforecast.remaining-channel.description = Forecast of todays re channel-type.solarforecast.today-channel.label = Forecast Today channel-type.solarforecast.today-channel.description = Todays forecast in total +# status details +solarforecast.site.status.api-key-missing = API Key is mandatory +solarforecast.plane.status.bridge-missing = Bridge not set +solarforecast.plane.status.bridge-handler-not-found = BridgeHandler not found +solarforecast.plane.status.wrong-handler = Wrong Handler {0} + # thing actions actionDayLabel = Daily Energy Production actionDayDesc = Get energy production for complete day From 7d100831ba7dea56fec01ebbeb118a3c99440cee Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Wed, 7 Jun 2023 12:58:34 +0200 Subject: [PATCH 039/111] Using Instant for Internal time measurements Signed-off-by: Bernd Weymann --- .../forecastsolar/ForecastSolarObject.java | 9 +++-- .../ForecastSolarPlaneConfiguration.java | 2 +- .../handler/ForecastSolarPlaneHandler.java | 5 ++- .../internal/solcast/SolcastObject.java | 8 ++-- .../config/SolcastPlaneConfiguration.java | 2 +- .../solcast/handler/SolcastPlaneHandler.java | 6 ++- .../solarforecast/ForecastSolarTest.java | 39 ++++++++++--------- .../binding/solarforecast/SolcastTest.java | 22 +++++------ 8 files changed, 49 insertions(+), 44 deletions(-) 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 index 5889f2b2d899b..5b9e290d36376 100644 --- 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 @@ -12,6 +12,7 @@ */ package org.openhab.binding.solarforecast.internal.forecastsolar; +import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -47,13 +48,13 @@ public class ForecastSolarObject implements SolarForecast { private Optional rawData = Optional.empty(); private boolean valid = false; - private LocalDateTime expirationDateTime; + private Instant expirationDateTime; public ForecastSolarObject() { - expirationDateTime = LocalDateTime.now(); + expirationDateTime = Instant.now(); } - public ForecastSolarObject(String content, LocalDateTime expirationDate) { + public ForecastSolarObject(String content, Instant expirationDate) { expirationDateTime = expirationDate; if (!content.equals(SolarForecastBindingConstants.EMPTY)) { rawData = Optional.of(content); @@ -81,7 +82,7 @@ public ForecastSolarObject(String content, LocalDateTime expirationDate) { public boolean isValid() { if (valid) { if (!wattHourMap.isEmpty()) { - if (expirationDateTime.isAfter(LocalDateTime.now())) { + if (expirationDateTime.isAfter(Instant.now())) { return true; } else { logger.debug("Forecast data expired"); 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 index 0bb37cc808855..d8636824cc358 100644 --- 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 @@ -25,7 +25,7 @@ public class ForecastSolarPlaneConfiguration { public int declination = -1; public int azimuth = 360; public double kwp = 0; - public int refreshInterval = -1; + public long refreshInterval = -1; public double dampAM = 0; public double dampPM = 0; public String horizon = SolarForecastBindingConstants.EMPTY; 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 index c35cf9ef5e834..379227f004b8d 100644 --- 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 @@ -15,7 +15,9 @@ import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; import static org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarConstants.BASE_URL; +import java.time.Instant; import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -150,8 +152,7 @@ protected ForecastSolarObject fetchData() { ContentResponse cr = httpClient.GET(url); if (cr.getStatus() == 200) { ForecastSolarObject localForecast = new ForecastSolarObject(cr.getContentAsString(), - - LocalDateTime.now().plusMinutes(configuration.get().refreshInterval)); + Instant.now().plus(configuration.get().refreshInterval, ChronoUnit.MINUTES)); setForecast(localForecast); logger.debug("Fetched new data. {} {} HTTP errors since last successful update", thing.getLabel(), failureCounter); 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 index c9935f0312a1d..5f6f07e4e32de 100644 --- 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 @@ -51,7 +51,7 @@ public class SolcastObject implements SolarForecast { private final TimeZoneProvider timeZoneProvider; private Optional rawData = Optional.of(new JSONObject()); - private ZonedDateTime expirationDateTime; + private Instant expirationDateTime; private boolean valid = false; public enum QueryMode { @@ -75,10 +75,10 @@ public String toString() { public SolcastObject(TimeZoneProvider tzp) { // invalid forecast object timeZoneProvider = tzp; - expirationDateTime = ZonedDateTime.now(timeZoneProvider.getTimeZone()); + expirationDateTime = Instant.now(); } - public SolcastObject(String content, ZonedDateTime expiration, TimeZoneProvider tzp) { + public SolcastObject(String content, Instant expiration, TimeZoneProvider tzp) { expirationDateTime = expiration; timeZoneProvider = tzp; add(content); @@ -150,7 +150,7 @@ private void addJSONArray(JSONArray resultJsonArray) { public boolean isValid() { if (valid) { if (!estimationDataMap.isEmpty()) { - if (expirationDateTime.isAfter(ZonedDateTime.now(timeZoneProvider.getTimeZone()))) { + if (expirationDateTime.isAfter(Instant.now())) { return true; } else { logger.debug("Forecast data expired"); 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 index 7128619770c43..e533e5df5d062 100644 --- 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 @@ -24,6 +24,6 @@ public class SolcastPlaneConfiguration { public String resourceId = SolarForecastBindingConstants.EMPTY; public String powerItem = SolarForecastBindingConstants.EMPTY; - public int refreshInterval = -1; + public long refreshInterval = -1; public String powerUnit = SolarForecastBindingConstants.AUTODETECT; } 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 index 5d186566892fe..1f0849a334669 100644 --- 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 @@ -15,9 +15,11 @@ import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; import static org.openhab.binding.solarforecast.internal.solcast.SolcastConstants.*; +import java.time.Instant; import java.time.LocalDate; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -172,8 +174,8 @@ protected SolcastObject fetchData() { estimateRequest.header(HttpHeader.AUTHORIZATION, BEARER + bridgeHandler.get().getApiKey()); ContentResponse crEstimate = estimateRequest.send(); if (crEstimate.getStatus() == 200) { - SolcastObject localForecast = new SolcastObject(crEstimate.getContentAsString(), ZonedDateTime - .now(timeZoneProvider.getTimeZone()).plusMinutes(configuration.get().refreshInterval), + SolcastObject localForecast = new SolcastObject(crEstimate.getContentAsString(), + Instant.now().plus(configuration.get().refreshInterval, ChronoUnit.MINUTES), timeZoneProvider); logger.trace("{} Fetched data {}", thing.getLabel(), localForecast.toString()); 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 index 89594479428bf..c41492adbf558 100644 --- 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 @@ -22,6 +22,7 @@ import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.time.LocalDateTime; +import java.time.ZoneId; import java.time.format.DateTimeFormatter; import javax.measure.quantity.Energy; @@ -42,14 +43,15 @@ */ @NonNullByDefault class ForecastSolarTest { - // double comparison tolerance = 1 Watt private static final double TOLERANCE = 0.001; + public static final ZoneId TEST_ZONE = ZoneId.of("Europe/Berlin"); @Test void testForecastObject() { String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); LocalDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 17, 00); - ForecastSolarObject fo = new ForecastSolarObject(content, queryDateTime); + ForecastSolarObject fo = new ForecastSolarObject(content, + queryDateTime.toInstant(TEST_ZONE.getRules().getOffset(queryDateTime))); // "2022-07-17 21:32:00": 63583, assertEquals(63.583, fo.getDayTotal(queryDateTime.toLocalDate()), TOLERANCE, "Total production"); // "2022-07-17 17:00:00": 52896, @@ -72,7 +74,8 @@ void testForecastObject() { void testActualPower() { String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); LocalDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 10, 00); - ForecastSolarObject fo = new ForecastSolarObject(content, queryDateTime); + ForecastSolarObject fo = new ForecastSolarObject(content, + queryDateTime.toInstant(TEST_ZONE.getRules().getOffset(queryDateTime))); // "2022-07-17 10:00:00": 4874, assertEquals(4.874, fo.getActualPowerValue(queryDateTime), TOLERANCE, "Actual estimation"); @@ -85,7 +88,8 @@ void testActualPower() { void testInterpolation() { String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); LocalDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 0); - ForecastSolarObject fo = new ForecastSolarObject(content, queryDateTime); + ForecastSolarObject fo = new ForecastSolarObject(content, + queryDateTime.toInstant(TEST_ZONE.getRules().getOffset(queryDateTime))); // test steady value increase double previousValue = 0; @@ -106,7 +110,8 @@ void testInterpolation() { void testForecastSum() { String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); LocalDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 23); - ForecastSolarObject fo = new ForecastSolarObject(content, queryDateTime); + ForecastSolarObject fo = new ForecastSolarObject(content, + queryDateTime.toInstant(TEST_ZONE.getRules().getOffset(queryDateTime))); QuantityType actual = QuantityType.valueOf(0, Units.KILOWATT_HOUR); State st = Utils.getEnergyState(fo.getActualValue(queryDateTime)); assertTrue(st instanceof QuantityType); @@ -130,7 +135,7 @@ void testCornerCases() { // 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); - fo = new ForecastSolarObject(content, query); + fo = new ForecastSolarObject(content, query.toInstant(TEST_ZONE.getRules().getOffset(query))); assertEquals(-1.0, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope"); assertEquals(-1.0, fo.getActualValue(query), TOLERANCE, "Actual out of scope"); assertEquals(-1.0, fo.getRemainingProduction(query), TOLERANCE, "Remain out of scope"); @@ -173,29 +178,29 @@ void testCornerCases() { @Test void testActions() { String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); - LocalDateTime query = LocalDateTime.of(2022, 7, 17, 16, 23); - ForecastSolarObject fo = new ForecastSolarObject(content, query); + LocalDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 23); + ForecastSolarObject fo = new ForecastSolarObject(content, + queryDateTime.toInstant(TEST_ZONE.getRules().getOffset(queryDateTime))); assertEquals("2022-07-17T05:31:00", fo.getForecastBegin().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), "Forecast begin"); assertEquals("2022-07-18T21:31:00", fo.getForecastEnd().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), "Forecast end"); assertEquals(QuantityType.valueOf(63.583, Units.KILOWATT_HOUR).toString(), - fo.getDay(query.toLocalDate()).toFullString(), "Actual out of scope"); + fo.getDay(queryDateTime.toLocalDate()).toFullString(), "Actual out of scope"); - query = LocalDateTime.of(2022, 7, 17, 0, 0); + queryDateTime = LocalDateTime.of(2022, 7, 17, 0, 0); // "watt_hours_day": { // "2022-07-17": 63583, // "2022-07-18": 65554 // } assertEquals(QuantityType.valueOf(129.137, Units.KILOWATT_HOUR).toString(), - fo.getEnergy(query, query.plusDays(2)).toFullString(), "Actual out of scope"); + fo.getEnergy(queryDateTime, queryDateTime.plusDays(2)).toFullString(), "Actual out of scope"); - assertEquals(UnDefType.UNDEF, fo.getDay(query.toLocalDate(), "optimistic")); - assertEquals(UnDefType.UNDEF, fo.getDay(query.toLocalDate(), "pessimistic")); - assertEquals(UnDefType.UNDEF, fo.getDay(query.toLocalDate(), "total", "rubbish")); + assertEquals(UnDefType.UNDEF, fo.getDay(queryDateTime.toLocalDate(), "optimistic")); + assertEquals(UnDefType.UNDEF, fo.getDay(queryDateTime.toLocalDate(), "pessimistic")); + assertEquals(UnDefType.UNDEF, fo.getDay(queryDateTime.toLocalDate(), "total", "rubbish")); } - @Test public void testHorizon() throws URISyntaxException, IOException, InterruptedException { String url = "https://api.forecast.solar/estimate/50.55598767987004/8.49558522179684/12/-40/5.525"; String horizon = "2,2,2,2,1,1,3,3,4,3,3,4,3,3,3,3,4,5,7,5,4,2,2,2,2,1,1,1,1,1,2,2,2,2,1,2"; @@ -203,12 +208,8 @@ public void testHorizon() throws URISyntaxException, IOException, InterruptedExc HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder().uri(new URI(url)).GET().build(); HttpResponse response = client.send(request, BodyHandlers.ofString()); - System.out.println(url); - System.out.println(response.body()); url += "?horizon=" + horizon; request = HttpRequest.newBuilder().uri(new URI(url)).GET().build(); response = client.send(request, BodyHandlers.ofString()); - System.out.println(url); - System.out.println(response.body()); } } 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 index 89adce8df54a9..eac4bb3b1274b 100644 --- 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 @@ -97,7 +97,7 @@ class SolcastTest { 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(content, now, TIMEZONEPROVIDER); + SolcastObject scfo = new SolcastObject(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 @@ -126,7 +126,7 @@ void testForecastObject() { 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(content, now, TIMEZONEPROVIDER); + SolcastObject scfo = new SolcastObject(content, now.toInstant(), TIMEZONEPROVIDER); content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); scfo.join(content); @@ -233,7 +233,7 @@ void testPower() { 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(content, now, TIMEZONEPROVIDER); + SolcastObject scfo = new SolcastObject(content, now.toInstant(), TIMEZONEPROVIDER); assertEquals(0.42, scfo.getActualValue(now, QueryMode.Estimation), TOLERANCE, "Actual estimation"); assertEquals(25.413, scfo.getDayTotal(now.toLocalDate(), QueryMode.Estimation), TOLERANCE, "Day total"); } @@ -242,7 +242,7 @@ void testForecastTreeMap() { 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(content, now, TIMEZONEPROVIDER); + SolcastObject scfo = new SolcastObject(content, now.toInstant(), TIMEZONEPROVIDER); assertEquals(-1.0, scfo.getActualValue(now, QueryMode.Estimation), 0.01, "Invalid"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); @@ -257,7 +257,7 @@ void testJoin() { 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(content, now, TIMEZONEPROVIDER); + SolcastObject scfo = new SolcastObject(content, now.toInstant(), TIMEZONEPROVIDER); assertEquals(-1.0, scfo.getActualValue(now, QueryMode.Estimation), 0.01, "Invalid"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); @@ -287,7 +287,7 @@ void testActions() { 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(content, now, TIMEZONEPROVIDER); + SolcastObject scfo = new SolcastObject(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.Estimation), TOLERANCE, @@ -316,7 +316,7 @@ void testOptimisticPessimistic() { void testInavlid() { String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); ZonedDateTime now = ZonedDateTime.now(TEST_ZONE); - SolcastObject scfo = new SolcastObject(content, now, TIMEZONEPROVIDER); + SolcastObject scfo = new SolcastObject(content, now.toInstant(), TIMEZONEPROVIDER); assertEquals(-1.0, scfo.getActualValue(now, QueryMode.Estimation), 0.01, "Data available - day not in"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); @@ -346,7 +346,7 @@ void testTimeframes() { 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(content, now, TIMEZONEPROVIDER); + SolcastObject sco = new SolcastObject(content, now.toInstant(), TIMEZONEPROVIDER); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); sco.join(content); @@ -365,7 +365,7 @@ void testPowerInterpolation() { 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(content, now, TIMEZONEPROVIDER); + SolcastObject sco = new SolcastObject(content, now.toInstant(), TIMEZONEPROVIDER); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); sco.join(content); @@ -390,7 +390,7 @@ void testEnergyInterpolation() { 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(content, now, TIMEZONEPROVIDER); + SolcastObject sco = new SolcastObject(content, now.toInstant(), TIMEZONEPROVIDER); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); sco.join(content); JSONObject joined = new JSONObject(sco.getRaw()); @@ -402,7 +402,7 @@ void testRawChannel() { 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(content, now, TIMEZONEPROVIDER); + SolcastObject sco = new SolcastObject(content, now.toInstant(), TIMEZONEPROVIDER); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); sco.join(content); JSONObject joined = new JSONObject(sco.getRaw()); From c492ffbef1c51820137bcf81e8b27b348bff756b Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Wed, 7 Jun 2023 13:03:40 +0200 Subject: [PATCH 040/111] checkstyle warnings solved Signed-off-by: Bernd Weymann --- .../handler/ForecastSolarPlaneHandler.java | 1 - .../solcast/handler/SolcastPlaneHandler.java | 1 - .../solarforecast/ForecastSolarTest.java | 31 ++++++++----------- 3 files changed, 13 insertions(+), 20 deletions(-) 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 index 379227f004b8d..4c8ad06acf9ed 100644 --- 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 @@ -94,7 +94,6 @@ public void initialize() { } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, "@text/solarforecast.site.status.wrong-handler" + " [\"" + handler + "\"]"); - } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, 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 index 1f0849a334669..427f691643df0 100644 --- 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 @@ -135,7 +135,6 @@ public void initialize() { } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, "@text/solarforecast.site.status.wrong-handler" + " [\"" + handler + "\"]"); - } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, 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 index c41492adbf558..81a5c8cfefcfd 100644 --- 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 @@ -14,13 +14,6 @@ import static org.junit.jupiter.api.Assertions.*; -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.net.http.HttpResponse.BodyHandlers; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; @@ -201,15 +194,17 @@ void testActions() { assertEquals(UnDefType.UNDEF, fo.getDay(queryDateTime.toLocalDate(), "total", "rubbish")); } - public void testHorizon() throws URISyntaxException, IOException, InterruptedException { - String url = "https://api.forecast.solar/estimate/50.55598767987004/8.49558522179684/12/-40/5.525"; - String horizon = "2,2,2,2,1,1,3,3,4,3,3,4,3,3,3,3,4,5,7,5,4,2,2,2,2,1,1,1,1,1,2,2,2,2,1,2"; - - HttpClient client = HttpClient.newHttpClient(); - HttpRequest request = HttpRequest.newBuilder().uri(new URI(url)).GET().build(); - HttpResponse response = client.send(request, BodyHandlers.ofString()); - url += "?horizon=" + horizon; - request = HttpRequest.newBuilder().uri(new URI(url)).GET().build(); - response = client.send(request, BodyHandlers.ofString()); - } + /* + * public void testHorizon() throws URISyntaxException, IOException, InterruptedException { + * String url = "https://api.forecast.solar/estimate/50.55598767987004/8.49558522179684/12/-40/5.525"; + * String horizon = "2,2,2,2,1,1,3,3,4,3,3,4,3,3,3,3,4,5,7,5,4,2,2,2,2,1,1,1,1,1,2,2,2,2,1,2"; + * + * HttpClient client = HttpClient.newHttpClient(); + * HttpRequest request = HttpRequest.newBuilder().uri(new URI(url)).GET().build(); + * HttpResponse response = client.send(request, BodyHandlers.ofString()); + * url += "?horizon=" + horizon; + * request = HttpRequest.newBuilder().uri(new URI(url)).GET().build(); + * response = client.send(request, BodyHandlers.ofString()); + * } + */ } From 23fd3f345ff6f9fe61493c987f86b35d6ba930b6 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Thu, 8 Jun 2023 15:37:23 +0200 Subject: [PATCH 041/111] logging rework Signed-off-by: Bernd Weymann --- .../actions/SolarForecastActions.java | 68 ++++++++++++------- .../forecastsolar/ForecastSolarConstants.java | 27 -------- .../forecastsolar/ForecastSolarObject.java | 10 +-- .../handler/ForecastSolarBridgeHandler.java | 4 +- .../handler/ForecastSolarPlaneHandler.java | 18 ++--- .../internal/solcast/SolcastObject.java | 10 +-- .../solcast/handler/SolcastPlaneHandler.java | 43 ++++-------- 7 files changed, 65 insertions(+), 115 deletions(-) delete mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarConstants.java 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 index e540f94596c19..de64fb8006bd8 100644 --- 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 @@ -14,7 +14,6 @@ import java.time.LocalDate; import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.Iterator; import java.util.List; import java.util.Optional; @@ -54,25 +53,28 @@ public State getDay( String... args) { if (thingHandler.isPresent()) { List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); - logger.trace("Found {} SolarForecast entries", l.size()); if (!l.isEmpty()) { QuantityType measure = QuantityType.valueOf(0, Units.KILOWATT_HOUR); for (Iterator iterator = l.iterator(); iterator.hasNext();) { SolarForecast solarForecast = iterator.next(); State s = solarForecast.getDay(localDate, args); - logger.trace("Found measure {}", s); if (s instanceof QuantityType) { measure = measure.add((QuantityType) s); } else { // break in case of failure getting values to avoid ambiguous values - logger.debug("Found measure {} - break", s); + logger.trace("Ambiguous measure {} found for {} - return UNDEF", s, localDate); return UnDefType.UNDEF; } } return measure; + } else { + logger.trace("No forecasts found for {} - return UNDEF", localDate); + return UnDefType.UNDEF; } + } else { + logger.trace("Handler missing - return UNDEF"); + return UnDefType.UNDEF; } - return UnDefType.UNDEF; } @RuleAction(label = "@text/actionPowerLabel", description = "@text/actionPowerDesc") @@ -81,25 +83,28 @@ public State getPower( String... args) { if (thingHandler.isPresent()) { List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); - logger.trace("Found {} SolarForecast entries", l.size()); if (!l.isEmpty()) { QuantityType measure = QuantityType.valueOf(0, MetricPrefix.KILO(Units.WATT)); for (Iterator iterator = l.iterator(); iterator.hasNext();) { SolarForecast solarForecast = iterator.next(); State s = solarForecast.getPower(localDateTime, args); - logger.trace("Found measure {}", s); if (s instanceof QuantityType) { measure = measure.add((QuantityType) s); } else { // break in case of failure getting values to avoid ambiguous values - logger.debug("Found measure {} - break", s); + logger.trace("Ambiguous measure {} found for {} - return UNDEF", s, localDateTime); return UnDefType.UNDEF; } } return measure; + } else { + logger.trace("No forecasts found for {} - return UNDEF", localDateTime); + return UnDefType.UNDEF; } + } else { + logger.trace("Handler missing - return UNDEF"); + return UnDefType.UNDEF; } - return UnDefType.UNDEF; } @RuleAction(label = "@text/actionEnergyLabel", description = "@text/actionEnergyDesc") @@ -109,57 +114,64 @@ public State getEnergy( String... args) { if (thingHandler.isPresent()) { List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); - logger.trace("Found {} SolarForecast entries", l.size()); if (!l.isEmpty()) { QuantityType measure = QuantityType.valueOf(0, Units.KILOWATT_HOUR); for (Iterator iterator = l.iterator(); iterator.hasNext();) { SolarForecast solarForecast = iterator.next(); State s = solarForecast.getEnergy(localDateTimeBegin, localDateTimeEnd, args); - logger.trace("Found measure {}", s); if (s instanceof QuantityType) { measure = measure.add((QuantityType) s); } else { // break in case of failure getting values to avoid ambiguous values - logger.debug("Found measure {} - break", s); + logger.trace("Ambiguous measure {} found between {} and {} - return UNDEF", s, + localDateTimeBegin, localDateTimeEnd); return UnDefType.UNDEF; } } return measure; + } else { + logger.trace("No forecasts found for between {} and {} - return UNDEF", localDateTimeBegin, + localDateTimeEnd); + return UnDefType.UNDEF; } + } else { + logger.trace("Handler missing - return UNDEF"); + return UnDefType.UNDEF; } - return UnDefType.UNDEF; } @RuleAction(label = "@text/actionForecastBeginLabel", description = "@text/actionForecastBeginDesc") public LocalDateTime getForecastBegin() { - LocalDateTime returnLdt = LocalDateTime.MIN; + LocalDateTime returnLdt = LocalDateTime.MAX; if (thingHandler.isPresent()) { List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); - logger.trace("Found {} SolarForecast entries", l.size()); if (!l.isEmpty()) { for (Iterator iterator = l.iterator(); iterator.hasNext();) { SolarForecast solarForecast = iterator.next(); LocalDateTime forecastLdt = solarForecast.getForecastBegin(); - logger.trace("Found {}", forecastLdt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); // break in case of failure getting values to avoid ambiguous values - if (forecastLdt.equals(LocalDateTime.MIN)) { - return LocalDateTime.MIN; + if (forecastLdt.equals(LocalDateTime.MAX)) { + return LocalDateTime.MAX; } // take latest possible timestamp to avoid ambiguous values - if (forecastLdt.isAfter(returnLdt)) { + if (forecastLdt.isBefore(returnLdt)) { returnLdt = forecastLdt; - logger.trace("Set {}", forecastLdt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); } } return returnLdt; + } else { + logger.trace("No forecasts found - return invalid date MAX"); + return LocalDateTime.MAX; } + } else { + logger.trace("Handler missing - return invalid date MAX"); + return LocalDateTime.MAX; } - return LocalDateTime.MIN; } @RuleAction(label = "@text/actionForecastEndLabel", description = "@text/actionForecastEndDesc") public LocalDateTime getForecastEnd() { - LocalDateTime returnLdt = LocalDateTime.MAX; + LocalDateTime returnLdt = LocalDateTime.MIN; if (thingHandler.isPresent()) { List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); if (!l.isEmpty()) { @@ -168,17 +180,22 @@ public LocalDateTime getForecastEnd() { LocalDateTime forecastLdt = solarForecast.getForecastEnd(); // break in case of failure getting values to avoid ambiguous values if (forecastLdt.equals(LocalDateTime.MIN)) { - return LocalDateTime.MAX; + return LocalDateTime.MIN; } // take earliest possible timestamp to avoid ambiguous values - if (forecastLdt.isBefore(returnLdt)) { + if (forecastLdt.isAfter(returnLdt)) { returnLdt = forecastLdt; } } return returnLdt; + } else { + logger.trace("No forecasts found - return invalid date MIN"); + return LocalDateTime.MIN; } + } else { + logger.trace("Handler missing - return invalid date MIN"); + return LocalDateTime.MIN; } - return LocalDateTime.MAX; } public static State getDay(ThingActions actions, LocalDate ld, String... args) { @@ -203,7 +220,6 @@ public static LocalDateTime getForecastEnd(ThingActions actions) { @Override public void setThingHandler(ThingHandler handler) { - logger.trace("ThingHandler {} set", handler.toString()); thingHandler = Optional.of(handler); } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarConstants.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarConstants.java deleted file mode 100644 index afcbd9511493c..0000000000000 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarConstants.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2010-2022 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 org.eclipse.jdt.annotation.NonNullByDefault; - -/** - * The {@link ForecastSolarConstants} class defines common constants, which are - * used across the whole binding. - * - * @author Bernd Weymann - Initial contribution - */ -@NonNullByDefault -public class ForecastSolarConstants { - - public static final String BASE_URL = "https://api.forecast.solar/"; -} 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 index 5b9e290d36376..23987dabe5649 100644 --- 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 @@ -74,7 +74,7 @@ public ForecastSolarObject(String content, Instant expirationDate) { } valid = true; } catch (JSONException je) { - logger.info("Error parsing JSON response {} - {}", content, je.getMessage()); + logger.debug("Error parsing JSON response {} - {}", content, je.getMessage()); } } } @@ -84,14 +84,8 @@ public boolean isValid() { if (!wattHourMap.isEmpty()) { if (expirationDateTime.isAfter(Instant.now())) { return true; - } else { - logger.debug("Forecast data expired"); } - } else { - logger.debug("Empty data map"); } - } else { - logger.debug("No Forecast data available"); } return false; } @@ -219,7 +213,6 @@ public State getDay(LocalDate localDate, String... args) { return UnDefType.UNDEF; } double measure = getDayTotal(localDate); - logger.trace("Actions: deliver measure {}", measure); return Utils.getEnergyState(measure); } @@ -260,7 +253,6 @@ public State getPower(LocalDateTime localDateTime, String... args) { return UnDefType.UNDEF; } double measure = getActualPowerValue(localDateTime); - logger.trace("Actions: deliver measure {}", measure); return Utils.getPowerState(measure); } 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 index e8aa060b76cba..72ecd6c94970b 100644 --- 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 @@ -103,7 +103,7 @@ private void startSchedule(int interval) { } /** - * Get data for all planes. Protect parts map from being modified during update + * Get data for all planes. Synchronized to protect parts map from being modified during update */ private synchronized void getData() { if (parts.isEmpty()) { @@ -164,8 +164,6 @@ public synchronized List getSolarForecasts() { List l = new ArrayList(); parts.forEach(entry -> { l.addAll(entry.getSolarForecasts()); - logger.trace("Actions: {} forecast added", entry.getSolarForecasts().size()); - }); 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 index 4c8ad06acf9ed..031e8b376736a 100644 --- 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 @@ -13,7 +13,6 @@ package org.openhab.binding.solarforecast.internal.forecastsolar.handler; import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; -import static org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarConstants.BASE_URL; import java.time.Instant; import java.time.LocalDateTime; @@ -57,6 +56,8 @@ */ @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; @@ -70,7 +71,6 @@ public class ForecastSolarPlaneHandler extends BaseThingHandler implements Solar public ForecastSolarPlaneHandler(Thing thing, HttpClient hc) { super(thing); httpClient = hc; - logger.debug("{} Constructor", thing.getLabel()); } @Override @@ -80,7 +80,6 @@ public Collection> getServices() { @Override public void initialize() { - logger.debug("{} initialize", thing.getLabel()); ForecastSolarPlaneConfiguration c = getConfigAs(ForecastSolarPlaneConfiguration.class); configuration = Optional.of(c); Bridge bridge = getBridge(); @@ -115,7 +114,6 @@ public void dispose() { @Override public void handleCommand(ChannelUID channelUID, Command command) { - logger.trace("Handle command {} for channel {}", channelUID, command); if (command instanceof RefreshType) { fetchData(); } @@ -146,27 +144,22 @@ protected ForecastSolarObject fetchData() { if (!SolarForecastBindingConstants.EMPTY.equals(configuration.get().horizon)) { url += "?horizon=" + configuration.get().horizon; } - logger.debug("{} Call {}", thing.getLabel(), url); try { ContentResponse cr = httpClient.GET(url); if (cr.getStatus() == 200) { ForecastSolarObject localForecast = new ForecastSolarObject(cr.getContentAsString(), Instant.now().plus(configuration.get().refreshInterval, ChronoUnit.MINUTES)); setForecast(localForecast); - logger.debug("Fetched new data. {} {} HTTP errors since last successful update", - thing.getLabel(), failureCounter); failureCounter = 0; updateState(CHANNEL_RAW, StringType.valueOf(cr.getContentAsString())); } else { - logger.debug("{} Call {} failed {}", thing.getLabel(), url, cr.getStatus()); + logger.info("{} Call {} failed {}", thing.getLabel(), url, cr.getStatus()); failureCounter++; } } catch (InterruptedException | ExecutionException | TimeoutException e) { - logger.debug("{} Call {} failed {}", thing.getLabel(), url, e.getMessage()); + logger.info("{} Call {} failed {}", thing.getLabel(), url, e.getMessage()); } - } else { - logger.trace("{} use available forecast", thing.getLabel()); - } + } // else use available forecast updateChannels(forecast); } else { logger.info("{} Location not present", thing.getLabel()); @@ -204,7 +197,6 @@ void setApiKey(String key) { } private synchronized void setForecast(ForecastSolarObject f) { - logger.debug("Forecast set - valid? {}", f.isValid()); forecast = f; } 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 index 5f6f07e4e32de..9be2a7176c9e1 100644 --- 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 @@ -152,14 +152,8 @@ public boolean isValid() { if (!estimationDataMap.isEmpty()) { if (expirationDateTime.isAfter(Instant.now())) { return true; - } else { - logger.debug("Forecast data expired"); } - } else { - logger.debug("Empty data map"); } - } else { - logger.debug("No Forecast data available"); } return false; } @@ -186,7 +180,7 @@ public double getActualValue(ZonedDateTime query, QueryMode mode) { forecastValue += addedValue; previousEstimate = endValue.doubleValue(); } else { - logger.info("No estimation found for {}", key); + logger.trace("No estimation found for {}", key); } } } @@ -275,7 +269,7 @@ public double getDayTotal(LocalDate query, QueryMode mode) { forecastValue += addedValue; previousEstimate = endValue.doubleValue(); } else { - logger.info("No estimation found for {}", key); + logger.trace("No estimation found for {}", key); } } return forecastValue; 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 index 427f691643df0..2a939c74505a7 100644 --- 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 @@ -96,7 +96,6 @@ public SolcastPlaneHandler(Thing thing, HttpClient hc, Optional> getServices() { @Override public void initialize() { - logger.debug("{} initialize", thing.getLabel()); SolcastPlaneConfiguration c = getConfigAs(SolcastPlaneConfiguration.class); configuration = Optional.of(c); @@ -117,10 +115,10 @@ public void initialize() { if (item != null) { powerItem = Optional.of(item); } else { - logger.info("Item {} not found", c.powerItem); + logger.debug("Item {} not found", c.powerItem); } } else { - logger.debug("No Power item configured"); + logger.trace("No Power item configured"); } // connect Bridge & Status @@ -156,7 +154,6 @@ public void dispose() { @Override public void handleCommand(ChannelUID channelUID, Command command) { - logger.trace("Handle command {} for channel {}", channelUID, command); if (command instanceof RefreshType) { fetchData(); } @@ -166,7 +163,6 @@ protected SolcastObject fetchData() { if (!forecast.isValid()) { String forecastUrl = String.format(FORECAST_URL, configuration.get().resourceId); String currentEstimateUrl = String.format(CURRENT_ESTIMATE_URL, configuration.get().resourceId); - logger.debug("{} Call {}", thing.getLabel(), currentEstimateUrl); try { // get actual estimate Request estimateRequest = httpClient.newRequest(currentEstimateUrl); @@ -176,10 +172,8 @@ protected SolcastObject fetchData() { SolcastObject localForecast = new SolcastObject(crEstimate.getContentAsString(), Instant.now().plus(configuration.get().refreshInterval, ChronoUnit.MINUTES), timeZoneProvider); - logger.trace("{} Fetched data {}", thing.getLabel(), localForecast.toString()); // get forecast - logger.debug("{} Call {}", thing.getLabel(), forecastUrl); Request forecastRequest = httpClient.newRequest(forecastUrl); forecastRequest.header(HttpHeader.AUTHORIZATION, BEARER + bridgeHandler.get().getApiKey()); ContentResponse crForecast = forecastRequest.send(); @@ -188,21 +182,18 @@ protected SolcastObject fetchData() { localForecast.join(crForecast.getContentAsString()); setForecast(localForecast); updateState(CHANNEL_RAW, StringType.valueOf(forecast.getRaw())); - logger.trace("{} Fetched data {}", thing.getLabel(), forecast.toString()); } else { - logger.info("{} Call {} failed {}", thing.getLabel(), forecastUrl, crForecast.getStatus()); + logger.debug("{} Call {} failed {}", thing.getLabel(), forecastUrl, crForecast.getStatus()); } } else { - logger.info("{} Call {} failed {}", thing.getLabel(), currentEstimateUrl, crEstimate.getStatus()); + logger.debug("{} Call {} failed {}", thing.getLabel(), currentEstimateUrl, crEstimate.getStatus()); } } catch (InterruptedException | ExecutionException | TimeoutException e) { - logger.info("{} Call {} failed {}", thing.getLabel(), currentEstimateUrl, e.getMessage()); + logger.debug("{} Call {} failed {}", thing.getLabel(), currentEstimateUrl, e.getMessage()); } - } else { - logger.trace("{} use available forecast", thing.getLabel()); - } + } // else use available forecast updateChannels(forecast); - if (ZonedDateTime.now(timeZoneProvider.getTimeZone()).isAfter(nextMeasurement)) { + if (ZonedDateTime.now(timeZoneProvider.getTimeZone()).isAfter(nextMeasurement)) { // [Todo] switch to Instant sendMeasure(); } return forecast; @@ -214,7 +205,6 @@ protected SolcastObject fetchData() { private void sendMeasure() { State updateState = UnDefType.UNDEF; if (persistenceService.isPresent() && powerItem.isPresent()) { - logger.debug("Get item {}", configuration.get().powerItem); ZonedDateTime beginPeriodDT = nextMeasurement.minusMinutes(MEASURE_INTERVAL_MIN); ZonedDateTime endPeriodDT = nextMeasurement; FilterCriteria fc = new FilterCriteria(); @@ -225,7 +215,6 @@ private void sendMeasure() { int count = 0; double total = 0; for (HistoricItem historicItem : historicItems) { - // logger.info("Found {} item with average {} power", historicItem.getTimestamp(), // historicItem.getState().toFullString()); DecimalType dt = historicItem.getState().as(DecimalType.class); if (dt != null) { @@ -248,11 +237,11 @@ private void sendMeasure() { // just round and keep 3 digits after comma power = Math.round(power * 1000.0) / 1000.0; } else { - logger.info("No Power unit detected - result is {}", unitDetected.toString()); + logger.trace("No valid Power unit detected {}", unitDetected.toString()); power = UNDEF_DOUBLE; } } else { - logger.info("No autodetection for State class {} possible", state.getClass()); + logger.trace("No autodetection for State class {} possible", state.getClass()); power = UNDEF_DOUBLE; } } else if (Units.WATT.toString().equals(configuration.get().powerUnit)) { @@ -262,19 +251,18 @@ private void sendMeasure() { // just round and keep 3 digits after comma power = Math.round(power * 1000.0) / 1000.0; } else { - logger.info("No Unit conversion possible for {}", configuration.get().powerUnit); + logger.trace("No Unit conversion possible for {}", configuration.get().powerUnit); power = UNDEF_DOUBLE; } if (power >= 0) { - logger.debug("Found {} items with average {} power", count, total / count); JSONObject measureObject = new JSONObject(); JSONObject measure = new JSONObject(); measure.put("period_end", endPeriodDT.format(DateTimeFormatter.ISO_INSTANT)); measure.put("period", "PT" + MEASURE_INTERVAL_MIN + "M"); measure.put("total_power", power); measureObject.put("measurement", measure); - logger.debug("Send {}", measureObject.toString()); + logger.trace("Send {}", measureObject.toString()); String measureUrl = String.format(MEASUREMENT_URL, configuration.get().resourceId); Request request = httpClient.POST(measureUrl); @@ -284,18 +272,16 @@ private void sendMeasure() { try { ContentResponse crMeasure = request.send(); if (crMeasure.getStatus() == 200) { - logger.debug("{} Call {} finished {}", thing.getLabel(), measureUrl, - crMeasure.getContentAsString()); updateState = StringType.valueOf(crMeasure.getContentAsString()); } else { - logger.info("{} Call {} failed {} - {}", thing.getLabel(), measureUrl, crMeasure.getStatus(), + logger.debug("{} Call {} failed {} - {}", thing.getLabel(), measureUrl, crMeasure.getStatus(), crMeasure.getContentAsString()); } } catch (InterruptedException | TimeoutException | ExecutionException e) { - logger.info("{} Call {} failed {}", thing.getLabel(), measureUrl, e.getMessage()); + logger.debug("{} Call {} failed {}", thing.getLabel(), measureUrl, e.getMessage()); } } else { - logger.info("Persistence empty"); + logger.debug("Persistence for {} empty", powerItem); } } updateState(CHANNEL_RAW_TUNING, updateState); @@ -330,7 +316,6 @@ private void updateChannels(SolcastObject f) { } private synchronized void setForecast(SolcastObject f) { - logger.debug("{} Forecast set", thing.getLabel()); forecast = f; } From 5a1c1ec24778388e2bcc85546468733e03b4eb8f Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Thu, 8 Jun 2023 15:39:48 +0200 Subject: [PATCH 042/111] remove unused failure counter Signed-off-by: Bernd Weymann --- .../forecastsolar/handler/ForecastSolarPlaneHandler.java | 3 --- 1 file changed, 3 deletions(-) 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 index 031e8b376736a..706081f8570e1 100644 --- 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 @@ -66,7 +66,6 @@ public class ForecastSolarPlaneHandler extends BaseThingHandler implements Solar private Optional location = Optional.empty(); private Optional apiKey = Optional.empty(); private ForecastSolarObject forecast = new ForecastSolarObject(); - private int failureCounter = 0; public ForecastSolarPlaneHandler(Thing thing, HttpClient hc) { super(thing); @@ -150,11 +149,9 @@ protected ForecastSolarObject fetchData() { ForecastSolarObject localForecast = new ForecastSolarObject(cr.getContentAsString(), Instant.now().plus(configuration.get().refreshInterval, ChronoUnit.MINUTES)); setForecast(localForecast); - failureCounter = 0; updateState(CHANNEL_RAW, StringType.valueOf(cr.getContentAsString())); } else { logger.info("{} Call {} failed {}", thing.getLabel(), url, cr.getStatus()); - failureCounter++; } } catch (InterruptedException | ExecutionException | TimeoutException e) { logger.info("{} Call {} failed {}", thing.getLabel(), url, e.getMessage()); From c54ef69a07f1aef479aadcbef5bd676e3b651a86 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Thu, 8 Jun 2023 17:57:24 +0200 Subject: [PATCH 043/111] Using Instant for sendMeasure timings Signed-off-by: Bernd Weymann --- .../README.md | 18 ++++++++-------- .../internal/solcast/SolcastObject.java | 21 +++++++++---------- .../solcast/handler/SolcastPlaneHandler.java | 13 ++++++------ .../solarforecast/internal/utils/Utils.java | 12 +++++++++-- .../binding/solarforecast/SolcastTest.java | 21 ++++++++++++------- 5 files changed, 49 insertions(+), 36 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index a2661e93337d2..9147e1c73ee81 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -36,7 +36,7 @@ Each service needs one `xx-site` for your location and at least one photovoltaic [Solcast service](https://solcast.com/) requires a personal registration with an email 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. -After configuration the necessary information is available. +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. @@ -47,13 +47,13 @@ You've the opportunity to [send your own measurements back to Solcast API](https This data is used internally to improve the forecast for your specific site. Configuration and channels can be set after checking the *Show advanced* checkbox. You need an item which reports the electric power for the specific rooftop. -If this item isn't set no measures will be sent. +If item isn't set no measures will be sent. As described in [Solcast Rooftop Measurement](https://legacy-docs.solcast.com.au/#measurements-rooftop-site) check in beforehand if your measures are *sane*. -- item is delivering good values and they are stored in persistence +- item is delivering correct values and they are stored in persistence - time settings in openHAB are correct in order to so measurements are matching to the measure time frame -After the measurement is sent the `raw-tuning` channel is reporting the result. +After measurement is sent the `raw-tuning` channel is reporting the result. ### Solcast Bridge Configuration @@ -83,7 +83,7 @@ Note: `channelRefreshInterval` from [Bridge Configuration](#solcast-bridge-confi `powerItem` shall reflect the power for this specific rooftop. It's an optional setting and the [measure is sent to Solcast API in order to tune the forecast](https://legacy-docs.solcast.com.au/#measurements-rooftop-site) in the future. -If you don't want to sent measures to Solcast leave this configuration item empty. +If you don't want to send measures to Solcast leave this configuration item empty. `powerUnit` is set to `auto-detect`. In case the `powerItem` is delivering a valid `QuantityType` state this setting is fine. @@ -91,7 +91,7 @@ If the item delivers a raw number without unit please select `powerUnit` accordi ## Solcast Channels -Each `sc-plane` reports it's own values including a `raw` channel holding json content. +Each `sc-plane` reports its own values including a `raw` channel holding json content. The `sc-site` bridge sums up all attached `sc-plane` values and provides the total forecast for your home location. Channels are covering today's actual data with current, remaining and today's total prediction. @@ -117,7 +117,7 @@ Day*X* channels are referring to forecasts plus *X* days: 1 = tomorrow, 2 = day ## 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 pre-conditions. +You can try it without any registration or other preconditions. ### ForecastSolar Bridge Configuration @@ -207,7 +207,7 @@ Double check your time zone in *openHAB - Settings - Regional Settings* which is Returns `LocalDateTime` of the earliest possible forecast data available. It's located in the past, e.g. Solcast provides data from the last 7 days. -`LocalDateTime.MIN` is returned in case of no forecast data is available. +`LocalDateTime.MAX` is returned in case of no forecast data is available. ### Get Forecast End @@ -221,7 +221,7 @@ It's located in the past, e.g. Solcast provides data from the last 7 days. ```` Returns `LocalDateTime` of the latest possible forecast data available. -`LocalDateTime.MAX` is returned in case of no forecast data is available. +`LocalDateTime.MIN` is returned in case of no forecast data is available. ### Get Power 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 index 9be2a7176c9e1..9af586405ec62 100644 --- 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 @@ -113,13 +113,13 @@ private void addJSONArray(JSONArray resultJsonArray) { for (int i = 0; i < resultJsonArray.length(); i++) { JSONObject jo = resultJsonArray.getJSONObject(i); String periodEnd = jo.getString("period_end"); - LocalDate ld = getZdtFromUTC(periodEnd).toLocalDate(); + LocalDate ld = Utils.getZdtFromUTC(periodEnd, timeZoneProvider).toLocalDate(); TreeMap forecastMap = estimationDataMap.get(ld); if (forecastMap == null) { forecastMap = new TreeMap(); estimationDataMap.put(ld, forecastMap); } - forecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate")); + forecastMap.put(Utils.getZdtFromUTC(periodEnd, timeZoneProvider), jo.getDouble("pv_estimate")); // fill pessimistic values TreeMap pessimisticForecastMap = pessimisticDataMap.get(ld); @@ -128,9 +128,11 @@ private void addJSONArray(JSONArray resultJsonArray) { pessimisticDataMap.put(ld, pessimisticForecastMap); } if (jo.has("pv_estimate10")) { - pessimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate10")); + pessimisticForecastMap.put(Utils.getZdtFromUTC(periodEnd, timeZoneProvider), + jo.getDouble("pv_estimate10")); } else { - pessimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate")); + pessimisticForecastMap.put(Utils.getZdtFromUTC(periodEnd, timeZoneProvider), + jo.getDouble("pv_estimate")); } // fill optimistic values @@ -140,9 +142,11 @@ private void addJSONArray(JSONArray resultJsonArray) { optimisticDataMap.put(ld, optimisticForecastMap); } if (jo.has("pv_estimate90")) { - optimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate90")); + optimisticForecastMap.put(Utils.getZdtFromUTC(periodEnd, timeZoneProvider), + jo.getDouble("pv_estimate90")); } else { - optimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate")); + optimisticForecastMap.put(Utils.getZdtFromUTC(periodEnd, timeZoneProvider), + jo.getDouble("pv_estimate")); } } } @@ -293,11 +297,6 @@ public String getRaw() { return rawData.get().toString(); } - public ZonedDateTime getZdtFromUTC(String utc) { - Instant timestamp = Instant.parse(utc); - return timestamp.atZone(timeZoneProvider.getTimeZone()); - } - private TreeMap getDataMap(LocalDate ld, QueryMode mode) { TreeMap returnMap = EMPTY_MAP; switch (mode) { 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 index 2a939c74505a7..e7dfeddf15357 100644 --- 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 @@ -85,7 +85,7 @@ public class SolcastPlaneHandler extends BaseThingHandler implements SolarForeca private Optional powerItem = Optional.empty(); private Optional persistenceService; private SolcastObject forecast; - private ZonedDateTime nextMeasurement; + private Instant nextMeasurement; public SolcastPlaneHandler(Thing thing, HttpClient hc, Optional qps, ItemRegistry ir, TimeZoneProvider tzp) { @@ -95,7 +95,7 @@ public SolcastPlaneHandler(Thing thing, HttpClient hc, Optional Date: Thu, 8 Jun 2023 20:54:44 +0200 Subject: [PATCH 044/111] Add timeZone config for Solcast Signed-off-by: Bernd Weymann --- .../README.md | 27 +++++++++++-- .../internal/SolarForecastHandlerFactory.java | 2 +- .../config/SolcastBridgeConfiguration.java | 1 + .../solcast/handler/SolcastBridgeHandler.java | 23 +++++++++-- .../solcast/handler/SolcastPlaneHandler.java | 39 +++++++++---------- .../OH-INF/config/sc-site-config.xml | 6 +++ .../OH-INF/i18n/solarforecast.properties | 2 + 7 files changed, 70 insertions(+), 30 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 9147e1c73ee81..6bbc7c17d279c 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -57,13 +57,16 @@ After measurement is sent the `raw-tuning` channel is reporting the result. ### Solcast Bridge Configuration -| Name | Type | Description | Default | Required | -|------------------------|---------|---------------------------------------|---------|----------| -| apiKey | text | API Key | N/A | yes | -| channelRefreshInterval | integer | Channel Refresh Interval in minutes | 1 | yes | +| Name | Type | Description | Default | Required | Advanced | +|------------------------|---------|---------------------------------------|-------------|----------|----------| +| apiKey | text | API Key | N/A | yes | no | +| channelRefreshInterval | integer | Channel Refresh Interval in minutes | 1 | yes | no | +| timeZone | text | Time Zone of forecast location | auto-detect | no | yes | `apiKey` can be obtained in your [Account Settings](https://toolkit.solcast.com.au/account) +`timeZone` is set to `auto-detect` to evaluate Regional Settings of your openHAB installation. +See [DateTime](#date-time) section for more information. ### Solcast Plane Configuration @@ -192,6 +195,7 @@ While channels are providing actual forecast data and daily forecasts in future You can execute this for each `xx-plane` for specific plane values or `xx-site` to sum up all attached planes. Input for queries are `LocalDateTime` and `LocalDate` objects. +See [Date Time](#date-time) section for more information. Double check your time zone in *openHAB - Settings - Regional Settings* which is crucial for calculation. ### Get Forecast Begin @@ -396,3 +400,18 @@ shall produce following output 2022-08-10 00:02:16.574 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast Optimist 6 days 319.827 kWh 2022-08-10 00:02:16.578 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast Pessimist 6 days 208.235 kWh ```` + +## Date Time +Each forecast is bound to a certain location and this location is automatically bound to the TimeZone. +This binding is translating the forecast timestamps according to your Regional Settings in openHAB. +So you can query forecast data based on `LocalDate` and `LocalDateTime`. +This shall cover the majority of use cases. + +There might be rare use cases querying foreign locations, e.g. `Aisa/Tokyo`. +For this + +- Forecast.Solar is every time delivering the local asia date time measures +- Solcast needs to be configured with parameter `timeZone` in the advanced settings + +Taking this into account every time you query forecast data e.g. at 12 PM it will deliver the data at _high noon_ time of this location. + 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 index 73c676991438a..41c46ceea61b8 100644 --- 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 @@ -97,7 +97,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { } else if (SOLCAST_BRIDGE_STRING.equals(thingTypeUID)) { return new SolcastBridgeHandler((Bridge) thing, timeZoneProvider); } else if (SOLCAST_PART_STRING.equals(thingTypeUID)) { - return new SolcastPlaneHandler(thing, httpClient, qps, itemRegistry, timeZoneProvider); + return new SolcastPlaneHandler(thing, httpClient, qps, itemRegistry); } return null; } 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 index 4a97f850af98c..869586978fa14 100644 --- 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 @@ -24,4 +24,5 @@ public class SolcastBridgeConfiguration { public int channelRefreshInterval = -1; public String apiKey = SolarForecastBindingConstants.EMPTY; + public String timeZone = SolarForecastBindingConstants.AUTODETECT; } 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 index 37911219cbe76..2396571345975 100644 --- 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 @@ -15,6 +15,7 @@ import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; import java.time.LocalDate; +import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collection; @@ -51,9 +52,9 @@ * @author Bernd Weymann - Initial contribution */ @NonNullByDefault -public class SolcastBridgeHandler extends BaseBridgeHandler implements SolarForecastProvider { +public class SolcastBridgeHandler extends BaseBridgeHandler implements SolarForecastProvider, TimeZoneProvider { private final Logger logger = LoggerFactory.getLogger(SolcastBridgeHandler.class); - private final TimeZoneProvider timeZoneProvider; + private final TimeZoneProvider localTimeZoneProvider; private List parts = new ArrayList(); private Optional configuration = Optional.empty(); @@ -61,7 +62,7 @@ public class SolcastBridgeHandler extends BaseBridgeHandler implements SolarFore public SolcastBridgeHandler(Bridge bridge, TimeZoneProvider tzp) { super(bridge); - timeZoneProvider = tzp; + localTimeZoneProvider = tzp; } @Override @@ -119,7 +120,7 @@ private synchronized void getData() { logger.debug("No PV plane defined yet"); return; } - ZonedDateTime now = ZonedDateTime.now(timeZoneProvider.getTimeZone()); + ZonedDateTime now = ZonedDateTime.now(localTimeZoneProvider.getTimeZone()); double actualSum = 0; double actualPowerSum = 0; double remainSum = 0; @@ -217,4 +218,18 @@ public synchronized List getSolarForecasts() { }); return l; } + + @Override + public ZoneId getTimeZone() { + if (AUTODETECT.equals(configuration.get().timeZone)) { + return localTimeZoneProvider.getTimeZone(); + } else { + try { + return ZoneId.of(configuration.get().timeZone); + } catch (Throwable t) { + logger.info("Timezone {} not found", configuration.get().timeZone); + return localTimeZoneProvider.getTimeZone(); + } + } + } } 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 index e7dfeddf15357..4cb68e513373b 100644 --- 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 @@ -43,7 +43,6 @@ 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.i18n.TimeZoneProvider; import org.openhab.core.items.Item; import org.openhab.core.items.ItemRegistry; import org.openhab.core.library.types.DecimalType; @@ -79,23 +78,18 @@ public class SolcastPlaneHandler extends BaseThingHandler implements SolarForeca private final Logger logger = LoggerFactory.getLogger(SolcastPlaneHandler.class); private final HttpClient httpClient; private final ItemRegistry itemRegistry; - private final TimeZoneProvider timeZoneProvider; private Optional configuration = Optional.empty(); private Optional bridgeHandler = Optional.empty(); private Optional powerItem = Optional.empty(); private Optional persistenceService; - private SolcastObject forecast; - private Instant nextMeasurement; + private Optional forecast = Optional.empty(); + private Optional nextMeasurement = Optional.empty(); - public SolcastPlaneHandler(Thing thing, HttpClient hc, Optional qps, ItemRegistry ir, - TimeZoneProvider tzp) { + public SolcastPlaneHandler(Thing thing, HttpClient hc, Optional qps, ItemRegistry ir) { super(thing); httpClient = hc; persistenceService = qps; itemRegistry = ir; - timeZoneProvider = tzp; - forecast = new SolcastObject(timeZoneProvider); - nextMeasurement = Utils.getNextTimeframe(Instant.now(), timeZoneProvider); } @Override @@ -129,6 +123,9 @@ public void initialize() { if (handler instanceof SolcastBridgeHandler) { bridgeHandler = Optional.of((SolcastBridgeHandler) handler); bridgeHandler.get().addPlane(this); + forecast = Optional.of(new SolcastObject(bridgeHandler.get())); + nextMeasurement = Optional.of(Utils.getNextTimeframe(Instant.now(), bridgeHandler.get())); + updateStatus(ThingStatus.ONLINE); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, @@ -160,7 +157,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { } protected SolcastObject fetchData() { - if (!forecast.isValid()) { + if (!forecast.get().isValid()) { String forecastUrl = String.format(FORECAST_URL, configuration.get().resourceId); String currentEstimateUrl = String.format(CURRENT_ESTIMATE_URL, configuration.get().resourceId); try { @@ -171,7 +168,7 @@ protected SolcastObject fetchData() { if (crEstimate.getStatus() == 200) { SolcastObject localForecast = new SolcastObject(crEstimate.getContentAsString(), Instant.now().plus(configuration.get().refreshInterval, ChronoUnit.MINUTES), - timeZoneProvider); + bridgeHandler.get()); // get forecast Request forecastRequest = httpClient.newRequest(forecastUrl); @@ -181,7 +178,7 @@ protected SolcastObject fetchData() { if (crForecast.getStatus() == 200) { localForecast.join(crForecast.getContentAsString()); setForecast(localForecast); - updateState(CHANNEL_RAW, StringType.valueOf(forecast.getRaw())); + updateState(CHANNEL_RAW, StringType.valueOf(forecast.get().getRaw())); } else { logger.debug("{} Call {} failed {}", thing.getLabel(), forecastUrl, crForecast.getStatus()); } @@ -192,11 +189,11 @@ protected SolcastObject fetchData() { logger.debug("{} Call {} failed {}", thing.getLabel(), currentEstimateUrl, e.getMessage()); } } // else use available forecast - updateChannels(forecast); - if (Instant.now().isAfter(nextMeasurement)) { // [Todo] switch to Instant + updateChannels(forecast.get()); + if (Instant.now().isAfter(nextMeasurement.get())) { sendMeasure(); } - return forecast; + return forecast.get(); } /** @@ -205,9 +202,9 @@ protected SolcastObject fetchData() { private void sendMeasure() { State updateState = UnDefType.UNDEF; if (persistenceService.isPresent() && powerItem.isPresent()) { - ZonedDateTime beginPeriodDT = nextMeasurement.atZone(timeZoneProvider.getTimeZone()) + ZonedDateTime beginPeriodDT = nextMeasurement.get().atZone(bridgeHandler.get().getTimeZone()) .minusMinutes(MEASURE_INTERVAL_MIN); - ZonedDateTime endPeriodDT = nextMeasurement.atZone(timeZoneProvider.getTimeZone()); + ZonedDateTime endPeriodDT = nextMeasurement.get().atZone(bridgeHandler.get().getTimeZone()); FilterCriteria fc = new FilterCriteria(); fc.setBeginDate(beginPeriodDT); fc.setEndDate(endPeriodDT); @@ -286,11 +283,11 @@ private void sendMeasure() { } } updateState(CHANNEL_RAW_TUNING, updateState); - nextMeasurement = Utils.getNextTimeframe(Instant.now(), timeZoneProvider); + nextMeasurement = Optional.of(Utils.getNextTimeframe(Instant.now(), bridgeHandler.get())); } private void updateChannels(SolcastObject f) { - ZonedDateTime now = ZonedDateTime.now(timeZoneProvider.getTimeZone()); + ZonedDateTime now = ZonedDateTime.now(bridgeHandler.get().getTimeZone()); updateState(CHANNEL_ACTUAL, Utils.getEnergyState(f.getActualValue(now, QueryMode.Estimation))); updateState(CHANNEL_ACTUAL_POWER, Utils.getPowerState(f.getActualPowerValue(now, QueryMode.Estimation))); updateState(CHANNEL_REMAINING, Utils.getEnergyState(f.getRemainingProduction(now, QueryMode.Estimation))); @@ -317,11 +314,11 @@ private void updateChannels(SolcastObject f) { } private synchronized void setForecast(SolcastObject f) { - forecast = f; + forecast = Optional.of(f); } @Override public synchronized List getSolarForecasts() { - return List.of(forecast); + return List.of(forecast.get()); } } 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 index f76ad5970f5b5..8def54764478d 100644 --- 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 @@ -14,5 +14,11 @@ Refresh rate of channel data 1 + + + Time Zone of Forecast Location + auto-detect + 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 index 0bd79ad8b68ce..915cb3717a51f 100644 --- 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 @@ -45,6 +45,8 @@ 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.channelRefreshInterval.label = Channel Refresh Interval thing-type.config.solarforecast.sc-site.channelRefreshInterval.description = Refresh rate of channel data +thing-type.config.solarforecast.sc-site.timeZone.label = Time Zone +thing-type.config.solarforecast.sc-site.timeZone.description = Time Zone of Forecast Location # channel types From 7e7b384d5cfc28bb28d65247cf98e1c7e8af4fab Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Thu, 8 Jun 2023 21:14:55 +0200 Subject: [PATCH 045/111] i18n for rule actions Signed-off-by: Bernd Weymann --- .../OH-INF/i18n/solarforecast.properties | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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 index 915cb3717a51f..b65d50064fa27 100644 --- 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 @@ -107,4 +107,21 @@ solarforecast.plane.status.wrong-handler = Wrong Handler {0} # thing actions actionDayLabel = Daily Energy Production -actionDayDesc = Get energy production for complete day +actionDayDesc = Returns energy production for complete day in kw/h +actionInputDayLabel = Date +actionInputDayDesc = LocalDate for daily energy query +actionPowerLabel = Power +actionPowerDesc = Returns power in kw for a specific point in time +actionInputDateTimeLabel = Date Time +actionInputDateTimeDesc = LocalDateTime for power query +actionEnergyLabel = Energy Production +actionEnergyDesc = Returns energy productions between two different timestamps +actionInputDateTimeBeginLabel = Timestamp Begin +actionInputDateTimeBeginDesc = LocalDateTime as starting point of the energy query +actionInputDateTimeEndLabel = TimeStamp End +actionInputDateTimeEndDesc = LocalDateTime 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 + From f2f0f1aa1d6cbff3954df76c95332ad688901689 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Thu, 8 Jun 2023 21:55:46 +0200 Subject: [PATCH 046/111] resolve compiler warnings Signed-off-by: Bernd Weymann --- bundles/org.openhab.binding.solarforecast/README.md | 1 + .../solcast/handler/SolcastBridgeHandler.java | 5 +++-- .../main/resources/OH-INF/config/sc-site-config.xml | 12 ++++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 6bbc7c17d279c..8e59634095bce 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -402,6 +402,7 @@ shall produce following output ```` ## Date Time + Each forecast is bound to a certain location and this location is automatically bound to the TimeZone. This binding is translating the forecast timestamps according to your Regional Settings in openHAB. So you can query forecast data based on `LocalDate` and `LocalDateTime`. 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 index 2396571345975..0c588f9c32e5e 100644 --- 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 @@ -14,6 +14,7 @@ import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; +import java.time.DateTimeException; import java.time.LocalDate; import java.time.ZoneId; import java.time.ZonedDateTime; @@ -226,8 +227,8 @@ public ZoneId getTimeZone() { } else { try { return ZoneId.of(configuration.get().timeZone); - } catch (Throwable t) { - logger.info("Timezone {} not found", configuration.get().timeZone); + } catch (DateTimeException e) { + logger.info("Timezone {} not found {}", configuration.get().timeZone, e.getMessage()); return localTimeZoneProvider.getTimeZone(); } } 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 index 8def54764478d..cc25a35654e57 100644 --- 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 @@ -14,11 +14,11 @@ Refresh rate of channel data 1 - - - Time Zone of Forecast Location - auto-detect - true - + + + Time Zone of Forecast Location + auto-detect + true + From 0591e54fdd52df8d4d72312a0d74a23d3d7aa647 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Thu, 8 Jun 2023 22:15:41 +0200 Subject: [PATCH 047/111] Switch to OH4 Signed-off-by: Bernd Weymann --- bundles/org.openhab.binding.solarforecast/pom.xml | 2 +- .../solarforecast/internal/SolarForecastBindingConstants.java | 2 +- .../solarforecast/internal/SolarForecastHandlerFactory.java | 2 +- .../binding/solarforecast/internal/actions/SolarForecast.java | 2 +- .../solarforecast/internal/actions/SolarForecastActions.java | 2 +- .../solarforecast/internal/actions/SolarForecastProvider.java | 2 +- .../internal/forecastsolar/ForecastSolarObject.java | 2 +- .../forecastsolar/config/ForecastSolarBridgeConfiguration.java | 2 +- .../forecastsolar/config/ForecastSolarPlaneConfiguration.java | 2 +- .../forecastsolar/handler/ForecastSolarBridgeHandler.java | 2 +- .../forecastsolar/handler/ForecastSolarPlaneHandler.java | 2 +- .../solarforecast/internal/solcast/SolcastConstants.java | 2 +- .../binding/solarforecast/internal/solcast/SolcastObject.java | 2 +- .../internal/solcast/config/SolcastBridgeConfiguration.java | 2 +- .../internal/solcast/config/SolcastPlaneConfiguration.java | 2 +- .../internal/solcast/handler/SolcastBridgeHandler.java | 2 +- .../internal/solcast/handler/SolcastPlaneHandler.java | 2 +- .../org/openhab/binding/solarforecast/internal/utils/Utils.java | 2 +- .../test/java/org/openhab/binding/solarforecast/FileReader.java | 2 +- .../org/openhab/binding/solarforecast/ForecastSolarTest.java | 2 +- .../java/org/openhab/binding/solarforecast/SolcastTest.java | 2 +- .../src/test/java/org/openhab/binding/solarforecast/TimeZP.java | 2 +- 22 files changed, 22 insertions(+), 22 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/pom.xml b/bundles/org.openhab.binding.solarforecast/pom.xml index 7edc69dd33f17..20214bf08687b 100644 --- a/bundles/org.openhab.binding.solarforecast/pom.xml +++ b/bundles/org.openhab.binding.solarforecast/pom.xml @@ -7,7 +7,7 @@ org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 3.4.0-SNAPSHOT + 4.0.0-SNAPSHOT org.openhab.binding.solarforecast 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 index 3510970fa6016..a5deac5af4176 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index 41c46ceea61b8..1d2bdad15d8ba 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index e9ad0cc256678..616915c7965f4 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index de64fb8006bd8..2e2caa190a6a3 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index 9f18c3d11b7b1..7a1d3fceca894 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index 23987dabe5649..3b4456c526b70 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index ffef1cca86add..b480cb76b6fab 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index d8636824cc358..a4d035ca350fa 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index 72ecd6c94970b..7f1705357b67b 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index 706081f8570e1..012a25329eb4b 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index 232b0a9ef6ac5..31d7a9e426e45 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index 9af586405ec62..587bc9d481528 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index 869586978fa14..b192aee393c56 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index e533e5df5d062..3d6afe4e3731a 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index 0c588f9c32e5e..bd25ce7b4d0a8 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index 4cb68e513373b..927a6217bc7a2 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index b18575d29072e..cbee56dc4f7f4 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index fefde673c218d..a8c45625133f1 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index 81a5c8cfefcfd..6c84200a09689 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index adf10c2f0efed..b2a4bc36eb757 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index a5af8e59bbef8..9751478297d56 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. From fe2af4d76761a7a34ac797d782e27b9134b98de8 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Thu, 8 Jun 2023 22:24:08 +0200 Subject: [PATCH 048/111] addon.xml Signed-off-by: Bernd Weymann --- .../src/main/resources/OH-INF/addon/addon.xml | 10 ++++++++++ .../src/main/resources/OH-INF/binding/binding.xml | 9 --------- 2 files changed, 10 insertions(+), 9 deletions(-) create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/addon/addon.xml delete mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/binding/binding.xml 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 0000000000000..2e41e9af1d3c7 --- /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/binding/binding.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/binding/binding.xml deleted file mode 100644 index 57efb9f07be2c..0000000000000 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/binding/binding.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - SolarForecast Binding - Solar Forecast for your location - - From 8ee2f2bc67645b79e668c94fc6267dbfb3d670ea Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sun, 11 Jun 2023 16:15:38 +0200 Subject: [PATCH 049/111] bugfix horizon url Signed-off-by: Bernd Weymann --- .../forecastsolar/handler/ForecastSolarPlaneHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 012a25329eb4b..bfcf9a010361d 100644 --- 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 @@ -141,7 +141,7 @@ protected ForecastSolarObject fetchData() { + configuration.get().dampAM + "," + configuration.get().dampPM; } if (!SolarForecastBindingConstants.EMPTY.equals(configuration.get().horizon)) { - url += "?horizon=" + configuration.get().horizon; + url += "&horizon=" + configuration.get().horizon; } try { ContentResponse cr = httpClient.GET(url); From a2e3592e2844ca3ac71576fc1786a3a121fe010a Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Thu, 27 Jul 2023 14:38:52 +0200 Subject: [PATCH 050/111] update traget to OH4.1 Signed-off-by: Bernd Weymann --- bundles/org.openhab.binding.solarforecast/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.solarforecast/pom.xml b/bundles/org.openhab.binding.solarforecast/pom.xml index 20214bf08687b..a67779f71347a 100644 --- a/bundles/org.openhab.binding.solarforecast/pom.xml +++ b/bundles/org.openhab.binding.solarforecast/pom.xml @@ -7,7 +7,7 @@ org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 4.0.0-SNAPSHOT + 4.1.0-SNAPSHOT org.openhab.binding.solarforecast From 737cc301f3b93bc93c4aeb514203ee9cee8f7d4b Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Thu, 27 Jul 2023 15:08:04 +0200 Subject: [PATCH 051/111] update json version Signed-off-by: Bernd Weymann --- bundles/org.openhab.binding.solarforecast/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.solarforecast/pom.xml b/bundles/org.openhab.binding.solarforecast/pom.xml index a67779f71347a..82a0f6aba8f74 100644 --- a/bundles/org.openhab.binding.solarforecast/pom.xml +++ b/bundles/org.openhab.binding.solarforecast/pom.xml @@ -18,7 +18,7 @@ org.json json - 20180813 + 20230618 compile From a0478f57bbb0abf8f698eb184058d5684441a771 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 1 Aug 2023 00:35:02 +0200 Subject: [PATCH 052/111] bugfix addon.xml Signed-off-by: Bernd Weymann --- .../src/main/resources/OH-INF/addon/addon.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 2e41e9af1d3c7..faaa013112b97 100644 --- 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 @@ -1,5 +1,5 @@ - From a21388f5898bb3f51a4412ae00c1dcd542ae501a Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Wed, 15 Nov 2023 11:56:02 +0100 Subject: [PATCH 053/111] Forecast Solar internal zone handling Signed-off-by: Bernd Weymann --- .../forecastsolar/ForecastSolarObject.java | 47 +++++++++++-------- .../handler/ForecastSolarBridgeHandler.java | 5 +- .../handler/ForecastSolarPlaneHandler.java | 4 +- .../solarforecast/ForecastSolarTest.java | 42 ++++++++--------- .../binding/solarforecast/SolcastTest.java | 10 ++++ 5 files changed, 61 insertions(+), 47 deletions(-) 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 index 3b4456c526b70..cb1e0c71bb928 100644 --- 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 @@ -15,6 +15,8 @@ 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.util.Iterator; import java.util.Map.Entry; @@ -42,10 +44,11 @@ public class ForecastSolarObject implements SolarForecast { private static final double UNDEF = -1; private final Logger logger = LoggerFactory.getLogger(ForecastSolarObject.class); - private final TreeMap wattHourMap = new TreeMap(); - private final TreeMap wattMap = new TreeMap(); + 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 ZoneId zone = ZoneId.systemDefault(); private Optional rawData = Optional.empty(); private boolean valid = false; private Instant expirationDateTime; @@ -68,11 +71,13 @@ public ForecastSolarObject(String content, Instant expirationDate) { while (iter.hasNext()) { String dateStr = iter.next(); // convert date time into machine readable format - LocalDateTime ldt = LocalDateTime.parse(dateStr, dateInputFormatter); - wattHourMap.put(ldt, wattHourJson.getDouble(dateStr)); - wattMap.put(ldt, wattJson.getDouble(dateStr)); + ZonedDateTime zdt = LocalDateTime.parse(dateStr, dateInputFormatter).atZone(zone); + wattHourMap.put(zdt, wattHourJson.getDouble(dateStr)); + wattMap.put(zdt, wattJson.getDouble(dateStr)); } valid = true; + String zoneStr = contentJson.getJSONObject("message").getJSONObject("info").getString("timezone"); + zone = ZoneId.of(zoneStr); } catch (JSONException je) { logger.debug("Error parsing JSON response {} - {}", content, je.getMessage()); } @@ -90,12 +95,12 @@ public boolean isValid() { return false; } - public double getActualValue(LocalDateTime queryDateTime) { + public double getActualValue(ZonedDateTime queryDateTime) { if (wattHourMap.isEmpty()) { return UNDEF; } - Entry f = wattHourMap.floorEntry(queryDateTime); - Entry c = wattHourMap.ceilingEntry(queryDateTime); + 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())) { @@ -135,13 +140,13 @@ public double getActualValue(LocalDateTime queryDateTime) { return UNDEF; } - public double getActualPowerValue(LocalDateTime queryDateTime) { + public double getActualPowerValue(ZonedDateTime queryDateTime) { if (wattMap.isEmpty()) { return UNDEF; } double actualPowerValue = 0; - Entry f = wattMap.floorEntry(queryDateTime); - Entry c = wattMap.ceilingEntry(queryDateTime); + 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())) { @@ -186,7 +191,7 @@ public double getDayTotal(LocalDate queryDate) { return UNDEF; } - public double getRemainingProduction(LocalDateTime queryDateTime) { + public double getRemainingProduction(ZonedDateTime queryDateTime) { if (wattHourMap.isEmpty()) { return UNDEF; } @@ -226,10 +231,10 @@ public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDate LocalDate endDate = localDateTimeEnd.toLocalDate(); double measure = UNDEF; if (beginDate.equals(endDate)) { - measure = getDayTotal(beginDate) - getActualValue(localDateTimeBegin) - - getRemainingProduction(localDateTimeEnd); + measure = getDayTotal(beginDate) - getActualValue(localDateTimeBegin.atZone(zone)) + - getRemainingProduction(localDateTimeEnd.atZone(zone)); } else { - measure = getRemainingProduction(localDateTimeBegin); + measure = getRemainingProduction(localDateTimeBegin.atZone(zone)); beginDate = beginDate.plusDays(1); while (beginDate.isBefore(endDate) && measure >= 0) { double day = getDayTotal(beginDate); @@ -238,7 +243,7 @@ public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDate } beginDate = beginDate.plusDays(1); } - double lastDay = getActualValue(localDateTimeEnd); + double lastDay = getActualValue(localDateTimeEnd.atZone(zone)); if (lastDay >= 0) { measure += lastDay; } @@ -252,14 +257,14 @@ public State getPower(LocalDateTime localDateTime, String... args) { logger.info("ForecastSolar doesn't accept arguments"); return UnDefType.UNDEF; } - double measure = getActualPowerValue(localDateTime); + double measure = getActualPowerValue(localDateTime.atZone(zone)); return Utils.getPowerState(measure); } @Override public LocalDateTime getForecastBegin() { if (!wattHourMap.isEmpty()) { - LocalDateTime ldt = wattHourMap.firstEntry().getKey(); + LocalDateTime ldt = wattHourMap.firstEntry().getKey().toLocalDateTime(); return ldt; } return LocalDateTime.MIN; @@ -268,9 +273,13 @@ public LocalDateTime getForecastBegin() { @Override public LocalDateTime getForecastEnd() { if (!wattHourMap.isEmpty()) { - LocalDateTime ldt = wattHourMap.lastEntry().getKey(); + LocalDateTime ldt = wattHourMap.lastEntry().getKey().toLocalDateTime(); return ldt; } return LocalDateTime.MIN; } + + public ZoneId getZone() { + return zone; + } } 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 index 7f1705357b67b..dc175d59c475b 100644 --- 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 @@ -14,7 +14,7 @@ import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; -import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -107,10 +107,8 @@ private void startSchedule(int interval) { */ private synchronized void getData() { if (parts.isEmpty()) { - logger.debug("No PV plane defined yet"); return; } - LocalDateTime now = LocalDateTime.now(); double actualSum = 0; double actualPowerSum = 0; double remainSum = 0; @@ -121,6 +119,7 @@ private synchronized void getData() { for (Iterator iterator = parts.iterator(); iterator.hasNext();) { ForecastSolarPlaneHandler sfph = iterator.next(); ForecastSolarObject fo = sfph.fetchData(); + ZonedDateTime now = ZonedDateTime.now(fo.getZone()); actualSum += fo.getActualValue(now); actualPowerSum += fo.getActualPowerValue(now); remainSum += fo.getRemainingProduction(now); 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 index bfcf9a010361d..47bb596dc47ab 100644 --- 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 @@ -15,7 +15,7 @@ import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; import java.time.Instant; -import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.Collection; import java.util.Collections; @@ -165,7 +165,7 @@ protected ForecastSolarObject fetchData() { } private void updateChannels(ForecastSolarObject f) { - LocalDateTime now = LocalDateTime.now(); + ZonedDateTime now = ZonedDateTime.now(f.getZone()); updateState(CHANNEL_ACTUAL, Utils.getEnergyState(f.getActualValue(now))); updateState(CHANNEL_ACTUAL_POWER, Utils.getPowerState(f.getActualPowerValue(now))); updateState(CHANNEL_REMAINING, Utils.getEnergyState(f.getRemainingProduction(now))); 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 index 6c84200a09689..8d99b8682f339 100644 --- 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 @@ -16,6 +16,7 @@ import java.time.LocalDateTime; import java.time.ZoneId; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import javax.measure.quantity.Energy; @@ -42,9 +43,8 @@ class ForecastSolarTest { @Test void testForecastObject() { String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); - LocalDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 17, 00); - ForecastSolarObject fo = new ForecastSolarObject(content, - queryDateTime.toInstant(TEST_ZONE.getRules().getOffset(queryDateTime))); + ZonedDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 17, 00).atZone(TEST_ZONE); + ForecastSolarObject fo = new ForecastSolarObject(content, queryDateTime.toInstant()); // "2022-07-17 21:32:00": 63583, assertEquals(63.583, fo.getDayTotal(queryDateTime.toLocalDate()), TOLERANCE, "Total production"); // "2022-07-17 17:00:00": 52896, @@ -56,7 +56,7 @@ void testForecastObject() { fo.getActualValue(queryDateTime) + fo.getRemainingProduction(queryDateTime), TOLERANCE, "actual + remain = total"); - queryDateTime = LocalDateTime.of(2022, 7, 18, 19, 00); + queryDateTime = LocalDateTime.of(2022, 7, 18, 19, 00).atZone(TEST_ZONE); // "2022-07-18 19:00:00": 63067, assertEquals(63.067, fo.getActualValue(queryDateTime), TOLERANCE, "Actual production"); // "2022-07-18 21:31:00": 65554 @@ -66,13 +66,12 @@ void testForecastObject() { @Test void testActualPower() { String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); - LocalDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 10, 00); - ForecastSolarObject fo = new ForecastSolarObject(content, - queryDateTime.toInstant(TEST_ZONE.getRules().getOffset(queryDateTime))); + ZonedDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 10, 00).atZone(TEST_ZONE); + ForecastSolarObject fo = new ForecastSolarObject(content, queryDateTime.toInstant()); // "2022-07-17 10:00:00": 4874, assertEquals(4.874, fo.getActualPowerValue(queryDateTime), TOLERANCE, "Actual estimation"); - queryDateTime = LocalDateTime.of(2022, 7, 18, 14, 00); + 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"); } @@ -80,9 +79,8 @@ void testActualPower() { @Test void testInterpolation() { String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); - LocalDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 0); - ForecastSolarObject fo = new ForecastSolarObject(content, - queryDateTime.toInstant(TEST_ZONE.getRules().getOffset(queryDateTime))); + ZonedDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 0).atZone(TEST_ZONE); + ForecastSolarObject fo = new ForecastSolarObject(content, queryDateTime.toInstant()); // test steady value increase double previousValue = 0; @@ -92,7 +90,7 @@ void testInterpolation() { previousValue = fo.getActualValue(queryDateTime); } - queryDateTime = LocalDateTime.of(2022, 7, 18, 6, 23); + 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 @@ -102,9 +100,8 @@ void testInterpolation() { @Test void testForecastSum() { String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); - LocalDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 23); - ForecastSolarObject fo = new ForecastSolarObject(content, - queryDateTime.toInstant(TEST_ZONE.getRules().getOffset(queryDateTime))); + ZonedDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 23).atZone(TEST_ZONE); + ForecastSolarObject fo = new ForecastSolarObject(content, queryDateTime.toInstant()); QuantityType actual = QuantityType.valueOf(0, Units.KILOWATT_HOUR); State st = Utils.getEnergyState(fo.getActualValue(queryDateTime)); assertTrue(st instanceof QuantityType); @@ -119,7 +116,7 @@ void testCornerCases() { // invalid object ForecastSolarObject fo = new ForecastSolarObject(); assertFalse(fo.isValid()); - LocalDateTime query = LocalDateTime.of(2022, 7, 17, 16, 23); + ZonedDateTime query = LocalDateTime.of(2022, 7, 17, 16, 23).atZone(TEST_ZONE); assertEquals(-1.0, fo.getActualValue(query), TOLERANCE, "Actual Production"); assertEquals(-1.0, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Today Production"); assertEquals(-1.0, fo.getRemainingProduction(query), TOLERANCE, "Remaining Production"); @@ -127,8 +124,8 @@ void testCornerCases() { // 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); - fo = new ForecastSolarObject(content, query.toInstant(TEST_ZONE.getRules().getOffset(query))); + query = LocalDateTime.of(2022, 7, 16, 23, 59).atZone(TEST_ZONE); + fo = new ForecastSolarObject(content, query.toInstant()); assertEquals(-1.0, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope"); assertEquals(-1.0, fo.getActualValue(query), TOLERANCE, "Actual out of scope"); assertEquals(-1.0, fo.getRemainingProduction(query), TOLERANCE, "Remain out of scope"); @@ -142,7 +139,7 @@ void testCornerCases() { 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); + query = LocalDateTime.of(2022, 7, 19, 0, 0).atZone(TEST_ZONE); assertEquals(-1.0, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope"); assertEquals(-1.0, fo.getActualValue(query), TOLERANCE, "Actual out of scope"); assertEquals(-1.0, fo.getRemainingProduction(query), TOLERANCE, "Remain out of scope"); @@ -156,7 +153,7 @@ void testCornerCases() { assertEquals(0.0, fo.getActualPowerValue(query), TOLERANCE, "Remain out of scope"); // test times between 2 dates - query = LocalDateTime.of(2022, 7, 17, 23, 59); + 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.getActualValue(query), TOLERANCE, "Actual out of scope"); assertEquals(0.0, fo.getRemainingProduction(query), TOLERANCE, "Remain out of scope"); @@ -172,8 +169,7 @@ void testCornerCases() { void testActions() { String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); LocalDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 23); - ForecastSolarObject fo = new ForecastSolarObject(content, - queryDateTime.toInstant(TEST_ZONE.getRules().getOffset(queryDateTime))); + ForecastSolarObject fo = new ForecastSolarObject(content, queryDateTime.atZone(TEST_ZONE).toInstant()); assertEquals("2022-07-17T05:31:00", fo.getForecastBegin().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), "Forecast begin"); assertEquals("2022-07-18T21:31:00", fo.getForecastEnd().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), @@ -198,7 +194,7 @@ void testActions() { * public void testHorizon() throws URISyntaxException, IOException, InterruptedException { * String url = "https://api.forecast.solar/estimate/50.55598767987004/8.49558522179684/12/-40/5.525"; * String horizon = "2,2,2,2,1,1,3,3,4,3,3,4,3,3,3,3,4,5,7,5,4,2,2,2,2,1,1,1,1,1,2,2,2,2,1,2"; - * + * * HttpClient client = HttpClient.newHttpClient(); * HttpRequest request = HttpRequest.newBuilder().uri(new URI(url)).GET().build(); * HttpResponse response = client.send(request, BodyHandlers.ofString()); 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 index b2a4bc36eb757..3b0715db582d9 100644 --- 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 @@ -433,4 +433,14 @@ void testTimes() { LocalTime lt = zdt.toLocalTime(); assertEquals("21:30", lt.toString(), "LocalTime"); } + + @Test + void testCST() { + LocalDateTime ldt = LocalDateTime.of(2023, 3, 26, 1, 30); + System.out.println(ldt); + System.out.println(ldt.atZone(TEST_ZONE)); + ldt = LocalDateTime.of(2023, 3, 26, 2, 30); + System.out.println(ldt); + System.out.println(ldt.atZone(TEST_ZONE)); + } } From f5c4576d8d04336fc8a9928bbf5dc42ac56b1c7e Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Wed, 15 Nov 2023 16:04:52 +0100 Subject: [PATCH 054/111] bugfix: Solcast zone conversions Signed-off-by: Bernd Weymann --- .../README.md | 2 +- .../internal/solcast/SolcastObject.java | 22 ++++++++++--------- .../solcast/handler/SolcastBridgeHandler.java | 2 +- .../solarforecast/internal/utils/Utils.java | 5 ----- .../binding/solarforecast/SolcastTest.java | 3 ++- 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 8e59634095bce..77f3ec86c239f 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -299,7 +299,7 @@ Exchange the configuration data in [thing file](#thing-file) and you're ready to ### Thing file ```` -Bridge solarforecast:fs-site:homeSite "ForecastSolar Home" [ location="54.321,8.976", channelRefreshInterval="1"] { +Bridge solarforecast:fs-site:homeSite "ForecastSolar Home" [ location="54.321,8.976", channelRefreshInterval=1] { Thing fs-plane homeSouthWest "ForecastSolar Home South-West" [ refreshInterval=10, azimuth=45, declination=35, kwp=5.5] Thing fs-plane homeNorthEast "ForecastSolar Home North-East" [ refreshInterval=10, azimuth=-145, declination=35, kwp=4.425] } 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 index 587bc9d481528..7c6534d6c9965 100644 --- 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 @@ -113,13 +113,13 @@ private void addJSONArray(JSONArray resultJsonArray) { for (int i = 0; i < resultJsonArray.length(); i++) { JSONObject jo = resultJsonArray.getJSONObject(i); String periodEnd = jo.getString("period_end"); - LocalDate ld = Utils.getZdtFromUTC(periodEnd, timeZoneProvider).toLocalDate(); + LocalDate ld = getZdtFromUTC(periodEnd).toLocalDate(); TreeMap forecastMap = estimationDataMap.get(ld); if (forecastMap == null) { forecastMap = new TreeMap(); estimationDataMap.put(ld, forecastMap); } - forecastMap.put(Utils.getZdtFromUTC(periodEnd, timeZoneProvider), jo.getDouble("pv_estimate")); + forecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate")); // fill pessimistic values TreeMap pessimisticForecastMap = pessimisticDataMap.get(ld); @@ -128,11 +128,9 @@ private void addJSONArray(JSONArray resultJsonArray) { pessimisticDataMap.put(ld, pessimisticForecastMap); } if (jo.has("pv_estimate10")) { - pessimisticForecastMap.put(Utils.getZdtFromUTC(periodEnd, timeZoneProvider), - jo.getDouble("pv_estimate10")); + pessimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate10")); } else { - pessimisticForecastMap.put(Utils.getZdtFromUTC(periodEnd, timeZoneProvider), - jo.getDouble("pv_estimate")); + pessimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate")); } // fill optimistic values @@ -142,11 +140,9 @@ private void addJSONArray(JSONArray resultJsonArray) { optimisticDataMap.put(ld, optimisticForecastMap); } if (jo.has("pv_estimate90")) { - optimisticForecastMap.put(Utils.getZdtFromUTC(periodEnd, timeZoneProvider), - jo.getDouble("pv_estimate90")); + optimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate90")); } else { - optimisticForecastMap.put(Utils.getZdtFromUTC(periodEnd, timeZoneProvider), - jo.getDouble("pv_estimate")); + optimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate")); } } } @@ -436,4 +432,10 @@ private QueryMode evalArguments(String[] args) { return QueryMode.Estimation; } } + + public ZonedDateTime getZdtFromUTC(String utc) { + Instant timestamp = Instant.parse(utc); + return timestamp.atZone(timeZoneProvider.getTimeZone()); + } + } 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 index bd25ce7b4d0a8..d82983b071947 100644 --- 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 @@ -121,7 +121,7 @@ private synchronized void getData() { logger.debug("No PV plane defined yet"); return; } - ZonedDateTime now = ZonedDateTime.now(localTimeZoneProvider.getTimeZone()); + ZonedDateTime now = ZonedDateTime.now(getTimeZone()); double actualSum = 0; double actualPowerSum = 0; double remainSum = 0; 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 index cbee56dc4f7f4..87395979a7176 100644 --- 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 @@ -75,9 +75,4 @@ public static Instant getNextTimeframe(Instant timeStamp, TimeZoneProvider tzp) } return nextTime.toInstant(); } - - public static ZonedDateTime getZdtFromUTC(String utc, TimeZoneProvider tzp) { - Instant timestamp = Instant.parse(utc); - return timestamp.atZone(tzp.getTimeZone()); - } } 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 index 3b0715db582d9..d3e8efcc290df 100644 --- 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 @@ -426,7 +426,8 @@ void testUnitDetection() { @Test void testTimes() { String utcTimeString = "2022-07-17T19:30:00.0000000Z"; - ZonedDateTime zdt = Utils.getZdtFromUTC(utcTimeString, TIMEZONEPROVIDER); + SolcastObject so = new SolcastObject(TIMEZONEPROVIDER); + ZonedDateTime zdt = so.getZdtFromUTC(utcTimeString); assertEquals("2022-07-17T21:30+02:00[Europe/Berlin]", zdt.toString(), "ZonedDateTime"); LocalDateTime ldt = zdt.toLocalDateTime(); assertEquals("2022-07-17T21:30", ldt.toString(), "LocalDateTime"); From 3c4aa30a37e3d3082dfbaeb6ec4a28cd7a30b5e6 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Thu, 16 Nov 2023 20:26:02 +0100 Subject: [PATCH 055/111] review comments corrections Signed-off-by: Bernd Weymann --- .../README.md | 29 ++++++------ .../org.openhab.binding.solarforecast/pom.xml | 2 +- .../internal/SolarForecastHandlerFactory.java | 2 +- .../actions/SolarForecastActions.java | 14 +++--- .../actions/SolarForecastProvider.java | 2 +- .../forecastsolar/ForecastSolarObject.java | 3 +- .../ForecastSolarBridgeConfiguration.java | 2 +- .../ForecastSolarPlaneConfiguration.java | 8 ++-- .../handler/ForecastSolarPlaneHandler.java | 18 +++----- .../internal/solcast/SolcastConstants.java | 1 - .../internal/solcast/SolcastObject.java | 30 ++++++++----- .../config/SolcastBridgeConfiguration.java | 2 +- .../config/SolcastPlaneConfiguration.java | 2 +- .../solcast/handler/SolcastBridgeHandler.java | 3 +- .../solcast/handler/SolcastPlaneHandler.java | 44 +++++++++---------- .../OH-INF/config/fs-plane-config.xml | 6 +-- .../OH-INF/config/fs-site-config.xml | 4 +- .../OH-INF/config/sc-site-config.xml | 4 +- .../OH-INF/i18n/solarforecast.properties | 25 ++++++++--- .../solarforecast/ForecastSolarTest.java | 14 ------ .../binding/solarforecast/SolcastTest.java | 10 ----- 21 files changed, 107 insertions(+), 118 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 77f3ec86c239f..3079acc2dc3e7 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -11,7 +11,7 @@ Supported Services - [Forecast.Solar](https://forecast.solar/) - Public, Personal and Professional [plans](https://forecast.solar/#accounts) available -Display Forecast *Power values* and measures of *PV innverter* item +Display Forecast *Power values* and measures of *PV inverter* item @@ -33,7 +33,7 @@ Each service needs one `xx-site` for your location and at least one photovoltaic ## Solcast Configuration -[Solcast service](https://solcast.com/) requires a personal registration with an email address. +[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. @@ -43,15 +43,15 @@ Correct time zone is necessary to show correct forecast times in UI. ### Solcast Tuning -You've the opportunity to [send your own measurements back to Solcast API](https://legacy-docs.solcast.com.au/#measurements-rooftop-site). +You have the opportunity to [send your own measurements back to Solcast API](https://legacy-docs.solcast.com.au/#measurements-rooftop-site). This data is used internally to improve the forecast for your specific site. Configuration and channels can be set after checking the *Show advanced* checkbox. You need an item which reports the electric power for the specific rooftop. -If item isn't set no measures will be sent. +If item isn't set, no measures will be sent. As described in [Solcast Rooftop Measurement](https://legacy-docs.solcast.com.au/#measurements-rooftop-site) check in beforehand if your measures are *sane*. - item is delivering correct values and they are stored in persistence -- time settings in openHAB are correct in order to so measurements are matching to the measure time frame +- time zone setting in openHAB is correct to deliver correct timestamp After measurement is sent the `raw-tuning` channel is reporting the result. @@ -80,21 +80,21 @@ See [DateTime](#date-time) section for more information. `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've 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. +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. Note: `channelRefreshInterval` from [Bridge Configuration](#solcast-bridge-configuration) will calculate intermediate values without requesting new forecast data. `powerItem` shall reflect the power for this specific rooftop. It's an optional setting and the [measure is sent to Solcast API in order to tune the forecast](https://legacy-docs.solcast.com.au/#measurements-rooftop-site) in the future. -If you don't want to send measures to Solcast leave this configuration item empty. +If you don't want to send measures to Solcast, leave this configuration item empty. `powerUnit` is set to `auto-detect`. -In case the `powerItem` is delivering a valid `QuantityType` state this setting is fine. -If the item delivers a raw number without unit please select `powerUnit` accordingly if item state is Watt or Kilowatt unit. +In case the `powerItem` is delivering a valid `QuantityType` state, this setting is fine. +If the item delivers a raw number without unit please select `powerUnit` accordingly if item state, is Watt or Kilowatt unit. ## Solcast Channels -Each `sc-plane` reports its own values including a `raw` channel holding json content. +Each `sc-plane` reports its own values including a `raw` channel holding JSON content. The `sc-site` bridge sums up all attached `sc-plane` values and provides the total forecast for your home location. Channels are covering today's actual data with current, remaining and today's total prediction. @@ -170,10 +170,10 @@ So you need to know what you're doing. ## ForecastSolar Channels -Each `fs-plane` reports it's own values including a `raw` channel holding json content. +Each `fs-plane` reports it's own values including a `raw` 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 todays actual data with current, remaining and today's total prediction. +Channels are covering today's actual data with current, remaining and total prediction. Forecasts are delivered up to 3 days for paid personal plans. Day*X* channels are referring to forecasts plus *X* days: 1 = tomorrow, 2 = day after tomorrow, ... @@ -187,7 +187,6 @@ Day*X* channels are referring to forecasts plus *X* days: 1 = tomorrow, 2 = day | day*X* | Number:Energy | Day *X* forecast in total | no | | raw | 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. @@ -298,7 +297,7 @@ Exchange the configuration data in [thing file](#thing-file) and you're ready to ### Thing file -```` +````java Bridge solarforecast:fs-site:homeSite "ForecastSolar Home" [ location="54.321,8.976", channelRefreshInterval=1] { Thing fs-plane homeSouthWest "ForecastSolar Home South-West" [ refreshInterval=10, azimuth=45, declination=35, kwp=5.5] Thing fs-plane homeNorthEast "ForecastSolar Home North-East" [ refreshInterval=10, azimuth=-145, declination=35, kwp=4.425] @@ -409,7 +408,7 @@ So you can query forecast data based on `LocalDate` and `LocalDateTime`. This shall cover the majority of use cases. There might be rare use cases querying foreign locations, e.g. `Aisa/Tokyo`. -For this +For this: - Forecast.Solar is every time delivering the local asia date time measures - Solcast needs to be configured with parameter `timeZone` in the advanced settings diff --git a/bundles/org.openhab.binding.solarforecast/pom.xml b/bundles/org.openhab.binding.solarforecast/pom.xml index 82a0f6aba8f74..4ccf149b6ea0f 100644 --- a/bundles/org.openhab.binding.solarforecast/pom.xml +++ b/bundles/org.openhab.binding.solarforecast/pom.xml @@ -18,7 +18,7 @@ org.json json - 20230618 + 20231013 compile 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 index 1d2bdad15d8ba..33e6081d863e0 100644 --- 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 @@ -78,7 +78,7 @@ public SolarForecastHandlerFactory(final @Reference HttpClientFactory hcf, final if (s instanceof QueryablePersistenceService) { qps = Optional.of((QueryablePersistenceService) s); } else { - logger.info("Persistence {} cannot be queried. Feature Solcast Tuninng will not work", s); + logger.info("Persistence {} cannot be queried. Feature Solcast Tuning will not work", s); } } 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 index 2e2caa190a6a3..daafb3750b65f 100644 --- 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 @@ -37,7 +37,7 @@ import org.slf4j.LoggerFactory; /** - * ActionsS to query forecast objects + * Actions to query forecast objects * * @author Bernd Weymann - Initial contribution */ @@ -58,8 +58,8 @@ public State getDay( for (Iterator iterator = l.iterator(); iterator.hasNext();) { SolarForecast solarForecast = iterator.next(); State s = solarForecast.getDay(localDate, args); - if (s instanceof QuantityType) { - measure = measure.add((QuantityType) s); + if (s instanceof QuantityType quantityState) { + measure = measure.add((QuantityType) quantityState); } else { // break in case of failure getting values to avoid ambiguous values logger.trace("Ambiguous measure {} found for {} - return UNDEF", s, localDate); @@ -88,8 +88,8 @@ public State getPower( for (Iterator iterator = l.iterator(); iterator.hasNext();) { SolarForecast solarForecast = iterator.next(); State s = solarForecast.getPower(localDateTime, args); - if (s instanceof QuantityType) { - measure = measure.add((QuantityType) s); + if (s instanceof QuantityType quantityState) { + measure = measure.add((QuantityType) quantityState); } else { // break in case of failure getting values to avoid ambiguous values logger.trace("Ambiguous measure {} found for {} - return UNDEF", s, localDateTime); @@ -119,8 +119,8 @@ public State getEnergy( for (Iterator iterator = l.iterator(); iterator.hasNext();) { SolarForecast solarForecast = iterator.next(); State s = solarForecast.getEnergy(localDateTimeBegin, localDateTimeEnd, args); - if (s instanceof QuantityType) { - measure = measure.add((QuantityType) s); + if (s instanceof QuantityType quantityState) { + measure = measure.add((QuantityType) quantityState); } else { // break in case of failure getting values to avoid ambiguous values logger.trace("Ambiguous measure {} found between {} and {} - return UNDEF", s, 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 index 7a1d3fceca894..dc2da3a42069a 100644 --- 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 @@ -29,5 +29,5 @@ public interface SolarForecastProvider { * * @return list of SolarForecast objects */ - public List getSolarForecasts(); + 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 index cb1e0c71bb928..d13b1f0937310 100644 --- 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 @@ -26,7 +26,6 @@ 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.actions.SolarForecast; import org.openhab.binding.solarforecast.internal.utils.Utils; import org.openhab.core.types.State; @@ -59,7 +58,7 @@ public ForecastSolarObject() { public ForecastSolarObject(String content, Instant expirationDate) { expirationDateTime = expirationDate; - if (!content.equals(SolarForecastBindingConstants.EMPTY)) { + if (!content.isEmpty()) { rawData = Optional.of(content); try { JSONObject contentJson = new JSONObject(content); 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 index b480cb76b6fab..d3079218aa4c3 100644 --- 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 @@ -23,7 +23,7 @@ @NonNullByDefault public class ForecastSolarBridgeConfiguration { public String location = "0.0,0.0"; - public int channelRefreshInterval = -1; + public int channelRefreshInterval = 1; 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 index a4d035ca350fa..9ded25dcceb55 100644 --- 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 @@ -25,13 +25,13 @@ public class ForecastSolarPlaneConfiguration { public int declination = -1; public int azimuth = 360; public double kwp = 0; - public long refreshInterval = -1; - public double dampAM = 0; - public double dampPM = 0; + public long refreshInterval = 30; + public double dampAM = 0.25; + public double dampPM = 0.25; public String horizon = SolarForecastBindingConstants.EMPTY; @Override public String toString() { - return " Dec " + declination + " Azi " + azimuth + " KWP " + kwp + " Ref " + refreshInterval; + return "Dec " + declination + " Azi " + azimuth + " KWP " + kwp + " Ref " + refreshInterval; } } 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 index 47bb596dc47ab..160dcf7cd03fd 100644 --- 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 @@ -18,7 +18,6 @@ import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutionException; @@ -74,7 +73,7 @@ public ForecastSolarPlaneHandler(Thing thing, HttpClient hc) { @Override public Collection> getServices() { - return Collections.singleton(SolarForecastActions.class); + return List.of(SolarForecastActions.class); } @Override @@ -85,20 +84,20 @@ public void initialize() { if (bridge != null) { BridgeHandler handler = bridge.getHandler(); if (handler != null) { - if (handler instanceof ForecastSolarBridgeHandler) { - bridgeHandler = Optional.of((ForecastSolarBridgeHandler) handler); + if (handler instanceof ForecastSolarBridgeHandler fsbh) { + bridgeHandler = Optional.of(fsbh); bridgeHandler.get().addPlane(this); updateStatus(ThingStatus.ONLINE); } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/solarforecast.site.status.wrong-handler" + " [\"" + handler + "\"]"); } } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/solarforecast.site.status.bridge-handler-not-found"); } } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/solarforecast.site.status.bridge-missing"); } } @@ -184,11 +183,6 @@ void setLocation(PointType loc) { location = Optional.of(loc); } - /** - * Used by Bridge to set location directly - * - * @param loc - */ void setApiKey(String key) { apiKey = Optional.of(key); } 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 index 31d7a9e426e45..0b14ffef32288 100644 --- 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 @@ -27,7 +27,6 @@ */ @NonNullByDefault public class SolcastConstants { - // public static final String FORECAST_URL = "https://api.solcast.com.au/rooftop_sites/%s/forecasts?format=json"; public static final String FORECAST_URL = "https://api.solcast.com.au/rooftop_sites/%s/forecasts?format=json&hours=168"; public static final String CURRENT_ESTIMATE_URL = "https://api.solcast.com.au/rooftop_sites/%s/estimated_actuals?format=json"; public static final String MEASUREMENT_URL = "https://api.solcast.com.au/rooftop_sites/%s/measurements?format=json"; 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 index 7c6534d6c9965..e9320920d6b5b 100644 --- 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 @@ -16,6 +16,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZonedDateTime; +import java.time.format.DateTimeParseException; import java.util.Arrays; import java.util.Map.Entry; import java.util.Optional; @@ -23,6 +24,7 @@ import java.util.TreeMap; 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; @@ -113,13 +115,17 @@ private void addJSONArray(JSONArray resultJsonArray) { for (int i = 0; i < resultJsonArray.length(); i++) { JSONObject jo = resultJsonArray.getJSONObject(i); String periodEnd = jo.getString("period_end"); - LocalDate ld = getZdtFromUTC(periodEnd).toLocalDate(); + ZonedDateTime periadEndZdt = getZdtFromUTC(periodEnd); + if (periadEndZdt == null) { + return; + } + LocalDate ld = periadEndZdt.toLocalDate(); TreeMap forecastMap = estimationDataMap.get(ld); if (forecastMap == null) { forecastMap = new TreeMap(); estimationDataMap.put(ld, forecastMap); } - forecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate")); + forecastMap.put(periadEndZdt, jo.getDouble("pv_estimate")); // fill pessimistic values TreeMap pessimisticForecastMap = pessimisticDataMap.get(ld); @@ -128,9 +134,9 @@ private void addJSONArray(JSONArray resultJsonArray) { pessimisticDataMap.put(ld, pessimisticForecastMap); } if (jo.has("pv_estimate10")) { - pessimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate10")); + pessimisticForecastMap.put(periadEndZdt, jo.getDouble("pv_estimate10")); } else { - pessimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate")); + pessimisticForecastMap.put(periadEndZdt, jo.getDouble("pv_estimate")); } // fill optimistic values @@ -140,9 +146,9 @@ private void addJSONArray(JSONArray resultJsonArray) { optimisticDataMap.put(ld, optimisticForecastMap); } if (jo.has("pv_estimate90")) { - optimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate90")); + optimisticForecastMap.put(periadEndZdt, jo.getDouble("pv_estimate90")); } else { - optimisticForecastMap.put(getZdtFromUTC(periodEnd), jo.getDouble("pv_estimate")); + optimisticForecastMap.put(periadEndZdt, jo.getDouble("pv_estimate")); } } } @@ -433,9 +439,13 @@ private QueryMode evalArguments(String[] args) { } } - public ZonedDateTime getZdtFromUTC(String utc) { - Instant timestamp = Instant.parse(utc); - return timestamp.atZone(timeZoneProvider.getTimeZone()); + 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; } - } 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 index b192aee393c56..35c363afd279c 100644 --- 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 @@ -22,7 +22,7 @@ */ @NonNullByDefault public class SolcastBridgeConfiguration { - public int channelRefreshInterval = -1; + public int channelRefreshInterval = 1; public String apiKey = SolarForecastBindingConstants.EMPTY; public String timeZone = SolarForecastBindingConstants.AUTODETECT; } 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 index 3d6afe4e3731a..0e2882639da00 100644 --- 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 @@ -24,6 +24,6 @@ public class SolcastPlaneConfiguration { public String resourceId = SolarForecastBindingConstants.EMPTY; public String powerItem = SolarForecastBindingConstants.EMPTY; - public long refreshInterval = -1; + public long refreshInterval = 120; public String powerUnit = SolarForecastBindingConstants.AUTODETECT; } 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 index d82983b071947..8bbf1d81e1f83 100644 --- 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 @@ -20,7 +20,6 @@ import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Optional; @@ -68,7 +67,7 @@ public SolcastBridgeHandler(Bridge bridge, TimeZoneProvider tzp) { @Override public Collection> getServices() { - return Collections.singleton(SolarForecastActions.class); + return List.of(SolarForecastActions.class); } @Override 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 index 927a6217bc7a2..56e281e811ca9 100644 --- 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 @@ -21,7 +21,6 @@ import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutionException; @@ -94,7 +93,7 @@ public SolcastPlaneHandler(Thing thing, HttpClient hc, Optional> getServices() { - return Collections.singleton(SolarForecastActions.class); + return List.of(SolarForecastActions.class); } @Override @@ -102,41 +101,40 @@ public void initialize() { SolcastPlaneConfiguration c = getConfigAs(SolcastPlaneConfiguration.class); configuration = Optional.of(c); - // initialize Power Item - if (!EMPTY.equals(c.powerItem)) { - // power item configured - Item item = itemRegistry.get(c.powerItem); - if (item != null) { - powerItem = Optional.of(item); - } else { - logger.debug("Item {} not found", c.powerItem); - } - } else { - logger.trace("No Power item configured"); - } - // connect Bridge & Status Bridge bridge = getBridge(); if (bridge != null) { BridgeHandler handler = bridge.getHandler(); if (handler != null) { - if (handler instanceof SolcastBridgeHandler) { - bridgeHandler = Optional.of((SolcastBridgeHandler) handler); + if (handler instanceof SolcastBridgeHandler sbh) { + bridgeHandler = Optional.of(sbh); bridgeHandler.get().addPlane(this); forecast = Optional.of(new SolcastObject(bridgeHandler.get())); nextMeasurement = Optional.of(Utils.getNextTimeframe(Instant.now(), bridgeHandler.get())); - - updateStatus(ThingStatus.ONLINE); + // initialize Power Item + if (!EMPTY.equals(c.powerItem)) { + // power item configured + Item item = itemRegistry.get(c.powerItem); + if (item != null) { + powerItem = Optional.of(item); + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/solarforecast.site.status.power-item [\"" + c.powerItem + "\"]"); + } + } else { + updateStatus(ThingStatus.ONLINE); + } } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "@text/solarforecast.site.status.wrong-handler" + " [\"" + handler + "\"]"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/solarforecast.site.status.wrong-handler [\"" + handler + "\"]"); } } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/solarforecast.site.status.bridge-handler-not-found"); } } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/solarforecast.site.status.bridge-missing"); } } 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 index 3bb50b0cf0f93..96b145d3c440e 100644 --- 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 @@ -20,7 +20,7 @@ - Installed Module power of this plane + Installed module power of this plane @@ -35,8 +35,8 @@ true - - Horizon definition as comma separated integer values + + 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 index e5c2e79eac8d1..603a7c5b83b3a 100644 --- 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 @@ -8,7 +8,7 @@ location - Location of Photovoltaic system + Location of photovoltaic system auto-detect @@ -22,7 +22,7 @@ - Inverter maximum Kilowatt Peak capability + Inverter maximum kilowatt peak capability 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 index cc25a35654e57..a56431f64d64d 100644 --- 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 @@ -7,7 +7,7 @@ - API Key from your subscription + API key from your subscription @@ -16,7 +16,7 @@ - Time Zone of Forecast Location + Time zone of forecast location auto-detect 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 index b65d50064fa27..67f821565b122 100644 --- 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 @@ -1,7 +1,7 @@ -# binding +# add-on -binding.solarforecast.name = SolarForecast Binding -binding.solarforecast.description = Solar Forecast for your location +addon.solarforecast.name = SolarForecast Binding +addon.solarforecast.description = Solar Forecast for your location # thing types @@ -18,8 +18,14 @@ thing-type.solarforecast.sc-site.description = Solcast service site definition 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 @@ -28,6 +34,8 @@ 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.channelRefreshInterval.label = Channel Refresh Interval thing-type.config.solarforecast.fs-site.channelRefreshInterval.description = Refresh rate of channel data +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.powerItem.label = Power Item @@ -99,13 +107,21 @@ channel-type.solarforecast.remaining-channel.description = Forecast of todays re channel-type.solarforecast.today-channel.label = Forecast Today channel-type.solarforecast.today-channel.description = Todays forecast in total +# binding + +binding.solarforecast.name = SolarForecast Binding +binding.solarforecast.description = Solar Forecast for your location + # status details + solarforecast.site.status.api-key-missing = API Key is mandatory solarforecast.plane.status.bridge-missing = Bridge not set solarforecast.plane.status.bridge-handler-not-found = BridgeHandler not found solarforecast.plane.status.wrong-handler = Wrong Handler {0} +solarforecast.plane.status.power-item = Power Item {0} not found # thing actions + actionDayLabel = Daily Energy Production actionDayDesc = Returns energy production for complete day in kw/h actionInputDayLabel = Date @@ -114,7 +130,7 @@ actionPowerLabel = Power actionPowerDesc = Returns power in kw for a specific point in time actionInputDateTimeLabel = Date Time actionInputDateTimeDesc = LocalDateTime for power query -actionEnergyLabel = Energy Production +actionEnergyLabel = Energy Production actionEnergyDesc = Returns energy productions between two different timestamps actionInputDateTimeBeginLabel = Timestamp Begin actionInputDateTimeBeginDesc = LocalDateTime as starting point of the energy query @@ -124,4 +140,3 @@ 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/test/java/org/openhab/binding/solarforecast/ForecastSolarTest.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/ForecastSolarTest.java index 8d99b8682f339..8b0c677936233 100644 --- 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 @@ -189,18 +189,4 @@ void testActions() { assertEquals(UnDefType.UNDEF, fo.getDay(queryDateTime.toLocalDate(), "pessimistic")); assertEquals(UnDefType.UNDEF, fo.getDay(queryDateTime.toLocalDate(), "total", "rubbish")); } - - /* - * public void testHorizon() throws URISyntaxException, IOException, InterruptedException { - * String url = "https://api.forecast.solar/estimate/50.55598767987004/8.49558522179684/12/-40/5.525"; - * String horizon = "2,2,2,2,1,1,3,3,4,3,3,4,3,3,3,3,4,5,7,5,4,2,2,2,2,1,1,1,1,1,2,2,2,2,1,2"; - * - * HttpClient client = HttpClient.newHttpClient(); - * HttpRequest request = HttpRequest.newBuilder().uri(new URI(url)).GET().build(); - * HttpResponse response = client.send(request, BodyHandlers.ofString()); - * url += "?horizon=" + horizon; - * request = HttpRequest.newBuilder().uri(new URI(url)).GET().build(); - * response = client.send(request, BodyHandlers.ofString()); - * } - */ } 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 index d3e8efcc290df..f8e620c1084ce 100644 --- 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 @@ -434,14 +434,4 @@ void testTimes() { LocalTime lt = zdt.toLocalTime(); assertEquals("21:30", lt.toString(), "LocalTime"); } - - @Test - void testCST() { - LocalDateTime ldt = LocalDateTime.of(2023, 3, 26, 1, 30); - System.out.println(ldt); - System.out.println(ldt.atZone(TEST_ZONE)); - ldt = LocalDateTime.of(2023, 3, 26, 2, 30); - System.out.println(ldt); - System.out.println(ldt.atZone(TEST_ZONE)); - } } From c86a311bb3f18a60739ab08b88c378ad17dbee4c Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Thu, 16 Nov 2023 23:48:41 +0100 Subject: [PATCH 056/111] review comments Signed-off-by: Bernd Weymann --- .../README.md | 152 ++-- .../forecastsolar/ForecastSolarObject.java | 12 +- .../ForecastSolarBridgeConfiguration.java | 2 +- .../handler/ForecastSolarBridgeHandler.java | 23 +- .../handler/ForecastSolarPlaneHandler.java | 5 +- .../internal/solcast/SolcastConstants.java | 3 - .../solcast/handler/SolcastBridgeHandler.java | 48 +- .../solcast/handler/SolcastPlaneHandler.java | 11 +- .../OH-INF/i18n/solarforecast.properties | 1 + .../src/test/resources/forecastsolar/horizon | 2 - .../forecastsolar/horizon_50.556_8.495.csv | 62 -- .../forecastsolar/horizon_50.556_8.495.json | 674 ------------------ .../test/resources/forecastsolar/result.txt | 4 - .../forecastsolar/resultWithHorizon.json | 102 --- .../forecastsolar/resultWithoutHorizon.json | 102 --- 15 files changed, 103 insertions(+), 1100 deletions(-) delete mode 100644 bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon delete mode 100644 bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon_50.556_8.495.csv delete mode 100644 bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon_50.556_8.495.json delete mode 100644 bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/result.txt delete mode 100644 bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/resultWithHorizon.json delete mode 100644 bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/resultWithoutHorizon.json diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 3079acc2dc3e7..099d740ff934a 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -11,11 +11,11 @@ Supported Services - [Forecast.Solar](https://forecast.solar/) - Public, Personal and Professional [plans](https://forecast.solar/#accounts) available -Display Forecast *Power values* and measures of *PV inverter* item +Display Power values of Forecast and PV Inverter items -Display added up values during the day of *Forecast* and *PV inverter* item. +Display Energy values of Forecast and PV inverter items Yellow line shows *Daily Total Forecast*. @@ -105,17 +105,17 @@ Forecasts are delivered up to 6 days in advance including Day*X* channels are referring to forecasts plus *X* days: 1 = tomorrow, 2 = day after tomorrow, ... -| Channel | Type | Description | Advanced | -|-------------------------|---------------|------------------------------------------|----------| -| actual | Number:Energy | Today's forecast till now | no | -| actual-power | Number:Power | Predicted power in this moment | no | -| remaining | Number:Energy | Forecast of today's remaining production | no | -| today | Number:Energy | Today's forecast in total | no | -| day*X* | Number:Energy | Day *X* forecast in total | no | -| day*X*-low | Number:Energy | Day *X* pessimistic forecast | no | -| day*X*-high | Number:Energy | Day *X* optimistic forecast | no | -| raw | String | Plain JSON response without conversions | yes | -| raw-tuning | String | JSON response from tuning call | yes | +| Channel | Type | Unit | Description | Advanced | +|-------------------------|---------------|------|------------------------------------------|----------| +| actual | Number:Energy | kWh | Today's forecast till now | no | +| actual-power | Number:Power | W | Predicted power in this moment | no | +| remaining | Number:Energy | kWh | Forecast of today's remaining production | no | +| today | Number:Energy | kWh | Today's forecast in total | no | +| day*X* | Number:Energy | kWh | Day *X* forecast in total | no | +| day*X*-low | Number:Energy | kWh | Day *X* pessimistic forecast | no | +| day*X*-high | Number:Energy | kWh | Day *X* optimistic forecast | no | +| raw | String | - | Plain JSON response without conversions | yes | +| raw-tuning | String | - | JSON response from tuning call | yes | ## ForecastSolar Configuration @@ -178,14 +178,14 @@ Forecasts are delivered up to 3 days for paid personal plans. Day*X* channels are referring to forecasts plus *X* days: 1 = tomorrow, 2 = day after tomorrow, ... -| Channel | Type | Description | Advanced | -|-------------------------|---------------|------------------------------------------|----------| -| actual | Number:Energy | Today's forecast till now | no | -| actual-power | Number:Power | Predicted power in this moment | no | -| remaining | Number:Energy | Forecast of today's remaining production | no | -| today | Number:Energy | Today's forecast in total | no | -| day*X* | Number:Energy | Day *X* forecast in total | no | -| raw | String | Plain JSON response without conversions | yes | +| Channel | Type | Unit | Description | Advanced | +|-------------------------|---------------|------|------------------------------------------|----------| +| actual | Number:Energy | kWh | Today's forecast till now | no | +| actual-power | Number:Power | W | Predicted power in this moment | no | +| remaining | Number:Energy | kWh | Forecast of today's remaining production | no | +| today | Number:Energy | kWh | Today's forecast in total | no | +| day*X* | Number:Energy | kWh | Day *X* forecast in total | no | +| raw | String | - | Plain JSON response without conversions | yes | ## Thing Actions @@ -197,80 +197,42 @@ Input for queries are `LocalDateTime` and `LocalDate` objects. See [Date Time](#date-time) section for more information. Double check your time zone in *openHAB - Settings - Regional Settings* which is crucial for calculation. -### Get Forecast Begin - -````java - /** - * Get the first date and time of forecast data - * - * @return your localized date time - */ - public LocalDateTime getForecastBegin(); -```` +### `getForecastBegin` Returns `LocalDateTime` of the earliest possible forecast data available. It's located in the past, e.g. Solcast provides data from the last 7 days. `LocalDateTime.MAX` is returned in case of no forecast data is available. -### Get Forecast End - -````java - /** - * Get the last date and time of forecast data - * - * @return your localized date time - */ - public LocalDateTime getForecastEnd(); -```` +### `getForecastEnd` Returns `LocalDateTime` of the latest possible forecast data available. `LocalDateTime.MIN` is returned in case of no forecast data is available. -### Get Power +### `getPower` -````java - /** - * Returns electric power at one specific point of time - * - * @param localDateTime - * @param args possible arguments from this interface - * @return QuantityType in kW - */ - public State getPower(LocalDateTime localDateTime, String... args); -```` +| Parameter | Type | Description | +|-----------|---------------|--------------------------------------------------------------------------------------------------------------| +| timestamp | LocalDateTime | Time stamp | +| mode | String | Choose `optimistic` or `pessimistic` to get values for a positive or negative future scenario. Only Solcast. | Returns `QuantityType` at the given `localDateTime`. Respect `getForecastBegin` and `getForecastEnd` to get a valid value. Check for `UndefType.UNDEF` in case of errors. -Solcast things are supporting arguments. -Choose `optimistic` or `pessimistic` to get values for a positive or negative future scenario. -For these scenarios `localDateTime` needs to be located between `now` and `getForecastEnd`. - -### Get Day +### `getDay` -````java - /** - * Returns electric energy production for one day - * - * @param localDate - * @param args possible arguments from this interface - * @return QuantityType in kW/h - */ - public State getDay(LocalDate localDate, String... args); -```` +| Parameter | Type | Description | +|-----------|---------------|--------------------------------------------------------------------------------------------------------------| +| date | LocalDate | Date of the day | +| mode | String | Choose `optimistic` or `pessimistic` to get values for a positive or negative future scenario. Only Solcast. | Returns `QuantityType` at the given `localDate`. -Respect `getForecastBegin` and `getForecastEnd` to avoid ambigouos values. +Respect `getForecastBegin` and `getForecastEnd` to avoid ambiguous values. Check for `UndefType.UNDEF` in case of errors. -Solcast things are supporting arguments. -Choose `optimistic` or `pessimistic` to get values for a positive or negative future scenario. -For these scenarios `localDate` needs to be between *today* and `getForecastEnd`. - -### Get Energy +### `getEnergy` -````java +```java /** * Returns electric energy between two timestamps * @@ -280,15 +242,17 @@ For these scenarios `localDate` needs to be between *today* and `getForecastEnd` * @return QuantityType in kW/h */ public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDateTimeEnd, String... args); -```` +``` -Returns `QuantityType` between the timestamps `localDateTimeBegin` and `localDateTimeEnd`. -Respect `getForecastBegin` and `getForecastEnd` to avoid ambigouos values. -Check for `UndefType.UNDEF` in case of errors. +| Parameter | Type | Description | +|-----------------|---------------|--------------------------------------------------------------------------------------------------------------| +| startTimestamp | LocalDateTime | Date of the day | +| endTimestamp | LocalDateTime | Date of the day | +| mode | String | Choose `optimistic` or `pessimistic` to get values for a positive or negative future scenario. Only Solcast. | -Solcast things are supporting arguments. -Choose `optimistic` or `pessimistic` to get values for a positive or negative future scenario. -For these scenarios `localDateTimeEnd` needs to be located between `now` and `getForecastEnd`. +Returns `QuantityType` between the timestamps `startTimestamp` and `endTimestamp`. +Respect `getForecastBegin` and `getForecastEnd` to avoid ambiguous values. +Check for `UndefType.UNDEF` in case of errors. ## Example @@ -297,16 +261,16 @@ Exchange the configuration data in [thing file](#thing-file) and you're ready to ### Thing file -````java +```java Bridge solarforecast:fs-site:homeSite "ForecastSolar Home" [ location="54.321,8.976", channelRefreshInterval=1] { Thing fs-plane homeSouthWest "ForecastSolar Home South-West" [ refreshInterval=10, azimuth=45, declination=35, kwp=5.5] Thing fs-plane homeNorthEast "ForecastSolar Home North-East" [ refreshInterval=10, azimuth=-145, declination=35, kwp=4.425] } -```` +``` ### Items file -```` +```java Number:Energy ForecastSolarHome_Actual "Actual Forecast Today [%.3f %unit%]" {channel="solarforecast:fs-site:homeSite:actual" } Number:Power ForecastSolarHome_Actual_Power "Actual Power Forecast [%.3f %unit%]" {channel="solarforecast:fs-site:homeSite:actual-power" } Number:Energy ForecastSolarHome_Remaining "Remaining Forecast Today [%.3f %unit%]" {channel="solarforecast:fs-site:homeSite:remaining" } @@ -324,11 +288,11 @@ Number:Power ForecastSolarHome_Actual_Power_SW "Actual SW Power Fore Number:Energy ForecastSolarHome_Remaining_SW "Remaining SW Forecast Today [%.3f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:remaining" } Number:Energy ForecastSolarHome_Today_SW "Total SW Forecast Today [%.3f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:today" } Number:Energy ForecastSolarHome_Day_SW "Tomorrow SW Forecast [%.3f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:day1" } -```` +``` ### Actions rule -```` +```java rule "Forecast Solar Actions" when Time cron "0 0 23 * * ?" // trigger whatever you like @@ -359,11 +323,11 @@ rule "Forecast Solar Actions" val twoDaysForecastFromNowValue = (twoDaysForecastFromNowState as Number).doubleValue logInfo("SF Tests","Forecast 2 days value: "+ twoDaysForecastFromNowValue) end -```` +``` shall produce following output -```` +``` 2022-08-07 18:02:19.874 [INFO ] [g.openhab.core.model.script.SF Tests] - Begin: 2022-07-31T18:30 End: 2022-08-14T18:00 2022-08-07 18:02:19.878 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast tomorrow state: 55.999 kWh 2022-08-07 18:02:19.880 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast tomorrow value: 55.999 @@ -371,14 +335,14 @@ shall produce following output 2022-08-07 18:02:19.886 [INFO ] [g.openhab.core.model.script.SF Tests] - Hour+1 power value: 2.497 2022-08-07 18:02:19.891 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast 2 days state: 112.483 kWh 2022-08-07 18:02:19.892 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast 2 days value: 112.483 -```` +``` ### Actions rule with Arguments -Only Solcast is deliering `optimistic` and `pessimistic` scenario data. +Only Solcast is delivering `optimistic` and `pessimistic` scenario data. If arguments are used on ForecastSolar `UNDEF` state is returned -```` +```java rule "Solcast Actions" when Time cron "0 0 23 * * ?" // trigger whatever you like @@ -390,15 +354,15 @@ rule "Solcast Actions" val sixDayPessimistic = solarforecastActions.getEnergy(LocalDateTime.now,LocalDateTime.now.plusDays(6),"pessimistic") logInfo("SF Tests","Forecast Pessimist 6 days "+ sixDayPessimistic) end -```` +``` shall produce following output -```` +``` 2022-08-10 00:02:16.569 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast Estimate 6 days 309.424 kWh 2022-08-10 00:02:16.574 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast Optimist 6 days 319.827 kWh 2022-08-10 00:02:16.578 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast Pessimist 6 days 208.235 kWh -```` +``` ## Date Time 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 index d13b1f0937310..0301c5425b032 100644 --- 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 @@ -18,6 +18,7 @@ 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; @@ -70,9 +71,14 @@ public ForecastSolarObject(String content, Instant expirationDate) { while (iter.hasNext()) { String dateStr = iter.next(); // convert date time into machine readable format - ZonedDateTime zdt = LocalDateTime.parse(dateStr, dateInputFormatter).atZone(zone); - wattHourMap.put(zdt, wattHourJson.getDouble(dateStr)); - wattMap.put(zdt, wattJson.getDouble(dateStr)); + 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("Exception parsing time {} Reason: {}", dateStr, dtpe.getMessage()); + return; + } } valid = true; String zoneStr = contentJson.getJSONObject("message").getJSONObject("info").getString("timezone"); 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 index d3079218aa4c3..b4cb42a580077 100644 --- 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 @@ -22,7 +22,7 @@ */ @NonNullByDefault public class ForecastSolarBridgeConfiguration { - public String location = "0.0,0.0"; + public String location = "auto-detect"; public int channelRefreshInterval = 1; 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/handler/ForecastSolarBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarBridgeHandler.java index dc175d59c475b..3a78ebfb89df6 100644 --- 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 @@ -40,8 +40,6 @@ import org.openhab.core.thing.binding.BaseBridgeHandler; import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.types.Command; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link ForecastSolarBridgeHandler} is a non active handler instance. It will be triggerer by the bridge. @@ -50,7 +48,6 @@ */ @NonNullByDefault public class ForecastSolarBridgeHandler extends BaseBridgeHandler implements SolarForecastProvider { - private final Logger logger = LoggerFactory.getLogger(ForecastSolarBridgeHandler.class); private final PointType homeLocation; private List parts = new ArrayList(); @@ -78,30 +75,14 @@ public void initialize() { } configuration = Optional.of(config); updateStatus(ThingStatus.ONLINE); - startSchedule(configuration.get().channelRefreshInterval); + refreshJob = Optional.of( + scheduler.scheduleWithFixedDelay(this::getData, 10, config.channelRefreshInterval, TimeUnit.MINUTES)); } @Override public void handleCommand(ChannelUID channelUID, Command command) { } - private void startSchedule(int interval) { - /** - * Interval given in minutes so seconds needs multiplier. Wait for 10 seconds until attached planes are created - * and registered. If now waiting time is defined user will see some glitches e.g. after restart if no or not - * all planes are initialized - */ - refreshJob.ifPresentOrElse(job -> { - if (job.isCancelled()) { - refreshJob = Optional - .of(scheduler.scheduleWithFixedDelay(this::getData, 10, interval * 60, TimeUnit.SECONDS)); - } // else - scheduler is already running! - }, () -> { - refreshJob = Optional - .of(scheduler.scheduleWithFixedDelay(this::getData, 10, interval * 60, TimeUnit.SECONDS)); - }); - } - /** * Get data for all planes. Synchronized to protect parts map from being modified during update */ 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 index 160dcf7cd03fd..d9dfa19a6aa32 100644 --- 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 @@ -87,7 +87,7 @@ public void initialize() { if (handler instanceof ForecastSolarBridgeHandler fsbh) { bridgeHandler = Optional.of(fsbh); bridgeHandler.get().addPlane(this); - updateStatus(ThingStatus.ONLINE); + updateStatus(ThingStatus.UNKNOWN); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/solarforecast.site.status.wrong-handler" + " [\"" + handler + "\"]"); @@ -149,11 +149,14 @@ protected ForecastSolarObject fetchData() { Instant.now().plus(configuration.get().refreshInterval, ChronoUnit.MINUTES)); setForecast(localForecast); updateState(CHANNEL_RAW, StringType.valueOf(cr.getContentAsString())); + updateStatus(ThingStatus.ONLINE); } else { logger.info("{} Call {} failed {}", thing.getLabel(), url, cr.getStatus()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); } } catch (InterruptedException | ExecutionException | TimeoutException e) { logger.info("{} Call {} failed {}", thing.getLabel(), url, e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); } } // else use available forecast updateChannels(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 index 0b14ffef32288..53f333a64a3f4 100644 --- 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 @@ -30,10 +30,7 @@ public class SolcastConstants { public static final String FORECAST_URL = "https://api.solcast.com.au/rooftop_sites/%s/forecasts?format=json&hours=168"; public static final String CURRENT_ESTIMATE_URL = "https://api.solcast.com.au/rooftop_sites/%s/estimated_actuals?format=json"; public static final String MEASUREMENT_URL = "https://api.solcast.com.au/rooftop_sites/%s/measurements?format=json"; - public static final String BEARER = "Bearer "; - public static final Unit KILOWATT_UNIT = MetricPrefix.KILO(Units.WATT); - public static final double UNDEF_DOUBLE = -1.0; } 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 index 8bbf1d81e1f83..fb26ee40ff0e0 100644 --- 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 @@ -59,10 +59,12 @@ public class SolcastBridgeHandler extends BaseBridgeHandler implements SolarFore private List parts = new ArrayList(); private Optional configuration = Optional.empty(); private Optional> refreshJob = Optional.empty(); + private ZoneId timeZone; public SolcastBridgeHandler(Bridge bridge, TimeZoneProvider tzp) { super(bridge); localTimeZoneProvider = tzp; + timeZone = tzp.getTimeZone(); } @Override @@ -75,8 +77,22 @@ public void initialize() { SolcastBridgeConfiguration config = getConfigAs(SolcastBridgeConfiguration.class); configuration = Optional.of(config); if (!EMPTY.equals(config.apiKey)) { - updateStatus(ThingStatus.ONLINE); - startSchedule(configuration.get().channelRefreshInterval); + if (!AUTODETECT.equals(configuration.get().timeZone)) { + try { + timeZone = ZoneId.of(configuration.get().timeZone); + updateStatus(ThingStatus.ONLINE); + refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 10, + configuration.get().channelRefreshInterval, TimeUnit.MINUTES)); + } catch (DateTimeException e) { + logger.warn("ConfiguredTimezone {} not found {}", configuration.get().timeZone, e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/solarforecast.site.status.timezone" + " [\"" + configuration.get().timeZone + "\"]"); + } + } else { + updateStatus(ThingStatus.ONLINE); + refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 10, + configuration.get().channelRefreshInterval, TimeUnit.MINUTES)); + } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/solarforecast.site.status.api-key-missing"); @@ -90,23 +106,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } - private void startSchedule(int interval) { - /** - * Interval given in minutes so seconds needs multiplier. Wait for 10 seconds until attached planes are created - * and registered. If now waiting time is defined user will see some glitches e.g. after restart if no or not - * all planes are initialized - */ - refreshJob.ifPresentOrElse(job -> { - if (job.isCancelled()) { - refreshJob = Optional - .of(scheduler.scheduleWithFixedDelay(this::getData, 10, interval * 60, TimeUnit.SECONDS)); - } // else - scheduler is already running! - }, () -> { - refreshJob = Optional - .of(scheduler.scheduleWithFixedDelay(this::getData, 10, interval * 60, TimeUnit.SECONDS)); - }); - } - @Override public void dispose() { refreshJob.ifPresent(job -> job.cancel(true)); @@ -221,15 +220,6 @@ public synchronized List getSolarForecasts() { @Override public ZoneId getTimeZone() { - if (AUTODETECT.equals(configuration.get().timeZone)) { - return localTimeZoneProvider.getTimeZone(); - } else { - try { - return ZoneId.of(configuration.get().timeZone); - } catch (DateTimeException e) { - logger.info("Timezone {} not found {}", configuration.get().timeZone, e.getMessage()); - return localTimeZoneProvider.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 index 56e281e811ca9..fcb9c7db130e6 100644 --- 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 @@ -117,13 +117,13 @@ public void initialize() { Item item = itemRegistry.get(c.powerItem); if (item != null) { powerItem = Optional.of(item); - updateStatus(ThingStatus.ONLINE); + updateStatus(ThingStatus.UNKNOWN); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/solarforecast.site.status.power-item [\"" + c.powerItem + "\"]"); } } else { - updateStatus(ThingStatus.ONLINE); + updateStatus(ThingStatus.UNKNOWN); } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, @@ -177,14 +177,18 @@ protected SolcastObject fetchData() { localForecast.join(crForecast.getContentAsString()); setForecast(localForecast); updateState(CHANNEL_RAW, StringType.valueOf(forecast.get().getRaw())); + updateStatus(ThingStatus.ONLINE); } else { logger.debug("{} Call {} failed {}", thing.getLabel(), forecastUrl, crForecast.getStatus()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); } } else { logger.debug("{} Call {} failed {}", thing.getLabel(), currentEstimateUrl, crEstimate.getStatus()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); } } catch (InterruptedException | ExecutionException | TimeoutException e) { logger.debug("{} Call {} failed {}", thing.getLabel(), currentEstimateUrl, e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); } } // else use available forecast updateChannels(forecast.get()); @@ -269,12 +273,15 @@ private void sendMeasure() { ContentResponse crMeasure = request.send(); if (crMeasure.getStatus() == 200) { updateState = StringType.valueOf(crMeasure.getContentAsString()); + updateStatus(ThingStatus.ONLINE); } else { logger.debug("{} Call {} failed {} - {}", thing.getLabel(), measureUrl, crMeasure.getStatus(), crMeasure.getContentAsString()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); } } catch (InterruptedException | TimeoutException | ExecutionException e) { logger.debug("{} Call {} failed {}", thing.getLabel(), measureUrl, e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); } } else { logger.debug("Persistence for {} empty", powerItem); 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 index 67f821565b122..ecaeaf2f3eb04 100644 --- 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 @@ -115,6 +115,7 @@ binding.solarforecast.description = Solar Forecast for your location # status details solarforecast.site.status.api-key-missing = API Key is mandatory +solarforecast.site.status.timezone = Time zone {0} not found solarforecast.plane.status.bridge-missing = Bridge not set solarforecast.plane.status.bridge-handler-not-found = BridgeHandler not found solarforecast.plane.status.wrong-handler = Wrong Handler {0} diff --git a/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon b/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon deleted file mode 100644 index 72a3d7518de96..0000000000000 --- a/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon +++ /dev/null @@ -1,2 +0,0 @@ -2,2,2,2,2,1,1,3,3,3,4,3,3,3,4,3,3,3,3,4,3,4,5,7,7,5,4,3,2,2,2,3,2,1,1,0,1,1,1,2,2,2,2,2,2,1,2,2 -2,2,2,2,1,1,3,3,4,3,3,4,3,3,3,3,4,5,7,5,4,2,2,2,2,1,1,1,1,1,2,2,2,2,1,2 \ No newline at end of file diff --git a/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon_50.556_8.495.csv b/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon_50.556_8.495.csv deleted file mode 100644 index 4594f2b9633ed..0000000000000 --- a/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon_50.556_8.495.csv +++ /dev/null @@ -1,62 +0,0 @@ -Latitude (deg.): 50.556 -Longitude (deg.): 8.495 - -A H_hor A_sun(w) H_sun(w) A_sun(s) H_sun(s) --180.0 1.5 -180.0 0.0 -180.0 0.0 --172.5 1.5 -165.1 0.0 -172.9 0.0 --165.0 1.5 -151.2 0.0 -165.8 0.0 --157.5 1.5 -138.8 0.0 -158.8 0.0 --150.0 1.5 -128.2 0.0 -152.1 0.0 --142.5 1.1 -119.0 0.0 -145.6 0.0 --135.0 1.1 -110.9 0.0 -139.3 0.0 --127.5 2.7 -103.8 0.0 -133.2 0.0 --120.0 3.1 -97.3 0.0 -127.4 0.9 --112.5 3.4 -91.2 0.0 -121.7 4.8 --105.0 3.8 -85.5 0.0 -116.2 9.0 --97.5 3.4 -80.0 0.0 -110.8 13.4 --90.0 3.4 -74.6 0.0 -105.4 17.9 --82.5 3.4 -69.2 0.0 -100.0 22.5 --75.0 3.8 -63.8 0.0 -94.5 27.3 --67.5 3.1 -58.3 0.0 -88.8 32.0 --60.0 3.1 -52.6 0.0 -82.7 36.8 --52.5 3.1 -46.8 2.7 -76.2 41.5 --45.0 3.1 -40.7 6.0 -69.1 46.0 --37.5 3.8 -34.4 8.9 -61.0 50.3 --30.0 3.4 -27.9 11.4 -51.8 54.3 --22.5 3.8 -21.2 13.4 -41.2 57.8 --15.0 5.3 -14.2 14.8 -28.8 60.5 --7.5 6.9 -7.1 15.7 -14.9 62.3 -0.0 6.9 0.0 16.0 0.0 62.9 -7.5 5.3 7.1 15.7 14.9 62.3 -15.0 3.8 14.2 14.8 28.8 60.5 -22.5 3.1 21.2 13.4 41.2 57.8 -30.0 2.3 27.9 11.4 51.8 54.3 -37.5 2.3 34.4 8.9 61.0 50.3 -45.0 2.3 40.7 6.0 69.1 46.0 -52.5 2.7 46.8 2.7 76.2 41.5 -60.0 2.3 52.6 0.0 82.7 36.8 -67.5 1.1 58.3 0.0 88.8 32.0 -75.0 0.8 63.8 0.0 94.5 27.3 -82.5 0.4 69.2 0.0 100.0 22.5 -90.0 0.8 74.6 0.0 105.4 17.9 -97.5 1.1 80.0 0.0 110.8 13.4 -105.0 1.1 85.5 0.0 116.2 9.0 -112.5 1.5 91.2 0.0 121.7 4.8 -120.0 1.9 97.3 0.0 127.4 0.9 -127.5 1.9 103.8 0.0 133.2 0.0 -135.0 1.5 110.9 0.0 139.3 0.0 -142.5 1.5 119.0 0.0 145.6 0.0 -150.0 1.5 128.2 0.0 152.1 0.0 -157.5 1.1 138.8 0.0 158.8 0.0 -165.0 1.5 151.2 0.0 165.8 0.0 -172.5 1.9 165.1 0.0 172.9 0.0 -180.0 1.5 180.0 0.0 180.0 0.0 - -A: Azimuth (0 = S, 90 = W, -90 = E) (degree) -H_hor: Horizon height (degree) -A_sun(w): Sun azimuth in the winter solstice (Dec 21) (0 = S, 90 = W, -90 = E) (degree) -H_sun(w): Sun height in the winter solstice (Dec 21) (degree) -A_sun(s): Sun azimuth in the summer solstice (June 21) (0 = S, 90 = W, -90 = E) (degree) -H_sun(s): Sun height in the summer solstice (June 21) (degree) - -PVGIS (c) European Union, 2001-2022 \ No newline at end of file diff --git a/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon_50.556_8.495.json b/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon_50.556_8.495.json deleted file mode 100644 index 72558323b0c0d..0000000000000 --- a/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/horizon_50.556_8.495.json +++ /dev/null @@ -1,674 +0,0 @@ -{ - "inputs": { - "location": { - "latitude": 50.556, - "longitude": 8.495, - "elevation": 149.0 - }, - "horizon_db": "DEM-calculated" - }, - "outputs": { - "horizon_profile": [ - { - "A": -180.0, - "H_hor": 1.5 - }, - { - "A": -172.5, - "H_hor": 1.5 - }, - { - "A": -165.0, - "H_hor": 1.5 - }, - { - "A": -157.5, - "H_hor": 1.5 - }, - { - "A": -150.0, - "H_hor": 1.5 - }, - { - "A": -142.5, - "H_hor": 1.1 - }, - { - "A": -135.0, - "H_hor": 1.1 - }, - { - "A": -127.5, - "H_hor": 2.7 - }, - { - "A": -120.0, - "H_hor": 3.1 - }, - { - "A": -112.5, - "H_hor": 3.4 - }, - { - "A": -105.0, - "H_hor": 3.8 - }, - { - "A": -97.5, - "H_hor": 3.4 - }, - { - "A": -90.0, - "H_hor": 3.4 - }, - { - "A": -82.5, - "H_hor": 3.4 - }, - { - "A": -75.0, - "H_hor": 3.8 - }, - { - "A": -67.5, - "H_hor": 3.1 - }, - { - "A": -60.0, - "H_hor": 3.1 - }, - { - "A": -52.5, - "H_hor": 3.1 - }, - { - "A": -45.0, - "H_hor": 3.1 - }, - { - "A": -37.5, - "H_hor": 3.8 - }, - { - "A": -30.0, - "H_hor": 3.4 - }, - { - "A": -22.5, - "H_hor": 3.8 - }, - { - "A": -15.0, - "H_hor": 5.3 - }, - { - "A": -7.5, - "H_hor": 6.9 - }, - { - "A": 0.0, - "H_hor": 6.9 - }, - { - "A": 7.5, - "H_hor": 5.3 - }, - { - "A": 15.0, - "H_hor": 3.8 - }, - { - "A": 22.5, - "H_hor": 3.1 - }, - { - "A": 30.0, - "H_hor": 2.3 - }, - { - "A": 37.5, - "H_hor": 2.3 - }, - { - "A": 45.0, - "H_hor": 2.3 - }, - { - "A": 52.5, - "H_hor": 2.7 - }, - { - "A": 60.0, - "H_hor": 2.3 - }, - { - "A": 67.5, - "H_hor": 1.1 - }, - { - "A": 75.0, - "H_hor": 0.8 - }, - { - "A": 82.5, - "H_hor": 0.4 - }, - { - "A": 90.0, - "H_hor": 0.8 - }, - { - "A": 97.5, - "H_hor": 1.1 - }, - { - "A": 105.0, - "H_hor": 1.1 - }, - { - "A": 112.5, - "H_hor": 1.5 - }, - { - "A": 120.0, - "H_hor": 1.9 - }, - { - "A": 127.5, - "H_hor": 1.9 - }, - { - "A": 135.0, - "H_hor": 1.5 - }, - { - "A": 142.5, - "H_hor": 1.5 - }, - { - "A": 150.0, - "H_hor": 1.5 - }, - { - "A": 157.5, - "H_hor": 1.1 - }, - { - "A": 165.0, - "H_hor": 1.5 - }, - { - "A": 172.5, - "H_hor": 1.9 - }, - { - "A": 180.0, - "H_hor": 1.5 - } - ], - "winter_solstice": [ - { - "A_sun(w)": -180.0, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": -165.1, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": -151.2, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": -138.8, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": -128.2, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": -119.0, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": -110.9, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": -103.8, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": -97.3, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": -91.2, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": -85.5, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": -80.0, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": -74.6, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": -69.2, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": -63.8, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": -58.3, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": -52.6, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": -46.8, - "H_sun(w)": 2.7 - }, - { - "A_sun(w)": -40.7, - "H_sun(w)": 6.0 - }, - { - "A_sun(w)": -34.4, - "H_sun(w)": 8.9 - }, - { - "A_sun(w)": -27.9, - "H_sun(w)": 11.4 - }, - { - "A_sun(w)": -21.2, - "H_sun(w)": 13.4 - }, - { - "A_sun(w)": -14.2, - "H_sun(w)": 14.8 - }, - { - "A_sun(w)": -7.1, - "H_sun(w)": 15.7 - }, - { - "A_sun(w)": 0.0, - "H_sun(w)": 16.0 - }, - { - "A_sun(w)": 7.1, - "H_sun(w)": 15.7 - }, - { - "A_sun(w)": 14.2, - "H_sun(w)": 14.8 - }, - { - "A_sun(w)": 21.2, - "H_sun(w)": 13.4 - }, - { - "A_sun(w)": 27.9, - "H_sun(w)": 11.4 - }, - { - "A_sun(w)": 34.4, - "H_sun(w)": 8.9 - }, - { - "A_sun(w)": 40.7, - "H_sun(w)": 6.0 - }, - { - "A_sun(w)": 46.8, - "H_sun(w)": 2.7 - }, - { - "A_sun(w)": 52.6, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": 58.3, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": 63.8, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": 69.2, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": 74.6, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": 80.0, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": 85.5, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": 91.2, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": 97.3, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": 103.8, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": 110.9, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": 119.0, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": 128.2, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": 138.8, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": 151.2, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": 165.1, - "H_sun(w)": 0.0 - }, - { - "A_sun(w)": 180.0, - "H_sun(w)": 0.0 - } - ], - "summer_solstice": [ - { - "A_sun(s)": -180.0, - "H_sun(s)": 0.0 - }, - { - "A_sun(s)": -172.9, - "H_sun(s)": 0.0 - }, - { - "A_sun(s)": -165.8, - "H_sun(s)": 0.0 - }, - { - "A_sun(s)": -158.8, - "H_sun(s)": 0.0 - }, - { - "A_sun(s)": -152.1, - "H_sun(s)": 0.0 - }, - { - "A_sun(s)": -145.6, - "H_sun(s)": 0.0 - }, - { - "A_sun(s)": -139.3, - "H_sun(s)": 0.0 - }, - { - "A_sun(s)": -133.2, - "H_sun(s)": 0.0 - }, - { - "A_sun(s)": -127.4, - "H_sun(s)": 0.9 - }, - { - "A_sun(s)": -121.7, - "H_sun(s)": 4.8 - }, - { - "A_sun(s)": -116.2, - "H_sun(s)": 9.0 - }, - { - "A_sun(s)": -110.8, - "H_sun(s)": 13.4 - }, - { - "A_sun(s)": -105.4, - "H_sun(s)": 17.9 - }, - { - "A_sun(s)": -100.0, - "H_sun(s)": 22.5 - }, - { - "A_sun(s)": -94.5, - "H_sun(s)": 27.3 - }, - { - "A_sun(s)": -88.8, - "H_sun(s)": 32.0 - }, - { - "A_sun(s)": -82.7, - "H_sun(s)": 36.8 - }, - { - "A_sun(s)": -76.2, - "H_sun(s)": 41.5 - }, - { - "A_sun(s)": -69.1, - "H_sun(s)": 46.0 - }, - { - "A_sun(s)": -61.0, - "H_sun(s)": 50.3 - }, - { - "A_sun(s)": -51.8, - "H_sun(s)": 54.3 - }, - { - "A_sun(s)": -41.2, - "H_sun(s)": 57.8 - }, - { - "A_sun(s)": -28.8, - "H_sun(s)": 60.5 - }, - { - "A_sun(s)": -14.9, - "H_sun(s)": 62.3 - }, - { - "A_sun(s)": 0.0, - "H_sun(s)": 62.9 - }, - { - "A_sun(s)": 14.9, - "H_sun(s)": 62.3 - }, - { - "A_sun(s)": 28.8, - "H_sun(s)": 60.5 - }, - { - "A_sun(s)": 41.2, - "H_sun(s)": 57.8 - }, - { - "A_sun(s)": 51.8, - "H_sun(s)": 54.3 - }, - { - "A_sun(s)": 61.0, - "H_sun(s)": 50.3 - }, - { - "A_sun(s)": 69.1, - "H_sun(s)": 46.0 - }, - { - "A_sun(s)": 76.2, - "H_sun(s)": 41.5 - }, - { - "A_sun(s)": 82.7, - "H_sun(s)": 36.8 - }, - { - "A_sun(s)": 88.8, - "H_sun(s)": 32.0 - }, - { - "A_sun(s)": 94.5, - "H_sun(s)": 27.3 - }, - { - "A_sun(s)": 100.0, - "H_sun(s)": 22.5 - }, - { - "A_sun(s)": 105.4, - "H_sun(s)": 17.9 - }, - { - "A_sun(s)": 110.8, - "H_sun(s)": 13.4 - }, - { - "A_sun(s)": 116.2, - "H_sun(s)": 9.0 - }, - { - "A_sun(s)": 121.7, - "H_sun(s)": 4.8 - }, - { - "A_sun(s)": 127.4, - "H_sun(s)": 0.9 - }, - { - "A_sun(s)": 133.2, - "H_sun(s)": 0.0 - }, - { - "A_sun(s)": 139.3, - "H_sun(s)": 0.0 - }, - { - "A_sun(s)": 145.6, - "H_sun(s)": 0.0 - }, - { - "A_sun(s)": 152.1, - "H_sun(s)": 0.0 - }, - { - "A_sun(s)": 158.8, - "H_sun(s)": 0.0 - }, - { - "A_sun(s)": 165.8, - "H_sun(s)": 0.0 - }, - { - "A_sun(s)": 172.9, - "H_sun(s)": 0.0 - }, - { - "A_sun(s)": 180.0, - "H_sun(s)": 0.0 - } - ] - }, - "meta": { - "inputs": { - "location": { - "description": "Selected location", - "variables": { - "latitude": { - "description": "Latitude", - "units": "decimal degree" - }, - "longitude": { - "description": "Longitude", - "units": "decimal degree" - }, - "elevation": { - "description": "Elevation", - "units": "m" - } - } - }, - "horizon_db": { - "description": "Source of horizon data" - } - }, - "outputs": { - "horizon_profile": { - "type": "series", - "description": "Horizon profile", - "variables": { - "A": { - "description": "Azimuth (0 = S, 90 = W, -90 = E)", - "units": "degree" - }, - "H_hor": { - "description": "Horizon height", - "units": "degree" - } - } - }, - "winter_solstice": { - "type": "series", - "description": "Winter solstice", - "variables": { - "A_sun(w)": { - "description": "Sun azimuth in the winter solstice (Dec 21) (0 = S, 90 = W, -90 = E)", - "units": "degree" - }, - "H_sun(w)": { - "description": "Sun height in the winter solstice (Dec 21)", - "units": "degree" - } - } - }, - "summer_solstice": { - "type": "series", - "description": "Summer solstice profile", - "variables": { - "A_sun(s)": { - "description": "Sun azimuth in the summer solstice (June 21) (0 = S, 90 = W, -90 = E)", - "units": "degree" - }, - "H_sun(s)": { - "description": "Sun height in the summer solstice (June 21)", - "units": "degree" - } - } - } - } - } -} \ No newline at end of file diff --git a/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/result.txt b/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/result.txt deleted file mode 100644 index 73c0de3eeba47..0000000000000 --- a/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/result.txt +++ /dev/null @@ -1,4 +0,0 @@ -https://api.forecast.solar/estimate/50.55598767987004/8.49558522179684/12/-40/5.525 -{"result":{"watts":{"2022-11-05 07:22:00":0,"2022-11-05 08:00:00":520,"2022-11-05 09:00:00":1426,"2022-11-05 10:00:00":2226,"2022-11-05 11:00:00":2379,"2022-11-05 12:00:00":2613,"2022-11-05 13:00:00":2596,"2022-11-05 14:00:00":2321,"2022-11-05 15:00:00":1775,"2022-11-05 16:00:00":712,"2022-11-05 16:57:00":0,"2022-11-06 07:23:00":0,"2022-11-06 08:00:00":361,"2022-11-06 09:00:00":842,"2022-11-06 10:00:00":994,"2022-11-06 11:00:00":1061,"2022-11-06 12:00:00":968,"2022-11-06 13:00:00":901,"2022-11-06 14:00:00":874,"2022-11-06 15:00:00":454,"2022-11-06 16:00:00":113,"2022-11-06 16:55:00":0},"watt_hours_period":{"2022-11-05 07:22:00":0,"2022-11-05 08:00:00":165,"2022-11-05 09:00:00":973,"2022-11-05 10:00:00":1826,"2022-11-05 11:00:00":2303,"2022-11-05 12:00:00":2496,"2022-11-05 13:00:00":2605,"2022-11-05 14:00:00":2459,"2022-11-05 15:00:00":2048,"2022-11-05 16:00:00":1244,"2022-11-05 16:57:00":338,"2022-11-06 07:23:00":0,"2022-11-06 08:00:00":111,"2022-11-06 09:00:00":602,"2022-11-06 10:00:00":918,"2022-11-06 11:00:00":1028,"2022-11-06 12:00:00":1015,"2022-11-06 13:00:00":935,"2022-11-06 14:00:00":888,"2022-11-06 15:00:00":664,"2022-11-06 16:00:00":284,"2022-11-06 16:55:00":52},"watt_hours":{"2022-11-05 07:22:00":0,"2022-11-05 08:00:00":165,"2022-11-05 09:00:00":1138,"2022-11-05 10:00:00":2964,"2022-11-05 11:00:00":5267,"2022-11-05 12:00:00":7763,"2022-11-05 13:00:00":10368,"2022-11-05 14:00:00":12827,"2022-11-05 15:00:00":14875,"2022-11-05 16:00:00":16119,"2022-11-05 16:57:00":16457,"2022-11-06 07:23:00":0,"2022-11-06 08:00:00":111,"2022-11-06 09:00:00":713,"2022-11-06 10:00:00":1631,"2022-11-06 11:00:00":2659,"2022-11-06 12:00:00":3674,"2022-11-06 13:00:00":4609,"2022-11-06 14:00:00":5497,"2022-11-06 15:00:00":6161,"2022-11-06 16:00:00":6445,"2022-11-06 16:55:00":6497},"watt_hours_day":{"2022-11-05":16457,"2022-11-06":6497}},"message":{"code":0,"type":"success","text":"","info":{"latitude":50.556,"longitude":8.4956,"distance":0,"place":"4b, Uferstra\u00dfe, Neustadt, Wetzlar, Lahn-Dill-Kreis, Hessen, 35576, Deutschland","timezone":"Europe/Berlin","time":"2022-11-05T16:11:37+01:00","time_utc":"2022-11-05T15:11:37+00:00"},"ratelimit":{"period":3600,"limit":12,"remaining":9}}} -https://api.forecast.solar/estimate/50.55598767987004/8.49558522179684/12/-40/5.525?horizon=2,2,2,2,1,1,3,3,4,3,3,4,3,3,3,3,4,5,7,5,4,2,2,2,2,1,1,1,1,1,2,2,2,2,1,2 -{"result":{"watts":{"2022-11-05 07:22:00":0,"2022-11-05 08:00:00":1434,"2022-11-05 09:00:00":2215,"2022-11-05 10:00:00":2561,"2022-11-05 11:00:00":2588,"2022-11-05 12:00:00":2577,"2022-11-05 13:00:00":2345,"2022-11-05 14:00:00":1736,"2022-11-05 15:00:00":1026,"2022-11-05 16:00:00":305,"2022-11-05 16:57:00":0,"2022-11-06 07:23:00":0,"2022-11-06 08:00:00":1028,"2022-11-06 09:00:00":1262,"2022-11-06 10:00:00":1231,"2022-11-06 11:00:00":1201,"2022-11-06 12:00:00":1042,"2022-11-06 13:00:00":739,"2022-11-06 14:00:00":672,"2022-11-06 15:00:00":278,"2022-11-06 16:00:00":51,"2022-11-06 16:55:00":0},"watt_hours_period":{"2022-11-05 07:22:00":0,"2022-11-05 08:00:00":454,"2022-11-05 09:00:00":1825,"2022-11-05 10:00:00":2388,"2022-11-05 11:00:00":2575,"2022-11-05 12:00:00":2583,"2022-11-05 13:00:00":2461,"2022-11-05 14:00:00":2041,"2022-11-05 15:00:00":1381,"2022-11-05 16:00:00":666,"2022-11-05 16:57:00":145,"2022-11-06 07:23:00":0,"2022-11-06 08:00:00":317,"2022-11-06 09:00:00":1145,"2022-11-06 10:00:00":1247,"2022-11-06 11:00:00":1216,"2022-11-06 12:00:00":1122,"2022-11-06 13:00:00":891,"2022-11-06 14:00:00":706,"2022-11-06 15:00:00":475,"2022-11-06 16:00:00":165,"2022-11-06 16:55:00":23},"watt_hours":{"2022-11-05 07:22:00":0,"2022-11-05 08:00:00":454,"2022-11-05 09:00:00":2279,"2022-11-05 10:00:00":4667,"2022-11-05 11:00:00":7242,"2022-11-05 12:00:00":9825,"2022-11-05 13:00:00":12286,"2022-11-05 14:00:00":14327,"2022-11-05 15:00:00":15708,"2022-11-05 16:00:00":16374,"2022-11-05 16:57:00":16519,"2022-11-06 07:23:00":0,"2022-11-06 08:00:00":317,"2022-11-06 09:00:00":1462,"2022-11-06 10:00:00":2709,"2022-11-06 11:00:00":3925,"2022-11-06 12:00:00":5047,"2022-11-06 13:00:00":5938,"2022-11-06 14:00:00":6644,"2022-11-06 15:00:00":7119,"2022-11-06 16:00:00":7284,"2022-11-06 16:55:00":7307},"watt_hours_day":{"2022-11-05":16519,"2022-11-06":7307}},"message":{"code":0,"type":"success","text":"","info":{"latitude":50.556,"longitude":8.4956,"distance":0,"place":"4b, Uferstra\u00dfe, Neustadt, Wetzlar, Lahn-Dill-Kreis, Hessen, 35576, Deutschland","timezone":"Europe/Berlin","time":"2022-11-05T16:11:38+01:00","time_utc":"2022-11-05T15:11:38+00:00"},"ratelimit":{"period":3600,"limit":12,"remaining":8}}} diff --git a/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/resultWithHorizon.json b/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/resultWithHorizon.json deleted file mode 100644 index d0342b0b3d30e..0000000000000 --- a/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/resultWithHorizon.json +++ /dev/null @@ -1,102 +0,0 @@ -{ - "result": { - "watts": { - "2022-11-02 07:17:00": 0, - "2022-11-02 08:00:00": 271, - "2022-11-02 09:00:00": 1077, - "2022-11-02 10:00:00": 1945, - "2022-11-02 11:00:00": 2122, - "2022-11-02 12:00:00": 2403, - "2022-11-02 13:00:00": 2508, - "2022-11-02 14:00:00": 2149, - "2022-11-02 15:00:00": 1387, - "2022-11-02 16:00:00": 663, - "2022-11-02 17:00:00": 55, - "2022-11-02 17:02:00": 0, - "2022-11-03 07:18:00": 0, - "2022-11-03 08:00:00": 50, - "2022-11-03 09:00:00": 127, - "2022-11-03 10:00:00": 166, - "2022-11-03 11:00:00": 249, - "2022-11-03 12:00:00": 296, - "2022-11-03 13:00:00": 287, - "2022-11-03 14:00:00": 199, - "2022-11-03 15:00:00": 127, - "2022-11-03 16:00:00": 66, - "2022-11-03 17:00:00": 0 - }, - "watt_hours_period": { - "2022-11-02 07:17:00": 0, - "2022-11-02 08:00:00": 97, - "2022-11-02 09:00:00": 674, - "2022-11-02 10:00:00": 1511, - "2022-11-02 11:00:00": 2034, - "2022-11-02 12:00:00": 2263, - "2022-11-02 13:00:00": 2456, - "2022-11-02 14:00:00": 2329, - "2022-11-02 15:00:00": 1768, - "2022-11-02 16:00:00": 1025, - "2022-11-02 17:00:00": 359, - "2022-11-02 17:02:00": 1, - "2022-11-03 07:18:00": 0, - "2022-11-03 08:00:00": 18, - "2022-11-03 09:00:00": 89, - "2022-11-03 10:00:00": 147, - "2022-11-03 11:00:00": 208, - "2022-11-03 12:00:00": 273, - "2022-11-03 13:00:00": 292, - "2022-11-03 14:00:00": 243, - "2022-11-03 15:00:00": 163, - "2022-11-03 16:00:00": 97, - "2022-11-03 17:00:00": 33 - }, - "watt_hours": { - "2022-11-02 07:17:00": 0, - "2022-11-02 08:00:00": 97, - "2022-11-02 09:00:00": 771, - "2022-11-02 10:00:00": 2282, - "2022-11-02 11:00:00": 4316, - "2022-11-02 12:00:00": 6579, - "2022-11-02 13:00:00": 9035, - "2022-11-02 14:00:00": 11364, - "2022-11-02 15:00:00": 13132, - "2022-11-02 16:00:00": 14157, - "2022-11-02 17:00:00": 14516, - "2022-11-02 17:02:00": 14517, - "2022-11-03 07:18:00": 0, - "2022-11-03 08:00:00": 18, - "2022-11-03 09:00:00": 107, - "2022-11-03 10:00:00": 254, - "2022-11-03 11:00:00": 462, - "2022-11-03 12:00:00": 735, - "2022-11-03 13:00:00": 1027, - "2022-11-03 14:00:00": 1270, - "2022-11-03 15:00:00": 1433, - "2022-11-03 16:00:00": 1530, - "2022-11-03 17:00:00": 1563 - }, - "watt_hours_day": { - "2022-11-02": 14517, - "2022-11-03": 1563 - } - }, - "message": { - "code": 0, - "type": "success", - "text": "", - "info": { - "latitude": 50.556, - "longitude": 8.4956, - "distance": 0, - "place": "4b, Uferstra\u00dfe, Neustadt, Wetzlar, Lahn-Dill-Kreis, Hessen, 35576, Deutschland", - "timezone": "Europe/Berlin", - "time": "2022-11-02T23:27:03+01:00", - "time_utc": "2022-11-02T22:27:03+00:00" - }, - "ratelimit": { - "period": 3600, - "limit": 12, - "remaining": 5 - } - } -} diff --git a/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/resultWithoutHorizon.json b/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/resultWithoutHorizon.json deleted file mode 100644 index 295827a422dba..0000000000000 --- a/bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/resultWithoutHorizon.json +++ /dev/null @@ -1,102 +0,0 @@ -{ - "result": { - "watts": { - "2022-11-02 07:17:00": 0, - "2022-11-02 08:00:00": 315, - "2022-11-02 09:00:00": 1122, - "2022-11-02 10:00:00": 1867, - "2022-11-02 11:00:00": 2006, - "2022-11-02 12:00:00": 2326, - "2022-11-02 13:00:00": 2528, - "2022-11-02 14:00:00": 2155, - "2022-11-02 15:00:00": 1365, - "2022-11-02 16:00:00": 597, - "2022-11-02 17:00:00": 17, - "2022-11-02 17:02:00": 0, - "2022-11-03 07:18:00": 0, - "2022-11-03 08:00:00": 33, - "2022-11-03 09:00:00": 88, - "2022-11-03 10:00:00": 155, - "2022-11-03 11:00:00": 265, - "2022-11-03 12:00:00": 398, - "2022-11-03 13:00:00": 398, - "2022-11-03 14:00:00": 265, - "2022-11-03 15:00:00": 127, - "2022-11-03 16:00:00": 39, - "2022-11-03 17:00:00": 0 - }, - "watt_hours_period": { - "2022-11-02 07:17:00": 0, - "2022-11-02 08:00:00": 113, - "2022-11-02 09:00:00": 719, - "2022-11-02 10:00:00": 1495, - "2022-11-02 11:00:00": 1937, - "2022-11-02 12:00:00": 2166, - "2022-11-02 13:00:00": 2427, - "2022-11-02 14:00:00": 2342, - "2022-11-02 15:00:00": 1760, - "2022-11-02 16:00:00": 981, - "2022-11-02 17:00:00": 307, - "2022-11-02 17:02:00": 0, - "2022-11-03 07:18:00": 0, - "2022-11-03 08:00:00": 12, - "2022-11-03 09:00:00": 61, - "2022-11-03 10:00:00": 122, - "2022-11-03 11:00:00": 210, - "2022-11-03 12:00:00": 332, - "2022-11-03 13:00:00": 398, - "2022-11-03 14:00:00": 332, - "2022-11-03 15:00:00": 196, - "2022-11-03 16:00:00": 83, - "2022-11-03 17:00:00": 20 - }, - "watt_hours": { - "2022-11-02 07:17:00": 0, - "2022-11-02 08:00:00": 113, - "2022-11-02 09:00:00": 832, - "2022-11-02 10:00:00": 2327, - "2022-11-02 11:00:00": 4264, - "2022-11-02 12:00:00": 6430, - "2022-11-02 13:00:00": 8857, - "2022-11-02 14:00:00": 11199, - "2022-11-02 15:00:00": 12959, - "2022-11-02 16:00:00": 13940, - "2022-11-02 17:00:00": 14247, - "2022-11-02 17:02:00": 14247, - "2022-11-03 07:18:00": 0, - "2022-11-03 08:00:00": 12, - "2022-11-03 09:00:00": 73, - "2022-11-03 10:00:00": 195, - "2022-11-03 11:00:00": 405, - "2022-11-03 12:00:00": 737, - "2022-11-03 13:00:00": 1135, - "2022-11-03 14:00:00": 1467, - "2022-11-03 15:00:00": 1663, - "2022-11-03 16:00:00": 1746, - "2022-11-03 17:00:00": 1766 - }, - "watt_hours_day": { - "2022-11-02": 14247, - "2022-11-03": 1766 - } - }, - "message": { - "code": 0, - "type": "success", - "text": "", - "info": { - "latitude": 50.556, - "longitude": 8.4956, - "distance": 0, - "place": "4b, Uferstra\u00dfe, Neustadt, Wetzlar, Lahn-Dill-Kreis, Hessen, 35576, Deutschland", - "timezone": "Europe/Berlin", - "time": "2022-11-02T23:27:02+01:00", - "time_utc": "2022-11-02T22:27:02+00:00" - }, - "ratelimit": { - "period": 3600, - "limit": 12, - "remaining": 6 - } - } -} From 4e297a7e85bf8ef866656047c8154cc36673c4b4 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Fri, 17 Nov 2023 00:21:08 +0100 Subject: [PATCH 057/111] review comments Signed-off-by: Bernd Weymann --- bundles/org.openhab.binding.solarforecast/README.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 099d740ff934a..43a3a63166f95 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -232,18 +232,6 @@ Check for `UndefType.UNDEF` in case of errors. ### `getEnergy` -```java - /** - * Returns electric energy between two timestamps - * - * @param localDateTimeBegin - * @param localDateTimeEnd - * @param args possible arguments from this interface - * @return QuantityType in kW/h - */ - public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDateTimeEnd, String... args); -``` - | Parameter | Type | Description | |-----------------|---------------|--------------------------------------------------------------------------------------------------------------| | startTimestamp | LocalDateTime | Date of the day | From 73bf4b5b37f029b10f05bfae298fb9625c4b500c Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Fri, 17 Nov 2023 00:26:59 +0100 Subject: [PATCH 058/111] bugfix: zone calculation Signed-off-by: Bernd Weymann --- .../internal/forecastsolar/ForecastSolarObject.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 0301c5425b032..f4abf1066f728 100644 --- 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 @@ -66,6 +66,8 @@ public ForecastSolarObject(String content, Instant expirationDate) { 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); Iterator iter = wattHourJson.keys(); // put all values of the current day into sorted tree map while (iter.hasNext()) { @@ -81,8 +83,6 @@ public ForecastSolarObject(String content, Instant expirationDate) { } } valid = true; - String zoneStr = contentJson.getJSONObject("message").getJSONObject("info").getString("timezone"); - zone = ZoneId.of(zoneStr); } catch (JSONException je) { logger.debug("Error parsing JSON response {} - {}", content, je.getMessage()); } From 6cb8257b0c8127d42f074d303ee4557f401d5b33 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Fri, 17 Nov 2023 09:58:02 +0100 Subject: [PATCH 059/111] switch to ZonedDateTime Signed-off-by: Bernd Weymann --- .../internal/actions/SolarForecast.java | 20 +++--- .../actions/SolarForecastActions.java | 68 +++++++++---------- .../forecastsolar/ForecastSolarObject.java | 33 +++++---- .../internal/solcast/SolcastObject.java | 50 ++++++-------- .../solarforecast/ForecastSolarTest.java | 6 +- .../binding/solarforecast/SolcastTest.java | 14 ++-- 6 files changed, 92 insertions(+), 99 deletions(-) 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 index 616915c7965f4..5fdd73278a96f 100644 --- 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 @@ -13,7 +13,7 @@ package org.openhab.binding.solarforecast.internal.actions; import java.time.LocalDate; -import java.time.LocalDateTime; +import java.time.ZonedDateTime; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.types.State; @@ -37,42 +37,42 @@ public interface SolarForecast { /** * Returns electric energy production for one day * - * @param localDate + * @param date * @param args possible arguments from this interface * @return QuantityType in kW/h */ - public State getDay(LocalDate localDate, String... args); + public State getDay(LocalDate date, String... args); /** * Returns electric energy between two timestamps * - * @param localDateTimeBegin - * @param localDateTimeEnd + * @param start + * @param end * @param args possible arguments from this interface * @return QuantityType in kW/h */ - public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDateTimeEnd, String... args); + public State getEnergy(ZonedDateTime start, ZonedDateTime end, String... args); /** * Returns electric power at one specific point of time * - * @param localDateTime + * @param timestamp * @param args possible arguments from this interface * @return QuantityType in kW */ - public State getPower(LocalDateTime localDateTime, String... args); + public State getPower(ZonedDateTime timestamp, String... args); /** * Get the first date and time of forecast data * * @return your localized date time */ - public LocalDateTime getForecastBegin(); + public ZonedDateTime getForecastBegin(); /** * Get the last date and time of forecast data * * @return your localized date time */ - public LocalDateTime getForecastEnd(); + public ZonedDateTime getForecastEnd(); } 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 index daafb3750b65f..ebfe2d4eaf642 100644 --- 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 @@ -14,6 +14,8 @@ import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Iterator; import java.util.List; import java.util.Optional; @@ -79,7 +81,7 @@ public State getDay( @RuleAction(label = "@text/actionPowerLabel", description = "@text/actionPowerDesc") public State getPower( - @ActionInput(name = "localDateTime", label = "@text/actionInputDateTimeLabel", description = "@text/actionInputDateTimeDesc") LocalDateTime localDateTime, + @ActionInput(name = "timestamp", label = "@text/actionInputDateTimeLabel", description = "@text/actionInputDateTimeDesc") ZonedDateTime timestamp, String... args) { if (thingHandler.isPresent()) { List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); @@ -87,18 +89,18 @@ public State getPower( QuantityType measure = QuantityType.valueOf(0, MetricPrefix.KILO(Units.WATT)); for (Iterator iterator = l.iterator(); iterator.hasNext();) { SolarForecast solarForecast = iterator.next(); - State s = solarForecast.getPower(localDateTime, args); + State s = solarForecast.getPower(timestamp, args); if (s instanceof QuantityType quantityState) { measure = measure.add((QuantityType) quantityState); } else { // break in case of failure getting values to avoid ambiguous values - logger.trace("Ambiguous measure {} found for {} - return UNDEF", s, localDateTime); + logger.trace("Ambiguous measure {} found for {} - return UNDEF", s, timestamp); return UnDefType.UNDEF; } } return measure; } else { - logger.trace("No forecasts found for {} - return UNDEF", localDateTime); + logger.trace("No forecasts found for {} - return UNDEF", timestamp); return UnDefType.UNDEF; } } else { @@ -109,8 +111,8 @@ public State getPower( @RuleAction(label = "@text/actionEnergyLabel", description = "@text/actionEnergyDesc") public State getEnergy( - @ActionInput(name = "localDateTimeBegin", label = "@text/actionInputDateTimeBeginLabel", description = "@text/actionInputDateTimeBeginDesc") LocalDateTime localDateTimeBegin, - @ActionInput(name = "localDateTimeEnd", label = "@text/actionInputDateTimeEndLabel", description = "@text/actionInputDateTimeEndDesc") LocalDateTime localDateTimeEnd, + @ActionInput(name = "start", label = "@text/actionInputDateTimeBeginLabel", description = "@text/actionInputDateTimeBeginDesc") ZonedDateTime start, + @ActionInput(name = "end", label = "@text/actionInputDateTimeEndLabel", description = "@text/actionInputDateTimeEndDesc") ZonedDateTime end, String... args) { if (thingHandler.isPresent()) { List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); @@ -118,20 +120,18 @@ public State getEnergy( QuantityType measure = QuantityType.valueOf(0, Units.KILOWATT_HOUR); for (Iterator iterator = l.iterator(); iterator.hasNext();) { SolarForecast solarForecast = iterator.next(); - State s = solarForecast.getEnergy(localDateTimeBegin, localDateTimeEnd, args); + State s = solarForecast.getEnergy(start, end, args); if (s instanceof QuantityType quantityState) { measure = measure.add((QuantityType) quantityState); } else { // break in case of failure getting values to avoid ambiguous values - logger.trace("Ambiguous measure {} found between {} and {} - return UNDEF", s, - localDateTimeBegin, localDateTimeEnd); + logger.trace("Ambiguous measure {} found between {} and {} - return UNDEF", s, start, end); return UnDefType.UNDEF; } } return measure; } else { - logger.trace("No forecasts found for between {} and {} - return UNDEF", localDateTimeBegin, - localDateTimeEnd); + logger.trace("No forecasts found for between {} and {} - return UNDEF", start, end); return UnDefType.UNDEF; } } else { @@ -141,60 +141,60 @@ public State getEnergy( } @RuleAction(label = "@text/actionForecastBeginLabel", description = "@text/actionForecastBeginDesc") - public LocalDateTime getForecastBegin() { - LocalDateTime returnLdt = LocalDateTime.MAX; + public ZonedDateTime getForecastBegin() { + ZonedDateTime returnZdt = LocalDateTime.MAX.atZone(ZoneId.systemDefault()); if (thingHandler.isPresent()) { List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); if (!l.isEmpty()) { for (Iterator iterator = l.iterator(); iterator.hasNext();) { SolarForecast solarForecast = iterator.next(); - LocalDateTime forecastLdt = solarForecast.getForecastBegin(); + ZonedDateTime begin = solarForecast.getForecastBegin(); // break in case of failure getting values to avoid ambiguous values - if (forecastLdt.equals(LocalDateTime.MAX)) { - return LocalDateTime.MAX; + if (begin.toLocalDateTime().equals(LocalDateTime.MAX)) { + return LocalDateTime.MAX.atZone(ZoneId.systemDefault()); } // take latest possible timestamp to avoid ambiguous values - if (forecastLdt.isBefore(returnLdt)) { - returnLdt = forecastLdt; + if (begin.isBefore(returnZdt)) { + returnZdt = begin; } } - return returnLdt; + return returnZdt; } else { logger.trace("No forecasts found - return invalid date MAX"); - return LocalDateTime.MAX; + return LocalDateTime.MAX.atZone(ZoneId.systemDefault()); } } else { logger.trace("Handler missing - return invalid date MAX"); - return LocalDateTime.MAX; + return LocalDateTime.MAX.atZone(ZoneId.systemDefault()); } } @RuleAction(label = "@text/actionForecastEndLabel", description = "@text/actionForecastEndDesc") - public LocalDateTime getForecastEnd() { - LocalDateTime returnLdt = LocalDateTime.MIN; + public ZonedDateTime getForecastEnd() { + ZonedDateTime returnZdt = LocalDateTime.MIN.atZone(ZoneId.systemDefault()); if (thingHandler.isPresent()) { List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); if (!l.isEmpty()) { for (Iterator iterator = l.iterator(); iterator.hasNext();) { SolarForecast solarForecast = iterator.next(); - LocalDateTime forecastLdt = solarForecast.getForecastEnd(); + ZonedDateTime forecastLdt = solarForecast.getForecastEnd(); // break in case of failure getting values to avoid ambiguous values if (forecastLdt.equals(LocalDateTime.MIN)) { - return LocalDateTime.MIN; + return LocalDateTime.MIN.atZone(ZoneId.systemDefault()); } // take earliest possible timestamp to avoid ambiguous values - if (forecastLdt.isAfter(returnLdt)) { - returnLdt = forecastLdt; + if (forecastLdt.isAfter(returnZdt)) { + returnZdt = forecastLdt; } } - return returnLdt; + return returnZdt; } else { logger.trace("No forecasts found - return invalid date MIN"); - return LocalDateTime.MIN; + return LocalDateTime.MIN.atZone(ZoneId.systemDefault()); } } else { logger.trace("Handler missing - return invalid date MIN"); - return LocalDateTime.MIN; + return LocalDateTime.MIN.atZone(ZoneId.systemDefault()); } } @@ -202,19 +202,19 @@ public static State getDay(ThingActions actions, LocalDate ld, String... args) { return ((SolarForecastActions) actions).getDay(ld, args); } - public static State getPower(ThingActions actions, LocalDateTime dateTime, String... args) { + public static State getPower(ThingActions actions, ZonedDateTime dateTime, String... args) { return ((SolarForecastActions) actions).getPower(dateTime, args); } - public static State getEnergy(ThingActions actions, LocalDateTime begin, LocalDateTime end, String... args) { + public static State getEnergy(ThingActions actions, ZonedDateTime begin, ZonedDateTime end, String... args) { return ((SolarForecastActions) actions).getEnergy(begin, end, args); } - public static LocalDateTime getForecastBegin(ThingActions actions) { + public static ZonedDateTime getForecastBegin(ThingActions actions) { return ((SolarForecastActions) actions).getForecastBegin(); } - public static LocalDateTime getForecastEnd(ThingActions actions) { + public static ZonedDateTime getForecastEnd(ThingActions actions) { return ((SolarForecastActions) actions).getForecastEnd(); } 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 index f4abf1066f728..90091d5ad9a98 100644 --- 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 @@ -227,19 +227,18 @@ public State getDay(LocalDate localDate, String... args) { } @Override - public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDateTimeEnd, String... args) { + public State getEnergy(ZonedDateTime start, ZonedDateTime end, String... args) { if (args.length > 0) { logger.info("ForecastSolar doesn't accept arguments"); return UnDefType.UNDEF; } - LocalDate beginDate = localDateTimeBegin.toLocalDate(); - LocalDate endDate = localDateTimeEnd.toLocalDate(); + LocalDate beginDate = start.toLocalDate(); + LocalDate endDate = end.toLocalDate(); double measure = UNDEF; if (beginDate.equals(endDate)) { - measure = getDayTotal(beginDate) - getActualValue(localDateTimeBegin.atZone(zone)) - - getRemainingProduction(localDateTimeEnd.atZone(zone)); + measure = getDayTotal(beginDate) - getActualValue(start) - getRemainingProduction(end); } else { - measure = getRemainingProduction(localDateTimeBegin.atZone(zone)); + measure = getRemainingProduction(start); beginDate = beginDate.plusDays(1); while (beginDate.isBefore(endDate) && measure >= 0) { double day = getDayTotal(beginDate); @@ -248,7 +247,7 @@ public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDate } beginDate = beginDate.plusDays(1); } - double lastDay = getActualValue(localDateTimeEnd.atZone(zone)); + double lastDay = getActualValue(end); if (lastDay >= 0) { measure += lastDay; } @@ -257,31 +256,31 @@ public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDate } @Override - public State getPower(LocalDateTime localDateTime, String... args) { + public State getPower(ZonedDateTime timestamp, String... args) { if (args.length > 0) { logger.info("ForecastSolar doesn't accept arguments"); return UnDefType.UNDEF; } - double measure = getActualPowerValue(localDateTime.atZone(zone)); + double measure = getActualPowerValue(timestamp); return Utils.getPowerState(measure); } @Override - public LocalDateTime getForecastBegin() { + public ZonedDateTime getForecastBegin() { if (!wattHourMap.isEmpty()) { - LocalDateTime ldt = wattHourMap.firstEntry().getKey().toLocalDateTime(); - return ldt; + ZonedDateTime zdt = wattHourMap.firstEntry().getKey(); + return zdt; } - return LocalDateTime.MIN; + return LocalDateTime.MAX.atZone(zone); } @Override - public LocalDateTime getForecastEnd() { + public ZonedDateTime getForecastEnd() { if (!wattHourMap.isEmpty()) { - LocalDateTime ldt = wattHourMap.lastEntry().getKey().toLocalDateTime(); - return ldt; + ZonedDateTime zdt = wattHourMap.lastEntry().getKey(); + return zdt; } - return LocalDateTime.MIN; + return LocalDateTime.MIN.atZone(zone); } public ZoneId getZone() { 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 index e9320920d6b5b..b269546d4be81 100644 --- 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 @@ -328,45 +328,43 @@ private TreeMap getDataMap(LocalDate ld, QueryMode mode) * SolarForecast Interface */ @Override - public State getDay(LocalDate localDate, String... args) { + public State getDay(LocalDate date, String... args) { QueryMode mode = evalArguments(args); if (mode.equals(QueryMode.Error)) { return UnDefType.UNDEF; } else if (mode.equals(QueryMode.Optimistic) || mode.equals(QueryMode.Pessimistic)) { - if (localDate.isBefore(LocalDate.now())) { + if (date.isBefore(LocalDate.now())) { logger.info("{} forecasts only available for future", mode); return UnDefType.UNDEF; } } - double measure = getDayTotal(localDate, mode); + double measure = getDayTotal(date, mode); return Utils.getEnergyState(measure); } @Override - public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDateTimeEnd, String... args) { - if (localDateTimeEnd.isBefore(localDateTimeBegin)) { - logger.info("End {} defined before Start {}", localDateTimeEnd, localDateTimeBegin); + public State getEnergy(ZonedDateTime start, ZonedDateTime end, String... args) { + if (end.isBefore(start)) { + logger.info("End {} defined before Start {}", end, start); return UnDefType.UNDEF; } QueryMode mode = evalArguments(args); if (mode.equals(QueryMode.Error)) { return UnDefType.UNDEF; } else if (mode.equals(QueryMode.Optimistic) || mode.equals(QueryMode.Pessimistic)) { - if (localDateTimeEnd.isBefore(LocalDateTime.now())) { + if (end.isBefore(ZonedDateTime.now())) { logger.info("{} forecasts only available for future", mode); return UnDefType.UNDEF; } } - ZonedDateTime zdtBegin = localDateTimeBegin.atZone(timeZoneProvider.getTimeZone()); - ZonedDateTime zdtEnd = localDateTimeEnd.atZone(timeZoneProvider.getTimeZone()); - LocalDate beginDate = zdtBegin.toLocalDate(); - LocalDate endDate = zdtEnd.toLocalDate(); + LocalDate beginDate = start.toLocalDate(); + LocalDate endDate = end.toLocalDate(); double measure = UNDEF; if (beginDate.isEqual(endDate)) { - measure = getDayTotal(zdtEnd.toLocalDate(), mode) - getActualValue(zdtBegin, mode) - - getRemainingProduction(zdtEnd, mode); + measure = getDayTotal(end.toLocalDate(), mode) - getActualValue(start, mode) + - getRemainingProduction(end, mode); } else { - measure = getRemainingProduction(zdtBegin, mode); + measure = getRemainingProduction(start, mode); beginDate = beginDate.plusDays(1); while (beginDate.isBefore(endDate) && measure >= 0) { double day = getDayTotal(beginDate, mode); @@ -375,7 +373,7 @@ public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDate } beginDate = beginDate.plusDays(1); } - double lastDay = getActualValue(zdtEnd, mode); + double lastDay = getActualValue(end, mode); if (lastDay >= 0) { measure += lastDay; } @@ -384,39 +382,35 @@ public State getEnergy(LocalDateTime localDateTimeBegin, LocalDateTime localDate } @Override - public State getPower(LocalDateTime queryDateTime, String... args) { + public State getPower(ZonedDateTime timestamp, String... args) { // eliminate error cases and return immediately QueryMode mode = evalArguments(args); if (mode.equals(QueryMode.Error)) { return UnDefType.UNDEF; } else if (mode.equals(QueryMode.Optimistic) || mode.equals(QueryMode.Pessimistic)) { - if (queryDateTime.isBefore(LocalDateTime.now().minusMinutes(1))) { + if (timestamp.isBefore(ZonedDateTime.now().minusMinutes(1))) { logger.info("{} forecasts only available for future", mode); return UnDefType.UNDEF; } } - - ZonedDateTime zdt = queryDateTime.atZone(timeZoneProvider.getTimeZone()); - double measure = getActualPowerValue(zdt, mode); + double measure = getActualPowerValue(timestamp, mode); return Utils.getPowerState(measure); } @Override - public LocalDateTime getForecastBegin() { + public ZonedDateTime getForecastBegin() { if (!estimationDataMap.isEmpty()) { - ZonedDateTime zdt = estimationDataMap.firstEntry().getValue().firstEntry().getKey(); - return zdt.toLocalDateTime(); + return estimationDataMap.firstEntry().getValue().firstEntry().getKey(); } - return LocalDateTime.MIN; + return LocalDateTime.MAX.atZone(timeZoneProvider.getTimeZone()); } @Override - public LocalDateTime getForecastEnd() { + public ZonedDateTime getForecastEnd() { if (!estimationDataMap.isEmpty()) { - ZonedDateTime zdt = estimationDataMap.lastEntry().getValue().lastEntry().getKey(); - return zdt.toLocalDateTime(); + return estimationDataMap.lastEntry().getValue().lastEntry().getKey(); } - return LocalDateTime.MIN; + return LocalDateTime.MIN.atZone(timeZoneProvider.getTimeZone()); } private QueryMode evalArguments(String[] args) { 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 index 8b0c677936233..ef1e8c47e1d63 100644 --- 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 @@ -168,8 +168,8 @@ void testCornerCases() { @Test void testActions() { String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); - LocalDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 23); - ForecastSolarObject fo = new ForecastSolarObject(content, queryDateTime.atZone(TEST_ZONE).toInstant()); + ZonedDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 23).atZone(TEST_ZONE); + ForecastSolarObject fo = new ForecastSolarObject(content, queryDateTime.toInstant()); assertEquals("2022-07-17T05:31:00", fo.getForecastBegin().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), "Forecast begin"); assertEquals("2022-07-18T21:31:00", fo.getForecastEnd().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), @@ -177,7 +177,7 @@ void testActions() { assertEquals(QuantityType.valueOf(63.583, Units.KILOWATT_HOUR).toString(), fo.getDay(queryDateTime.toLocalDate()).toFullString(), "Actual out of scope"); - queryDateTime = LocalDateTime.of(2022, 7, 17, 0, 0); + queryDateTime = LocalDateTime.of(2022, 7, 17, 0, 0).atZone(TEST_ZONE); // "watt_hours_day": { // "2022-07-17": 63583, // "2022-07-18": 65554 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 index f8e620c1084ce..10fed250faf4a 100644 --- 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 @@ -262,14 +262,14 @@ void testActions() { content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); - assertEquals("2022-07-10T23:30", scfo.getForecastBegin().toString(), "Forecast begin"); - assertEquals("2022-07-24T23:00", scfo.getForecastEnd().toString(), "Forecast end"); + assertEquals("2022-07-10T23:30+02:00[Europe/Berlin]", scfo.getForecastBegin().toString(), "Forecast begin"); + assertEquals("2022-07-24T23:00+02:00[Europe/Berlin]", scfo.getForecastEnd().toString(), "Forecast end"); // test daily forecasts + cumulated getEnergy double totalEnergy = 0; - LocalDateTime ldtStartTime = LocalDateTime.of(2022, 7, 18, 0, 0); + ZonedDateTime start = LocalDateTime.of(2022, 7, 18, 0, 0).atZone(TEST_ZONE); for (int i = 0; i < 7; i++) { - QuantityType qt = (QuantityType) scfo.getDay(ldtStartTime.toLocalDate().plusDays(i)); - QuantityType eqt = (QuantityType) scfo.getEnergy(ldtStartTime.plusDays(i), ldtStartTime.plusDays(i + 1)); + QuantityType qt = (QuantityType) scfo.getDay(start.toLocalDate().plusDays(i)); + QuantityType eqt = (QuantityType) scfo.getEnergy(start.plusDays(i), start.plusDays(i + 1)); // check if energy calculation fits to daily query assertEquals(qt.doubleValue(), eqt.doubleValue(), TOLERANCE, "Total " + i + " days forecast"); @@ -277,7 +277,7 @@ void testActions() { totalEnergy += qt.doubleValue(); // check if sum is fitting to total energy query - qt = (QuantityType) scfo.getEnergy(ldtStartTime, ldtStartTime.plusDays(i + 1)); + qt = (QuantityType) scfo.getEnergy(start, start.plusDays(i + 1)); // System.out.println("Total: " + qt.doubleValue()); assertEquals(totalEnergy, qt.doubleValue(), TOLERANCE * 2, "Total " + i + " days forecast"); } @@ -304,7 +304,7 @@ void testOptimisticPessimistic() { "Estimation"); // access in past shall be rejected - LocalDateTime past = LocalDateTime.now().minusMinutes(5); + ZonedDateTime past = ZonedDateTime.now().minusMinutes(5); assertEquals(UnDefType.UNDEF, scfo.getPower(past, SolarForecast.OPTIMISTIC), "Optimistic Power"); assertEquals(UnDefType.UNDEF, scfo.getPower(past, SolarForecast.PESSIMISTIC), "Pessimistic Power"); assertEquals(UnDefType.UNDEF, scfo.getPower(past, "total", "rubbish"), "Rubbish arguments"); From ff345c7aedc53aa592c440ce0038f6d8e575db6a Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sat, 18 Nov 2023 16:58:57 +0100 Subject: [PATCH 060/111] Instant interface Signed-off-by: Bernd Weymann --- .../internal/actions/SolarForecast.java | 10 ++-- .../actions/SolarForecastActions.java | 58 +++++++++---------- .../forecastsolar/ForecastSolarObject.java | 37 ++++++------ .../internal/solcast/SolcastObject.java | 56 +++++++++--------- .../solarforecast/ForecastSolarTest.java | 10 ++-- .../binding/solarforecast/SolcastTest.java | 18 ++++-- 6 files changed, 98 insertions(+), 91 deletions(-) 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 index 5fdd73278a96f..eff395907bc36 100644 --- 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 @@ -12,8 +12,8 @@ */ package org.openhab.binding.solarforecast.internal.actions; +import java.time.Instant; import java.time.LocalDate; -import java.time.ZonedDateTime; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.types.State; @@ -51,7 +51,7 @@ public interface SolarForecast { * @param args possible arguments from this interface * @return QuantityType in kW/h */ - public State getEnergy(ZonedDateTime start, ZonedDateTime end, String... args); + public State getEnergy(Instant start, Instant end, String... args); /** * Returns electric power at one specific point of time @@ -60,19 +60,19 @@ public interface SolarForecast { * @param args possible arguments from this interface * @return QuantityType in kW */ - public State getPower(ZonedDateTime timestamp, String... args); + public State getPower(Instant timestamp, String... args); /** * Get the first date and time of forecast data * * @return your localized date time */ - public ZonedDateTime getForecastBegin(); + public Instant getForecastBegin(); /** * Get the last date and time of forecast data * * @return your localized date time */ - public ZonedDateTime getForecastEnd(); + public Instant getForecastEnd(); } 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 index ebfe2d4eaf642..1e190290c02ef 100644 --- 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 @@ -12,10 +12,8 @@ */ package org.openhab.binding.solarforecast.internal.actions; +import java.time.Instant; import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; import java.util.Iterator; import java.util.List; import java.util.Optional; @@ -81,7 +79,7 @@ public State getDay( @RuleAction(label = "@text/actionPowerLabel", description = "@text/actionPowerDesc") public State getPower( - @ActionInput(name = "timestamp", label = "@text/actionInputDateTimeLabel", description = "@text/actionInputDateTimeDesc") ZonedDateTime timestamp, + @ActionInput(name = "timestamp", label = "@text/actionInputDateTimeLabel", description = "@text/actionInputDateTimeDesc") Instant timestamp, String... args) { if (thingHandler.isPresent()) { List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); @@ -111,8 +109,8 @@ public State getPower( @RuleAction(label = "@text/actionEnergyLabel", description = "@text/actionEnergyDesc") public State getEnergy( - @ActionInput(name = "start", label = "@text/actionInputDateTimeBeginLabel", description = "@text/actionInputDateTimeBeginDesc") ZonedDateTime start, - @ActionInput(name = "end", label = "@text/actionInputDateTimeEndLabel", description = "@text/actionInputDateTimeEndDesc") ZonedDateTime end, + @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(); @@ -141,60 +139,60 @@ public State getEnergy( } @RuleAction(label = "@text/actionForecastBeginLabel", description = "@text/actionForecastBeginDesc") - public ZonedDateTime getForecastBegin() { - ZonedDateTime returnZdt = LocalDateTime.MAX.atZone(ZoneId.systemDefault()); + public Instant getForecastBegin() { + Instant returnBeginTime = Instant.MAX; if (thingHandler.isPresent()) { List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); if (!l.isEmpty()) { for (Iterator iterator = l.iterator(); iterator.hasNext();) { SolarForecast solarForecast = iterator.next(); - ZonedDateTime begin = solarForecast.getForecastBegin(); + Instant begin = solarForecast.getForecastBegin(); // break in case of failure getting values to avoid ambiguous values - if (begin.toLocalDateTime().equals(LocalDateTime.MAX)) { - return LocalDateTime.MAX.atZone(ZoneId.systemDefault()); + if (begin.equals(Instant.MAX)) { + return Instant.MAX; } // take latest possible timestamp to avoid ambiguous values - if (begin.isBefore(returnZdt)) { - returnZdt = begin; + if (begin.isBefore(returnBeginTime)) { + returnBeginTime = begin; } } - return returnZdt; + return returnBeginTime; } else { logger.trace("No forecasts found - return invalid date MAX"); - return LocalDateTime.MAX.atZone(ZoneId.systemDefault()); + return returnBeginTime; } } else { logger.trace("Handler missing - return invalid date MAX"); - return LocalDateTime.MAX.atZone(ZoneId.systemDefault()); + return returnBeginTime; } } @RuleAction(label = "@text/actionForecastEndLabel", description = "@text/actionForecastEndDesc") - public ZonedDateTime getForecastEnd() { - ZonedDateTime returnZdt = LocalDateTime.MIN.atZone(ZoneId.systemDefault()); + public Instant getForecastEnd() { + Instant returnEndTime = Instant.MIN; if (thingHandler.isPresent()) { List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); if (!l.isEmpty()) { for (Iterator iterator = l.iterator(); iterator.hasNext();) { SolarForecast solarForecast = iterator.next(); - ZonedDateTime forecastLdt = solarForecast.getForecastEnd(); + Instant forecastEndTime = solarForecast.getForecastEnd(); // break in case of failure getting values to avoid ambiguous values - if (forecastLdt.equals(LocalDateTime.MIN)) { - return LocalDateTime.MIN.atZone(ZoneId.systemDefault()); + if (forecastEndTime.equals(Instant.MIN)) { + return Instant.MIN; } // take earliest possible timestamp to avoid ambiguous values - if (forecastLdt.isAfter(returnZdt)) { - returnZdt = forecastLdt; + if (forecastEndTime.isAfter(returnEndTime)) { + returnEndTime = forecastEndTime; } } - return returnZdt; + return returnEndTime; } else { logger.trace("No forecasts found - return invalid date MIN"); - return LocalDateTime.MIN.atZone(ZoneId.systemDefault()); + return returnEndTime; } } else { logger.trace("Handler missing - return invalid date MIN"); - return LocalDateTime.MIN.atZone(ZoneId.systemDefault()); + return returnEndTime; } } @@ -202,19 +200,19 @@ public static State getDay(ThingActions actions, LocalDate ld, String... args) { return ((SolarForecastActions) actions).getDay(ld, args); } - public static State getPower(ThingActions actions, ZonedDateTime dateTime, String... args) { + public static State getPower(ThingActions actions, Instant dateTime, String... args) { return ((SolarForecastActions) actions).getPower(dateTime, args); } - public static State getEnergy(ThingActions actions, ZonedDateTime begin, ZonedDateTime end, String... args) { + public static State getEnergy(ThingActions actions, Instant begin, Instant end, String... args) { return ((SolarForecastActions) actions).getEnergy(begin, end, args); } - public static ZonedDateTime getForecastBegin(ThingActions actions) { + public static Instant getForecastBegin(ThingActions actions) { return ((SolarForecastActions) actions).getForecastBegin(); } - public static ZonedDateTime getForecastEnd(ThingActions actions) { + public static Instant getForecastEnd(ThingActions actions) { return ((SolarForecastActions) actions).getForecastEnd(); } 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 index 90091d5ad9a98..2526f4c53844c 100644 --- 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 @@ -208,6 +208,10 @@ public double getRemainingProduction(ZonedDateTime queryDateTime) { return daily - actual; } + public ZoneId getZone() { + return zone; + } + @Override public String toString() { return "Expiration: " + expirationDateTime + ", Valid: " + valid + ", Data:" + wattHourMap; @@ -227,18 +231,19 @@ public State getDay(LocalDate localDate, String... args) { } @Override - public State getEnergy(ZonedDateTime start, ZonedDateTime end, String... args) { + public State getEnergy(Instant start, Instant end, String... args) { if (args.length > 0) { logger.info("ForecastSolar doesn't accept arguments"); return UnDefType.UNDEF; } - LocalDate beginDate = start.toLocalDate(); - LocalDate endDate = end.toLocalDate(); + LocalDate beginDate = start.atZone(zone).toLocalDate(); + LocalDate endDate = end.atZone(zone).toLocalDate(); double measure = UNDEF; if (beginDate.equals(endDate)) { - measure = getDayTotal(beginDate) - getActualValue(start) - getRemainingProduction(end); + measure = getDayTotal(beginDate) - getActualValue(start.atZone(zone)) + - getRemainingProduction(end.atZone(zone)); } else { - measure = getRemainingProduction(start); + measure = getRemainingProduction(start.atZone(zone)); beginDate = beginDate.plusDays(1); while (beginDate.isBefore(endDate) && measure >= 0) { double day = getDayTotal(beginDate); @@ -247,7 +252,7 @@ public State getEnergy(ZonedDateTime start, ZonedDateTime end, String... args) { } beginDate = beginDate.plusDays(1); } - double lastDay = getActualValue(end); + double lastDay = getActualValue(end.atZone(zone)); if (lastDay >= 0) { measure += lastDay; } @@ -256,34 +261,30 @@ public State getEnergy(ZonedDateTime start, ZonedDateTime end, String... args) { } @Override - public State getPower(ZonedDateTime timestamp, String... args) { + public State getPower(Instant timestamp, String... args) { if (args.length > 0) { logger.info("ForecastSolar doesn't accept arguments"); return UnDefType.UNDEF; } - double measure = getActualPowerValue(timestamp); + double measure = getActualPowerValue(timestamp.atZone(zone)); return Utils.getPowerState(measure); } @Override - public ZonedDateTime getForecastBegin() { + public Instant getForecastBegin() { if (!wattHourMap.isEmpty()) { ZonedDateTime zdt = wattHourMap.firstEntry().getKey(); - return zdt; + return zdt.toInstant(); } - return LocalDateTime.MAX.atZone(zone); + return Instant.MAX; } @Override - public ZonedDateTime getForecastEnd() { + public Instant getForecastEnd() { if (!wattHourMap.isEmpty()) { ZonedDateTime zdt = wattHourMap.lastEntry().getKey(); - return zdt; + return zdt.toInstant(); } - return LocalDateTime.MIN.atZone(zone); - } - - public ZoneId getZone() { - return zone; + return Instant.MIN; } } 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 index b269546d4be81..541f271224959 100644 --- 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 @@ -14,9 +14,9 @@ import java.time.Instant; import java.time.LocalDate; -import java.time.LocalDateTime; import java.time.ZonedDateTime; import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Map.Entry; import java.util.Optional; @@ -324,6 +324,16 @@ private TreeMap getDataMap(LocalDate ld, QueryMode mode) 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 */ @@ -343,7 +353,7 @@ public State getDay(LocalDate date, String... args) { } @Override - public State getEnergy(ZonedDateTime start, ZonedDateTime end, String... args) { + public State getEnergy(Instant start, Instant end, String... args) { if (end.isBefore(start)) { logger.info("End {} defined before Start {}", end, start); return UnDefType.UNDEF; @@ -352,19 +362,19 @@ public State getEnergy(ZonedDateTime start, ZonedDateTime end, String... args) { if (mode.equals(QueryMode.Error)) { return UnDefType.UNDEF; } else if (mode.equals(QueryMode.Optimistic) || mode.equals(QueryMode.Pessimistic)) { - if (end.isBefore(ZonedDateTime.now())) { + if (end.isBefore(Instant.now())) { logger.info("{} forecasts only available for future", mode); return UnDefType.UNDEF; } } - LocalDate beginDate = start.toLocalDate(); - LocalDate endDate = end.toLocalDate(); + LocalDate beginDate = start.atZone(timeZoneProvider.getTimeZone()).toLocalDate(); + LocalDate endDate = end.atZone(timeZoneProvider.getTimeZone()).toLocalDate(); double measure = UNDEF; if (beginDate.isEqual(endDate)) { - measure = getDayTotal(end.toLocalDate(), mode) - getActualValue(start, mode) - - getRemainingProduction(end, mode); + measure = getDayTotal(beginDate, mode) - getActualValue(start.atZone(timeZoneProvider.getTimeZone()), mode) + - getRemainingProduction(end.atZone(timeZoneProvider.getTimeZone()), mode); } else { - measure = getRemainingProduction(start, mode); + measure = getRemainingProduction(start.atZone(timeZoneProvider.getTimeZone()), mode); beginDate = beginDate.plusDays(1); while (beginDate.isBefore(endDate) && measure >= 0) { double day = getDayTotal(beginDate, mode); @@ -373,7 +383,7 @@ public State getEnergy(ZonedDateTime start, ZonedDateTime end, String... args) { } beginDate = beginDate.plusDays(1); } - double lastDay = getActualValue(end, mode); + double lastDay = getActualValue(end.atZone(timeZoneProvider.getTimeZone()), mode); if (lastDay >= 0) { measure += lastDay; } @@ -382,35 +392,35 @@ public State getEnergy(ZonedDateTime start, ZonedDateTime end, String... args) { } @Override - public State getPower(ZonedDateTime timestamp, String... args) { + public State getPower(Instant timestamp, String... args) { // eliminate error cases and return immediately QueryMode mode = evalArguments(args); if (mode.equals(QueryMode.Error)) { return UnDefType.UNDEF; } else if (mode.equals(QueryMode.Optimistic) || mode.equals(QueryMode.Pessimistic)) { - if (timestamp.isBefore(ZonedDateTime.now().minusMinutes(1))) { + if (timestamp.isBefore(Instant.now().minus(1, ChronoUnit.MINUTES))) { logger.info("{} forecasts only available for future", mode); return UnDefType.UNDEF; } } - double measure = getActualPowerValue(timestamp, mode); + double measure = getActualPowerValue(ZonedDateTime.ofInstant(timestamp, timeZoneProvider.getTimeZone()), mode); return Utils.getPowerState(measure); } @Override - public ZonedDateTime getForecastBegin() { + public Instant getForecastBegin() { if (!estimationDataMap.isEmpty()) { - return estimationDataMap.firstEntry().getValue().firstEntry().getKey(); + return estimationDataMap.firstEntry().getValue().firstEntry().getKey().toInstant(); } - return LocalDateTime.MAX.atZone(timeZoneProvider.getTimeZone()); + return Instant.MAX; } @Override - public ZonedDateTime getForecastEnd() { + public Instant getForecastEnd() { if (!estimationDataMap.isEmpty()) { - return estimationDataMap.lastEntry().getValue().lastEntry().getKey(); + return estimationDataMap.lastEntry().getValue().lastEntry().getKey().toInstant(); } - return LocalDateTime.MIN.atZone(timeZoneProvider.getTimeZone()); + return Instant.MIN; } private QueryMode evalArguments(String[] args) { @@ -432,14 +442,4 @@ private QueryMode evalArguments(String[] args) { return QueryMode.Estimation; } } - - 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; - } } 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 index ef1e8c47e1d63..43a9f632948c3 100644 --- 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 @@ -170,10 +170,11 @@ void testActions() { 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(content, queryDateTime.toInstant()); - assertEquals("2022-07-17T05:31:00", fo.getForecastBegin().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), + 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().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), - "Forecast end"); + 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"); @@ -183,7 +184,8 @@ void testActions() { // "2022-07-18": 65554 // } assertEquals(QuantityType.valueOf(129.137, Units.KILOWATT_HOUR).toString(), - fo.getEnergy(queryDateTime, queryDateTime.plusDays(2)).toFullString(), "Actual out of scope"); + fo.getEnergy(queryDateTime.toInstant(), queryDateTime.plusDays(2).toInstant()).toFullString(), + "Actual out of scope"); assertEquals(UnDefType.UNDEF, fo.getDay(queryDateTime.toLocalDate(), "optimistic")); assertEquals(UnDefType.UNDEF, fo.getDay(queryDateTime.toLocalDate(), "pessimistic")); 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 index 10fed250faf4a..5738bfa3db88c 100644 --- 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 @@ -14,10 +14,12 @@ 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 org.eclipse.jdt.annotation.NonNullByDefault; import org.json.JSONObject; @@ -262,14 +264,17 @@ void testActions() { content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); - assertEquals("2022-07-10T23:30+02:00[Europe/Berlin]", scfo.getForecastBegin().toString(), "Forecast begin"); - assertEquals("2022-07-24T23:00+02:00[Europe/Berlin]", scfo.getForecastEnd().toString(), "Forecast end"); + 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 < 7; i++) { QuantityType qt = (QuantityType) scfo.getDay(start.toLocalDate().plusDays(i)); - QuantityType eqt = (QuantityType) scfo.getEnergy(start.plusDays(i), start.plusDays(i + 1)); + QuantityType eqt = (QuantityType) 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"); @@ -277,7 +282,7 @@ void testActions() { totalEnergy += qt.doubleValue(); // check if sum is fitting to total energy query - qt = (QuantityType) scfo.getEnergy(start, start.plusDays(i + 1)); + qt = (QuantityType) scfo.getEnergy(start.toInstant(), start.plusDays(i + 1).toInstant()); // System.out.println("Total: " + qt.doubleValue()); assertEquals(totalEnergy, qt.doubleValue(), TOLERANCE * 2, "Total " + i + " days forecast"); } @@ -304,11 +309,12 @@ void testOptimisticPessimistic() { "Estimation"); // access in past shall be rejected - ZonedDateTime past = ZonedDateTime.now().minusMinutes(5); + Instant past = Instant.now().minus(5, ChronoUnit.MINUTES); assertEquals(UnDefType.UNDEF, scfo.getPower(past, SolarForecast.OPTIMISTIC), "Optimistic Power"); assertEquals(UnDefType.UNDEF, scfo.getPower(past, SolarForecast.PESSIMISTIC), "Pessimistic Power"); assertEquals(UnDefType.UNDEF, scfo.getPower(past, "total", "rubbish"), "Rubbish arguments"); - assertEquals(UnDefType.UNDEF, scfo.getPower(past.plusHours(2), "total", "rubbish"), "Rubbish arguments"); + assertEquals(UnDefType.UNDEF, scfo.getPower(past.plus(2, ChronoUnit.HOURS), "total", "rubbish"), + "Rubbish arguments"); assertEquals(UnDefType.UNDEF, scfo.getPower(past), "Normal Power"); } From d0e642378f105d897a709811e1301af1aa082b1d Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sat, 18 Nov 2023 21:21:24 +0100 Subject: [PATCH 061/111] thing status bugfixes Signed-off-by: Bernd Weymann --- .../handler/ForecastSolarPlaneHandler.java | 16 ++++++++------ .../solcast/handler/SolcastBridgeHandler.java | 5 +---- .../solcast/handler/SolcastPlaneHandler.java | 22 +++++++++++-------- .../OH-INF/i18n/solarforecast.properties | 8 ++++--- 4 files changed, 28 insertions(+), 23 deletions(-) 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 index d9dfa19a6aa32..def1b4dd5d2b5 100644 --- 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 @@ -86,19 +86,20 @@ public void initialize() { if (handler != null) { if (handler instanceof ForecastSolarBridgeHandler fsbh) { bridgeHandler = Optional.of(fsbh); + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, + "@text/solarforecast.plane.status.await-feedback"); bridgeHandler.get().addPlane(this); - updateStatus(ThingStatus.UNKNOWN); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "@text/solarforecast.site.status.wrong-handler" + " [\"" + handler + "\"]"); + "@text/solarforecast.plane.status.wrong-handler" + " [\"" + handler + "\"]"); } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "@text/solarforecast.site.status.bridge-handler-not-found"); + "@text/solarforecast.plane.status.bridge-handler-not-found"); } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "@text/solarforecast.site.status.bridge-missing"); + "@text/solarforecast.plane.status.bridge-missing"); } } @@ -152,16 +153,17 @@ protected ForecastSolarObject fetchData() { updateStatus(ThingStatus.ONLINE); } else { logger.info("{} Call {} failed {}", thing.getLabel(), url, cr.getStatus()); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/solarforecast.plane.status.http-status [\"" + cr.getStatus() + "\"]"); } } catch (InterruptedException | ExecutionException | TimeoutException e) { logger.info("{} Call {} failed {}", thing.getLabel(), url, e.getMessage()); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); } } // else use available forecast updateChannels(forecast); } else { - logger.info("{} Location not present", thing.getLabel()); + logger.warn("{} Location not present", thing.getLabel()); } return forecast; } 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 index fb26ee40ff0e0..f606c2d15db60 100644 --- 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 @@ -47,14 +47,13 @@ import org.slf4j.LoggerFactory; /** - * The {@link SolcastBridgeHandler} is a non active handler instance. It will be triggerer by the bridge. + * 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 final TimeZoneProvider localTimeZoneProvider; private List parts = new ArrayList(); private Optional configuration = Optional.empty(); @@ -63,7 +62,6 @@ public class SolcastBridgeHandler extends BaseBridgeHandler implements SolarFore public SolcastBridgeHandler(Bridge bridge, TimeZoneProvider tzp) { super(bridge); - localTimeZoneProvider = tzp; timeZone = tzp.getTimeZone(); } @@ -84,7 +82,6 @@ public void initialize() { refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 10, configuration.get().channelRefreshInterval, TimeUnit.MINUTES)); } catch (DateTimeException e) { - logger.warn("ConfiguredTimezone {} not found {}", configuration.get().timeZone, e.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/solarforecast.site.status.timezone" + " [\"" + configuration.get().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 index fcb9c7db130e6..c05b7946a111d 100644 --- 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 @@ -117,25 +117,27 @@ public void initialize() { Item item = itemRegistry.get(c.powerItem); if (item != null) { powerItem = Optional.of(item); - updateStatus(ThingStatus.UNKNOWN); + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, + "@text/solarforecast.plane.status.await-feedback"); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "@text/solarforecast.site.status.power-item [\"" + c.powerItem + "\"]"); + "@text/solarforecast.plane.status.power-item [\"" + c.powerItem + "\"]"); } } else { - updateStatus(ThingStatus.UNKNOWN); + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, + "@text/solarforecast.plane.status.await-feedback"); } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "@text/solarforecast.site.status.wrong-handler [\"" + handler + "\"]"); + "@text/solarforecast.plane.status.wrong-handler [\"" + handler + "\"]"); } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "@text/solarforecast.site.status.bridge-handler-not-found"); + "@text/solarforecast.plane.status.bridge-handler-not-found"); } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "@text/solarforecast.site.status.bridge-missing"); + "@text/solarforecast.plane.status.bridge-missing"); } } @@ -180,15 +182,17 @@ protected SolcastObject fetchData() { updateStatus(ThingStatus.ONLINE); } else { logger.debug("{} Call {} failed {}", thing.getLabel(), forecastUrl, crForecast.getStatus()); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + 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); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/solarforecast.plane.status.http-status [\"" + crEstimate.getStatus() + "\"]"); } } catch (InterruptedException | ExecutionException | TimeoutException e) { logger.debug("{} Call {} failed {}", thing.getLabel(), currentEstimateUrl, e.getMessage()); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); } } // else use available forecast updateChannels(forecast.get()); 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 index ecaeaf2f3eb04..9f1ed2ef1e49f 100644 --- 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 @@ -114,12 +114,14 @@ binding.solarforecast.description = Solar Forecast for your location # status details -solarforecast.site.status.api-key-missing = API Key is mandatory +solarforecast.site.status.api-key-missing = API key is mandatory solarforecast.site.status.timezone = Time zone {0} not found solarforecast.plane.status.bridge-missing = Bridge not set -solarforecast.plane.status.bridge-handler-not-found = BridgeHandler not found -solarforecast.plane.status.wrong-handler = Wrong Handler {0} +solarforecast.plane.status.bridge-handler-not-found = Bridge handler not found +solarforecast.plane.status.wrong-handler = Wrong handler {0} solarforecast.plane.status.power-item = Power Item {0} not found +solarforecast.plane.status.await-feedback = Await first feedback +solarforecast.plane.status.http-status = HTTP Status Code {0} # thing actions From 642b922c1f7187f2c0ef5631cf1c2230a098389d Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sun, 19 Nov 2023 00:24:25 +0100 Subject: [PATCH 062/111] readme Instant adaptions Signed-off-by: Bernd Weymann --- .../README.md | 90 ++++++++++--------- .../OH-INF/i18n/solarforecast.properties | 6 +- 2 files changed, 51 insertions(+), 45 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 43a3a63166f95..466085deace69 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -190,32 +190,32 @@ Day*X* channels are referring to forecasts plus *X* days: 1 = tomorrow, 2 = day ## Thing Actions All things `sc-site`, `sc-plane`, `fs-site` and `fs-plane` are providing the same Actions. -While channels are providing actual forecast data and daily forecasts in future Actions provides an interface to execute more sophisticated handling in rules. +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. -Input for queries are `LocalDateTime` and `LocalDate` objects. 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 `LocalDateTime` of the earliest possible forecast data available. +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. -`LocalDateTime.MAX` is returned in case of no forecast data is available. +`Instant.MAX` is returned in case of no forecast data is available. ### `getForecastEnd` -Returns `LocalDateTime` of the latest possible forecast data available. -`LocalDateTime.MIN` is returned in case of no forecast data is available. +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 | LocalDateTime | Time stamp | +| timestamp | Instant | Timestamp of power query | | mode | String | Choose `optimistic` or `pessimistic` to get values for a positive or negative future scenario. Only Solcast. | -Returns `QuantityType` at the given `localDateTime`. +Returns `QuantityType` at the given `Instant` timestamp. Respect `getForecastBegin` and `getForecastEnd` to get a valid value. Check for `UndefType.UNDEF` in case of errors. @@ -234,14 +234,26 @@ Check for `UndefType.UNDEF` in case of errors. | Parameter | Type | Description | |-----------------|---------------|--------------------------------------------------------------------------------------------------------------| -| startTimestamp | LocalDateTime | Date of the day | -| endTimestamp | LocalDateTime | Date of the day | +| 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 for `UndefType.UNDEF` in case of errors. +## Date Time + +Each forecast is bound to a certain location which automatically defines the time zone. +Most common use case is forecast and your locarion 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. @@ -281,17 +293,20 @@ Number:Energy ForecastSolarHome_Day_SW "Tomorrow SW Forecast ### Actions rule ```java +import java.time.temporal.ChronoUnit + rule "Forecast Solar Actions" when Time cron "0 0 23 * * ?" // trigger whatever you like then // get Actions for specific fs-site val solarforecastActions = getActions("solarforecast","solarforecast:fs-site:homeSite") - + val timeZone = "Europe/Berlin" + // get earliest and latest forecast dates val beginDT = solarforecastActions.getForecastBegin val endDT = solarforecastActions.getForecastEnd - logInfo("SF Tests","Begin: "+ beginDT+" End: "+endDT) + logInfo("SF Tests","Begin: "+ beginDT.atZone(ZoneId.of(timeZone))+" End: "+endDT.atZone(ZoneId.of(timeZone))) // get forecast for tomorrow val fcTomorrowState = solarforecastActions.getDay(LocalDate.now.plusDays(1)) @@ -300,13 +315,15 @@ rule "Forecast Solar Actions" logInfo("SF Tests","Forecast tomorrow value: "+ fcToTomorrowDouble) // get power forecast in one hour - val hourPlusOnePowerState = solarforecastActions.getPower(LocalDateTime.now.plusHours(1)) + val hourPlusOnePowerState = solarforecastActions.getPower(Instant.now.plus(1,ChronoUnit.HOURS)) logInfo("SF Tests","Hour+1 power state: "+ hourPlusOnePowerState.toString) val hourPlusOnePowerValue = (hourPlusOnePowerState as Number).doubleValue logInfo("SF Tests","Hour+1 power value: "+ hourPlusOnePowerValue) - // get total energy forecast from now till 2 days ahead - val twoDaysForecastFromNowState = solarforecastActions.getEnergy(LocalDateTime.now,LocalDateTime.now.plusDays(2)) + // get energy forecast at specific time: Nov 18th 2023, 16:00 + val startDT = LocalDateTime.of(2023,11,18,16,00).atZone(ZoneId.of(timeZone)) + val stopDT = startDT.plusDays(2) + val twoDaysForecastFromNowState = solarforecastActions.getEnergy(startDT.toInstant, stopDT.toInstant) logInfo("SF Tests","Forecast 2 days state: "+ twoDaysForecastFromNowState.toString) val twoDaysForecastFromNowValue = (twoDaysForecastFromNowState as Number).doubleValue logInfo("SF Tests","Forecast 2 days value: "+ twoDaysForecastFromNowValue) @@ -316,13 +333,13 @@ end shall produce following output ``` -2022-08-07 18:02:19.874 [INFO ] [g.openhab.core.model.script.SF Tests] - Begin: 2022-07-31T18:30 End: 2022-08-14T18:00 -2022-08-07 18:02:19.878 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast tomorrow state: 55.999 kWh -2022-08-07 18:02:19.880 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast tomorrow value: 55.999 -2022-08-07 18:02:19.884 [INFO ] [g.openhab.core.model.script.SF Tests] - Hour+1 power state: 2.497 kW -2022-08-07 18:02:19.886 [INFO ] [g.openhab.core.model.script.SF Tests] - Hour+1 power value: 2.497 -2022-08-07 18:02:19.891 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast 2 days state: 112.483 kWh -2022-08-07 18:02:19.892 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast 2 days value: 112.483 +2023-11-18 22:00:59.250 [INFO ] [g.openhab.core.model.script.SF Tests] - Begin: 2023-11-18T07:34:23+01:00[Europe/Berlin] End: 2023-11-19T16:26:50+01:00[Europe/Berlin] +2023-11-18 22:00:59.262 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast tomorrow state: 3.861 kWh +2023-11-18 22:00:59.267 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast tomorrow value: 3.861 +2023-11-18 22:00:59.275 [INFO ] [g.openhab.core.model.script.SF Tests] - Hour+1 power state: 0 kW +2023-11-18 22:00:59.280 [INFO ] [g.openhab.core.model.script.SF Tests] - Hour+1 power value: 0.0 +2023-11-18 22:00:59.296 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast 2 days state: 3.865 kWh +2023-11-18 22:00:59.300 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast 2 days value: 3.865 ``` ### Actions rule with Arguments @@ -331,15 +348,20 @@ Only Solcast is delivering `optimistic` and `pessimistic` scenario data. If arguments are used on ForecastSolar `UNDEF` state is returned ```java -rule "Solcast Actions" +import java.time.temporal.ChronoUnit + +rrule "Solcast Actions" when Time cron "0 0 23 * * ?" // trigger whatever you like then - val sixDayForecast = solarforecastActions.getEnergy(LocalDateTime.now,LocalDateTime.now.plusDays(6)) + 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 Estimate 6 days "+ sixDayForecast) - val sixDayOptimistic = solarforecastActions.getEnergy(LocalDateTime.now,LocalDateTime.now.plusDays(6),"optimistic") + val sixDayOptimistic = solarforecastActions.getEnergy(startTimestamp,endTimestamp, "optimistic") logInfo("SF Tests","Forecast Optimist 6 days "+ sixDayOptimistic) - val sixDayPessimistic = solarforecastActions.getEnergy(LocalDateTime.now,LocalDateTime.now.plusDays(6),"pessimistic") + val sixDayPessimistic = solarforecastActions.getEnergy(startTimestamp,endTimestamp, "pessimistic") logInfo("SF Tests","Forecast Pessimist 6 days "+ sixDayPessimistic) end ``` @@ -351,19 +373,3 @@ shall produce following output 2022-08-10 00:02:16.574 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast Optimist 6 days 319.827 kWh 2022-08-10 00:02:16.578 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast Pessimist 6 days 208.235 kWh ``` - -## Date Time - -Each forecast is bound to a certain location and this location is automatically bound to the TimeZone. -This binding is translating the forecast timestamps according to your Regional Settings in openHAB. -So you can query forecast data based on `LocalDate` and `LocalDateTime`. -This shall cover the majority of use cases. - -There might be rare use cases querying foreign locations, e.g. `Aisa/Tokyo`. -For this: - -- Forecast.Solar is every time delivering the local asia date time measures -- Solcast needs to be configured with parameter `timeZone` in the advanced settings - -Taking this into account every time you query forecast data e.g. at 12 PM it will deliver the data at _high noon_ time of this location. - 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 index 9f1ed2ef1e49f..e7a9a1fbab629 100644 --- 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 @@ -132,13 +132,13 @@ actionInputDayDesc = LocalDate for daily energy query actionPowerLabel = Power actionPowerDesc = Returns power in kw for a specific point in time actionInputDateTimeLabel = Date Time -actionInputDateTimeDesc = LocalDateTime for power query +actionInputDateTimeDesc = Instant timestamp for power query actionEnergyLabel = Energy Production actionEnergyDesc = Returns energy productions between two different timestamps actionInputDateTimeBeginLabel = Timestamp Begin -actionInputDateTimeBeginDesc = LocalDateTime as starting point of the energy query +actionInputDateTimeBeginDesc = Instant timestamp as starting point of the energy query actionInputDateTimeEndLabel = TimeStamp End -actionInputDateTimeEndDesc = LocalDateTime as end point of the energy query +actionInputDateTimeEndDesc = Instant timestamp as end point of the energy query actionForecastBeginLabel = Forecast Startpoint actionForecastBeginDesc = Returns earliest timestamp of forecast data actionForecastEndLabel = Forecast End From eedc604efdcbcd95c7e3f0655cff431e031adbc8 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sun, 19 Nov 2023 17:20:18 +0100 Subject: [PATCH 063/111] bugfix initial delay Signed-off-by: Bernd Weymann --- .../forecastsolar/handler/ForecastSolarBridgeHandler.java | 2 +- .../internal/solcast/handler/SolcastBridgeHandler.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 index 3a78ebfb89df6..1c2518ec1e783 100644 --- 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 @@ -76,7 +76,7 @@ public void initialize() { configuration = Optional.of(config); updateStatus(ThingStatus.ONLINE); refreshJob = Optional.of( - scheduler.scheduleWithFixedDelay(this::getData, 10, config.channelRefreshInterval, TimeUnit.MINUTES)); + scheduler.scheduleWithFixedDelay(this::getData, 1, config.channelRefreshInterval, TimeUnit.MINUTES)); } @Override 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 index f606c2d15db60..b93ee881d8219 100644 --- 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 @@ -79,7 +79,7 @@ public void initialize() { try { timeZone = ZoneId.of(configuration.get().timeZone); updateStatus(ThingStatus.ONLINE); - refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 10, + refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 1, configuration.get().channelRefreshInterval, TimeUnit.MINUTES)); } catch (DateTimeException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, @@ -87,7 +87,7 @@ public void initialize() { } } else { updateStatus(ThingStatus.ONLINE); - refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 10, + refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 1, configuration.get().channelRefreshInterval, TimeUnit.MINUTES)); } } else { From 2f4b17511baaba9d633125d7614ecf49cf2fbad8 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Fri, 24 Nov 2023 11:02:08 +0100 Subject: [PATCH 064/111] minor: initial delay has no effect - remove Signed-off-by: Bernd Weymann --- .../forecastsolar/handler/ForecastSolarBridgeHandler.java | 2 +- .../internal/solcast/handler/SolcastBridgeHandler.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 index 1c2518ec1e783..ea912ff3dc453 100644 --- 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 @@ -76,7 +76,7 @@ public void initialize() { configuration = Optional.of(config); updateStatus(ThingStatus.ONLINE); refreshJob = Optional.of( - scheduler.scheduleWithFixedDelay(this::getData, 1, config.channelRefreshInterval, TimeUnit.MINUTES)); + scheduler.scheduleWithFixedDelay(this::getData, 0, config.channelRefreshInterval, TimeUnit.MINUTES)); } @Override 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 index b93ee881d8219..6d3f438beb33d 100644 --- 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 @@ -79,7 +79,7 @@ public void initialize() { try { timeZone = ZoneId.of(configuration.get().timeZone); updateStatus(ThingStatus.ONLINE); - refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 1, + refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, configuration.get().channelRefreshInterval, TimeUnit.MINUTES)); } catch (DateTimeException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, @@ -87,7 +87,7 @@ public void initialize() { } } else { updateStatus(ThingStatus.ONLINE); - refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 1, + refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, configuration.get().channelRefreshInterval, TimeUnit.MINUTES)); } } else { From 693fac6ebd0da31e711b82dfb6f6f80f5086ba36 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Thu, 7 Dec 2023 18:52:50 +0100 Subject: [PATCH 065/111] readme typos Signed-off-by: Bernd Weymann --- bundles/org.openhab.binding.solarforecast/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 466085deace69..33b5a47ab83e7 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -90,7 +90,7 @@ If you don't want to send measures to Solcast, leave this configuration item emp `powerUnit` is set to `auto-detect`. In case the `powerItem` is delivering a valid `QuantityType` state, this setting is fine. -If the item delivers a raw number without unit please select `powerUnit` accordingly if item state, is Watt or Kilowatt unit. +If the item delivers a raw number without unit, please select `powerUnit` accordingly if item state, is Watt or Kilowatt unit. ## Solcast Channels From d32bc8f26f886c7025bc4c2c74be789c239bfe41 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Mon, 8 Jan 2024 20:41:37 +0100 Subject: [PATCH 066/111] review comments Signed-off-by: Bernd Weymann --- bundles/org.openhab.binding.solarforecast/README.md | 6 +++--- bundles/org.openhab.binding.solarforecast/pom.xml | 2 +- .../solarforecast/internal/solcast/SolcastObject.java | 6 ------ 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 33b5a47ab83e7..b67be9aab9a15 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -94,7 +94,7 @@ If the item delivers a raw number without unit, please select `powerUnit` accord ## Solcast Channels -Each `sc-plane` reports its own values including a `raw` channel holding JSON content. +Each `sc-plane` reports it's own values including a `raw` channel holding JSON content. The `sc-site` bridge sums up all attached `sc-plane` values and provides the total forecast for your home location. Channels are covering today's actual data with current, remaining and today's total prediction. @@ -159,7 +159,7 @@ 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. +[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. @@ -245,7 +245,7 @@ Check for `UndefType.UNDEF` in case of errors. ## Date Time Each forecast is bound to a certain location which automatically defines the time zone. -Most common use case is forecast and your locarion are matching the same 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. diff --git a/bundles/org.openhab.binding.solarforecast/pom.xml b/bundles/org.openhab.binding.solarforecast/pom.xml index 4ccf149b6ea0f..ecd0e93a857a6 100644 --- a/bundles/org.openhab.binding.solarforecast/pom.xml +++ b/bundles/org.openhab.binding.solarforecast/pom.xml @@ -7,7 +7,7 @@ org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT org.openhab.binding.solarforecast 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 index 541f271224959..ee2f6ce11baae 100644 --- 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 @@ -31,9 +31,7 @@ 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.types.State; import org.openhab.core.types.UnDefType; -import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** @@ -181,8 +179,6 @@ public double getActualValue(ZonedDateTime query, QueryMode mode) { // production during period is half of previous and next value if (endValue != null) { double addedValue = (endValue.doubleValue() + previousEstimate) / 2.0 / 2.0; - // System.out.println( - // "End " + endValue.doubleValue() + " Prev: " + previousEstimate + " Add " + addedValue); forecastValue += addedValue; previousEstimate = endValue.doubleValue(); } else { @@ -199,7 +195,6 @@ public double getActualValue(ZonedDateTime query, QueryMode mode) { int interpolation = query.getMinute() - f.getKey().getMinute(); double interpolationProduction = getActualPowerValue(query, mode) * interpolation / 30.0 / 2.0; forecastValue += interpolationProduction; - // System.out.println(interpolationProduction); return forecastValue; } else { // if ceiling value is 0 there's no further production in this period @@ -271,7 +266,6 @@ public double getDayTotal(LocalDate query, QueryMode mode) { // production during period is half of previous and next value if (endValue != null) { double addedValue = (endValue.doubleValue() + previousEstimate) / 2.0 / 2.0; - // System.out.println(key + " add " + addedValue); forecastValue += addedValue; previousEstimate = endValue.doubleValue(); } else { From 071b98e5ee3b520027624e16c56820e177179d88 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 9 Jan 2024 09:44:38 +0100 Subject: [PATCH 067/111] header year switch Signed-off-by: Bernd Weymann --- .../solarforecast/internal/SolarForecastBindingConstants.java | 2 +- .../solarforecast/internal/SolarForecastHandlerFactory.java | 2 +- .../binding/solarforecast/internal/actions/SolarForecast.java | 2 +- .../solarforecast/internal/actions/SolarForecastActions.java | 2 +- .../solarforecast/internal/actions/SolarForecastProvider.java | 2 +- .../internal/forecastsolar/ForecastSolarObject.java | 2 +- .../config/ForecastSolarBridgeConfiguration.java | 2 +- .../forecastsolar/config/ForecastSolarPlaneConfiguration.java | 2 +- .../forecastsolar/handler/ForecastSolarBridgeHandler.java | 2 +- .../forecastsolar/handler/ForecastSolarPlaneHandler.java | 2 +- .../solarforecast/internal/solcast/SolcastConstants.java | 2 +- .../binding/solarforecast/internal/solcast/SolcastObject.java | 4 +++- .../internal/solcast/config/SolcastBridgeConfiguration.java | 2 +- .../internal/solcast/config/SolcastPlaneConfiguration.java | 2 +- .../internal/solcast/handler/SolcastBridgeHandler.java | 2 +- .../internal/solcast/handler/SolcastPlaneHandler.java | 2 +- .../openhab/binding/solarforecast/internal/utils/Utils.java | 2 +- .../java/org/openhab/binding/solarforecast/FileReader.java | 2 +- .../org/openhab/binding/solarforecast/ForecastSolarTest.java | 2 +- .../java/org/openhab/binding/solarforecast/SolcastTest.java | 2 +- .../test/java/org/openhab/binding/solarforecast/TimeZP.java | 2 +- 21 files changed, 23 insertions(+), 21 deletions(-) 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 index a5deac5af4176..1f7ab40dc7f6a 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index 33e6081d863e0..e71601bb1af99 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index eff395907bc36..690a6ca1084b1 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index 1e190290c02ef..7725452eddc29 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index dc2da3a42069a..0163cf2bb5b4a 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index 2526f4c53844c..676718efc0f10 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index b4cb42a580077..40b3c9b03e4f0 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index 9ded25dcceb55..df2b2925b3094 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index ea912ff3dc453..b6168bcd6cfc7 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index def1b4dd5d2b5..19ff4780e2a01 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index 53f333a64a3f4..4f5c50fea9fdf 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index ee2f6ce11baae..7673f3c48969a 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. @@ -31,7 +31,9 @@ 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.types.State; import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** 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 index 35c363afd279c..88e4bfd6d2cbb 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index 0e2882639da00..56dd16c6eb58a 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index 6d3f438beb33d..9ac999c8d24f9 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index c05b7946a111d..b124411764a7e 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index 87395979a7176..21abc649c41b0 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index a8c45625133f1..fcdb41b9a13a0 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index 43a9f632948c3..bdd41d9f9decd 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index 5738bfa3db88c..770644df71724 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. 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 index 9751478297d56..44712a326f50a 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. From 6974ea49a9d24f371a90d026059abf66d8c60d05 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 9 Jan 2024 10:50:33 +0100 Subject: [PATCH 068/111] remove power feedback Signed-off-by: Bernd Weymann --- .../README.md | 10 -- .../internal/SolarForecastHandlerFactory.java | 23 +-- .../config/SolcastPlaneConfiguration.java | 2 - .../solcast/handler/SolcastPlaneHandler.java | 137 +----------------- .../solarforecast/internal/utils/Utils.java | 33 ----- .../OH-INF/config/sc-plane-config.xml | 17 --- .../binding/solarforecast/SolcastTest.java | 23 --- 7 files changed, 2 insertions(+), 243 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index b67be9aab9a15..be17b27e0e115 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -74,8 +74,6 @@ See [DateTime](#date-time) section for more information. |-----------------|---------|--------------------------------------------------------|-----------------|----------|----------| | resourceId | text | Resource Id of Solcast rooftop site | N/A | yes | no | | refreshInterval | integer | Forecast Refresh Interval in minutes | 120 | yes | no | -| powerItem | text | Power item from your solar inverter for this rooftop | N/A | no | yes | -| powerUnit | text | Unit selection of the powerItem | auto-detect | no | yes | `resourceId` for each plane can be obtained in your [Rooftop Sites](https://toolkit.solcast.com.au/rooftop-sites) @@ -84,14 +82,6 @@ If you have 25 free calls per day, each plane needs 2 calls per update a refresh Note: `channelRefreshInterval` from [Bridge Configuration](#solcast-bridge-configuration) will calculate intermediate values without requesting new forecast data. -`powerItem` shall reflect the power for this specific rooftop. -It's an optional setting and the [measure is sent to Solcast API in order to tune the forecast](https://legacy-docs.solcast.com.au/#measurements-rooftop-site) in the future. -If you don't want to send measures to Solcast, leave this configuration item empty. - -`powerUnit` is set to `auto-detect`. -In case the `powerItem` is delivering a valid `QuantityType` state, this setting is fine. -If the item delivers a raw number without unit, please select `powerUnit` accordingly if item state, is Watt or Kilowatt unit. - ## Solcast Channels Each `sc-plane` reports it's own values including a `raw` channel holding JSON content. 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 index e71601bb1af99..f37092eb61e65 100644 --- 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 @@ -14,8 +14,6 @@ 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; @@ -26,11 +24,7 @@ import org.openhab.core.i18n.LocationProvider; import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.io.net.http.HttpClientFactory; -import org.openhab.core.items.ItemRegistry; import org.openhab.core.library.types.PointType; -import org.openhab.core.persistence.PersistenceService; -import org.openhab.core.persistence.PersistenceServiceRegistry; -import org.openhab.core.persistence.QueryablePersistenceService; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingTypeUID; @@ -40,8 +34,6 @@ import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link SolarForecastHandlerFactory} is responsible for creating things and thing @@ -52,19 +44,13 @@ @NonNullByDefault @Component(configurationPid = "binding.solarforecast", service = ThingHandlerFactory.class) public class SolarForecastHandlerFactory extends BaseThingHandlerFactory { - private final Logger logger = LoggerFactory.getLogger(SolarForecastHandlerFactory.class); private final TimeZoneProvider timeZoneProvider; - private final ItemRegistry itemRegistry; private final HttpClient httpClient; private final PointType location; - private Optional qps = Optional.empty(); - @Activate public SolarForecastHandlerFactory(final @Reference HttpClientFactory hcf, final @Reference LocationProvider lp, - final @Reference PersistenceServiceRegistry psr, final @Reference ItemRegistry ir, final @Reference TimeZoneProvider tzp) { - itemRegistry = ir; timeZoneProvider = tzp; httpClient = hcf.getCommonHttpClient(); PointType pt = lp.getLocation(); @@ -73,13 +59,6 @@ public SolarForecastHandlerFactory(final @Reference HttpClientFactory hcf, final } else { location = PointType.valueOf("0.0,0.0"); } - - PersistenceService s = psr.getDefault(); - if (s instanceof QueryablePersistenceService) { - qps = Optional.of((QueryablePersistenceService) s); - } else { - logger.info("Persistence {} cannot be queried. Feature Solcast Tuning will not work", s); - } } @Override @@ -97,7 +76,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { } else if (SOLCAST_BRIDGE_STRING.equals(thingTypeUID)) { return new SolcastBridgeHandler((Bridge) thing, timeZoneProvider); } else if (SOLCAST_PART_STRING.equals(thingTypeUID)) { - return new SolcastPlaneHandler(thing, httpClient, qps, itemRegistry); + return new SolcastPlaneHandler(thing, httpClient); } return null; } 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 index 56dd16c6eb58a..1a2d2c1de8a2a 100644 --- 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 @@ -23,7 +23,5 @@ @NonNullByDefault public class SolcastPlaneConfiguration { public String resourceId = SolarForecastBindingConstants.EMPTY; - public String powerItem = SolarForecastBindingConstants.EMPTY; public long refreshInterval = 120; - public String powerUnit = SolarForecastBindingConstants.AUTODETECT; } 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 index b124411764a7e..ad0f21e943648 100644 --- 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 @@ -18,7 +18,6 @@ import java.time.Instant; import java.time.LocalDate; import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.Collection; import java.util.List; @@ -26,15 +25,11 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; -import javax.measure.Unit; - 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.client.util.StringContentProvider; import org.eclipse.jetty.http.HttpHeader; -import org.json.JSONObject; import org.openhab.binding.solarforecast.internal.actions.SolarForecast; import org.openhab.binding.solarforecast.internal.actions.SolarForecastActions; import org.openhab.binding.solarforecast.internal.actions.SolarForecastProvider; @@ -42,15 +37,7 @@ 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.items.Item; -import org.openhab.core.items.ItemRegistry; -import org.openhab.core.library.types.DecimalType; -import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.StringType; -import org.openhab.core.library.unit.Units; -import org.openhab.core.persistence.FilterCriteria; -import org.openhab.core.persistence.HistoricItem; -import org.openhab.core.persistence.QueryablePersistenceService; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; @@ -61,8 +48,6 @@ import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; -import org.openhab.core.types.State; -import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -73,22 +58,15 @@ */ @NonNullByDefault public class SolcastPlaneHandler extends BaseThingHandler implements SolarForecastProvider { - private static final int MEASURE_INTERVAL_MIN = 15; private final Logger logger = LoggerFactory.getLogger(SolcastPlaneHandler.class); private final HttpClient httpClient; - private final ItemRegistry itemRegistry; private Optional configuration = Optional.empty(); private Optional bridgeHandler = Optional.empty(); - private Optional powerItem = Optional.empty(); - private Optional persistenceService; private Optional forecast = Optional.empty(); - private Optional nextMeasurement = Optional.empty(); - public SolcastPlaneHandler(Thing thing, HttpClient hc, Optional qps, ItemRegistry ir) { + public SolcastPlaneHandler(Thing thing, HttpClient hc) { super(thing); httpClient = hc; - persistenceService = qps; - itemRegistry = ir; } @Override @@ -110,23 +88,6 @@ public void initialize() { bridgeHandler = Optional.of(sbh); bridgeHandler.get().addPlane(this); forecast = Optional.of(new SolcastObject(bridgeHandler.get())); - nextMeasurement = Optional.of(Utils.getNextTimeframe(Instant.now(), bridgeHandler.get())); - // initialize Power Item - if (!EMPTY.equals(c.powerItem)) { - // power item configured - Item item = itemRegistry.get(c.powerItem); - if (item != null) { - powerItem = Optional.of(item); - updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, - "@text/solarforecast.plane.status.await-feedback"); - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "@text/solarforecast.plane.status.power-item [\"" + c.powerItem + "\"]"); - } - } else { - updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, - "@text/solarforecast.plane.status.await-feedback"); - } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/solarforecast.plane.status.wrong-handler [\"" + handler + "\"]"); @@ -196,105 +157,9 @@ protected SolcastObject fetchData() { } } // else use available forecast updateChannels(forecast.get()); - if (Instant.now().isAfter(nextMeasurement.get())) { - sendMeasure(); - } return forecast.get(); } - /** - * https://legacy-docs.solcast.com.au/#measurements-rooftop-site - */ - private void sendMeasure() { - State updateState = UnDefType.UNDEF; - if (persistenceService.isPresent() && powerItem.isPresent()) { - ZonedDateTime beginPeriodDT = nextMeasurement.get().atZone(bridgeHandler.get().getTimeZone()) - .minusMinutes(MEASURE_INTERVAL_MIN); - ZonedDateTime endPeriodDT = nextMeasurement.get().atZone(bridgeHandler.get().getTimeZone()); - FilterCriteria fc = new FilterCriteria(); - fc.setBeginDate(beginPeriodDT); - fc.setEndDate(endPeriodDT); - fc.setItemName(configuration.get().powerItem); - Iterable historicItems = persistenceService.get().query(fc); - int count = 0; - double total = 0; - for (HistoricItem historicItem : historicItems) { - // historicItem.getState().toFullString()); - DecimalType dt = historicItem.getState().as(DecimalType.class); - if (dt != null) { - total += dt.doubleValue(); - } - count++; - - } - double power = total / count; - - // detect unit - if (AUTODETECT.equals(configuration.get().powerUnit)) { - State state = powerItem.get().getState(); - if (state instanceof QuantityType) { - Unit unitDetected = ((QuantityType) state).getUnit(); - if (Units.WATT.toString().equals(unitDetected.toString())) { - // scale to kW necessary, keep 3 digits after comma - power = Math.round(power) / 1000.0; - } else if (KILOWATT_UNIT.toString().equals(unitDetected.toString())) { - // just round and keep 3 digits after comma - power = Math.round(power * 1000.0) / 1000.0; - } else { - logger.trace("No valid Power unit detected {}", unitDetected.toString()); - power = UNDEF_DOUBLE; - } - } else { - logger.trace("No autodetection for State class {} possible", state.getClass()); - power = UNDEF_DOUBLE; - } - } else if (Units.WATT.toString().equals(configuration.get().powerUnit)) { - // scale to kW necessary, keep 3 digits after comma - power = Math.round(power) / 1000.0; - } else if (KILOWATT_UNIT.toString().equals(configuration.get().powerUnit)) { - // just round and keep 3 digits after comma - power = Math.round(power * 1000.0) / 1000.0; - } else { - logger.trace("No Unit conversion possible for {}", configuration.get().powerUnit); - power = UNDEF_DOUBLE; - } - - if (power >= 0) { - JSONObject measureObject = new JSONObject(); - JSONObject measure = new JSONObject(); - measure.put("period_end", endPeriodDT.format(DateTimeFormatter.ISO_INSTANT)); - measure.put("period", "PT" + MEASURE_INTERVAL_MIN + "M"); - measure.put("total_power", power); - measureObject.put("measurement", measure); - logger.trace("Send {}", measureObject.toString()); - - String measureUrl = String.format(MEASUREMENT_URL, configuration.get().resourceId); - Request request = httpClient.POST(measureUrl); - request.header(HttpHeader.AUTHORIZATION, BEARER + bridgeHandler.get().getApiKey()); - request.content(new StringContentProvider(measureObject.toString())); - request.header(HttpHeader.CONTENT_TYPE, "application/json"); - try { - ContentResponse crMeasure = request.send(); - if (crMeasure.getStatus() == 200) { - updateState = StringType.valueOf(crMeasure.getContentAsString()); - updateStatus(ThingStatus.ONLINE); - } else { - logger.debug("{} Call {} failed {} - {}", thing.getLabel(), measureUrl, crMeasure.getStatus(), - crMeasure.getContentAsString()); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); - } - } catch (InterruptedException | TimeoutException | ExecutionException e) { - logger.debug("{} Call {} failed {}", thing.getLabel(), measureUrl, e.getMessage()); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); - } - } else { - logger.debug("Persistence for {} empty", powerItem); - } - } - updateState(CHANNEL_RAW_TUNING, updateState); - nextMeasurement = Optional.of(Utils.getNextTimeframe(Instant.now(), bridgeHandler.get())); - } - private void updateChannels(SolcastObject f) { ZonedDateTime now = ZonedDateTime.now(bridgeHandler.get().getTimeZone()); updateState(CHANNEL_ACTUAL, Utils.getEnergyState(f.getActualValue(now, QueryMode.Estimation))); 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 index 21abc649c41b0..e58c5928e3e83 100644 --- 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 @@ -12,13 +12,9 @@ */ package org.openhab.binding.solarforecast.internal.utils; -import java.time.Instant; -import java.time.ZonedDateTime; - import javax.measure.MetricPrefix; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.Units; import org.openhab.core.types.State; @@ -46,33 +42,4 @@ public static State getPowerState(double d) { return QuantityType.valueOf(Math.round(d * 1000) / 1000.0, MetricPrefix.KILO(Units.WATT)); } } - - /** - * Get time frames in 15 minutes intervals - * - * @return - */ - public static Instant getNextTimeframe(Instant timeStamp, TimeZoneProvider tzp) { - ZonedDateTime now = timeStamp.atZone(tzp.getTimeZone()); - ZonedDateTime nextTime; - int quarter = now.getMinute() / 15; - switch (quarter) { - case 0: - nextTime = now.withMinute(15).withSecond(0).withNano(0); - break; - case 1: - nextTime = now.withMinute(30).withSecond(0).withNano(0); - break; - case 2: - nextTime = now.withMinute(45).withSecond(0).withNano(0); - break; - case 3: - nextTime = now.withMinute(0).withSecond(0).withNano(0).plusHours(1); - break; - default: - nextTime = now; - break; - } - return nextTime.toInstant(); - } } 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 index ec1e9477595aa..d9cfdccfc0d42 100644 --- 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 @@ -14,22 +14,5 @@ Resource Id of Solcast rooftop site - - item - - Power item from your solar inverter for this rooftop - true - - - - Unit delivered by power item - - - - - - auto-detect - true - 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 index 770644df71724..11b4493298712 100644 --- 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 @@ -28,7 +28,6 @@ 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.utils.Utils; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.Units; import org.openhab.core.types.UnDefType; @@ -332,28 +331,6 @@ void testInavlid() { "Data available after merge - day not in"); } - @Test - void testTimeframes() { - ZonedDateTime zdt = ZonedDateTime.of(2022, 7, 22, 17, 3, 10, 345, TEST_ZONE); - assertEquals("17:15", Utils.getNextTimeframe(zdt.toInstant(), TIMEZONEPROVIDER) - .atZone(TIMEZONEPROVIDER.getTimeZone()).toLocalTime().toString(), "Q1"); - zdt = zdt.plusMinutes(20); - assertEquals("17:30", Utils.getNextTimeframe(zdt.toInstant(), TIMEZONEPROVIDER) - .atZone(TIMEZONEPROVIDER.getTimeZone()).toLocalTime().toString(), "Q2"); - zdt = zdt.plusMinutes(3); - assertEquals("17:30", Utils.getNextTimeframe(zdt.toInstant(), TIMEZONEPROVIDER) - .atZone(TIMEZONEPROVIDER.getTimeZone()).toLocalTime().toString(), "Q2"); - zdt = zdt.plusMinutes(5); - assertEquals("17:45", Utils.getNextTimeframe(zdt.toInstant(), TIMEZONEPROVIDER) - .atZone(TIMEZONEPROVIDER.getTimeZone()).toLocalTime().toString(), "Q3"); - zdt = zdt.plusMinutes(25); - assertEquals("18:00", Utils.getNextTimeframe(zdt.toInstant(), TIMEZONEPROVIDER) - .atZone(TIMEZONEPROVIDER.getTimeZone()).toLocalTime().toString(), "Q4"); - zdt = zdt.plusMinutes(6); - assertEquals("18:15", Utils.getNextTimeframe(zdt.toInstant(), TIMEZONEPROVIDER) - .atZone(TIMEZONEPROVIDER.getTimeZone()).toLocalTime().toString(), "Q4"); - } - @Test void testPowerInterpolation() { String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); From 0e8921c5dff6cd1911a2d6c42a9d7521669375d6 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 9 Jan 2024 18:40:34 +0100 Subject: [PATCH 069/111] TimeSeries introduction Signed-off-by: Bernd Weymann --- .../SolarForecastBindingConstants.java | 35 ++-- .../forecastsolar/ForecastSolarObject.java | 28 +++- .../ForecastSolarBridgeConfiguration.java | 6 - .../handler/ForecastSolarBridgeHandler.java | 38 ++--- .../handler/ForecastSolarPlaneHandler.java | 15 +- .../internal/solcast/SolcastObject.java | 152 +++++++++--------- .../config/SolcastBridgeConfiguration.java | 1 - .../solcast/handler/SolcastBridgeHandler.java | 89 ++-------- .../solcast/handler/SolcastPlaneHandler.java | 36 ++--- .../OH-INF/config/fs-site-config.xml | 5 - .../OH-INF/config/sc-plane-config.xml | 5 - .../OH-INF/config/sc-site-config.xml | 5 - .../resources/OH-INF/thing/channel-types.xml | 147 ++++------------- .../resources/OH-INF/thing/fs-plane-type.xml | 13 +- .../resources/OH-INF/thing/fs-site-type.xml | 13 +- .../resources/OH-INF/thing/sc-plane-type.xml | 33 ++-- .../resources/OH-INF/thing/sc-site-type.xml | 32 ++-- .../solarforecast/ForecastSolarTest.java | 53 ++++-- .../binding/solarforecast/SolcastTest.java | 123 ++++++++++++-- 19 files changed, 367 insertions(+), 462 deletions(-) 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 index 1f7ab40dc7f6a..d7d5a46e2df9d 100644 --- 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 @@ -35,31 +35,18 @@ public class SolarForecastBindingConstants { public static final Set SUPPORTED_THING_SET = Set.of(FORECAST_SOLAR_MULTI_STRING, FORECAST_SOLAR_PART_STRING, SOLCAST_BRIDGE_STRING, SOLCAST_PART_STRING); - public static final String CHANNEL_TODAY = "today"; - public static final String CHANNEL_ACTUAL = "actual"; - public static final String CHANNEL_ACTUAL_POWER = "actual-power"; - public static final String CHANNEL_REMAINING = "remaining"; - public static final String CHANNEL_DAY1 = "day1"; - public static final String CHANNEL_DAY1_LOW = "day1-low"; - public static final String CHANNEL_DAY1_HIGH = "day1-high"; - public static final String CHANNEL_DAY2 = "day2"; - public static final String CHANNEL_DAY2_LOW = "day2-low"; - public static final String CHANNEL_DAY2_HIGH = "day2-high"; - public static final String CHANNEL_DAY3 = "day3"; - public static final String CHANNEL_DAY3_LOW = "day3-low"; - public static final String CHANNEL_DAY3_HIGH = "day3-high"; - public static final String CHANNEL_DAY4 = "day4"; - public static final String CHANNEL_DAY4_LOW = "day4-low"; - public static final String CHANNEL_DAY4_HIGH = "day4-high"; - public static final String CHANNEL_DAY5 = "day5"; - public static final String CHANNEL_DAY5_LOW = "day5-low"; - public static final String CHANNEL_DAY5_HIGH = "day5-high"; - public static final String CHANNEL_DAY6 = "day6"; - public static final String CHANNEL_DAY6_LOW = "day6-low"; - public static final String CHANNEL_DAY6_HIGH = "day6-high"; - + public static final String CHANNEL_POWER_ACTUAL = "power-actual"; + public static final String CHANNEL_POWER_ESTIMATE = "power-estimate"; + public static final String CHANNEL_POWER_ESTIMATE10 = "power-estimate10"; + public static final String CHANNEL_POWER_ESTIMATE90 = "power-estimate90"; + 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_ENERGY_ESTIMATE = "energy-estimate"; + public static final String CHANNEL_ENERGY_ESTIMATE10 = "energy-estimate10"; + public static final String CHANNEL_ENERGY_ESTIMATE90 = "energy-estimate90"; public static final String CHANNEL_RAW = "raw"; - public static final String CHANNEL_RAW_TUNING = "raw-tuning"; + public static final int REFRESH_ACTUAL_INTERVAL = 1; public static final String AUTODETECT = "auto-detect"; public static final String SLASH = "/"; 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 index 676718efc0f10..01949b7e11ee9 100644 --- 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 @@ -30,6 +30,8 @@ import org.openhab.binding.solarforecast.internal.actions.SolarForecast; import org.openhab.binding.solarforecast.internal.utils.Utils; import org.openhab.core.types.State; +import org.openhab.core.types.TimeSeries; +import org.openhab.core.types.TimeSeries.Policy; import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -100,7 +102,7 @@ public boolean isValid() { return false; } - public double getActualValue(ZonedDateTime queryDateTime) { + public double getActualEnergyValue(ZonedDateTime queryDateTime) { if (wattHourMap.isEmpty()) { return UNDEF; } @@ -141,10 +143,18 @@ public double getActualValue(ZonedDateTime queryDateTime) { // floor invalid - ceiling not reached return 0; } - } // else both null - this shall not happen + } // else both null - date time doesn't fit to forecast data return UNDEF; } + public TimeSeries getEnergyTimeSeries() { + 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) { if (wattMap.isEmpty()) { return UNDEF; @@ -182,6 +192,14 @@ public double getActualPowerValue(ZonedDateTime queryDateTime) { return UNDEF; } + public TimeSeries getPowerTimeSeries() { + 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()) { return UNDEF; @@ -201,7 +219,7 @@ public double getRemainingProduction(ZonedDateTime queryDateTime) { return UNDEF; } double daily = getDayTotal(queryDateTime.toLocalDate()); - double actual = getActualValue(queryDateTime); + double actual = getActualEnergyValue(queryDateTime); if (daily < 0 || actual < 0) { return UNDEF; } @@ -240,7 +258,7 @@ public State getEnergy(Instant start, Instant end, String... args) { LocalDate endDate = end.atZone(zone).toLocalDate(); double measure = UNDEF; if (beginDate.equals(endDate)) { - measure = getDayTotal(beginDate) - getActualValue(start.atZone(zone)) + measure = getDayTotal(beginDate) - getActualEnergyValue(start.atZone(zone)) - getRemainingProduction(end.atZone(zone)); } else { measure = getRemainingProduction(start.atZone(zone)); @@ -252,7 +270,7 @@ public State getEnergy(Instant start, Instant end, String... args) { } beginDate = beginDate.plusDays(1); } - double lastDay = getActualValue(end.atZone(zone)); + double lastDay = getActualEnergyValue(end.atZone(zone)); if (lastDay >= 0) { measure += lastDay; } 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 index 40b3c9b03e4f0..8281842fc5248 100644 --- 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 @@ -23,12 +23,6 @@ @NonNullByDefault public class ForecastSolarBridgeConfiguration { public String location = "auto-detect"; - public int channelRefreshInterval = 1; public String apiKey = SolarForecastBindingConstants.EMPTY; public double inverterKwp = Double.MAX_VALUE; - - @Override - public String toString() { - return "Loc " + location + " Ref " + channelRefreshInterval; - } } 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 index b6168bcd6cfc7..af2e688e6e2ca 100644 --- 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 @@ -25,7 +25,6 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; import org.openhab.binding.solarforecast.internal.actions.SolarForecast; import org.openhab.binding.solarforecast.internal.actions.SolarForecastActions; import org.openhab.binding.solarforecast.internal.actions.SolarForecastProvider; @@ -67,7 +66,7 @@ public Collection> getServices() { @Override public void initialize() { ForecastSolarBridgeConfiguration config = getConfigAs(ForecastSolarBridgeConfiguration.class); - if (config.location.equals(SolarForecastBindingConstants.AUTODETECT)) { + if (config.location.equals(AUTODETECT)) { Configuration editConfig = editConfiguration(); editConfig.put("location", homeLocation.toString()); updateConfiguration(editConfig); @@ -75,8 +74,8 @@ public void initialize() { } configuration = Optional.of(config); updateStatus(ThingStatus.ONLINE); - refreshJob = Optional.of( - scheduler.scheduleWithFixedDelay(this::getData, 0, config.channelRefreshInterval, TimeUnit.MINUTES)); + refreshJob = Optional + .of(scheduler.scheduleWithFixedDelay(this::getData, 0, REFRESH_ACTUAL_INTERVAL, TimeUnit.MINUTES)); } @Override @@ -90,32 +89,21 @@ private synchronized void getData() { if (parts.isEmpty()) { return; } - double actualSum = 0; - double actualPowerSum = 0; - double remainSum = 0; - double todaySum = 0; - double day1Sum = 0; - double day2Sum = 0; - double day3Sum = 0; + double energySum = 0; + double powerSum = 0; + double daySum = 0; for (Iterator iterator = parts.iterator(); iterator.hasNext();) { ForecastSolarPlaneHandler sfph = iterator.next(); ForecastSolarObject fo = sfph.fetchData(); ZonedDateTime now = ZonedDateTime.now(fo.getZone()); - actualSum += fo.getActualValue(now); - actualPowerSum += fo.getActualPowerValue(now); - remainSum += fo.getRemainingProduction(now); - todaySum += fo.getDayTotal(now.toLocalDate()); - day1Sum += fo.getDayTotal(now.plusDays(1).toLocalDate()); - day2Sum += fo.getDayTotal(now.plusDays(2).toLocalDate()); - day3Sum += fo.getDayTotal(now.plusDays(3).toLocalDate()); + energySum += fo.getActualEnergyValue(now); + powerSum += fo.getActualPowerValue(now); + daySum += fo.getDayTotal(now.toLocalDate()); } - updateState(CHANNEL_ACTUAL, Utils.getEnergyState(actualSum)); - updateState(CHANNEL_ACTUAL_POWER, Utils.getPowerState(actualPowerSum)); - updateState(CHANNEL_REMAINING, Utils.getEnergyState(remainSum)); - updateState(CHANNEL_TODAY, Utils.getEnergyState(todaySum)); - updateState(CHANNEL_DAY1, Utils.getEnergyState(day1Sum)); - updateState(CHANNEL_DAY2, Utils.getEnergyState(day2Sum)); - updateState(CHANNEL_DAY3, Utils.getEnergyState(day3Sum)); + 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)); } @Override 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 index 19ff4780e2a01..52fa500e5361e 100644 --- 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 @@ -170,13 +170,12 @@ protected ForecastSolarObject fetchData() { private void updateChannels(ForecastSolarObject f) { ZonedDateTime now = ZonedDateTime.now(f.getZone()); - updateState(CHANNEL_ACTUAL, Utils.getEnergyState(f.getActualValue(now))); - updateState(CHANNEL_ACTUAL_POWER, Utils.getPowerState(f.getActualPowerValue(now))); - updateState(CHANNEL_REMAINING, Utils.getEnergyState(f.getRemainingProduction(now))); - updateState(CHANNEL_TODAY, Utils.getEnergyState(f.getDayTotal(now.toLocalDate()))); - updateState(CHANNEL_DAY1, Utils.getEnergyState(f.getDayTotal(now.plusDays(1).toLocalDate()))); - updateState(CHANNEL_DAY2, Utils.getEnergyState(f.getDayTotal(now.plusDays(2).toLocalDate()))); - updateState(CHANNEL_DAY3, Utils.getEnergyState(f.getDayTotal(now.plusDays(3).toLocalDate()))); + 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))); } /** @@ -194,6 +193,8 @@ void setApiKey(String key) { private synchronized void setForecast(ForecastSolarObject f) { forecast = f; + sendTimeSeries(CHANNEL_POWER_ESTIMATE, forecast.getPowerTimeSeries()); + sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, forecast.getEnergyTimeSeries()); } @Override 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 index 7673f3c48969a..8f7b044d0ea77 100644 --- 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 @@ -20,7 +20,6 @@ import java.util.Arrays; import java.util.Map.Entry; import java.util.Optional; -import java.util.Set; import java.util.TreeMap; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -32,6 +31,8 @@ import org.openhab.binding.solarforecast.internal.utils.Utils; import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.types.State; +import org.openhab.core.types.TimeSeries; +import org.openhab.core.types.TimeSeries.Policy; import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,9 +48,9 @@ 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 TreeMap estimationDataMap = new TreeMap(); + private final TreeMap optimisticDataMap = new TreeMap(); + private final TreeMap pessimisticDataMap = new TreeMap(); private final TimeZoneProvider timeZoneProvider; private Optional rawData = Optional.of(new JSONObject()); @@ -119,36 +120,20 @@ private void addJSONArray(JSONArray resultJsonArray) { if (periadEndZdt == null) { return; } - LocalDate ld = periadEndZdt.toLocalDate(); - TreeMap forecastMap = estimationDataMap.get(ld); - if (forecastMap == null) { - forecastMap = new TreeMap(); - estimationDataMap.put(ld, forecastMap); - } - forecastMap.put(periadEndZdt, jo.getDouble("pv_estimate")); + estimationDataMap.put(periadEndZdt, jo.getDouble("pv_estimate")); // fill pessimistic values - TreeMap pessimisticForecastMap = pessimisticDataMap.get(ld); - if (pessimisticForecastMap == null) { - pessimisticForecastMap = new TreeMap(); - pessimisticDataMap.put(ld, pessimisticForecastMap); - } if (jo.has("pv_estimate10")) { - pessimisticForecastMap.put(periadEndZdt, jo.getDouble("pv_estimate10")); + pessimisticDataMap.put(periadEndZdt, jo.getDouble("pv_estimate10")); } else { - pessimisticForecastMap.put(periadEndZdt, jo.getDouble("pv_estimate")); + pessimisticDataMap.put(periadEndZdt, jo.getDouble("pv_estimate")); } // fill optimistic values - TreeMap optimisticForecastMap = optimisticDataMap.get(ld); - if (optimisticForecastMap == null) { - optimisticForecastMap = new TreeMap(); - optimisticDataMap.put(ld, optimisticForecastMap); - } if (jo.has("pv_estimate90")) { - optimisticForecastMap.put(periadEndZdt, jo.getDouble("pv_estimate90")); + optimisticDataMap.put(periadEndZdt, jo.getDouble("pv_estimate90")); } else { - optimisticForecastMap.put(periadEndZdt, jo.getDouble("pv_estimate")); + optimisticDataMap.put(periadEndZdt, jo.getDouble("pv_estimate")); } } } @@ -164,31 +149,29 @@ public boolean isValid() { return false; } - public double getActualValue(ZonedDateTime query, QueryMode mode) { - LocalDate ld = query.toLocalDate(); - TreeMap dtm = getDataMap(ld, mode); - if (dtm.isEmpty()) { + public double getActualEnergyValue(ZonedDateTime query, QueryMode mode) { + ZonedDateTime iterationDateTime = query.withHour(0).withMinute(0).withSecond(0); + TreeMap dtm = getDataMap(mode); + Entry nextEntry = dtm.higherEntry(iterationDateTime); + if (nextEntry == null) { return UNDEF; } - double previousEstimate = 0; double forecastValue = 0; - Set keySet = dtm.keySet(); - for (ZonedDateTime key : keySet) { - if (key.isBefore(query) || query.isEqual(key)) { - // value are reported in PT30M = 30 minutes interval with kw value - // for kw/h it's half the value - Double endValue = dtm.get(key); - // production during period is half of previous and next value - if (endValue != null) { - double addedValue = (endValue.doubleValue() + previousEstimate) / 2.0 / 2.0; - forecastValue += addedValue; - previousEstimate = endValue.doubleValue(); - } else { - logger.trace("No estimation found for {}", key); - } + 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 / 2.0; + forecastValue += addedValue; + previousEstimate = endValue.doubleValue(); + iterationDateTime = nextEntry.getKey(); + nextEntry = dtm.higherEntry(iterationDateTime); + if (nextEntry == null) { + break; } } - Entry f = dtm.floorEntry(query); Entry c = dtm.ceilingEntry(query); if (f != null) { @@ -212,16 +195,23 @@ public double getActualValue(ZonedDateTime query, QueryMode mode) { } } + 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) { - LocalDate ld = query.toLocalDate(); - TreeMap dtm = getDataMap(ld, mode); - if (dtm.isEmpty()) { + if (query.toInstant().isBefore(getForecastBegin()) || query.toInstant().isAfter(getForecastEnd())) { return UNDEF; } + TreeMap dtm = getDataMap(mode); double actualPowerValue = 0; Entry f = dtm.floorEntry(query); Entry c = dtm.ceilingEntry(query); @@ -249,41 +239,47 @@ public double getActualPowerValue(ZonedDateTime query, QueryMode mode) { } } + 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(query, mode); - if (dtm.isEmpty()) { + TreeMap dtm = getDataMap(mode); + ZonedDateTime iterationDateTime = query.atStartOfDay(timeZoneProvider.getTimeZone()); + Entry nextEntry = dtm.higherEntry(iterationDateTime); + if (nextEntry == null) { return UNDEF; } - double previousEstimate = 0; + ZonedDateTime endDateTime = query.atTime(23, 59, 59).atZone(timeZoneProvider.getTimeZone()); double forecastValue = 0; - Set keySet = dtm.keySet(); - for (ZonedDateTime key : keySet) { + double previousEstimate = 0; + while (nextEntry.getKey().isBefore(endDateTime) || nextEntry.getKey().isEqual(endDateTime)) { // value are reported in PT30M = 30 minutes interval with kw value // for kw/h it's half the value - Double endValue = dtm.get(key); + Double endValue = nextEntry.getValue(); // production during period is half of previous and next value - if (endValue != null) { - double addedValue = (endValue.doubleValue() + previousEstimate) / 2.0 / 2.0; - forecastValue += addedValue; - previousEstimate = endValue.doubleValue(); - } else { - logger.trace("No estimation found for {}", key); + double addedValue = (endValue.doubleValue() + previousEstimate) / 2.0 / 2.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) { - LocalDate ld = query.toLocalDate(); - TreeMap dtm = getDataMap(ld, mode); - if (dtm.isEmpty()) { - return UNDEF; - } - return getDayTotal(query.toLocalDate(), mode) - getActualValue(query, mode); + return getDayTotal(query.toLocalDate(), mode) - getActualEnergyValue(query, mode); } @Override @@ -295,17 +291,17 @@ public String getRaw() { return rawData.get().toString(); } - private TreeMap getDataMap(LocalDate ld, QueryMode mode) { + private TreeMap getDataMap(QueryMode mode) { TreeMap returnMap = EMPTY_MAP; switch (mode) { case Estimation: - returnMap = estimationDataMap.get(ld); + returnMap = estimationDataMap; break; case Optimistic: - returnMap = optimisticDataMap.get(ld); + returnMap = optimisticDataMap; break; case Pessimistic: - returnMap = pessimisticDataMap.get(ld); + returnMap = pessimisticDataMap; break; case Error: // nothing to do @@ -314,9 +310,6 @@ private TreeMap getDataMap(LocalDate ld, QueryMode mode) // nothing to do break; } - if (returnMap == null) { - return EMPTY_MAP; - } return returnMap; } @@ -367,7 +360,8 @@ public State getEnergy(Instant start, Instant end, String... args) { LocalDate endDate = end.atZone(timeZoneProvider.getTimeZone()).toLocalDate(); double measure = UNDEF; if (beginDate.isEqual(endDate)) { - measure = getDayTotal(beginDate, mode) - getActualValue(start.atZone(timeZoneProvider.getTimeZone()), mode) + measure = getDayTotal(beginDate, mode) + - getActualEnergyValue(start.atZone(timeZoneProvider.getTimeZone()), mode) - getRemainingProduction(end.atZone(timeZoneProvider.getTimeZone()), mode); } else { measure = getRemainingProduction(start.atZone(timeZoneProvider.getTimeZone()), mode); @@ -379,7 +373,7 @@ public State getEnergy(Instant start, Instant end, String... args) { } beginDate = beginDate.plusDays(1); } - double lastDay = getActualValue(end.atZone(timeZoneProvider.getTimeZone()), mode); + double lastDay = getActualEnergyValue(end.atZone(timeZoneProvider.getTimeZone()), mode); if (lastDay >= 0) { measure += lastDay; } @@ -406,7 +400,7 @@ public State getPower(Instant timestamp, String... args) { @Override public Instant getForecastBegin() { if (!estimationDataMap.isEmpty()) { - return estimationDataMap.firstEntry().getValue().firstEntry().getKey().toInstant(); + return estimationDataMap.firstEntry().getKey().toInstant(); } return Instant.MAX; } @@ -414,7 +408,7 @@ public Instant getForecastBegin() { @Override public Instant getForecastEnd() { if (!estimationDataMap.isEmpty()) { - return estimationDataMap.lastEntry().getValue().lastEntry().getKey().toInstant(); + return estimationDataMap.lastEntry().getKey().toInstant(); } return Instant.MIN; } 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 index 88e4bfd6d2cbb..9dc52934ee63d 100644 --- 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 @@ -22,7 +22,6 @@ */ @NonNullByDefault public class SolcastBridgeConfiguration { - public int channelRefreshInterval = 1; public String apiKey = SolarForecastBindingConstants.EMPTY; public String timeZone = SolarForecastBindingConstants.AUTODETECT; } 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 index 9ac999c8d24f9..9fde42a3b3a95 100644 --- 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 @@ -1,4 +1,6 @@ /** + updateState(CHANNEL_ENERGY_REMAIN, Utils.getEnergyState(daySum - energySum)); + updateState(CHANNEL_ENERGY_TODAY, Utils.getEnergyState(daySum)); * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional @@ -15,7 +17,6 @@ import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; import java.time.DateTimeException; -import java.time.LocalDate; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; @@ -79,16 +80,16 @@ public void initialize() { try { timeZone = ZoneId.of(configuration.get().timeZone); updateStatus(ThingStatus.ONLINE); - refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, - configuration.get().channelRefreshInterval, TimeUnit.MINUTES)); + refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, REFRESH_ACTUAL_INTERVAL, + TimeUnit.MINUTES)); } catch (DateTimeException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/solarforecast.site.status.timezone" + " [\"" + configuration.get().timeZone + "\"]"); } } else { updateStatus(ThingStatus.ONLINE); - refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, - configuration.get().channelRefreshInterval, TimeUnit.MINUTES)); + refreshJob = Optional.of( + scheduler.scheduleWithFixedDelay(this::getData, 0, REFRESH_ACTUAL_INTERVAL, TimeUnit.MINUTES)); } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, @@ -117,78 +118,20 @@ private synchronized void getData() { return; } ZonedDateTime now = ZonedDateTime.now(getTimeZone()); - double actualSum = 0; - double actualPowerSum = 0; - double remainSum = 0; - double todaySum = 0; - double day1Sum = 0; - double day1SumLow = 0; - double day1SumHigh = 0; - double day2Sum = 0; - double day2SumLow = 0; - double day2SumHigh = 0; - double day3Sum = 0; - double day3SumLow = 0; - double day3SumHigh = 0; - double day4Sum = 0; - double day4SumLow = 0; - double day4SumHigh = 0; - double day5Sum = 0; - double day5SumLow = 0; - double day5SumHigh = 0; - double day6Sum = 0; - double day6SumLow = 0; - double day6SumHigh = 0; - + double energySum = 0; + double powerSum = 0; + double daySum = 0; for (Iterator iterator = parts.iterator(); iterator.hasNext();) { SolcastPlaneHandler sfph = iterator.next(); SolcastObject fo = sfph.fetchData(); - actualSum += fo.getActualValue(now, QueryMode.Estimation); - actualPowerSum += fo.getActualPowerValue(now, QueryMode.Estimation); - remainSum += fo.getRemainingProduction(now, QueryMode.Estimation); - LocalDate nowDate = now.toLocalDate(); - todaySum += fo.getDayTotal(nowDate, QueryMode.Estimation); - day1Sum += fo.getDayTotal(nowDate.plusDays(1), QueryMode.Estimation); - day1SumLow += fo.getDayTotal(nowDate.plusDays(1), QueryMode.Pessimistic); - day1SumHigh += fo.getDayTotal(nowDate.plusDays(1), QueryMode.Optimistic); - day2Sum += fo.getDayTotal(nowDate.plusDays(2), QueryMode.Estimation); - day2SumLow += fo.getDayTotal(nowDate.plusDays(2), QueryMode.Pessimistic); - day2SumHigh += fo.getDayTotal(nowDate.plusDays(2), QueryMode.Optimistic); - day3Sum += fo.getDayTotal(nowDate.plusDays(3), QueryMode.Estimation); - day3SumLow += fo.getDayTotal(nowDate.plusDays(3), QueryMode.Pessimistic); - day3SumHigh += fo.getDayTotal(nowDate.plusDays(3), QueryMode.Optimistic); - day4Sum += fo.getDayTotal(nowDate.plusDays(4), QueryMode.Estimation); - day4SumLow += fo.getDayTotal(nowDate.plusDays(4), QueryMode.Pessimistic); - day4SumHigh += fo.getDayTotal(nowDate.plusDays(4), QueryMode.Optimistic); - day5Sum += fo.getDayTotal(nowDate.plusDays(5), QueryMode.Estimation); - day5SumLow += fo.getDayTotal(nowDate.plusDays(5), QueryMode.Pessimistic); - day5SumHigh += fo.getDayTotal(nowDate.plusDays(5), QueryMode.Optimistic); - day6Sum += fo.getDayTotal(nowDate.plusDays(6), QueryMode.Estimation); - day6SumLow += fo.getDayTotal(nowDate.plusDays(6), QueryMode.Pessimistic); - day6SumHigh += fo.getDayTotal(nowDate.plusDays(6), QueryMode.Optimistic); + energySum += fo.getActualEnergyValue(now, QueryMode.Estimation); + powerSum += fo.getActualPowerValue(now, QueryMode.Estimation); + daySum += fo.getDayTotal(now.toLocalDate(), QueryMode.Estimation); } - updateState(CHANNEL_ACTUAL, Utils.getEnergyState(actualSum)); - updateState(CHANNEL_ACTUAL_POWER, Utils.getPowerState(actualPowerSum)); - updateState(CHANNEL_REMAINING, Utils.getEnergyState(remainSum)); - updateState(CHANNEL_TODAY, Utils.getEnergyState(todaySum)); - updateState(CHANNEL_DAY1, Utils.getEnergyState(day1Sum)); - updateState(CHANNEL_DAY1_HIGH, Utils.getEnergyState(day1SumHigh)); - updateState(CHANNEL_DAY1_LOW, Utils.getEnergyState(day1SumLow)); - updateState(CHANNEL_DAY2, Utils.getEnergyState(day2Sum)); - updateState(CHANNEL_DAY2_HIGH, Utils.getEnergyState(day2SumHigh)); - updateState(CHANNEL_DAY2_LOW, Utils.getEnergyState(day2SumLow)); - updateState(CHANNEL_DAY3, Utils.getEnergyState(day3Sum)); - updateState(CHANNEL_DAY3_HIGH, Utils.getEnergyState(day3SumHigh)); - updateState(CHANNEL_DAY3_LOW, Utils.getEnergyState(day3SumLow)); - updateState(CHANNEL_DAY4, Utils.getEnergyState(day4Sum)); - updateState(CHANNEL_DAY4_HIGH, Utils.getEnergyState(day4SumHigh)); - updateState(CHANNEL_DAY4_LOW, Utils.getEnergyState(day4SumLow)); - updateState(CHANNEL_DAY5, Utils.getEnergyState(day5Sum)); - updateState(CHANNEL_DAY5_HIGH, Utils.getEnergyState(day5SumHigh)); - updateState(CHANNEL_DAY5_LOW, Utils.getEnergyState(day5SumLow)); - updateState(CHANNEL_DAY6, Utils.getEnergyState(day6Sum)); - updateState(CHANNEL_DAY6_HIGH, Utils.getEnergyState(day6SumHigh)); - updateState(CHANNEL_DAY6_LOW, Utils.getEnergyState(day6SumLow)); + 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)); } synchronized void addPlane(SolcastPlaneHandler sph) { 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 index ad0f21e943648..b66cae33cfc66 100644 --- 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 @@ -16,7 +16,6 @@ import static org.openhab.binding.solarforecast.internal.solcast.SolcastConstants.*; import java.time.Instant; -import java.time.LocalDate; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.Collection; @@ -162,33 +161,22 @@ protected SolcastObject fetchData() { private void updateChannels(SolcastObject f) { ZonedDateTime now = ZonedDateTime.now(bridgeHandler.get().getTimeZone()); - updateState(CHANNEL_ACTUAL, Utils.getEnergyState(f.getActualValue(now, QueryMode.Estimation))); - updateState(CHANNEL_ACTUAL_POWER, Utils.getPowerState(f.getActualPowerValue(now, QueryMode.Estimation))); - updateState(CHANNEL_REMAINING, Utils.getEnergyState(f.getRemainingProduction(now, QueryMode.Estimation))); - LocalDate nowDate = now.toLocalDate(); - updateState(CHANNEL_TODAY, Utils.getEnergyState(f.getDayTotal(nowDate, QueryMode.Estimation))); - updateState(CHANNEL_DAY1, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(1), QueryMode.Estimation))); - updateState(CHANNEL_DAY1_HIGH, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(1), QueryMode.Optimistic))); - updateState(CHANNEL_DAY1_LOW, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(1), QueryMode.Pessimistic))); - updateState(CHANNEL_DAY2, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(2), QueryMode.Estimation))); - updateState(CHANNEL_DAY2_HIGH, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(2), QueryMode.Optimistic))); - updateState(CHANNEL_DAY2_LOW, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(2), QueryMode.Pessimistic))); - updateState(CHANNEL_DAY3, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(3), QueryMode.Estimation))); - updateState(CHANNEL_DAY3_HIGH, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(3), QueryMode.Optimistic))); - updateState(CHANNEL_DAY3_LOW, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(3), QueryMode.Pessimistic))); - updateState(CHANNEL_DAY4, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(4), QueryMode.Estimation))); - updateState(CHANNEL_DAY4_HIGH, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(4), QueryMode.Optimistic))); - updateState(CHANNEL_DAY4_LOW, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(4), QueryMode.Pessimistic))); - updateState(CHANNEL_DAY5, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(5), QueryMode.Estimation))); - updateState(CHANNEL_DAY5_HIGH, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(5), QueryMode.Optimistic))); - updateState(CHANNEL_DAY5_LOW, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(5), QueryMode.Pessimistic))); - updateState(CHANNEL_DAY6, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(6), QueryMode.Estimation))); - updateState(CHANNEL_DAY6_HIGH, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(6), QueryMode.Optimistic))); - updateState(CHANNEL_DAY6_LOW, Utils.getEnergyState(f.getDayTotal(nowDate.plusDays(6), QueryMode.Pessimistic))); + double energyDay = f.getDayTotal(now.toLocalDate(), QueryMode.Estimation); + double energyProduced = f.getActualEnergyValue(now, QueryMode.Estimation); + 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, QueryMode.Estimation))); } private synchronized void setForecast(SolcastObject f) { forecast = Optional.of(f); + sendTimeSeries(CHANNEL_POWER_ESTIMATE, f.getPowerTimeSeries(QueryMode.Estimation)); + sendTimeSeries(CHANNEL_POWER_ESTIMATE10, f.getPowerTimeSeries(QueryMode.Optimistic)); + sendTimeSeries(CHANNEL_POWER_ESTIMATE90, f.getPowerTimeSeries(QueryMode.Pessimistic)); + sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, f.getEnergyTimeSeries(QueryMode.Estimation)); + sendTimeSeries(CHANNEL_ENERGY_ESTIMATE10, f.getEnergyTimeSeries(QueryMode.Optimistic)); + sendTimeSeries(CHANNEL_ENERGY_ESTIMATE90, f.getEnergyTimeSeries(QueryMode.Pessimistic)); } @Override 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 index 603a7c5b83b3a..ab24394360db0 100644 --- 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 @@ -15,11 +15,6 @@ If you have a paid subscription plan - - - Refresh rate of channel data - 1 - 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 index d9cfdccfc0d42..a762827525d4b 100644 --- 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 @@ -5,11 +5,6 @@ xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> - - - Data refresh rate of forecast data - 120 - Resource Id of Solcast rooftop site 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 index a56431f64d64d..404c3b3de90fe 100644 --- 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 @@ -9,11 +9,6 @@ API key from your subscription - - - Refresh rate of channel data - 1 - Time zone of forecast location 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 index eeaafe6314b7d..f70bb05b74181 100644 --- 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 @@ -4,146 +4,57 @@ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - - Number:Energy - - Forecast of todays remaining production - - - - Number:Energy - - Todays production forecast till now - - - + Number:Power Predicted power in this moment - - Number:Energy - - Todays forecast in total - - - - String - - Plain JSON response without conversions - - - String - - Plain JSON response from tuning call - - - Number:Energy - - Tomorrows forecast in total - - - - Number:Energy - - Tomorrows pessimistic forecast - - - - Number:Energy - - Tomorrows optimistic forecast - - - - Number:Energy - - Day after tomorrow estimation - - - - Number:Energy - - Day after tomorrow pessimistic estimation - - - - Number:Energy - - Day after tomorrow optimistic estimation - - - - Number:Energy - - Day 3 estimation - - - - Number:Energy - - Day 3 pessimistic estimation - - - - Number:Energy - - Day 3 optimistic estimation - - - - Number:Energy - - Day 4 estimation - - - - Number:Energy - - Day 4 pessimistic estimation + + Number:Power + + Predicted power in this moment - - Number:Energy - - Day 4 optimistic estimation + + Number:Power + + Predicted power in this moment - - Number:Energy - - Day 5 estimation + + Number:Power + + Predicted power in this moment - + Number:Energy - - Day 5 pessimistic estimation + + Todays production forecast till now - + Number:Energy - - Day 5 optimistic estimation + + Todays production forecast till now - + Number:Energy - - Day 6 estimation + + Todays production forecast till now - + Number:Energy - - Day 6 pessimistic estimation + + Todays production forecast till now - - Number:Energy - - Day 6 optimistic estimation - + + 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 index 687ab2d153257..863c63d26bee0 100644 --- 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 @@ -13,13 +13,12 @@ PV Plane as part 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 index e5639712e7760..2f4b0655254f0 100644 --- 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 @@ -9,13 +9,12 @@ Site location for Forecast Solar - - - - - - - + + + + + + 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 index 199dec4839d7d..970b5b9282fa5 100644 --- 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 @@ -13,30 +13,17 @@ PV Plane as part 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 index 82bf320ba0dd1..f41c6925f72fa 100644 --- 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 @@ -9,28 +9,16 @@ Solcast service site definition - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + 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 index bdd41d9f9decd..9e8d4ae68336e 100644 --- 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 @@ -20,6 +20,7 @@ import java.time.format.DateTimeFormatter; import javax.measure.quantity.Energy; +import javax.measure.quantity.Power; import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.Test; @@ -28,6 +29,7 @@ import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.Units; import org.openhab.core.types.State; +import org.openhab.core.types.TimeSeries; import org.openhab.core.types.UnDefType; /** @@ -48,17 +50,17 @@ void testForecastObject() { // "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.getActualValue(queryDateTime), TOLERANCE, "Current Production"); + 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.getActualValue(queryDateTime) + fo.getRemainingProduction(queryDateTime), TOLERANCE, + 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.getActualValue(queryDateTime), TOLERANCE, "Actual production"); + 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"); } @@ -86,15 +88,15 @@ void testInterpolation() { double previousValue = 0; for (int i = 0; i < 60; i++) { queryDateTime = queryDateTime.plusMinutes(1); - assertTrue(previousValue < fo.getActualValue(queryDateTime)); - previousValue = fo.getActualValue(queryDateTime); + 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.getActualValue(queryDateTime), 0.002, "Actual estimation"); + assertEquals(0.535, fo.getActualEnergyValue(queryDateTime), 0.002, "Actual estimation"); } @Test @@ -103,7 +105,7 @@ void testForecastSum() { ZonedDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 23).atZone(TEST_ZONE); ForecastSolarObject fo = new ForecastSolarObject(content, queryDateTime.toInstant()); QuantityType actual = QuantityType.valueOf(0, Units.KILOWATT_HOUR); - State st = Utils.getEnergyState(fo.getActualValue(queryDateTime)); + State st = Utils.getEnergyState(fo.getActualEnergyValue(queryDateTime)); assertTrue(st instanceof QuantityType); actual = actual.add((QuantityType) st); assertEquals(49.431, actual.floatValue(), TOLERANCE, "Current Production"); @@ -117,7 +119,7 @@ void testCornerCases() { ForecastSolarObject fo = new ForecastSolarObject(); assertFalse(fo.isValid()); ZonedDateTime query = LocalDateTime.of(2022, 7, 17, 16, 23).atZone(TEST_ZONE); - assertEquals(-1.0, fo.getActualValue(query), TOLERANCE, "Actual Production"); + assertEquals(-1.0, fo.getActualEnergyValue(query), TOLERANCE, "Actual Production"); assertEquals(-1.0, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Today Production"); assertEquals(-1.0, fo.getRemainingProduction(query), TOLERANCE, "Remaining Production"); assertEquals(-1.0, fo.getDayTotal(query.plusDays(1).toLocalDate()), TOLERANCE, "Tomorrow Production"); @@ -127,40 +129,40 @@ void testCornerCases() { query = LocalDateTime.of(2022, 7, 16, 23, 59).atZone(TEST_ZONE); fo = new ForecastSolarObject(content, query.toInstant()); assertEquals(-1.0, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope"); - assertEquals(-1.0, fo.getActualValue(query), TOLERANCE, "Actual out of scope"); + assertEquals(-1.0, fo.getActualEnergyValue(query), TOLERANCE, "Actual out of scope"); assertEquals(-1.0, fo.getRemainingProduction(query), TOLERANCE, "Remain out of scope"); assertEquals(-1.0, fo.getActualPowerValue(query), TOLERANCE, "Remain out of scope"); // 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.getActualValue(query), 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); assertEquals(-1.0, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope"); - assertEquals(-1.0, fo.getActualValue(query), TOLERANCE, "Actual out of scope"); + assertEquals(-1.0, fo.getActualEnergyValue(query), TOLERANCE, "Actual out of scope"); assertEquals(-1.0, fo.getRemainingProduction(query), TOLERANCE, "Remain out of scope"); assertEquals(-1.0, fo.getActualPowerValue(query), TOLERANCE, "Remain out of scope"); // 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.getActualValue(query), 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.getActualValue(query), 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.getActualValue(query), 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"); } @@ -191,4 +193,27 @@ void testActions() { assertEquals(UnDefType.UNDEF, fo.getDay(queryDateTime.toLocalDate(), "pessimistic")); assertEquals(UnDefType.UNDEF, fo.getDay(queryDateTime.toLocalDate(), "total", "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(content, queryDateTime.toInstant()); + + TimeSeries powerSeries = fo.getPowerTimeSeries(); + 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(); + assertEquals(36, energySeries.size()); + energySeries.getStates().forEachOrdered(entry -> { + State s = entry.state(); + assertTrue(s instanceof QuantityType); + assertEquals("kWh", ((QuantityType) s).getUnit().toString()); + }); + } } 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 index 11b4493298712..b98f3546b30e9 100644 --- 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 @@ -20,6 +20,10 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; + +import javax.measure.quantity.Power; import org.eclipse.jdt.annotation.NonNullByDefault; import org.json.JSONObject; @@ -30,6 +34,8 @@ import org.openhab.binding.solarforecast.internal.solcast.SolcastObject.QueryMode; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.Units; +import org.openhab.core.types.State; +import org.openhab.core.types.TimeSeries; import org.openhab.core.types.UnDefType; /** @@ -103,7 +109,7 @@ void testForecastObject() { scfo.join(content); // test one day, step ahead in time and cross check channel values double dayTotal = scfo.getDayTotal(now.toLocalDate(), QueryMode.Estimation); - double actual = scfo.getActualValue(now, QueryMode.Estimation); + double actual = scfo.getActualEnergyValue(now, QueryMode.Estimation); double remain = scfo.getRemainingProduction(now, QueryMode.Estimation); assertEquals(0.0, actual, TOLERANCE, "Begin of day actual"); assertEquals(23.107, remain, TOLERANCE, "Begin of day remaining"); @@ -115,7 +121,7 @@ void testForecastObject() { double power = scfo.getActualPowerValue(now, QueryMode.Estimation) / 2.0; double powerAddOn = ((power + previousPower) / 2.0); actual += powerAddOn; - assertEquals(actual, scfo.getActualValue(now, QueryMode.Estimation), TOLERANCE, "Actual at " + now); + assertEquals(actual, scfo.getActualEnergyValue(now, QueryMode.Estimation), TOLERANCE, "Actual at " + now); remain -= powerAddOn; assertEquals(remain, scfo.getRemainingProduction(now, QueryMode.Estimation), TOLERANCE, "Remain at " + now); assertEquals(dayTotal, actual + remain, TOLERANCE, "Total sum at " + now); @@ -235,7 +241,7 @@ 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(content, now.toInstant(), TIMEZONEPROVIDER); - assertEquals(0.42, scfo.getActualValue(now, QueryMode.Estimation), TOLERANCE, "Actual estimation"); + assertEquals(0.42, scfo.getActualEnergyValue(now, QueryMode.Estimation), TOLERANCE, "Actual estimation"); assertEquals(25.413, scfo.getDayTotal(now.toLocalDate(), QueryMode.Estimation), TOLERANCE, "Day total"); } @@ -244,10 +250,10 @@ 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(content, now.toInstant(), TIMEZONEPROVIDER); - assertEquals(-1.0, scfo.getActualValue(now, QueryMode.Estimation), 0.01, "Invalid"); + assertEquals(-1.0, scfo.getActualEnergyValue(now, QueryMode.Estimation), 0.01, "Invalid"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); - assertEquals(18.946, scfo.getActualValue(now, QueryMode.Estimation), 0.01, "Actual data"); + assertEquals(18.946, scfo.getActualEnergyValue(now, QueryMode.Estimation), 0.01, "Actual data"); assertEquals(23.107, scfo.getDayTotal(now.toLocalDate(), QueryMode.Estimation), 0.01, "Today data"); JSONObject rawJson = new JSONObject(scfo.getRaw()); assertTrue(rawJson.has("forecasts")); @@ -259,7 +265,7 @@ 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(content, now.toInstant(), TIMEZONEPROVIDER); - assertEquals(-1.0, scfo.getActualValue(now, QueryMode.Estimation), 0.01, "Invalid"); + assertEquals(-1.0, scfo.getActualEnergyValue(now, QueryMode.Estimation), 0.01, "Invalid"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); @@ -322,10 +328,10 @@ void testInavlid() { String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); ZonedDateTime now = ZonedDateTime.now(TEST_ZONE); SolcastObject scfo = new SolcastObject(content, now.toInstant(), TIMEZONEPROVIDER); - assertEquals(-1.0, scfo.getActualValue(now, QueryMode.Estimation), 0.01, "Data available - day not in"); + assertEquals(-1.0, scfo.getActualEnergyValue(now, QueryMode.Estimation), 0.01, "Data available - day not in"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); - assertEquals(-1.0, scfo.getActualValue(now, QueryMode.Estimation), 0.01, + assertEquals(-1.0, scfo.getActualEnergyValue(now, QueryMode.Estimation), 0.01, "Data available after merge - day not in"); assertEquals(-1.0, scfo.getDayTotal(now.toLocalDate(), QueryMode.Estimation), 0.01, "Data available after merge - day not in"); @@ -361,16 +367,17 @@ void testEnergyInterpolation() { double maxDiff = 0; double productionExpected = 0; for (int i = 0; i < 1000; i++) { - double forecast = sco.getActualValue(now.plusMinutes(i), QueryMode.Estimation); + double forecast = sco.getActualEnergyValue(now.plusMinutes(i), QueryMode.Estimation); // int hour = now.plusMinutes(i).getHour(); // int minute = now.plusMinutes(i).getMinute(); double addOnExpected = sco.getActualPowerValue(now.plusMinutes(i), QueryMode.Estimation) / 60.0; productionExpected += addOnExpected; double diff = forecast - productionExpected; maxDiff = Math.max(diff, maxDiff); - // System.out.println(hour + ":" + minute + " : " + Math.round(forecast * 1000) / 1000.0 + " - " - // + Math.round(productionExpected * 1000) / 1000.0 + " - " + Math.round(diff * 1000) / 1000.0); - assertEquals(productionExpected, sco.getActualValue(now.plusMinutes(i), QueryMode.Estimation), + // System.out.println(hour + ":" + minute + " : Forecast " + Math.round(forecast * 1000) / 1000.0 + // + " - Expected " + Math.round(productionExpected * 1000) / 1000.0 + " - Diff " + // + Math.round(diff * 1000) / 1000.0); + assertEquals(productionExpected, sco.getActualEnergyValue(now.plusMinutes(i), QueryMode.Estimation), 100 * TOLERANCE, "Step " + i); } } @@ -417,4 +424,96 @@ void testTimes() { 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(content, now.toInstant(), TIMEZONEPROVIDER); + content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + sco.join(content); + + TimeSeries powerSeries = sco.getPowerTimeSeries(QueryMode.Estimation); + 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()); + estimateL.add((QuantityType) entry.state()); + }); + + 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()); + estimate10.add((QuantityType) entry.state()); + }); + + 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()); + estimate90.add((QuantityType) entry.state()); + }); + + 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(content, now.toInstant(), TIMEZONEPROVIDER); + content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + sco.join(content); + + TimeSeries energySeries = sco.getEnergyTimeSeries(QueryMode.Estimation); + 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()); + estimateL.add((QuantityType) entry.state()); + }); + + 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()); + estimate10.add((QuantityType) entry.state()); + }); + + 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()); + estimate90.add((QuantityType) entry.state()); + }); + + 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); + } + } } From d659adb2b2df3b7c7db1ab4c7ff048b959e999d5 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 9 Jan 2024 20:55:09 +0100 Subject: [PATCH 070/111] review comments Signed-off-by: Bernd Weymann --- .../README.md | 8 +-- .../SolarForecastBindingConstants.java | 1 - .../internal/actions/SolarForecast.java | 15 +++-- .../forecastsolar/ForecastSolarObject.java | 18 +++--- .../ForecastSolarBridgeConfiguration.java | 2 +- .../handler/ForecastSolarBridgeHandler.java | 2 +- .../internal/solcast/SolcastObject.java | 29 +++++----- .../config/SolcastBridgeConfiguration.java | 2 +- .../solcast/handler/SolcastBridgeHandler.java | 4 +- .../solarforecast/internal/utils/Utils.java | 18 +++--- .../OH-INF/config/fs-site-config.xml | 1 - .../OH-INF/config/sc-site-config.xml | 1 - .../OH-INF/i18n/solarforecast.properties | 55 ++++++++++++------- .../resources/OH-INF/thing/fs-plane-type.xml | 4 +- .../resources/OH-INF/thing/fs-site-type.xml | 4 +- .../resources/OH-INF/thing/sc-plane-type.xml | 4 +- .../resources/OH-INF/thing/sc-site-type.xml | 4 +- .../solarforecast/ForecastSolarTest.java | 9 +-- .../binding/solarforecast/SolcastTest.java | 22 ++++---- 19 files changed, 109 insertions(+), 94 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index be17b27e0e115..4cbbce205e692 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -61,11 +61,11 @@ After measurement is sent the `raw-tuning` channel is reporting the result. |------------------------|---------|---------------------------------------|-------------|----------|----------| | apiKey | text | API Key | N/A | yes | no | | channelRefreshInterval | integer | Channel Refresh Interval in minutes | 1 | yes | no | -| timeZone | text | Time Zone of forecast location | auto-detect | no | yes | +| 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` is set to `auto-detect` to evaluate Regional Settings of your openHAB installation. +`timeZone` leave it empty to evaluate Regional Settings of your openHAB installation. See [DateTime](#date-time) section for more information. ### Solcast Plane Configuration @@ -116,12 +116,12 @@ You can try it without any registration or other preconditions. | Name | Type | Description | Default | Required | |------------------------|---------|---------------------------------------|--------------|----------| -| location | text | Location of Photovoltaic system | auto-detect | yes | +| location | text | Location of Photovoltaic system | empty | yes | | channelRefreshInterval | integer | Channel Refresh Interval in minutes | 1 | yes | | apiKey | text | API Key | N/A | no | `location` defines latitude, longitude values of your PV system. -In case of auto-detect the location configured in openHAB is obtained. +In case of empty the location configured in openHAB is obtained. `apiKey` can be given in case you subscribed to a paid plan 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 index d7d5a46e2df9d..1cfe28a9b65b7 100644 --- 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 @@ -48,7 +48,6 @@ public class SolarForecastBindingConstants { public static final String CHANNEL_RAW = "raw"; public static final int REFRESH_ACTUAL_INTERVAL = 1; - public static final String AUTODETECT = "auto-detect"; public static final String SLASH = "/"; public static final String EMPTY = ""; } 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 index 690a6ca1084b1..83f3f387ec317 100644 --- 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 @@ -15,8 +15,11 @@ 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.core.types.State; +import org.openhab.core.library.types.QuantityType; /** * The {@link SolarForecast} Interface needed for Actions @@ -41,7 +44,7 @@ public interface SolarForecast { * @param args possible arguments from this interface * @return QuantityType in kW/h */ - public State getDay(LocalDate date, String... args); + public QuantityType getDay(LocalDate date, String... args); /** * Returns electric energy between two timestamps @@ -51,7 +54,7 @@ public interface SolarForecast { * @param args possible arguments from this interface * @return QuantityType in kW/h */ - public State getEnergy(Instant start, Instant end, String... args); + public QuantityType getEnergy(Instant start, Instant end, String... args); /** * Returns electric power at one specific point of time @@ -60,19 +63,19 @@ public interface SolarForecast { * @param args possible arguments from this interface * @return QuantityType in kW */ - public State getPower(Instant timestamp, String... args); + public QuantityType getPower(Instant timestamp, String... args); /** * Get the first date and time of forecast data * - * @return your localized date time + * @return date time */ public Instant getForecastBegin(); /** * Get the last date and time of forecast data * - * @return your localized date time + * @return date time */ public Instant getForecastEnd(); } 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 index 01949b7e11ee9..e885f3b9fe0f7 100644 --- 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 @@ -24,15 +24,17 @@ 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.actions.SolarForecast; import org.openhab.binding.solarforecast.internal.utils.Utils; -import org.openhab.core.types.State; +import org.openhab.core.library.types.QuantityType; import org.openhab.core.types.TimeSeries; import org.openhab.core.types.TimeSeries.Policy; -import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -239,20 +241,20 @@ public String toString() { * SolarForecast Interface */ @Override - public State getDay(LocalDate localDate, String... args) { + public QuantityType getDay(LocalDate localDate, String... args) { if (args.length > 0) { logger.info("ForecastSolar doesn't accept arguments"); - return UnDefType.UNDEF; + return Utils.getEnergyState(-1); } double measure = getDayTotal(localDate); return Utils.getEnergyState(measure); } @Override - public State getEnergy(Instant start, Instant end, String... args) { + public QuantityType getEnergy(Instant start, Instant end, String... args) { if (args.length > 0) { logger.info("ForecastSolar doesn't accept arguments"); - return UnDefType.UNDEF; + return Utils.getEnergyState(-1); } LocalDate beginDate = start.atZone(zone).toLocalDate(); LocalDate endDate = end.atZone(zone).toLocalDate(); @@ -279,10 +281,10 @@ public State getEnergy(Instant start, Instant end, String... args) { } @Override - public State getPower(Instant timestamp, String... args) { + public QuantityType getPower(Instant timestamp, String... args) { if (args.length > 0) { logger.info("ForecastSolar doesn't accept arguments"); - return UnDefType.UNDEF; + return Utils.getPowerState(-1); } double measure = getActualPowerValue(timestamp.atZone(zone)); return Utils.getPowerState(measure); 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 index 8281842fc5248..89f1fd1208218 100644 --- 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 @@ -22,7 +22,7 @@ */ @NonNullByDefault public class ForecastSolarBridgeConfiguration { - public String location = "auto-detect"; + 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/handler/ForecastSolarBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarBridgeHandler.java index af2e688e6e2ca..a0004f750d87c 100644 --- 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 @@ -66,7 +66,7 @@ public Collection> getServices() { @Override public void initialize() { ForecastSolarBridgeConfiguration config = getConfigAs(ForecastSolarBridgeConfiguration.class); - if (config.location.equals(AUTODETECT)) { + if (config.location.isEmpty()) { Configuration editConfig = editConfiguration(); editConfig.put("location", homeLocation.toString()); updateConfiguration(editConfig); 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 index 8f7b044d0ea77..e24b9df87895c 100644 --- 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 @@ -22,18 +22,19 @@ 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.actions.SolarForecast; import org.openhab.binding.solarforecast.internal.utils.Utils; import org.openhab.core.i18n.TimeZoneProvider; -import org.openhab.core.types.State; +import org.openhab.core.library.types.QuantityType; import org.openhab.core.types.TimeSeries; import org.openhab.core.types.TimeSeries.Policy; -import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -92,7 +93,7 @@ public void join(String content) { } private void add(String content) { - if (!content.equals(SolarForecastBindingConstants.EMPTY)) { + if (!content.isEmpty()) { valid = true; JSONObject contentJson = new JSONObject(content); JSONArray resultJsonArray; @@ -327,14 +328,14 @@ private TreeMap getDataMap(QueryMode mode) { * SolarForecast Interface */ @Override - public State getDay(LocalDate date, String... args) { + public QuantityType getDay(LocalDate date, String... args) { QueryMode mode = evalArguments(args); if (mode.equals(QueryMode.Error)) { - return UnDefType.UNDEF; + return Utils.getEnergyState(-1); } else if (mode.equals(QueryMode.Optimistic) || mode.equals(QueryMode.Pessimistic)) { if (date.isBefore(LocalDate.now())) { logger.info("{} forecasts only available for future", mode); - return UnDefType.UNDEF; + return Utils.getEnergyState(-1); } } double measure = getDayTotal(date, mode); @@ -342,18 +343,18 @@ public State getDay(LocalDate date, String... args) { } @Override - public State getEnergy(Instant start, Instant end, String... args) { + public QuantityType getEnergy(Instant start, Instant end, String... args) { if (end.isBefore(start)) { logger.info("End {} defined before Start {}", end, start); - return UnDefType.UNDEF; + return Utils.getEnergyState(-1); } QueryMode mode = evalArguments(args); if (mode.equals(QueryMode.Error)) { - return UnDefType.UNDEF; + return Utils.getEnergyState(-1); } else if (mode.equals(QueryMode.Optimistic) || mode.equals(QueryMode.Pessimistic)) { if (end.isBefore(Instant.now())) { logger.info("{} forecasts only available for future", mode); - return UnDefType.UNDEF; + return Utils.getEnergyState(-1); } } LocalDate beginDate = start.atZone(timeZoneProvider.getTimeZone()).toLocalDate(); @@ -382,15 +383,15 @@ public State getEnergy(Instant start, Instant end, String... args) { } @Override - public State getPower(Instant timestamp, String... args) { + public QuantityType getPower(Instant timestamp, String... args) { // eliminate error cases and return immediately QueryMode mode = evalArguments(args); if (mode.equals(QueryMode.Error)) { - return UnDefType.UNDEF; + return Utils.getPowerState(-1); } else if (mode.equals(QueryMode.Optimistic) || mode.equals(QueryMode.Pessimistic)) { if (timestamp.isBefore(Instant.now().minus(1, ChronoUnit.MINUTES))) { logger.info("{} forecasts only available for future", mode); - return UnDefType.UNDEF; + return Utils.getPowerState(-1); } } double measure = getActualPowerValue(ZonedDateTime.ofInstant(timestamp, timeZoneProvider.getTimeZone()), mode); 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 index 9dc52934ee63d..248b718854c88 100644 --- 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 @@ -23,5 +23,5 @@ @NonNullByDefault public class SolcastBridgeConfiguration { public String apiKey = SolarForecastBindingConstants.EMPTY; - public String timeZone = SolarForecastBindingConstants.AUTODETECT; + public String timeZone = SolarForecastBindingConstants.EMPTY; } 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 index 9fde42a3b3a95..2314bf6fd6cf1 100644 --- 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 @@ -1,6 +1,4 @@ /** - updateState(CHANNEL_ENERGY_REMAIN, Utils.getEnergyState(daySum - energySum)); - updateState(CHANNEL_ENERGY_TODAY, Utils.getEnergyState(daySum)); * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional @@ -76,7 +74,7 @@ public void initialize() { SolcastBridgeConfiguration config = getConfigAs(SolcastBridgeConfiguration.class); configuration = Optional.of(config); if (!EMPTY.equals(config.apiKey)) { - if (!AUTODETECT.equals(configuration.get().timeZone)) { + if (!configuration.get().timeZone.isEmpty()) { try { timeZone = ZoneId.of(configuration.get().timeZone); updateStatus(ThingStatus.ONLINE); 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 index e58c5928e3e83..ae016e24d6f06 100644 --- 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 @@ -13,12 +13,12 @@ package org.openhab.binding.solarforecast.internal.utils; import javax.measure.MetricPrefix; +import javax.measure.quantity.Energy; +import javax.measure.quantity.Power; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.Units; -import org.openhab.core.types.State; -import org.openhab.core.types.UnDefType; /** * The {@link Utils} Helpers for Solcast and ForecastSolar @@ -27,19 +27,17 @@ */ @NonNullByDefault public class Utils { - public static State getEnergyState(double d) { + public static QuantityType getEnergyState(double d) { if (d < 0) { - return UnDefType.UNDEF; - } else { - return QuantityType.valueOf(Math.round(d * 1000) / 1000.0, Units.KILOWATT_HOUR); + return QuantityType.valueOf(-1, Units.KILOWATT_HOUR); } + return QuantityType.valueOf(Math.round(d * 1000) / 1000.0, Units.KILOWATT_HOUR); } - public static State getPowerState(double d) { + public static QuantityType getPowerState(double d) { if (d < 0) { - return UnDefType.UNDEF; - } else { - return QuantityType.valueOf(Math.round(d * 1000) / 1000.0, MetricPrefix.KILO(Units.WATT)); + return QuantityType.valueOf(-1, MetricPrefix.KILO(Units.WATT)); } + return QuantityType.valueOf(Math.round(d * 1000) / 1000.0, MetricPrefix.KILO(Units.WATT)); } } 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 index ab24394360db0..3be22fa52a92e 100644 --- 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 @@ -9,7 +9,6 @@ location Location of photovoltaic system - auto-detect 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 index 404c3b3de90fe..956eec83c08c0 100644 --- 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 @@ -12,7 +12,6 @@ Time zone of forecast location - auto-detect 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 index e7a9a1fbab629..47b5b5539d432 100644 --- 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 @@ -25,36 +25,51 @@ thing-type.config.solarforecast.fs-plane.dampPM.description = Damping factor of 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.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.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 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.channelRefreshInterval.label = Channel Refresh Interval -thing-type.config.solarforecast.fs-site.channelRefreshInterval.description = Refresh rate of channel data 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.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.powerItem.label = Power Item -thing-type.config.solarforecast.sc-plane.powerItem.description = Power item from your solar inverter for this rooftop -thing-type.config.solarforecast.sc-plane.powerUnit.label = Power Item Unit -thing-type.config.solarforecast.sc-plane.powerUnit.description = Unit delivered by power item -thing-type.config.solarforecast.sc-plane.powerUnit.option.auto-detect = auto-detect -thing-type.config.solarforecast.sc-plane.powerUnit.option.kW = Kilowatt -thing-type.config.solarforecast.sc-plane.powerUnit.option.W = Watt -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 +thing-type.config.solarforecast.fs-site.location.description = Location of photovoltaic system 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.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 types + +channel-type.solarforecast.energy-actual-channel.label = Actual Energy Forecast +channel-type.solarforecast.energy-actual-channel.description = Todays production forecast till now +channel-type.solarforecast.energy-estimate-channel.label = Actual Energy Forecast +channel-type.solarforecast.energy-estimate-channel.description = Todays production forecast till now +channel-type.solarforecast.energy-estimate10-channel.label = Actual Energy Forecast +channel-type.solarforecast.energy-estimate10-channel.description = Todays production forecast till now +channel-type.solarforecast.energy-estimate90-channel.label = Actual Energy Forecast +channel-type.solarforecast.energy-estimate90-channel.description = Todays production forecast till now +channel-type.solarforecast.power-actual-channel.label = Actual Power Forecast +channel-type.solarforecast.power-actual-channel.description = Predicted power in this moment +channel-type.solarforecast.power-estimate-channel.label = Actual Power Forecast +channel-type.solarforecast.power-estimate-channel.description = Predicted power in this moment +channel-type.solarforecast.power-estimate10-channel.label = Actual Power Forecast +channel-type.solarforecast.power-estimate10-channel.description = Predicted power in this moment +channel-type.solarforecast.power-estimate90-channel.label = Actual Power Forecast +channel-type.solarforecast.power-estimate90-channel.description = Predicted power in this moment +channel-type.solarforecast.raw-channel.label = Raw JSON Response +channel-type.solarforecast.raw-channel.description = Plain JSON response without conversions + +# thing types config + +thing-type.config.solarforecast.fs-site.channelRefreshInterval.label = Channel Refresh Interval +thing-type.config.solarforecast.fs-site.channelRefreshInterval.description = Refresh rate of channel data thing-type.config.solarforecast.sc-site.channelRefreshInterval.label = Channel Refresh Interval thing-type.config.solarforecast.sc-site.channelRefreshInterval.description = Refresh rate of channel data -thing-type.config.solarforecast.sc-site.timeZone.label = Time Zone -thing-type.config.solarforecast.sc-site.timeZone.description = Time Zone of Forecast Location # channel types @@ -98,8 +113,6 @@ channel-type.solarforecast.day6-high-channel.label = Day 6 High Estimation channel-type.solarforecast.day6-high-channel.description = Day 6 optimistic estimation channel-type.solarforecast.day6-low-channel.label = Day 6 Low Estimation channel-type.solarforecast.day6-low-channel.description = Day 6 pessimistic estimation -channel-type.solarforecast.raw-channel.label = Raw JSON Response -channel-type.solarforecast.raw-channel.description = Plain JSON response without conversions channel-type.solarforecast.raw-tuning-channel.label = Raw JSON Tuning Response channel-type.solarforecast.raw-tuning-channel.description = Plain JSON response from tuning call channel-type.solarforecast.remaining-channel.label = Remaining Production Today @@ -121,7 +134,7 @@ solarforecast.plane.status.bridge-handler-not-found = Bridge handler not found solarforecast.plane.status.wrong-handler = Wrong handler {0} solarforecast.plane.status.power-item = Power Item {0} not found solarforecast.plane.status.await-feedback = Await first feedback -solarforecast.plane.status.http-status = HTTP Status Code {0} +solarforecast.plane.status.http-status = HTTP Status Code {0} # thing actions 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 index 863c63d26bee0..25a19c62e0c39 100644 --- 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 @@ -16,8 +16,8 @@ - - + + 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 index 2f4b0655254f0..63093090d284d 100644 --- 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 @@ -12,8 +12,8 @@ - - + + 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 index 970b5b9282fa5..27b49e217c061 100644 --- 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 @@ -18,8 +18,8 @@ - - + + 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 index f41c6925f72fa..b3f008c3b4245 100644 --- 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 @@ -14,8 +14,8 @@ - - + + 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 index 9e8d4ae68336e..c8d74f790e72e 100644 --- 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 @@ -30,7 +30,6 @@ import org.openhab.core.library.unit.Units; import org.openhab.core.types.State; import org.openhab.core.types.TimeSeries; -import org.openhab.core.types.UnDefType; /** * The {@link ForecastSolarTest} tests responses from forecast solar object @@ -41,6 +40,8 @@ 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);; @Test void testForecastObject() { @@ -189,9 +190,9 @@ void testActions() { fo.getEnergy(queryDateTime.toInstant(), queryDateTime.plusDays(2).toInstant()).toFullString(), "Actual out of scope"); - assertEquals(UnDefType.UNDEF, fo.getDay(queryDateTime.toLocalDate(), "optimistic")); - assertEquals(UnDefType.UNDEF, fo.getDay(queryDateTime.toLocalDate(), "pessimistic")); - assertEquals(UnDefType.UNDEF, fo.getDay(queryDateTime.toLocalDate(), "total", "rubbish")); + assertEquals(ENERGY_UNDEF, fo.getDay(queryDateTime.toLocalDate(), "optimistic")); + assertEquals(ENERGY_UNDEF, fo.getDay(queryDateTime.toLocalDate(), "pessimistic")); + assertEquals(ENERGY_UNDEF, fo.getDay(queryDateTime.toLocalDate(), "total", "rubbish")); } @Test 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 index b98f3546b30e9..d92614bb12936 100644 --- 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 @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.List; +import javax.measure.quantity.Energy; import javax.measure.quantity.Power; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -32,11 +33,11 @@ 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.utils.Utils; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.Units; import org.openhab.core.types.State; import org.openhab.core.types.TimeSeries; -import org.openhab.core.types.UnDefType; /** * The {@link SolcastTest} tests responses from forecast solar website @@ -49,6 +50,8 @@ class SolcastTest { private static final TimeZP TIMEZONEPROVIDER = new TimeZP(); // double comparison tolerance = 1 Watt private static final double TOLERANCE = 0.001; + public static final QuantityType POWER_UNDEF = Utils.getPowerState(-1);; + public static final QuantityType ENERGY_UNDEF = Utils.getEnergyState(-1);; /** * "2022-07-18T00:00+02:00[Europe/Berlin]": 0, @@ -277,9 +280,8 @@ void testActions() { double totalEnergy = 0; ZonedDateTime start = LocalDateTime.of(2022, 7, 18, 0, 0).atZone(TEST_ZONE); for (int i = 0; i < 7; i++) { - QuantityType qt = (QuantityType) scfo.getDay(start.toLocalDate().plusDays(i)); - QuantityType eqt = (QuantityType) scfo.getEnergy(start.plusDays(i).toInstant(), - start.plusDays(i + 1).toInstant()); + 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"); @@ -287,7 +289,7 @@ void testActions() { totalEnergy += qt.doubleValue(); // check if sum is fitting to total energy query - qt = (QuantityType) scfo.getEnergy(start.toInstant(), start.plusDays(i + 1).toInstant()); + qt = scfo.getEnergy(start.toInstant(), start.plusDays(i + 1).toInstant()); // System.out.println("Total: " + qt.doubleValue()); assertEquals(totalEnergy, qt.doubleValue(), TOLERANCE * 2, "Total " + i + " days forecast"); } @@ -315,12 +317,12 @@ void testOptimisticPessimistic() { // access in past shall be rejected Instant past = Instant.now().minus(5, ChronoUnit.MINUTES); - assertEquals(UnDefType.UNDEF, scfo.getPower(past, SolarForecast.OPTIMISTIC), "Optimistic Power"); - assertEquals(UnDefType.UNDEF, scfo.getPower(past, SolarForecast.PESSIMISTIC), "Pessimistic Power"); - assertEquals(UnDefType.UNDEF, scfo.getPower(past, "total", "rubbish"), "Rubbish arguments"); - assertEquals(UnDefType.UNDEF, scfo.getPower(past.plus(2, ChronoUnit.HOURS), "total", "rubbish"), + assertEquals(POWER_UNDEF, scfo.getPower(past, SolarForecast.OPTIMISTIC), "Optimistic Power"); + assertEquals(POWER_UNDEF, scfo.getPower(past, SolarForecast.PESSIMISTIC), "Pessimistic Power"); + assertEquals(POWER_UNDEF, scfo.getPower(past, "total", "rubbish"), "Rubbish arguments"); + assertEquals(POWER_UNDEF, scfo.getPower(past.plus(2, ChronoUnit.HOURS), "total", "rubbish"), "Rubbish arguments"); - assertEquals(UnDefType.UNDEF, scfo.getPower(past), "Normal Power"); + assertEquals(POWER_UNDEF, scfo.getPower(past), "Normal Power"); } @Test From 81b0ab49287753e9da0a85e16e1f782ee9b06857 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Wed, 10 Jan 2024 00:38:41 +0100 Subject: [PATCH 071/111] IllegalArguments introduced Signed-off-by: Bernd Weymann --- .../README.md | 2 - .../internal/actions/SolarForecast.java | 6 +-- .../actions/SolarForecastActions.java | 26 ++++----- .../forecastsolar/ForecastSolarObject.java | 43 +++++++-------- .../handler/ForecastSolarBridgeHandler.java | 3 +- .../handler/ForecastSolarPlaneHandler.java | 18 ++++--- .../internal/solcast/SolcastConstants.java | 8 +-- .../internal/solcast/SolcastObject.java | 46 +++++++++------- .../solcast/handler/SolcastBridgeHandler.java | 11 ++-- .../solcast/handler/SolcastPlaneHandler.java | 6 ++- .../OH-INF/config/fs-plane-config.xml | 2 +- .../OH-INF/config/sc-plane-config.xml | 5 ++ .../OH-INF/i18n/solarforecast.properties | 53 +------------------ .../solarforecast/ForecastSolarTest.java | 22 ++++++-- .../binding/solarforecast/SolcastTest.java | 31 +++++++++-- 15 files changed, 140 insertions(+), 142 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 4cbbce205e692..c766d84e4dd02 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -60,7 +60,6 @@ After measurement is sent the `raw-tuning` channel is reporting the result. | Name | Type | Description | Default | Required | Advanced | |------------------------|---------|---------------------------------------|-------------|----------|----------| | apiKey | text | API Key | N/A | yes | no | -| channelRefreshInterval | integer | Channel Refresh Interval in minutes | 1 | 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) @@ -117,7 +116,6 @@ You can try it without any registration or other preconditions. | Name | Type | Description | Default | Required | |------------------------|---------|---------------------------------------|--------------|----------| | location | text | Location of Photovoltaic system | empty | yes | -| channelRefreshInterval | integer | Channel Refresh Interval in minutes | 1 | yes | | apiKey | text | API Key | N/A | no | `location` defines latitude, longitude values of your PV system. 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 index 83f3f387ec317..682ca7feb751b 100644 --- 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 @@ -44,7 +44,7 @@ public interface SolarForecast { * @param args possible arguments from this interface * @return QuantityType in kW/h */ - public QuantityType getDay(LocalDate date, String... args); + public QuantityType getDay(LocalDate date, String... args) throws IllegalArgumentException; /** * Returns electric energy between two timestamps @@ -54,7 +54,7 @@ public interface SolarForecast { * @param args possible arguments from this interface * @return QuantityType in kW/h */ - public QuantityType getEnergy(Instant start, Instant end, String... args); + public QuantityType getEnergy(Instant start, Instant end, String... args) throws IllegalArgumentException; /** * Returns electric power at one specific point of time @@ -63,7 +63,7 @@ public interface SolarForecast { * @param args possible arguments from this interface * @return QuantityType in kW */ - public QuantityType getPower(Instant timestamp, String... args); + public QuantityType getPower(Instant timestamp, String... args) throws IllegalArgumentException; /** * Get the first date and time of forecast data 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 index 7725452eddc29..60b9e607f90ee 100644 --- 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 @@ -24,6 +24,7 @@ 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; @@ -32,7 +33,6 @@ import org.openhab.core.thing.binding.ThingActionsScope; import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.types.State; -import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,7 +48,7 @@ public class SolarForecastActions implements ThingActions { private Optional thingHandler = Optional.empty(); @RuleAction(label = "@text/actionDayLabel", description = "@text/actionDayDesc") - public State getDay( + public QuantityType getDay( @ActionInput(name = "localDate", label = "@text/actionInputDayLabel", description = "@text/actionInputDayDesc") LocalDate localDate, String... args) { if (thingHandler.isPresent()) { @@ -63,22 +63,22 @@ public State getDay( } else { // break in case of failure getting values to avoid ambiguous values logger.trace("Ambiguous measure {} found for {} - return UNDEF", s, localDate); - return UnDefType.UNDEF; + return Utils.getEnergyState(-1); } } return measure; } else { logger.trace("No forecasts found for {} - return UNDEF", localDate); - return UnDefType.UNDEF; + return Utils.getEnergyState(-1); } } else { logger.trace("Handler missing - return UNDEF"); - return UnDefType.UNDEF; + return Utils.getEnergyState(-1); } } @RuleAction(label = "@text/actionPowerLabel", description = "@text/actionPowerDesc") - public State getPower( + public QuantityType getPower( @ActionInput(name = "timestamp", label = "@text/actionInputDateTimeLabel", description = "@text/actionInputDateTimeDesc") Instant timestamp, String... args) { if (thingHandler.isPresent()) { @@ -93,22 +93,22 @@ public State getPower( } else { // break in case of failure getting values to avoid ambiguous values logger.trace("Ambiguous measure {} found for {} - return UNDEF", s, timestamp); - return UnDefType.UNDEF; + return Utils.getPowerState(-1); } } return measure; } else { logger.trace("No forecasts found for {} - return UNDEF", timestamp); - return UnDefType.UNDEF; + return Utils.getPowerState(-1); } } else { logger.trace("Handler missing - return UNDEF"); - return UnDefType.UNDEF; + return Utils.getPowerState(-1); } } @RuleAction(label = "@text/actionEnergyLabel", description = "@text/actionEnergyDesc") - public State getEnergy( + 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) { @@ -124,17 +124,17 @@ public State getEnergy( } else { // break in case of failure getting values to avoid ambiguous values logger.trace("Ambiguous measure {} found between {} and {} - return UNDEF", s, start, end); - return UnDefType.UNDEF; + return Utils.getEnergyState(-1); } } return measure; } else { logger.trace("No forecasts found for between {} and {} - return UNDEF", start, end); - return UnDefType.UNDEF; + return Utils.getEnergyState(-1); } } else { logger.trace("Handler missing - return UNDEF"); - return UnDefType.UNDEF; + return Utils.getEnergyState(-1); } } 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 index e885f3b9fe0f7..aaf0ecf867f93 100644 --- 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 @@ -45,8 +45,6 @@ */ @NonNullByDefault public class ForecastSolarObject implements SolarForecast { - private static final double UNDEF = -1; - private final Logger logger = LoggerFactory.getLogger(ForecastSolarObject.class); private final TreeMap wattHourMap = new TreeMap(); private final TreeMap wattMap = new TreeMap(); @@ -106,7 +104,7 @@ public boolean isValid() { public double getActualEnergyValue(ZonedDateTime queryDateTime) { if (wattHourMap.isEmpty()) { - return UNDEF; + return -1; } Entry f = wattHourMap.floorEntry(queryDateTime); Entry c = wattHourMap.ceilingEntry(queryDateTime); @@ -117,7 +115,7 @@ public double getActualEnergyValue(ZonedDateTime queryDateTime) { return f.getValue() / 1000.0; } else { // floor date doesn't fit - return UNDEF; + return -1; } } else if (f == null && c != null) { if (c.getKey().toLocalDate().equals(queryDateTime.toLocalDate())) { @@ -125,7 +123,7 @@ public double getActualEnergyValue(ZonedDateTime queryDateTime) { return 0; } else { // ceiling date doesn't fit - return UNDEF; + return -1; } } else if (f != null && c != null) { // ceiling and floor available @@ -146,7 +144,7 @@ public double getActualEnergyValue(ZonedDateTime queryDateTime) { return 0; } } // else both null - date time doesn't fit to forecast data - return UNDEF; + return -1; } public TimeSeries getEnergyTimeSeries() { @@ -159,7 +157,7 @@ public TimeSeries getEnergyTimeSeries() { public double getActualPowerValue(ZonedDateTime queryDateTime) { if (wattMap.isEmpty()) { - return UNDEF; + return -1; } double actualPowerValue = 0; Entry f = wattMap.floorEntry(queryDateTime); @@ -171,7 +169,7 @@ public double getActualPowerValue(ZonedDateTime queryDateTime) { return f.getValue() / 1000.0; } else { // floor date doesn't fit - return UNDEF; + return -1; } } else if (f == null && c != null) { if (c.getKey().toLocalDate().equals(queryDateTime.toLocalDate())) { @@ -179,7 +177,7 @@ public double getActualPowerValue(ZonedDateTime queryDateTime) { return 0; } else { // ceiling date doesn't fit - return UNDEF; + return -1; } } else if (f != null && c != null) { // we're during suntime! @@ -191,7 +189,7 @@ public double getActualPowerValue(ZonedDateTime queryDateTime) { actualPowerValue = ((1 - interpolation) * powerFloor) + (interpolation * powerCeiling); return actualPowerValue / 1000.0; } // else both null - this shall not happen - return UNDEF; + return -1; } public TimeSeries getPowerTimeSeries() { @@ -204,7 +202,7 @@ public TimeSeries getPowerTimeSeries() { public double getDayTotal(LocalDate queryDate) { if (rawData.isEmpty()) { - return UNDEF; + return -1; } JSONObject contentJson = new JSONObject(rawData.get()); JSONObject resultJson = contentJson.getJSONObject("result"); @@ -213,17 +211,17 @@ public double getDayTotal(LocalDate queryDate) { if (wattsDay.has(queryDate.toString())) { return wattsDay.getDouble(queryDate.toString()) / 1000.0; } - return UNDEF; + return -1; } public double getRemainingProduction(ZonedDateTime queryDateTime) { if (wattHourMap.isEmpty()) { - return UNDEF; + return -1; } double daily = getDayTotal(queryDateTime.toLocalDate()); double actual = getActualEnergyValue(queryDateTime); if (daily < 0 || actual < 0) { - return UNDEF; + return -1; } return daily - actual; } @@ -241,24 +239,22 @@ public String toString() { * SolarForecast Interface */ @Override - public QuantityType getDay(LocalDate localDate, String... args) { + public QuantityType getDay(LocalDate localDate, String... args) throws IllegalArgumentException { if (args.length > 0) { - logger.info("ForecastSolar doesn't accept arguments"); - return Utils.getEnergyState(-1); + 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) { + public QuantityType getEnergy(Instant start, Instant end, String... args) throws IllegalArgumentException { if (args.length > 0) { - logger.info("ForecastSolar doesn't accept arguments"); - return Utils.getEnergyState(-1); + throw new IllegalArgumentException("ForecastSolar doesn't accept arguments"); } LocalDate beginDate = start.atZone(zone).toLocalDate(); LocalDate endDate = end.atZone(zone).toLocalDate(); - double measure = UNDEF; + double measure = -1; if (beginDate.equals(endDate)) { measure = getDayTotal(beginDate) - getActualEnergyValue(start.atZone(zone)) - getRemainingProduction(end.atZone(zone)); @@ -281,10 +277,9 @@ public QuantityType getEnergy(Instant start, Instant end, String... args } @Override - public QuantityType getPower(Instant timestamp, String... args) { + public QuantityType getPower(Instant timestamp, String... args) throws IllegalArgumentException { if (args.length > 0) { - logger.info("ForecastSolar doesn't accept arguments"); - return Utils.getPowerState(-1); + throw new IllegalArgumentException("ForecastSolar doesn't accept arguments"); } double measure = getActualPowerValue(timestamp.atZone(zone)); return Utils.getPowerState(measure); 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 index a0004f750d87c..32e984c86e9a3 100644 --- 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 @@ -17,7 +17,6 @@ import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Optional; @@ -60,7 +59,7 @@ public ForecastSolarBridgeHandler(Bridge bridge, PointType location) { @Override public Collection> getServices() { - return Collections.singleton(SolarForecastActions.class); + return List.of(SolarForecastActions.class); } @Override 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 index 52fa500e5361e..97e77e3f0d9e8 100644 --- 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 @@ -88,7 +88,7 @@ public void initialize() { bridgeHandler = Optional.of(fsbh); updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "@text/solarforecast.plane.status.await-feedback"); - bridgeHandler.get().addPlane(this); + fsbh.addPlane(this); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/solarforecast.plane.status.wrong-handler" + " [\"" + handler + "\"]"); @@ -148,17 +148,23 @@ protected ForecastSolarObject fetchData() { if (cr.getStatus() == 200) { ForecastSolarObject localForecast = new ForecastSolarObject(cr.getContentAsString(), Instant.now().plus(configuration.get().refreshInterval, ChronoUnit.MINUTES)); - setForecast(localForecast); updateState(CHANNEL_RAW, StringType.valueOf(cr.getContentAsString())); - updateStatus(ThingStatus.ONLINE); + if (localForecast.isValid()) { + setForecast(localForecast); + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/solarforecast.plane.status.json-status"); + } } else { - logger.info("{} Call {} failed {}", thing.getLabel(), url, cr.getStatus()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/solarforecast.plane.status.http-status [\"" + cr.getStatus() + "\"]"); } - } catch (InterruptedException | ExecutionException | TimeoutException e) { - logger.info("{} Call {} failed {}", thing.getLabel(), url, e.getMessage()); + } 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 use available forecast updateChannels(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 index 4f5c50fea9fdf..3b4a54f05297a 100644 --- 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 @@ -27,10 +27,10 @@ */ @NonNullByDefault public class SolcastConstants { - public static final String FORECAST_URL = "https://api.solcast.com.au/rooftop_sites/%s/forecasts?format=json&hours=168"; - public static final String CURRENT_ESTIMATE_URL = "https://api.solcast.com.au/rooftop_sites/%s/estimated_actuals?format=json"; - public static final String MEASUREMENT_URL = "https://api.solcast.com.au/rooftop_sites/%s/measurements?format=json"; + 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 MEASUREMENT_URL = BASE_URL + "%s/measurements?format=json"; public static final String BEARER = "Bearer "; public static final Unit KILOWATT_UNIT = MetricPrefix.KILO(Units.WATT); - public static final double UNDEF_DOUBLE = -1.0; } 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 index e24b9df87895c..a9a4acf4fdf38 100644 --- 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 @@ -45,7 +45,6 @@ */ @NonNullByDefault public class SolcastObject implements SolarForecast { - private static final double UNDEF = -1; private static final TreeMap EMPTY_MAP = new TreeMap(); private final Logger logger = LoggerFactory.getLogger(SolcastObject.class); @@ -155,7 +154,7 @@ public double getActualEnergyValue(ZonedDateTime query, QueryMode mode) { TreeMap dtm = getDataMap(mode); Entry nextEntry = dtm.higherEntry(iterationDateTime); if (nextEntry == null) { - return UNDEF; + return -1; } double forecastValue = 0; double previousEstimate = 0; @@ -210,7 +209,7 @@ public TimeSeries getEnergyTimeSeries(QueryMode mode) { */ public double getActualPowerValue(ZonedDateTime query, QueryMode mode) { if (query.toInstant().isBefore(getForecastBegin()) || query.toInstant().isAfter(getForecastEnd())) { - return UNDEF; + return -1; } TreeMap dtm = getDataMap(mode); double actualPowerValue = 0; @@ -257,7 +256,7 @@ public double getDayTotal(LocalDate query, QueryMode mode) { ZonedDateTime iterationDateTime = query.atStartOfDay(timeZoneProvider.getTimeZone()); Entry nextEntry = dtm.higherEntry(iterationDateTime); if (nextEntry == null) { - return UNDEF; + return -1; } ZonedDateTime endDateTime = query.atTime(23, 59, 59).atZone(timeZoneProvider.getTimeZone()); double forecastValue = 0; @@ -328,14 +327,18 @@ private TreeMap getDataMap(QueryMode mode) { * SolarForecast Interface */ @Override - public QuantityType getDay(LocalDate date, String... args) { + public QuantityType getDay(LocalDate date, String... args) throws IllegalArgumentException { QueryMode mode = evalArguments(args); if (mode.equals(QueryMode.Error)) { - return Utils.getEnergyState(-1); + 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())) { - logger.info("{} forecasts only available for future", mode); - return Utils.getEnergyState(-1); + throw new IllegalArgumentException( + "Solcast argument " + mode.toString() + " only available for future values"); } } double measure = getDayTotal(date, mode); @@ -343,23 +346,26 @@ public QuantityType getDay(LocalDate date, String... args) { } @Override - public QuantityType getEnergy(Instant start, Instant end, String... args) { + public QuantityType getEnergy(Instant start, Instant end, String... args) throws IllegalArgumentException { if (end.isBefore(start)) { - logger.info("End {} defined before Start {}", end, start); - return Utils.getEnergyState(-1); + 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())) { - logger.info("{} forecasts only available for future", mode); - return Utils.getEnergyState(-1); + 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 = UNDEF; + double measure = -1; if (beginDate.isEqual(endDate)) { measure = getDayTotal(beginDate, mode) - getActualEnergyValue(start.atZone(timeZoneProvider.getTimeZone()), mode) @@ -383,15 +389,19 @@ public QuantityType getEnergy(Instant start, Instant end, String... args } @Override - public QuantityType getPower(Instant timestamp, String... args) { + public QuantityType getPower(Instant timestamp, String... args) throws IllegalArgumentException { // eliminate error cases and return immediately QueryMode mode = evalArguments(args); if (mode.equals(QueryMode.Error)) { - return Utils.getPowerState(-1); + 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))) { - logger.info("{} forecasts only available for future", mode); - return Utils.getPowerState(-1); + throw new IllegalArgumentException( + "Solcast argument " + mode.toString() + " only available for future values"); } } double measure = getActualPowerValue(ZonedDateTime.ofInstant(timestamp, timeZoneProvider.getTimeZone()), mode); 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 index 2314bf6fd6cf1..aa0fe532e6aa3 100644 --- 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 @@ -77,18 +77,15 @@ public void initialize() { if (!configuration.get().timeZone.isEmpty()) { try { timeZone = ZoneId.of(configuration.get().timeZone); - updateStatus(ThingStatus.ONLINE); - refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, REFRESH_ACTUAL_INTERVAL, - TimeUnit.MINUTES)); } catch (DateTimeException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/solarforecast.site.status.timezone" + " [\"" + configuration.get().timeZone + "\"]"); + return; } - } else { - updateStatus(ThingStatus.ONLINE); - refreshJob = Optional.of( - scheduler.scheduleWithFixedDelay(this::getData, 0, REFRESH_ACTUAL_INTERVAL, TimeUnit.MINUTES)); } + updateStatus(ThingStatus.ONLINE); + 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"); 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 index b66cae33cfc66..86a86899e68a1 100644 --- 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 @@ -150,9 +150,11 @@ protected SolcastObject fetchData() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/solarforecast.plane.status.http-status [\"" + crEstimate.getStatus() + "\"]"); } - } catch (InterruptedException | ExecutionException | TimeoutException e) { - logger.debug("{} Call {} failed {}", thing.getLabel(), currentEstimateUrl, e.getMessage()); + } 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 use available forecast updateChannels(forecast.get()); 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 index 96b145d3c440e..3e413fcb328f5 100644 --- 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 @@ -7,7 +7,7 @@ - Data refresh rate of forecast data + Data refresh rate of forecast data in minutes 30 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 index a762827525d4b..d17426c82e4be 100644 --- 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 @@ -9,5 +9,10 @@ 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/i18n/solarforecast.properties b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast.properties index 47b5b5539d432..21f1ea47af660 100644 --- 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 @@ -29,7 +29,7 @@ thing-type.config.solarforecast.fs-plane.horizon.description = Horizon definitio 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 +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 @@ -71,55 +71,6 @@ thing-type.config.solarforecast.fs-site.channelRefreshInterval.description = Ref thing-type.config.solarforecast.sc-site.channelRefreshInterval.label = Channel Refresh Interval thing-type.config.solarforecast.sc-site.channelRefreshInterval.description = Refresh rate of channel data -# channel types - -channel-type.solarforecast.actual-channel.label = Actual Energy Forecast -channel-type.solarforecast.actual-channel.description = Todays production forecast till now -channel-type.solarforecast.actual-power-channel.label = Actual Power Forecast -channel-type.solarforecast.actual-power-channel.description = Predicted power in this moment -channel-type.solarforecast.day1-channel.label = Forecast Tomorrow -channel-type.solarforecast.day1-channel.description = Tomorrows forecast in total -channel-type.solarforecast.day1-high-channel.label = Forecast Tomorrow High -channel-type.solarforecast.day1-high-channel.description = Tomorrows optimistic forecast -channel-type.solarforecast.day1-low-channel.label = Forecast Tomorrow Low -channel-type.solarforecast.day1-low-channel.description = Tomorrows pessimistic forecast -channel-type.solarforecast.day2-channel.label = Day 2 Estimation -channel-type.solarforecast.day2-channel.description = Day after tomorrow estimation -channel-type.solarforecast.day2-high-channel.label = Day 2 High Estimation -channel-type.solarforecast.day2-high-channel.description = Day after tomorrow optimistic estimation -channel-type.solarforecast.day2-low-channel.label = Day 2 Low Estimation -channel-type.solarforecast.day2-low-channel.description = Day after tomorrow pessimistic estimation -channel-type.solarforecast.day3-channel.label = Day 3 Estimation -channel-type.solarforecast.day3-channel.description = Day 3 estimation -channel-type.solarforecast.day3-high-channel.label = Day 3 High Estimation -channel-type.solarforecast.day3-high-channel.description = Day 3 optimistic estimation -channel-type.solarforecast.day3-low-channel.label = Day 3 Low Estimation -channel-type.solarforecast.day3-low-channel.description = Day 3 pessimistic estimation -channel-type.solarforecast.day4-channel.label = Day 4 Estimation -channel-type.solarforecast.day4-channel.description = Day 4 estimation -channel-type.solarforecast.day4-high-channel.label = Day 4 High Estimation -channel-type.solarforecast.day4-high-channel.description = Day 4 optimistic estimation -channel-type.solarforecast.day4-low-channel.label = Day 4 Low Estimation -channel-type.solarforecast.day4-low-channel.description = Day 4 pessimistic estimation -channel-type.solarforecast.day5-channel.label = Day 5 Estimation -channel-type.solarforecast.day5-channel.description = Day 5 estimation -channel-type.solarforecast.day5-high-channel.label = Day 5 High Estimation -channel-type.solarforecast.day5-high-channel.description = Day 5 optimistic estimation -channel-type.solarforecast.day5-low-channel.label = Day 5 Low Estimation -channel-type.solarforecast.day5-low-channel.description = Day 5 pessimistic estimation -channel-type.solarforecast.day6-channel.label = Day 6 Estimation -channel-type.solarforecast.day6-channel.description = Day 6 estimation -channel-type.solarforecast.day6-high-channel.label = Day 6 High Estimation -channel-type.solarforecast.day6-high-channel.description = Day 6 optimistic estimation -channel-type.solarforecast.day6-low-channel.label = Day 6 Low Estimation -channel-type.solarforecast.day6-low-channel.description = Day 6 pessimistic estimation -channel-type.solarforecast.raw-tuning-channel.label = Raw JSON Tuning Response -channel-type.solarforecast.raw-tuning-channel.description = Plain JSON response from tuning call -channel-type.solarforecast.remaining-channel.label = Remaining Production Today -channel-type.solarforecast.remaining-channel.description = Forecast of todays remaining production -channel-type.solarforecast.today-channel.label = Forecast Today -channel-type.solarforecast.today-channel.description = Todays forecast in total - # binding binding.solarforecast.name = SolarForecast Binding @@ -132,9 +83,9 @@ solarforecast.site.status.timezone = Time zone {0} not found 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.power-item = Power Item {0} not found solarforecast.plane.status.await-feedback = Await first feedback solarforecast.plane.status.http-status = HTTP Status Code {0} +solarforecast.plane.status.json-status = JSON error, check raw channel # thing actions 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 index c8d74f790e72e..fe735f449c27f 100644 --- 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 @@ -189,10 +189,24 @@ void testActions() { assertEquals(QuantityType.valueOf(129.137, Units.KILOWATT_HOUR).toString(), fo.getEnergy(queryDateTime.toInstant(), queryDateTime.plusDays(2).toInstant()).toFullString(), "Actual out of scope"); - - assertEquals(ENERGY_UNDEF, fo.getDay(queryDateTime.toLocalDate(), "optimistic")); - assertEquals(ENERGY_UNDEF, fo.getDay(queryDateTime.toLocalDate(), "pessimistic")); - assertEquals(ENERGY_UNDEF, fo.getDay(queryDateTime.toLocalDate(), "total", "rubbish")); + 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 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 index d92614bb12936..6185677b86c07 100644 --- 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 @@ -317,11 +317,32 @@ void testOptimisticPessimistic() { // access in past shall be rejected Instant past = Instant.now().minus(5, ChronoUnit.MINUTES); - assertEquals(POWER_UNDEF, scfo.getPower(past, SolarForecast.OPTIMISTIC), "Optimistic Power"); - assertEquals(POWER_UNDEF, scfo.getPower(past, SolarForecast.PESSIMISTIC), "Pessimistic Power"); - assertEquals(POWER_UNDEF, scfo.getPower(past, "total", "rubbish"), "Rubbish arguments"); - assertEquals(POWER_UNDEF, scfo.getPower(past.plus(2, ChronoUnit.HOURS), "total", "rubbish"), - "Rubbish arguments"); + 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"); + } assertEquals(POWER_UNDEF, scfo.getPower(past), "Normal Power"); } From 463ba863ad4b18fa8258b4f0af41416262a177a1 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Wed, 10 Jan 2024 11:32:48 +0100 Subject: [PATCH 072/111] fast update for RefreshType Signed-off-by: Bernd Weymann --- .../forecastsolar/ForecastSolarObject.java | 7 ++ .../handler/ForecastSolarPlaneHandler.java | 18 ++- .../internal/solcast/SolcastObject.java | 8 +- .../solcast/handler/SolcastPlaneHandler.java | 104 +++++++++++------- .../OH-INF/i18n/solarforecast.properties | 32 +++--- .../resources/OH-INF/thing/channel-types.xml | 38 ++++--- 6 files changed, 139 insertions(+), 68 deletions(-) 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 index aaf0ecf867f93..81699a8e88328 100644 --- 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 @@ -226,6 +226,13 @@ public double getRemainingProduction(ZonedDateTime queryDateTime) { return daily - actual; } + public String getRaw() { + if (valid && rawData.isPresent()) { + return rawData.get(); + } + return "{}"; + } + public ZoneId getZone() { return zone; } 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 index 97e77e3f0d9e8..7b2f2db8e116b 100644 --- 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 @@ -114,7 +114,17 @@ public void dispose() { @Override public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { - fetchData(); + if (forecast.isValid()) { + if (CHANNEL_POWER_ESTIMATE.equals(channelUID.getIdWithoutGroup())) { + sendTimeSeries(CHANNEL_POWER_ESTIMATE, forecast.getPowerTimeSeries()); + } else if (CHANNEL_ENERGY_ESTIMATE.equals(channelUID.getIdWithoutGroup())) { + sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, forecast.getEnergyTimeSeries()); + } else if (CHANNEL_RAW.equals(channelUID.getIdWithoutGroup())) { + updateState(CHANNEL_RAW, StringType.valueOf(forecast.getRaw())); + } else { + fetchData(); + } + } } } @@ -166,8 +176,10 @@ protected ForecastSolarObject fetchData() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); Thread.currentThread().interrupt(); } - } // else use available forecast - updateChannels(forecast); + } else { + // else use available forecast + updateChannels(forecast); + } } else { logger.warn("{} Location not present", thing.getLabel()); } 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 index a9a4acf4fdf38..776c494c69802 100644 --- 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 @@ -93,7 +93,6 @@ public void join(String content) { private void add(String content) { if (!content.isEmpty()) { - valid = true; JSONObject contentJson = new JSONObject(content); JSONArray resultJsonArray; @@ -102,11 +101,13 @@ private void add(String content) { resultJsonArray = contentJson.getJSONArray("forecasts"); addJSONArray(resultJsonArray); rawData.get().put("forecasts", resultJsonArray); + valid = true; } if (contentJson.has("estimated_actuals")) { resultJsonArray = contentJson.getJSONArray("estimated_actuals"); addJSONArray(resultJsonArray); rawData.get().put("estimated_actuals", resultJsonArray); + valid = true; } } } @@ -288,7 +289,10 @@ public String toString() { } public String getRaw() { - return rawData.get().toString(); + if (valid && rawData.isPresent()) { + return rawData.get().toString(); + } + return "{}"; } private TreeMap getDataMap(QueryMode mode) { 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 index 86a86899e68a1..4e40195e7d691 100644 --- 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 @@ -112,52 +112,82 @@ public void dispose() { @Override public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { - fetchData(); + forecast.ifPresent(forecastObject -> { + if (forecastObject.isValid()) { + if (CHANNEL_POWER_ESTIMATE.equals(channelUID.getIdWithoutGroup())) { + sendTimeSeries(CHANNEL_POWER_ESTIMATE, forecastObject.getPowerTimeSeries(QueryMode.Estimation)); + } else if (CHANNEL_POWER_ESTIMATE10.equals(channelUID.getIdWithoutGroup())) { + sendTimeSeries(CHANNEL_POWER_ESTIMATE10, + forecastObject.getPowerTimeSeries(QueryMode.Pessimistic)); + } else if (CHANNEL_POWER_ESTIMATE90.equals(channelUID.getIdWithoutGroup())) { + sendTimeSeries(CHANNEL_POWER_ESTIMATE90, + forecastObject.getPowerTimeSeries(QueryMode.Optimistic)); + } else if (CHANNEL_ENERGY_ESTIMATE.equals(channelUID.getIdWithoutGroup())) { + sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, + forecastObject.getEnergyTimeSeries(QueryMode.Estimation)); + } else if (CHANNEL_ENERGY_ESTIMATE10.equals(channelUID.getIdWithoutGroup())) { + sendTimeSeries(CHANNEL_ENERGY_ESTIMATE10, + forecastObject.getEnergyTimeSeries(QueryMode.Pessimistic)); + } else if (CHANNEL_ENERGY_ESTIMATE90.equals(channelUID.getIdWithoutGroup())) { + sendTimeSeries(CHANNEL_ENERGY_ESTIMATE90, + forecastObject.getEnergyTimeSeries(QueryMode.Optimistic)); + } else if (CHANNEL_RAW.equals(channelUID.getIdWithoutGroup())) { + updateState(CHANNEL_RAW, StringType.valueOf(forecastObject.getRaw())); + } else { + fetchData(); + } + } + }); } } protected SolcastObject fetchData() { - if (!forecast.get().isValid()) { - String forecastUrl = String.format(FORECAST_URL, configuration.get().resourceId); - String currentEstimateUrl = String.format(CURRENT_ESTIMATE_URL, configuration.get().resourceId); - try { - // get actual estimate - Request estimateRequest = httpClient.newRequest(currentEstimateUrl); - estimateRequest.header(HttpHeader.AUTHORIZATION, BEARER + bridgeHandler.get().getApiKey()); - ContentResponse crEstimate = estimateRequest.send(); - if (crEstimate.getStatus() == 200) { - SolcastObject localForecast = new SolcastObject(crEstimate.getContentAsString(), - Instant.now().plus(configuration.get().refreshInterval, ChronoUnit.MINUTES), - bridgeHandler.get()); - - // get forecast - Request forecastRequest = httpClient.newRequest(forecastUrl); - forecastRequest.header(HttpHeader.AUTHORIZATION, BEARER + bridgeHandler.get().getApiKey()); - ContentResponse crForecast = forecastRequest.send(); - - if (crForecast.getStatus() == 200) { - localForecast.join(crForecast.getContentAsString()); - setForecast(localForecast); - updateState(CHANNEL_RAW, StringType.valueOf(forecast.get().getRaw())); - updateStatus(ThingStatus.ONLINE); + forecast.ifPresent(forecastObject -> { + if (!forecastObject.isValid()) { + String forecastUrl = String.format(FORECAST_URL, configuration.get().resourceId); + String currentEstimateUrl = String.format(CURRENT_ESTIMATE_URL, configuration.get().resourceId); + try { + // get actual estimate + Request estimateRequest = httpClient.newRequest(currentEstimateUrl); + estimateRequest.header(HttpHeader.AUTHORIZATION, BEARER + bridgeHandler.get().getApiKey()); + ContentResponse crEstimate = estimateRequest.send(); + if (crEstimate.getStatus() == 200) { + SolcastObject localForecast = new SolcastObject(crEstimate.getContentAsString(), + Instant.now().plus(configuration.get().refreshInterval, ChronoUnit.MINUTES), + bridgeHandler.get()); + + // get forecast + Request forecastRequest = httpClient.newRequest(forecastUrl); + forecastRequest.header(HttpHeader.AUTHORIZATION, BEARER + bridgeHandler.get().getApiKey()); + ContentResponse crForecast = forecastRequest.send(); + + if (crForecast.getStatus() == 200) { + localForecast.join(crForecast.getContentAsString()); + setForecast(localForecast); + updateState(CHANNEL_RAW, 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(), forecastUrl, crForecast.getStatus()); + logger.debug("{} Call {} failed {}", thing.getLabel(), currentEstimateUrl, + crEstimate.getStatus()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "@text/solarforecast.plane.status.http-status [\"" + crForecast.getStatus() + "\"]"); + "@text/solarforecast.plane.status.http-status [\"" + crEstimate.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(); } - } 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); } - } // else use available forecast - updateChannels(forecast.get()); + }); return forecast.get(); } 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 index 21f1ea47af660..0f22302542ccc 100644 --- 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 @@ -36,6 +36,8 @@ thing-type.config.solarforecast.fs-site.inverterKwp.label = Inverter Kilowatt Pe 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 @@ -47,20 +49,24 @@ thing-type.config.solarforecast.sc-site.timeZone.description = Time zone of fore channel-type.solarforecast.energy-actual-channel.label = Actual Energy Forecast channel-type.solarforecast.energy-actual-channel.description = Todays production forecast till now -channel-type.solarforecast.energy-estimate-channel.label = Actual Energy Forecast -channel-type.solarforecast.energy-estimate-channel.description = Todays production forecast till now -channel-type.solarforecast.energy-estimate10-channel.label = Actual Energy Forecast -channel-type.solarforecast.energy-estimate10-channel.description = Todays production forecast till now -channel-type.solarforecast.energy-estimate90-channel.label = Actual Energy Forecast -channel-type.solarforecast.energy-estimate90-channel.description = Todays production forecast till now -channel-type.solarforecast.power-actual-channel.label = Actual Power Forecast +channel-type.solarforecast.energy-estimate-channel.label = Energy Forecast +channel-type.solarforecast.energy-estimate-channel.description = Energy forecast for next hours / days +channel-type.solarforecast.energy-estimate10-channel.label = Pessimistic Energy Forecast +channel-type.solarforecast.energy-estimate10-channel.description = Pessimistic energy forecast for next hours / days +channel-type.solarforecast.energy-estimate90-channel.label = Optimistic Energy Forecast +channel-type.solarforecast.energy-estimate90-channel.description = Optimistic energy forecast for next hours / days +channel-type.solarforecast.energy-remain-channel.label = Remaining Energy Forecast +channel-type.solarforecast.energy-remain-channel.description = Todays remaining production forecast till sunset +channel-type.solarforecast.energy-today-channel.label = Todays Energy Forecast +channel-type.solarforecast.energy-today-channel.description = Todays total energy forecast +channel-type.solarforecast.power-actual-channel.label = Actual Power channel-type.solarforecast.power-actual-channel.description = Predicted power in this moment -channel-type.solarforecast.power-estimate-channel.label = Actual Power Forecast -channel-type.solarforecast.power-estimate-channel.description = Predicted power in this moment -channel-type.solarforecast.power-estimate10-channel.label = Actual Power Forecast -channel-type.solarforecast.power-estimate10-channel.description = Predicted power in this moment -channel-type.solarforecast.power-estimate90-channel.label = Actual Power Forecast -channel-type.solarforecast.power-estimate90-channel.description = Predicted power in this moment +channel-type.solarforecast.power-estimate-channel.label = Power Forecast +channel-type.solarforecast.power-estimate-channel.description = Power forecast for next hours / days +channel-type.solarforecast.power-estimate10-channel.label = Pessimistic Power Forecast +channel-type.solarforecast.power-estimate10-channel.description = Pessimistic power forecast for next hours / days +channel-type.solarforecast.power-estimate90-channel.label = Optimistic Power Forecast +channel-type.solarforecast.power-estimate90-channel.description = Optimistic power forecast for next hours / days channel-type.solarforecast.raw-channel.label = Raw JSON Response channel-type.solarforecast.raw-channel.description = Plain JSON response without conversions 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 index f70bb05b74181..18896842451c4 100644 --- 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 @@ -6,26 +6,26 @@ Number:Power - + Predicted power in this moment Number:Power - - Predicted power in this moment + + Power forecast for next hours / days Number:Power - - Predicted power in this moment + + Pessimistic power forecast for next hours / days Number:Power - - Predicted power in this moment + + Optimistic power forecast for next hours / days @@ -34,22 +34,34 @@ Todays production forecast till now + + Number:Energy + + Todays remaining production forecast till sunset + + + + Number:Energy + + Todays total energy forecast + + Number:Energy - - Todays production forecast till now + + Energy forecast for next hours / days Number:Energy - - Todays production forecast till now + + Pessimistic energy forecast for next hours / days Number:Energy - - Todays production forecast till now + + Optimistic energy forecast for next hours / days From 607a4f50270e886c9f40cdbe552a239305225ca9 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Wed, 10 Jan 2024 12:28:23 +0100 Subject: [PATCH 073/111] readme and naming adaptions Signed-off-by: Bernd Weymann --- .../README.md | 83 +++++++++---------- .../SolarForecastBindingConstants.java | 17 ++-- .../internal/SolarForecastHandlerFactory.java | 8 +- .../handler/ForecastSolarBridgeHandler.java | 12 +-- .../handler/ForecastSolarPlaneHandler.java | 2 +- .../solcast/handler/SolcastBridgeHandler.java | 12 +-- 6 files changed, 68 insertions(+), 66 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index c766d84e4dd02..a5127f5542731 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -92,19 +92,20 @@ Forecasts are delivered up to 6 days in advance including - a pessimistic scenario: 10th percentile - an optimistic scenario: 90th percentile -Day*X* channels are referring to forecasts plus *X* days: 1 = tomorrow, 2 = day after tomorrow, ... - -| Channel | Type | Unit | Description | Advanced | -|-------------------------|---------------|------|------------------------------------------|----------| -| actual | Number:Energy | kWh | Today's forecast till now | no | -| actual-power | Number:Power | W | Predicted power in this moment | no | -| remaining | Number:Energy | kWh | Forecast of today's remaining production | no | -| today | Number:Energy | kWh | Today's forecast in total | no | -| day*X* | Number:Energy | kWh | Day *X* forecast in total | no | -| day*X*-low | Number:Energy | kWh | Day *X* pessimistic forecast | no | -| day*X*-high | Number:Energy | kWh | Day *X* optimistic forecast | no | -| raw | String | - | Plain JSON response without conversions | yes | -| raw-tuning | String | - | JSON response from tuning call | yes | + +| Channel | Type | Unit | Description | Advanced | +|-------------------------|---------------|------|-------------------------------------------------|----------| +| power-actual | Number:Power | W | Power prediction for this moment | no | +| power-estimate | Number:Power | W | Power forecast for next hours/days | no | +| power-estimate10 | Number:Power | W | Pessimistic power forecast for next hours/days | no | +| power-estimate90 | Number:Power | W | Optimistic power forecast for next hours/days | no | +| energy-estimate | Number:Energy | kWh | Energy forecast for next hours/days | no | +| energy-estimate10 | Number:Energy | kWh | Pessimistic energy forecast for next hours/days | no | +| energy-estimate90 | Number:Energy | kWh | Optimistic energy forecast for next hours/days | 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 | +| raw | String | - | Plain JSON response without conversions | yes | ## ForecastSolar Configuration @@ -164,16 +165,17 @@ The `fs-site` bridge sums up all attached `fs-plane` values and provides the tot Channels are covering today's actual data with current, remaining and total prediction. Forecasts are delivered up to 3 days for paid personal plans. -Day*X* channels are referring to forecasts plus *X* days: 1 = tomorrow, 2 = day after tomorrow, ... -| Channel | Type | Unit | Description | Advanced | -|-------------------------|---------------|------|------------------------------------------|----------| -| actual | Number:Energy | kWh | Today's forecast till now | no | -| actual-power | Number:Power | W | Predicted power in this moment | no | -| remaining | Number:Energy | kWh | Forecast of today's remaining production | no | -| today | Number:Energy | kWh | Today's forecast in total | no | -| day*X* | Number:Energy | kWh | Day *X* forecast in total | no | -| raw | String | - | Plain JSON response without conversions | yes | +| Channel | Type | Unit | Description | Advanced | +|-------------------------|---------------|------|-------------------------------------------------|----------| +| power-actual | Number:Power | W | Power prediction for this moment | no | +| power-estimate | Number:Power | W | Power forecast for next hours/days | no | +| energy-estimate | Number:Energy | kWh | Energy forecast for next hours/days | 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 | +| raw | String | - | Plain JSON response without conversions | yes | + ## Thing Actions @@ -250,32 +252,29 @@ Exchange the configuration data in [thing file](#thing-file) and you're ready to ### Thing file ```java -Bridge solarforecast:fs-site:homeSite "ForecastSolar Home" [ location="54.321,8.976", channelRefreshInterval=1] { - Thing fs-plane homeSouthWest "ForecastSolar Home South-West" [ refreshInterval=10, azimuth=45, declination=35, kwp=5.5] - Thing fs-plane homeNorthEast "ForecastSolar Home North-East" [ refreshInterval=10, azimuth=-145, declination=35, kwp=4.425] +Bridge solarforecast:fs-site:homeSite "ForecastSolar Home" [ location="54.321,8.976"] { + Thing fs-plane homeSouthWest "ForecastSolar Home South-West" [ refreshInterval=30, azimuth=45, declination=35, kwp=5.5] + Thing fs-plane homeNorthEast "ForecastSolar Home North-East" [ refreshInterval=30, azimuth=-145, declination=35, kwp=4.425] } ``` ### Items file ```java -Number:Energy ForecastSolarHome_Actual "Actual Forecast Today [%.3f %unit%]" {channel="solarforecast:fs-site:homeSite:actual" } -Number:Power ForecastSolarHome_Actual_Power "Actual Power Forecast [%.3f %unit%]" {channel="solarforecast:fs-site:homeSite:actual-power" } -Number:Energy ForecastSolarHome_Remaining "Remaining Forecast Today [%.3f %unit%]" {channel="solarforecast:fs-site:homeSite:remaining" } -Number:Energy ForecastSolarHome_Today "Today Total Forecast [%.3f %unit%]" {channel="solarforecast:fs-site:homeSite:today" } -Number:Energy ForecastSolarHome_Day1 "Tomorrow Total Forecast [%.3f %unit%]" {channel="solarforecast:fs-site:homeSite:day1" } - -Number:Energy ForecastSolarHome_Actual_NE "Actual NE Forecast Today [%.3f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:actual" } -Number:Power ForecastSolarHome_Actual_Power_NE "Actual NE Power Forecast [%.3f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:actual-power" } -Number:Energy ForecastSolarHome_Remaining_NE "Remaining NE Forecast Today [%.3f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:remaining" } -Number:Energy ForecastSolarHome_Today_NE "Total NE Forecast Today [%.3f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:today" } -Number:Energy ForecastSolarHome_Day_NE "Tomorrow NE Forecast [%.3f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeNorthEast:day1" } - -Number:Energy ForecastSolarHome_Actual_SW "Actual SW Forecast Today [%.3f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:actual" } -Number:Power ForecastSolarHome_Actual_Power_SW "Actual SW Power Forecast [%.3f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:actual-power" } -Number:Energy ForecastSolarHome_Remaining_SW "Remaining SW Forecast Today [%.3f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:remaining" } -Number:Energy ForecastSolarHome_Today_SW "Total SW Forecast Today [%.3f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:today" } -Number:Energy ForecastSolarHome_Day_SW "Tomorrow SW Forecast [%.3f %unit%]" {channel="solarforecast:fs-plane:homeSite:homeSouthWest:day1" } +Number:Power ForecastSolarHome_Actual_Power "Power prediction for this moment" {channel="solarforecast:fs-site:homeSite:power-actual" } +Number:Energy ForecastSolarHome_Actual "Today's forecast till now" {channel="solarforecast:fs-site:homeSite:energy-actual" } +Number:Energy ForecastSolarHome_Remaining "Today's remaining forecast till sunset" {channel="solarforecast:fs-site:homeSite:energy-remain" } +Number:Energy ForecastSolarHome_Today "Today's total energy forecast" {channel="solarforecast:fs-site:homeSite:energy-today" } + +Number:Power ForecastSolarHome_Actual_Power_NE "NE Power prediction for this moment" {channel="solarforecast:fs-site:homeSite:homeNorthEast:power-actual" } +Number:Energy ForecastSolarHome_Actual_NE "NE Today's forecast till now" {channel="solarforecast:fs-site:homeSite:homeNorthEast:energy-actual" } +Number:Energy ForecastSolarHome_Remaining_NE "NE Today's remaining forecast till sunset" {channel="solarforecast:fs-site:homeSite:homeNorthEast:energy-remain" } +Number:Energy ForecastSolarHome_Today_NE "NE Today's total energy forecast" {channel="solarforecast:fs-site:homeSite:homeNorthEast:energy-today" } + +Number:Power ForecastSolarHome_Actual_Power_SW "SW Power prediction for this moment" {channel="solarforecast:fs-site:homeSite:homeSouthWest:power-actual" } +Number:Energy ForecastSolarHome_Actual_SW "SW Today's forecast till now" {channel="solarforecast:fs-site:homeSite:homeSouthWest:energy-actual" } +Number:Energy ForecastSolarHome_Remaining_SW "SW Today's remaining forecast till sunset" {channel="solarforecast:fs-site:homeSite:homeSouthWest:energy-remain" } +Number:Energy ForecastSolarHome_Today_SW "SW Today's total energy forecast" {channel="solarforecast:fs-site:homeSite:homeSouthWest:energy-today" } ``` ### Actions rule 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 index 1cfe28a9b65b7..b9f04c787c3e7 100644 --- 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 @@ -28,13 +28,15 @@ public class SolarForecastBindingConstants { private static final String BINDING_ID = "solarforecast"; - public static final ThingTypeUID FORECAST_SOLAR_MULTI_STRING = new ThingTypeUID(BINDING_ID, "fs-site"); - public static final ThingTypeUID FORECAST_SOLAR_PART_STRING = new ThingTypeUID(BINDING_ID, "fs-plane"); - public static final ThingTypeUID SOLCAST_BRIDGE_STRING = new ThingTypeUID(BINDING_ID, "sc-site"); - public static final ThingTypeUID SOLCAST_PART_STRING = new ThingTypeUID(BINDING_ID, "sc-plane"); - public static final Set SUPPORTED_THING_SET = Set.of(FORECAST_SOLAR_MULTI_STRING, - FORECAST_SOLAR_PART_STRING, SOLCAST_BRIDGE_STRING, SOLCAST_PART_STRING); + // 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_SITGE = 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_SITGE, SOLCAST_PLANE); + // Channels public static final String CHANNEL_POWER_ACTUAL = "power-actual"; public static final String CHANNEL_POWER_ESTIMATE = "power-estimate"; public static final String CHANNEL_POWER_ESTIMATE10 = "power-estimate10"; @@ -46,8 +48,9 @@ public class SolarForecastBindingConstants { public static final String CHANNEL_ENERGY_ESTIMATE10 = "energy-estimate10"; public static final String CHANNEL_ENERGY_ESTIMATE90 = "energy-estimate90"; public static final String CHANNEL_RAW = "raw"; - public static final int REFRESH_ACTUAL_INTERVAL = 1; + // Other + public static final int REFRESH_ACTUAL_INTERVAL = 1; public static final String SLASH = "/"; public static final String EMPTY = ""; } 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 index f37092eb61e65..2d89271a9ae74 100644 --- 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 @@ -69,13 +69,13 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { @Override protected @Nullable ThingHandler createHandler(Thing thing) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); - if (FORECAST_SOLAR_MULTI_STRING.equals(thingTypeUID)) { + if (FORECAST_SOLAR_SITE.equals(thingTypeUID)) { return new ForecastSolarBridgeHandler((Bridge) thing, location); - } else if (FORECAST_SOLAR_PART_STRING.equals(thingTypeUID)) { + } else if (FORECAST_SOLAR_PLANE.equals(thingTypeUID)) { return new ForecastSolarPlaneHandler(thing, httpClient); - } else if (SOLCAST_BRIDGE_STRING.equals(thingTypeUID)) { + } else if (SOLCAST_SITGE.equals(thingTypeUID)) { return new SolcastBridgeHandler((Bridge) thing, timeZoneProvider); - } else if (SOLCAST_PART_STRING.equals(thingTypeUID)) { + } 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/forecastsolar/handler/ForecastSolarBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarBridgeHandler.java index 32e984c86e9a3..6e2d5a98d978b 100644 --- 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 @@ -48,7 +48,7 @@ public class ForecastSolarBridgeHandler extends BaseBridgeHandler implements SolarForecastProvider { private final PointType homeLocation; - private List parts = new ArrayList(); + private List planes = new ArrayList(); private Optional configuration = Optional.empty(); private Optional> refreshJob = Optional.empty(); @@ -85,13 +85,13 @@ public void handleCommand(ChannelUID channelUID, Command command) { * Get data for all planes. Synchronized to protect parts map from being modified during update */ private synchronized void getData() { - if (parts.isEmpty()) { + if (planes.isEmpty()) { return; } double energySum = 0; double powerSum = 0; double daySum = 0; - for (Iterator iterator = parts.iterator(); iterator.hasNext();) { + for (Iterator iterator = planes.iterator(); iterator.hasNext();) { ForecastSolarPlaneHandler sfph = iterator.next(); ForecastSolarObject fo = sfph.fetchData(); ZonedDateTime now = ZonedDateTime.now(fo.getZone()); @@ -111,7 +111,7 @@ public void dispose() { } synchronized void addPlane(ForecastSolarPlaneHandler sfph) { - parts.add(sfph); + planes.add(sfph); // update passive PV plane with necessary data if (configuration.isPresent()) { sfph.setLocation(new PointType(configuration.get().location)); @@ -123,13 +123,13 @@ synchronized void addPlane(ForecastSolarPlaneHandler sfph) { } synchronized void removePlane(ForecastSolarPlaneHandler sfph) { - parts.remove(sfph); + planes.remove(sfph); } @Override public synchronized List getSolarForecasts() { List l = new ArrayList(); - parts.forEach(entry -> { + 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 index 7b2f2db8e116b..8d6625d9ffbdb 100644 --- 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 @@ -49,7 +49,7 @@ import org.slf4j.LoggerFactory; /** - * The {@link ForecastSolarPlaneHandler} is a non active handler instance. It will be triggerer by the bridge. + * The {@link ForecastSolarPlaneHandler} is a non active handler instance. It will be triggered by the bridge. * * @author Bernd Weymann - Initial contribution */ 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 index aa0fe532e6aa3..cd8be35ba7d29 100644 --- 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 @@ -54,7 +54,7 @@ public class SolcastBridgeHandler extends BaseBridgeHandler implements SolarForecastProvider, TimeZoneProvider { private final Logger logger = LoggerFactory.getLogger(SolcastBridgeHandler.class); - private List parts = new ArrayList(); + private List planes = new ArrayList(); private Optional configuration = Optional.empty(); private Optional> refreshJob = Optional.empty(); private ZoneId timeZone; @@ -108,7 +108,7 @@ public void dispose() { * Get data for all planes. Protect parts map from being modified during update */ private synchronized void getData() { - if (parts.isEmpty()) { + if (planes.isEmpty()) { logger.debug("No PV plane defined yet"); return; } @@ -116,7 +116,7 @@ private synchronized void getData() { double energySum = 0; double powerSum = 0; double daySum = 0; - for (Iterator iterator = parts.iterator(); iterator.hasNext();) { + for (Iterator iterator = planes.iterator(); iterator.hasNext();) { SolcastPlaneHandler sfph = iterator.next(); SolcastObject fo = sfph.fetchData(); energySum += fo.getActualEnergyValue(now, QueryMode.Estimation); @@ -130,11 +130,11 @@ private synchronized void getData() { } synchronized void addPlane(SolcastPlaneHandler sph) { - parts.add(sph); + planes.add(sph); } synchronized void removePlane(SolcastPlaneHandler sph) { - parts.remove(sph); + planes.remove(sph); } String getApiKey() { @@ -147,7 +147,7 @@ String getApiKey() { @Override public synchronized List getSolarForecasts() { List l = new ArrayList(); - parts.forEach(entry -> { + planes.forEach(entry -> { l.addAll(entry.getSolarForecasts()); }); return l; From a973f79da25a6032735cb5042014f021168c3763 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 16 Jan 2024 11:25:18 +0100 Subject: [PATCH 074/111] add up TimeSeries for Bridge Signed-off-by: Bernd Weymann --- .../SolarForecastBindingConstants.java | 4 +- .../internal/actions/SolarForecast.java | 18 +++ .../forecastsolar/ForecastSolarObject.java | 7 +- .../handler/ForecastSolarBridgeHandler.java | 45 +++++- .../handler/ForecastSolarPlaneHandler.java | 16 +- .../internal/solcast/SolcastObject.java | 2 + .../solcast/handler/SolcastBridgeHandler.java | 61 +++++++- .../solcast/handler/SolcastPlaneHandler.java | 5 +- .../solarforecast/internal/utils/Utils.java | 15 ++ .../resources/OH-INF/thing/channel-types.xml | 20 +-- .../resources/OH-INF/thing/fs-site-type.xml | 4 +- .../resources/OH-INF/thing/sc-plane-type.xml | 8 +- .../binding/solarforecast/CallbackMock.java | 137 ++++++++++++++++++ .../solarforecast/ForecastSolarPlaneMock.java | 38 +++++ .../solarforecast/ForecastSolarTest.java | 72 ++++++++- .../binding/solarforecast/SolcastTest.java | 65 +++++++++ 16 files changed, 484 insertions(+), 33 deletions(-) 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/ForecastSolarPlaneMock.java 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 index b9f04c787c3e7..4fcd8f7711643 100644 --- 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 @@ -33,8 +33,8 @@ public class SolarForecastBindingConstants { public static final ThingTypeUID FORECAST_SOLAR_PLANE = new ThingTypeUID(BINDING_ID, "fs-plane"); public static final ThingTypeUID SOLCAST_SITGE = 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_SITGE, SOLCAST_PLANE); + public static final Set SUPPORTED_THING_SET = Set.of(FORECAST_SOLAR_SITE, FORECAST_SOLAR_PLANE, + SOLCAST_SITGE, SOLCAST_PLANE); // Channels public static final String CHANNEL_POWER_ACTUAL = "power-actual"; 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 index 682ca7feb751b..46c4d2a5c9b29 100644 --- 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 @@ -19,7 +19,9 @@ 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 @@ -78,4 +80,20 @@ public interface SolarForecast { * @return date time */ public Instant getForecastEnd(); + + /** + * Get TimeSeries for Power forecast + * + * @param mode QueryMode for optimistic, pessimistic or average estimation + * @return TimeSeries containing QuantityType + */ + public TimeSeries getPowerTimeSeries(QueryMode mode); + + /** + * Get TimeSeries for Energy forecast + * + * @param mode QueryMode for optimistic, pessimistic or average estimation + * @return TimeSeries containing QuantityType + */ + public TimeSeries getEnergyTimeSeries(QueryMode mode); } 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 index 81699a8e88328..fc85fc82a722d 100644 --- 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 @@ -31,6 +31,7 @@ import org.json.JSONException; import org.json.JSONObject; 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; @@ -147,7 +148,8 @@ public double getActualEnergyValue(ZonedDateTime queryDateTime) { return -1; } - public TimeSeries getEnergyTimeSeries() { + @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)); @@ -192,7 +194,8 @@ public double getActualPowerValue(ZonedDateTime queryDateTime) { return -1; } - public TimeSeries getPowerTimeSeries() { + @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)); 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 index 6e2d5a98d978b..3ec379ec13a7d 100644 --- 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 @@ -14,12 +14,14 @@ 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; @@ -29,15 +31,19 @@ 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.binding.BaseBridgeHandler; import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.types.Command; +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. @@ -105,12 +111,47 @@ private synchronized void getData() { updateState(CHANNEL_POWER_ACTUAL, Utils.getPowerState(powerSum)); } + public 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()); + } + forecastObjects.forEach(fc -> { + TimeSeries powerTS = fc.getPowerTimeSeries(QueryMode.Estimation); + powerTS.getStates().forEach(entry -> { + Utils.addState(combinedPowerForecast, entry); + }); + TimeSeries energyTS = fc.getEnergyTimeSeries(QueryMode.Estimation); + energyTS.getStates().forEach(entry -> { + 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)); } - synchronized void addPlane(ForecastSolarPlaneHandler sfph) { + public synchronized void addPlane(ForecastSolarPlaneHandler sfph) { planes.add(sfph); // update passive PV plane with necessary data if (configuration.isPresent()) { @@ -122,7 +163,7 @@ synchronized void addPlane(ForecastSolarPlaneHandler sfph) { getData(); } - synchronized void removePlane(ForecastSolarPlaneHandler sfph) { + public synchronized void removePlane(ForecastSolarPlaneHandler sfph) { planes.remove(sfph); } 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 index 8d6625d9ffbdb..d8d4acd540ef7 100644 --- 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 @@ -32,6 +32,7 @@ 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; @@ -116,9 +117,9 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { if (forecast.isValid()) { if (CHANNEL_POWER_ESTIMATE.equals(channelUID.getIdWithoutGroup())) { - sendTimeSeries(CHANNEL_POWER_ESTIMATE, forecast.getPowerTimeSeries()); + sendTimeSeries(CHANNEL_POWER_ESTIMATE, forecast.getPowerTimeSeries(QueryMode.Estimation)); } else if (CHANNEL_ENERGY_ESTIMATE.equals(channelUID.getIdWithoutGroup())) { - sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, forecast.getEnergyTimeSeries()); + sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, forecast.getEnergyTimeSeries(QueryMode.Estimation)); } else if (CHANNEL_RAW.equals(channelUID.getIdWithoutGroup())) { updateState(CHANNEL_RAW, StringType.valueOf(forecast.getRaw())); } else { @@ -160,8 +161,8 @@ protected ForecastSolarObject fetchData() { Instant.now().plus(configuration.get().refreshInterval, ChronoUnit.MINUTES)); updateState(CHANNEL_RAW, StringType.valueOf(cr.getContentAsString())); if (localForecast.isValid()) { - setForecast(localForecast); updateStatus(ThingStatus.ONLINE); + setForecast(localForecast); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/solarforecast.plane.status.json-status"); @@ -209,10 +210,13 @@ void setApiKey(String key) { apiKey = Optional.of(key); } - private synchronized void setForecast(ForecastSolarObject f) { + protected synchronized void setForecast(ForecastSolarObject f) { forecast = f; - sendTimeSeries(CHANNEL_POWER_ESTIMATE, forecast.getPowerTimeSeries()); - sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, forecast.getEnergyTimeSeries()); + sendTimeSeries(CHANNEL_POWER_ESTIMATE, forecast.getPowerTimeSeries(QueryMode.Estimation)); + sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, forecast.getEnergyTimeSeries(QueryMode.Estimation)); + bridgeHandler.ifPresent(h -> { + h.forecastUpdate(); + }); } @Override 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 index 776c494c69802..7e886b19f2f29 100644 --- 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 @@ -196,6 +196,7 @@ public double getActualEnergyValue(ZonedDateTime query, QueryMode mode) { } } + @Override public TimeSeries getEnergyTimeSeries(QueryMode mode) { TreeMap dtm = getDataMap(mode); TimeSeries ts = new TimeSeries(Policy.REPLACE); @@ -240,6 +241,7 @@ public double getActualPowerValue(ZonedDateTime query, QueryMode mode) { } } + @Override public TimeSeries getPowerTimeSeries(QueryMode mode) { TreeMap dtm = getDataMap(mode); TimeSeries ts = new TimeSeries(Policy.REPLACE); 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 index cd8be35ba7d29..6f4fbb4597602 100644 --- 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 @@ -15,6 +15,7 @@ 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; @@ -22,6 +23,7 @@ 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; @@ -34,6 +36,7 @@ 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; @@ -42,6 +45,8 @@ 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; @@ -129,11 +134,63 @@ private synchronized void getData() { updateState(CHANNEL_POWER_ACTUAL, Utils.getPowerState(powerSum)); } - synchronized void addPlane(SolcastPlaneHandler sph) { + public void forecastUpdate() { + if (planes.isEmpty()) { + return; + } + List forecastObjects = new ArrayList(); + for (Iterator iterator = planes.iterator(); iterator.hasNext();) { + SolcastPlaneHandler sfph = iterator.next(); + forecastObjects.addAll(sfph.getSolarForecasts()); + } + List modes = List.of(QueryMode.Estimation, QueryMode.Optimistic, QueryMode.Optimistic); + modes.forEach(mode -> { + TreeMap> combinedPowerForecast = new TreeMap>(); + TreeMap> combinedEnergyForecast = new TreeMap>(); + forecastObjects.forEach(fc -> { + TimeSeries powerTS = fc.getPowerTimeSeries(mode); + powerTS.getStates().forEach(entry -> { + Utils.addState(combinedPowerForecast, entry); + }); + TimeSeries energyTS = fc.getEnergyTimeSeries(mode); + energyTS.getStates().forEach(entry -> { + Utils.addState(combinedEnergyForecast, entry); + }); + }); + + 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 Estimation: + sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, energySeries); + sendTimeSeries(CHANNEL_POWER_ESTIMATE, powerSeries); + break; + case Optimistic: + sendTimeSeries(CHANNEL_ENERGY_ESTIMATE90, energySeries); + sendTimeSeries(CHANNEL_POWER_ESTIMATE90, powerSeries); + break; + case Pessimistic: + sendTimeSeries(CHANNEL_ENERGY_ESTIMATE10, energySeries); + sendTimeSeries(CHANNEL_POWER_ESTIMATE10, powerSeries); + break; + default: + break; + } + }); + } + + public synchronized void addPlane(SolcastPlaneHandler sph) { planes.add(sph); } - synchronized void removePlane(SolcastPlaneHandler sph) { + public synchronized void removePlane(SolcastPlaneHandler sph) { planes.remove(sph); } 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 index 4e40195e7d691..1fc7f4cc86feb 100644 --- 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 @@ -201,7 +201,7 @@ private void updateChannels(SolcastObject f) { updateState(CHANNEL_POWER_ACTUAL, Utils.getPowerState(f.getActualPowerValue(now, QueryMode.Estimation))); } - private synchronized void setForecast(SolcastObject f) { + protected synchronized void setForecast(SolcastObject f) { forecast = Optional.of(f); sendTimeSeries(CHANNEL_POWER_ESTIMATE, f.getPowerTimeSeries(QueryMode.Estimation)); sendTimeSeries(CHANNEL_POWER_ESTIMATE10, f.getPowerTimeSeries(QueryMode.Optimistic)); @@ -209,6 +209,9 @@ private synchronized void setForecast(SolcastObject f) { sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, f.getEnergyTimeSeries(QueryMode.Estimation)); sendTimeSeries(CHANNEL_ENERGY_ESTIMATE10, f.getEnergyTimeSeries(QueryMode.Optimistic)); sendTimeSeries(CHANNEL_ENERGY_ESTIMATE90, f.getEnergyTimeSeries(QueryMode.Pessimistic)); + bridgeHandler.ifPresent(h -> { + h.forecastUpdate(); + }); } @Override 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 index ae016e24d6f06..52d3693f17057 100644 --- 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 @@ -12,6 +12,9 @@ */ package org.openhab.binding.solarforecast.internal.utils; +import java.time.Instant; +import java.util.TreeMap; + import javax.measure.MetricPrefix; import javax.measure.quantity.Energy; import javax.measure.quantity.Power; @@ -19,6 +22,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; 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 @@ -40,4 +44,15 @@ public static QuantityType getPowerState(double d) { } 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(); + map.put(timestamp, QuantityType.valueOf(qt1.doubleValue() + qt2.doubleValue(), qt2.getUnit())); + } else { + map.put(timestamp, (QuantityType) entry.state()); + } + } } 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 index 18896842451c4..4052d48c59ae9 100644 --- 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 @@ -7,61 +7,61 @@ Number:Power - Predicted power in this moment + Power prediction for this moment Number:Power - Power forecast for next hours / days + Power forecast for next hours/days Number:Power - Pessimistic power forecast for next hours / days + Pessimistic power forecast for next hours/days Number:Power - Optimistic power forecast for next hours / days + Optimistic power forecast for next hours/days Number:Energy - Todays production forecast till now + Today's forecast till now Number:Energy - Todays remaining production forecast till sunset + Today's remaining forecast till sunset Number:Energy - Todays total energy forecast + Today's total energy forecast Number:Energy - Energy forecast for next hours / days + Energy forecast for next hours/days Number:Energy - Pessimistic energy forecast for next hours / days + Pessimistic energy forecast for next hours/days Number:Energy - Optimistic energy forecast for next hours / days + Optimistic energy forecast for next hours/days 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 index 63093090d284d..b3cd0c2f3fc67 100644 --- 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 @@ -9,12 +9,12 @@ Site location for Forecast Solar - + + - 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 index 27b49e217c061..e60fa410ea4af 100644 --- 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 @@ -13,16 +13,16 @@ PV Plane as part of Multi Plane Bridge - - - - + + + + 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 0000000000000..10f665942f7f0 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/CallbackMock.java @@ -0,0 +1,137 @@ +/** + * 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.NonNull; +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<@NonNull String, @NonNull Object> configurationParameters) { + } + + @Override + public void validateConfigurationParameters(Channel channel, + Map<@NonNull String, @NonNull Object> 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<@NonNull ChannelBuilder> 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/ForecastSolarPlaneMock.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/ForecastSolarPlaneMock.java new file mode 100644 index 0000000000000..e5155469ce52b --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/ForecastSolarPlaneMock.java @@ -0,0 +1,38 @@ +/** + * 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.mockito.Mockito.mock; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; +import org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarObject; +import org.openhab.binding.solarforecast.internal.forecastsolar.handler.ForecastSolarPlaneHandler; +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.setForecast(fso); + } +} 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 index fe735f449c27f..b864d6fa0976f 100644 --- 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 @@ -14,20 +14,28 @@ import static org.junit.jupiter.api.Assertions.*; +import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.util.Iterator; 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.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.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; @@ -215,7 +223,7 @@ void testTimeSeries() { ZonedDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 23).atZone(TEST_ZONE); ForecastSolarObject fo = new ForecastSolarObject(content, queryDateTime.toInstant()); - TimeSeries powerSeries = fo.getPowerTimeSeries(); + TimeSeries powerSeries = fo.getPowerTimeSeries(QueryMode.Estimation); assertEquals(36, powerSeries.size()); // 18 values each day for 2 days powerSeries.getStates().forEachOrdered(entry -> { State s = entry.state(); @@ -223,7 +231,7 @@ void testTimeSeries() { assertEquals("kW", ((QuantityType) s).getUnit().toString()); }); - TimeSeries energySeries = fo.getEnergyTimeSeries(); + TimeSeries energySeries = fo.getEnergyTimeSeries(QueryMode.Estimation); assertEquals(36, energySeries.size()); energySeries.getStates().forEachOrdered(entry -> { State s = entry.state(); @@ -231,4 +239,64 @@ void testTimeSeries() { assertEquals("kWh", ((QuantityType) s).getUnit().toString()); }); } + + @Test + void testPowerTimeSeries() { + ForecastSolarBridgeHandler fsbh = new ForecastSolarBridgeHandler( + new BridgeImpl(SolarForecastBindingConstants.FORECAST_SOLAR_SITE, "bridge"), 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(content, Instant.now()); + 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 testEnergyTimeSeries() { + ForecastSolarBridgeHandler fsbh = new ForecastSolarBridgeHandler( + new BridgeImpl(SolarForecastBindingConstants.FORECAST_SOLAR_SITE, "bridge"), 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(content, Instant.now()); + 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 index 6185677b86c07..1684d5d4e3355 100644 --- 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 @@ -21,6 +21,7 @@ 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; @@ -29,13 +30,17 @@ 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.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.utils.Utils; 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; @@ -539,4 +544,64 @@ void testEnergyTimeSeries() { assertTrue(lowValue <= estValue && estValue <= highValue); } } + + @Test + void testCombinedPowerTimeSeries() { + SolcastBridgeHandler scbh = new SolcastBridgeHandler( + new BridgeImpl(SolarForecastBindingConstants.SOLCAST_SITGE, "bridge"), new TimeZP()); + CallbackMock cm = new CallbackMock(); + scbh.setCallback(cm); + + String content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + SolcastObject sco1 = new SolcastObject(content, Instant.now(), new TimeZP()); + SolcastPlaneHandler scph1 = new SolcastPlaneMock(sco1); + scbh.addPlane(scph1); + scbh.forecastUpdate(); + TimeSeries ts1 = cm.getTimeSeries("solarforecast:sc-site:bridge:power-estimate"); + + SolcastPlaneHandler scph2 = new SolcastPlaneMock(sco1); + scbh.addPlane(scph2); + scbh.forecastUpdate(); + TimeSeries ts2 = cm.getTimeSeries("solarforecast:sc-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 testCombinedEnergyTimeSeries() { + SolcastBridgeHandler scbh = new SolcastBridgeHandler( + new BridgeImpl(SolarForecastBindingConstants.SOLCAST_SITGE, "bridge"), new TimeZP()); + CallbackMock cm = new CallbackMock(); + scbh.setCallback(cm); + + String content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + SolcastObject sco1 = new SolcastObject(content, Instant.now(), new TimeZP()); + SolcastPlaneHandler scph1 = new SolcastPlaneMock(sco1); + scbh.addPlane(scph1); + scbh.forecastUpdate(); + TimeSeries ts1 = cm.getTimeSeries("solarforecast:sc-site:bridge:energy-estimate"); + + SolcastPlaneHandler scph2 = new SolcastPlaneMock(sco1); + scbh.addPlane(scph2); + scbh.forecastUpdate(); + TimeSeries ts2 = cm.getTimeSeries("solarforecast:sc-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"); + } + } } From 80ecfba106c8da6cdaff73e79707252880111407 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 16 Jan 2024 11:25:35 +0100 Subject: [PATCH 075/111] add up TimeSeries for Bridge Signed-off-by: Bernd Weymann --- .../solarforecast/SolcastPlaneMock.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java diff --git a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java new file mode 100644 index 0000000000000..f72feb47c9bc0 --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java @@ -0,0 +1,38 @@ +/** + * 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.mockito.Mockito.mock; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; +import org.openhab.binding.solarforecast.internal.solcast.SolcastObject; +import org.openhab.binding.solarforecast.internal.solcast.handler.SolcastPlaneHandler; +import org.openhab.core.thing.ThingUID; +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 { + + public SolcastPlaneMock(SolcastObject sco) { + super(new ThingImpl(SolarForecastBindingConstants.SOLCAST_PLANE, new ThingUID("test", "plane")), + mock(HttpClient.class)); + super.setForecast(sco); + } +} From 3358fc33bfc4287ad07c661b237f37f2462a60a3 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 16 Jan 2024 11:36:21 +0100 Subject: [PATCH 076/111] correct state pattern for Watt Signed-off-by: Bernd Weymann --- bundles/org.openhab.binding.solarforecast/README.md | 4 ++-- .../src/main/resources/OH-INF/thing/channel-types.xml | 8 ++++---- .../src/main/resources/OH-INF/thing/fs-plane-type.xml | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index a5127f5542731..9fa7a57478a26 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -95,13 +95,13 @@ Forecasts are delivered up to 6 days in advance including | Channel | Type | Unit | Description | Advanced | |-------------------------|---------------|------|-------------------------------------------------|----------| -| power-actual | Number:Power | W | Power prediction for this moment | no | | power-estimate | Number:Power | W | Power forecast for next hours/days | no | | power-estimate10 | Number:Power | W | Pessimistic power forecast for next hours/days | no | | power-estimate90 | Number:Power | W | Optimistic power forecast for next hours/days | no | | energy-estimate | Number:Energy | kWh | Energy forecast for next hours/days | no | | energy-estimate10 | Number:Energy | kWh | Pessimistic energy forecast for next hours/days | no | | energy-estimate90 | Number:Energy | kWh | Optimistic 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 | @@ -168,9 +168,9 @@ Forecasts are delivered up to 3 days for paid personal plans. | Channel | Type | Unit | Description | Advanced | |-------------------------|---------------|------|-------------------------------------------------|----------| -| power-actual | Number:Power | W | Power prediction for this moment | no | | 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 | 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 index 4052d48c59ae9..d723e81a18791 100644 --- 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 @@ -8,25 +8,25 @@ Number:Power Power prediction for this moment - + Number:Power Power forecast for next hours/days - + Number:Power Pessimistic power forecast for next hours/days - + Number:Power Optimistic power forecast for next hours/days - + Number:Energy 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 index 25a19c62e0c39..a4370345070cc 100644 --- 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 @@ -13,12 +13,12 @@ PV Plane as part of Multi Plane Bridge + + - - From 6851d402afb74c63ff2ce355c6714486049d1433 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 16 Jan 2024 13:50:30 +0100 Subject: [PATCH 077/111] split valid into valid and expired Signed-off-by: Bernd Weymann --- .../forecastsolar/ForecastSolarObject.java | 41 ++++++++++++------- .../handler/ForecastSolarPlaneHandler.java | 2 +- .../internal/solcast/SolcastObject.java | 22 +++++----- .../solcast/handler/SolcastPlaneHandler.java | 2 +- 4 files changed, 39 insertions(+), 28 deletions(-) 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 index fc85fc82a722d..2c07e6cc652f3 100644 --- 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 @@ -53,7 +53,6 @@ public class ForecastSolarObject implements SolarForecast { private ZoneId zone = ZoneId.systemDefault(); private Optional rawData = Optional.empty(); - private boolean valid = false; private Instant expirationDateTime; public ForecastSolarObject() { @@ -85,26 +84,38 @@ public ForecastSolarObject(String content, Instant expirationDate) { return; } } - valid = true; } catch (JSONException je) { logger.debug("Error parsing JSON response {} - {}", content, je.getMessage()); } } } + /** + * Check if ForecastObject has some valid forecast data for the future + * + * @return true: valid forecast data available till the end of the day + */ public boolean isValid() { - if (valid) { - if (!wattHourMap.isEmpty()) { - if (expirationDateTime.isAfter(Instant.now())) { - return true; - } - } + if (wattHourMap.isEmpty() || wattMap.isEmpty()) { + return false; + } + return true; + } + + /** + * Check if refresh of forecast data shall happen + * + * @return true: refresh + */ + public boolean isExpired() { + if (expirationDateTime.isAfter(Instant.now())) { + return true; } return false; } public double getActualEnergyValue(ZonedDateTime queryDateTime) { - if (wattHourMap.isEmpty()) { + if (!isValid()) { return -1; } Entry f = wattHourMap.floorEntry(queryDateTime); @@ -158,7 +169,7 @@ public TimeSeries getEnergyTimeSeries(QueryMode mode) { } public double getActualPowerValue(ZonedDateTime queryDateTime) { - if (wattMap.isEmpty()) { + if (!isValid()) { return -1; } double actualPowerValue = 0; @@ -218,7 +229,7 @@ public double getDayTotal(LocalDate queryDate) { } public double getRemainingProduction(ZonedDateTime queryDateTime) { - if (wattHourMap.isEmpty()) { + if (!isValid()) { return -1; } double daily = getDayTotal(queryDateTime.toLocalDate()); @@ -230,7 +241,7 @@ public double getRemainingProduction(ZonedDateTime queryDateTime) { } public String getRaw() { - if (valid && rawData.isPresent()) { + if (isValid() && rawData.isPresent()) { return rawData.get(); } return "{}"; @@ -242,7 +253,7 @@ public ZoneId getZone() { @Override public String toString() { - return "Expiration: " + expirationDateTime + ", Valid: " + valid + ", Data:" + wattHourMap; + return "Expiration: " + expirationDateTime + ", Valid: " + isValid() + ", Data:" + wattHourMap; } /** @@ -297,7 +308,7 @@ public QuantityType getPower(Instant timestamp, String... args) throws Il @Override public Instant getForecastBegin() { - if (!wattHourMap.isEmpty()) { + if (isValid()) { ZonedDateTime zdt = wattHourMap.firstEntry().getKey(); return zdt.toInstant(); } @@ -306,7 +317,7 @@ public Instant getForecastBegin() { @Override public Instant getForecastEnd() { - if (!wattHourMap.isEmpty()) { + if (isValid()) { ZonedDateTime zdt = wattHourMap.lastEntry().getKey(); return zdt.toInstant(); } 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 index d8d4acd540ef7..9d8aab34793fd 100644 --- 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 @@ -134,7 +134,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { */ protected ForecastSolarObject fetchData() { if (location.isPresent()) { - if (!forecast.isValid()) { + if (forecast.isExpired() || !forecast.isValid()) { String url; if (apiKey.isEmpty()) { // use public API 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 index 7e886b19f2f29..a3e89a3b73f0a 100644 --- 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 @@ -55,7 +55,6 @@ public class SolcastObject implements SolarForecast { private Optional rawData = Optional.of(new JSONObject()); private Instant expirationDateTime; - private boolean valid = false; public enum QueryMode { Estimation("Estimation"), @@ -101,13 +100,11 @@ private void add(String content) { resultJsonArray = contentJson.getJSONArray("forecasts"); addJSONArray(resultJsonArray); rawData.get().put("forecasts", resultJsonArray); - valid = true; } if (contentJson.has("estimated_actuals")) { resultJsonArray = contentJson.getJSONArray("estimated_actuals"); addJSONArray(resultJsonArray); rawData.get().put("estimated_actuals", resultJsonArray); - valid = true; } } } @@ -140,12 +137,15 @@ private void addJSONArray(JSONArray resultJsonArray) { } public boolean isValid() { - if (valid) { - if (!estimationDataMap.isEmpty()) { - if (expirationDateTime.isAfter(Instant.now())) { - return true; - } - } + if (estimationDataMap.isEmpty()) { + return false; + } + return true; + } + + public boolean isExpired() { + if (expirationDateTime.isAfter(Instant.now())) { + return true; } return false; } @@ -287,11 +287,11 @@ public double getRemainingProduction(ZonedDateTime query, QueryMode mode) { @Override public String toString() { - return "Expiration: " + expirationDateTime + ", Valid: " + valid + ", Data:" + estimationDataMap; + return "Expiration: " + expirationDateTime + ", Valid: " + isValid() + ", Data:" + estimationDataMap; } public String getRaw() { - if (valid && rawData.isPresent()) { + if (isValid() && rawData.isPresent()) { return rawData.get().toString(); } return "{}"; 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 index 1fc7f4cc86feb..6dbcbc946bab8 100644 --- 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 @@ -143,7 +143,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { protected SolcastObject fetchData() { forecast.ifPresent(forecastObject -> { - if (!forecastObject.isValid()) { + if (forecastObject.isExpired() || !forecastObject.isValid()) { String forecastUrl = String.format(FORECAST_URL, configuration.get().resourceId); String currentEstimateUrl = String.format(CURRENT_ESTIMATE_URL, configuration.get().resourceId); try { From 09220ed839df2482971b54444377f485d1f16bda Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 16 Jan 2024 16:53:34 +0100 Subject: [PATCH 078/111] use Duration instead of subtracting minutes Signed-off-by: Bernd Weymann --- .../forecastsolar/ForecastSolarObject.java | 19 ++++++++++--- .../internal/solcast/SolcastObject.java | 27 +++++++++++++++---- 2 files changed, 37 insertions(+), 9 deletions(-) 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 index 2c07e6cc652f3..ef57f67788b5a 100644 --- 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 @@ -12,6 +12,7 @@ */ package org.openhab.binding.solarforecast.internal.forecastsolar; +import java.time.Duration; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; @@ -143,8 +144,13 @@ public double getActualEnergyValue(ZonedDateTime queryDateTime) { if (c.getKey().toLocalDate().equals(queryDateTime.toLocalDate())) { // we're during suntime! double production = c.getValue() - f.getValue(); - int interpolation = queryDateTime.getMinute() - f.getKey().getMinute(); - double interpolationProduction = production * interpolation / 60; + 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 { @@ -194,11 +200,16 @@ public double getActualPowerValue(ZonedDateTime queryDateTime) { } } else if (f != null && c != null) { // we're during suntime! - double powerCeiling = c.getValue(); + 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 - double interpolation = (queryDateTime.getMinute() - f.getKey().getMinute()) / 60.0; + 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 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 index a3e89a3b73f0a..5e69a1f89d4a5 100644 --- 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 @@ -12,6 +12,7 @@ */ package org.openhab.binding.solarforecast.internal.solcast; +import java.time.Duration; import java.time.Instant; import java.time.LocalDate; import java.time.ZonedDateTime; @@ -55,6 +56,7 @@ public class SolcastObject implements SolarForecast { private Optional rawData = Optional.of(new JSONObject()); private Instant expirationDateTime; + private long period = 30; public enum QueryMode { Estimation("Estimation"), @@ -133,6 +135,9 @@ private void addJSONArray(JSONArray resultJsonArray) { } else { optimisticDataMap.put(periadEndZdt, jo.getDouble("pv_estimate")); } + if (jo.has("period")) { + period = Duration.parse(jo.getString("period")).toMinutes(); + } } } @@ -151,6 +156,7 @@ public boolean isExpired() { } 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); @@ -164,7 +170,7 @@ public double getActualEnergyValue(ZonedDateTime query, QueryMode mode) { // 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 / 2.0; + double addedValue = ((endValue.doubleValue() + previousEstimate) / 2.0) * period / 60.0; forecastValue += addedValue; previousEstimate = endValue.doubleValue(); iterationDateTime = nextEntry.getKey(); @@ -173,13 +179,19 @@ public double getActualEnergyValue(ZonedDateTime query, QueryMode mode) { 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) { - int interpolation = query.getMinute() - f.getKey().getMinute(); - double interpolationProduction = getActualPowerValue(query, mode) * interpolation / 30.0 / 2.0; + double interpolation = Duration.between(f.getKey(), query).toMinutes() / 60.0; + double interpolationProduction = getActualPowerValue(query, mode) * interpolation; forecastValue += interpolationProduction; return forecastValue; } else { @@ -220,11 +232,16 @@ public double getActualPowerValue(ZonedDateTime query, QueryMode mode) { 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 = (query.getMinute() - f.getKey().getMinute()) / 30.0; + double interpolation = Duration.between(f.getKey(), query).toMinutes() / (double) period; actualPowerValue = ((1 - interpolation) * powerFloor) + (interpolation * powerCeiling); return actualPowerValue; } else { @@ -269,7 +286,7 @@ public double getDayTotal(LocalDate query, QueryMode mode) { // 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 / 2.0; + double addedValue = ((endValue.doubleValue() + previousEstimate) / 2.0) * period / 60.0; forecastValue += addedValue; previousEstimate = endValue.doubleValue(); iterationDateTime = nextEntry.getKey(); From 7d8ab41a0a1cfd359feaca488922d2fbbd526d69 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 16 Jan 2024 17:00:27 +0100 Subject: [PATCH 079/111] avoid unit test logging Signed-off-by: Bernd Weymann --- .../openhab/binding/solarforecast/ForecastSolarPlaneMock.java | 1 + .../java/org/openhab/binding/solarforecast/SolcastPlaneMock.java | 1 + 2 files changed, 2 insertions(+) diff --git a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/ForecastSolarPlaneMock.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/ForecastSolarPlaneMock.java index e5155469ce52b..d1af7a875b30a 100644 --- a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/ForecastSolarPlaneMock.java +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/ForecastSolarPlaneMock.java @@ -33,6 +33,7 @@ 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()); super.setForecast(fso); } } diff --git a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java index f72feb47c9bc0..dc277df1acd8c 100644 --- a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java @@ -33,6 +33,7 @@ public class SolcastPlaneMock extends SolcastPlaneHandler { public SolcastPlaneMock(SolcastObject sco) { super(new ThingImpl(SolarForecastBindingConstants.SOLCAST_PLANE, new ThingUID("test", "plane")), mock(HttpClient.class)); + super.setCallback(new CallbackMock()); super.setForecast(sco); } } From fe3a5e98261783e2e8a389363002bfc9c83f49bd Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sun, 11 Feb 2024 21:44:36 +0100 Subject: [PATCH 080/111] bugfix expiration calculation Signed-off-by: Bernd Weymann --- .../internal/solcast/SolcastObject.java | 4 +- .../solcast/handler/SolcastBridgeHandler.java | 2 +- .../solcast/handler/SolcastPlaneHandler.java | 6 +- .../resources/OH-INF/thing/fs-plane-type.xml | 4 +- .../solarforecast/SolcastPlaneMock.java | 30 +++++++- .../binding/solarforecast/SolcastTest.java | 74 +++++++++++-------- 6 files changed, 81 insertions(+), 39 deletions(-) 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 index 5e69a1f89d4a5..be23fafc196d1 100644 --- 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 @@ -150,9 +150,9 @@ public boolean isValid() { public boolean isExpired() { if (expirationDateTime.isAfter(Instant.now())) { - return true; + return false; } - return false; + return true; } public double getActualEnergyValue(ZonedDateTime query, QueryMode mode) { 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 index 6f4fbb4597602..46a8697abdc73 100644 --- 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 @@ -112,7 +112,7 @@ public void dispose() { /** * Get data for all planes. Protect parts map from being modified during update */ - private synchronized void getData() { + public synchronized void getData() { if (planes.isEmpty()) { logger.debug("No PV plane defined yet"); return; 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 index 6dbcbc946bab8..e3d3b6abf5c74 100644 --- 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 @@ -61,7 +61,7 @@ public class SolcastPlaneHandler extends BaseThingHandler implements SolarForeca private final HttpClient httpClient; private Optional configuration = Optional.empty(); private Optional bridgeHandler = Optional.empty(); - private Optional forecast = Optional.empty(); + protected Optional forecast = Optional.empty(); public SolcastPlaneHandler(Thing thing, HttpClient hc) { super(thing); @@ -141,7 +141,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } - protected SolcastObject fetchData() { + protected synchronized SolcastObject fetchData() { forecast.ifPresent(forecastObject -> { if (forecastObject.isExpired() || !forecastObject.isValid()) { String forecastUrl = String.format(FORECAST_URL, configuration.get().resourceId); @@ -191,7 +191,7 @@ protected SolcastObject fetchData() { return forecast.get(); } - private void updateChannels(SolcastObject f) { + protected void updateChannels(SolcastObject f) { ZonedDateTime now = ZonedDateTime.now(bridgeHandler.get().getTimeZone()); double energyDay = f.getDayTotal(now.toLocalDate(), QueryMode.Estimation); double energyProduced = f.getActualEnergyValue(now, QueryMode.Estimation); 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 index a4370345070cc..3c0cdcf6dde49 100644 --- 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 @@ -13,8 +13,8 @@ PV Plane as part of Multi Plane Bridge - - + + diff --git a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java index dc277df1acd8c..14f8fd07c75ca 100644 --- a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java @@ -14,12 +14,17 @@ import static org.mockito.Mockito.mock; +import java.time.Instant; + +import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; import org.openhab.binding.solarforecast.internal.solcast.SolcastObject; import org.openhab.binding.solarforecast.internal.solcast.handler.SolcastPlaneHandler; +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; /** @@ -29,11 +34,32 @@ */ @NonNullByDefault public class SolcastPlaneMock extends SolcastPlaneHandler { + Bridge bridge; - public SolcastPlaneMock(SolcastObject sco) { + public SolcastPlaneMock(BridgeImpl b) { super(new ThingImpl(SolarForecastBindingConstants.SOLCAST_PLANE, new ThingUID("test", "plane")), mock(HttpClient.class)); super.setCallback(new CallbackMock()); - super.setForecast(sco); + bridge = b; + } + + @Override + public @NonNull Bridge getBridge() { + return bridge; + } + + @Override + protected SolcastObject fetchData() { + forecast.ifPresent(forecastObject -> { + if (forecastObject.isExpired() || !forecastObject.isValid()) { + String content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); + SolcastObject sco1 = new SolcastObject(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/java/org/openhab/binding/solarforecast/SolcastTest.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastTest.java index 1684d5d4e3355..dc13c8e831410 100644 --- 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 @@ -40,7 +40,9 @@ import org.openhab.binding.solarforecast.internal.utils.Utils; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.internal.BridgeImpl; +import org.openhab.core.types.RefreshType; import org.openhab.core.types.State; import org.openhab.core.types.TimeSeries; @@ -290,12 +292,10 @@ void testActions() { // check if energy calculation fits to daily query assertEquals(qt.doubleValue(), eqt.doubleValue(), TOLERANCE, "Total " + i + " days forecast"); - // System.out.println("Day: " + day + " ADay: " + qt.doubleValue() + " EDay: " + eqt.doubleValue()); totalEnergy += qt.doubleValue(); // check if sum is fitting to total energy query qt = scfo.getEnergy(start.toInstant(), start.plusDays(i + 1).toInstant()); - // System.out.println("Total: " + qt.doubleValue()); assertEquals(totalEnergy, qt.doubleValue(), TOLERANCE * 2, "Total " + i + " days forecast"); } } @@ -376,7 +376,6 @@ void testPowerInterpolation() { double startValue = sco.getActualPowerValue(now, QueryMode.Estimation); double endValue = sco.getActualPowerValue(now.plusMinutes(30), QueryMode.Estimation); for (int i = 0; i < 31; i++) { - // System.out.println(i + " : " + sco.getActualPowerValue(now.plusMinutes(i), QueryMode.Estimation)); double interpolation = i / 30.0; double expected = ((1 - interpolation) * startValue) + (interpolation * endValue); assertEquals(expected, sco.getActualPowerValue(now.plusMinutes(i), QueryMode.Estimation), TOLERANCE, @@ -396,15 +395,10 @@ void testEnergyInterpolation() { double productionExpected = 0; for (int i = 0; i < 1000; i++) { double forecast = sco.getActualEnergyValue(now.plusMinutes(i), QueryMode.Estimation); - // int hour = now.plusMinutes(i).getHour(); - // int minute = now.plusMinutes(i).getMinute(); double addOnExpected = sco.getActualPowerValue(now.plusMinutes(i), QueryMode.Estimation) / 60.0; productionExpected += addOnExpected; double diff = forecast - productionExpected; maxDiff = Math.max(diff, maxDiff); - // System.out.println(hour + ":" + minute + " : Forecast " + Math.round(forecast * 1000) / 1000.0 - // + " - Expected " + Math.round(productionExpected * 1000) / 1000.0 + " - Diff " - // + Math.round(diff * 1000) / 1000.0); assertEquals(productionExpected, sco.getActualEnergyValue(now.plusMinutes(i), QueryMode.Estimation), 100 * TOLERANCE, "Step " + i); } @@ -432,7 +426,6 @@ void testUpdates() { JSONObject joined = new JSONObject(sco.getRaw()); assertTrue(joined.has("forecasts"), "Forecasts available"); assertTrue(joined.has("estimated_actuals"), "Actual data available"); - // System.out.println(sco.getDay(LocalDate.now())); } @Test @@ -547,22 +540,20 @@ void testEnergyTimeSeries() { @Test void testCombinedPowerTimeSeries() { - SolcastBridgeHandler scbh = new SolcastBridgeHandler( - new BridgeImpl(SolarForecastBindingConstants.SOLCAST_SITGE, "bridge"), new TimeZP()); + BridgeImpl bi = new BridgeImpl(SolarForecastBindingConstants.SOLCAST_SITGE, "bridge"); + SolcastBridgeHandler scbh = new SolcastBridgeHandler(bi, new TimeZP()); + bi.setHandler(scbh); CallbackMock cm = new CallbackMock(); scbh.setCallback(cm); - String content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); - SolcastObject sco1 = new SolcastObject(content, Instant.now(), new TimeZP()); - SolcastPlaneHandler scph1 = new SolcastPlaneMock(sco1); - scbh.addPlane(scph1); - scbh.forecastUpdate(); + SolcastPlaneHandler scph1 = new SolcastPlaneMock(bi); + scph1.initialize(); TimeSeries ts1 = cm.getTimeSeries("solarforecast:sc-site:bridge:power-estimate"); - SolcastPlaneHandler scph2 = new SolcastPlaneMock(sco1); - scbh.addPlane(scph2); - scbh.forecastUpdate(); + SolcastPlaneHandler scph2 = new SolcastPlaneMock(bi); + scph2.initialize(); TimeSeries ts2 = cm.getTimeSeries("solarforecast:sc-site:bridge:power-estimate"); + Iterator iter1 = ts1.getStates().iterator(); Iterator iter2 = ts2.getStates().iterator(); while (iter1.hasNext()) { @@ -577,22 +568,20 @@ void testCombinedPowerTimeSeries() { @Test void testCombinedEnergyTimeSeries() { - SolcastBridgeHandler scbh = new SolcastBridgeHandler( - new BridgeImpl(SolarForecastBindingConstants.SOLCAST_SITGE, "bridge"), new TimeZP()); + BridgeImpl bi = new BridgeImpl(SolarForecastBindingConstants.SOLCAST_SITGE, "bridge"); + SolcastBridgeHandler scbh = new SolcastBridgeHandler(bi, new TimeZP()); + bi.setHandler(scbh); CallbackMock cm = new CallbackMock(); scbh.setCallback(cm); - String content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); - SolcastObject sco1 = new SolcastObject(content, Instant.now(), new TimeZP()); - SolcastPlaneHandler scph1 = new SolcastPlaneMock(sco1); - scbh.addPlane(scph1); - scbh.forecastUpdate(); + SolcastPlaneHandler scph1 = new SolcastPlaneMock(bi); + scph1.initialize(); TimeSeries ts1 = cm.getTimeSeries("solarforecast:sc-site:bridge:energy-estimate"); - SolcastPlaneHandler scph2 = new SolcastPlaneMock(sco1); - scbh.addPlane(scph2); - scbh.forecastUpdate(); + SolcastPlaneHandler scph2 = new SolcastPlaneMock(bi); + scph2.initialize(); TimeSeries ts2 = cm.getTimeSeries("solarforecast:sc-site:bridge:energy-estimate"); + Iterator iter1 = ts1.getStates().iterator(); Iterator iter2 = ts2.getStates().iterator(); while (iter1.hasNext()) { @@ -604,4 +593,31 @@ void testCombinedEnergyTimeSeries() { 0.1, "Power Value"); } } + + @Test + void testSingleEnergyTimeSeries() { + BridgeImpl bi = new BridgeImpl(SolarForecastBindingConstants.SOLCAST_SITGE, "bridge"); + SolcastBridgeHandler scbh = new SolcastBridgeHandler(bi, new TimeZP()); + bi.setHandler(scbh); + CallbackMock cm = new CallbackMock(); + scbh.setCallback(cm); + + SolcastPlaneHandler scph1 = new SolcastPlaneMock(bi); + scph1.initialize(); + + // simulate trigger of refresh job + scbh.getData(); + + TimeSeries ts1 = cm.getTimeSeries("solarforecast:sc-site:bridge: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"); + } + + // simulate item refresh + scbh.handleCommand(new ChannelUID("a:b:c:" + SolarForecastBindingConstants.CHANNEL_POWER_ACTUAL), + RefreshType.REFRESH); + } } From c1cec44b81a2c5ab3735eca856f3ebb762b562b7 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sun, 11 Feb 2024 22:18:26 +0100 Subject: [PATCH 081/111] bugfix pessimistic optimisitc channels Signed-off-by: Bernd Weymann --- .../forecastsolar/ForecastSolarObject.java | 14 ++------------ .../solcast/handler/SolcastPlaneHandler.java | 10 ++++++---- 2 files changed, 8 insertions(+), 16 deletions(-) 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 index ef57f67788b5a..c4e222c7934db 100644 --- 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 @@ -91,11 +91,6 @@ public ForecastSolarObject(String content, Instant expirationDate) { } } - /** - * Check if ForecastObject has some valid forecast data for the future - * - * @return true: valid forecast data available till the end of the day - */ public boolean isValid() { if (wattHourMap.isEmpty() || wattMap.isEmpty()) { return false; @@ -103,16 +98,11 @@ public boolean isValid() { return true; } - /** - * Check if refresh of forecast data shall happen - * - * @return true: refresh - */ public boolean isExpired() { if (expirationDateTime.isAfter(Instant.now())) { - return true; + return false; } - return false; + return true; } public double getActualEnergyValue(ZonedDateTime queryDateTime) { 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 index e3d3b6abf5c74..89cb70c3604bb 100644 --- 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 @@ -144,6 +144,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { protected synchronized SolcastObject fetchData() { forecast.ifPresent(forecastObject -> { if (forecastObject.isExpired() || !forecastObject.isValid()) { + logger.info("Get new forecast " + forecastObject.toString()); String forecastUrl = String.format(FORECAST_URL, configuration.get().resourceId); String currentEstimateUrl = String.format(CURRENT_ESTIMATE_URL, configuration.get().resourceId); try { @@ -202,13 +203,14 @@ protected void updateChannels(SolcastObject f) { } protected synchronized void setForecast(SolcastObject f) { + logger.info("New forecast " + f.toString()); forecast = Optional.of(f); sendTimeSeries(CHANNEL_POWER_ESTIMATE, f.getPowerTimeSeries(QueryMode.Estimation)); - sendTimeSeries(CHANNEL_POWER_ESTIMATE10, f.getPowerTimeSeries(QueryMode.Optimistic)); - sendTimeSeries(CHANNEL_POWER_ESTIMATE90, f.getPowerTimeSeries(QueryMode.Pessimistic)); + sendTimeSeries(CHANNEL_POWER_ESTIMATE10, f.getPowerTimeSeries(QueryMode.Pessimistic)); + sendTimeSeries(CHANNEL_POWER_ESTIMATE90, f.getPowerTimeSeries(QueryMode.Optimistic)); sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, f.getEnergyTimeSeries(QueryMode.Estimation)); - sendTimeSeries(CHANNEL_ENERGY_ESTIMATE10, f.getEnergyTimeSeries(QueryMode.Optimistic)); - sendTimeSeries(CHANNEL_ENERGY_ESTIMATE90, f.getEnergyTimeSeries(QueryMode.Pessimistic)); + sendTimeSeries(CHANNEL_ENERGY_ESTIMATE10, f.getEnergyTimeSeries(QueryMode.Pessimistic)); + sendTimeSeries(CHANNEL_ENERGY_ESTIMATE90, f.getEnergyTimeSeries(QueryMode.Optimistic)); bridgeHandler.ifPresent(h -> { h.forecastUpdate(); }); From 434c45ad9c5d5e8568bfe5eab978575c72036fa6 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Mon, 12 Feb 2024 12:49:30 +0100 Subject: [PATCH 082/111] bugfix optimistic pessimitic sc-site Signed-off-by: Bernd Weymann --- .../README.md | 104 ++++++------------ .../handler/ForecastSolarBridgeHandler.java | 16 +++ .../solcast/handler/SolcastBridgeHandler.java | 24 +++- .../solcast/handler/SolcastPlaneHandler.java | 4 +- 4 files changed, 72 insertions(+), 76 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 9fa7a57478a26..2cfe683f59dc8 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -253,91 +253,61 @@ Exchange the configuration data in [thing file](#thing-file) and you're ready to ```java Bridge solarforecast:fs-site:homeSite "ForecastSolar Home" [ location="54.321,8.976"] { - Thing fs-plane homeSouthWest "ForecastSolar Home South-West" [ refreshInterval=30, azimuth=45, declination=35, kwp=5.5] - Thing fs-plane homeNorthEast "ForecastSolar Home North-East" [ refreshInterval=30, azimuth=-145, declination=35, kwp=4.425] + 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 -Number:Power ForecastSolarHome_Actual_Power "Power prediction for this moment" {channel="solarforecast:fs-site:homeSite:power-actual" } -Number:Energy ForecastSolarHome_Actual "Today's forecast till now" {channel="solarforecast:fs-site:homeSite:energy-actual" } -Number:Energy ForecastSolarHome_Remaining "Today's remaining forecast till sunset" {channel="solarforecast:fs-site:homeSite:energy-remain" } -Number:Energy ForecastSolarHome_Today "Today's total energy forecast" {channel="solarforecast:fs-site:homeSite:energy-today" } - -Number:Power ForecastSolarHome_Actual_Power_NE "NE Power prediction for this moment" {channel="solarforecast:fs-site:homeSite:homeNorthEast:power-actual" } -Number:Energy ForecastSolarHome_Actual_NE "NE Today's forecast till now" {channel="solarforecast:fs-site:homeSite:homeNorthEast:energy-actual" } -Number:Energy ForecastSolarHome_Remaining_NE "NE Today's remaining forecast till sunset" {channel="solarforecast:fs-site:homeSite:homeNorthEast:energy-remain" } -Number:Energy ForecastSolarHome_Today_NE "NE Today's total energy forecast" {channel="solarforecast:fs-site:homeSite:homeNorthEast:energy-today" } - -Number:Power ForecastSolarHome_Actual_Power_SW "SW Power prediction for this moment" {channel="solarforecast:fs-site:homeSite:homeSouthWest:power-actual" } -Number:Energy ForecastSolarHome_Actual_SW "SW Today's forecast till now" {channel="solarforecast:fs-site:homeSite:homeSouthWest:energy-actual" } -Number:Energy ForecastSolarHome_Remaining_SW "SW Today's remaining forecast till sunset" {channel="solarforecast:fs-site:homeSite:homeSouthWest:energy-remain" } -Number:Energy ForecastSolarHome_Today_SW "SW Today's total energy forecast" {channel="solarforecast:fs-site:homeSite:homeSouthWest:energy-today" } +// 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" } ``` ### Actions rule ```java -import java.time.temporal.ChronoUnit - -rule "Forecast Solar Actions" +rule "Tomorrow Forecast Calculation" when - Time cron "0 0 23 * * ?" // trigger whatever you like + Item ForecastSolarHome_Today received update then - // get Actions for specific fs-site val solarforecastActions = getActions("solarforecast","solarforecast:fs-site:homeSite") - val timeZone = "Europe/Berlin" - - // get earliest and latest forecast dates - val beginDT = solarforecastActions.getForecastBegin - val endDT = solarforecastActions.getForecastEnd - logInfo("SF Tests","Begin: "+ beginDT.atZone(ZoneId.of(timeZone))+" End: "+endDT.atZone(ZoneId.of(timeZone))) - - // get forecast for tomorrow - val fcTomorrowState = solarforecastActions.getDay(LocalDate.now.plusDays(1)) - logInfo("SF Tests","Forecast tomorrow state: "+ fcTomorrowState.toString) - val fcToTomorrowDouble = (fcTomorrowState as Number).doubleValue - logInfo("SF Tests","Forecast tomorrow value: "+ fcToTomorrowDouble) - - // get power forecast in one hour - val hourPlusOnePowerState = solarforecastActions.getPower(Instant.now.plus(1,ChronoUnit.HOURS)) - logInfo("SF Tests","Hour+1 power state: "+ hourPlusOnePowerState.toString) - val hourPlusOnePowerValue = (hourPlusOnePowerState as Number).doubleValue - logInfo("SF Tests","Hour+1 power value: "+ hourPlusOnePowerValue) - - // get energy forecast at specific time: Nov 18th 2023, 16:00 - val startDT = LocalDateTime.of(2023,11,18,16,00).atZone(ZoneId.of(timeZone)) - val stopDT = startDT.plusDays(2) - val twoDaysForecastFromNowState = solarforecastActions.getEnergy(startDT.toInstant, stopDT.toInstant) - logInfo("SF Tests","Forecast 2 days state: "+ twoDaysForecastFromNowState.toString) - val twoDaysForecastFromNowValue = (twoDaysForecastFromNowState as Number).doubleValue - logInfo("SF Tests","Forecast 2 days value: "+ twoDaysForecastFromNowValue) + val energyState = solarforecastActions.getDay(LocalDate.now.plusDays(1)) + logInfo("SF Tests","{}",energyState) + ForecastSolarHome_Tomorrow.postUpdate(energyState) + //val energy = (ForecastSolar_PV_Plane_Power_Forecast.historicState(now.plusDays(1)).state as Number) end ``` -shall produce following output - -``` -2023-11-18 22:00:59.250 [INFO ] [g.openhab.core.model.script.SF Tests] - Begin: 2023-11-18T07:34:23+01:00[Europe/Berlin] End: 2023-11-19T16:26:50+01:00[Europe/Berlin] -2023-11-18 22:00:59.262 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast tomorrow state: 3.861 kWh -2023-11-18 22:00:59.267 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast tomorrow value: 3.861 -2023-11-18 22:00:59.275 [INFO ] [g.openhab.core.model.script.SF Tests] - Hour+1 power state: 0 kW -2023-11-18 22:00:59.280 [INFO ] [g.openhab.core.model.script.SF Tests] - Hour+1 power value: 0.0 -2023-11-18 22:00:59.296 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast 2 days state: 3.865 kWh -2023-11-18 22:00:59.300 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast 2 days value: 3.865 -``` - ### Actions rule with Arguments -Only Solcast is delivering `optimistic` and `pessimistic` scenario data. -If arguments are used on ForecastSolar `UNDEF` state is returned - ```java import java.time.temporal.ChronoUnit -rrule "Solcast Actions" +rule "Solcast Actions" when Time cron "0 0 23 * * ?" // trigger whatever you like then @@ -352,11 +322,3 @@ rrule "Solcast Actions" logInfo("SF Tests","Forecast Pessimist 6 days "+ sixDayPessimistic) end ``` - -shall produce following output - -``` -2022-08-10 00:02:16.569 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast Estimate 6 days 309.424 kWh -2022-08-10 00:02:16.574 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast Optimist 6 days 319.827 kWh -2022-08-10 00:02:16.578 [INFO ] [g.openhab.core.model.script.SF Tests] - Forecast Pessimist 6 days 208.235 kWh -``` 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 index 3ec379ec13a7d..65e6a282693e2 100644 --- 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 @@ -42,6 +42,7 @@ 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; @@ -85,6 +86,21 @@ public void initialize() { @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; + } + } } /** 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 index 46a8697abdc73..2e8b7529c7718 100644 --- 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 @@ -100,7 +100,23 @@ public void initialize() { @Override public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { - getData(); + 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_POWER_ESTIMATE10: + case CHANNEL_POWER_ESTIMATE90: + case CHANNEL_ENERGY_ESTIMATE: + case CHANNEL_ENERGY_ESTIMATE10: + case CHANNEL_ENERGY_ESTIMATE90: + forecastUpdate(); + break; + } } } @@ -138,12 +154,14 @@ public 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()); } - List modes = List.of(QueryMode.Estimation, QueryMode.Optimistic, QueryMode.Optimistic); + // sort in Tree according to times for each scenario + List modes = List.of(QueryMode.Estimation, QueryMode.Pessimistic, QueryMode.Optimistic); modes.forEach(mode -> { TreeMap> combinedPowerForecast = new TreeMap>(); TreeMap> combinedEnergyForecast = new TreeMap>(); @@ -157,7 +175,7 @@ public void forecastUpdate() { Utils.addState(combinedEnergyForecast, entry); }); }); - + // create TimeSeries and distribute TimeSeries powerSeries = new TimeSeries(Policy.REPLACE); combinedPowerForecast.forEach((timestamp, state) -> { powerSeries.add(timestamp, state); 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 index 89cb70c3604bb..5f33c0adc632b 100644 --- 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 @@ -144,7 +144,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { protected synchronized SolcastObject fetchData() { forecast.ifPresent(forecastObject -> { if (forecastObject.isExpired() || !forecastObject.isValid()) { - logger.info("Get new forecast " + forecastObject.toString()); + logger.info("Get new forecast {}", forecastObject.toString()); String forecastUrl = String.format(FORECAST_URL, configuration.get().resourceId); String currentEstimateUrl = String.format(CURRENT_ESTIMATE_URL, configuration.get().resourceId); try { @@ -203,7 +203,7 @@ protected void updateChannels(SolcastObject f) { } protected synchronized void setForecast(SolcastObject f) { - logger.info("New forecast " + f.toString()); + logger.info("New forecast {}", f.toString()); forecast = Optional.of(f); sendTimeSeries(CHANNEL_POWER_ESTIMATE, f.getPowerTimeSeries(QueryMode.Estimation)); sendTimeSeries(CHANNEL_POWER_ESTIMATE10, f.getPowerTimeSeries(QueryMode.Pessimistic)); From 027dd39e3b0afafb3b7b6bf45aad679fab57e5a5 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sun, 18 Feb 2024 17:47:05 +0100 Subject: [PATCH 083/111] Solcast channel rework Signed-off-by: Bernd Weymann --- .../README.md | 32 ++++- .../SolarForecastBindingConstants.java | 16 +-- .../internal/actions/SolarForecast.java | 4 + .../forecastsolar/ForecastSolarObject.java | 10 +- .../handler/ForecastSolarBridgeHandler.java | 4 +- .../handler/ForecastSolarPlaneHandler.java | 14 +-- .../internal/solcast/SolcastObject.java | 16 +-- .../solcast/handler/SolcastBridgeHandler.java | 63 ++++++---- .../solcast/handler/SolcastPlaneHandler.java | 101 +++++++++------ .../OH-INF/i18n/solarforecast.properties | 54 ++++---- .../OH-INF/i18n/solarforecast2.properties | 115 ++++++++++++++++++ .../resources/OH-INF/thing/average-group.xml | 18 +++ .../resources/OH-INF/thing/channel-types.xml | 26 +--- .../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 | 19 +-- .../resources/OH-INF/thing/sc-site-type.xml | 17 +-- .../solarforecast/ForecastSolarTest.java | 8 +- .../solarforecast/SolcastPlaneMock.java | 9 +- .../binding/solarforecast/SolcastTest.java | 96 +++++++++------ 21 files changed, 451 insertions(+), 220 deletions(-) create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast2.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/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 diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 2cfe683f59dc8..c562bf7679176 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -287,6 +287,26 @@ Number:Power ForecastSolarHome_Power_Estimate_SW "SW Power estima 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 @@ -311,14 +331,22 @@ 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 Estimate 6 days "+ sixDayForecast) + logInfo("SF Tests","Forecast Average 6 days "+ sixDayForecast) val sixDayOptimistic = solarforecastActions.getEnergy(startTimestamp,endTimestamp, "optimistic") - logInfo("SF Tests","Forecast Optimist 6 days "+ sixDayOptimistic) + 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/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 index 4fcd8f7711643..7eb3e38618e3f 100644 --- 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 @@ -36,18 +36,20 @@ public class SolarForecastBindingConstants { public static final Set SUPPORTED_THING_SET = Set.of(FORECAST_SOLAR_SITE, FORECAST_SOLAR_PLANE, SOLCAST_SITGE, 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_ACTUAL = "power-actual"; public static final String CHANNEL_POWER_ESTIMATE = "power-estimate"; - public static final String CHANNEL_POWER_ESTIMATE10 = "power-estimate10"; - public static final String CHANNEL_POWER_ESTIMATE90 = "power-estimate90"; + 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_ENERGY_ESTIMATE = "energy-estimate"; - public static final String CHANNEL_ENERGY_ESTIMATE10 = "energy-estimate10"; - public static final String CHANNEL_ENERGY_ESTIMATE90 = "energy-estimate90"; - public static final String CHANNEL_RAW = "raw"; + public static final String CHANNEL_JSON = "json"; // Other public static final int REFRESH_ACTUAL_INTERVAL = 1; 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 index 46c4d2a5c9b29..8bcaf540f21cc 100644 --- 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 @@ -30,6 +30,10 @@ */ @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 */ 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 index c4e222c7934db..651d5850cf76a 100644 --- 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 @@ -92,17 +92,11 @@ public ForecastSolarObject(String content, Instant expirationDate) { } public boolean isValid() { - if (wattHourMap.isEmpty() || wattMap.isEmpty()) { - return false; - } - return true; + return !(wattHourMap.isEmpty() || wattMap.isEmpty()); } public boolean isExpired() { - if (expirationDateTime.isAfter(Instant.now())) { - return false; - } - return true; + return expirationDateTime.isBefore(Instant.now()); } public double getActualEnergyValue(ZonedDateTime queryDateTime) { 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 index 65e6a282693e2..861293e0668c6 100644 --- 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 @@ -139,11 +139,11 @@ public void forecastUpdate() { forecastObjects.addAll(sfph.getSolarForecasts()); } forecastObjects.forEach(fc -> { - TimeSeries powerTS = fc.getPowerTimeSeries(QueryMode.Estimation); + TimeSeries powerTS = fc.getPowerTimeSeries(QueryMode.Average); powerTS.getStates().forEach(entry -> { Utils.addState(combinedPowerForecast, entry); }); - TimeSeries energyTS = fc.getEnergyTimeSeries(QueryMode.Estimation); + TimeSeries energyTS = fc.getEnergyTimeSeries(QueryMode.Average); energyTS.getStates().forEach(entry -> { Utils.addState(combinedEnergyForecast, entry); }); 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 index 9d8aab34793fd..cb1d4735d1a26 100644 --- 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 @@ -117,11 +117,11 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { if (forecast.isValid()) { if (CHANNEL_POWER_ESTIMATE.equals(channelUID.getIdWithoutGroup())) { - sendTimeSeries(CHANNEL_POWER_ESTIMATE, forecast.getPowerTimeSeries(QueryMode.Estimation)); + sendTimeSeries(CHANNEL_POWER_ESTIMATE, forecast.getPowerTimeSeries(QueryMode.Average)); } else if (CHANNEL_ENERGY_ESTIMATE.equals(channelUID.getIdWithoutGroup())) { - sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, forecast.getEnergyTimeSeries(QueryMode.Estimation)); - } else if (CHANNEL_RAW.equals(channelUID.getIdWithoutGroup())) { - updateState(CHANNEL_RAW, StringType.valueOf(forecast.getRaw())); + sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, forecast.getEnergyTimeSeries(QueryMode.Average)); + } else if (CHANNEL_JSON.equals(channelUID.getIdWithoutGroup())) { + updateState(CHANNEL_JSON, StringType.valueOf(forecast.getRaw())); } else { fetchData(); } @@ -159,7 +159,7 @@ protected ForecastSolarObject fetchData() { if (cr.getStatus() == 200) { ForecastSolarObject localForecast = new ForecastSolarObject(cr.getContentAsString(), Instant.now().plus(configuration.get().refreshInterval, ChronoUnit.MINUTES)); - updateState(CHANNEL_RAW, StringType.valueOf(cr.getContentAsString())); + updateState(CHANNEL_JSON, StringType.valueOf(cr.getContentAsString())); if (localForecast.isValid()) { updateStatus(ThingStatus.ONLINE); setForecast(localForecast); @@ -212,8 +212,8 @@ void setApiKey(String key) { protected synchronized void setForecast(ForecastSolarObject f) { forecast = f; - sendTimeSeries(CHANNEL_POWER_ESTIMATE, forecast.getPowerTimeSeries(QueryMode.Estimation)); - sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, forecast.getEnergyTimeSeries(QueryMode.Estimation)); + sendTimeSeries(CHANNEL_POWER_ESTIMATE, forecast.getPowerTimeSeries(QueryMode.Average)); + sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, forecast.getEnergyTimeSeries(QueryMode.Average)); bridgeHandler.ifPresent(h -> { h.forecastUpdate(); }); 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 index be23fafc196d1..ece13c98a352a 100644 --- 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 @@ -59,7 +59,7 @@ public class SolcastObject implements SolarForecast { private long period = 30; public enum QueryMode { - Estimation("Estimation"), + Average(SolarForecast.AVERAGE), Optimistic(SolarForecast.OPTIMISTIC), Pessimistic(SolarForecast.PESSIMISTIC), Error("Error"); @@ -142,17 +142,11 @@ private void addJSONArray(JSONArray resultJsonArray) { } public boolean isValid() { - if (estimationDataMap.isEmpty()) { - return false; - } - return true; + return !estimationDataMap.isEmpty(); } public boolean isExpired() { - if (expirationDateTime.isAfter(Instant.now())) { - return false; - } - return true; + return (expirationDateTime.isBefore(Instant.now())); } public double getActualEnergyValue(ZonedDateTime query, QueryMode mode) { @@ -317,7 +311,7 @@ public String getRaw() { private TreeMap getDataMap(QueryMode mode) { TreeMap returnMap = EMPTY_MAP; switch (mode) { - case Estimation: + case Average: returnMap = estimationDataMap; break; case Optimistic: @@ -463,7 +457,7 @@ private QueryMode evalArguments(String[] args) { return QueryMode.Error; } } else { - return QueryMode.Estimation; + return QueryMode.Average; } } } 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 index 2e8b7529c7718..dd6c72ca0ce5c 100644 --- 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 @@ -109,11 +109,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { getData(); break; case CHANNEL_POWER_ESTIMATE: - case CHANNEL_POWER_ESTIMATE10: - case CHANNEL_POWER_ESTIMATE90: case CHANNEL_ENERGY_ESTIMATE: - case CHANNEL_ENERGY_ESTIMATE10: - case CHANNEL_ENERGY_ESTIMATE90: forecastUpdate(); break; } @@ -134,20 +130,35 @@ public synchronized void getData() { return; } ZonedDateTime now = ZonedDateTime.now(getTimeZone()); - double energySum = 0; - double powerSum = 0; - double daySum = 0; - for (Iterator iterator = planes.iterator(); iterator.hasNext();) { - SolcastPlaneHandler sfph = iterator.next(); - SolcastObject fo = sfph.fetchData(); - energySum += fo.getActualEnergyValue(now, QueryMode.Estimation); - powerSum += fo.getActualPowerValue(now, QueryMode.Estimation); - daySum += fo.getDayTotal(now.toLocalDate(), QueryMode.Estimation); - } - 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)); + List modes = List.of(QueryMode.Average, QueryMode.Pessimistic, QueryMode.Optimistic); + modes.forEach(mode -> { + String group = GROUP_AVERAGE; + switch (mode) { + case Average: + group = GROUP_AVERAGE; + break; + case Optimistic: + group = GROUP_OPTIMISTIC; + break; + case Pessimistic: + group = GROUP_PESSIMISTIC; + break; + } + double energySum = 0; + double powerSum = 0; + double daySum = 0; + for (Iterator iterator = planes.iterator(); iterator.hasNext();) { + SolcastPlaneHandler sfph = iterator.next(); + SolcastObject fo = sfph.fetchData(); + energySum += fo.getActualEnergyValue(now, mode); + powerSum += fo.getActualPowerValue(now, mode); + daySum += fo.getDayTotal(now.toLocalDate(), mode); + } + updateState(group + "#" + CHANNEL_ENERGY_ACTUAL, Utils.getEnergyState(energySum)); + updateState(group + "#" + CHANNEL_ENERGY_REMAIN, Utils.getEnergyState(daySum - energySum)); + updateState(group + "#" + CHANNEL_ENERGY_TODAY, Utils.getEnergyState(daySum)); + updateState(group + "#" + CHANNEL_POWER_ACTUAL, Utils.getPowerState(powerSum)); + }); } public void forecastUpdate() { @@ -161,7 +172,7 @@ public void forecastUpdate() { forecastObjects.addAll(sfph.getSolarForecasts()); } // sort in Tree according to times for each scenario - List modes = List.of(QueryMode.Estimation, QueryMode.Pessimistic, QueryMode.Optimistic); + List modes = List.of(QueryMode.Average, QueryMode.Pessimistic, QueryMode.Optimistic); modes.forEach(mode -> { TreeMap> combinedPowerForecast = new TreeMap>(); TreeMap> combinedEnergyForecast = new TreeMap>(); @@ -186,17 +197,17 @@ public void forecastUpdate() { energySeries.add(timestamp, state); }); switch (mode) { - case Estimation: - sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, energySeries); - sendTimeSeries(CHANNEL_POWER_ESTIMATE, powerSeries); + case Average: + sendTimeSeries(GROUP_AVERAGE + "#" + CHANNEL_ENERGY_ESTIMATE, energySeries); + sendTimeSeries(GROUP_AVERAGE + "#" + CHANNEL_POWER_ESTIMATE, powerSeries); break; case Optimistic: - sendTimeSeries(CHANNEL_ENERGY_ESTIMATE90, energySeries); - sendTimeSeries(CHANNEL_POWER_ESTIMATE90, powerSeries); + sendTimeSeries(GROUP_OPTIMISTIC + "#" + CHANNEL_ENERGY_ESTIMATE, energySeries); + sendTimeSeries(GROUP_OPTIMISTIC + "#" + CHANNEL_POWER_ESTIMATE, powerSeries); break; case Pessimistic: - sendTimeSeries(CHANNEL_ENERGY_ESTIMATE10, energySeries); - sendTimeSeries(CHANNEL_POWER_ESTIMATE10, powerSeries); + sendTimeSeries(GROUP_PESSIMISTIC + "#" + CHANNEL_ENERGY_ESTIMATE, energySeries); + sendTimeSeries(GROUP_PESSIMISTIC + "#" + CHANNEL_POWER_ESTIMATE, powerSeries); break; default: break; 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 index 5f33c0adc632b..12391de7fc092 100644 --- 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 @@ -114,27 +114,33 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { forecast.ifPresent(forecastObject -> { if (forecastObject.isValid()) { - if (CHANNEL_POWER_ESTIMATE.equals(channelUID.getIdWithoutGroup())) { - sendTimeSeries(CHANNEL_POWER_ESTIMATE, forecastObject.getPowerTimeSeries(QueryMode.Estimation)); - } else if (CHANNEL_POWER_ESTIMATE10.equals(channelUID.getIdWithoutGroup())) { - sendTimeSeries(CHANNEL_POWER_ESTIMATE10, - forecastObject.getPowerTimeSeries(QueryMode.Pessimistic)); - } else if (CHANNEL_POWER_ESTIMATE90.equals(channelUID.getIdWithoutGroup())) { - sendTimeSeries(CHANNEL_POWER_ESTIMATE90, - forecastObject.getPowerTimeSeries(QueryMode.Optimistic)); - } else if (CHANNEL_ENERGY_ESTIMATE.equals(channelUID.getIdWithoutGroup())) { - sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, - forecastObject.getEnergyTimeSeries(QueryMode.Estimation)); - } else if (CHANNEL_ENERGY_ESTIMATE10.equals(channelUID.getIdWithoutGroup())) { - sendTimeSeries(CHANNEL_ENERGY_ESTIMATE10, - forecastObject.getEnergyTimeSeries(QueryMode.Pessimistic)); - } else if (CHANNEL_ENERGY_ESTIMATE90.equals(channelUID.getIdWithoutGroup())) { - sendTimeSeries(CHANNEL_ENERGY_ESTIMATE90, - forecastObject.getEnergyTimeSeries(QueryMode.Optimistic)); - } else if (CHANNEL_RAW.equals(channelUID.getIdWithoutGroup())) { - updateState(CHANNEL_RAW, StringType.valueOf(forecastObject.getRaw())); - } else { - fetchData(); + String group = channelUID.getGroupId(); + 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 + "#" + 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); } } }); @@ -165,7 +171,7 @@ protected synchronized SolcastObject fetchData() { if (crForecast.getStatus() == 200) { localForecast.join(crForecast.getContentAsString()); setForecast(localForecast); - updateState(CHANNEL_RAW, StringType.valueOf(forecast.get().getRaw())); + updateState(GROUP_RAW + "#" + CHANNEL_JSON, StringType.valueOf(forecast.get().getRaw())); updateStatus(ThingStatus.ONLINE); } else { logger.debug("{} Call {} failed {}", thing.getLabel(), forecastUrl, crForecast.getStatus()); @@ -194,23 +200,50 @@ protected synchronized SolcastObject fetchData() { protected void updateChannels(SolcastObject f) { ZonedDateTime now = ZonedDateTime.now(bridgeHandler.get().getTimeZone()); - double energyDay = f.getDayTotal(now.toLocalDate(), QueryMode.Estimation); - double energyProduced = f.getActualEnergyValue(now, QueryMode.Estimation); - 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, QueryMode.Estimation))); + 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); + switch (mode) { + case Average: + updateState(GROUP_AVERAGE + "#" + CHANNEL_ENERGY_ACTUAL, Utils.getEnergyState(energyProduced)); + updateState(GROUP_AVERAGE + "#" + CHANNEL_ENERGY_REMAIN, + Utils.getEnergyState(energyDay - energyProduced)); + updateState(GROUP_AVERAGE + "#" + CHANNEL_ENERGY_TODAY, Utils.getEnergyState(energyDay)); + updateState(GROUP_AVERAGE + "#" + CHANNEL_POWER_ACTUAL, + Utils.getPowerState(f.getActualPowerValue(now, QueryMode.Average))); + break; + case Optimistic: + updateState(GROUP_OPTIMISTIC + "#" + CHANNEL_ENERGY_ACTUAL, Utils.getEnergyState(energyProduced)); + updateState(GROUP_OPTIMISTIC + "#" + CHANNEL_ENERGY_REMAIN, + Utils.getEnergyState(energyDay - energyProduced)); + updateState(GROUP_OPTIMISTIC + "#" + CHANNEL_ENERGY_TODAY, Utils.getEnergyState(energyDay)); + updateState(GROUP_OPTIMISTIC + "#" + CHANNEL_POWER_ACTUAL, + Utils.getPowerState(f.getActualPowerValue(now, QueryMode.Average))); + break; + case Pessimistic: + updateState(GROUP_PESSIMISTIC + "#" + CHANNEL_ENERGY_ACTUAL, Utils.getEnergyState(energyProduced)); + updateState(GROUP_PESSIMISTIC + "#" + CHANNEL_ENERGY_REMAIN, + Utils.getEnergyState(energyDay - energyProduced)); + updateState(GROUP_PESSIMISTIC + "#" + CHANNEL_ENERGY_TODAY, Utils.getEnergyState(energyDay)); + updateState(GROUP_PESSIMISTIC + "#" + CHANNEL_POWER_ACTUAL, + Utils.getPowerState(f.getActualPowerValue(now, QueryMode.Average))); + break; + default: + break; + } + }); } protected synchronized void setForecast(SolcastObject f) { logger.info("New forecast {}", f.toString()); forecast = Optional.of(f); - sendTimeSeries(CHANNEL_POWER_ESTIMATE, f.getPowerTimeSeries(QueryMode.Estimation)); - sendTimeSeries(CHANNEL_POWER_ESTIMATE10, f.getPowerTimeSeries(QueryMode.Pessimistic)); - sendTimeSeries(CHANNEL_POWER_ESTIMATE90, f.getPowerTimeSeries(QueryMode.Optimistic)); - sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, f.getEnergyTimeSeries(QueryMode.Estimation)); - sendTimeSeries(CHANNEL_ENERGY_ESTIMATE10, f.getEnergyTimeSeries(QueryMode.Pessimistic)); - sendTimeSeries(CHANNEL_ENERGY_ESTIMATE90, f.getEnergyTimeSeries(QueryMode.Optimistic)); + sendTimeSeries(GROUP_AVERAGE + "#" + CHANNEL_POWER_ESTIMATE, f.getPowerTimeSeries(QueryMode.Average)); + sendTimeSeries(GROUP_AVERAGE + "#" + CHANNEL_ENERGY_ESTIMATE, f.getEnergyTimeSeries(QueryMode.Average)); + sendTimeSeries(GROUP_OPTIMISTIC + "#" + CHANNEL_POWER_ESTIMATE, f.getPowerTimeSeries(QueryMode.Optimistic)); + sendTimeSeries(GROUP_OPTIMISTIC + "#" + CHANNEL_ENERGY_ESTIMATE, f.getEnergyTimeSeries(QueryMode.Optimistic)); + sendTimeSeries(GROUP_PESSIMISTIC + "#" + CHANNEL_POWER_ESTIMATE, f.getPowerTimeSeries(QueryMode.Pessimistic)); + sendTimeSeries(GROUP_PESSIMISTIC + "#" + CHANNEL_ENERGY_ESTIMATE, f.getEnergyTimeSeries(QueryMode.Pessimistic)); bridgeHandler.ifPresent(h -> { h.forecastUpdate(); }); 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 index 0f22302542ccc..8953fc0e51ced 100644 --- 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 @@ -45,42 +45,44 @@ thing-type.config.solarforecast.sc-site.apiKey.description = API key from your s 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-channel.label = Actual Energy Forecast -channel-type.solarforecast.energy-actual-channel.description = Todays production forecast till now +channel-type.solarforecast.energy-actual-channel.description = Today's forecast till now channel-type.solarforecast.energy-estimate-channel.label = Energy Forecast -channel-type.solarforecast.energy-estimate-channel.description = Energy forecast for next hours / days -channel-type.solarforecast.energy-estimate10-channel.label = Pessimistic Energy Forecast -channel-type.solarforecast.energy-estimate10-channel.description = Pessimistic energy forecast for next hours / days -channel-type.solarforecast.energy-estimate90-channel.label = Optimistic Energy Forecast -channel-type.solarforecast.energy-estimate90-channel.description = Optimistic energy forecast for next hours / days +channel-type.solarforecast.energy-estimate-channel.description = Energy forecast for next hours/days channel-type.solarforecast.energy-remain-channel.label = Remaining Energy Forecast -channel-type.solarforecast.energy-remain-channel.description = Todays remaining production forecast till sunset +channel-type.solarforecast.energy-remain-channel.description = Today's remaining forecast till sunset channel-type.solarforecast.energy-today-channel.label = Todays Energy Forecast -channel-type.solarforecast.energy-today-channel.description = Todays total energy forecast +channel-type.solarforecast.energy-today-channel.description = Today's total energy forecast +channel-type.solarforecast.json-channel.label = Raw JSON Response +channel-type.solarforecast.json-channel.description = Plain JSON response without conversions channel-type.solarforecast.power-actual-channel.label = Actual Power -channel-type.solarforecast.power-actual-channel.description = Predicted power in this moment +channel-type.solarforecast.power-actual-channel.description = Power prediction for this moment channel-type.solarforecast.power-estimate-channel.label = Power Forecast -channel-type.solarforecast.power-estimate-channel.description = Power forecast for next hours / days -channel-type.solarforecast.power-estimate10-channel.label = Pessimistic Power Forecast -channel-type.solarforecast.power-estimate10-channel.description = Pessimistic power forecast for next hours / days -channel-type.solarforecast.power-estimate90-channel.label = Optimistic Power Forecast -channel-type.solarforecast.power-estimate90-channel.description = Optimistic power forecast for next hours / days -channel-type.solarforecast.raw-channel.label = Raw JSON Response -channel-type.solarforecast.raw-channel.description = Plain JSON response without conversions - -# thing types config - -thing-type.config.solarforecast.fs-site.channelRefreshInterval.label = Channel Refresh Interval -thing-type.config.solarforecast.fs-site.channelRefreshInterval.description = Refresh rate of channel data -thing-type.config.solarforecast.sc-site.channelRefreshInterval.label = Channel Refresh Interval -thing-type.config.solarforecast.sc-site.channelRefreshInterval.description = Refresh rate of channel data +channel-type.solarforecast.power-estimate-channel.description = Power forecast for next hours/days -# binding +# channel types -binding.solarforecast.name = SolarForecast Binding -binding.solarforecast.description = Solar Forecast for your location +channel-type.solarforecast.energy-estimate10-channel.label = Pessimistic Energy Forecast +channel-type.solarforecast.energy-estimate10-channel.description = Pessimistic energy forecast for next hours/days +channel-type.solarforecast.energy-estimate90-channel.label = Optimistic Energy Forecast +channel-type.solarforecast.energy-estimate90-channel.description = Optimistic energy forecast for next hours/days +channel-type.solarforecast.power-estimate10-channel.label = Pessimistic Power Forecast +channel-type.solarforecast.power-estimate10-channel.description = Pessimistic power forecast for next hours/days +channel-type.solarforecast.power-estimate90-channel.label = Optimistic Power Forecast +channel-type.solarforecast.power-estimate90-channel.description = Optimistic power forecast for next hours/days # status details diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast2.properties b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast2.properties new file mode 100644 index 0000000000000..0f22302542ccc --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast2.properties @@ -0,0 +1,115 @@ +# 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 types + +channel-type.solarforecast.energy-actual-channel.label = Actual Energy Forecast +channel-type.solarforecast.energy-actual-channel.description = Todays production forecast till now +channel-type.solarforecast.energy-estimate-channel.label = Energy Forecast +channel-type.solarforecast.energy-estimate-channel.description = Energy forecast for next hours / days +channel-type.solarforecast.energy-estimate10-channel.label = Pessimistic Energy Forecast +channel-type.solarforecast.energy-estimate10-channel.description = Pessimistic energy forecast for next hours / days +channel-type.solarforecast.energy-estimate90-channel.label = Optimistic Energy Forecast +channel-type.solarforecast.energy-estimate90-channel.description = Optimistic energy forecast for next hours / days +channel-type.solarforecast.energy-remain-channel.label = Remaining Energy Forecast +channel-type.solarforecast.energy-remain-channel.description = Todays remaining production forecast till sunset +channel-type.solarforecast.energy-today-channel.label = Todays Energy Forecast +channel-type.solarforecast.energy-today-channel.description = Todays total energy forecast +channel-type.solarforecast.power-actual-channel.label = Actual Power +channel-type.solarforecast.power-actual-channel.description = Predicted power in this moment +channel-type.solarforecast.power-estimate-channel.label = Power Forecast +channel-type.solarforecast.power-estimate-channel.description = Power forecast for next hours / days +channel-type.solarforecast.power-estimate10-channel.label = Pessimistic Power Forecast +channel-type.solarforecast.power-estimate10-channel.description = Pessimistic power forecast for next hours / days +channel-type.solarforecast.power-estimate90-channel.label = Optimistic Power Forecast +channel-type.solarforecast.power-estimate90-channel.description = Optimistic power forecast for next hours / days +channel-type.solarforecast.raw-channel.label = Raw JSON Response +channel-type.solarforecast.raw-channel.description = Plain JSON response without conversions + +# thing types config + +thing-type.config.solarforecast.fs-site.channelRefreshInterval.label = Channel Refresh Interval +thing-type.config.solarforecast.fs-site.channelRefreshInterval.description = Refresh rate of channel data +thing-type.config.solarforecast.sc-site.channelRefreshInterval.label = Channel Refresh Interval +thing-type.config.solarforecast.sc-site.channelRefreshInterval.description = Refresh rate of channel data + +# binding + +binding.solarforecast.name = SolarForecast Binding +binding.solarforecast.description = Solar Forecast for your location + +# status details + +solarforecast.site.status.api-key-missing = API key is mandatory +solarforecast.site.status.timezone = Time zone {0} not found +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, check raw channel + +# thing actions + +actionDayLabel = Daily Energy Production +actionDayDesc = Returns energy production for complete day in kw/h +actionInputDayLabel = Date +actionInputDayDesc = LocalDate for daily energy query +actionPowerLabel = Power +actionPowerDesc = Returns power in kw 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 0000000000000..1d70a14495648 --- /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 index d723e81a18791..370b2fabdf3cd 100644 --- 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 @@ -16,18 +16,6 @@ Power forecast for next hours/days - - Number:Power - - Pessimistic power forecast for next hours/days - - - - Number:Power - - Optimistic power forecast for next hours/days - - Number:Energy @@ -52,19 +40,7 @@ Energy forecast for next hours/days - - Number:Energy - - Pessimistic energy forecast for next hours/days - - - - Number:Energy - - Optimistic 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/optimistic-group.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/thing/optimistic-group.xml new file mode 100644 index 0000000000000..fee61b38a2726 --- /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 0000000000000..cfddb03e3fde9 --- /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 0000000000000..807fccb9404eb --- /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 index e60fa410ea4af..d99966c42deaa 100644 --- 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 @@ -12,19 +12,12 @@ PV Plane as part 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 index b3f008c3b4245..aab7c4418d316 100644 --- 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 @@ -8,18 +8,11 @@ Solcast service site definition - - - - - - - - - - - - + + + + + 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 index b864d6fa0976f..8c5067593775c 100644 --- 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 @@ -48,8 +48,8 @@ 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 QuantityType POWER_UNDEF = Utils.getPowerState(-1); + public static final QuantityType ENERGY_UNDEF = Utils.getEnergyState(-1); @Test void testForecastObject() { @@ -223,7 +223,7 @@ void testTimeSeries() { ZonedDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 23).atZone(TEST_ZONE); ForecastSolarObject fo = new ForecastSolarObject(content, queryDateTime.toInstant()); - TimeSeries powerSeries = fo.getPowerTimeSeries(QueryMode.Estimation); + 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(); @@ -231,7 +231,7 @@ void testTimeSeries() { assertEquals("kW", ((QuantityType) s).getUnit().toString()); }); - TimeSeries energySeries = fo.getEnergyTimeSeries(QueryMode.Estimation); + TimeSeries energySeries = fo.getEnergyTimeSeries(QueryMode.Average); assertEquals(36, energySeries.size()); energySeries.getStates().forEachOrdered(entry -> { State s = entry.state(); diff --git a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java index 14f8fd07c75ca..a5cec4ecc255b 100644 --- a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java @@ -16,8 +16,8 @@ import java.time.Instant; -import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants; import org.openhab.binding.solarforecast.internal.solcast.SolcastObject; @@ -36,15 +36,16 @@ public class SolcastPlaneMock extends SolcastPlaneHandler { Bridge bridge; + // solarforecast:sc-site:bridge public SolcastPlaneMock(BridgeImpl b) { - super(new ThingImpl(SolarForecastBindingConstants.SOLCAST_PLANE, new ThingUID("test", "plane")), - mock(HttpClient.class)); + super(new ThingImpl(SolarForecastBindingConstants.SOLCAST_PLANE, + new ThingUID("solarforecast", "sc-plane", "thing")), mock(HttpClient.class)); super.setCallback(new CallbackMock()); bridge = b; } @Override - public @NonNull Bridge getBridge() { + public @Nullable Bridge getBridge() { return bridge; } 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 index dc13c8e831410..a510cc6eab3a8 100644 --- 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 @@ -57,8 +57,8 @@ class SolcastTest { private static final TimeZP TIMEZONEPROVIDER = new TimeZP(); // double comparison tolerance = 1 Watt private static final double TOLERANCE = 0.001; - public static final QuantityType POWER_UNDEF = Utils.getPowerState(-1);; - public static final QuantityType ENERGY_UNDEF = Utils.getEnergyState(-1);; + public static final QuantityType POWER_UNDEF = Utils.getPowerState(-1); + public static final QuantityType ENERGY_UNDEF = Utils.getEnergyState(-1); /** * "2022-07-18T00:00+02:00[Europe/Berlin]": 0, @@ -118,22 +118,22 @@ void testForecastObject() { 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.Estimation); - double actual = scfo.getActualEnergyValue(now, QueryMode.Estimation); - double remain = scfo.getRemainingProduction(now, QueryMode.Estimation); + 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.Estimation), TOLERANCE, "Begin of day power"); + 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.Estimation) / 2.0; + double power = scfo.getActualPowerValue(now, QueryMode.Average) / 2.0; double powerAddOn = ((power + previousPower) / 2.0); actual += powerAddOn; - assertEquals(actual, scfo.getActualEnergyValue(now, QueryMode.Estimation), TOLERANCE, "Actual at " + now); + assertEquals(actual, scfo.getActualEnergyValue(now, QueryMode.Average), TOLERANCE, "Actual at " + now); remain -= powerAddOn; - assertEquals(remain, scfo.getRemainingProduction(now, QueryMode.Estimation), TOLERANCE, "Remain at " + now); + assertEquals(remain, scfo.getRemainingProduction(now, QueryMode.Average), TOLERANCE, "Remain at " + now); assertEquals(dayTotal, actual + remain, TOLERANCE, "Total sum at " + now); previousPower = power; } @@ -162,8 +162,8 @@ void testPower() { * "period_end": "2022-07-23T14:30:00.0000000Z", * "period": "PT30M" */ - assertEquals(1.9176, scfo.getActualPowerValue(now, QueryMode.Estimation), TOLERANCE, "Estimate power " + now); - assertEquals(1.754, scfo.getActualPowerValue(now.plusMinutes(30), QueryMode.Estimation), TOLERANCE, + assertEquals(1.9176, scfo.getActualPowerValue(now, QueryMode.Average), 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); @@ -189,8 +189,8 @@ void testPower() { **/ // 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.Estimation), TOLERANCE, "Estimate power " + past); - assertEquals(1.724, scfo.getActualPowerValue(past.plusMinutes(30), QueryMode.Estimation), TOLERANCE, + 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, @@ -251,8 +251,8 @@ 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(content, now.toInstant(), TIMEZONEPROVIDER); - assertEquals(0.42, scfo.getActualEnergyValue(now, QueryMode.Estimation), TOLERANCE, "Actual estimation"); - assertEquals(25.413, scfo.getDayTotal(now.toLocalDate(), QueryMode.Estimation), TOLERANCE, "Day total"); + assertEquals(0.42, scfo.getActualEnergyValue(now, QueryMode.Average), TOLERANCE, "Actual estimation"); + assertEquals(25.413, scfo.getDayTotal(now.toLocalDate(), QueryMode.Average), TOLERANCE, "Day total"); } @Test @@ -260,11 +260,11 @@ 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(content, now.toInstant(), TIMEZONEPROVIDER); - assertEquals(-1.0, scfo.getActualEnergyValue(now, QueryMode.Estimation), 0.01, "Invalid"); + assertEquals(-1.0, scfo.getActualEnergyValue(now, QueryMode.Average), 0.01, "Invalid"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); - assertEquals(18.946, scfo.getActualEnergyValue(now, QueryMode.Estimation), 0.01, "Actual data"); - assertEquals(23.107, scfo.getDayTotal(now.toLocalDate(), QueryMode.Estimation), 0.01, "Today data"); + 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")); @@ -275,7 +275,7 @@ 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(content, now.toInstant(), TIMEZONEPROVIDER); - assertEquals(-1.0, scfo.getActualEnergyValue(now, QueryMode.Estimation), 0.01, "Invalid"); + assertEquals(-1.0, scfo.getActualEnergyValue(now, QueryMode.Average), 0.01, "Invalid"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); @@ -307,13 +307,13 @@ void testOptimisticPessimistic() { SolcastObject scfo = new SolcastObject(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.Estimation), TOLERANCE, + 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.Estimation), TOLERANCE, + 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"); @@ -356,12 +356,12 @@ void testInavlid() { String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); ZonedDateTime now = ZonedDateTime.now(TEST_ZONE); SolcastObject scfo = new SolcastObject(content, now.toInstant(), TIMEZONEPROVIDER); - assertEquals(-1.0, scfo.getActualEnergyValue(now, QueryMode.Estimation), 0.01, "Data available - day not in"); + assertEquals(-1.0, scfo.getActualEnergyValue(now, QueryMode.Average), 0.01, "Data available - day not in"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); - assertEquals(-1.0, scfo.getActualEnergyValue(now, QueryMode.Estimation), 0.01, + assertEquals(-1.0, scfo.getActualEnergyValue(now, QueryMode.Average), 0.01, "Data available after merge - day not in"); - assertEquals(-1.0, scfo.getDayTotal(now.toLocalDate(), QueryMode.Estimation), 0.01, + assertEquals(-1.0, scfo.getDayTotal(now.toLocalDate(), QueryMode.Average), 0.01, "Data available after merge - day not in"); } @@ -373,12 +373,12 @@ void testPowerInterpolation() { content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); sco.join(content); - double startValue = sco.getActualPowerValue(now, QueryMode.Estimation); - double endValue = sco.getActualPowerValue(now.plusMinutes(30), QueryMode.Estimation); + 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.Estimation), TOLERANCE, + assertEquals(expected, sco.getActualPowerValue(now.plusMinutes(i), QueryMode.Average), TOLERANCE, "Step " + i); } } @@ -394,12 +394,12 @@ void testEnergyInterpolation() { double maxDiff = 0; double productionExpected = 0; for (int i = 0; i < 1000; i++) { - double forecast = sco.getActualEnergyValue(now.plusMinutes(i), QueryMode.Estimation); - double addOnExpected = sco.getActualPowerValue(now.plusMinutes(i), QueryMode.Estimation) / 60.0; + 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.Estimation), + assertEquals(productionExpected, sco.getActualEnergyValue(now.plusMinutes(i), QueryMode.Average), 100 * TOLERANCE, "Step " + i); } } @@ -454,7 +454,7 @@ void testPowerTimeSeries() { content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); sco.join(content); - TimeSeries powerSeries = sco.getPowerTimeSeries(QueryMode.Estimation); + TimeSeries powerSeries = sco.getPowerTimeSeries(QueryMode.Average); List> estimateL = new ArrayList>(); assertEquals(672, powerSeries.size()); powerSeries.getStates().forEachOrdered(entry -> { @@ -500,7 +500,7 @@ void testEnergyTimeSeries() { content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); sco.join(content); - TimeSeries energySeries = sco.getEnergyTimeSeries(QueryMode.Estimation); + 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 -> { @@ -547,12 +547,21 @@ void testCombinedPowerTimeSeries() { scbh.setCallback(cm); SolcastPlaneHandler scph1 = new SolcastPlaneMock(bi); + CallbackMock cm1 = new CallbackMock(); scph1.initialize(); - TimeSeries ts1 = cm.getTimeSeries("solarforecast:sc-site:bridge:power-estimate"); + scph1.setCallback(cm1); SolcastPlaneHandler scph2 = new SolcastPlaneMock(bi); + CallbackMock cm2 = new CallbackMock(); scph2.initialize(); - TimeSeries ts2 = cm.getTimeSeries("solarforecast:sc-site:bridge:power-estimate"); + scph2.setCallback(cm2); + + // simulate trigger of refresh job + 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"); Iterator iter1 = ts1.getStates().iterator(); Iterator iter2 = ts2.getStates().iterator(); @@ -561,7 +570,7 @@ void testCombinedPowerTimeSeries() { 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, + assertEquals(((QuantityType) e1.state()).doubleValue(), ((QuantityType) e2.state()).doubleValue() * 2, 0.1, "Power Value"); } } @@ -575,12 +584,21 @@ void testCombinedEnergyTimeSeries() { scbh.setCallback(cm); SolcastPlaneHandler scph1 = new SolcastPlaneMock(bi); + CallbackMock cm1 = new CallbackMock(); scph1.initialize(); - TimeSeries ts1 = cm.getTimeSeries("solarforecast:sc-site:bridge:energy-estimate"); + scph1.setCallback(cm1); SolcastPlaneHandler scph2 = new SolcastPlaneMock(bi); + CallbackMock cm2 = new CallbackMock(); scph2.initialize(); - TimeSeries ts2 = cm.getTimeSeries("solarforecast:sc-site:bridge:energy-estimate"); + 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"); Iterator iter1 = ts1.getStates().iterator(); Iterator iter2 = ts2.getStates().iterator(); @@ -589,7 +607,7 @@ void testCombinedEnergyTimeSeries() { 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, + assertEquals(((QuantityType) e1.state()).doubleValue(), ((QuantityType) e2.state()).doubleValue() * 2, 0.1, "Power Value"); } } @@ -608,7 +626,7 @@ void testSingleEnergyTimeSeries() { // simulate trigger of refresh job scbh.getData(); - TimeSeries ts1 = cm.getTimeSeries("solarforecast:sc-site:bridge:energy-estimate"); + 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()) { From 15e465f6bb9a103c5371db60e2ebd4dc38a0b72b Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Mon, 26 Feb 2024 22:06:07 +0100 Subject: [PATCH 084/111] readme adaptions Signed-off-by: Bernd Weymann --- .../README.md | 22 ++-- .../OH-INF/i18n/solarforecast2.properties | 115 ------------------ 2 files changed, 8 insertions(+), 129 deletions(-) delete mode 100644 bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast2.properties diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index c562bf7679176..ac878894b03ad 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -87,20 +87,18 @@ Each `sc-plane` reports it's own values including a `raw` channel holding JSON c The `sc-site` bridge sums up all attached `sc-plane` values and provides the 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 including +Forecasts are delivered up to 6 days in advance. +Scenarios are clustered in groups -- a pessimistic scenario: 10th percentile -- an optimistic scenario: 90th percentile +- `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 | -| power-estimate10 | Number:Power | W | Pessimistic power forecast for next hours/days | no | -| power-estimate90 | Number:Power | W | Optimistic power forecast for next hours/days | no | | energy-estimate | Number:Energy | kWh | Energy forecast for next hours/days | no | -| energy-estimate10 | Number:Energy | kWh | Pessimistic energy forecast for next hours/days | no | -| energy-estimate90 | Number:Energy | kWh | Optimistic 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 | @@ -139,8 +137,6 @@ In case of empty the location configured in openHAB is obtained. `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. -Note: `channelRefreshInterval` from [Bridge Configuration](#forecastsolar-bridge-configuration) will calculate intermediate values without requesting new forecast data. - #### Advanced Configuration Advanced configuration parameters are available to *fine tune* your forecast data. @@ -207,7 +203,7 @@ Returns `Instant` of the latest possible forecast data available. Returns `QuantityType` at the given `Instant` timestamp. Respect `getForecastBegin` and `getForecastEnd` to get a valid value. -Check for `UndefType.UNDEF` in case of errors. +Check for negative value in case of errors. ### `getDay` @@ -218,7 +214,7 @@ Check for `UndefType.UNDEF` in case of errors. Returns `QuantityType` at the given `localDate`. Respect `getForecastBegin` and `getForecastEnd` to avoid ambiguous values. -Check for `UndefType.UNDEF` in case of errors. +Check for negative value in case of errors. ### `getEnergy` @@ -230,7 +226,7 @@ Check for `UndefType.UNDEF` in case of errors. Returns `QuantityType` between the timestamps `startTimestamp` and `endTimestamp`. Respect `getForecastBegin` and `getForecastEnd` to avoid ambiguous values. -Check for `UndefType.UNDEF` in case of errors. +Check for negative value in case of errors. ## Date Time @@ -302,7 +298,6 @@ Strategies { * Item (excl. the group Item itself). */ Items { - influxdb* : strategy = restoreOnStartup, forecast } ``` @@ -318,7 +313,6 @@ rule "Tomorrow Forecast Calculation" val energyState = solarforecastActions.getDay(LocalDate.now.plusDays(1)) logInfo("SF Tests","{}",energyState) ForecastSolarHome_Tomorrow.postUpdate(energyState) - //val energy = (ForecastSolar_PV_Plane_Power_Forecast.historicState(now.plusDays(1)).state as Number) end ``` diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast2.properties b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast2.properties deleted file mode 100644 index 0f22302542ccc..0000000000000 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast2.properties +++ /dev/null @@ -1,115 +0,0 @@ -# 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 types - -channel-type.solarforecast.energy-actual-channel.label = Actual Energy Forecast -channel-type.solarforecast.energy-actual-channel.description = Todays production forecast till now -channel-type.solarforecast.energy-estimate-channel.label = Energy Forecast -channel-type.solarforecast.energy-estimate-channel.description = Energy forecast for next hours / days -channel-type.solarforecast.energy-estimate10-channel.label = Pessimistic Energy Forecast -channel-type.solarforecast.energy-estimate10-channel.description = Pessimistic energy forecast for next hours / days -channel-type.solarforecast.energy-estimate90-channel.label = Optimistic Energy Forecast -channel-type.solarforecast.energy-estimate90-channel.description = Optimistic energy forecast for next hours / days -channel-type.solarforecast.energy-remain-channel.label = Remaining Energy Forecast -channel-type.solarforecast.energy-remain-channel.description = Todays remaining production forecast till sunset -channel-type.solarforecast.energy-today-channel.label = Todays Energy Forecast -channel-type.solarforecast.energy-today-channel.description = Todays total energy forecast -channel-type.solarforecast.power-actual-channel.label = Actual Power -channel-type.solarforecast.power-actual-channel.description = Predicted power in this moment -channel-type.solarforecast.power-estimate-channel.label = Power Forecast -channel-type.solarforecast.power-estimate-channel.description = Power forecast for next hours / days -channel-type.solarforecast.power-estimate10-channel.label = Pessimistic Power Forecast -channel-type.solarforecast.power-estimate10-channel.description = Pessimistic power forecast for next hours / days -channel-type.solarforecast.power-estimate90-channel.label = Optimistic Power Forecast -channel-type.solarforecast.power-estimate90-channel.description = Optimistic power forecast for next hours / days -channel-type.solarforecast.raw-channel.label = Raw JSON Response -channel-type.solarforecast.raw-channel.description = Plain JSON response without conversions - -# thing types config - -thing-type.config.solarforecast.fs-site.channelRefreshInterval.label = Channel Refresh Interval -thing-type.config.solarforecast.fs-site.channelRefreshInterval.description = Refresh rate of channel data -thing-type.config.solarforecast.sc-site.channelRefreshInterval.label = Channel Refresh Interval -thing-type.config.solarforecast.sc-site.channelRefreshInterval.description = Refresh rate of channel data - -# binding - -binding.solarforecast.name = SolarForecast Binding -binding.solarforecast.description = Solar Forecast for your location - -# status details - -solarforecast.site.status.api-key-missing = API key is mandatory -solarforecast.site.status.timezone = Time zone {0} not found -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, check raw channel - -# thing actions - -actionDayLabel = Daily Energy Production -actionDayDesc = Returns energy production for complete day in kw/h -actionInputDayLabel = Date -actionInputDayDesc = LocalDate for daily energy query -actionPowerLabel = Power -actionPowerDesc = Returns power in kw 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 From 6d28f720be2a9d220be30044d1cdbc3a4f399713 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Mon, 26 Feb 2024 22:59:16 +0100 Subject: [PATCH 085/111] readme adaptions Signed-off-by: Bernd Weymann --- bundles/org.openhab.binding.solarforecast/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index ac878894b03ad..6f24586ea1f21 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -83,8 +83,8 @@ Note: `channelRefreshInterval` from [Bridge Configuration](#solcast-bridge-confi ## Solcast Channels -Each `sc-plane` reports it's own values including a `raw` channel holding JSON content. -The `sc-site` bridge sums up all attached `sc-plane` values and provides the total forecast for your home location. +Each `sc-plane` reports its own values including a `raw` 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. @@ -155,7 +155,7 @@ So you need to know what you're doing. ## ForecastSolar Channels -Each `fs-plane` reports it's own values including a `raw` channel holding JSON content. +Each `fs-plane` reports its own values including a `raw` 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. @@ -203,7 +203,7 @@ Returns `Instant` of the latest possible forecast data available. Returns `QuantityType` at the given `Instant` timestamp. Respect `getForecastBegin` and `getForecastEnd` to get a valid value. -Check for negative value in case of errors. +Check for `UndefType.UNDEF` in case of errors. ### `getDay` @@ -214,7 +214,7 @@ Check for negative value in case of errors. Returns `QuantityType` at the given `localDate`. Respect `getForecastBegin` and `getForecastEnd` to avoid ambiguous values. -Check for negative value in case of errors. +Check for `UndefType.UNDEF` in case of errors. ### `getEnergy` @@ -226,7 +226,7 @@ Check for negative value in case of errors. Returns `QuantityType` between the timestamps `startTimestamp` and `endTimestamp`. Respect `getForecastBegin` and `getForecastEnd` to avoid ambiguous values. -Check for negative value in case of errors. +Check for `UndefType.UNDEF` in case of errors. ## Date Time From 7f345f8727b2fff86eee02ebfa824cd6c5eee2a3 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Fri, 5 Apr 2024 00:01:15 +0200 Subject: [PATCH 086/111] fix review comments Signed-off-by: Bernd Weymann --- .../README.md | 2 +- .../SolarForecastBindingConstants.java | 4 +- .../internal/SolarForecastHandlerFactory.java | 2 +- .../internal/actions/SolarForecast.java | 14 ++--- .../ForecastSolarPlaneConfiguration.java | 7 +-- .../handler/ForecastSolarBridgeHandler.java | 12 ++--- .../handler/ForecastSolarPlaneHandler.java | 28 +++++----- .../internal/solcast/SolcastConstants.java | 3 +- .../internal/solcast/SolcastObject.java | 14 ++--- .../solcast/handler/SolcastBridgeHandler.java | 40 ++++++++------ .../solcast/handler/SolcastPlaneHandler.java | 54 ++++++++++++------- .../OH-INF/config/fs-site-config.xml | 2 +- .../binding/solarforecast/FileReader.java | 4 +- .../binding/solarforecast/SolcastTest.java | 6 +-- 14 files changed, 103 insertions(+), 89 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 6f24586ea1f21..e9ead78e45fc7 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -114,7 +114,7 @@ You can try it without any registration or other preconditions. | Name | Type | Description | Default | Required | |------------------------|---------|---------------------------------------|--------------|----------| -| location | text | Location of Photovoltaic system | empty | yes | +| location | text | Location of Photovoltaic system. | empty | no | | apiKey | text | API Key | N/A | no | `location` defines latitude, longitude values of your PV system. 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 index 7eb3e38618e3f..573f6a41ee96a 100644 --- 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 @@ -31,10 +31,10 @@ public class SolarForecastBindingConstants { // 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_SITGE = new ThingTypeUID(BINDING_ID, "sc-site"); + 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_SITGE, SOLCAST_PLANE); + SOLCAST_SITE, SOLCAST_PLANE); // Channel groups public static final String GROUP_AVERAGE = "average"; 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 index 2d89271a9ae74..24689033677d9 100644 --- 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 @@ -73,7 +73,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { return new ForecastSolarBridgeHandler((Bridge) thing, location); } else if (FORECAST_SOLAR_PLANE.equals(thingTypeUID)) { return new ForecastSolarPlaneHandler(thing, httpClient); - } else if (SOLCAST_SITGE.equals(thingTypeUID)) { + } else if (SOLCAST_SITE.equals(thingTypeUID)) { return new SolcastBridgeHandler((Bridge) thing, timeZoneProvider); } else if (SOLCAST_PLANE.equals(thingTypeUID)) { return new SolcastPlaneHandler(thing, httpClient); 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 index 8bcaf540f21cc..42c7871389482 100644 --- 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 @@ -50,7 +50,7 @@ public interface SolarForecast { * @param args possible arguments from this interface * @return QuantityType in kW/h */ - public QuantityType getDay(LocalDate date, String... args) throws IllegalArgumentException; + QuantityType getDay(LocalDate date, String... args); /** * Returns electric energy between two timestamps @@ -60,7 +60,7 @@ public interface SolarForecast { * @param args possible arguments from this interface * @return QuantityType in kW/h */ - public QuantityType getEnergy(Instant start, Instant end, String... args) throws IllegalArgumentException; + QuantityType getEnergy(Instant start, Instant end, String... args); /** * Returns electric power at one specific point of time @@ -69,21 +69,21 @@ public interface SolarForecast { * @param args possible arguments from this interface * @return QuantityType in kW */ - public QuantityType getPower(Instant timestamp, String... args) throws IllegalArgumentException; + QuantityType getPower(Instant timestamp, String... args); /** * Get the first date and time of forecast data * * @return date time */ - public Instant getForecastBegin(); + Instant getForecastBegin(); /** * Get the last date and time of forecast data * * @return date time */ - public Instant getForecastEnd(); + Instant getForecastEnd(); /** * Get TimeSeries for Power forecast @@ -91,7 +91,7 @@ public interface SolarForecast { * @param mode QueryMode for optimistic, pessimistic or average estimation * @return TimeSeries containing QuantityType */ - public TimeSeries getPowerTimeSeries(QueryMode mode); + TimeSeries getPowerTimeSeries(QueryMode mode); /** * Get TimeSeries for Energy forecast @@ -99,5 +99,5 @@ public interface SolarForecast { * @param mode QueryMode for optimistic, pessimistic or average estimation * @return TimeSeries containing QuantityType */ - public TimeSeries getEnergyTimeSeries(QueryMode mode); + TimeSeries getEnergyTimeSeries(QueryMode mode); } 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 index df2b2925b3094..ed2da9a384bb1 100644 --- 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 @@ -23,15 +23,10 @@ @NonNullByDefault public class ForecastSolarPlaneConfiguration { public int declination = -1; - public int azimuth = 360; + 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; - - @Override - public String toString() { - return "Dec " + declination + " Azi " + azimuth + " KWP " + kwp + " Ref " + refreshInterval; - } } 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 index 861293e0668c6..4835d9c18ecca 100644 --- 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 @@ -55,7 +55,7 @@ public class ForecastSolarBridgeHandler extends BaseBridgeHandler implements SolarForecastProvider { private final PointType homeLocation; - private List planes = new ArrayList(); + private List planes = new ArrayList<>(); private Optional configuration = Optional.empty(); private Optional> refreshJob = Optional.empty(); @@ -72,7 +72,7 @@ public Collection> getServices() { @Override public void initialize() { ForecastSolarBridgeConfiguration config = getConfigAs(ForecastSolarBridgeConfiguration.class); - if (config.location.isEmpty()) { + if (config.location.isBlank()) { Configuration editConfig = editConfiguration(); editConfig.put("location", homeLocation.toString()); updateConfiguration(editConfig); @@ -131,9 +131,9 @@ public void forecastUpdate() { if (planes.isEmpty()) { return; } - TreeMap> combinedPowerForecast = new TreeMap>(); - TreeMap> combinedEnergyForecast = new TreeMap>(); - List forecastObjects = new ArrayList(); + 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()); @@ -172,7 +172,7 @@ public synchronized void addPlane(ForecastSolarPlaneHandler sfph) { // update passive PV plane with necessary data if (configuration.isPresent()) { sfph.setLocation(new PointType(configuration.get().location)); - if (!EMPTY.equals(configuration.get().apiKey)) { + if (!configuration.get().apiKey.isBlank()) { sfph.setApiKey(configuration.get().apiKey); } } 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 index cb1d4735d1a26..b86635c3e378c 100644 --- 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 @@ -135,22 +135,10 @@ public void handleCommand(ChannelUID channelUID, Command command) { protected ForecastSolarObject fetchData() { if (location.isPresent()) { if (forecast.isExpired() || !forecast.isValid()) { - String url; - if (apiKey.isEmpty()) { - // use public API - // https://api.forecast.solar/estimate/:lat/:lon/:dec/:az/:kwp - url = BASE_URL + "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; - } else { - // use paid API - // https://api.forecast.solar/:apikey/estimate/:lat/:lon/:dec/:az/:kwp - url = BASE_URL + apiKey.get() + "/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; - } + 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; } @@ -210,6 +198,14 @@ 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)); 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 index 3b4a54f05297a..b485c5448b759 100644 --- 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 @@ -20,8 +20,7 @@ import org.openhab.core.library.unit.Units; /** - * The {@link SolcastConstants} class defines common constants, which are - * used across the whole binding. + * The {@link SolcastConstants} class defines common constants for Solcast Service * * @author Bernd Weymann - Initial contribution */ 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 index ece13c98a352a..b2959e1614c73 100644 --- 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 @@ -46,12 +46,12 @@ */ @NonNullByDefault public class SolcastObject implements SolarForecast { - private static final TreeMap EMPTY_MAP = new TreeMap(); + 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 TreeMap estimationDataMap = new TreeMap<>(); + private final TreeMap optimisticDataMap = new TreeMap<>(); + private final TreeMap pessimisticDataMap = new TreeMap<>(); private final TimeZoneProvider timeZoneProvider; private Optional rawData = Optional.of(new JSONObject()); @@ -272,10 +272,10 @@ public double getDayTotal(LocalDate query, QueryMode mode) { if (nextEntry == null) { return -1; } - ZonedDateTime endDateTime = query.atTime(23, 59, 59).atZone(timeZoneProvider.getTimeZone()); + ZonedDateTime endDateTime = iterationDateTime.plusDays(1); double forecastValue = 0; double previousEstimate = 0; - while (nextEntry.getKey().isBefore(endDateTime) || nextEntry.getKey().isEqual(endDateTime)) { + 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(); @@ -298,7 +298,7 @@ public double getRemainingProduction(ZonedDateTime query, QueryMode mode) { @Override public String toString() { - return "Expiration: " + expirationDateTime + ", Valid: " + isValid() + ", Data:" + estimationDataMap; + return "Expiration: " + expirationDateTime + ", Valid: " + isValid() + ", Data: " + estimationDataMap; } public String getRaw() { 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 index dd6c72ca0ce5c..9ae8a22e66176 100644 --- 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 @@ -78,8 +78,8 @@ public Collection> getServices() { public void initialize() { SolcastBridgeConfiguration config = getConfigAs(SolcastBridgeConfiguration.class); configuration = Optional.of(config); - if (!EMPTY.equals(config.apiKey)) { - if (!configuration.get().timeZone.isEmpty()) { + if (config.apiKey.isBlank()) { + if (!configuration.get().timeZone.isBlank()) { try { timeZone = ZoneId.of(configuration.get().timeZone); } catch (DateTimeException e) { @@ -154,10 +154,14 @@ public synchronized void getData() { powerSum += fo.getActualPowerValue(now, mode); daySum += fo.getDayTotal(now.toLocalDate(), mode); } - updateState(group + "#" + CHANNEL_ENERGY_ACTUAL, Utils.getEnergyState(energySum)); - updateState(group + "#" + CHANNEL_ENERGY_REMAIN, Utils.getEnergyState(daySum - energySum)); - updateState(group + "#" + CHANNEL_ENERGY_TODAY, Utils.getEnergyState(daySum)); - updateState(group + "#" + CHANNEL_POWER_ACTUAL, Utils.getPowerState(powerSum)); + 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)); }); } @@ -166,7 +170,7 @@ public void forecastUpdate() { return; } // get all available forecasts - List forecastObjects = new ArrayList(); + List forecastObjects = new ArrayList<>(); for (Iterator iterator = planes.iterator(); iterator.hasNext();) { SolcastPlaneHandler sfph = iterator.next(); forecastObjects.addAll(sfph.getSolarForecasts()); @@ -174,8 +178,8 @@ public void forecastUpdate() { // 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>(); + TreeMap> combinedPowerForecast = new TreeMap<>(); + TreeMap> combinedEnergyForecast = new TreeMap<>(); forecastObjects.forEach(fc -> { TimeSeries powerTS = fc.getPowerTimeSeries(mode); powerTS.getStates().forEach(entry -> { @@ -198,16 +202,22 @@ public void forecastUpdate() { }); switch (mode) { case Average: - sendTimeSeries(GROUP_AVERAGE + "#" + CHANNEL_ENERGY_ESTIMATE, energySeries); - sendTimeSeries(GROUP_AVERAGE + "#" + CHANNEL_POWER_ESTIMATE, powerSeries); + 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 + "#" + CHANNEL_ENERGY_ESTIMATE, energySeries); - sendTimeSeries(GROUP_OPTIMISTIC + "#" + CHANNEL_POWER_ESTIMATE, powerSeries); + 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 + "#" + CHANNEL_ENERGY_ESTIMATE, energySeries); - sendTimeSeries(GROUP_PESSIMISTIC + "#" + CHANNEL_POWER_ESTIMATE, powerSeries); + 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; 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 index 12391de7fc092..e4da1465da2e1 100644 --- 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 @@ -129,7 +129,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { break; case GROUP_RAW: forecast.ifPresent(f -> { - updateState(GROUP_RAW + "#" + CHANNEL_JSON, StringType.valueOf(f.getRaw())); + updateState(GROUP_RAW + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_JSON, + StringType.valueOf(f.getRaw())); }); } switch (channel) { @@ -171,7 +172,8 @@ protected synchronized SolcastObject fetchData() { if (crForecast.getStatus() == 200) { localForecast.join(crForecast.getContentAsString()); setForecast(localForecast); - updateState(GROUP_RAW + "#" + CHANNEL_JSON, StringType.valueOf(forecast.get().getRaw())); + 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()); @@ -206,27 +208,33 @@ protected void updateChannels(SolcastObject f) { double energyProduced = f.getActualEnergyValue(now, mode); switch (mode) { case Average: - updateState(GROUP_AVERAGE + "#" + CHANNEL_ENERGY_ACTUAL, Utils.getEnergyState(energyProduced)); - updateState(GROUP_AVERAGE + "#" + CHANNEL_ENERGY_REMAIN, + updateState(GROUP_AVERAGE + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_ACTUAL, + Utils.getEnergyState(energyProduced)); + updateState(GROUP_AVERAGE + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_REMAIN, Utils.getEnergyState(energyDay - energyProduced)); - updateState(GROUP_AVERAGE + "#" + CHANNEL_ENERGY_TODAY, Utils.getEnergyState(energyDay)); - updateState(GROUP_AVERAGE + "#" + CHANNEL_POWER_ACTUAL, + updateState(GROUP_AVERAGE + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_TODAY, + Utils.getEnergyState(energyDay)); + updateState(GROUP_AVERAGE + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_POWER_ACTUAL, Utils.getPowerState(f.getActualPowerValue(now, QueryMode.Average))); break; case Optimistic: - updateState(GROUP_OPTIMISTIC + "#" + CHANNEL_ENERGY_ACTUAL, Utils.getEnergyState(energyProduced)); - updateState(GROUP_OPTIMISTIC + "#" + CHANNEL_ENERGY_REMAIN, + updateState(GROUP_OPTIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_ACTUAL, + Utils.getEnergyState(energyProduced)); + updateState(GROUP_OPTIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_REMAIN, Utils.getEnergyState(energyDay - energyProduced)); - updateState(GROUP_OPTIMISTIC + "#" + CHANNEL_ENERGY_TODAY, Utils.getEnergyState(energyDay)); - updateState(GROUP_OPTIMISTIC + "#" + CHANNEL_POWER_ACTUAL, + updateState(GROUP_OPTIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_TODAY, + Utils.getEnergyState(energyDay)); + updateState(GROUP_OPTIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_POWER_ACTUAL, Utils.getPowerState(f.getActualPowerValue(now, QueryMode.Average))); break; case Pessimistic: - updateState(GROUP_PESSIMISTIC + "#" + CHANNEL_ENERGY_ACTUAL, Utils.getEnergyState(energyProduced)); - updateState(GROUP_PESSIMISTIC + "#" + CHANNEL_ENERGY_REMAIN, + updateState(GROUP_PESSIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_ACTUAL, + Utils.getEnergyState(energyProduced)); + updateState(GROUP_PESSIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_REMAIN, Utils.getEnergyState(energyDay - energyProduced)); - updateState(GROUP_PESSIMISTIC + "#" + CHANNEL_ENERGY_TODAY, Utils.getEnergyState(energyDay)); - updateState(GROUP_PESSIMISTIC + "#" + CHANNEL_POWER_ACTUAL, + updateState(GROUP_PESSIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_TODAY, + Utils.getEnergyState(energyDay)); + updateState(GROUP_PESSIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_POWER_ACTUAL, Utils.getPowerState(f.getActualPowerValue(now, QueryMode.Average))); break; default: @@ -238,12 +246,18 @@ protected void updateChannels(SolcastObject f) { protected synchronized void setForecast(SolcastObject f) { logger.info("New forecast {}", f.toString()); forecast = Optional.of(f); - sendTimeSeries(GROUP_AVERAGE + "#" + CHANNEL_POWER_ESTIMATE, f.getPowerTimeSeries(QueryMode.Average)); - sendTimeSeries(GROUP_AVERAGE + "#" + CHANNEL_ENERGY_ESTIMATE, f.getEnergyTimeSeries(QueryMode.Average)); - sendTimeSeries(GROUP_OPTIMISTIC + "#" + CHANNEL_POWER_ESTIMATE, f.getPowerTimeSeries(QueryMode.Optimistic)); - sendTimeSeries(GROUP_OPTIMISTIC + "#" + CHANNEL_ENERGY_ESTIMATE, f.getEnergyTimeSeries(QueryMode.Optimistic)); - sendTimeSeries(GROUP_PESSIMISTIC + "#" + CHANNEL_POWER_ESTIMATE, f.getPowerTimeSeries(QueryMode.Pessimistic)); - sendTimeSeries(GROUP_PESSIMISTIC + "#" + CHANNEL_ENERGY_ESTIMATE, f.getEnergyTimeSeries(QueryMode.Pessimistic)); + 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(); }); 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 index 3be22fa52a92e..58ddf944c76ba 100644 --- 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 @@ -8,7 +8,7 @@ location - Location of photovoltaic system + Location of photovoltaic system. Location of openHAB settings used in case of empty value. 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 index fcdb41b9a13a0..7aa82c2e9738b 100644 --- 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 @@ -12,7 +12,7 @@ */ package org.openhab.binding.solarforecast; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import java.io.BufferedReader; import java.io.FileInputStream; @@ -41,7 +41,7 @@ public static String readFileInString(String filename) { return buf.toString(); } catch (IOException e) { // fail if file cannot be read - assertEquals(filename, SolarForecastBindingConstants.EMPTY, "Read failute " + filename); + assertFalse(filename.isBlank(), "Read failute " + filename); } return SolarForecastBindingConstants.EMPTY; } 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 index a510cc6eab3a8..21ed70083e42b 100644 --- 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 @@ -540,7 +540,7 @@ void testEnergyTimeSeries() { @Test void testCombinedPowerTimeSeries() { - BridgeImpl bi = new BridgeImpl(SolarForecastBindingConstants.SOLCAST_SITGE, "bridge"); + BridgeImpl bi = new BridgeImpl(SolarForecastBindingConstants.SOLCAST_SITE, "bridge"); SolcastBridgeHandler scbh = new SolcastBridgeHandler(bi, new TimeZP()); bi.setHandler(scbh); CallbackMock cm = new CallbackMock(); @@ -577,7 +577,7 @@ void testCombinedPowerTimeSeries() { @Test void testCombinedEnergyTimeSeries() { - BridgeImpl bi = new BridgeImpl(SolarForecastBindingConstants.SOLCAST_SITGE, "bridge"); + BridgeImpl bi = new BridgeImpl(SolarForecastBindingConstants.SOLCAST_SITE, "bridge"); SolcastBridgeHandler scbh = new SolcastBridgeHandler(bi, new TimeZP()); bi.setHandler(scbh); CallbackMock cm = new CallbackMock(); @@ -614,7 +614,7 @@ void testCombinedEnergyTimeSeries() { @Test void testSingleEnergyTimeSeries() { - BridgeImpl bi = new BridgeImpl(SolarForecastBindingConstants.SOLCAST_SITGE, "bridge"); + BridgeImpl bi = new BridgeImpl(SolarForecastBindingConstants.SOLCAST_SITE, "bridge"); SolcastBridgeHandler scbh = new SolcastBridgeHandler(bi, new TimeZP()); bi.setHandler(scbh); CallbackMock cm = new CallbackMock(); From abb77d93e52e5ffe86c09a21dcea4432f274c2b3 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Fri, 5 Apr 2024 01:46:13 +0200 Subject: [PATCH 087/111] fix review comments Signed-off-by: Bernd Weymann --- .../README.md | 43 ++++++------------- .../actions/SolarForecastActions.java | 18 ++++---- .../forecastsolar/ForecastSolarObject.java | 4 +- .../internal/solcast/SolcastObject.java | 14 +++--- .../solcast/handler/SolcastBridgeHandler.java | 4 +- .../solcast/handler/SolcastPlaneHandler.java | 3 +- .../binding/solarforecast/SolcastTest.java | 12 +++--- 7 files changed, 40 insertions(+), 58 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index e9ead78e45fc7..d7d350146f253 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -41,20 +41,6 @@ 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 Tuning - -You have the opportunity to [send your own measurements back to Solcast API](https://legacy-docs.solcast.com.au/#measurements-rooftop-site). -This data is used internally to improve the forecast for your specific site. -Configuration and channels can be set after checking the *Show advanced* checkbox. -You need an item which reports the electric power for the specific rooftop. -If item isn't set, no measures will be sent. -As described in [Solcast Rooftop Measurement](https://legacy-docs.solcast.com.au/#measurements-rooftop-site) check in beforehand if your measures are *sane*. - -- item is delivering correct values and they are stored in persistence -- time zone setting in openHAB is correct to deliver correct timestamp - -After measurement is sent the `raw-tuning` channel is reporting the result. - ### Solcast Bridge Configuration | Name | Type | Description | Default | Required | Advanced | @@ -79,8 +65,6 @@ See [DateTime](#date-time) section for more information. `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. -Note: `channelRefreshInterval` from [Bridge Configuration](#solcast-bridge-configuration) will calculate intermediate values without requesting new forecast data. - ## Solcast Channels Each `sc-plane` reports its own values including a `raw` channel holding JSON content. @@ -88,13 +72,12 @@ The `sc-site` bridge sums up all attached `sc-plane` values and provides total f 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 +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 | @@ -120,7 +103,7 @@ You can try it without any registration or other preconditions. `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 +`apiKey` can be given in case you subscribed to a paid plan. ### ForecastSolar Plane Configuration @@ -196,25 +179,25 @@ Returns `Instant` of the latest possible forecast data available. ### `getPower` -| Parameter | Type | Description | -|-----------|---------------|--------------------------------------------------------------------------------------------------------------| -| timestamp | Instant | Timestamp of power query | -| mode | String | Choose `optimistic` or `pessimistic` to get values for a positive or negative future scenario. Only Solcast. | +| 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 for `UndefType.UNDEF` in case of errors. +Check for negative value in case of errors. ### `getDay` -| Parameter | Type | Description | -|-----------|---------------|--------------------------------------------------------------------------------------------------------------| -| date | LocalDate | Date of the day | -| mode | String | Choose `optimistic` or `pessimistic` to get values for a positive or negative future scenario. Only Solcast. | +| 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 for `UndefType.UNDEF` in case of errors. +Check for negative value in case of errors. ### `getEnergy` @@ -226,7 +209,7 @@ Check for `UndefType.UNDEF` in case of errors. Returns `QuantityType` between the timestamps `startTimestamp` and `endTimestamp`. Respect `getForecastBegin` and `getForecastEnd` to avoid ambiguous values. -Check for `UndefType.UNDEF` in case of errors. +Check for negative value in case of errors. ## Date Time 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 index 60b9e607f90ee..73917b2df8bb9 100644 --- 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 @@ -62,17 +62,17 @@ public QuantityType getDay( measure = measure.add((QuantityType) quantityState); } else { // break in case of failure getting values to avoid ambiguous values - logger.trace("Ambiguous measure {} found for {} - return UNDEF", s, localDate); + logger.debug("Ambiguous measure {} found for {}", s, localDate); return Utils.getEnergyState(-1); } } return measure; } else { - logger.trace("No forecasts found for {} - return UNDEF", localDate); + logger.debug("No forecasts found for {}", localDate); return Utils.getEnergyState(-1); } } else { - logger.trace("Handler missing - return UNDEF"); + logger.trace("Handler missing"); return Utils.getEnergyState(-1); } } @@ -92,17 +92,17 @@ public QuantityType getPower( measure = measure.add((QuantityType) quantityState); } else { // break in case of failure getting values to avoid ambiguous values - logger.trace("Ambiguous measure {} found for {} - return UNDEF", s, timestamp); + logger.debug("Ambiguous measure {} found for {}", s, timestamp); return Utils.getPowerState(-1); } } return measure; } else { - logger.trace("No forecasts found for {} - return UNDEF", timestamp); + logger.debug("No forecasts found for {}", timestamp); return Utils.getPowerState(-1); } } else { - logger.trace("Handler missing - return UNDEF"); + logger.trace("Handler missing"); return Utils.getPowerState(-1); } } @@ -123,17 +123,17 @@ public QuantityType getEnergy( measure = measure.add((QuantityType) quantityState); } else { // break in case of failure getting values to avoid ambiguous values - logger.trace("Ambiguous measure {} found between {} and {} - return UNDEF", s, start, end); + logger.debug("Ambiguous measure {} found between {} and {}", s, start, end); return Utils.getEnergyState(-1); } } return measure; } else { - logger.trace("No forecasts found for between {} and {} - return UNDEF", start, end); + logger.debug("No forecasts found for between {} and {}", start, end); return Utils.getEnergyState(-1); } } else { - logger.trace("Handler missing - return UNDEF"); + logger.trace("Handler missing"); return Utils.getEnergyState(-1); } } 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 index 651d5850cf76a..a58bbe7886b67 100644 --- 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 @@ -48,8 +48,8 @@ @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 TreeMap wattHourMap = new TreeMap<>(); + private final TreeMap wattMap = new TreeMap<>(); private final DateTimeFormatter dateInputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private ZoneId zone = ZoneId.systemDefault(); 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 index b2959e1614c73..0dc81b3743970 100644 --- 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 @@ -116,24 +116,24 @@ private void addJSONArray(JSONArray resultJsonArray) { for (int i = 0; i < resultJsonArray.length(); i++) { JSONObject jo = resultJsonArray.getJSONObject(i); String periodEnd = jo.getString("period_end"); - ZonedDateTime periadEndZdt = getZdtFromUTC(periodEnd); - if (periadEndZdt == null) { + ZonedDateTime periodEndZdt = getZdtFromUTC(periodEnd); + if (periodEndZdt == null) { return; } - estimationDataMap.put(periadEndZdt, jo.getDouble("pv_estimate")); + estimationDataMap.put(periodEndZdt, jo.getDouble("pv_estimate")); // fill pessimistic values if (jo.has("pv_estimate10")) { - pessimisticDataMap.put(periadEndZdt, jo.getDouble("pv_estimate10")); + pessimisticDataMap.put(periodEndZdt, jo.getDouble("pv_estimate10")); } else { - pessimisticDataMap.put(periadEndZdt, jo.getDouble("pv_estimate")); + pessimisticDataMap.put(periodEndZdt, jo.getDouble("pv_estimate")); } // fill optimistic values if (jo.has("pv_estimate90")) { - optimisticDataMap.put(periadEndZdt, jo.getDouble("pv_estimate90")); + optimisticDataMap.put(periodEndZdt, jo.getDouble("pv_estimate90")); } else { - optimisticDataMap.put(periadEndZdt, jo.getDouble("pv_estimate")); + optimisticDataMap.put(periodEndZdt, jo.getDouble("pv_estimate")); } if (jo.has("period")) { period = Duration.parse(jo.getString("period")).toMinutes(); 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 index 9ae8a22e66176..e041b6b5c0051 100644 --- 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 @@ -59,7 +59,7 @@ public class SolcastBridgeHandler extends BaseBridgeHandler implements SolarForecastProvider, TimeZoneProvider { private final Logger logger = LoggerFactory.getLogger(SolcastBridgeHandler.class); - private List planes = new ArrayList(); + private List planes = new ArrayList<>(); private Optional configuration = Optional.empty(); private Optional> refreshJob = Optional.empty(); private ZoneId timeZone; @@ -242,7 +242,7 @@ String getApiKey() { @Override public synchronized List getSolarForecasts() { - List l = new ArrayList(); + List l = new ArrayList<>(); planes.forEach(entry -> { l.addAll(entry.getSolarForecasts()); }); 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 index e4da1465da2e1..702e766758d31 100644 --- 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 @@ -151,7 +151,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { protected synchronized SolcastObject fetchData() { forecast.ifPresent(forecastObject -> { if (forecastObject.isExpired() || !forecastObject.isValid()) { - logger.info("Get new forecast {}", forecastObject.toString()); + logger.trace("Get new forecast {}", forecastObject.toString()); String forecastUrl = String.format(FORECAST_URL, configuration.get().resourceId); String currentEstimateUrl = String.format(CURRENT_ESTIMATE_URL, configuration.get().resourceId); try { @@ -244,7 +244,6 @@ protected void updateChannels(SolcastObject f) { } protected synchronized void setForecast(SolcastObject f) { - logger.info("New forecast {}", f.toString()); forecast = Optional.of(f); sendTimeSeries(GROUP_AVERAGE + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_POWER_ESTIMATE, f.getPowerTimeSeries(QueryMode.Average)); 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 index 21ed70083e42b..751896fc0d192 100644 --- 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 @@ -455,7 +455,7 @@ void testPowerTimeSeries() { sco.join(content); TimeSeries powerSeries = sco.getPowerTimeSeries(QueryMode.Average); - List> estimateL = new ArrayList>(); + List> estimateL = new ArrayList<>(); assertEquals(672, powerSeries.size()); powerSeries.getStates().forEachOrdered(entry -> { State s = entry.state(); @@ -465,7 +465,7 @@ void testPowerTimeSeries() { }); TimeSeries powerSeries10 = sco.getPowerTimeSeries(QueryMode.Pessimistic); - List> estimate10 = new ArrayList>(); + List> estimate10 = new ArrayList<>(); assertEquals(672, powerSeries10.size()); powerSeries10.getStates().forEachOrdered(entry -> { State s = entry.state(); @@ -475,7 +475,7 @@ void testPowerTimeSeries() { }); TimeSeries powerSeries90 = sco.getPowerTimeSeries(QueryMode.Optimistic); - List> estimate90 = new ArrayList>(); + List> estimate90 = new ArrayList<>(); assertEquals(672, powerSeries90.size()); powerSeries90.getStates().forEachOrdered(entry -> { State s = entry.state(); @@ -501,7 +501,7 @@ void testEnergyTimeSeries() { sco.join(content); TimeSeries energySeries = sco.getEnergyTimeSeries(QueryMode.Average); - List> estimateL = new ArrayList>(); + List> estimateL = new ArrayList<>(); assertEquals(672, energySeries.size()); // 18 values each day for 2 days energySeries.getStates().forEachOrdered(entry -> { State s = entry.state(); @@ -511,7 +511,7 @@ void testEnergyTimeSeries() { }); TimeSeries energySeries10 = sco.getEnergyTimeSeries(QueryMode.Pessimistic); - List> estimate10 = new ArrayList>(); + List> estimate10 = new ArrayList<>(); assertEquals(672, energySeries10.size()); // 18 values each day for 2 days energySeries10.getStates().forEachOrdered(entry -> { State s = entry.state(); @@ -521,7 +521,7 @@ void testEnergyTimeSeries() { }); TimeSeries energySeries90 = sco.getEnergyTimeSeries(QueryMode.Optimistic); - List> estimate90 = new ArrayList>(); + List> estimate90 = new ArrayList<>(); assertEquals(672, energySeries90.size()); // 18 values each day for 2 days energySeries90.getStates().forEachOrdered(entry -> { State s = entry.state(); From 4dac4230629c82f4449716055f7e2663c792a950 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Wed, 17 Apr 2024 20:20:05 +0200 Subject: [PATCH 088/111] Improve SolarForecast location config handling Signed-off-by: Bernd Weymann --- .../internal/SolarForecastHandlerFactory.java | 8 ++--- .../handler/ForecastSolarBridgeHandler.java | 35 ++++++++++++++----- .../solcast/handler/SolcastBridgeHandler.java | 4 +-- .../OH-INF/config/fs-site-config.xml | 2 +- .../OH-INF/i18n/solarforecast.properties | 2 ++ .../resources/OH-INF/thing/fs-plane-type.xml | 2 +- .../resources/OH-INF/thing/sc-plane-type.xml | 2 +- .../solarforecast/ForecastSolarTest.java | 7 ++-- 8 files changed, 43 insertions(+), 19 deletions(-) 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 index 24689033677d9..9597ab2fdb60a 100644 --- 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 @@ -14,6 +14,8 @@ 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; @@ -46,7 +48,7 @@ public class SolarForecastHandlerFactory extends BaseThingHandlerFactory { private final TimeZoneProvider timeZoneProvider; private final HttpClient httpClient; - private final PointType location; + private Optional location = Optional.empty(); @Activate public SolarForecastHandlerFactory(final @Reference HttpClientFactory hcf, final @Reference LocationProvider lp, @@ -55,9 +57,7 @@ public SolarForecastHandlerFactory(final @Reference HttpClientFactory hcf, final httpClient = hcf.getCommonHttpClient(); PointType pt = lp.getLocation(); if (pt != null) { - location = pt; - } else { - location = PointType.valueOf("0.0,0.0"); + location = Optional.of(pt); } } 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 index 4835d9c18ecca..cdf469bc58e16 100644 --- 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 @@ -39,6 +39,7 @@ 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; @@ -53,13 +54,12 @@ */ @NonNullByDefault public class ForecastSolarBridgeHandler extends BaseBridgeHandler implements SolarForecastProvider { - private final PointType homeLocation; - private List planes = new ArrayList<>(); + private Optional homeLocation; private Optional configuration = Optional.empty(); private Optional> refreshJob = Optional.empty(); - public ForecastSolarBridgeHandler(Bridge bridge, PointType location) { + public ForecastSolarBridgeHandler(Bridge bridge, Optional location) { super(bridge); homeLocation = location; } @@ -72,12 +72,31 @@ public Collection> getServices() { @Override public void initialize() { ForecastSolarBridgeConfiguration config = getConfigAs(ForecastSolarBridgeConfiguration.class); + PointType locationConfigured; + + // handle location error cases if (config.location.isBlank()) { - Configuration editConfig = editConfiguration(); - editConfig.put("location", homeLocation.toString()); - updateConfiguration(editConfig); - config = getConfigAs(ForecastSolarBridgeConfiguration.class); + 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.ONLINE); refreshJob = Optional @@ -104,7 +123,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { } /** - * Get data for all planes. Synchronized to protect parts map from being modified during update + * Get data for all planes. Synchronized to protect plane list from being modified during update */ private synchronized void getData() { if (planes.isEmpty()) { 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 index e041b6b5c0051..9a0dfa09320c5 100644 --- 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 @@ -78,7 +78,7 @@ public Collection> getServices() { public void initialize() { SolcastBridgeConfiguration config = getConfigAs(SolcastBridgeConfiguration.class); configuration = Optional.of(config); - if (config.apiKey.isBlank()) { + if (!config.apiKey.isBlank()) { if (!configuration.get().timeZone.isBlank()) { try { timeZone = ZoneId.of(configuration.get().timeZone); @@ -122,7 +122,7 @@ public void dispose() { } /** - * Get data for all planes. Protect parts map from being modified during update + * Get data for all planes. Protect plane list from being modified during update */ public synchronized void getData() { if (planes.isEmpty()) { 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 index 58ddf944c76ba..081ac44e09998 100644 --- 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 @@ -8,7 +8,7 @@ location - Location of photovoltaic system. Location of openHAB settings used in case of empty value. + Location of photovoltaic system. Location from openHAB settings is used in case of empty value. 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 index 8953fc0e51ced..df46f0634e224 100644 --- 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 @@ -88,6 +88,7 @@ channel-type.solarforecast.power-estimate90-channel.description = Optimistic pow 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.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} @@ -95,6 +96,7 @@ solarforecast.plane.status.await-feedback = Await first feedback solarforecast.plane.status.http-status = HTTP Status Code {0} solarforecast.plane.status.json-status = JSON error, check raw channel + # thing actions actionDayLabel = Daily Energy Production 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 index 3c0cdcf6dde49..5cba964bcd2dc 100644 --- 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 @@ -10,7 +10,7 @@ - PV Plane as part of Multi Plane Bridge + One PV Plane of Multi Plane Bridge 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 index d99966c42deaa..c549cc8fe1988 100644 --- 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 @@ -10,7 +10,7 @@ - PV Plane as part of Multi Plane Bridge + One PV Plane of Multi Plane Bridge 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 index 8c5067593775c..1730389685433 100644 --- 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 @@ -20,6 +20,7 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.Iterator; +import java.util.Optional; import javax.measure.quantity.Energy; import javax.measure.quantity.Power; @@ -243,7 +244,8 @@ void testTimeSeries() { @Test void testPowerTimeSeries() { ForecastSolarBridgeHandler fsbh = new ForecastSolarBridgeHandler( - new BridgeImpl(SolarForecastBindingConstants.FORECAST_SOLAR_SITE, "bridge"), PointType.valueOf("1,2")); + new BridgeImpl(SolarForecastBindingConstants.FORECAST_SOLAR_SITE, "bridge"), + Optional.of(PointType.valueOf("1,2"))); CallbackMock cm = new CallbackMock(); fsbh.setCallback(cm); @@ -273,7 +275,8 @@ void testPowerTimeSeries() { @Test void testEnergyTimeSeries() { ForecastSolarBridgeHandler fsbh = new ForecastSolarBridgeHandler( - new BridgeImpl(SolarForecastBindingConstants.FORECAST_SOLAR_SITE, "bridge"), PointType.valueOf("1,2")); + new BridgeImpl(SolarForecastBindingConstants.FORECAST_SOLAR_SITE, "bridge"), + Optional.of(PointType.valueOf("1,2"))); CallbackMock cm = new CallbackMock(); fsbh.setCallback(cm); From 723b8907c13643703a2756bdc5512a647d772daa Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Wed, 17 Apr 2024 22:30:56 +0200 Subject: [PATCH 089/111] config updates Signed-off-by: Bernd Weymann --- .../handler/ForecastSolarBridgeHandler.java | 3 +- .../handler/ForecastSolarPlaneHandler.java | 2 +- .../solcast/handler/SolcastBridgeHandler.java | 3 +- .../OH-INF/i18n/solarforecast.properties | 28 +++++++++---------- .../resources/OH-INF/thing/average-group.xml | 12 ++++---- .../resources/OH-INF/thing/channel-types.xml | 14 +++++----- .../resources/OH-INF/thing/fs-plane-type.xml | 12 ++++---- .../resources/OH-INF/thing/fs-site-type.xml | 12 ++++---- .../OH-INF/thing/optimistic-group.xml | 12 ++++---- .../OH-INF/thing/pessimistic-group.xml | 12 ++++---- .../main/resources/OH-INF/thing/raw-group.xml | 2 +- 11 files changed, 57 insertions(+), 55 deletions(-) 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 index cdf469bc58e16..b966addd11687 100644 --- 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 @@ -98,7 +98,7 @@ public void initialize() { updateConfiguration(editConfig); config = getConfigAs(ForecastSolarBridgeConfiguration.class); configuration = Optional.of(config); - updateStatus(ThingStatus.ONLINE); + updateStatus(ThingStatus.UNKNOWN); refreshJob = Optional .of(scheduler.scheduleWithFixedDelay(this::getData, 0, REFRESH_ACTUAL_INTERVAL, TimeUnit.MINUTES)); } @@ -140,6 +140,7 @@ private synchronized void getData() { powerSum += fo.getActualPowerValue(now); daySum += fo.getDayTotal(now.toLocalDate()); } + 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)); 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 index b86635c3e378c..10ec4621a4806 100644 --- 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 @@ -147,9 +147,9 @@ protected ForecastSolarObject fetchData() { if (cr.getStatus() == 200) { ForecastSolarObject localForecast = new ForecastSolarObject(cr.getContentAsString(), Instant.now().plus(configuration.get().refreshInterval, ChronoUnit.MINUTES)); - updateState(CHANNEL_JSON, StringType.valueOf(cr.getContentAsString())); if (localForecast.isValid()) { updateStatus(ThingStatus.ONLINE); + updateState(CHANNEL_JSON, StringType.valueOf(cr.getContentAsString())); setForecast(localForecast); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, 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 index 9a0dfa09320c5..967a20e764bfa 100644 --- 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 @@ -88,7 +88,7 @@ public void initialize() { return; } } - updateStatus(ThingStatus.ONLINE); + updateStatus(ThingStatus.UNKNOWN); refreshJob = Optional .of(scheduler.scheduleWithFixedDelay(this::getData, 0, REFRESH_ACTUAL_INTERVAL, TimeUnit.MINUTES)); } else { @@ -154,6 +154,7 @@ public synchronized void getData() { powerSum += fo.getActualPowerValue(now, mode); daySum += fo.getDayTotal(now.toLocalDate(), mode); } + updateStatus(ThingStatus.ONLINE); updateState(group + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_ACTUAL, Utils.getEnergyState(energySum)); updateState(group + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_REMAIN, 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 index df46f0634e224..4b8e582410541 100644 --- 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 @@ -58,20 +58,20 @@ channel-group-type.solarforecast.raw-values.description = Raw response from serv # channel types -channel-type.solarforecast.energy-actual-channel.label = Actual Energy Forecast -channel-type.solarforecast.energy-actual-channel.description = Today's forecast till now -channel-type.solarforecast.energy-estimate-channel.label = Energy Forecast -channel-type.solarforecast.energy-estimate-channel.description = Energy forecast for next hours/days -channel-type.solarforecast.energy-remain-channel.label = Remaining Energy Forecast -channel-type.solarforecast.energy-remain-channel.description = Today's remaining forecast till sunset -channel-type.solarforecast.energy-today-channel.label = Todays Energy Forecast -channel-type.solarforecast.energy-today-channel.description = Today's total energy forecast -channel-type.solarforecast.json-channel.label = Raw JSON Response -channel-type.solarforecast.json-channel.description = Plain JSON response without conversions -channel-type.solarforecast.power-actual-channel.label = Actual Power -channel-type.solarforecast.power-actual-channel.description = Power prediction for this moment -channel-type.solarforecast.power-estimate-channel.label = Power Forecast -channel-type.solarforecast.power-estimate-channel.description = Power forecast for next hours/days +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 # channel types 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 index 1d70a14495648..8f00a6c3f59d6 100644 --- 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 @@ -7,12 +7,12 @@ 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 index 370b2fabdf3cd..b6302715b919c 100644 --- 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 @@ -4,43 +4,43 @@ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - + 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 index 5cba964bcd2dc..ecc51319b5d0e 100644 --- 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 @@ -13,12 +13,12 @@ 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 index b3cd0c2f3fc67..0e7c2b91f19f5 100644 --- 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 @@ -9,12 +9,12 @@ 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 index fee61b38a2726..6ca53734dcd60 100644 --- 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 @@ -7,12 +7,12 @@ 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 index cfddb03e3fde9..e5c61debd811c 100644 --- 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 @@ -7,12 +7,12 @@ 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 index 807fccb9404eb..3427c90e0a904 100644 --- 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 @@ -7,7 +7,7 @@ Raw response from service provider - + From 646559a731fd288e28c134ef6e2f5ad4f9c6ff8c Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Wed, 17 Apr 2024 22:38:05 +0200 Subject: [PATCH 090/111] config Optional handling Signed-off-by: Bernd Weymann --- .../solcast/handler/SolcastBridgeHandler.java | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) 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 index 967a20e764bfa..74985da934099 100644 --- 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 @@ -60,8 +60,8 @@ public class SolcastBridgeHandler extends BaseBridgeHandler implements SolarFore private final Logger logger = LoggerFactory.getLogger(SolcastBridgeHandler.class); private List planes = new ArrayList<>(); - private Optional configuration = Optional.empty(); private Optional> refreshJob = Optional.empty(); + private SolcastBridgeConfiguration configuration = new SolcastBridgeConfiguration(); private ZoneId timeZone; public SolcastBridgeHandler(Bridge bridge, TimeZoneProvider tzp) { @@ -76,15 +76,14 @@ public Collection> getServices() { @Override public void initialize() { - SolcastBridgeConfiguration config = getConfigAs(SolcastBridgeConfiguration.class); - configuration = Optional.of(config); - if (!config.apiKey.isBlank()) { - if (!configuration.get().timeZone.isBlank()) { + SolcastBridgeConfiguration configuration = getConfigAs(SolcastBridgeConfiguration.class); + if (!configuration.apiKey.isBlank()) { + if (!configuration.timeZone.isBlank()) { try { - timeZone = ZoneId.of(configuration.get().timeZone); + timeZone = ZoneId.of(configuration.timeZone); } catch (DateTimeException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "@text/solarforecast.site.status.timezone" + " [\"" + configuration.get().timeZone + "\"]"); + "@text/solarforecast.site.status.timezone" + " [\"" + configuration.timeZone + "\"]"); return; } } @@ -235,10 +234,7 @@ public synchronized void removePlane(SolcastPlaneHandler sph) { } String getApiKey() { - if (configuration.isPresent()) { - return configuration.get().apiKey; - } - return EMPTY; + return configuration.apiKey; } @Override From 6978890e44b024b255a9c8a6eb2ff6d847f77cf3 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sun, 21 Apr 2024 15:35:50 +0200 Subject: [PATCH 091/111] first version exception handling Signed-off-by: Bernd Weymann --- .../SolarForecastBindingConstants.java | 2 + .../internal/actions/SolarForecast.java | 7 ++ .../forecastsolar/ForecastSolarObject.java | 88 ++++++++++++------- .../handler/ForecastSolarBridgeHandler.java | 38 ++++---- .../handler/ForecastSolarPlaneHandler.java | 33 +++---- .../internal/solcast/SolcastObject.java | 12 ++- .../solcast/handler/SolcastPlaneHandler.java | 5 +- .../solarforecast/ForecastSolarTest.java | 85 +++++++++++++----- .../solarforecast/SolcastPlaneMock.java | 3 +- .../binding/solarforecast/SolcastTest.java | 28 +++--- 10 files changed, 197 insertions(+), 104 deletions(-) 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 index 573f6a41ee96a..be4013b3a2fe0 100644 --- 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 @@ -55,4 +55,6 @@ public class SolarForecastBindingConstants { 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/actions/SolarForecast.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecast.java index 42c7871389482..b6d37bb26971c 100644 --- 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 @@ -100,4 +100,11 @@ public interface SolarForecast { * @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/forecastsolar/ForecastSolarObject.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarObject.java index a58bbe7886b67..2589783003fe1 100644 --- 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 @@ -31,6 +31,8 @@ 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; @@ -51,17 +53,21 @@ public class ForecastSolarObject implements SolarForecast { private final TreeMap wattHourMap = new TreeMap<>(); private final TreeMap wattMap = new TreeMap<>(); private final DateTimeFormatter dateInputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - + 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() { + public ForecastSolarObject(String id) { expirationDateTime = Instant.now(); + identifier = id; } - public ForecastSolarObject(String content, Instant expirationDate) { + public ForecastSolarObject(String id, String content, Instant expirationDate) throws SolarForecastException { expirationDateTime = expirationDate; + identifier = id; if (!content.isEmpty()) { rawData = Optional.of(content); try { @@ -71,6 +77,8 @@ public ForecastSolarObject(String content, Instant expirationDate) { 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()) { @@ -81,28 +89,24 @@ public ForecastSolarObject(String content, Instant expirationDate) { wattHourMap.put(zdt, wattHourJson.getDouble(dateStr)); wattMap.put(zdt, wattJson.getDouble(dateStr)); } catch (DateTimeParseException dtpe) { - logger.warn("Exception parsing time {} Reason: {}", dateStr, dtpe.getMessage()); - return; + logger.warn("Error parsing time {} Reason: {}", dateStr, dtpe.getMessage()); + throw new SolarForecastException(this, + "Error parsing time " + dateStr + " Reason: " + dtpe.getMessage()); } } } catch (JSONException je) { logger.debug("Error parsing JSON response {} - {}", content, je.getMessage()); + throw new SolarForecastException(this, + "Error parsing JSON response " + content + " Reason: " + je.getMessage()); } } } - public boolean isValid() { - return !(wattHourMap.isEmpty() || wattMap.isEmpty()); - } - public boolean isExpired() { return expirationDateTime.isBefore(Instant.now()); } - public double getActualEnergyValue(ZonedDateTime queryDateTime) { - if (!isValid()) { - return -1; - } + public double getActualEnergyValue(ZonedDateTime queryDateTime) throws SolarForecastException { Entry f = wattHourMap.floorEntry(queryDateTime); Entry c = wattHourMap.ceilingEntry(queryDateTime); if (f != null && c == null) { @@ -112,7 +116,7 @@ public double getActualEnergyValue(ZonedDateTime queryDateTime) { return f.getValue() / 1000.0; } else { // floor date doesn't fit - return -1; + throwOutOfRangeException(queryDateTime.toInstant()); } } else if (f == null && c != null) { if (c.getKey().toLocalDate().equals(queryDateTime.toLocalDate())) { @@ -120,7 +124,7 @@ public double getActualEnergyValue(ZonedDateTime queryDateTime) { return 0; } else { // ceiling date doesn't fit - return -1; + throwOutOfRangeException(queryDateTime.toInstant()); } } else if (f != null && c != null) { // ceiling and floor available @@ -146,6 +150,7 @@ public double getActualEnergyValue(ZonedDateTime queryDateTime) { return 0; } } // else both null - date time doesn't fit to forecast data + throwOutOfRangeException(queryDateTime.toInstant()); return -1; } @@ -159,9 +164,6 @@ public TimeSeries getEnergyTimeSeries(QueryMode mode) { } public double getActualPowerValue(ZonedDateTime queryDateTime) { - if (!isValid()) { - return -1; - } double actualPowerValue = 0; Entry f = wattMap.floorEntry(queryDateTime); Entry c = wattMap.ceilingEntry(queryDateTime); @@ -224,9 +226,6 @@ public double getDayTotal(LocalDate queryDate) { } public double getRemainingProduction(ZonedDateTime queryDateTime) { - if (!isValid()) { - return -1; - } double daily = getDayTotal(queryDateTime.toLocalDate()); double actual = getActualEnergyValue(queryDateTime); if (daily < 0 || actual < 0) { @@ -236,7 +235,7 @@ public double getRemainingProduction(ZonedDateTime queryDateTime) { } public String getRaw() { - if (isValid() && rawData.isPresent()) { + if (rawData.isPresent()) { return rawData.get(); } return "{}"; @@ -248,7 +247,7 @@ public ZoneId getZone() { @Override public String toString() { - return "Expiration: " + expirationDateTime + ", Valid: " + isValid() + ", Data:" + wattHourMap; + return "Expiration: " + expirationDateTime + ", Data:" + wattHourMap; } /** @@ -303,19 +302,46 @@ public QuantityType getPower(Instant timestamp, String... args) throws Il @Override public Instant getForecastBegin() { - if (isValid()) { - ZonedDateTime zdt = wattHourMap.firstEntry().getKey(); - return zdt.toInstant(); + if (wattHourMap.isEmpty()) { + return Instant.MAX; } - return Instant.MAX; + ZonedDateTime zdt = wattHourMap.firstEntry().getKey(); + return zdt.toInstant(); } @Override public Instant getForecastEnd() { - if (isValid()) { - ZonedDateTime zdt = wattHourMap.lastEntry().getKey(); - return zdt.toInstant(); + if (wattHourMap.isEmpty()) { + return Instant.MIN; + } + ZonedDateTime zdt = wattHourMap.lastEntry().getKey(); + return zdt.toInstant(); + } + + private void throwOutOfRangeException(Instant query) { + if (getForecastBegin() == Instant.MAX || getForecastEnd() == Instant.MIN) { + throw new SolarForecastException(this, "Forecast invalid time range"); + } + if (query.isBefore(getForecastBegin())) { + throw new SolarForecastException(this, + "Query " + dateOutputFormatter.format(query) + " too early. Valid range: " + + dateOutputFormatter.format(getForecastBegin()) + " - " + + dateOutputFormatter.format(getForecastEnd())); + } else if (query.isAfter(getForecastEnd())) { + throw new SolarForecastException(this, + "Query " + dateOutputFormatter.format(query) + " too late. Valid range: " + + dateOutputFormatter.format(getForecastBegin()) + " - " + + dateOutputFormatter.format(getForecastEnd())); + } else { + logger.warn("Query " + dateOutputFormatter.format(query) + " is fine in range: " + + dateOutputFormatter.format(getForecastBegin()) + " - " + + dateOutputFormatter.format(getForecastEnd()) + ". This shouldn't happen!"); } - return Instant.MIN; } + + @Override + public String getIdentifier() { + return identifier; + } + } 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 index b966addd11687..bb7cfb2f17769 100644 --- 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 @@ -26,6 +26,7 @@ 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; @@ -46,6 +47,8 @@ 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 ForecastSolarBridgeHandler} is a non active handler instance. It will be triggerer by the bridge. @@ -54,6 +57,7 @@ */ @NonNullByDefault public class ForecastSolarBridgeHandler extends BaseBridgeHandler implements SolarForecastProvider { + private final Logger logger = LoggerFactory.getLogger(ForecastSolarBridgeHandler.class); private List planes = new ArrayList<>(); private Optional homeLocation; private Optional configuration = Optional.empty(); @@ -129,22 +133,26 @@ private synchronized void getData() { if (planes.isEmpty()) { return; } - double energySum = 0; - double powerSum = 0; - double daySum = 0; - for (Iterator iterator = planes.iterator(); iterator.hasNext();) { - 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()); + try { + double energySum = 0; + double powerSum = 0; + double daySum = 0; + for (Iterator iterator = planes.iterator(); iterator.hasNext();) { + 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()); + } + 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)); + } catch (SolarForecastException sfe) { + logger.warn(sfe.getMessage()); } - 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 void forecastUpdate() { 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 index 10ec4621a4806..5d38797774141 100644 --- 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 @@ -27,6 +27,7 @@ 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; @@ -65,11 +66,12 @@ public class ForecastSolarPlaneHandler extends BaseThingHandler implements Solar private Optional bridgeHandler = Optional.empty(); private Optional location = Optional.empty(); private Optional apiKey = Optional.empty(); - private ForecastSolarObject forecast = new ForecastSolarObject(); + private ForecastSolarObject forecast; public ForecastSolarPlaneHandler(Thing thing, HttpClient hc) { super(thing); httpClient = hc; + forecast = new ForecastSolarObject(thing.getUID().getAsString()); } @Override @@ -115,16 +117,14 @@ public void dispose() { @Override public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { - if (forecast.isValid()) { - 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(); - } + 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(); } } } @@ -134,7 +134,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { */ protected ForecastSolarObject fetchData() { if (location.isPresent()) { - if (forecast.isExpired() || !forecast.isValid()) { + 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=" @@ -145,13 +145,14 @@ protected ForecastSolarObject fetchData() { try { ContentResponse cr = httpClient.GET(url); if (cr.getStatus() == 200) { - ForecastSolarObject localForecast = new ForecastSolarObject(cr.getContentAsString(), - Instant.now().plus(configuration.get().refreshInterval, ChronoUnit.MINUTES)); - if (localForecast.isValid()) { + 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); - } else { + } catch (SolarForecastException fse) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/solarforecast.plane.status.json-status"); } 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 index 0dc81b3743970..626275cbedaee 100644 --- 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 @@ -54,6 +54,7 @@ public class SolcastObject implements SolarForecast { private final TreeMap pessimisticDataMap = new TreeMap<>(); private final TimeZoneProvider timeZoneProvider; + private String identifier; private Optional rawData = Optional.of(new JSONObject()); private Instant expirationDateTime; private long period = 30; @@ -76,13 +77,15 @@ public String toString() { } } - public SolcastObject(TimeZoneProvider tzp) { + public SolcastObject(String id, TimeZoneProvider tzp) { // invalid forecast object + identifier = id; timeZoneProvider = tzp; expirationDateTime = Instant.now(); } - public SolcastObject(String content, Instant expiration, TimeZoneProvider tzp) { + public SolcastObject(String id, String content, Instant expiration, TimeZoneProvider tzp) { + identifier = id; expirationDateTime = expiration; timeZoneProvider = tzp; add(content); @@ -460,4 +463,9 @@ private QueryMode evalArguments(String[] args) { return QueryMode.Average; } } + + @Override + public String getIdentifier() { + return identifier; + } } 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 index 702e766758d31..9d6c5c17c7485 100644 --- 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 @@ -86,7 +86,7 @@ public void initialize() { if (handler instanceof SolcastBridgeHandler sbh) { bridgeHandler = Optional.of(sbh); bridgeHandler.get().addPlane(this); - forecast = Optional.of(new SolcastObject(bridgeHandler.get())); + forecast = Optional.of(new SolcastObject(thing.getUID().getAsString(), bridgeHandler.get())); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/solarforecast.plane.status.wrong-handler [\"" + handler + "\"]"); @@ -160,7 +160,8 @@ protected synchronized SolcastObject fetchData() { estimateRequest.header(HttpHeader.AUTHORIZATION, BEARER + bridgeHandler.get().getApiKey()); ContentResponse crEstimate = estimateRequest.send(); if (crEstimate.getStatus() == 200) { - SolcastObject localForecast = new SolcastObject(crEstimate.getContentAsString(), + SolcastObject localForecast = new SolcastObject(thing.getUID().getAsString(), + crEstimate.getContentAsString(), Instant.now().plus(configuration.get().refreshInterval, ChronoUnit.MINUTES), bridgeHandler.get()); 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 index 1730389685433..9cb9fc8aae778 100644 --- 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 @@ -28,6 +28,7 @@ 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.forecastsolar.ForecastSolarObject; import org.openhab.binding.solarforecast.internal.forecastsolar.handler.ForecastSolarBridgeHandler; import org.openhab.binding.solarforecast.internal.forecastsolar.handler.ForecastSolarPlaneHandler; @@ -56,7 +57,7 @@ class ForecastSolarTest { 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(content, queryDateTime.toInstant()); + ForecastSolarObject fo = new ForecastSolarObject("fs-test", content, queryDateTime.toInstant()); // "2022-07-17 21:32:00": 63583, assertEquals(63.583, fo.getDayTotal(queryDateTime.toLocalDate()), TOLERANCE, "Total production"); // "2022-07-17 17:00:00": 52896, @@ -79,7 +80,7 @@ void testForecastObject() { 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(content, queryDateTime.toInstant()); + ForecastSolarObject fo = new ForecastSolarObject("fs-test", content, queryDateTime.toInstant()); // "2022-07-17 10:00:00": 4874, assertEquals(4.874, fo.getActualPowerValue(queryDateTime), TOLERANCE, "Actual estimation"); @@ -92,7 +93,7 @@ void testActualPower() { 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(content, queryDateTime.toInstant()); + ForecastSolarObject fo = new ForecastSolarObject("fs-test", content, queryDateTime.toInstant()); // test steady value increase double previousValue = 0; @@ -113,7 +114,7 @@ void testInterpolation() { 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(content, queryDateTime.toInstant()); + ForecastSolarObject fo = new ForecastSolarObject("fs-test", content, queryDateTime.toInstant()); QuantityType actual = QuantityType.valueOf(0, Units.KILOWATT_HOUR); State st = Utils.getEnergyState(fo.getActualEnergyValue(queryDateTime)); assertTrue(st instanceof QuantityType); @@ -126,22 +127,47 @@ void testForecastSum() { @Test void testCornerCases() { // invalid object - ForecastSolarObject fo = new ForecastSolarObject(); - assertFalse(fo.isValid()); + ForecastSolarObject fo = new ForecastSolarObject("fs-test"); ZonedDateTime query = LocalDateTime.of(2022, 7, 17, 16, 23).atZone(TEST_ZONE); - assertEquals(-1.0, fo.getActualEnergyValue(query), TOLERANCE, "Actual Production"); + try { + double d = fo.getActualEnergyValue(query); + fail("Actual Energy without time range"); + } catch (SolarForecastException sfe) { + assertTrue(sfe.getMessage().contains("invalid time range"), "invalid time range"); + } + try { + double d = fo.getRemainingProduction(query); + fail("Remaining Production without time range"); + } catch (SolarForecastException sfe) { + assertTrue(sfe.getMessage().contains("invalid time range"), "invalid time range"); + } assertEquals(-1.0, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Today Production"); - assertEquals(-1.0, fo.getRemainingProduction(query), TOLERANCE, "Remaining Production"); assertEquals(-1.0, fo.getDayTotal(query.plusDays(1).toLocalDate()), TOLERANCE, "Tomorrow Production"); // 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(content, query.toInstant()); + fo = new ForecastSolarObject("fs-test", content, query.toInstant()); + try { + double d = fo.getActualEnergyValue(query); + fail("Remaining Production without time range " + d); + } catch (SolarForecastException sfe) { + assertTrue(sfe.getMessage().contains("too early"), "too early"); + } + try { + double d = fo.getRemainingProduction(query); + fail("Remaining Production without time range " + d); + } catch (SolarForecastException sfe) { + assertTrue(sfe.getMessage().contains("too early"), "too early"); + } + try { + double d = fo.getActualPowerValue(query); + // fail("Remaining Production without time range " + d); + } catch (SolarForecastException sfe) { + System.out.println(sfe.getMessage()); + assertTrue(sfe.getMessage().contains("too early"), "too early"); + } assertEquals(-1.0, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope"); - assertEquals(-1.0, fo.getActualEnergyValue(query), TOLERANCE, "Actual out of scope"); - assertEquals(-1.0, fo.getRemainingProduction(query), TOLERANCE, "Remain out of scope"); - assertEquals(-1.0, fo.getActualPowerValue(query), TOLERANCE, "Remain out of scope"); // one minute later we reach a valid date query = query.plusMinutes(1); @@ -152,10 +178,20 @@ void testCornerCases() { // valid object - query date one day too late query = LocalDateTime.of(2022, 7, 19, 0, 0).atZone(TEST_ZONE); - assertEquals(-1.0, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope"); - assertEquals(-1.0, fo.getActualEnergyValue(query), TOLERANCE, "Actual out of scope"); - assertEquals(-1.0, fo.getRemainingProduction(query), TOLERANCE, "Remain out of scope"); + try { + double d = fo.getActualEnergyValue(query); + fail("Remaining Production without time range " + d); + } catch (SolarForecastException sfe) { + assertTrue(sfe.getMessage().contains("too late"), "too late"); + } + try { + double d = fo.getRemainingProduction(query); + fail("Remaining Production without time range " + d); + } catch (SolarForecastException sfe) { + assertTrue(sfe.getMessage().contains("too late"), "too late"); + } assertEquals(-1.0, fo.getActualPowerValue(query), TOLERANCE, "Remain out of scope"); + assertEquals(-1.0, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope"); // one minute earlier we reach a valid date query = query.minusMinutes(1); @@ -181,7 +217,7 @@ void testCornerCases() { void testActions() { 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(content, queryDateTime.toInstant()); + 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"); @@ -190,14 +226,17 @@ void testActions() { assertEquals(QuantityType.valueOf(63.583, Units.KILOWATT_HOUR).toString(), fo.getDay(queryDateTime.toLocalDate()).toFullString(), "Actual out of scope"); - queryDateTime = LocalDateTime.of(2022, 7, 17, 0, 0).atZone(TEST_ZONE); + queryDateTime = LocalDateTime.of(2022, 7, 10, 0, 0).atZone(TEST_ZONE); // "watt_hours_day": { // "2022-07-17": 63583, // "2022-07-18": 65554 // } - assertEquals(QuantityType.valueOf(129.137, Units.KILOWATT_HOUR).toString(), - fo.getEnergy(queryDateTime.toInstant(), queryDateTime.plusDays(2).toInstant()).toFullString(), - "Actual out of scope"); + try { + fo.getEnergy(queryDateTime.toInstant(), queryDateTime.plusDays(2).toInstant()); + fail("Too early exception missing"); + } catch (SolarForecastException sfe) { + assertTrue(sfe.getMessage().contains("too early"), "Too early exception"); + } try { fo.getDay(queryDateTime.toLocalDate(), "optimistic"); fail(); @@ -222,7 +261,7 @@ void testActions() { 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(content, queryDateTime.toInstant()); + 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 @@ -250,7 +289,7 @@ void testPowerTimeSeries() { fsbh.setCallback(cm); String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); - ForecastSolarObject fso1 = new ForecastSolarObject(content, Instant.now()); + ForecastSolarObject fso1 = new ForecastSolarObject("fs-test", content, Instant.now()); ForecastSolarPlaneHandler fsph1 = new ForecastSolarPlaneMock(fso1); fsbh.addPlane(fsph1); fsbh.forecastUpdate(); @@ -281,7 +320,7 @@ void testEnergyTimeSeries() { fsbh.setCallback(cm); String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); - ForecastSolarObject fso1 = new ForecastSolarObject(content, Instant.now()); + ForecastSolarObject fso1 = new ForecastSolarObject("fs-test", content, Instant.now()); ForecastSolarPlaneHandler fsph1 = new ForecastSolarPlaneMock(fso1); fsbh.addPlane(fsph1); fsbh.forecastUpdate(); diff --git a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java index a5cec4ecc255b..684d3cdc88ab9 100644 --- a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java @@ -54,7 +54,8 @@ protected SolcastObject fetchData() { forecast.ifPresent(forecastObject -> { if (forecastObject.isExpired() || !forecastObject.isValid()) { String content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); - SolcastObject sco1 = new SolcastObject(content, Instant.now().plusSeconds(3600), new TimeZP()); + SolcastObject sco1 = new SolcastObject("sc-test", content, Instant.now().plusSeconds(3600), + new TimeZP()); super.setForecast(sco1); // new forecast } else { 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 index 751896fc0d192..062de9db5fcee 100644 --- 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 @@ -114,7 +114,7 @@ class SolcastTest { 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(content, now.toInstant(), TIMEZONEPROVIDER); + 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 @@ -143,7 +143,7 @@ void testForecastObject() { 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(content, now.toInstant(), TIMEZONEPROVIDER); + SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER); content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); scfo.join(content); @@ -250,7 +250,7 @@ void testPower() { 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(content, now.toInstant(), TIMEZONEPROVIDER); + 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"); } @@ -259,7 +259,7 @@ void testForecastTreeMap() { 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(content, now.toInstant(), TIMEZONEPROVIDER); + SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER); assertEquals(-1.0, scfo.getActualEnergyValue(now, QueryMode.Average), 0.01, "Invalid"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); @@ -274,7 +274,7 @@ void testJoin() { 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(content, now.toInstant(), TIMEZONEPROVIDER); + SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER); assertEquals(-1.0, scfo.getActualEnergyValue(now, QueryMode.Average), 0.01, "Invalid"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); @@ -304,7 +304,7 @@ void testActions() { 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(content, now.toInstant(), TIMEZONEPROVIDER); + 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, @@ -355,7 +355,7 @@ void testOptimisticPessimistic() { void testInavlid() { String content = FileReader.readFileInString("src/test/resources/solcast/estimated-actuals.json"); ZonedDateTime now = ZonedDateTime.now(TEST_ZONE); - SolcastObject scfo = new SolcastObject(content, now.toInstant(), TIMEZONEPROVIDER); + SolcastObject scfo = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER); assertEquals(-1.0, scfo.getActualEnergyValue(now, QueryMode.Average), 0.01, "Data available - day not in"); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); @@ -369,7 +369,7 @@ void testInavlid() { 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(content, now.toInstant(), TIMEZONEPROVIDER); + SolcastObject sco = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); sco.join(content); @@ -387,7 +387,7 @@ void testPowerInterpolation() { 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(content, now.toInstant(), TIMEZONEPROVIDER); + SolcastObject sco = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); sco.join(content); @@ -408,7 +408,7 @@ void testEnergyInterpolation() { 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(content, now.toInstant(), TIMEZONEPROVIDER); + 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()); @@ -420,7 +420,7 @@ void testRawChannel() { 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(content, now.toInstant(), TIMEZONEPROVIDER); + 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()); @@ -437,7 +437,7 @@ void testUnitDetection() { @Test void testTimes() { String utcTimeString = "2022-07-17T19:30:00.0000000Z"; - SolcastObject so = new SolcastObject(TIMEZONEPROVIDER); + SolcastObject so = new SolcastObject("sc-test", TIMEZONEPROVIDER); ZonedDateTime zdt = so.getZdtFromUTC(utcTimeString); assertEquals("2022-07-17T21:30+02:00[Europe/Berlin]", zdt.toString(), "ZonedDateTime"); LocalDateTime ldt = zdt.toLocalDateTime(); @@ -450,7 +450,7 @@ void testTimes() { 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(content, now.toInstant(), TIMEZONEPROVIDER); + SolcastObject sco = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); sco.join(content); @@ -496,7 +496,7 @@ void testPowerTimeSeries() { 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(content, now.toInstant(), TIMEZONEPROVIDER); + SolcastObject sco = new SolcastObject("sc-test", content, now.toInstant(), TIMEZONEPROVIDER); content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); sco.join(content); From 76275288f6b49dcdffe8d73d8c6d616e25e02e8b Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sun, 21 Apr 2024 15:39:05 +0200 Subject: [PATCH 092/111] first version exception handling Signed-off-by: Bernd Weymann --- .../internal/SolarForecastException.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastException.java 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 0000000000000..bad9fe1674c7d --- /dev/null +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/SolarForecastException.java @@ -0,0 +1,41 @@ +/** + * 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.NonNull; +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); + } + + @Override + public @NonNull String getMessage() { + String message = super.getMessage(); + if (message == null) { + return SolarForecastBindingConstants.EMPTY; + } else { + return message; + } + } +} From 472137ce5fc470854854677f5657337838f665d6 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sun, 21 Apr 2024 15:55:26 +0200 Subject: [PATCH 093/111] switch case optimization Signed-off-by: Bernd Weymann --- .../solcast/handler/SolcastBridgeHandler.java | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) 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 index 74985da934099..2c31714474a33 100644 --- 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 @@ -131,18 +131,12 @@ public synchronized void getData() { ZonedDateTime now = ZonedDateTime.now(getTimeZone()); List modes = List.of(QueryMode.Average, QueryMode.Pessimistic, QueryMode.Optimistic); modes.forEach(mode -> { - String group = GROUP_AVERAGE; - switch (mode) { - case Average: - group = GROUP_AVERAGE; - break; - case Optimistic: - group = GROUP_OPTIMISTIC; - break; - case Pessimistic: - group = GROUP_PESSIMISTIC; - break; - } + String group = switch (mode) { + case Average -> GROUP_AVERAGE; + case Optimistic -> GROUP_OPTIMISTIC; + case Pessimistic -> GROUP_PESSIMISTIC; + default -> GROUP_AVERAGE; + }; double energySum = 0; double powerSum = 0; double daySum = 0; From fa206869079159649d6f1aeb4fd0e28e7bd20cae Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sun, 21 Apr 2024 18:08:25 +0200 Subject: [PATCH 094/111] switched ForecastSolar to exception handling Signed-off-by: Bernd Weymann --- .../forecastsolar/ForecastSolarObject.java | 13 ++- .../handler/ForecastSolarBridgeHandler.java | 2 +- .../solcast/handler/SolcastBridgeHandler.java | 2 +- .../solarforecast/ForecastSolarTest.java | 83 ++++++++++++++----- 4 files changed, 71 insertions(+), 29 deletions(-) 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 index 2589783003fe1..7d44ceff42689 100644 --- 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 @@ -174,7 +174,7 @@ public double getActualPowerValue(ZonedDateTime queryDateTime) { return f.getValue() / 1000.0; } else { // floor date doesn't fit - return -1; + throwOutOfRangeException(queryDateTime.toInstant()); } } else if (f == null && c != null) { if (c.getKey().toLocalDate().equals(queryDateTime.toLocalDate())) { @@ -182,7 +182,7 @@ public double getActualPowerValue(ZonedDateTime queryDateTime) { return 0; } else { // ceiling date doesn't fit - return -1; + throwOutOfRangeException(queryDateTime.toInstant()); } } else if (f != null && c != null) { // we're during suntime! @@ -199,6 +199,7 @@ public double getActualPowerValue(ZonedDateTime queryDateTime) { actualPowerValue = ((1 - interpolation) * powerFloor) + (interpolation * powerCeiling); return actualPowerValue / 1000.0; } // else both null - this shall not happen + throwOutOfRangeException(queryDateTime.toInstant()); return -1; } @@ -213,7 +214,7 @@ public TimeSeries getPowerTimeSeries(QueryMode mode) { public double getDayTotal(LocalDate queryDate) { if (rawData.isEmpty()) { - return -1; + throw new SolarForecastException(this, "No forecast data available"); } JSONObject contentJson = new JSONObject(rawData.get()); JSONObject resultJson = contentJson.getJSONObject("result"); @@ -221,16 +222,14 @@ public double getDayTotal(LocalDate queryDate) { if (wattsDay.has(queryDate.toString())) { return wattsDay.getDouble(queryDate.toString()) / 1000.0; + } else { + throw new SolarForecastException(this, "Day " + queryDate + " not available in forecast"); } - return -1; } public double getRemainingProduction(ZonedDateTime queryDateTime) { double daily = getDayTotal(queryDateTime.toLocalDate()); double actual = getActualEnergyValue(queryDateTime); - if (daily < 0 || actual < 0) { - return -1; - } return daily - actual; } 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 index bb7cfb2f17769..f9d766b479dcb 100644 --- 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 @@ -155,7 +155,7 @@ private synchronized void getData() { } } - public void forecastUpdate() { + public synchronized void forecastUpdate() { if (planes.isEmpty()) { return; } 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 index 2c31714474a33..83ff25dc8b6a0 100644 --- 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 @@ -159,7 +159,7 @@ public synchronized void getData() { }); } - public void forecastUpdate() { + public synchronized void forecastUpdate() { if (planes.isEmpty()) { return; } 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 index 9cb9fc8aae778..675135880b4c6 100644 --- 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 @@ -53,6 +53,12 @@ class ForecastSolarTest { 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"); @@ -131,18 +137,32 @@ void testCornerCases() { ZonedDateTime query = LocalDateTime.of(2022, 7, 17, 16, 23).atZone(TEST_ZONE); try { double d = fo.getActualEnergyValue(query); - fail("Actual Energy without time range"); + fail("Exception expected instead of " + d); } catch (SolarForecastException sfe) { - assertTrue(sfe.getMessage().contains("invalid time range"), "invalid time range"); + assertTrue(sfe.getMessage().contains(INVALID_RANGE_INDICATOR), + "Expected: " + INVALID_RANGE_INDICATOR + " Received: " + sfe.getMessage()); } try { double d = fo.getRemainingProduction(query); - fail("Remaining Production without time range"); + fail("Exception expected instead of " + d); } catch (SolarForecastException sfe) { - assertTrue(sfe.getMessage().contains("invalid time range"), "invalid time range"); + assertTrue(sfe.getMessage().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) { + assertTrue(sfe.getMessage().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) { + assertTrue(sfe.getMessage().contains(NO_GORECAST_INDICATOR), + "Expected: " + NO_GORECAST_INDICATOR + " Received: " + sfe.getMessage()); } - assertEquals(-1.0, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Today Production"); - assertEquals(-1.0, fo.getDayTotal(query.plusDays(1).toLocalDate()), TOLERANCE, "Tomorrow Production"); // valid object - query date one day too early String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); @@ -150,24 +170,33 @@ void testCornerCases() { fo = new ForecastSolarObject("fs-test", content, query.toInstant()); try { double d = fo.getActualEnergyValue(query); - fail("Remaining Production without time range " + d); + fail("Exception expected instead of " + d); } catch (SolarForecastException sfe) { - assertTrue(sfe.getMessage().contains("too early"), "too early"); + assertTrue(sfe.getMessage().contains(TOO_EARLY_INDICATOR), + "Expected: " + TOO_EARLY_INDICATOR + " Received: " + sfe.getMessage()); } try { double d = fo.getRemainingProduction(query); - fail("Remaining Production without time range " + d); + fail("Exception expected instead of " + d); } catch (SolarForecastException sfe) { - assertTrue(sfe.getMessage().contains("too early"), "too early"); + assertTrue(sfe.getMessage().contains(DAY_MISSING_INDICATOR), + "Expected: " + DAY_MISSING_INDICATOR + " Received: " + sfe.getMessage()); } try { double d = fo.getActualPowerValue(query); - // fail("Remaining Production without time range " + d); + fail("Exception expected instead of " + d); } catch (SolarForecastException sfe) { System.out.println(sfe.getMessage()); - assertTrue(sfe.getMessage().contains("too early"), "too early"); + assertTrue(sfe.getMessage().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) { + assertTrue(sfe.getMessage().contains(DAY_MISSING_INDICATOR), + "Expected: " + DAY_MISSING_INDICATOR + " Received: " + sfe.getMessage()); } - assertEquals(-1.0, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope"); // one minute later we reach a valid date query = query.plusMinutes(1); @@ -180,18 +209,32 @@ void testCornerCases() { query = LocalDateTime.of(2022, 7, 19, 0, 0).atZone(TEST_ZONE); try { double d = fo.getActualEnergyValue(query); - fail("Remaining Production without time range " + d); + fail("Exception expected instead of " + d); } catch (SolarForecastException sfe) { - assertTrue(sfe.getMessage().contains("too late"), "too late"); + assertTrue(sfe.getMessage().contains(TOO_LATE_INDICATOR), + "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage()); } try { double d = fo.getRemainingProduction(query); - fail("Remaining Production without time range " + d); + fail("Exception expected instead of " + d); + } catch (SolarForecastException sfe) { + assertTrue(sfe.getMessage().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) { + assertTrue(sfe.getMessage().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) { - assertTrue(sfe.getMessage().contains("too late"), "too late"); + assertTrue(sfe.getMessage().contains(DAY_MISSING_INDICATOR), + "Expected: " + DAY_MISSING_INDICATOR + " Received: " + sfe.getMessage()); } - assertEquals(-1.0, fo.getActualPowerValue(query), TOLERANCE, "Remain out of scope"); - assertEquals(-1.0, fo.getDayTotal(query.toLocalDate()), TOLERANCE, "Actual out of scope"); // one minute earlier we reach a valid date query = query.minusMinutes(1); @@ -235,7 +278,7 @@ void testActions() { fo.getEnergy(queryDateTime.toInstant(), queryDateTime.plusDays(2).toInstant()); fail("Too early exception missing"); } catch (SolarForecastException sfe) { - assertTrue(sfe.getMessage().contains("too early"), "Too early exception"); + assertTrue(sfe.getMessage().contains("not available"), "not available expected: " + sfe.getMessage()); } try { fo.getDay(queryDateTime.toLocalDate(), "optimistic"); From 87c3ecf2da78193dcbee8bfb4b18f83ed1748eaf Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Mon, 22 Apr 2024 00:48:58 +0200 Subject: [PATCH 095/111] Solcast exception handling Signed-off-by: Bernd Weymann --- .../SolarForecastBindingConstants.java | 1 - .../forecastsolar/ForecastSolarObject.java | 8 +- .../handler/ForecastSolarBridgeHandler.java | 18 +++-- .../internal/solcast/SolcastObject.java | 47 ++++++++--- .../solcast/handler/SolcastBridgeHandler.java | 37 +++++---- .../solcast/handler/SolcastPlaneHandler.java | 62 +++++++-------- .../solarforecast/ForecastSolarTest.java | 1 - .../solarforecast/SolcastPlaneMock.java | 2 +- .../binding/solarforecast/SolcastTest.java | 78 ++++++++++++++----- 9 files changed, 163 insertions(+), 91 deletions(-) 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 index be4013b3a2fe0..7b4ba46b507ae 100644 --- 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 @@ -56,5 +56,4 @@ public class SolarForecastBindingConstants { 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/forecastsolar/ForecastSolarObject.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarObject.java index 7d44ceff42689..565874356b69e 100644 --- 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 @@ -53,15 +53,16 @@ public class ForecastSolarObject implements SolarForecast { private final TreeMap wattHourMap = new TreeMap<>(); private final TreeMap wattMap = new TreeMap<>(); private final DateTimeFormatter dateInputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - DateTimeFormatter dateOutputFormatter = DateTimeFormatter.ofPattern(SolarForecastBindingConstants.PATTERN_FORMAT) - .withZone(ZoneId.systemDefault()); + + 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(); + expirationDateTime = Instant.now().minusSeconds(1); identifier = id; } @@ -342,5 +343,4 @@ private void throwOutOfRangeException(Instant query) { public String getIdentifier() { return identifier; } - } 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 index f9d766b479dcb..57c55cdaf9c92 100644 --- 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 @@ -133,25 +133,29 @@ private synchronized void getData() { if (planes.isEmpty()) { return; } - try { - double energySum = 0; - double powerSum = 0; - double daySum = 0; - for (Iterator iterator = planes.iterator(); iterator.hasNext();) { + 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) { + logger.warn(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)); - } catch (SolarForecastException sfe) { - logger.warn(sfe.getMessage()); } } 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 index 626275cbedaee..c6b8455e527e5 100644 --- 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 @@ -16,6 +16,7 @@ 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; @@ -30,6 +31,8 @@ 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; @@ -54,6 +57,7 @@ public class SolcastObject implements SolarForecast { 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; @@ -81,13 +85,17 @@ public SolcastObject(String id, TimeZoneProvider tzp) { // invalid forecast object identifier = id; timeZoneProvider = tzp; - expirationDateTime = Instant.now(); + 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); } @@ -144,12 +152,8 @@ private void addJSONArray(JSONArray resultJsonArray) { } } - public boolean isValid() { - return !estimationDataMap.isEmpty(); - } - public boolean isExpired() { - return (expirationDateTime.isBefore(Instant.now())); + return expirationDateTime.isBefore(Instant.now()); } public double getActualEnergyValue(ZonedDateTime query, QueryMode mode) { @@ -158,7 +162,7 @@ public double getActualEnergyValue(ZonedDateTime query, QueryMode mode) { TreeMap dtm = getDataMap(mode); Entry nextEntry = dtm.higherEntry(iterationDateTime); if (nextEntry == null) { - return -1; + throwOutOfRangeException(query.toInstant()); } double forecastValue = 0; double previousEstimate = 0; @@ -220,7 +224,7 @@ public TimeSeries getEnergyTimeSeries(QueryMode mode) { */ public double getActualPowerValue(ZonedDateTime query, QueryMode mode) { if (query.toInstant().isBefore(getForecastBegin()) || query.toInstant().isAfter(getForecastEnd())) { - return -1; + throwOutOfRangeException(query.toInstant()); } TreeMap dtm = getDataMap(mode); double actualPowerValue = 0; @@ -273,7 +277,7 @@ public double getDayTotal(LocalDate query, QueryMode mode) { ZonedDateTime iterationDateTime = query.atStartOfDay(timeZoneProvider.getTimeZone()); Entry nextEntry = dtm.higherEntry(iterationDateTime); if (nextEntry == null) { - return -1; + throw new SolarForecastException(this, "Day " + query + " not available in forecast"); } ZonedDateTime endDateTime = iterationDateTime.plusDays(1); double forecastValue = 0; @@ -301,11 +305,11 @@ public double getRemainingProduction(ZonedDateTime query, QueryMode mode) { @Override public String toString() { - return "Expiration: " + expirationDateTime + ", Valid: " + isValid() + ", Data: " + estimationDataMap; + return "Expiration: " + expirationDateTime + ", Data: " + estimationDataMap; } public String getRaw() { - if (isValid() && rawData.isPresent()) { + if (rawData.isPresent()) { return rawData.get().toString(); } return "{}"; @@ -468,4 +472,25 @@ private QueryMode evalArguments(String[] args) { public String getIdentifier() { return identifier; } + + private void throwOutOfRangeException(Instant query) { + if (getForecastBegin() == Instant.MAX || getForecastEnd() == Instant.MIN) { + throw new SolarForecastException(this, "Forecast invalid time range"); + } + if (query.isBefore(getForecastBegin())) { + throw new SolarForecastException(this, + "Query " + dateOutputFormatter.format(query) + " too early. Valid range: " + + dateOutputFormatter.format(getForecastBegin()) + " - " + + dateOutputFormatter.format(getForecastEnd())); + } else if (query.isAfter(getForecastEnd())) { + throw new SolarForecastException(this, + "Query " + dateOutputFormatter.format(query) + " too late. Valid range: " + + dateOutputFormatter.format(getForecastBegin()) + " - " + + dateOutputFormatter.format(getForecastEnd())); + } else { + logger.warn("Query " + dateOutputFormatter.format(query) + " is fine in range: " + + dateOutputFormatter.format(getForecastBegin()) + " - " + + dateOutputFormatter.format(getForecastEnd()) + ". This shouldn't happen!"); + } + } } 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 index 83ff25dc8b6a0..64547f5a38131 100644 --- 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 @@ -28,6 +28,7 @@ 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; @@ -137,25 +138,33 @@ public synchronized void getData() { 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();) { - SolcastPlaneHandler sfph = iterator.next(); - SolcastObject fo = sfph.fetchData(); - energySum += fo.getActualEnergyValue(now, mode); - powerSum += fo.getActualPowerValue(now, mode); - daySum += fo.getDayTotal(now.toLocalDate(), mode); + 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) { + logger.warn(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)); } - 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)); }); } 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 index 9d6c5c17c7485..7edf966cdc098 100644 --- 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 @@ -85,8 +85,8 @@ public void initialize() { if (handler != null) { if (handler instanceof SolcastBridgeHandler sbh) { bridgeHandler = Optional.of(sbh); - bridgeHandler.get().addPlane(this); forecast = Optional.of(new SolcastObject(thing.getUID().getAsString(), bridgeHandler.get())); + bridgeHandler.get().addPlane(this); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/solarforecast.plane.status.wrong-handler [\"" + handler + "\"]"); @@ -113,36 +113,34 @@ public void dispose() { public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { forecast.ifPresent(forecastObject -> { - if (forecastObject.isValid()) { - String group = channelUID.getGroupId(); - 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); - } + String group = channelUID.getGroupId(); + 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); } }); } @@ -150,7 +148,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { protected synchronized SolcastObject fetchData() { forecast.ifPresent(forecastObject -> { - if (forecastObject.isExpired() || !forecastObject.isValid()) { + if (forecastObject.isExpired()) { logger.trace("Get new forecast {}", forecastObject.toString()); String forecastUrl = String.format(FORECAST_URL, configuration.get().resourceId); String currentEstimateUrl = String.format(CURRENT_ESTIMATE_URL, configuration.get().resourceId); 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 index 675135880b4c6..455c5824ae8ec 100644 --- 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 @@ -186,7 +186,6 @@ void testCornerCases() { double d = fo.getActualPowerValue(query); fail("Exception expected instead of " + d); } catch (SolarForecastException sfe) { - System.out.println(sfe.getMessage()); assertTrue(sfe.getMessage().contains(TOO_EARLY_INDICATOR), "Expected: " + TOO_EARLY_INDICATOR + " Received: " + sfe.getMessage()); } diff --git a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java index 684d3cdc88ab9..d7b42e92c26c3 100644 --- a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java @@ -52,7 +52,7 @@ public SolcastPlaneMock(BridgeImpl b) { @Override protected SolcastObject fetchData() { forecast.ifPresent(forecastObject -> { - if (forecastObject.isExpired() || !forecastObject.isValid()) { + 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()); 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 index 062de9db5fcee..a3882db9903f4 100644 --- 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 @@ -24,25 +24,22 @@ import java.util.Iterator; import java.util.List; -import javax.measure.quantity.Energy; import javax.measure.quantity.Power; 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.utils.Utils; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.Units; -import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.internal.BridgeImpl; -import org.openhab.core.types.RefreshType; import org.openhab.core.types.State; import org.openhab.core.types.TimeSeries; @@ -57,8 +54,9 @@ class SolcastTest { private static final TimeZP TIMEZONEPROVIDER = new TimeZP(); // double comparison tolerance = 1 Watt private static final double TOLERANCE = 0.001; - public static final QuantityType POWER_UNDEF = Utils.getPowerState(-1); - public static final QuantityType ENERGY_UNDEF = Utils.getEnergyState(-1); + + 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, @@ -260,7 +258,13 @@ 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); - assertEquals(-1.0, scfo.getActualEnergyValue(now, QueryMode.Average), 0.01, "Invalid"); + try { + double d = scfo.getActualEnergyValue(now, QueryMode.Average); + fail("Exception expected instead of " + d); + } catch (SolarForecastException sfe) { + assertTrue(sfe.getMessage().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"); @@ -275,7 +279,14 @@ 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); - assertEquals(-1.0, scfo.getActualEnergyValue(now, QueryMode.Average), 0.01, "Invalid"); + try { + double d = scfo.getActualEnergyValue(now, QueryMode.Average); + fail("Exception expected instead of " + d); + } catch (SolarForecastException sfe) { + assertTrue(sfe.getMessage().contains(TOO_LATE_INDICATOR), + "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage()); + } + content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); @@ -286,7 +297,7 @@ void testActions() { // 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 < 7; i++) { + 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()); @@ -348,7 +359,13 @@ void testOptimisticPessimistic() { } catch (IllegalArgumentException e) { assertEquals("Solcast doesn't support argument rubbish", e.getMessage(), "Rubbish argument"); } - assertEquals(POWER_UNDEF, scfo.getPower(past), "Normal Power"); + try { + scfo.getPower(past); + fail("Exception expected"); + } catch (SolarForecastException sfe) { + assertTrue(sfe.getMessage().contains(TOO_LATE_INDICATOR), + "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage()); + } } @Test @@ -356,13 +373,29 @@ 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); - assertEquals(-1.0, scfo.getActualEnergyValue(now, QueryMode.Average), 0.01, "Data available - day not in"); + try { + double d = scfo.getActualEnergyValue(now, QueryMode.Average); + fail("Exception expected instead of " + d); + } catch (SolarForecastException sfe) { + assertTrue(sfe.getMessage().contains(TOO_LATE_INDICATOR), + "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage()); + } content = FileReader.readFileInString("src/test/resources/solcast/forecasts.json"); scfo.join(content); - assertEquals(-1.0, scfo.getActualEnergyValue(now, QueryMode.Average), 0.01, - "Data available after merge - day not in"); - assertEquals(-1.0, scfo.getDayTotal(now.toLocalDate(), QueryMode.Average), 0.01, - "Data available after merge - day not in"); + try { + double d = scfo.getActualEnergyValue(now, QueryMode.Average); + fail("Exception expected instead of " + d); + } catch (SolarForecastException sfe) { + assertTrue(sfe.getMessage().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) { + assertTrue(sfe.getMessage().contains(DAY_MISSING_INDICATOR), + "Expected: " + DAY_MISSING_INDICATOR + " Received: " + sfe.getMessage()); + } } @Test @@ -562,7 +595,7 @@ void testCombinedPowerTimeSeries() { 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()) { @@ -573,6 +606,9 @@ void testCombinedPowerTimeSeries() { assertEquals(((QuantityType) e1.state()).doubleValue(), ((QuantityType) e2.state()).doubleValue() * 2, 0.1, "Power Value"); } + scbh.dispose(); + scph1.dispose(); + scph2.dispose(); } @Test @@ -599,6 +635,7 @@ void testCombinedEnergyTimeSeries() { 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(); @@ -610,6 +647,9 @@ void testCombinedEnergyTimeSeries() { assertEquals(((QuantityType) e1.state()).doubleValue(), ((QuantityType) e2.state()).doubleValue() * 2, 0.1, "Power Value"); } + scbh.dispose(); + scph1.dispose(); + scph2.dispose(); } @Test @@ -621,7 +661,9 @@ void testSingleEnergyTimeSeries() { scbh.setCallback(cm); SolcastPlaneHandler scph1 = new SolcastPlaneMock(bi); + CallbackMock cm1 = new CallbackMock(); scph1.initialize(); + scph1.setCallback(cm1); // simulate trigger of refresh job scbh.getData(); @@ -633,9 +675,5 @@ void testSingleEnergyTimeSeries() { TimeSeries.Entry e1 = iter1.next(); assertEquals("kWh", ((QuantityType) e1.state()).getUnit().toString(), "Power Unit"); } - - // simulate item refresh - scbh.handleCommand(new ChannelUID("a:b:c:" + SolarForecastBindingConstants.CHANNEL_POWER_ACTUAL), - RefreshType.REFRESH); } } From 10c2d37df44c9cfd611b39cbd8a7e042ca015294 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Mon, 22 Apr 2024 01:02:05 +0200 Subject: [PATCH 096/111] fix checkstyle Signed-off-by: Bernd Weymann --- .../internal/SolarForecastException.java | 11 ----------- .../internal/forecastsolar/ForecastSolarObject.java | 7 +++---- .../handler/ForecastSolarBridgeHandler.java | 2 +- .../solarforecast/internal/solcast/SolcastObject.java | 7 +++---- .../solcast/handler/SolcastBridgeHandler.java | 2 +- 5 files changed, 8 insertions(+), 21 deletions(-) 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 index bad9fe1674c7d..06c8c856cf16f 100644 --- 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 @@ -12,7 +12,6 @@ */ package org.openhab.binding.solarforecast.internal; -import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.solarforecast.internal.actions.SolarForecast; @@ -28,14 +27,4 @@ public class SolarForecastException extends RuntimeException { public SolarForecastException(SolarForecast ref, String message) { super(ref.getIdentifier() + " # " + message); } - - @Override - public @NonNull String getMessage() { - String message = super.getMessage(); - if (message == null) { - return SolarForecastBindingConstants.EMPTY; - } else { - return message; - } - } } 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 index 565874356b69e..0973b14ce2546 100644 --- 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 @@ -319,7 +319,7 @@ public Instant getForecastEnd() { } private void throwOutOfRangeException(Instant query) { - if (getForecastBegin() == Instant.MAX || getForecastEnd() == Instant.MIN) { + if (getForecastBegin().equals(Instant.MAX) || getForecastEnd().equals(Instant.MIN)) { throw new SolarForecastException(this, "Forecast invalid time range"); } if (query.isBefore(getForecastBegin())) { @@ -333,9 +333,8 @@ private void throwOutOfRangeException(Instant query) { + dateOutputFormatter.format(getForecastBegin()) + " - " + dateOutputFormatter.format(getForecastEnd())); } else { - logger.warn("Query " + dateOutputFormatter.format(query) + " is fine in range: " - + dateOutputFormatter.format(getForecastBegin()) + " - " - + dateOutputFormatter.format(getForecastEnd()) + ". This shouldn't happen!"); + logger.warn("Query {} is fine in range: {} - {}. This shouldn't happen!", dateOutputFormatter.format(query), + dateOutputFormatter.format(getForecastBegin()), dateOutputFormatter.format(getForecastEnd())); } } 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 index 57c55cdaf9c92..01e76f3331316 100644 --- 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 @@ -146,7 +146,7 @@ private synchronized void getData() { powerSum += fo.getActualPowerValue(now); daySum += fo.getDayTotal(now.toLocalDate()); } catch (SolarForecastException sfe) { - logger.warn(sfe.getMessage()); + logger.warn("{}", sfe.getMessage()); update = false; } } 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 index c6b8455e527e5..0d7544155235e 100644 --- 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 @@ -474,7 +474,7 @@ public String getIdentifier() { } private void throwOutOfRangeException(Instant query) { - if (getForecastBegin() == Instant.MAX || getForecastEnd() == Instant.MIN) { + if (getForecastBegin().equals(Instant.MAX) || getForecastEnd().equals(Instant.MIN)) { throw new SolarForecastException(this, "Forecast invalid time range"); } if (query.isBefore(getForecastBegin())) { @@ -488,9 +488,8 @@ private void throwOutOfRangeException(Instant query) { + dateOutputFormatter.format(getForecastBegin()) + " - " + dateOutputFormatter.format(getForecastEnd())); } else { - logger.warn("Query " + dateOutputFormatter.format(query) + " is fine in range: " - + dateOutputFormatter.format(getForecastBegin()) + " - " - + dateOutputFormatter.format(getForecastEnd()) + ". This shouldn't happen!"); + logger.warn("Query {} is fine in range: {} - {}. This shouldn't happen!", dateOutputFormatter.format(query), + dateOutputFormatter.format(getForecastBegin()), dateOutputFormatter.format(getForecastEnd())); } } } 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 index 64547f5a38131..b8bfa8b6ec56f 100644 --- 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 @@ -150,7 +150,7 @@ public synchronized void getData() { powerSum += fo.getActualPowerValue(now, mode); daySum += fo.getDayTotal(now.toLocalDate(), mode); } catch (SolarForecastException sfe) { - logger.warn(sfe.getMessage()); + logger.warn("{}", sfe.getMessage()); update = false; } } From da56d395f4a18420c5b0a34466873206be4ded7a Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Mon, 22 Apr 2024 13:18:39 +0200 Subject: [PATCH 097/111] readme adaptions Signed-off-by: Bernd Weymann --- bundles/org.openhab.binding.solarforecast/README.md | 12 +++++++++--- .../handler/ForecastSolarBridgeHandler.java | 2 +- .../solcast/handler/SolcastBridgeHandler.java | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index d7d350146f253..6cb7250348c0e 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -186,7 +186,9 @@ Returns `Instant` of the latest possible forecast data available. Returns `QuantityType` at the given `Instant` timestamp. Respect `getForecastBegin` and `getForecastEnd` to get a valid value. -Check for negative value in case of errors. +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` @@ -197,7 +199,9 @@ Check for negative value in case of errors. Returns `QuantityType` at the given `localDate`. Respect `getForecastBegin` and `getForecastEnd` to avoid ambiguous values. -Check for negative value in case of errors. +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` @@ -209,7 +213,9 @@ Check for negative value in case of errors. Returns `QuantityType` between the timestamps `startTimestamp` and `endTimestamp`. Respect `getForecastBegin` and `getForecastEnd` to avoid ambiguous values. -Check for negative value in case of errors. +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 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 index 01e76f3331316..bd709d694ace0 100644 --- 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 @@ -146,7 +146,7 @@ private synchronized void getData() { powerSum += fo.getActualPowerValue(now); daySum += fo.getDayTotal(now.toLocalDate()); } catch (SolarForecastException sfe) { - logger.warn("{}", sfe.getMessage()); + logger.info("{}", sfe.getMessage()); update = false; } } 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 index b8bfa8b6ec56f..44b5e34e7e280 100644 --- 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 @@ -150,7 +150,7 @@ public synchronized void getData() { powerSum += fo.getActualPowerValue(now, mode); daySum += fo.getDayTotal(now.toLocalDate(), mode); } catch (SolarForecastException sfe) { - logger.warn("{}", sfe.getMessage()); + logger.info("{}", sfe.getMessage()); update = false; } } From 278b735ed293da4ddcae81b3a38072689c74c81f Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Mon, 22 Apr 2024 18:14:26 +0200 Subject: [PATCH 098/111] bugfixing after testing Signed-off-by: Bernd Weymann --- .../README.md | 19 ++++++++++++------- .../forecastsolar/ForecastSolarObject.java | 19 ++++++++++--------- .../internal/solcast/SolcastConstants.java | 1 - .../internal/solcast/SolcastObject.java | 18 +++++++++--------- .../solcast/handler/SolcastBridgeHandler.java | 2 +- .../resources/OH-INF/thing/fs-plane-type.xml | 2 +- 6 files changed, 33 insertions(+), 28 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 6cb7250348c0e..50286fb45afe8 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -186,9 +186,11 @@ Returns `Instant` of the latest possible forecast data available. 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 + +- `IllegalArgumentException` thrown in case of problems with call arguments +- `SolarForecastException` thrown in case of problems with timestamp and available forecast data ### `getDay` @@ -199,9 +201,11 @@ Check log or catch exceptions for error handling 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 + +- `IllegalArgumentException` thrown in case of problems with call arguments +- `SolarForecastException` thrown in case of problems with timestamp and available forecast data ### `getEnergy` @@ -213,9 +217,11 @@ Check log or catch exceptions for error handling 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 + +- `IllegalArgumentException` thrown in case of problems with call arguments +- `SolarForecastException` thrown in case of problems with timestamp and available forecast data ## Date Time @@ -330,6 +336,5 @@ rule "Solcast Actions" 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/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 index 0973b14ce2546..824266101d3bc 100644 --- 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 @@ -224,7 +224,8 @@ public double getDayTotal(LocalDate queryDate) { if (wattsDay.has(queryDate.toString())) { return wattsDay.getDouble(queryDate.toString()) / 1000.0; } else { - throw new SolarForecastException(this, "Day " + queryDate + " not available in forecast"); + throw new SolarForecastException(this, + "Day " + queryDate + " not available in forecast. " + getTimeRange()); } } @@ -324,20 +325,20 @@ private void throwOutOfRangeException(Instant query) { } if (query.isBefore(getForecastBegin())) { throw new SolarForecastException(this, - "Query " + dateOutputFormatter.format(query) + " too early. Valid range: " - + dateOutputFormatter.format(getForecastBegin()) + " - " - + dateOutputFormatter.format(getForecastEnd())); + "Query " + dateOutputFormatter.format(query) + " too early. " + getTimeRange()); } else if (query.isAfter(getForecastEnd())) { throw new SolarForecastException(this, - "Query " + dateOutputFormatter.format(query) + " too late. Valid range: " - + dateOutputFormatter.format(getForecastBegin()) + " - " - + dateOutputFormatter.format(getForecastEnd())); + "Query " + dateOutputFormatter.format(query) + " too late. " + getTimeRange()); } else { - logger.warn("Query {} is fine in range: {} - {}. This shouldn't happen!", dateOutputFormatter.format(query), - dateOutputFormatter.format(getForecastBegin()), dateOutputFormatter.format(getForecastEnd())); + 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/solcast/SolcastConstants.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastConstants.java index b485c5448b759..f55b807eb9dc0 100644 --- 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 @@ -29,7 +29,6 @@ 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 MEASUREMENT_URL = BASE_URL + "%s/measurements?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 index 0d7544155235e..a0b151a93afba 100644 --- 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 @@ -277,7 +277,7 @@ public double getDayTotal(LocalDate query, QueryMode 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"); + throw new SolarForecastException(this, "Day " + query + " not available in forecast. " + getTimeRange()); } ZonedDateTime endDateTime = iterationDateTime.plusDays(1); double forecastValue = 0; @@ -479,17 +479,17 @@ private void throwOutOfRangeException(Instant query) { } if (query.isBefore(getForecastBegin())) { throw new SolarForecastException(this, - "Query " + dateOutputFormatter.format(query) + " too early. Valid range: " - + dateOutputFormatter.format(getForecastBegin()) + " - " - + dateOutputFormatter.format(getForecastEnd())); + "Query " + dateOutputFormatter.format(query) + " too early. " + getTimeRange()); } else if (query.isAfter(getForecastEnd())) { throw new SolarForecastException(this, - "Query " + dateOutputFormatter.format(query) + " too late. Valid range: " - + dateOutputFormatter.format(getForecastBegin()) + " - " - + dateOutputFormatter.format(getForecastEnd())); + "Query " + dateOutputFormatter.format(query) + " too late. " + getTimeRange()); } else { - logger.warn("Query {} is fine in range: {} - {}. This shouldn't happen!", dateOutputFormatter.format(query), - dateOutputFormatter.format(getForecastBegin()), dateOutputFormatter.format(getForecastEnd())); + 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/handler/SolcastBridgeHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastBridgeHandler.java index 44b5e34e7e280..eec96597d8917 100644 --- 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 @@ -77,7 +77,7 @@ public Collection> getServices() { @Override public void initialize() { - SolcastBridgeConfiguration configuration = getConfigAs(SolcastBridgeConfiguration.class); + configuration = getConfigAs(SolcastBridgeConfiguration.class); if (!configuration.apiKey.isBlank()) { if (!configuration.timeZone.isBlank()) { try { 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 index ecc51319b5d0e..f37bd94d41b7c 100644 --- 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 @@ -19,7 +19,7 @@ - + From 9d0ef6f5a16dbde3e3c2e82e2f458a0b3b8402ee Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Mon, 22 Apr 2024 18:22:36 +0200 Subject: [PATCH 099/111] bugfixing after testing Signed-off-by: Bernd Weymann --- .../internal/solcast/handler/SolcastPlaneHandler.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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 index 7edf966cdc098..64581471cc26d 100644 --- 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 @@ -59,7 +59,7 @@ public class SolcastPlaneHandler extends BaseThingHandler implements SolarForecastProvider { private final Logger logger = LoggerFactory.getLogger(SolcastPlaneHandler.class); private final HttpClient httpClient; - private Optional configuration = Optional.empty(); + private SolcastPlaneConfiguration configuration = new SolcastPlaneConfiguration(); private Optional bridgeHandler = Optional.empty(); protected Optional forecast = Optional.empty(); @@ -75,8 +75,7 @@ public Collection> getServices() { @Override public void initialize() { - SolcastPlaneConfiguration c = getConfigAs(SolcastPlaneConfiguration.class); - configuration = Optional.of(c); + configuration = getConfigAs(SolcastPlaneConfiguration.class); // connect Bridge & Status Bridge bridge = getBridge(); @@ -150,8 +149,8 @@ protected synchronized SolcastObject fetchData() { forecast.ifPresent(forecastObject -> { if (forecastObject.isExpired()) { logger.trace("Get new forecast {}", forecastObject.toString()); - String forecastUrl = String.format(FORECAST_URL, configuration.get().resourceId); - String currentEstimateUrl = String.format(CURRENT_ESTIMATE_URL, configuration.get().resourceId); + 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); @@ -160,7 +159,7 @@ protected synchronized SolcastObject fetchData() { if (crEstimate.getStatus() == 200) { SolcastObject localForecast = new SolcastObject(thing.getUID().getAsString(), crEstimate.getContentAsString(), - Instant.now().plus(configuration.get().refreshInterval, ChronoUnit.MINUTES), + Instant.now().plus(configuration.refreshInterval, ChronoUnit.MINUTES), bridgeHandler.get()); // get forecast From 7bb83b46a1b25a49df21631f463925858f513a9b Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Mon, 22 Apr 2024 22:31:27 +0200 Subject: [PATCH 100/111] readme json channel correction Signed-off-by: Bernd Weymann --- bundles/org.openhab.binding.solarforecast/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index 50286fb45afe8..c5e7f0f150e5a 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -67,7 +67,7 @@ If you have 25 free calls per day, each plane needs 2 calls per update a refresh ## Solcast Channels -Each `sc-plane` reports its own values including a `raw` channel holding JSON content. +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. @@ -86,7 +86,7 @@ Scenarios are clustered in groups: | 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 | -| raw | String | - | Plain JSON response without conversions | yes | +| json | String | - | Plain JSON response without conversions | yes | ## ForecastSolar Configuration @@ -138,7 +138,7 @@ So you need to know what you're doing. ## ForecastSolar Channels -Each `fs-plane` reports its own values including a `raw` channel holding JSON content. +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. @@ -153,7 +153,7 @@ Forecasts are delivered up to 3 days for paid personal plans. | 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 | -| raw | String | - | Plain JSON response without conversions | yes | +| json | String | - | Plain JSON response without conversions | yes | ## Thing Actions From 75de2f1a48e7bb17319f3fe430acf1663a486209 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Fri, 26 Apr 2024 13:36:41 +0200 Subject: [PATCH 101/111] bugfix: action call with average parameter Signed-off-by: Bernd Weymann --- .../binding/solarforecast/internal/solcast/SolcastObject.java | 2 ++ .../java/org/openhab/binding/solarforecast/SolcastTest.java | 2 ++ 2 files changed, 4 insertions(+) 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 index a0b151a93afba..a316f65451d5d 100644 --- 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 @@ -459,6 +459,8 @@ private QueryMode evalArguments(String[] args) { 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; 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 index a3882db9903f4..12896484d08e3 100644 --- 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 @@ -161,6 +161,8 @@ void testPower() { * "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)); From 8b8d940147d22cbca309f8cc132c19b61ee9a57e Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sat, 27 Apr 2024 01:51:48 +0200 Subject: [PATCH 102/111] adaptions from review comments Signed-off-by: Bernd Weymann --- .../README.md | 22 ++- .../handler/ForecastSolarBridgeHandler.java | 3 +- .../handler/ForecastSolarPlaneHandler.java | 4 +- .../solcast/handler/SolcastBridgeHandler.java | 3 +- .../solcast/handler/SolcastPlaneHandler.java | 142 ++++++++---------- .../OH-INF/i18n/solarforecast.properties | 19 +-- .../binding/solarforecast/FileReader.java | 2 +- 7 files changed, 93 insertions(+), 102 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index c5e7f0f150e5a..1de5035db8c81 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -50,7 +50,7 @@ Correct time zone is necessary to show correct forecast times in UI. `apiKey` can be obtained in your [Account Settings](https://toolkit.solcast.com.au/account) -`timeZone` leave it empty to evaluate Regional Settings of your openHAB installation. +`timeZone` can be left empty to evaluate Regional Settings of your openHAB installation. See [DateTime](#date-time) section for more information. ### Solcast Plane Configuration @@ -144,7 +144,6 @@ The `fs-site` bridge sums up all attached `fs-plane` values and provides the tot 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 | @@ -155,7 +154,6 @@ Forecasts are delivered up to 3 days for paid personal plans. | 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. @@ -311,6 +309,24 @@ rule "Tomorrow Forecast Calculation" 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 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 index bd709d694ace0..e9d7524a442f1 100644 --- 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 @@ -146,7 +146,8 @@ private synchronized void getData() { powerSum += fo.getActualPowerValue(now); daySum += fo.getDayTotal(now.toLocalDate()); } catch (SolarForecastException sfe) { - logger.info("{}", sfe.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, + "@text/solarforecast.site.status.exception [\"" + sfe.getMessage() + "\"]"); update = false; } } 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 index 5d38797774141..795422c60c463 100644 --- 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 @@ -153,8 +153,8 @@ protected ForecastSolarObject fetchData() { updateState(CHANNEL_JSON, StringType.valueOf(cr.getContentAsString())); setForecast(localForecast); } catch (SolarForecastException fse) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "@text/solarforecast.plane.status.json-status"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, + "@text/solarforecast.plane.status.json-status [\"" + fse.getMessage() + "\"]"); } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, 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 index eec96597d8917..b4b9803f4dc53 100644 --- 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 @@ -150,7 +150,8 @@ public synchronized void getData() { powerSum += fo.getActualPowerValue(now, mode); daySum += fo.getDayTotal(now.toLocalDate(), mode); } catch (SolarForecastException sfe) { - logger.info("{}", sfe.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, + "@text/solarforecast.site.status.exception [\"" + sfe.getMessage() + "\"]"); update = false; } } 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 index 64581471cc26d..70b2ddc1afcc9 100644 --- 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 @@ -103,9 +103,7 @@ public void initialize() { @Override public void dispose() { super.dispose(); - if (bridgeHandler.isPresent()) { - bridgeHandler.get().removePlane(this); - } + bridgeHandler.ifPresent(bridge -> bridge.removePlane(this)); } @Override @@ -146,98 +144,84 @@ public void handleCommand(ChannelUID channelUID, Command command) { } protected synchronized SolcastObject fetchData() { - 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 + bridgeHandler.get().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), - bridgeHandler.get()); - - // get forecast - Request forecastRequest = httpClient.newRequest(forecastUrl); - forecastRequest.header(HttpHeader.AUTHORIZATION, BEARER + bridgeHandler.get().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); + 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(), forecastUrl, crForecast.getStatus()); + logger.debug("{} Call {} failed {}", thing.getLabel(), currentEstimateUrl, + crEstimate.getStatus()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "@text/solarforecast.plane.status.http-status [\"" + crForecast.getStatus() + "@text/solarforecast.plane.status.http-status [\"" + crEstimate.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(); } - } 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); } - } 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); - switch (mode) { - case Average: - updateState(GROUP_AVERAGE + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_ACTUAL, - Utils.getEnergyState(energyProduced)); - updateState(GROUP_AVERAGE + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_REMAIN, - Utils.getEnergyState(energyDay - energyProduced)); - updateState(GROUP_AVERAGE + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_TODAY, - Utils.getEnergyState(energyDay)); - updateState(GROUP_AVERAGE + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_POWER_ACTUAL, - Utils.getPowerState(f.getActualPowerValue(now, QueryMode.Average))); - break; - case Optimistic: - updateState(GROUP_OPTIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_ACTUAL, - Utils.getEnergyState(energyProduced)); - updateState(GROUP_OPTIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_REMAIN, - Utils.getEnergyState(energyDay - energyProduced)); - updateState(GROUP_OPTIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_TODAY, - Utils.getEnergyState(energyDay)); - updateState(GROUP_OPTIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_POWER_ACTUAL, - Utils.getPowerState(f.getActualPowerValue(now, QueryMode.Average))); - break; - case Pessimistic: - updateState(GROUP_PESSIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_ACTUAL, - Utils.getEnergyState(energyProduced)); - updateState(GROUP_PESSIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_REMAIN, - Utils.getEnergyState(energyDay - energyProduced)); - updateState(GROUP_PESSIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_TODAY, - Utils.getEnergyState(energyDay)); - updateState(GROUP_PESSIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_POWER_ACTUAL, - Utils.getPowerState(f.getActualPowerValue(now, QueryMode.Average))); - break; - default: - break; - } + 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))); }); } 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 index 4b8e582410541..a2fe132236b2f 100644 --- 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 @@ -73,38 +73,27 @@ channel-type.solarforecast.power-actual.description = Power prediction for this channel-type.solarforecast.power-estimate.label = Power Forecast channel-type.solarforecast.power-estimate.description = Power forecast for next hours/days -# channel types - -channel-type.solarforecast.energy-estimate10-channel.label = Pessimistic Energy Forecast -channel-type.solarforecast.energy-estimate10-channel.description = Pessimistic energy forecast for next hours/days -channel-type.solarforecast.energy-estimate90-channel.label = Optimistic Energy Forecast -channel-type.solarforecast.energy-estimate90-channel.description = Optimistic energy forecast for next hours/days -channel-type.solarforecast.power-estimate10-channel.label = Pessimistic Power Forecast -channel-type.solarforecast.power-estimate10-channel.description = Pessimistic power forecast for next hours/days -channel-type.solarforecast.power-estimate90-channel.label = Optimistic Power Forecast -channel-type.solarforecast.power-estimate90-channel.description = Optimistic 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, check raw channel - +solarforecast.plane.status.json-status = JSON error: {0} # thing actions actionDayLabel = Daily Energy Production -actionDayDesc = Returns energy production for complete day in kw/h +actionDayDesc = Returns energy production for complete day in kWh actionInputDayLabel = Date actionInputDayDesc = LocalDate for daily energy query actionPowerLabel = Power -actionPowerDesc = Returns power in kw for a specific point in time +actionPowerDesc = Returns power in W for a specific point in time actionInputDateTimeLabel = Date Time actionInputDateTimeDesc = Instant timestamp for power query actionEnergyLabel = Energy Production 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 index 7aa82c2e9738b..1ae51267989fd 100644 --- 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 @@ -41,7 +41,7 @@ public static String readFileInString(String filename) { return buf.toString(); } catch (IOException e) { // fail if file cannot be read - assertFalse(filename.isBlank(), "Read failute " + filename); + assertFalse(filename.isBlank(), "Read failure " + filename); } return SolarForecastBindingConstants.EMPTY; } From a5bbe4b3a2fb3caedffe0a682263f099a45f0c80 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sun, 28 Apr 2024 17:16:41 +0200 Subject: [PATCH 103/111] remove instance check Signed-off-by: Bernd Weymann --- .../actions/SolarForecastActions.java | 24 +++++++++---------- .../solcast/handler/SolcastPlaneHandler.java | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) 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 index 73917b2df8bb9..e3517ffc682a2 100644 --- 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 @@ -57,12 +57,12 @@ public QuantityType getDay( QuantityType measure = QuantityType.valueOf(0, Units.KILOWATT_HOUR); for (Iterator iterator = l.iterator(); iterator.hasNext();) { SolarForecast solarForecast = iterator.next(); - State s = solarForecast.getDay(localDate, args); - if (s instanceof QuantityType quantityState) { - measure = measure.add((QuantityType) quantityState); + 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 {}", s, localDate); + logger.debug("Ambiguous measure {} found for {}", qt, localDate); return Utils.getEnergyState(-1); } } @@ -87,12 +87,12 @@ public QuantityType getPower( QuantityType measure = QuantityType.valueOf(0, MetricPrefix.KILO(Units.WATT)); for (Iterator iterator = l.iterator(); iterator.hasNext();) { SolarForecast solarForecast = iterator.next(); - State s = solarForecast.getPower(timestamp, args); - if (s instanceof QuantityType quantityState) { - measure = measure.add((QuantityType) quantityState); + 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 {}", s, timestamp); + logger.debug("Ambiguous measure {} found for {}", qt, timestamp); return Utils.getPowerState(-1); } } @@ -118,12 +118,12 @@ public QuantityType getEnergy( QuantityType measure = QuantityType.valueOf(0, Units.KILOWATT_HOUR); for (Iterator iterator = l.iterator(); iterator.hasNext();) { SolarForecast solarForecast = iterator.next(); - State s = solarForecast.getEnergy(start, end, args); - if (s instanceof QuantityType quantityState) { - measure = measure.add((QuantityType) quantityState); + 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 {}", s, start, end); + logger.debug("Ambiguous measure {} found between {} and {}", qt, start, end); return Utils.getEnergyState(-1); } } 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 index 70b2ddc1afcc9..77cff32f8e420 100644 --- 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 @@ -110,7 +110,7 @@ public void dispose() { public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { forecast.ifPresent(forecastObject -> { - String group = channelUID.getGroupId(); + String group = (channelUID.getGroupId() != null) ? channelUID.getGroupId() : ""; String channel = channelUID.getIdWithoutGroup(); QueryMode mode = QueryMode.Average; switch (group) { From 20c6452330002942b2a81e917917e7144abf88ee Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sun, 28 Apr 2024 17:23:06 +0200 Subject: [PATCH 104/111] remove unused logger Signed-off-by: Bernd Weymann --- .../forecastsolar/handler/ForecastSolarBridgeHandler.java | 3 --- 1 file changed, 3 deletions(-) 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 index e9d7524a442f1..893a8a1476908 100644 --- 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 @@ -47,8 +47,6 @@ 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 ForecastSolarBridgeHandler} is a non active handler instance. It will be triggerer by the bridge. @@ -57,7 +55,6 @@ */ @NonNullByDefault public class ForecastSolarBridgeHandler extends BaseBridgeHandler implements SolarForecastProvider { - private final Logger logger = LoggerFactory.getLogger(ForecastSolarBridgeHandler.class); private List planes = new ArrayList<>(); private Optional homeLocation; private Optional configuration = Optional.empty(); From 9ec9aeea8e8e99a8209859f2ed78c96e4c19a817 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Mon, 29 Apr 2024 01:13:01 +0200 Subject: [PATCH 105/111] fix review comments Signed-off-by: Bernd Weymann --- .../internal/forecastsolar/ForecastSolarObject.java | 1 - .../forecastsolar/handler/ForecastSolarPlaneHandler.java | 2 ++ .../internal/solcast/handler/SolcastPlaneHandler.java | 7 +++++-- .../org/openhab/binding/solarforecast/CallbackMock.java | 6 ++---- 4 files changed, 9 insertions(+), 7 deletions(-) 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 index 824266101d3bc..de15a50c06369 100644 --- 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 @@ -96,7 +96,6 @@ public ForecastSolarObject(String id, String content, Instant expirationDate) th } } } catch (JSONException je) { - logger.debug("Error parsing JSON response {} - {}", content, je.getMessage()); throw new SolarForecastException(this, "Error parsing JSON response " + content + " Reason: " + je.getMessage()); } 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 index 795422c60c463..a15f617fdc3c5 100644 --- 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 @@ -157,6 +157,8 @@ protected ForecastSolarObject fetchData() { "@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() + "\"]"); } 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 index 77cff32f8e420..6678bd2109a53 100644 --- 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 @@ -85,7 +85,7 @@ public void initialize() { if (handler instanceof SolcastBridgeHandler sbh) { bridgeHandler = Optional.of(sbh); forecast = Optional.of(new SolcastObject(thing.getUID().getAsString(), bridgeHandler.get())); - bridgeHandler.get().addPlane(this); + sbh.addPlane(this); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/solarforecast.plane.status.wrong-handler [\"" + handler + "\"]"); @@ -110,7 +110,10 @@ public void dispose() { public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { forecast.ifPresent(forecastObject -> { - String group = (channelUID.getGroupId() != null) ? channelUID.getGroupId() : ""; + String group = channelUID.getGroupId(); + if (group == null) { + group = EMPTY; + } String channel = channelUID.getIdWithoutGroup(); QueryMode mode = QueryMode.Average; switch (group) { 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 index 10f665942f7f0..2b8568c61aae6 100644 --- 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 @@ -78,13 +78,11 @@ public void thingUpdated(Thing thing) { } @Override - public void validateConfigurationParameters(Thing thing, - Map<@NonNull String, @NonNull Object> configurationParameters) { + public void validateConfigurationParameters(Thing thing, Map configurationParameters) { } @Override - public void validateConfigurationParameters(Channel channel, - Map<@NonNull String, @NonNull Object> configurationParameters) { + public void validateConfigurationParameters(Channel channel, Map configurationParameters) { } @Override From e6f32535d1277b294465298ceb24c39084960634 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 30 Apr 2024 16:15:49 +0200 Subject: [PATCH 106/111] bugfix persistence: common start and end time Signed-off-by: Bernd Weymann --- .../actions/SolarForecastActions.java | 48 ++--------- .../handler/ForecastSolarBridgeHandler.java | 15 +++- .../solcast/handler/SolcastBridgeHandler.java | 15 +++- .../solarforecast/internal/utils/Utils.java | 50 +++++++++++- .../solarforecast/ForecastSolarPlaneMock.java | 4 + .../solarforecast/ForecastSolarTest.java | 79 ++++++++++++++++++- .../solarforecast/SolcastPlaneMock.java | 1 - .../binding/solarforecast/SolcastTest.java | 6 +- 8 files changed, 165 insertions(+), 53 deletions(-) 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 index e3517ffc682a2..c794ebb1c3d03 100644 --- 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 @@ -140,59 +140,23 @@ public QuantityType getEnergy( @RuleAction(label = "@text/actionForecastBeginLabel", description = "@text/actionForecastBeginDesc") public Instant getForecastBegin() { - Instant returnBeginTime = Instant.MAX; if (thingHandler.isPresent()) { - List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); - if (!l.isEmpty()) { - for (Iterator iterator = l.iterator(); iterator.hasNext();) { - SolarForecast solarForecast = iterator.next(); - Instant begin = solarForecast.getForecastBegin(); - // break in case of failure getting values to avoid ambiguous values - if (begin.equals(Instant.MAX)) { - return Instant.MAX; - } - // take latest possible timestamp to avoid ambiguous values - if (begin.isBefore(returnBeginTime)) { - returnBeginTime = begin; - } - } - return returnBeginTime; - } else { - logger.trace("No forecasts found - return invalid date MAX"); - return returnBeginTime; - } + List forecastObjectList = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); + return Utils.getCommonStartTime(forecastObjectList); } else { logger.trace("Handler missing - return invalid date MAX"); - return returnBeginTime; + return Instant.MAX; } } @RuleAction(label = "@text/actionForecastEndLabel", description = "@text/actionForecastEndDesc") public Instant getForecastEnd() { - Instant returnEndTime = Instant.MIN; if (thingHandler.isPresent()) { - List l = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); - if (!l.isEmpty()) { - for (Iterator iterator = l.iterator(); iterator.hasNext();) { - SolarForecast solarForecast = iterator.next(); - Instant forecastEndTime = solarForecast.getForecastEnd(); - // break in case of failure getting values to avoid ambiguous values - if (forecastEndTime.equals(Instant.MIN)) { - return Instant.MIN; - } - // take earliest possible timestamp to avoid ambiguous values - if (forecastEndTime.isAfter(returnEndTime)) { - returnEndTime = forecastEndTime; - } - } - return returnEndTime; - } else { - logger.trace("No forecasts found - return invalid date MIN"); - return returnEndTime; - } + List forecastObjectList = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts(); + return Utils.getCommonEndTime(forecastObjectList); } else { logger.trace("Handler missing - return invalid date MIN"); - return returnEndTime; + return Instant.MIN; } } 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 index 893a8a1476908..487d93e86dd8b 100644 --- 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 @@ -168,14 +168,25 @@ public synchronized void forecastUpdate() { 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 -> { - Utils.addState(combinedPowerForecast, 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 -> { - Utils.addState(combinedEnergyForecast, entry); + if (Utils.isAfterOrEqual(entry.timestamp(), commonStart) + && Utils.isBeforeOrEqual(entry.timestamp(), commonEnd)) { + Utils.addState(combinedEnergyForecast, entry); + } }); }); 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 index b4b9803f4dc53..de5fe18eab238 100644 --- 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 @@ -184,14 +184,25 @@ public synchronized void forecastUpdate() { 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 -> { - Utils.addState(combinedPowerForecast, 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 -> { - Utils.addState(combinedEnergyForecast, entry); + if (Utils.isAfterOrEqual(entry.timestamp(), commonStart) + && Utils.isBeforeOrEqual(entry.timestamp(), commonEnd)) { + Utils.addState(combinedEnergyForecast, entry); + } }); }); // create TimeSeries and distribute 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 index 52d3693f17057..d819fef6998ce 100644 --- 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 @@ -13,6 +13,8 @@ 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; @@ -20,6 +22,7 @@ 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; @@ -50,9 +53,54 @@ public static void addState(TreeMap> map, Entry entry) QuantityType qt1 = map.get(timestamp); if (qt1 != null) { QuantityType qt2 = (QuantityType) entry.state(); - map.put(timestamp, QuantityType.valueOf(qt1.doubleValue() + qt2.doubleValue(), qt2.getUnit())); + 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.isBefore(reference) || query.equals(reference); + } + + public static boolean isAfterOrEqual(Instant query, Instant reference) { + return query.isAfter(reference) || query.equals(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/test/java/org/openhab/binding/solarforecast/ForecastSolarPlaneMock.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/ForecastSolarPlaneMock.java index d1af7a875b30a..7e4454ef4d068 100644 --- a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/ForecastSolarPlaneMock.java +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/ForecastSolarPlaneMock.java @@ -36,4 +36,8 @@ public ForecastSolarPlaneMock(ForecastSolarObject fso) { super.setCallback(new CallbackMock()); 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/ForecastSolarTest.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/ForecastSolarTest.java index 455c5824ae8ec..dd285384a9529 100644 --- 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 @@ -15,10 +15,12 @@ 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; @@ -29,6 +31,7 @@ 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; @@ -256,7 +259,7 @@ void testCornerCases() { } @Test - void testActions() { + void Exceptions() { 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()); @@ -353,6 +356,80 @@ void testPowerTimeSeries() { } } + @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()); + 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()); + 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()); + 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()); + 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( diff --git a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java index d7b42e92c26c3..89bee84283b57 100644 --- a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java @@ -40,7 +40,6 @@ public class SolcastPlaneMock extends SolcastPlaneHandler { public SolcastPlaneMock(BridgeImpl b) { super(new ThingImpl(SolarForecastBindingConstants.SOLCAST_PLANE, new ThingUID("solarforecast", "sc-plane", "thing")), mock(HttpClient.class)); - super.setCallback(new CallbackMock()); bridge = b; } 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 index 12896484d08e3..119270b2c40d7 100644 --- 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 @@ -580,18 +580,16 @@ void testCombinedPowerTimeSeries() { 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); - - // simulate trigger of refresh job scbh.getData(); TimeSeries ts1 = cm.getTimeSeries("solarforecast:sc-site:bridge:average#power-estimate"); @@ -606,7 +604,7 @@ void testCombinedPowerTimeSeries() { 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"); + 0.01, "Power Value"); } scbh.dispose(); scph1.dispose(); From 801b7bdc5632b060ddc998ee2328df9eaad82441 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 30 Apr 2024 16:43:39 +0200 Subject: [PATCH 107/111] eliminated compiler warnings Signed-off-by: Bernd Weymann --- .../internal/solcast/SolcastObject.java | 2 + .../binding/solarforecast/CallbackMock.java | 3 +- .../solarforecast/ForecastSolarTest.java | 62 +++++++---- .../binding/solarforecast/SolcastTest.java | 91 +++++++++++----- .../forecastsolar/resultNextDay.json | 100 ++++++++++++++++++ 5 files changed, 211 insertions(+), 47 deletions(-) create mode 100644 bundles/org.openhab.binding.solarforecast/src/test/resources/forecastsolar/resultNextDay.json 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 index a316f65451d5d..af1d5047821ff 100644 --- 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 @@ -163,9 +163,11 @@ public double getActualEnergyValue(ZonedDateTime query, QueryMode 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 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 index 2b8568c61aae6..a47b9d9c38b35 100644 --- 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 @@ -16,7 +16,6 @@ import java.util.List; import java.util.Map; -import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.config.core.ConfigDescription; @@ -118,7 +117,7 @@ public ChannelBuilder editChannel(Thing thing, ChannelUID channelUID) { } @Override - public List<@NonNull ChannelBuilder> createChannelBuilders(ChannelGroupUID channelGroupUID, + public List createChannelBuilders(ChannelGroupUID channelGroupUID, ChannelGroupTypeUID channelGroupTypeUID) { return List.of(); } 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 index dd285384a9529..7131422a9671d 100644 --- 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 @@ -125,11 +125,11 @@ void testForecastSum() { ZonedDateTime queryDateTime = LocalDateTime.of(2022, 7, 17, 16, 23).atZone(TEST_ZONE); ForecastSolarObject fo = new ForecastSolarObject("fs-test", content, queryDateTime.toInstant()); QuantityType actual = QuantityType.valueOf(0, Units.KILOWATT_HOUR); - State st = Utils.getEnergyState(fo.getActualEnergyValue(queryDateTime)); + QuantityType st = Utils.getEnergyState(fo.getActualEnergyValue(queryDateTime)); assertTrue(st instanceof QuantityType); - actual = actual.add((QuantityType) st); + actual = actual.add(st); assertEquals(49.431, actual.floatValue(), TOLERANCE, "Current Production"); - actual = actual.add((QuantityType) st); + actual = actual.add(st); assertEquals(98.862, actual.floatValue(), TOLERANCE, "Doubled Current Production"); } @@ -142,28 +142,36 @@ void testCornerCases() { double d = fo.getActualEnergyValue(query); fail("Exception expected instead of " + d); } catch (SolarForecastException sfe) { - assertTrue(sfe.getMessage().contains(INVALID_RANGE_INDICATOR), + 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) { - assertTrue(sfe.getMessage().contains(NO_GORECAST_INDICATOR), + 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) { - assertTrue(sfe.getMessage().contains(NO_GORECAST_INDICATOR), + 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) { - assertTrue(sfe.getMessage().contains(NO_GORECAST_INDICATOR), + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(NO_GORECAST_INDICATOR), "Expected: " + NO_GORECAST_INDICATOR + " Received: " + sfe.getMessage()); } @@ -175,28 +183,36 @@ void testCornerCases() { double d = fo.getActualEnergyValue(query); fail("Exception expected instead of " + d); } catch (SolarForecastException sfe) { - assertTrue(sfe.getMessage().contains(TOO_EARLY_INDICATOR), + 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) { - assertTrue(sfe.getMessage().contains(DAY_MISSING_INDICATOR), + 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) { - assertTrue(sfe.getMessage().contains(TOO_EARLY_INDICATOR), + 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) { - assertTrue(sfe.getMessage().contains(DAY_MISSING_INDICATOR), + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(DAY_MISSING_INDICATOR), "Expected: " + DAY_MISSING_INDICATOR + " Received: " + sfe.getMessage()); } @@ -213,28 +229,36 @@ void testCornerCases() { double d = fo.getActualEnergyValue(query); fail("Exception expected instead of " + d); } catch (SolarForecastException sfe) { - assertTrue(sfe.getMessage().contains(TOO_LATE_INDICATOR), + 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) { - assertTrue(sfe.getMessage().contains(DAY_MISSING_INDICATOR), + 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) { - assertTrue(sfe.getMessage().contains(TOO_LATE_INDICATOR), + 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) { - assertTrue(sfe.getMessage().contains(DAY_MISSING_INDICATOR), + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(DAY_MISSING_INDICATOR), "Expected: " + DAY_MISSING_INDICATOR + " Received: " + sfe.getMessage()); } @@ -280,7 +304,9 @@ void Exceptions() { fo.getEnergy(queryDateTime.toInstant(), queryDateTime.plusDays(2).toInstant()); fail("Too early exception missing"); } catch (SolarForecastException sfe) { - assertTrue(sfe.getMessage().contains("not available"), "not available expected: " + sfe.getMessage()); + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains("not available"), "not available expected: " + sfe.getMessage()); } try { fo.getDay(queryDateTime.toLocalDate(), "optimistic"); @@ -313,7 +339,7 @@ void testTimeSeries() { powerSeries.getStates().forEachOrdered(entry -> { State s = entry.state(); assertTrue(s instanceof QuantityType); - assertEquals("kW", ((QuantityType) s).getUnit().toString()); + assertEquals("kW", ((QuantityType) s).getUnit().toString()); }); TimeSeries energySeries = fo.getEnergyTimeSeries(QueryMode.Average); @@ -321,7 +347,7 @@ void testTimeSeries() { energySeries.getStates().forEachOrdered(entry -> { State s = entry.state(); assertTrue(s instanceof QuantityType); - assertEquals("kWh", ((QuantityType) s).getUnit().toString()); + assertEquals("kWh", ((QuantityType) s).getUnit().toString()); }); } 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 index 119270b2c40d7..e5975f458caf6 100644 --- 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 @@ -24,7 +24,7 @@ import java.util.Iterator; import java.util.List; -import javax.measure.quantity.Power; +import javax.measure.quantity.Energy; import org.eclipse.jdt.annotation.NonNullByDefault; import org.json.JSONObject; @@ -264,7 +264,9 @@ void testJoin() { double d = scfo.getActualEnergyValue(now, QueryMode.Average); fail("Exception expected instead of " + d); } catch (SolarForecastException sfe) { - assertTrue(sfe.getMessage().contains(TOO_LATE_INDICATOR), + 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"); @@ -285,7 +287,9 @@ void testActions() { double d = scfo.getActualEnergyValue(now, QueryMode.Average); fail("Exception expected instead of " + d); } catch (SolarForecastException sfe) { - assertTrue(sfe.getMessage().contains(TOO_LATE_INDICATOR), + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(TOO_LATE_INDICATOR), "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage()); } @@ -300,8 +304,8 @@ void testActions() { 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()); + 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"); @@ -365,7 +369,9 @@ void testOptimisticPessimistic() { scfo.getPower(past); fail("Exception expected"); } catch (SolarForecastException sfe) { - assertTrue(sfe.getMessage().contains(TOO_LATE_INDICATOR), + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(TOO_LATE_INDICATOR), "Expected: " + TOO_LATE_INDICATOR + " Received: " + sfe.getMessage()); } } @@ -379,7 +385,9 @@ void testInavlid() { double d = scfo.getActualEnergyValue(now, QueryMode.Average); fail("Exception expected instead of " + d); } catch (SolarForecastException sfe) { - assertTrue(sfe.getMessage().contains(TOO_LATE_INDICATOR), + 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"); @@ -388,14 +396,18 @@ void testInavlid() { double d = scfo.getActualEnergyValue(now, QueryMode.Average); fail("Exception expected instead of " + d); } catch (SolarForecastException sfe) { - assertTrue(sfe.getMessage().contains(TOO_LATE_INDICATOR), + 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) { - assertTrue(sfe.getMessage().contains(DAY_MISSING_INDICATOR), + String message = sfe.getMessage(); + assertNotNull(message); + assertTrue(message.contains(DAY_MISSING_INDICATOR), "Expected: " + DAY_MISSING_INDICATOR + " Received: " + sfe.getMessage()); } } @@ -474,6 +486,7 @@ 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"); @@ -490,33 +503,45 @@ void testPowerTimeSeries() { sco.join(content); TimeSeries powerSeries = sco.getPowerTimeSeries(QueryMode.Average); - List> estimateL = new ArrayList<>(); + 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()); - estimateL.add((QuantityType) entry.state()); + 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<>(); + 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()); - estimate10.add((QuantityType) entry.state()); + 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<>(); + 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()); - estimate90.add((QuantityType) entry.state()); + assertEquals("kW", ((QuantityType) s).getUnit().toString()); + if (s instanceof QuantityType qt) { + estimate90.add(qt); + } else { + fail(); + } }); for (int i = 0; i < estimateL.size(); i++) { @@ -536,33 +561,45 @@ void testEnergyTimeSeries() { sco.join(content); TimeSeries energySeries = sco.getEnergyTimeSeries(QueryMode.Average); - List> estimateL = new ArrayList<>(); + 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()); - estimateL.add((QuantityType) entry.state()); + 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<>(); + 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()); - estimate10.add((QuantityType) entry.state()); + 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<>(); + 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()); - estimate90.add((QuantityType) entry.state()); + assertEquals("kWh", ((QuantityType) s).getUnit().toString()); + if (s instanceof QuantityType qt) { + estimate90.add(qt); + } else { + fail(); + } }); for (int i = 0; i < estimateL.size(); i++) { 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 0000000000000..412612f378d3c --- /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 From ed62e832d7c209fba25430cf42981a643cbc392b Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 30 Apr 2024 17:27:49 +0200 Subject: [PATCH 108/111] avoid warning messages in unit tests Signed-off-by: Bernd Weymann --- .../solarforecast/ForecastSolarTest.java | 29 ++++++++++++------- .../binding/solarforecast/SolcastTest.java | 1 + .../handler}/ForecastSolarPlaneMock.java | 6 ++-- .../solcast/handler}/SolcastPlaneMock.java | 5 ++-- 4 files changed, 27 insertions(+), 14 deletions(-) rename bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/{ => internal/forecastsolar/handler}/ForecastSolarPlaneMock.java (85%) rename bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/{ => internal/solcast/handler}/SolcastPlaneMock.java (92%) 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 index 7131422a9671d..a78a2e08f3134 100644 --- 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 @@ -35,6 +35,7 @@ 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; @@ -66,7 +67,8 @@ class ForecastSolarTest { 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()); + 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, @@ -89,7 +91,8 @@ void testForecastObject() { 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()); + 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"); @@ -102,7 +105,8 @@ void testActualPower() { 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()); + ForecastSolarObject fo = new ForecastSolarObject("fs-test", content, + queryDateTime.toInstant().plus(1, ChronoUnit.DAYS)); // test steady value increase double previousValue = 0; @@ -123,7 +127,8 @@ void testInterpolation() { 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()); + 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); @@ -360,7 +365,7 @@ void testPowerTimeSeries() { fsbh.setCallback(cm); String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); - ForecastSolarObject fso1 = new ForecastSolarObject("fs-test", content, Instant.now()); + ForecastSolarObject fso1 = new ForecastSolarObject("fs-test", content, Instant.now().plus(1, ChronoUnit.DAYS)); ForecastSolarPlaneHandler fsph1 = new ForecastSolarPlaneMock(fso1); fsbh.addPlane(fsph1); fsbh.forecastUpdate(); @@ -390,13 +395,15 @@ void testCommonForecastStartEnd() { 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()); + 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()); + 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); @@ -432,13 +439,15 @@ void testActions() { 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()); + 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()); + 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); @@ -465,7 +474,7 @@ void testEnergyTimeSeries() { fsbh.setCallback(cm); String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); - ForecastSolarObject fso1 = new ForecastSolarObject("fs-test", content, Instant.now()); + ForecastSolarObject fso1 = new ForecastSolarObject("fs-test", content, Instant.now().plus(1, ChronoUnit.DAYS)); ForecastSolarPlaneHandler fsph1 = new ForecastSolarPlaneMock(fso1); fsbh.addPlane(fsph1); fsbh.forecastUpdate(); 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 index e5975f458caf6..a6606d54f37da 100644 --- 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 @@ -37,6 +37,7 @@ 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; diff --git a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/ForecastSolarPlaneMock.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarPlaneMock.java similarity index 85% rename from bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/ForecastSolarPlaneMock.java rename to bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarPlaneMock.java index 7e4454ef4d068..7b4edfef80477 100644 --- a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/ForecastSolarPlaneMock.java +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/internal/forecastsolar/handler/ForecastSolarPlaneMock.java @@ -10,15 +10,16 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.solarforecast; +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.binding.solarforecast.internal.forecastsolar.handler.ForecastSolarPlaneHandler; +import org.openhab.core.library.types.PointType; import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.internal.ThingImpl; @@ -34,6 +35,7 @@ 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); } diff --git a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastPlaneMock.java similarity index 92% rename from bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java rename to bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastPlaneMock.java index 89bee84283b57..b1b48a9778ba8 100644 --- a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastPlaneMock.java +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastPlaneMock.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.solarforecast; +package org.openhab.binding.solarforecast.internal.solcast.handler; import static org.mockito.Mockito.mock; @@ -19,9 +19,10 @@ 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.binding.solarforecast.internal.solcast.handler.SolcastPlaneHandler; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.internal.BridgeImpl; From d2e9dfb3017afde3daeb5826ff10f6b8f84a8121 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 30 Apr 2024 17:33:03 +0200 Subject: [PATCH 109/111] fix checkstyle Signed-off-by: Bernd Weymann --- .../org/openhab/binding/solarforecast/ForecastSolarTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index a78a2e08f3134..608bc02ebec7f 100644 --- 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 @@ -288,7 +288,7 @@ void testCornerCases() { } @Test - void Exceptions() { + 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()); From 2e17c8c3084aca41c3619fc7bbd4ad5ff853a605 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 30 Apr 2024 17:37:46 +0200 Subject: [PATCH 110/111] fix checkstyle Signed-off-by: Bernd Weymann --- .../binding/solarforecast/internal/solcast/SolcastObject.java | 1 - 1 file changed, 1 deletion(-) 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 index af1d5047821ff..667c0e77ff958 100644 --- 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 @@ -167,7 +167,6 @@ public double getActualEnergyValue(ZonedDateTime query, QueryMode mode) { } 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 From ba3850b05e68966182a6a07183d6a8b378cc2b83 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Wed, 1 May 2024 12:24:17 +0200 Subject: [PATCH 111/111] fix review comments Signed-off-by: Bernd Weymann --- .../internal/solcast/handler/SolcastPlaneHandler.java | 2 +- .../openhab/binding/solarforecast/internal/utils/Utils.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 index 6678bd2109a53..89c46564cfacf 100644 --- 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 @@ -84,7 +84,7 @@ public void initialize() { if (handler != null) { if (handler instanceof SolcastBridgeHandler sbh) { bridgeHandler = Optional.of(sbh); - forecast = Optional.of(new SolcastObject(thing.getUID().getAsString(), bridgeHandler.get())); + forecast = Optional.of(new SolcastObject(thing.getUID().getAsString(), sbh)); sbh.addPlane(this); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, 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 index d819fef6998ce..44844a6db2516 100644 --- 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 @@ -61,11 +61,11 @@ public static void addState(TreeMap> map, Entry entry) } public static boolean isBeforeOrEqual(Instant query, Instant reference) { - return query.isBefore(reference) || query.equals(reference); + return !query.isAfter(reference); } public static boolean isAfterOrEqual(Instant query, Instant reference) { - return query.isAfter(reference) || query.equals(reference); + return !query.isBefore(reference); } public static Instant getCommonStartTime(List forecastObjects) {