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.solcast.handler;
15 import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*;
16 import static org.openhab.binding.solarforecast.internal.solcast.SolcastConstants.*;
18 import java.time.Instant;
19 import java.time.ZonedDateTime;
20 import java.time.temporal.ChronoUnit;
21 import java.util.Collection;
22 import java.util.List;
23 import java.util.Optional;
24 import java.util.concurrent.ExecutionException;
25 import java.util.concurrent.TimeoutException;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jetty.client.HttpClient;
29 import org.eclipse.jetty.client.api.ContentResponse;
30 import org.eclipse.jetty.client.api.Request;
31 import org.eclipse.jetty.http.HttpHeader;
32 import org.openhab.binding.solarforecast.internal.actions.SolarForecast;
33 import org.openhab.binding.solarforecast.internal.actions.SolarForecastActions;
34 import org.openhab.binding.solarforecast.internal.actions.SolarForecastProvider;
35 import org.openhab.binding.solarforecast.internal.solcast.SolcastObject;
36 import org.openhab.binding.solarforecast.internal.solcast.SolcastObject.QueryMode;
37 import org.openhab.binding.solarforecast.internal.solcast.config.SolcastPlaneConfiguration;
38 import org.openhab.binding.solarforecast.internal.utils.Utils;
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 SolcastPlaneHandler} is a non active handler instance. It will be triggerer by the bridge.
56 * @author Bernd Weymann - Initial contribution
59 public class SolcastPlaneHandler extends BaseThingHandler implements SolarForecastProvider {
60 private final Logger logger = LoggerFactory.getLogger(SolcastPlaneHandler.class);
61 private final HttpClient httpClient;
62 private SolcastPlaneConfiguration configuration = new SolcastPlaneConfiguration();
63 private Optional<SolcastBridgeHandler> bridgeHandler = Optional.empty();
64 protected Optional<SolcastObject> forecast = Optional.empty();
66 public SolcastPlaneHandler(Thing thing, HttpClient hc) {
72 public Collection<Class<? extends ThingHandlerService>> getServices() {
73 return List.of(SolarForecastActions.class);
77 public void initialize() {
78 configuration = getConfigAs(SolcastPlaneConfiguration.class);
80 // connect Bridge & Status
81 Bridge bridge = getBridge();
83 BridgeHandler handler = bridge.getHandler();
84 if (handler != null) {
85 if (handler instanceof SolcastBridgeHandler sbh) {
86 bridgeHandler = Optional.of(sbh);
87 forecast = Optional.of(new SolcastObject(thing.getUID().getAsString(), sbh));
90 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
91 "@text/solarforecast.plane.status.wrong-handler [\"" + handler + "\"]");
94 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
95 "@text/solarforecast.plane.status.bridge-handler-not-found");
98 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
99 "@text/solarforecast.plane.status.bridge-missing");
104 public void dispose() {
106 bridgeHandler.ifPresent(bridge -> bridge.removePlane(this));
110 public void handleCommand(ChannelUID channelUID, Command command) {
111 if (command instanceof RefreshType) {
112 forecast.ifPresent(forecastObject -> {
113 String group = channelUID.getGroupId();
117 String channel = channelUID.getIdWithoutGroup();
118 QueryMode mode = QueryMode.Average;
121 mode = QueryMode.Average;
123 case GROUP_OPTIMISTIC:
124 mode = QueryMode.Optimistic;
126 case GROUP_PESSIMISTIC:
127 mode = QueryMode.Pessimistic;
130 forecast.ifPresent(f -> {
131 updateState(GROUP_RAW + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_JSON,
132 StringType.valueOf(f.getRaw()));
136 case CHANNEL_ENERGY_ESTIMATE:
137 sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, forecastObject.getEnergyTimeSeries(mode));
139 case CHANNEL_POWER_ESTIMATE:
140 sendTimeSeries(CHANNEL_POWER_ESTIMATE, forecastObject.getPowerTimeSeries(mode));
143 updateChannels(forecastObject);
149 protected synchronized SolcastObject fetchData() {
150 bridgeHandler.ifPresent(bridge -> {
151 forecast.ifPresent(forecastObject -> {
152 if (forecastObject.isExpired()) {
153 logger.trace("Get new forecast {}", forecastObject.toString());
154 String forecastUrl = String.format(FORECAST_URL, configuration.resourceId);
155 String currentEstimateUrl = String.format(CURRENT_ESTIMATE_URL, configuration.resourceId);
157 // get actual estimate
158 Request estimateRequest = httpClient.newRequest(currentEstimateUrl);
159 estimateRequest.header(HttpHeader.AUTHORIZATION, BEARER + bridge.getApiKey());
160 ContentResponse crEstimate = estimateRequest.send();
161 if (crEstimate.getStatus() == 200) {
162 SolcastObject localForecast = new SolcastObject(thing.getUID().getAsString(),
163 crEstimate.getContentAsString(),
164 Instant.now().plus(configuration.refreshInterval, ChronoUnit.MINUTES), bridge);
167 Request forecastRequest = httpClient.newRequest(forecastUrl);
168 forecastRequest.header(HttpHeader.AUTHORIZATION, BEARER + bridge.getApiKey());
169 ContentResponse crForecast = forecastRequest.send();
171 if (crForecast.getStatus() == 200) {
172 localForecast.join(crForecast.getContentAsString());
173 setForecast(localForecast);
174 updateState(GROUP_RAW + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_JSON,
175 StringType.valueOf(forecast.get().getRaw()));
176 updateStatus(ThingStatus.ONLINE);
178 logger.debug("{} Call {} failed {}", thing.getLabel(), forecastUrl,
179 crForecast.getStatus());
180 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
181 "@text/solarforecast.plane.status.http-status [\"" + crForecast.getStatus()
185 logger.debug("{} Call {} failed {}", thing.getLabel(), currentEstimateUrl,
186 crEstimate.getStatus());
187 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
188 "@text/solarforecast.plane.status.http-status [\"" + crEstimate.getStatus()
191 } catch (ExecutionException | TimeoutException e) {
192 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
193 } catch (InterruptedException e) {
194 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
195 Thread.currentThread().interrupt();
198 updateChannels(forecastObject);
202 return forecast.get();
205 protected void updateChannels(SolcastObject f) {
206 if (bridgeHandler.isEmpty()) {
209 ZonedDateTime now = ZonedDateTime.now(bridgeHandler.get().getTimeZone());
210 List<QueryMode> modes = List.of(QueryMode.Average, QueryMode.Pessimistic, QueryMode.Optimistic);
211 modes.forEach(mode -> {
212 double energyDay = f.getDayTotal(now.toLocalDate(), mode);
213 double energyProduced = f.getActualEnergyValue(now, mode);
214 String group = switch (mode) {
215 case Average -> GROUP_AVERAGE;
216 case Optimistic -> GROUP_OPTIMISTIC;
217 case Pessimistic -> GROUP_PESSIMISTIC;
218 case Error -> throw new IllegalStateException("mode " + mode + " not expected");
220 updateState(group + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_ACTUAL,
221 Utils.getEnergyState(energyProduced));
222 updateState(group + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_REMAIN,
223 Utils.getEnergyState(energyDay - energyProduced));
224 updateState(group + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_TODAY,
225 Utils.getEnergyState(energyDay));
226 updateState(group + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_POWER_ACTUAL,
227 Utils.getPowerState(f.getActualPowerValue(now, QueryMode.Average)));
231 protected synchronized void setForecast(SolcastObject f) {
232 forecast = Optional.of(f);
233 sendTimeSeries(GROUP_AVERAGE + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_POWER_ESTIMATE,
234 f.getPowerTimeSeries(QueryMode.Average));
235 sendTimeSeries(GROUP_AVERAGE + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_ESTIMATE,
236 f.getEnergyTimeSeries(QueryMode.Average));
237 sendTimeSeries(GROUP_OPTIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_POWER_ESTIMATE,
238 f.getPowerTimeSeries(QueryMode.Optimistic));
239 sendTimeSeries(GROUP_OPTIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_ESTIMATE,
240 f.getEnergyTimeSeries(QueryMode.Optimistic));
241 sendTimeSeries(GROUP_PESSIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_POWER_ESTIMATE,
242 f.getPowerTimeSeries(QueryMode.Pessimistic));
243 sendTimeSeries(GROUP_PESSIMISTIC + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ENERGY_ESTIMATE,
244 f.getEnergyTimeSeries(QueryMode.Pessimistic));
245 bridgeHandler.ifPresent(h -> {
251 public synchronized List<SolarForecast> getSolarForecasts() {
252 return List.of(forecast.get());