2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.solarforecast.internal.forecastsolar.handler;
15 import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*;
17 import java.time.Instant;
18 import java.time.ZonedDateTime;
19 import java.time.temporal.ChronoUnit;
20 import java.util.Collection;
21 import java.util.List;
22 import java.util.Optional;
23 import java.util.concurrent.ExecutionException;
24 import java.util.concurrent.TimeoutException;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jetty.client.HttpClient;
28 import org.eclipse.jetty.client.api.ContentResponse;
29 import org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants;
30 import org.openhab.binding.solarforecast.internal.SolarForecastException;
31 import org.openhab.binding.solarforecast.internal.actions.SolarForecast;
32 import org.openhab.binding.solarforecast.internal.actions.SolarForecastActions;
33 import org.openhab.binding.solarforecast.internal.actions.SolarForecastProvider;
34 import org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarObject;
35 import org.openhab.binding.solarforecast.internal.forecastsolar.config.ForecastSolarPlaneConfiguration;
36 import org.openhab.binding.solarforecast.internal.solcast.SolcastObject.QueryMode;
37 import org.openhab.binding.solarforecast.internal.utils.Utils;
38 import org.openhab.core.library.types.PointType;
39 import org.openhab.core.library.types.StringType;
40 import org.openhab.core.thing.Bridge;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.thing.binding.BaseThingHandler;
46 import org.openhab.core.thing.binding.BridgeHandler;
47 import org.openhab.core.thing.binding.ThingHandlerService;
48 import org.openhab.core.types.Command;
49 import org.openhab.core.types.RefreshType;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
54 * The {@link ForecastSolarPlaneHandler} is a non active handler instance. It will be triggered by the bridge.
56 * @author Bernd Weymann - Initial contribution
59 public class ForecastSolarPlaneHandler extends BaseThingHandler implements SolarForecastProvider {
60 public static final String BASE_URL = "https://api.forecast.solar/";
62 private final Logger logger = LoggerFactory.getLogger(ForecastSolarPlaneHandler.class);
63 private final HttpClient httpClient;
65 private Optional<ForecastSolarPlaneConfiguration> configuration = Optional.empty();
66 private Optional<ForecastSolarBridgeHandler> bridgeHandler = Optional.empty();
67 private Optional<PointType> location = Optional.empty();
68 private Optional<String> apiKey = Optional.empty();
69 private ForecastSolarObject forecast;
71 public ForecastSolarPlaneHandler(Thing thing, HttpClient hc) {
74 forecast = new ForecastSolarObject(thing.getUID().getAsString());
78 public Collection<Class<? extends ThingHandlerService>> getServices() {
79 return List.of(SolarForecastActions.class);
83 public void initialize() {
84 ForecastSolarPlaneConfiguration c = getConfigAs(ForecastSolarPlaneConfiguration.class);
85 configuration = Optional.of(c);
86 Bridge bridge = getBridge();
88 BridgeHandler handler = bridge.getHandler();
89 if (handler != null) {
90 if (handler instanceof ForecastSolarBridgeHandler fsbh) {
91 bridgeHandler = Optional.of(fsbh);
92 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE,
93 "@text/solarforecast.plane.status.await-feedback");
96 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
97 "@text/solarforecast.plane.status.wrong-handler" + " [\"" + handler + "\"]");
100 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
101 "@text/solarforecast.plane.status.bridge-handler-not-found");
104 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
105 "@text/solarforecast.plane.status.bridge-missing");
110 public void dispose() {
112 if (bridgeHandler.isPresent()) {
113 bridgeHandler.get().removePlane(this);
118 public void handleCommand(ChannelUID channelUID, Command command) {
119 if (command instanceof RefreshType) {
120 if (CHANNEL_POWER_ESTIMATE.equals(channelUID.getIdWithoutGroup())) {
121 sendTimeSeries(CHANNEL_POWER_ESTIMATE, forecast.getPowerTimeSeries(QueryMode.Average));
122 } else if (CHANNEL_ENERGY_ESTIMATE.equals(channelUID.getIdWithoutGroup())) {
123 sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, forecast.getEnergyTimeSeries(QueryMode.Average));
124 } else if (CHANNEL_JSON.equals(channelUID.getIdWithoutGroup())) {
125 updateState(CHANNEL_JSON, StringType.valueOf(forecast.getRaw()));
133 * https://doc.forecast.solar/doku.php?id=api:estimate
135 protected ForecastSolarObject fetchData() {
136 if (location.isPresent()) {
137 if (forecast.isExpired()) {
138 String url = getBaseUrl() + "estimate/" + location.get().getLatitude() + SLASH
139 + location.get().getLongitude() + SLASH + configuration.get().declination + SLASH
140 + configuration.get().azimuth + SLASH + configuration.get().kwp + "?damping="
141 + configuration.get().dampAM + "," + configuration.get().dampPM;
142 if (!SolarForecastBindingConstants.EMPTY.equals(configuration.get().horizon)) {
143 url += "&horizon=" + configuration.get().horizon;
146 ContentResponse cr = httpClient.GET(url);
147 if (cr.getStatus() == 200) {
149 ForecastSolarObject localForecast = new ForecastSolarObject(thing.getUID().getAsString(),
150 cr.getContentAsString(),
151 Instant.now().plus(configuration.get().refreshInterval, ChronoUnit.MINUTES));
152 updateStatus(ThingStatus.ONLINE);
153 updateState(CHANNEL_JSON, StringType.valueOf(cr.getContentAsString()));
154 setForecast(localForecast);
155 } catch (SolarForecastException fse) {
156 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
157 "@text/solarforecast.plane.status.json-status [\"" + fse.getMessage() + "\"]");
160 logger.trace("Call {} failed with status {}. Response: {}", url, cr.getStatus(),
161 cr.getContentAsString());
162 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
163 "@text/solarforecast.plane.status.http-status [\"" + cr.getStatus() + "\"]");
165 } catch (ExecutionException | TimeoutException e) {
166 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
167 } catch (InterruptedException e) {
168 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
169 Thread.currentThread().interrupt();
172 // else use available forecast
173 updateChannels(forecast);
176 logger.warn("{} Location not present", thing.getLabel());
181 private void updateChannels(ForecastSolarObject f) {
182 ZonedDateTime now = ZonedDateTime.now(f.getZone());
183 double energyDay = f.getDayTotal(now.toLocalDate());
184 double energyProduced = f.getActualEnergyValue(now);
185 updateState(CHANNEL_ENERGY_ACTUAL, Utils.getEnergyState(energyProduced));
186 updateState(CHANNEL_ENERGY_REMAIN, Utils.getEnergyState(energyDay - energyProduced));
187 updateState(CHANNEL_ENERGY_TODAY, Utils.getEnergyState(energyDay));
188 updateState(CHANNEL_POWER_ACTUAL, Utils.getPowerState(f.getActualPowerValue(now)));
192 * Used by Bridge to set location directly
196 void setLocation(PointType loc) {
197 location = Optional.of(loc);
200 void setApiKey(String key) {
201 apiKey = Optional.of(key);
204 String getBaseUrl() {
205 String url = BASE_URL;
206 if (apiKey.isPresent()) {
207 url += apiKey.get() + SLASH;
212 protected synchronized void setForecast(ForecastSolarObject f) {
214 sendTimeSeries(CHANNEL_POWER_ESTIMATE, forecast.getPowerTimeSeries(QueryMode.Average));
215 sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, forecast.getEnergyTimeSeries(QueryMode.Average));
216 bridgeHandler.ifPresent(h -> {
222 public synchronized List<SolarForecast> getSolarForecasts() {
223 return List.of(forecast);