From 7516ca557d47392d5b1303d9992576039f903b73 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sun, 9 Jun 2024 10:54:54 +0200 Subject: [PATCH] [solarforecast] wait 1 hour after http 429 error (#16819) * wait 1 hour after 429 error Signed-off-by: Bernd Weymann --- .../internal/SolarForecastHandlerFactory.java | 2 + .../handler/ForecastSolarBridgeHandler.java | 18 ++++- .../handler/ForecastSolarPlaneHandler.java | 17 +++-- .../solarforecast/internal/utils/Utils.java | 29 ++++++++ .../OH-INF/i18n/solarforecast.properties | 1 + .../binding/solarforecast/CallbackMock.java | 27 +++++++- .../solarforecast/ForecastSolarTest.java | 69 +++++++++++++++++++ 7 files changed, 157 insertions(+), 6 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 9597ab2fdb6..b1c11916d30 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 @@ -23,6 +23,7 @@ import org.openhab.binding.solarforecast.internal.forecastsolar.handler.Forecast 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.binding.solarforecast.internal.utils.Utils; import org.openhab.core.i18n.LocationProvider; import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.io.net.http.HttpClientFactory; @@ -55,6 +56,7 @@ public class SolarForecastHandlerFactory extends BaseThingHandlerFactory { final @Reference TimeZoneProvider tzp) { timeZoneProvider = tzp; httpClient = hcf.getCommonHttpClient(); + Utils.setTimeZoneProvider(tzp); PointType pt = lp.getLocation(); if (pt != null) { 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 487d93e86dd..0800444b6c2 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,8 +14,10 @@ package org.openhab.binding.solarforecast.internal.forecastsolar.handler; import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*; +import java.time.Duration; import java.time.Instant; import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; @@ -55,10 +57,13 @@ import org.openhab.core.types.TimeSeries.Policy; */ @NonNullByDefault public class ForecastSolarBridgeHandler extends BaseBridgeHandler implements SolarForecastProvider { + private static final int CALM_DOWN_TIME_MINUTES = 61; + private List planes = new ArrayList<>(); private Optional homeLocation; private Optional configuration = Optional.empty(); private Optional> refreshJob = Optional.empty(); + private Instant calmDownEnd = Instant.MIN; public ForecastSolarBridgeHandler(Bridge bridge, Optional location) { super(bridge); @@ -130,6 +135,13 @@ public class ForecastSolarBridgeHandler extends BaseBridgeHandler implements Sol if (planes.isEmpty()) { return; } + if (calmDownEnd.isAfter(Instant.now(Utils.getClock()))) { + // wait until calm down time is expired + long minutes = Duration.between(Instant.now(Utils.getClock()), calmDownEnd).toMinutes(); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/solarforecast.site.status.calmdown [\"" + minutes + "\"]"); + return; + } boolean update = true; double energySum = 0; double powerSum = 0; @@ -138,7 +150,7 @@ public class ForecastSolarBridgeHandler extends BaseBridgeHandler implements Sol try { ForecastSolarPlaneHandler sfph = iterator.next(); ForecastSolarObject fo = sfph.fetchData(); - ZonedDateTime now = ZonedDateTime.now(fo.getZone()); + ZonedDateTime now = ZonedDateTime.now(Utils.getClock()); energySum += fo.getActualEnergyValue(now); powerSum += fo.getActualPowerValue(now); daySum += fo.getDayTotal(now.toLocalDate()); @@ -232,4 +244,8 @@ public class ForecastSolarBridgeHandler extends BaseBridgeHandler implements Sol }); return l; } + + public void calmDown() { + calmDownEnd = Instant.now(Utils.getClock()).plus(CALM_DOWN_TIME_MINUTES, ChronoUnit.MINUTES); + } } 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 a15f617fdc3..faecb63c1a7 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 @@ -144,11 +144,12 @@ public class ForecastSolarPlaneHandler extends BaseThingHandler implements Solar } try { ContentResponse cr = httpClient.GET(url); - if (cr.getStatus() == 200) { + int responseStatus = cr.getStatus(); + if (responseStatus == 200) { try { ForecastSolarObject localForecast = new ForecastSolarObject(thing.getUID().getAsString(), - cr.getContentAsString(), - Instant.now().plus(configuration.get().refreshInterval, ChronoUnit.MINUTES)); + cr.getContentAsString(), Instant.now(Utils.getClock()) + .plus(configuration.get().refreshInterval, ChronoUnit.MINUTES)); updateStatus(ThingStatus.ONLINE); updateState(CHANNEL_JSON, StringType.valueOf(cr.getContentAsString())); setForecast(localForecast); @@ -156,6 +157,14 @@ public class ForecastSolarPlaneHandler extends BaseThingHandler implements Solar updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/solarforecast.plane.status.json-status [\"" + fse.getMessage() + "\"]"); } + } else if (responseStatus == 429) { + // special handling for 429 response: https://doc.forecast.solar/facing429 + // bridge shall "calm down" until at least one hour is expired + if (bridgeHandler.isPresent()) { + bridgeHandler.get().calmDown(); + } + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/solarforecast.plane.status.http-status [\"" + cr.getStatus() + "\"]"); } else { logger.trace("Call {} failed with status {}. Response: {}", url, cr.getStatus(), cr.getContentAsString()); @@ -179,7 +188,7 @@ public class ForecastSolarPlaneHandler extends BaseThingHandler implements Solar } private void updateChannels(ForecastSolarObject f) { - ZonedDateTime now = ZonedDateTime.now(f.getZone()); + ZonedDateTime now = ZonedDateTime.now(Utils.getClock()); double energyDay = f.getDayTotal(now.toLocalDate()); double energyProduced = f.getActualEnergyValue(now); updateState(CHANNEL_ENERGY_ACTUAL, Utils.getEnergyState(energyProduced)); 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 44844a6db25..e7237231b96 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,7 +12,9 @@ */ package org.openhab.binding.solarforecast.internal.utils; +import java.time.Clock; import java.time.Instant; +import java.time.ZoneId; import java.util.Iterator; import java.util.List; import java.util.TreeMap; @@ -23,6 +25,7 @@ import javax.measure.quantity.Power; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.solarforecast.internal.actions.SolarForecast; +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.TimeSeries.Entry; @@ -34,6 +37,32 @@ import org.openhab.core.types.TimeSeries.Entry; */ @NonNullByDefault public class Utils { + private static TimeZoneProvider timeZoneProvider = new TimeZoneProvider() { + @Override + public ZoneId getTimeZone() { + return ZoneId.systemDefault(); + } + }; + + private static Clock clock = Clock.systemDefaultZone(); + + /** + * Only for unit testing setting a fixed clock with desired date-time + * + * @param c + */ + public static void setClock(Clock c) { + clock = c; + } + + public static void setTimeZoneProvider(TimeZoneProvider tzp) { + timeZoneProvider = tzp; + } + + public static Clock getClock() { + return clock.withZone(timeZoneProvider.getTimeZone()); + } + public static QuantityType getEnergyState(double d) { if (d < 0) { return QuantityType.valueOf(-1, Units.KILOWATT_HOUR); 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 a2fe132236b..36ef6e7ac14 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 @@ -79,6 +79,7 @@ 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.site.status.calmdown = Too many requests, continue in {0} minutes 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} 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 a47b9d9c38b..d8ca0a52f8b 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 @@ -12,6 +12,7 @@ */ package org.openhab.binding.solarforecast; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -25,6 +26,8 @@ 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.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingStatusInfo; import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingUID; @@ -45,10 +48,27 @@ import org.openhab.core.types.TimeSeries.Policy; @NonNullByDefault public class CallbackMock implements ThingHandlerCallback { - Map seriesMap = new HashMap(); + Map seriesMap = new HashMap<>(); + Map> stateMap = new HashMap<>(); + ThingStatusInfo currentInfo = new ThingStatusInfo(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, null); @Override public void stateUpdated(ChannelUID channelUID, State state) { + String key = channelUID.getAsString(); + List stateList = stateMap.get(key); + if (stateList == null) { + stateList = new ArrayList<>(); + } + stateList.add(state); + stateMap.put(key, stateList); + } + + public List getStateList(String cuid) { + List stateList = stateMap.get(cuid); + if (stateList == null) { + stateList = new ArrayList(); + } + return stateList; } @Override @@ -70,6 +90,11 @@ public class CallbackMock implements ThingHandlerCallback { @Override public void statusUpdated(Thing thing, ThingStatusInfo thingStatus) { + currentInfo = thingStatus; + } + + public ThingStatusInfo getStatus() { + return currentInfo; } @Override 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 608bc02ebec..e81c7ca1eef 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,7 @@ package org.openhab.binding.solarforecast; import static org.junit.jupiter.api.Assertions.*; +import java.time.Clock; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; @@ -41,7 +42,11 @@ 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.ChannelUID; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; 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; @@ -358,6 +363,10 @@ class ForecastSolarTest { @Test void testPowerTimeSeries() { + // Instant matching the date of test resources + String fixedInstant = "2022-07-17T15:00:00Z"; + Clock fixedClock = Clock.fixed(Instant.parse(fixedInstant), TEST_ZONE); + Utils.setClock(fixedClock); ForecastSolarBridgeHandler fsbh = new ForecastSolarBridgeHandler( new BridgeImpl(SolarForecastBindingConstants.FORECAST_SOLAR_SITE, "bridge"), Optional.of(PointType.valueOf("1,2"))); @@ -389,11 +398,16 @@ class ForecastSolarTest { @Test void testCommonForecastStartEnd() { + // Instant matching the date of test resources + String fixedInstant = "2022-07-17T15:00:00Z"; + Clock fixedClock = Clock.fixed(Instant.parse(fixedInstant), TEST_ZONE); + Utils.setClock(fixedClock); ForecastSolarBridgeHandler fsbh = new ForecastSolarBridgeHandler( new BridgeImpl(SolarForecastBindingConstants.FORECAST_SOLAR_SITE, "bridge"), Optional.of(PointType.valueOf("1,2"))); CallbackMock cmSite = new CallbackMock(); fsbh.setCallback(cmSite); + String contentOne = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); ForecastSolarObject fso1One = new ForecastSolarObject("fs-test", contentOne, Instant.now().plus(1, ChronoUnit.DAYS)); @@ -433,11 +447,16 @@ class ForecastSolarTest { @Test void testActions() { + // Instant matching the date of test resources + String fixedInstant = "2022-07-17T15:00:00Z"; + Clock fixedClock = Clock.fixed(Instant.parse(fixedInstant), TEST_ZONE); + Utils.setClock(fixedClock); ForecastSolarBridgeHandler fsbh = new ForecastSolarBridgeHandler( new BridgeImpl(SolarForecastBindingConstants.FORECAST_SOLAR_SITE, "bridge"), Optional.of(PointType.valueOf("1,2"))); CallbackMock cmSite = new CallbackMock(); fsbh.setCallback(cmSite); + String contentOne = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); ForecastSolarObject fso1One = new ForecastSolarObject("fs-test", contentOne, Instant.now().plus(1, ChronoUnit.DAYS)); @@ -467,6 +486,10 @@ class ForecastSolarTest { @Test void testEnergyTimeSeries() { + // Instant matching the date of test resources + String fixedInstant = "2022-07-17T15:00:00Z"; + Clock fixedClock = Clock.fixed(Instant.parse(fixedInstant), TEST_ZONE); + Utils.setClock(fixedClock); ForecastSolarBridgeHandler fsbh = new ForecastSolarBridgeHandler( new BridgeImpl(SolarForecastBindingConstants.FORECAST_SOLAR_SITE, "bridge"), Optional.of(PointType.valueOf("1,2"))); @@ -495,4 +518,50 @@ class ForecastSolarTest { 0.1, "Power Value"); } } + + @Test + void testCalmDown() { + // Instant matching the date of test resources + String fixedInstant = "2022-07-17T15:00:00Z"; + Clock fixedClock = Clock.fixed(Instant.parse(fixedInstant), TEST_ZONE); + Utils.setClock(fixedClock); + ForecastSolarBridgeHandler fsbh = new ForecastSolarBridgeHandler( + new BridgeImpl(SolarForecastBindingConstants.FORECAST_SOLAR_SITE, "bridge"), + Optional.of(PointType.valueOf("1,2"))); + CallbackMock cm = new CallbackMock(); + fsbh.setCallback(cm); + + String content = FileReader.readFileInString("src/test/resources/forecastsolar/result.json"); + ForecastSolarObject fso1 = new ForecastSolarObject("fs-test", content, Instant.now().plus(1, ChronoUnit.DAYS)); + ForecastSolarPlaneHandler fsph1 = new ForecastSolarPlaneMock(fso1); + fsbh.addPlane(fsph1); + // first update after add plane - 1 state shall be received + assertEquals(1, cm.getStateList("solarforecast:fs-site:bridge:power-actual").size(), "First update"); + assertEquals(ThingStatus.ONLINE, cm.getStatus().getStatus(), "Online"); + fsbh.handleCommand( + new ChannelUID("solarforecast:fs-site:bridge:" + SolarForecastBindingConstants.CHANNEL_ENERGY_ACTUAL), + RefreshType.REFRESH); + // second update after refresh request - 2 states shall be received + assertEquals(2, cm.getStateList("solarforecast:fs-site:bridge:power-actual").size(), "Second update"); + assertEquals(ThingStatus.ONLINE, cm.getStatus().getStatus(), "Online"); + + fsbh.calmDown(); + fsbh.handleCommand( + new ChannelUID("solarforecast:fs-site:bridge:" + SolarForecastBindingConstants.CHANNEL_ENERGY_ACTUAL), + RefreshType.REFRESH); + // after calm down refresh shall have no effect . still 2 states + assertEquals(2, cm.getStateList("solarforecast:fs-site:bridge:power-actual").size(), "Calm update"); + assertEquals(ThingStatus.OFFLINE, cm.getStatus().getStatus(), "Offline"); + assertEquals(ThingStatusDetail.COMMUNICATION_ERROR, cm.getStatus().getStatusDetail(), "Offline"); + + // forward Clock to get ONLINE again + fixedInstant = "2022-07-17T16:15:00Z"; + fixedClock = Clock.fixed(Instant.parse(fixedInstant), ZoneId.of("UTC")); + Utils.setClock(fixedClock); + fsbh.handleCommand( + new ChannelUID("solarforecast:fs-site:bridge:" + SolarForecastBindingConstants.CHANNEL_ENERGY_ACTUAL), + RefreshType.REFRESH); + assertEquals(3, cm.getStateList("solarforecast:fs-site:bridge:power-actual").size(), "Second update"); + assertEquals(ThingStatus.ONLINE, cm.getStatus().getStatus(), "Online"); + } }