[solarforecast] wait 1 hour after http 429 error (#16819)

* wait 1 hour after 429 error

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Bernd Weymann 2024-06-09 10:54:54 +02:00 committed by Ciprian Pascu
parent 7d9c2f51d1
commit 792836d787
7 changed files with 157 additions and 6 deletions

View File

@ -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);

View File

@ -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<ForecastSolarPlaneHandler> planes = new ArrayList<>();
private Optional<PointType> homeLocation;
private Optional<ForecastSolarBridgeConfiguration> configuration = Optional.empty();
private Optional<ScheduledFuture<?>> refreshJob = Optional.empty();
private Instant calmDownEnd = Instant.MIN;
public ForecastSolarBridgeHandler(Bridge bridge, Optional<PointType> 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);
}
}

View File

@ -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));

View File

@ -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<Energy> getEnergyState(double d) {
if (d < 0) {
return QuantityType.valueOf(-1, Units.KILOWATT_HOUR);

View File

@ -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}

View File

@ -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<String, TimeSeries> seriesMap = new HashMap<String, TimeSeries>();
Map<String, TimeSeries> seriesMap = new HashMap<>();
Map<String, List<State>> 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<State> stateList = stateMap.get(key);
if (stateList == null) {
stateList = new ArrayList<>();
}
stateList.add(state);
stateMap.put(key, stateList);
}
public List<State> getStateList(String cuid) {
List<State> stateList = stateMap.get(cuid);
if (stateList == null) {
stateList = new ArrayList<State>();
}
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

View File

@ -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");
}
}