2 * Copyright (c) 2010-2023 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.windcentrale.internal.handler;
15 import static org.openhab.binding.windcentrale.internal.WindcentraleBindingConstants.*;
16 import static org.openhab.core.library.unit.MetricPrefix.KILO;
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;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
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;
55 * The {@link WindcentraleWindmillHandler} is responsible for handling commands, which are
56 * sent to one of the channels.
58 * @author Marcel Verpaalen - Initial contribution
59 * @author Wouter Born - Add null annotations
60 * @author Wouter Born - Add support for new API with authentication
63 public class WindcentraleWindmillHandler extends BaseThingHandler {
65 private static final long CACHE_EXPIRY = TimeUnit.SECONDS.toMillis(5);
67 private final Logger logger = LoggerFactory.getLogger(WindcentraleWindmillHandler.class);
69 private @NonNullByDefault({}) WindmillConfiguration config;
70 private @Nullable Windmill windmill;
72 private @Nullable ScheduledFuture<?> pollingJob;
74 private final ExpiringCache<@Nullable WindmillStatus> statusCache = new ExpiringCache<>(CACHE_EXPIRY, () -> {
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());
85 public WindcentraleWindmillHandler(Thing thing) {
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;
99 protected @Nullable WindcentraleAPI getAPI() {
100 Bridge bridge = getBridge();
101 if (bridge == null) {
104 WindcentraleAccountHandler accountHandler = ((WindcentraleAccountHandler) bridge.getHandler());
105 return accountHandler == null ? null : accountHandler.getAPI();
109 public void handleCommand(ChannelUID channelUID, Command command) {
110 if (command == RefreshType.REFRESH) {
111 logger.debug("Refreshing {}", channelUID);
114 logger.debug("This binding is a read-only binding and cannot handle commands");
119 public void initialize() {
120 logger.debug("Initializing Windcentrale handler '{}'", getThing().getUID());
122 WindmillConfiguration config = getConfig().as(WindmillConfiguration.class);
123 this.config = config;
125 Windmill windmill = Windmill.fromName(config.name);
126 this.windmill = windmill;
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);
135 updateProperties(getWindmillProperties(windmill));
136 updateStatus(ThingStatus.UNKNOWN);
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());
143 public static Map<String, String> getWindmillProperties(Windmill windmill) {
144 Map<String, String> properties = new HashMap<>();
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()));
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());
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);
169 private synchronized void updateData() {
170 logger.debug("Updating windmill data '{}'", getThing().getUID());
172 WindmillStatus status = statusCache.getValue();
173 if (status == null) {
177 logger.trace("Retrieved updated windmill status: {}", status);
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)
187 updateState(CHANNEL_RUN_TIME,
188 status.yearRuntime >= 0 ? new QuantityType<>(new BigDecimal(status.yearRuntime), Units.HOUR)
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));
194 if (!ThingStatus.ONLINE.equals(getThing().getStatus())) {
195 updateStatus(ThingStatus.ONLINE);