]> git.basschouten.com Git - openhab-addons.git/blob
a15f617fdc3c58c8cf030eed5a2d532c6c67ddfa
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.solarforecast.internal.forecastsolar.handler;
14
15 import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*;
16
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;
25
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;
52
53 /**
54  * The {@link ForecastSolarPlaneHandler} is a non active handler instance. It will be triggered by the bridge.
55  *
56  * @author Bernd Weymann - Initial contribution
57  */
58 @NonNullByDefault
59 public class ForecastSolarPlaneHandler extends BaseThingHandler implements SolarForecastProvider {
60     public static final String BASE_URL = "https://api.forecast.solar/";
61
62     private final Logger logger = LoggerFactory.getLogger(ForecastSolarPlaneHandler.class);
63     private final HttpClient httpClient;
64
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;
70
71     public ForecastSolarPlaneHandler(Thing thing, HttpClient hc) {
72         super(thing);
73         httpClient = hc;
74         forecast = new ForecastSolarObject(thing.getUID().getAsString());
75     }
76
77     @Override
78     public Collection<Class<? extends ThingHandlerService>> getServices() {
79         return List.of(SolarForecastActions.class);
80     }
81
82     @Override
83     public void initialize() {
84         ForecastSolarPlaneConfiguration c = getConfigAs(ForecastSolarPlaneConfiguration.class);
85         configuration = Optional.of(c);
86         Bridge bridge = getBridge();
87         if (bridge != null) {
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");
94                     fsbh.addPlane(this);
95                 } else {
96                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
97                             "@text/solarforecast.plane.status.wrong-handler" + " [\"" + handler + "\"]");
98                 }
99             } else {
100                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
101                         "@text/solarforecast.plane.status.bridge-handler-not-found");
102             }
103         } else {
104             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
105                     "@text/solarforecast.plane.status.bridge-missing");
106         }
107     }
108
109     @Override
110     public void dispose() {
111         super.dispose();
112         if (bridgeHandler.isPresent()) {
113             bridgeHandler.get().removePlane(this);
114         }
115     }
116
117     @Override
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()));
126             } else {
127                 fetchData();
128             }
129         }
130     }
131
132     /**
133      * https://doc.forecast.solar/doku.php?id=api:estimate
134      */
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;
144                 }
145                 try {
146                     ContentResponse cr = httpClient.GET(url);
147                     if (cr.getStatus() == 200) {
148                         try {
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() + "\"]");
158                         }
159                     } else {
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() + "\"]");
164                     }
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();
170                 }
171             } else {
172                 // else use available forecast
173                 updateChannels(forecast);
174             }
175         } else {
176             logger.warn("{} Location not present", thing.getLabel());
177         }
178         return forecast;
179     }
180
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)));
189     }
190
191     /**
192      * Used by Bridge to set location directly
193      *
194      * @param loc
195      */
196     void setLocation(PointType loc) {
197         location = Optional.of(loc);
198     }
199
200     void setApiKey(String key) {
201         apiKey = Optional.of(key);
202     }
203
204     String getBaseUrl() {
205         String url = BASE_URL;
206         if (apiKey.isPresent()) {
207             url += apiKey.get() + SLASH;
208         }
209         return url;
210     }
211
212     protected synchronized void setForecast(ForecastSolarObject f) {
213         forecast = f;
214         sendTimeSeries(CHANNEL_POWER_ESTIMATE, forecast.getPowerTimeSeries(QueryMode.Average));
215         sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, forecast.getEnergyTimeSeries(QueryMode.Average));
216         bridgeHandler.ifPresent(h -> {
217             h.forecastUpdate();
218         });
219     }
220
221     @Override
222     public synchronized List<SolarForecast> getSolarForecasts() {
223         return List.of(forecast);
224     }
225 }