2 * Copyright (c) 2010-2022 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.darksky.internal.handler;
15 import static org.openhab.binding.darksky.internal.DarkSkyBindingConstants.*;
16 import static org.openhab.core.library.unit.MetricPrefix.*;
17 import static org.openhab.core.library.unit.SIUnits.*;
18 import static org.openhab.core.library.unit.Units.*;
20 import java.time.Instant;
21 import java.time.ZoneId;
22 import java.time.ZonedDateTime;
23 import java.time.format.DateTimeFormatter;
24 import java.time.temporal.ChronoUnit;
25 import java.util.ArrayList;
26 import java.util.Collections;
27 import java.util.List;
30 import java.util.concurrent.ConcurrentHashMap;
31 import java.util.concurrent.ScheduledFuture;
32 import java.util.concurrent.TimeUnit;
33 import java.util.regex.Matcher;
34 import java.util.regex.Pattern;
36 import javax.measure.Unit;
38 import org.eclipse.jdt.annotation.NonNullByDefault;
39 import org.eclipse.jdt.annotation.Nullable;
40 import org.openhab.binding.darksky.internal.config.DarkSkyChannelConfiguration;
41 import org.openhab.binding.darksky.internal.config.DarkSkyWeatherAndForecastConfiguration;
42 import org.openhab.binding.darksky.internal.connection.DarkSkyCommunicationException;
43 import org.openhab.binding.darksky.internal.connection.DarkSkyConfigurationException;
44 import org.openhab.binding.darksky.internal.connection.DarkSkyConnection;
45 import org.openhab.binding.darksky.internal.model.DarkSkyCurrentlyData;
46 import org.openhab.binding.darksky.internal.model.DarkSkyDailyData.DailyData;
47 import org.openhab.binding.darksky.internal.model.DarkSkyHourlyData.HourlyData;
48 import org.openhab.binding.darksky.internal.model.DarkSkyJsonWeatherData;
49 import org.openhab.binding.darksky.internal.model.DarkSkyJsonWeatherData.AlertsData;
50 import org.openhab.core.library.types.DateTimeType;
51 import org.openhab.core.library.types.DecimalType;
52 import org.openhab.core.library.types.PointType;
53 import org.openhab.core.library.types.QuantityType;
54 import org.openhab.core.library.types.RawType;
55 import org.openhab.core.library.types.StringType;
56 import org.openhab.core.thing.Channel;
57 import org.openhab.core.thing.ChannelGroupUID;
58 import org.openhab.core.thing.ChannelUID;
59 import org.openhab.core.thing.Thing;
60 import org.openhab.core.thing.ThingStatus;
61 import org.openhab.core.thing.ThingStatusDetail;
62 import org.openhab.core.thing.ThingStatusInfo;
63 import org.openhab.core.thing.ThingTypeUID;
64 import org.openhab.core.thing.binding.BaseThingHandler;
65 import org.openhab.core.thing.binding.ThingHandlerCallback;
66 import org.openhab.core.thing.binding.builder.ChannelBuilder;
67 import org.openhab.core.thing.binding.builder.ThingBuilder;
68 import org.openhab.core.thing.type.ChannelGroupTypeUID;
69 import org.openhab.core.thing.type.ChannelKind;
70 import org.openhab.core.types.Command;
71 import org.openhab.core.types.RefreshType;
72 import org.openhab.core.types.State;
73 import org.openhab.core.types.UnDefType;
74 import org.slf4j.Logger;
75 import org.slf4j.LoggerFactory;
77 import com.google.gson.JsonSyntaxException;
80 * The {@link DarkSkyWeatherAndForecastHandler} is responsible for handling commands, which are sent to one of the
83 * @author Christoph Weitkamp - Initial contribution
86 public class DarkSkyWeatherAndForecastHandler extends BaseThingHandler {
88 private final Logger logger = LoggerFactory.getLogger(DarkSkyWeatherAndForecastHandler.class);
90 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections
91 .singleton(THING_TYPE_WEATHER_AND_FORECAST);
93 private static final String PRECIP_TYPE_SNOW = "snow";
94 private static final String PRECIP_TYPE_RAIN = "rain";
96 private static final String CHANNEL_GROUP_HOURLY_FORECAST_PREFIX = "forecastHours";
97 private static final String CHANNEL_GROUP_DAILY_FORECAST_PREFIX = "forecastDay";
98 private static final String CHANNEL_GROUP_ALERTS_PREFIX = "alerts";
99 private static final Pattern CHANNEL_GROUP_HOURLY_FORECAST_PREFIX_PATTERN = Pattern
100 .compile(CHANNEL_GROUP_HOURLY_FORECAST_PREFIX + "([0-9]*)");
101 private static final Pattern CHANNEL_GROUP_DAILY_FORECAST_PREFIX_PATTERN = Pattern
102 .compile(CHANNEL_GROUP_DAILY_FORECAST_PREFIX + "([0-9]*)");
103 private static final Pattern CHANNEL_GROUP_ALERTS_PREFIX_PATTERN = Pattern
104 .compile(CHANNEL_GROUP_ALERTS_PREFIX + "([0-9]*)");
106 // keeps track of all jobs
107 private static final Map<String, Job> JOBS = new ConcurrentHashMap<>();
109 // keeps track of the parsed location
110 protected @Nullable PointType location;
111 // keeps track of the parsed counts
112 private int forecastHours = 24;
113 private int forecastDays = 8;
114 private int numberOfAlerts = 0;
116 private @Nullable DarkSkyChannelConfiguration sunriseTriggerChannelConfig;
117 private @Nullable DarkSkyChannelConfiguration sunsetTriggerChannelConfig;
118 private @Nullable DarkSkyJsonWeatherData weatherData;
120 public DarkSkyWeatherAndForecastHandler(Thing thing) {
125 public void initialize() {
126 logger.debug("Initialize DarkSkyWeatherAndForecastHandler handler '{}'.", getThing().getUID());
127 DarkSkyWeatherAndForecastConfiguration config = getConfigAs(DarkSkyWeatherAndForecastConfiguration.class);
129 boolean configValid = true;
130 if (config.location == null || config.location.trim().isEmpty()) {
131 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
132 "@text/offline.conf-error-missing-location");
137 location = new PointType(config.location);
138 } catch (IllegalArgumentException e) {
139 logger.warn("Error parsing 'location' parameter: {}", e.getMessage());
140 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
141 "@text/offline.conf-error-parsing-location");
146 int newForecastHours = config.forecastHours;
147 if (newForecastHours < 0 || newForecastHours > 48) {
148 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
149 "@text/offline.conf-error-not-supported-number-of-hours");
152 int newForecastDays = config.forecastDays;
153 if (newForecastDays < 0 || newForecastDays > 8) {
154 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
155 "@text/offline.conf-error-not-supported-number-of-days");
158 int newNumberOfAlerts = config.numberOfAlerts;
159 if (newNumberOfAlerts < 0) {
160 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
161 "@text/offline.conf-error-not-supported-number-of-alerts");
166 logger.debug("Rebuilding thing '{}'.", getThing().getUID());
167 List<Channel> toBeAddedChannels = new ArrayList<>();
168 List<Channel> toBeRemovedChannels = new ArrayList<>();
169 if (forecastHours != newForecastHours) {
170 logger.debug("Rebuilding hourly forecast channel groups.");
171 if (forecastHours > newForecastHours) {
172 for (int i = newForecastHours + 1; i <= forecastHours; ++i) {
173 toBeRemovedChannels.addAll(removeChannelsOfGroup(
174 CHANNEL_GROUP_HOURLY_FORECAST_PREFIX + ((i < 10) ? "0" : "") + Integer.toString(i)));
177 for (int i = forecastHours + 1; i <= newForecastHours; ++i) {
178 toBeAddedChannels.addAll(createChannelsForGroup(
179 CHANNEL_GROUP_HOURLY_FORECAST_PREFIX + ((i < 10) ? "0" : "") + Integer.toString(i),
180 CHANNEL_GROUP_TYPE_HOURLY_FORECAST));
183 forecastHours = newForecastHours;
185 if (forecastDays != newForecastDays) {
186 logger.debug("Rebuilding daily forecast channel groups.");
187 if (forecastDays > newForecastDays) {
188 if (newForecastDays < 1) {
189 toBeRemovedChannels.addAll(removeChannelsOfGroup(CHANNEL_GROUP_FORECAST_TODAY));
191 if (newForecastDays < 2) {
192 toBeRemovedChannels.addAll(removeChannelsOfGroup(CHANNEL_GROUP_FORECAST_TOMORROW));
194 for (int i = newForecastDays; i < forecastDays; ++i) {
195 toBeRemovedChannels.addAll(
196 removeChannelsOfGroup(CHANNEL_GROUP_DAILY_FORECAST_PREFIX + Integer.toString(i)));
199 if (forecastDays == 0 && newForecastDays > 0) {
200 toBeAddedChannels.addAll(createChannelsForGroup(CHANNEL_GROUP_FORECAST_TODAY,
201 CHANNEL_GROUP_TYPE_DAILY_FORECAST));
203 if (forecastDays <= 1 && newForecastDays > 1) {
204 toBeAddedChannels.addAll(createChannelsForGroup(CHANNEL_GROUP_FORECAST_TOMORROW,
205 CHANNEL_GROUP_TYPE_DAILY_FORECAST));
207 for (int i = (forecastDays < 2) ? 2 : forecastDays; i < newForecastDays; ++i) {
208 toBeAddedChannels.addAll(
209 createChannelsForGroup(CHANNEL_GROUP_DAILY_FORECAST_PREFIX + Integer.toString(i),
210 CHANNEL_GROUP_TYPE_DAILY_FORECAST));
213 forecastDays = newForecastDays;
215 if (numberOfAlerts != newNumberOfAlerts) {
216 logger.debug("Rebuilding alerts channel groups.");
217 if (numberOfAlerts > newNumberOfAlerts) {
218 for (int i = newNumberOfAlerts + 1; i <= numberOfAlerts; ++i) {
220 .addAll(removeChannelsOfGroup(CHANNEL_GROUP_ALERTS_PREFIX + Integer.toString(i)));
223 for (int i = numberOfAlerts + 1; i <= newNumberOfAlerts; ++i) {
224 toBeAddedChannels.addAll(createChannelsForGroup(
225 CHANNEL_GROUP_ALERTS_PREFIX + Integer.toString(i), CHANNEL_GROUP_TYPE_ALERTS));
228 numberOfAlerts = newNumberOfAlerts;
230 ThingBuilder builder = editThing().withoutChannels(toBeRemovedChannels);
231 for (Channel channel : toBeAddedChannels) {
232 builder.withChannel(channel);
234 updateThing(builder.build());
236 Channel sunriseTriggerChannel = getThing().getChannel(TRIGGER_SUNRISE);
237 sunriseTriggerChannelConfig = (sunriseTriggerChannel == null) ? null
238 : sunriseTriggerChannel.getConfiguration().as(DarkSkyChannelConfiguration.class);
239 Channel sunsetTriggerChannel = getThing().getChannel(TRIGGER_SUNSET);
240 sunsetTriggerChannelConfig = (sunsetTriggerChannel == null) ? null
241 : sunsetTriggerChannel.getConfiguration().as(DarkSkyChannelConfiguration.class);
243 updateStatus(ThingStatus.UNKNOWN);
248 public void dispose() {
253 public void handleCommand(ChannelUID channelUID, Command command) {
254 if (command instanceof RefreshType) {
255 updateChannel(channelUID);
257 logger.debug("The Dark Sky binding is a read-only binding and cannot handle command '{}'.", command);
262 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
263 if (ThingStatus.ONLINE.equals(bridgeStatusInfo.getStatus())
264 && ThingStatusDetail.BRIDGE_OFFLINE.equals(getThing().getStatusInfo().getStatusDetail())) {
265 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
266 } else if (ThingStatus.OFFLINE.equals(bridgeStatusInfo.getStatus())
267 && !ThingStatus.OFFLINE.equals(getThing().getStatus())) {
268 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
273 * Creates all {@link Channel}s for the given {@link ChannelGroupTypeUID}.
275 * @param channelGroupId the channel group id
276 * @param channelGroupTypeUID the {@link ChannelGroupTypeUID}
277 * @return a list of all {@link Channel}s for the channel group
279 private List<Channel> createChannelsForGroup(String channelGroupId, ChannelGroupTypeUID channelGroupTypeUID) {
280 logger.debug("Building channel group '{}' for thing '{}'.", channelGroupId, getThing().getUID());
281 List<Channel> channels = new ArrayList<>();
282 ThingHandlerCallback callback = getCallback();
283 if (callback != null) {
284 for (ChannelBuilder channelBuilder : callback.createChannelBuilders(
285 new ChannelGroupUID(getThing().getUID(), channelGroupId), channelGroupTypeUID)) {
286 Channel newChannel = channelBuilder.build(),
287 existingChannel = getThing().getChannel(newChannel.getUID().getId());
288 if (existingChannel != null) {
289 logger.trace("Thing '{}' already has an existing channel '{}'. Omit adding new channel '{}'.",
290 getThing().getUID(), existingChannel.getUID(), newChannel.getUID());
293 channels.add(newChannel);
300 * Removes all {@link Channel}s of the given channel group.
302 * @param channelGroupId the channel group id
303 * @return a list of all {@link Channel}s in the given channel group
305 private List<Channel> removeChannelsOfGroup(String channelGroupId) {
306 logger.debug("Removing channel group '{}' from thing '{}'.", channelGroupId, getThing().getUID());
307 return getThing().getChannelsOfGroup(channelGroupId);
311 * Updates Dark Sky data for this location.
313 * @param connection {@link DarkSkyConnection} instance
315 public void updateData(DarkSkyConnection connection) {
317 if (requestData(connection)) {
319 updateStatus(ThingStatus.ONLINE);
321 } catch (DarkSkyCommunicationException e) {
322 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
323 } catch (DarkSkyConfigurationException e) {
324 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getLocalizedMessage());
329 * Requests the data from Dark Sky API.
331 * @param connection {@link DarkSkyConnection} instance
332 * @return true, if the request for the Dark Sky data was successful
333 * @throws DarkSkyCommunicationException
334 * @throws DarkSkyConfigurationException
336 private boolean requestData(DarkSkyConnection connection)
337 throws DarkSkyCommunicationException, DarkSkyConfigurationException {
338 logger.debug("Update weather and forecast data of thing '{}'.", getThing().getUID());
340 weatherData = connection.getWeatherData(location);
342 } catch (JsonSyntaxException e) {
343 logger.debug("JsonSyntaxException occurred during execution: {}", e.getLocalizedMessage(), e);
349 * Updates all channels of this handler from the latest Dark Sky data retrieved.
351 private void updateChannels() {
352 for (Channel channel : getThing().getChannels()) {
353 ChannelUID channelUID = channel.getUID();
354 if (ChannelKind.STATE.equals(channel.getKind()) && channelUID.isInGroup() && channelUID.getGroupId() != null
355 && isLinked(channelUID)) {
356 updateChannel(channelUID);
362 * Updates the channel with the given UID from the latest Dark Sky data retrieved.
364 * @param channelUID UID of the channel
366 private void updateChannel(ChannelUID channelUID) {
367 String channelGroupId = channelUID.getGroupId();
368 switch (channelGroupId) {
369 case CHANNEL_GROUP_CURRENT_WEATHER:
370 updateCurrentChannel(channelUID);
372 case CHANNEL_GROUP_FORECAST_TODAY:
373 updateDailyForecastChannel(channelUID, 0);
375 case CHANNEL_GROUP_FORECAST_TOMORROW:
376 updateDailyForecastChannel(channelUID, 1);
380 Matcher hourlyForecastMatcher = CHANNEL_GROUP_HOURLY_FORECAST_PREFIX_PATTERN.matcher(channelGroupId);
381 if (hourlyForecastMatcher.find() && (i = Integer.parseInt(hourlyForecastMatcher.group(1))) >= 1
383 updateHourlyForecastChannel(channelUID, i);
386 Matcher dailyForecastMatcher = CHANNEL_GROUP_DAILY_FORECAST_PREFIX_PATTERN.matcher(channelGroupId);
387 if (dailyForecastMatcher.find() && (i = Integer.parseInt(dailyForecastMatcher.group(1))) > 1
389 updateDailyForecastChannel(channelUID, i);
392 Matcher alertsMatcher = CHANNEL_GROUP_ALERTS_PREFIX_PATTERN.matcher(channelGroupId);
393 if (alertsMatcher.find() && (i = Integer.parseInt(alertsMatcher.group(1))) >= 1) {
394 updateAlertsChannel(channelUID, i);
397 logger.warn("Unknown channel group '{}'. Cannot update channel '{}'.", channelGroupId, channelUID);
403 * Update the channel from the last Dark Sky data retrieved.
405 * @param channelUID the id identifying the channel to be updated
407 private void updateCurrentChannel(ChannelUID channelUID) {
408 String channelId = channelUID.getIdWithoutGroup();
409 String channelGroupId = channelUID.getGroupId();
410 if (weatherData != null && weatherData.getCurrently() != null) {
411 DarkSkyCurrentlyData currentData = weatherData.getCurrently();
412 State state = UnDefType.UNDEF;
414 case CHANNEL_TIME_STAMP:
415 state = getDateTimeTypeState(currentData.getTime());
417 case CHANNEL_CONDITION:
418 state = getStringTypeState(currentData.getSummary());
420 case CHANNEL_CONDITION_ICON:
421 state = getRawTypeState(DarkSkyConnection.getWeatherIcon(currentData.getIcon()));
423 case CHANNEL_CONDITION_ICON_ID:
424 state = getStringTypeState(currentData.getIcon());
426 case CHANNEL_TEMPERATURE:
427 state = getQuantityTypeState(currentData.getTemperature(), CELSIUS);
429 case CHANNEL_APPARENT_TEMPERATURE:
430 state = getQuantityTypeState(currentData.getApparentTemperature(), CELSIUS);
432 case CHANNEL_PRESSURE:
433 state = getQuantityTypeState(currentData.getPressure(), HECTO(PASCAL));
435 case CHANNEL_HUMIDITY:
436 state = getQuantityTypeState(currentData.getHumidity() * 100, PERCENT);
438 case CHANNEL_WIND_SPEED:
439 state = getQuantityTypeState(currentData.getWindSpeed(), METRE_PER_SECOND);
441 case CHANNEL_WIND_DIRECTION:
442 state = getQuantityTypeState(currentData.getWindBearing(), DEGREE_ANGLE);
444 case CHANNEL_GUST_SPEED:
445 state = getQuantityTypeState(currentData.getWindGust(), METRE_PER_SECOND);
447 case CHANNEL_CLOUDINESS:
448 state = getQuantityTypeState(currentData.getCloudCover() * 100, PERCENT);
450 case CHANNEL_VISIBILITY:
451 state = getQuantityTypeState(currentData.getVisibility(), KILO(METRE));
454 state = getQuantityTypeState(
455 PRECIP_TYPE_RAIN.equals(currentData.getPrecipType()) ? currentData.getPrecipIntensity() : 0,
456 MILLIMETRE_PER_HOUR);
459 state = getQuantityTypeState(
460 PRECIP_TYPE_SNOW.equals(currentData.getPrecipType()) ? currentData.getPrecipIntensity() : 0,
461 MILLIMETRE_PER_HOUR);
463 case CHANNEL_PRECIPITATION_INTENSITY:
464 state = getQuantityTypeState(currentData.getPrecipIntensity(), MILLIMETRE_PER_HOUR);
466 case CHANNEL_PRECIPITATION_PROBABILITY:
467 state = getQuantityTypeState(currentData.getPrecipProbability() * 100, PERCENT);
469 case CHANNEL_PRECIPITATION_TYPE:
470 state = getStringTypeState(currentData.getPrecipType());
472 case CHANNEL_UVINDEX:
473 state = getDecimalTypeState(currentData.getUvIndex());
476 state = getQuantityTypeState(currentData.getOzone(), DOBSON_UNIT);
478 case CHANNEL_SUNRISE:
480 updateDailyForecastChannel(channelUID, 0);
483 logger.debug("Update channel '{}' of group '{}' with new state '{}'.", channelId, channelGroupId, state);
484 updateState(channelUID, state);
486 logger.debug("No weather data available to update channel '{}' of group '{}'.", channelId, channelGroupId);
491 * Update the channel from the last Dark Sky data retrieved.
493 * @param channelUID the id identifying the channel to be updated
496 private void updateHourlyForecastChannel(ChannelUID channelUID, int count) {
497 String channelId = channelUID.getIdWithoutGroup();
498 String channelGroupId = channelUID.getGroupId();
499 if (weatherData != null && weatherData.getHourly() != null
500 && weatherData.getHourly().getData().size() > count) {
501 HourlyData forecastData = weatherData.getHourly().getData().get(count);
502 State state = UnDefType.UNDEF;
504 case CHANNEL_TIME_STAMP:
505 state = getDateTimeTypeState(forecastData.getTime());
507 case CHANNEL_CONDITION:
508 state = getStringTypeState(forecastData.getSummary());
510 case CHANNEL_CONDITION_ICON:
511 state = getRawTypeState(DarkSkyConnection.getWeatherIcon(forecastData.getIcon()));
513 case CHANNEL_CONDITION_ICON_ID:
514 state = getStringTypeState(forecastData.getIcon());
516 case CHANNEL_TEMPERATURE:
517 state = getQuantityTypeState(forecastData.getTemperature(), CELSIUS);
519 case CHANNEL_APPARENT_TEMPERATURE:
520 state = getQuantityTypeState(forecastData.getApparentTemperature(), CELSIUS);
522 case CHANNEL_PRESSURE:
523 state = getQuantityTypeState(forecastData.getPressure(), HECTO(PASCAL));
525 case CHANNEL_HUMIDITY:
526 state = getQuantityTypeState(forecastData.getHumidity() * 100, PERCENT);
528 case CHANNEL_WIND_SPEED:
529 state = getQuantityTypeState(forecastData.getWindSpeed(), METRE_PER_SECOND);
531 case CHANNEL_WIND_DIRECTION:
532 state = getQuantityTypeState(forecastData.getWindBearing(), DEGREE_ANGLE);
534 case CHANNEL_GUST_SPEED:
535 state = getQuantityTypeState(forecastData.getWindGust(), METRE_PER_SECOND);
537 case CHANNEL_CLOUDINESS:
538 state = getQuantityTypeState(forecastData.getCloudCover() * 100, PERCENT);
540 case CHANNEL_VISIBILITY:
541 state = getQuantityTypeState(forecastData.getVisibility(), KILO(METRE));
544 state = getQuantityTypeState(
545 PRECIP_TYPE_RAIN.equals(forecastData.getPrecipType()) ? forecastData.getPrecipIntensity()
547 MILLIMETRE_PER_HOUR);
550 state = getQuantityTypeState(
551 PRECIP_TYPE_SNOW.equals(forecastData.getPrecipType()) ? forecastData.getPrecipIntensity()
553 MILLIMETRE_PER_HOUR);
555 case CHANNEL_PRECIPITATION_INTENSITY:
556 state = getQuantityTypeState(forecastData.getPrecipIntensity(), MILLIMETRE_PER_HOUR);
558 case CHANNEL_PRECIPITATION_PROBABILITY:
559 state = getQuantityTypeState(forecastData.getPrecipProbability() * 100, PERCENT);
561 case CHANNEL_PRECIPITATION_TYPE:
562 state = getStringTypeState(forecastData.getPrecipType());
564 case CHANNEL_UVINDEX:
565 state = getDecimalTypeState(forecastData.getUvIndex());
568 state = getQuantityTypeState(forecastData.getOzone(), DOBSON_UNIT);
571 logger.debug("Update channel '{}' of group '{}' with new state '{}'.", channelId, channelGroupId, state);
572 updateState(channelUID, state);
574 logger.debug("No weather data available to update channel '{}' of group '{}'.", channelId, channelGroupId);
579 * Update the channel from the last Dark Sky data retrieved.
581 * @param channelUID the id identifying the channel to be updated
584 private void updateDailyForecastChannel(ChannelUID channelUID, int count) {
585 String channelId = channelUID.getIdWithoutGroup();
586 String channelGroupId = channelUID.getGroupId();
587 if (weatherData != null && weatherData.getDaily() != null && weatherData.getDaily().getData().size() > count) {
588 DailyData forecastData = weatherData.getDaily().getData().get(count);
589 State state = UnDefType.UNDEF;
591 case CHANNEL_TIME_STAMP:
592 state = getDateTimeTypeState(forecastData.getTime());
594 case CHANNEL_CONDITION:
595 state = getStringTypeState(forecastData.getSummary());
597 case CHANNEL_CONDITION_ICON:
598 state = getRawTypeState(DarkSkyConnection.getWeatherIcon(forecastData.getIcon()));
600 case CHANNEL_CONDITION_ICON_ID:
601 state = getStringTypeState(forecastData.getIcon());
603 case CHANNEL_MIN_TEMPERATURE:
604 state = getQuantityTypeState(forecastData.getTemperatureMin(), CELSIUS);
606 case CHANNEL_MAX_TEMPERATURE:
607 state = getQuantityTypeState(forecastData.getTemperatureMax(), CELSIUS);
609 case CHANNEL_MIN_APPARENT_TEMPERATURE:
610 state = getQuantityTypeState(forecastData.getApparentTemperatureMin(), CELSIUS);
612 case CHANNEL_MAX_APPARENT_TEMPERATURE:
613 state = getQuantityTypeState(forecastData.getApparentTemperatureMax(), CELSIUS);
615 case CHANNEL_PRESSURE:
616 state = getQuantityTypeState(forecastData.getPressure(), HECTO(PASCAL));
618 case CHANNEL_HUMIDITY:
619 state = getQuantityTypeState(forecastData.getHumidity() * 100, PERCENT);
621 case CHANNEL_WIND_SPEED:
622 state = getQuantityTypeState(forecastData.getWindSpeed(), METRE_PER_SECOND);
624 case CHANNEL_WIND_DIRECTION:
625 state = getQuantityTypeState(forecastData.getWindBearing(), DEGREE_ANGLE);
627 case CHANNEL_GUST_SPEED:
628 state = getQuantityTypeState(forecastData.getWindGust(), METRE_PER_SECOND);
630 case CHANNEL_CLOUDINESS:
631 state = getQuantityTypeState(forecastData.getCloudCover() * 100, PERCENT);
633 case CHANNEL_VISIBILITY:
634 state = getQuantityTypeState(forecastData.getVisibility(), KILO(METRE));
637 state = getQuantityTypeState(
638 PRECIP_TYPE_RAIN.equals(forecastData.getPrecipType()) ? forecastData.getPrecipIntensity()
640 MILLIMETRE_PER_HOUR);
643 state = getQuantityTypeState(
644 PRECIP_TYPE_SNOW.equals(forecastData.getPrecipType()) ? forecastData.getPrecipIntensity()
646 MILLIMETRE_PER_HOUR);
648 case CHANNEL_PRECIPITATION_INTENSITY:
649 state = getQuantityTypeState(forecastData.getPrecipIntensity(), MILLIMETRE_PER_HOUR);
651 case CHANNEL_PRECIPITATION_PROBABILITY:
652 state = getQuantityTypeState(forecastData.getPrecipProbability() * 100, PERCENT);
654 case CHANNEL_PRECIPITATION_TYPE:
655 state = getStringTypeState(forecastData.getPrecipType());
657 case CHANNEL_UVINDEX:
658 state = getDecimalTypeState(forecastData.getUvIndex());
661 state = getQuantityTypeState(forecastData.getOzone(), DOBSON_UNIT);
663 case CHANNEL_SUNRISE:
664 state = getDateTimeTypeState(forecastData.getSunriseTime());
665 if (count == 0 && state instanceof DateTimeType) {
666 scheduleJob(TRIGGER_SUNRISE, applyChannelConfig(((DateTimeType) state).getZonedDateTime(),
667 sunriseTriggerChannelConfig));
671 state = getDateTimeTypeState(forecastData.getSunsetTime());
672 if (count == 0 && state instanceof DateTimeType) {
673 scheduleJob(TRIGGER_SUNSET, applyChannelConfig(((DateTimeType) state).getZonedDateTime(),
674 sunsetTriggerChannelConfig));
678 logger.debug("Update channel '{}' of group '{}' with new state '{}'.", channelId, channelGroupId, state);
679 updateState(channelUID, state);
681 logger.debug("No weather data available to update channel '{}' of group '{}'.", channelId, channelGroupId);
686 * Update the channel from the last Dark Sky data retrieved.
688 * @param channelUID the id identifying the channel to be updated
691 private void updateAlertsChannel(ChannelUID channelUID, int count) {
692 String channelId = channelUID.getIdWithoutGroup();
693 String channelGroupId = channelUID.getGroupId();
694 List<AlertsData> alerts = weatherData != null ? weatherData.getAlerts() : null;
695 State state = UnDefType.UNDEF;
696 if (alerts != null && alerts.size() > count) {
697 AlertsData alertsData = alerts.get(count - 1);
699 case CHANNEL_ALERT_TITLE:
700 state = getStringTypeState(alertsData.title);
702 case CHANNEL_ALERT_DESCRIPTION:
703 state = getStringTypeState(alertsData.description);
705 case CHANNEL_ALERT_SEVERITY:
706 state = getStringTypeState(alertsData.severity);
708 case CHANNEL_ALERT_ISSUED:
709 state = getDateTimeTypeState(alertsData.time);
711 case CHANNEL_ALERT_EXPIRES:
712 state = getDateTimeTypeState(alertsData.expires);
714 case CHANNEL_ALERT_URI:
715 state = getStringTypeState(alertsData.uri);
718 logger.debug("Update channel '{}' of group '{}' with new state '{}'.", channelId, channelGroupId, state);
720 logger.debug("No data available to update channel '{}' of group '{}'.", channelId, channelGroupId);
722 updateState(channelUID, state);
725 private State getDateTimeTypeState(int value) {
726 return new DateTimeType(ZonedDateTime.ofInstant(Instant.ofEpochSecond(value), ZoneId.systemDefault()));
729 private State getDecimalTypeState(int value) {
730 return new DecimalType(value);
733 private State getRawTypeState(@Nullable RawType image) {
734 return (image == null) ? UnDefType.UNDEF : image;
737 private State getStringTypeState(@Nullable String value) {
738 return (value == null) ? UnDefType.UNDEF : new StringType(value);
741 private State getQuantityTypeState(double value, Unit<?> unit) {
742 return new QuantityType<>(value, unit);
746 * Applies the given configuration to the given timestamp.
748 * @param dateTime timestamp represented as {@link ZonedDateTime}
749 * @param config {@link DarkSkyChannelConfiguration} instance
750 * @return the modified timestamp
752 private ZonedDateTime applyChannelConfig(ZonedDateTime dateTime, @Nullable DarkSkyChannelConfiguration config) {
753 ZonedDateTime modifiedDateTime = dateTime;
754 if (config != null) {
755 if (config.getOffset() != 0) {
756 if (logger.isTraceEnabled()) {
757 logger.trace("Apply offset of {} min to timestamp '{}'.", config.getOffset(),
758 modifiedDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
760 modifiedDateTime = modifiedDateTime.plusMinutes(config.getOffset());
762 long earliestInMinutes = config.getEarliestInMinutes();
763 if (earliestInMinutes > 0) {
764 ZonedDateTime earliestDateTime = modifiedDateTime.truncatedTo(ChronoUnit.DAYS)
765 .plusMinutes(earliestInMinutes);
766 if (modifiedDateTime.isBefore(earliestDateTime)) {
767 if (logger.isTraceEnabled()) {
768 logger.trace("Use earliest timestamp '{}' instead of '{}'.",
769 earliestDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
770 modifiedDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
772 return earliestDateTime;
775 long latestInMinutes = config.getLatestInMinutes();
776 if (latestInMinutes > 0) {
777 ZonedDateTime latestDateTime = modifiedDateTime.truncatedTo(ChronoUnit.DAYS)
778 .plusMinutes(latestInMinutes);
779 if (modifiedDateTime.isAfter(latestDateTime)) {
780 if (logger.isTraceEnabled()) {
781 logger.trace("Use latest timestamp '{}' instead of '{}'.",
782 latestDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
783 modifiedDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
785 return latestDateTime;
789 return modifiedDateTime;
793 * Schedules or reschedules a job for the channel with the given id if the given timestamp is in the future.
795 * @param channelId id of the channel
796 * @param dateTime timestamp of the job represented as {@link ZonedDateTime}
798 @SuppressWarnings("null")
799 private synchronized void scheduleJob(String channelId, ZonedDateTime dateTime) {
800 long delay = dateTime.toEpochSecond() - ZonedDateTime.now().toEpochSecond();
802 Job job = JOBS.get(channelId);
803 if (job == null || job.getFuture().isCancelled()) {
804 if (logger.isDebugEnabled()) {
805 logger.debug("Schedule job for '{}' in {} s (at '{}').", channelId, delay,
806 dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
808 JOBS.put(channelId, new Job(channelId, delay));
810 if (delay != job.getDelay()) {
811 if (logger.isDebugEnabled()) {
812 logger.debug("Reschedule job for '{}' in {} s (at '{}').", channelId, delay,
813 dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
815 job.getFuture().cancel(true);
816 JOBS.put(channelId, new Job(channelId, delay));
825 private void cancelAllJobs() {
826 logger.debug("Cancel all jobs.");
827 JOBS.keySet().forEach(this::cancelJob);
831 * Cancels the job for the channel with the given id.
833 * @param channelId id of the channel
835 @SuppressWarnings("null")
836 private synchronized void cancelJob(String channelId) {
837 Job job = JOBS.remove(channelId);
838 if (job != null && !job.getFuture().isCancelled()) {
839 logger.debug("Cancel job for '{}'.", channelId);
840 job.getFuture().cancel(true);
845 * Executes the job for the channel with the given id.
847 * @param channelId id of the channel
849 private void executeJob(String channelId) {
850 logger.debug("Trigger channel '{}' with event '{}'.", channelId, EVENT_START);
851 triggerChannel(channelId, EVENT_START);
854 private final class Job {
855 private final long delay;
856 private final ScheduledFuture<?> future;
858 public Job(String event, long delay) {
860 this.future = scheduler.schedule(() -> {
862 }, delay, TimeUnit.SECONDS);
865 public long getDelay() {
869 public ScheduledFuture<?> getFuture() {