]> git.basschouten.com Git - openhab-addons.git/blob
487d93e86dd8bb985564bdaee6df42117d61cd69
[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.util.ArrayList;
20 import java.util.Collection;
21 import java.util.Iterator;
22 import java.util.List;
23 import java.util.Optional;
24 import java.util.TreeMap;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.openhab.binding.solarforecast.internal.SolarForecastException;
30 import org.openhab.binding.solarforecast.internal.actions.SolarForecast;
31 import org.openhab.binding.solarforecast.internal.actions.SolarForecastActions;
32 import org.openhab.binding.solarforecast.internal.actions.SolarForecastProvider;
33 import org.openhab.binding.solarforecast.internal.forecastsolar.ForecastSolarObject;
34 import org.openhab.binding.solarforecast.internal.forecastsolar.config.ForecastSolarBridgeConfiguration;
35 import org.openhab.binding.solarforecast.internal.solcast.SolcastObject.QueryMode;
36 import org.openhab.binding.solarforecast.internal.utils.Utils;
37 import org.openhab.core.config.core.Configuration;
38 import org.openhab.core.library.types.PointType;
39 import org.openhab.core.library.types.QuantityType;
40 import org.openhab.core.thing.Bridge;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.ThingStatus;
43 import org.openhab.core.thing.ThingStatusDetail;
44 import org.openhab.core.thing.binding.BaseBridgeHandler;
45 import org.openhab.core.thing.binding.ThingHandlerService;
46 import org.openhab.core.types.Command;
47 import org.openhab.core.types.RefreshType;
48 import org.openhab.core.types.TimeSeries;
49 import org.openhab.core.types.TimeSeries.Policy;
50
51 /**
52  * The {@link ForecastSolarBridgeHandler} is a non active handler instance. It will be triggerer by the bridge.
53  *
54  * @author Bernd Weymann - Initial contribution
55  */
56 @NonNullByDefault
57 public class ForecastSolarBridgeHandler extends BaseBridgeHandler implements SolarForecastProvider {
58     private List<ForecastSolarPlaneHandler> planes = new ArrayList<>();
59     private Optional<PointType> homeLocation;
60     private Optional<ForecastSolarBridgeConfiguration> configuration = Optional.empty();
61     private Optional<ScheduledFuture<?>> refreshJob = Optional.empty();
62
63     public ForecastSolarBridgeHandler(Bridge bridge, Optional<PointType> location) {
64         super(bridge);
65         homeLocation = location;
66     }
67
68     @Override
69     public Collection<Class<? extends ThingHandlerService>> getServices() {
70         return List.of(SolarForecastActions.class);
71     }
72
73     @Override
74     public void initialize() {
75         ForecastSolarBridgeConfiguration config = getConfigAs(ForecastSolarBridgeConfiguration.class);
76         PointType locationConfigured;
77
78         // handle location error cases
79         if (config.location.isBlank()) {
80             if (homeLocation.isEmpty()) {
81                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
82                         "@text/solarforecast.site.status.location-missing");
83                 return;
84             } else {
85                 locationConfigured = homeLocation.get();
86                 // continue with openHAB location
87             }
88         } else {
89             try {
90                 locationConfigured = new PointType(config.location);
91                 // continue with location from configuration
92             } catch (Exception e) {
93                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
94                 return;
95             }
96         }
97         Configuration editConfig = editConfiguration();
98         editConfig.put("location", locationConfigured.toString());
99         updateConfiguration(editConfig);
100         config = getConfigAs(ForecastSolarBridgeConfiguration.class);
101         configuration = Optional.of(config);
102         updateStatus(ThingStatus.UNKNOWN);
103         refreshJob = Optional
104                 .of(scheduler.scheduleWithFixedDelay(this::getData, 0, REFRESH_ACTUAL_INTERVAL, TimeUnit.MINUTES));
105     }
106
107     @Override
108     public void handleCommand(ChannelUID channelUID, Command command) {
109         if (command instanceof RefreshType) {
110             String channel = channelUID.getIdWithoutGroup();
111             switch (channel) {
112                 case CHANNEL_ENERGY_ACTUAL:
113                 case CHANNEL_ENERGY_REMAIN:
114                 case CHANNEL_ENERGY_TODAY:
115                 case CHANNEL_POWER_ACTUAL:
116                     getData();
117                     break;
118                 case CHANNEL_POWER_ESTIMATE:
119                 case CHANNEL_ENERGY_ESTIMATE:
120                     forecastUpdate();
121                     break;
122             }
123         }
124     }
125
126     /**
127      * Get data for all planes. Synchronized to protect plane list from being modified during update
128      */
129     private synchronized void getData() {
130         if (planes.isEmpty()) {
131             return;
132         }
133         boolean update = true;
134         double energySum = 0;
135         double powerSum = 0;
136         double daySum = 0;
137         for (Iterator<ForecastSolarPlaneHandler> iterator = planes.iterator(); iterator.hasNext();) {
138             try {
139                 ForecastSolarPlaneHandler sfph = iterator.next();
140                 ForecastSolarObject fo = sfph.fetchData();
141                 ZonedDateTime now = ZonedDateTime.now(fo.getZone());
142                 energySum += fo.getActualEnergyValue(now);
143                 powerSum += fo.getActualPowerValue(now);
144                 daySum += fo.getDayTotal(now.toLocalDate());
145             } catch (SolarForecastException sfe) {
146                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
147                         "@text/solarforecast.site.status.exception [\"" + sfe.getMessage() + "\"]");
148                 update = false;
149             }
150         }
151         if (update) {
152             updateStatus(ThingStatus.ONLINE);
153             updateState(CHANNEL_ENERGY_ACTUAL, Utils.getEnergyState(energySum));
154             updateState(CHANNEL_ENERGY_REMAIN, Utils.getEnergyState(daySum - energySum));
155             updateState(CHANNEL_ENERGY_TODAY, Utils.getEnergyState(daySum));
156             updateState(CHANNEL_POWER_ACTUAL, Utils.getPowerState(powerSum));
157         }
158     }
159
160     public synchronized void forecastUpdate() {
161         if (planes.isEmpty()) {
162             return;
163         }
164         TreeMap<Instant, QuantityType<?>> combinedPowerForecast = new TreeMap<>();
165         TreeMap<Instant, QuantityType<?>> combinedEnergyForecast = new TreeMap<>();
166         List<SolarForecast> forecastObjects = new ArrayList<>();
167         for (Iterator<ForecastSolarPlaneHandler> iterator = planes.iterator(); iterator.hasNext();) {
168             ForecastSolarPlaneHandler sfph = iterator.next();
169             forecastObjects.addAll(sfph.getSolarForecasts());
170         }
171
172         // bugfix: https://github.com/weymann/OH3-SolarForecast-Drops/issues/5
173         // find common start and end time which fits to all forecast objects to avoid ambiguous values
174         final Instant commonStart = Utils.getCommonStartTime(forecastObjects);
175         final Instant commonEnd = Utils.getCommonEndTime(forecastObjects);
176         forecastObjects.forEach(fc -> {
177             TimeSeries powerTS = fc.getPowerTimeSeries(QueryMode.Average);
178             powerTS.getStates().forEach(entry -> {
179                 if (Utils.isAfterOrEqual(entry.timestamp(), commonStart)
180                         && Utils.isBeforeOrEqual(entry.timestamp(), commonEnd)) {
181                     Utils.addState(combinedPowerForecast, entry);
182                 }
183             });
184             TimeSeries energyTS = fc.getEnergyTimeSeries(QueryMode.Average);
185             energyTS.getStates().forEach(entry -> {
186                 if (Utils.isAfterOrEqual(entry.timestamp(), commonStart)
187                         && Utils.isBeforeOrEqual(entry.timestamp(), commonEnd)) {
188                     Utils.addState(combinedEnergyForecast, entry);
189                 }
190             });
191         });
192
193         TimeSeries powerSeries = new TimeSeries(Policy.REPLACE);
194         combinedPowerForecast.forEach((timestamp, state) -> {
195             powerSeries.add(timestamp, state);
196         });
197         sendTimeSeries(CHANNEL_POWER_ESTIMATE, powerSeries);
198
199         TimeSeries energySeries = new TimeSeries(Policy.REPLACE);
200         combinedEnergyForecast.forEach((timestamp, state) -> {
201             energySeries.add(timestamp, state);
202         });
203         sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, energySeries);
204     }
205
206     @Override
207     public void dispose() {
208         refreshJob.ifPresent(job -> job.cancel(true));
209     }
210
211     public synchronized void addPlane(ForecastSolarPlaneHandler sfph) {
212         planes.add(sfph);
213         // update passive PV plane with necessary data
214         if (configuration.isPresent()) {
215             sfph.setLocation(new PointType(configuration.get().location));
216             if (!configuration.get().apiKey.isBlank()) {
217                 sfph.setApiKey(configuration.get().apiKey);
218             }
219         }
220         getData();
221     }
222
223     public synchronized void removePlane(ForecastSolarPlaneHandler sfph) {
224         planes.remove(sfph);
225     }
226
227     @Override
228     public synchronized List<SolarForecast> getSolarForecasts() {
229         List<SolarForecast> l = new ArrayList<SolarForecast>();
230         planes.forEach(entry -> {
231             l.addAll(entry.getSolarForecasts());
232         });
233         return l;
234     }
235 }