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.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;
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;
52 * The {@link ForecastSolarBridgeHandler} is a non active handler instance. It will be triggerer by the bridge.
54 * @author Bernd Weymann - Initial contribution
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();
63 public ForecastSolarBridgeHandler(Bridge bridge, Optional<PointType> location) {
65 homeLocation = location;
69 public Collection<Class<? extends ThingHandlerService>> getServices() {
70 return List.of(SolarForecastActions.class);
74 public void initialize() {
75 ForecastSolarBridgeConfiguration config = getConfigAs(ForecastSolarBridgeConfiguration.class);
76 PointType locationConfigured;
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");
85 locationConfigured = homeLocation.get();
86 // continue with openHAB location
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());
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));
108 public void handleCommand(ChannelUID channelUID, Command command) {
109 if (command instanceof RefreshType) {
110 String channel = channelUID.getIdWithoutGroup();
112 case CHANNEL_ENERGY_ACTUAL:
113 case CHANNEL_ENERGY_REMAIN:
114 case CHANNEL_ENERGY_TODAY:
115 case CHANNEL_POWER_ACTUAL:
118 case CHANNEL_POWER_ESTIMATE:
119 case CHANNEL_ENERGY_ESTIMATE:
127 * Get data for all planes. Synchronized to protect plane list from being modified during update
129 private synchronized void getData() {
130 if (planes.isEmpty()) {
133 boolean update = true;
134 double energySum = 0;
137 for (Iterator<ForecastSolarPlaneHandler> iterator = planes.iterator(); iterator.hasNext();) {
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() + "\"]");
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));
160 public synchronized void forecastUpdate() {
161 if (planes.isEmpty()) {
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());
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);
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);
193 TimeSeries powerSeries = new TimeSeries(Policy.REPLACE);
194 combinedPowerForecast.forEach((timestamp, state) -> {
195 powerSeries.add(timestamp, state);
197 sendTimeSeries(CHANNEL_POWER_ESTIMATE, powerSeries);
199 TimeSeries energySeries = new TimeSeries(Policy.REPLACE);
200 combinedEnergyForecast.forEach((timestamp, state) -> {
201 energySeries.add(timestamp, state);
203 sendTimeSeries(CHANNEL_ENERGY_ESTIMATE, energySeries);
207 public void dispose() {
208 refreshJob.ifPresent(job -> job.cancel(true));
211 public synchronized void addPlane(ForecastSolarPlaneHandler 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);
223 public synchronized void removePlane(ForecastSolarPlaneHandler sfph) {
228 public synchronized List<SolarForecast> getSolarForecasts() {
229 List<SolarForecast> l = new ArrayList<SolarForecast>();
230 planes.forEach(entry -> {
231 l.addAll(entry.getSolarForecasts());