]> git.basschouten.com Git - openhab-addons.git/blob
89c46564cfacf5e83466fe36a30a3cceb4973ee8
[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.solcast.handler;
14
15 import static org.openhab.binding.solarforecast.internal.SolarForecastBindingConstants.*;
16 import static org.openhab.binding.solarforecast.internal.solcast.SolcastConstants.*;
17
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;
26
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;
52
53 /**
54  * The {@link SolcastPlaneHandler} is a non active handler instance. It will be triggerer by the bridge.
55  *
56  * @author Bernd Weymann - Initial contribution
57  */
58 @NonNullByDefault
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();
65
66     public SolcastPlaneHandler(Thing thing, HttpClient hc) {
67         super(thing);
68         httpClient = hc;
69     }
70
71     @Override
72     public Collection<Class<? extends ThingHandlerService>> getServices() {
73         return List.of(SolarForecastActions.class);
74     }
75
76     @Override
77     public void initialize() {
78         configuration = getConfigAs(SolcastPlaneConfiguration.class);
79
80         // connect Bridge & Status
81         Bridge bridge = getBridge();
82         if (bridge != null) {
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));
88                     sbh.addPlane(this);
89                 } else {
90                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
91                             "@text/solarforecast.plane.status.wrong-handler [\"" + handler + "\"]");
92                 }
93             } else {
94                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
95                         "@text/solarforecast.plane.status.bridge-handler-not-found");
96             }
97         } else {
98             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
99                     "@text/solarforecast.plane.status.bridge-missing");
100         }
101     }
102
103     @Override
104     public void dispose() {
105         super.dispose();
106         bridgeHandler.ifPresent(bridge -> bridge.removePlane(this));
107     }
108
109     @Override
110     public void handleCommand(ChannelUID channelUID, Command command) {
111         if (command instanceof RefreshType) {
112             forecast.ifPresent(forecastObject -> {
113                 String group = channelUID.getGroupId();
114                 if (group == null) {
115                     group = EMPTY;
116                 }
117                 String channel = channelUID.getIdWithoutGroup();
118                 QueryMode mode = QueryMode.Average;
119                 switch (group) {
120                     case GROUP_AVERAGE:
121                         mode = QueryMode.Average;
122                         break;
123                     case GROUP_OPTIMISTIC:
124                         mode = QueryMode.Optimistic;
125                         break;
126                     case GROUP_PESSIMISTIC:
127                         mode = QueryMode.Pessimistic;
128                         break;
129                     case GROUP_RAW:
130                         forecast.ifPresent(f -> {
131                             updateState(GROUP_RAW + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_JSON,
132                                     StringType.valueOf(f.getRaw()));
133                         });
134                 }
135                 switch (channel) {
136                     case CHANNEL_ENERGY_ESTIMATE:
137                         sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, forecastObject.getEnergyTimeSeries(mode));
138                         break;
139                     case CHANNEL_POWER_ESTIMATE:
140                         sendTimeSeries(CHANNEL_POWER_ESTIMATE, forecastObject.getPowerTimeSeries(mode));
141                         break;
142                     default:
143                         updateChannels(forecastObject);
144                 }
145             });
146         }
147     }
148
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);
156                     try {
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);
165
166                             // get forecast
167                             Request forecastRequest = httpClient.newRequest(forecastUrl);
168                             forecastRequest.header(HttpHeader.AUTHORIZATION, BEARER + bridge.getApiKey());
169                             ContentResponse crForecast = forecastRequest.send();
170
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);
177                             } else {
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()
182                                                 + "\"]");
183                             }
184                         } else {
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()
189                                             + "\"]");
190                         }
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();
196                     }
197                 } else {
198                     updateChannels(forecastObject);
199                 }
200             });
201         });
202         return forecast.get();
203     }
204
205     protected void updateChannels(SolcastObject f) {
206         if (bridgeHandler.isEmpty()) {
207             return;
208         }
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");
219             };
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)));
228         });
229     }
230
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 -> {
246             h.forecastUpdate();
247         });
248     }
249
250     @Override
251     public synchronized List<SolarForecast> getSolarForecasts() {
252         return List.of(forecast.get());
253     }
254 }