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.openuv.internal.handler;
15 import static org.openhab.binding.openuv.internal.OpenUVBindingConstants.*;
17 import java.time.ZoneId;
18 import java.time.ZonedDateTime;
19 import java.time.temporal.ChronoUnit;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.openuv.internal.config.ReportConfiguration;
27 import org.openhab.binding.openuv.internal.config.SafeExposureConfiguration;
28 import org.openhab.binding.openuv.internal.json.OpenUVResult;
29 import org.openhab.core.library.types.DateTimeType;
30 import org.openhab.core.library.types.DecimalType;
31 import org.openhab.core.library.types.HSBType;
32 import org.openhab.core.library.types.PointType;
33 import org.openhab.core.library.types.QuantityType;
34 import org.openhab.core.library.unit.Units;
35 import org.openhab.core.thing.Bridge;
36 import org.openhab.core.thing.Channel;
37 import org.openhab.core.thing.ChannelUID;
38 import org.openhab.core.thing.Thing;
39 import org.openhab.core.thing.ThingStatus;
40 import org.openhab.core.thing.ThingStatusDetail;
41 import org.openhab.core.thing.ThingStatusInfo;
42 import org.openhab.core.thing.binding.BaseThingHandler;
43 import org.openhab.core.thing.type.ChannelTypeUID;
44 import org.openhab.core.types.Command;
45 import org.openhab.core.types.RefreshType;
46 import org.openhab.core.types.State;
47 import org.openhab.core.types.UnDefType;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
52 * The {@link OpenUVReportHandler} is responsible for handling commands, which are
53 * sent to one of the channels.
55 * @author Gaƫl L'hopital - Initial contribution
58 public class OpenUVReportHandler extends BaseThingHandler {
59 private static final State ALERT_GREEN = DecimalType.ZERO;
60 private static final State ALERT_YELLOW = new DecimalType(1);
61 private static final State ALERT_ORANGE = new DecimalType(2);
62 private static final State ALERT_RED = new DecimalType(3);
63 private static final State ALERT_PURPLE = new DecimalType(4);
64 private static final State ALERT_UNDEF = HSBType.fromRGB(179, 179, 179);
66 private static final Map<State, State> ALERT_COLORS = Map.of(ALERT_GREEN, HSBType.fromRGB(85, 139, 47),
67 ALERT_YELLOW, HSBType.fromRGB(249, 168, 37), ALERT_ORANGE, HSBType.fromRGB(239, 108, 0), ALERT_RED,
68 HSBType.fromRGB(183, 28, 28), ALERT_PURPLE, HSBType.fromRGB(106, 27, 154));
70 private final Logger logger = LoggerFactory.getLogger(OpenUVReportHandler.class);
72 private @NonNullByDefault({}) OpenUVBridgeHandler bridgeHandler;
73 private @Nullable ScheduledFuture<?> refreshJob;
74 private @Nullable ScheduledFuture<?> uvMaxJob;
75 private boolean suspendUpdates = false;
77 public OpenUVReportHandler(Thing thing) {
82 public void initialize() {
83 logger.debug("Initializing OpenUV handler.");
85 ReportConfiguration config = getConfigAs(ReportConfiguration.class);
87 if (config.refresh < 3) {
88 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
89 "@text/offline.config-error-invalid-refresh");
91 Bridge bridge = getBridge();
93 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
95 bridgeHandler = (OpenUVBridgeHandler) bridge.getHandler();
96 updateStatus(ThingStatus.UNKNOWN);
97 startAutomaticRefresh();
103 * Start the job screening UV Max reached
107 private void scheduleUVMaxEvent(OpenUVResult openUVData) {
108 ScheduledFuture<?> job = this.uvMaxJob;
109 if ((job == null || job.isCancelled())) {
110 State uvMaxTime = openUVData.getUVMaxTime();
111 if (uvMaxTime != UnDefType.NULL) {
112 ZonedDateTime uvMaxZdt = ((DateTimeType) uvMaxTime).getZonedDateTime();
113 long timeDiff = ChronoUnit.MINUTES.between(ZonedDateTime.now(ZoneId.systemDefault()), uvMaxZdt);
115 logger.debug("Scheduling {} in {} minutes", UV_MAX_EVENT, timeDiff);
116 uvMaxJob = scheduler.schedule(() -> {
117 triggerChannel(UV_MAX_EVENT);
119 }, timeDiff, TimeUnit.MINUTES);
126 * Start the job refreshing the data
128 private void startAutomaticRefresh() {
129 ScheduledFuture<?> job = this.refreshJob;
130 if (job == null || job.isCancelled()) {
131 ReportConfiguration config = getConfigAs(ReportConfiguration.class);
132 refreshJob = scheduler.scheduleWithFixedDelay(() -> {
133 if (!suspendUpdates) {
134 updateChannels(config);
136 }, 0, config.refresh, TimeUnit.MINUTES);
140 private void updateChannels(ReportConfiguration config) {
141 ThingStatusInfo bridgeStatusInfo = bridgeHandler.getThing().getStatusInfo();
142 if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
143 PointType location = new PointType(config.location);
144 OpenUVResult openUVData = bridgeHandler.getUVData(location.getLatitude().toString(),
145 location.getLongitude().toString(), location.getAltitude().toString());
146 if (openUVData != null) {
147 scheduleUVMaxEvent(openUVData);
148 getThing().getChannels().stream().filter(channel -> isLinked(channel.getUID().getId()))
149 .forEach(channel -> updateState(channel.getUID(), getState(channel, openUVData)));
150 updateStatus(ThingStatus.ONLINE);
152 updateStatus(ThingStatus.OFFLINE, bridgeStatusInfo.getStatusDetail(),
153 bridgeStatusInfo.getDescription());
159 public void dispose() {
160 logger.debug("Disposing the OpenUV handler.");
161 ScheduledFuture<?> refresh = refreshJob;
162 if (refresh != null && !refresh.isCancelled()) {
163 refresh.cancel(true);
167 ScheduledFuture<?> uxMax = uvMaxJob;
168 if (uxMax != null && !uxMax.isCancelled()) {
175 public void handleCommand(ChannelUID channelUID, Command command) {
176 if (command instanceof RefreshType) {
177 scheduler.execute(() -> {
178 ReportConfiguration config = getConfigAs(ReportConfiguration.class);
179 updateChannels(config);
181 } else if (ELEVATION.equals(channelUID.getId()) && command instanceof QuantityType) {
182 QuantityType<?> qtty = (QuantityType<?>) command;
183 if (Units.DEGREE_ANGLE.equals(qtty.getUnit())) {
184 suspendUpdates = qtty.doubleValue() < 0;
186 logger.info("The OpenUV Report handles Sun Elevation of Number:Angle type, {} does not fit.", command);
189 logger.info("The OpenUV Report Thing handles Refresh or Sun Elevation command and not '{}'", command);
193 private State getState(Channel channel, OpenUVResult openUVData) {
194 ChannelUID uid = channel.getUID();
195 switch (uid.getId()) {
197 return new DecimalType(openUVData.getUv());
199 return asAlertLevel(openUVData.getUv());
201 return ALERT_COLORS.getOrDefault(asAlertLevel(openUVData.getUv()), ALERT_UNDEF);
203 return new DecimalType(openUVData.getUvMax());
205 return new QuantityType<>(openUVData.getOzone(), Units.DOBSON_UNIT);
207 return openUVData.getOzoneTime();
209 return openUVData.getUVMaxTime();
211 return openUVData.getUVTime();
213 ChannelTypeUID channelType = channel.getChannelTypeUID();
214 if (channelType != null && SAFE_EXPOSURE.equals(channelType.getId())) {
215 SafeExposureConfiguration configuration = channel.getConfiguration().as(SafeExposureConfiguration.class);
216 return openUVData.getSafeExposureTime(configuration.index);
218 return UnDefType.NULL;
221 private State asAlertLevel(double uv) {
224 } else if (uv >= 8) {
226 } else if (uv >= 6) {
228 } else if (uv >= 3) {
233 return UnDefType.NULL;