]> git.basschouten.com Git - openhab-addons.git/blob
7eddadc6b2ad82c175aaaa66eab17e6025c09ffa
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.windcentrale.internal.handler;
14
15 import static org.openhab.binding.windcentrale.internal.WindcentraleBindingConstants.*;
16 import static org.openhab.core.library.unit.MetricPrefix.KILO;
17
18 import java.math.BigDecimal;
19 import java.time.Duration;
20 import java.time.ZoneId;
21 import java.time.ZonedDateTime;
22 import java.time.temporal.ChronoUnit;
23 import java.util.HashMap;
24 import java.util.Map;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.windcentrale.internal.api.WindcentraleAPI;
31 import org.openhab.binding.windcentrale.internal.config.WindmillConfiguration;
32 import org.openhab.binding.windcentrale.internal.dto.Windmill;
33 import org.openhab.binding.windcentrale.internal.dto.WindmillStatus;
34 import org.openhab.binding.windcentrale.internal.exception.FailedGettingDataException;
35 import org.openhab.binding.windcentrale.internal.exception.InvalidAccessTokenException;
36 import org.openhab.core.cache.ExpiringCache;
37 import org.openhab.core.library.types.DateTimeType;
38 import org.openhab.core.library.types.DecimalType;
39 import org.openhab.core.library.types.QuantityType;
40 import org.openhab.core.library.types.StringType;
41 import org.openhab.core.library.unit.Units;
42 import org.openhab.core.thing.Bridge;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.thing.Thing;
45 import org.openhab.core.thing.ThingStatus;
46 import org.openhab.core.thing.ThingStatusDetail;
47 import org.openhab.core.thing.binding.BaseThingHandler;
48 import org.openhab.core.types.Command;
49 import org.openhab.core.types.RefreshType;
50 import org.openhab.core.types.UnDefType;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53
54 /**
55  * The {@link WindcentraleWindmillHandler} is responsible for handling commands, which are
56  * sent to one of the channels.
57  *
58  * @author Marcel Verpaalen - Initial contribution
59  * @author Wouter Born - Add null annotations
60  * @author Wouter Born - Add support for new API with authentication
61  */
62 @NonNullByDefault
63 public class WindcentraleWindmillHandler extends BaseThingHandler {
64
65     private static final long CACHE_EXPIRY = TimeUnit.SECONDS.toMillis(5);
66
67     private final Logger logger = LoggerFactory.getLogger(WindcentraleWindmillHandler.class);
68
69     private @NonNullByDefault({}) WindmillConfiguration config;
70     private @Nullable Windmill windmill;
71
72     private @Nullable ScheduledFuture<?> pollingJob;
73
74     private final ExpiringCache<@Nullable WindmillStatus> statusCache = new ExpiringCache<>(CACHE_EXPIRY, () -> {
75         try {
76             WindcentraleAPI api = getAPI();
77             Windmill windmill = this.windmill;
78             return api == null || windmill == null ? null : api.getLiveData(windmill);
79         } catch (FailedGettingDataException | InvalidAccessTokenException e) {
80             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
81             return null;
82         }
83     });
84
85     public WindcentraleWindmillHandler(Thing thing) {
86         super(thing);
87     }
88
89     @Override
90     public void dispose() {
91         logger.debug("Disposing Windcentrale handler '{}'", getThing().getUID());
92         final ScheduledFuture<?> pollingJob = this.pollingJob;
93         if (pollingJob != null) {
94             pollingJob.cancel(true);
95             this.pollingJob = null;
96         }
97     }
98
99     protected @Nullable WindcentraleAPI getAPI() {
100         Bridge bridge = getBridge();
101         if (bridge == null) {
102             return null;
103         }
104         WindcentraleAccountHandler accountHandler = ((WindcentraleAccountHandler) bridge.getHandler());
105         return accountHandler == null ? null : accountHandler.getAPI();
106     }
107
108     @Override
109     public void handleCommand(ChannelUID channelUID, Command command) {
110         if (command == RefreshType.REFRESH) {
111             logger.debug("Refreshing {}", channelUID);
112             updateData();
113         } else {
114             logger.debug("This binding is a read-only binding and cannot handle commands");
115         }
116     }
117
118     @Override
119     public void initialize() {
120         logger.debug("Initializing Windcentrale handler '{}'", getThing().getUID());
121
122         WindmillConfiguration config = getConfig().as(WindmillConfiguration.class);
123         this.config = config;
124
125         Windmill windmill = Windmill.fromName(config.name);
126         this.windmill = windmill;
127
128         if (windmill == null) {
129             // only occurs when a mismatch is introduced between config parameter options and enum values
130             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
131                     "Invalid windmill name: " + config.name);
132             return;
133         }
134
135         updateProperties(getWindmillProperties(windmill));
136         updateStatus(ThingStatus.UNKNOWN);
137
138         pollingJob = scheduler.scheduleWithFixedDelay(this::updateData, 0, config.refreshInterval, TimeUnit.SECONDS);
139         logger.debug("Polling job scheduled to run every {} sec. for '{}'", config.refreshInterval,
140                 getThing().getUID());
141     }
142
143     public static Map<String, String> getWindmillProperties(Windmill windmill) {
144         Map<String, String> properties = new HashMap<>();
145
146         properties.put(Thing.PROPERTY_VENDOR, "Windcentrale");
147         properties.put(Thing.PROPERTY_MODEL_ID, windmill.getType());
148         properties.put(Thing.PROPERTY_SERIAL_NUMBER, Integer.toString(windmill.getId()));
149
150         properties.put(PROPERTY_PROJECT_CODE, windmill.getProjectCode());
151         properties.put(PROPERTY_TOTAL_SHARES, Integer.toString(windmill.getTotalShares()));
152         properties.put(PROPERTY_BUILD_YEAR, Integer.toString(windmill.getBuildYear()));
153         properties.put(PROPERTY_MUNICIPALITY, windmill.getMunicipality());
154         properties.put(PROPERTY_PROVINCE, windmill.getProvince());
155         properties.put(PROPERTY_COORDINATES, windmill.getCoordinates());
156         properties.put(PROPERTY_DETAILS_URL, windmill.getDetailsUrl());
157
158         return properties;
159     }
160
161     private double yearRuntimePercentage(double yearRuntime) {
162         ZonedDateTime now = ZonedDateTime.now(ZoneId.of("Europe/Amsterdam"));
163         ZonedDateTime startOfThisYear = now.withDayOfMonth(1).withMonth(1).truncatedTo(ChronoUnit.DAYS);
164         long hoursThisYear = Duration.between(startOfThisYear, now).toHours();
165         // prevent divide by zero when the year has just started
166         return 100 * (hoursThisYear > 0 ? yearRuntime / hoursThisYear : 1);
167     }
168
169     private synchronized void updateData() {
170         logger.debug("Updating windmill data '{}'", getThing().getUID());
171
172         WindmillStatus status = statusCache.getValue();
173         if (status == null) {
174             return;
175         }
176
177         logger.trace("Retrieved updated windmill status: {}", status);
178
179         updateState(CHANNEL_ENERGY_TOTAL, new QuantityType<>(status.yearProduction, Units.KILOWATT_HOUR));
180         updateState(CHANNEL_POWER_RELATIVE, new QuantityType<>(status.powerPercentage, Units.PERCENT));
181         updateState(CHANNEL_POWER_SHARES, new QuantityType<>(
182                 new BigDecimal(status.powerPerShare).multiply(new BigDecimal(config.shares)), Units.WATT));
183         updateState(CHANNEL_POWER_TOTAL, new QuantityType<>(status.power, KILO(Units.WATT)));
184         updateState(CHANNEL_RUN_PERCENTAGE,
185                 status.yearRuntime >= 0 ? new QuantityType<>(yearRuntimePercentage(status.yearRuntime), Units.PERCENT)
186                         : UnDefType.UNDEF);
187         updateState(CHANNEL_RUN_TIME,
188                 status.yearRuntime >= 0 ? new QuantityType<>(new BigDecimal(status.yearRuntime), Units.HOUR)
189                         : UnDefType.UNDEF);
190         updateState(CHANNEL_WIND_DIRECTION, new StringType(status.windDirection));
191         updateState(CHANNEL_WIND_SPEED, new DecimalType(status.windPower));
192         updateState(CHANNEL_TIMESTAMP, new DateTimeType(status.timestamp));
193
194         if (!ThingStatus.ONLINE.equals(getThing().getStatus())) {
195             updateStatus(ThingStatus.ONLINE);
196         }
197     }
198 }