diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifBindingConstants.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifBindingConstants.java index b56b567303a..8a9d22fbc2e 100755 --- a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifBindingConstants.java +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/AirParifBindingConstants.java @@ -45,6 +45,8 @@ public class AirParifBindingConstants { public static final String CHANNEL_COMMENT = "comment"; public static final String CHANNEL_MESSAGE = "message"; public static final String CHANNEL_TOMORROW = "tomorrow"; + public static final String CHANNEL_TIMESTAMP = "timestamp"; + public static final String CHANNEL_VALUE = "value"; public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(APIBRIDGE_THING_TYPE, LOCATION_THING_TYPE); diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/AirParifDto.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/AirParifDto.java index 701ac785661..24c6e9a728a 100644 --- a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/AirParifDto.java +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/api/AirParifDto.java @@ -12,6 +12,7 @@ */ package org.openhab.binding.airparif.internal.api; +import java.time.Duration; import java.time.LocalDate; import java.time.ZoneId; import java.time.ZonedDateTime; @@ -19,6 +20,7 @@ import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.TreeSet; @@ -29,6 +31,9 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.airparif.internal.api.AirParifApi.Pollen; import org.openhab.binding.airparif.internal.api.AirParifApi.Scope; +import org.openhab.core.library.types.StringType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; import com.google.gson.annotations.SerializedName; @@ -74,6 +79,10 @@ public class AirParifDto { public String dayDescription() { return bulletin.fr; } + + public boolean isToday() { + return previsionDate.equals(LocalDate.now()); + } } public record DailyEpisode(// @@ -106,28 +115,51 @@ public class AirParifDto { private static ZoneId DEFAULT_ZONE = ZoneId.of("Europe/Paris"); public List data = List.of(); + private @Nullable Set validities; + private @Nullable ZonedDateTime beginValidity; + private @Nullable ZonedDateTime endValidity; public Optional getData() { return Optional.ofNullable(data.isEmpty() ? null : data.get(0)); } private Set getValidities() { - Set result = new TreeSet<>(); - getData().ifPresent(pollens -> { - Matcher matcher = PATTERN.matcher(pollens.periode); - while (matcher.find()) { - result.add(LocalDate.parse(matcher.group(), FORMATTER).atStartOfDay(DEFAULT_ZONE)); - } - }); - return result; + Set local; + if (validities != null) { + local = validities; + } else { + local = new TreeSet<>(); + getData().ifPresent(pollens -> { + Matcher matcher = PATTERN.matcher(pollens.periode); + while (matcher.find()) { + local.add(LocalDate.parse(matcher.group(), FORMATTER).atStartOfDay(DEFAULT_ZONE)); + } + }); + validities = local; + } + + return local; } public Optional getBeginValidity() { - return Optional.ofNullable(getValidities().iterator().next()); + if (beginValidity == null) { + beginValidity = getValidities().iterator().next(); + } + return Optional.ofNullable(beginValidity); } public Optional getEndValidity() { - return Optional.ofNullable(getValidities().stream().reduce((prev, next) -> next).orElse(null)); + if (endValidity == null) { + endValidity = getValidities().stream().reduce((prev, next) -> next).orElse(null); + } + return Optional.ofNullable(endValidity); + } + + public Duration getValidityDuration() { + return Objects.requireNonNull(getEndValidity().map(end -> { + Duration duration = Duration.between(ZonedDateTime.now().withZoneSameInstant(end.getZone()), end); + return duration.isNegative() ? Duration.ZERO : duration; + }).orElse(Duration.ZERO)); } public Optional getComment() { @@ -150,17 +182,25 @@ public class AirParifDto { } } - public record Result(// + public record Concentration(// @SerializedName("polluant") Pollutant pollutant, // ZonedDateTime date, // @SerializedName("valeurs") double[] values, // - Message message) { + @Nullable Message message) { + + public State getMessage() { + return message != null ? new StringType(message.fr()) : UnDefType.NULL; + } + + public double getValue() { + return values[0]; + } } public record Route(// @SerializedName("dateRequise") ZonedDateTime requestedDate, // double[][] longlats, // - @SerializedName("resultats") Result[] results, // + @SerializedName("resultats") List concentrations, // @Nullable Message[] messages) { } diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/AirParifBridgeHandler.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/AirParifBridgeHandler.java index c3db0bb8c95..353c22dfcde 100755 --- a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/AirParifBridgeHandler.java +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/AirParifBridgeHandler.java @@ -21,12 +21,15 @@ import java.net.URI; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.time.Duration; -import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -46,6 +49,7 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus.Code; import org.openhab.binding.airparif.internal.AirParifException; import org.openhab.binding.airparif.internal.AirParifIconProvider; +import org.openhab.binding.airparif.internal.api.AirParifApi.Pollen; import org.openhab.binding.airparif.internal.api.AirParifDto.Bulletin; import org.openhab.binding.airparif.internal.api.AirParifDto.Episode; import org.openhab.binding.airparif.internal.api.AirParifDto.ItineraireResponse; @@ -54,18 +58,23 @@ import org.openhab.binding.airparif.internal.api.AirParifDto.PollensResponse; import org.openhab.binding.airparif.internal.api.AirParifDto.Route; import org.openhab.binding.airparif.internal.api.AirParifDto.Version; import org.openhab.binding.airparif.internal.api.ColorMap; +import org.openhab.binding.airparif.internal.api.PollenAlertLevel; +import org.openhab.binding.airparif.internal.api.Pollutant; import org.openhab.binding.airparif.internal.config.BridgeConfiguration; import org.openhab.binding.airparif.internal.deserialization.AirParifDeserializer; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.StringType; import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelGroupUID; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.BaseBridgeHandler; import org.openhab.core.types.Command; +import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -78,7 +87,7 @@ import org.slf4j.LoggerFactory; * */ @NonNullByDefault -public class AirParifBridgeHandler extends BaseBridgeHandler { +public class AirParifBridgeHandler extends BaseBridgeHandler implements HandlerUtils { private static final int REQUEST_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(30); private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; @@ -86,11 +95,10 @@ public class AirParifBridgeHandler extends BaseBridgeHandler { private final AirParifDeserializer deserializer; private final AirParifIconProvider iconProvider; private final HttpClient httpClient; + private final Map> jobs = new HashMap<>(); private BridgeConfiguration config = new BridgeConfiguration(); - - private @Nullable ScheduledFuture pollensJob; - private @Nullable ScheduledFuture dailyJob; + private @Nullable PollensResponse pollens; public AirParifBridgeHandler(Bridge bridge, HttpClient httpClient, AirParifDeserializer deserializer, AirParifIconProvider iconProvider) { @@ -112,19 +120,10 @@ public class AirParifBridgeHandler extends BaseBridgeHandler { scheduler.execute(this::initiateConnexion); } - private @Nullable ScheduledFuture cancelFuture(@Nullable ScheduledFuture job) { - if (job != null && !job.isCancelled()) { - job.cancel(true); - } - return null; - } - @Override public void dispose() { logger.debug("Disposing the AirParif bridge handler."); - - pollensJob = cancelFuture(pollensJob); - dailyJob = cancelFuture(dailyJob); + cleanJobs(); } public synchronized String executeUri(URI uri, HttpMethod method, @Nullable String payload) @@ -154,10 +153,9 @@ public class AirParifBridgeHandler extends BaseBridgeHandler { } else if (statusCode == Code.FORBIDDEN) { throw new AirParifException("@text/offline.config-error-invalid-apikey"); } - String content = new String(response.getContent(), DEFAULT_CHARSET); throw new AirParifException("Error '%s' requesting: %s", statusCode.getMessage(), uri.toString()); } catch (TimeoutException | ExecutionException e) { - throw new AirParifException(e, "Exception while calling %s", request.getURI()); + throw new AirParifException(e, "Exception while calling %s: %s", request.getURI(), e.getMessage()); } catch (InterruptedException e) { throw new AirParifException(e, "Execution interrupted: %s", e.getMessage()); } @@ -180,7 +178,7 @@ public class AirParifBridgeHandler extends BaseBridgeHandler { private void initiateConnexion() { Version version; - try { // This does validate communication with the server + try { // This is only intended to validate communication with the server version = executeUri(VERSION_URI, Version.class); } catch (AirParifException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); @@ -195,9 +193,9 @@ public class AirParifBridgeHandler extends BaseBridgeHandler { return; } - getThing().setProperty("api-version", version.version()); - getThing().setProperty("key-expiration", keyInfo.expiration().toString()); - getThing().setProperty("scopes", keyInfo.scopes().stream().map(e -> e.name()).collect(Collectors.joining(","))); + thing.setProperty("api-version", version.version()); + thing.setProperty("key-expiration", keyInfo.expiration().toString()); + thing.setProperty("scopes", keyInfo.scopes().stream().map(e -> e.name()).collect(Collectors.joining(","))); logger.info("The api key is valid until {}", keyInfo.expiration().toString()); updateStatus(ThingStatus.ONLINE); @@ -209,73 +207,92 @@ public class AirParifBridgeHandler extends BaseBridgeHandler { logger.warn("Error reading ColorMap: {]", e.getMessage()); } - pollensJob = scheduler.schedule(this::updatePollens, 1, TimeUnit.SECONDS); - dailyJob = scheduler.schedule(this::updateDaily, 2, TimeUnit.SECONDS); + ThingUID thingUID = thing.getUID(); + + schedule("Pollens Update", () -> updatePollens(new ChannelGroupUID(thingUID, GROUP_POLLENS)), + Duration.ofSeconds(1)); + schedule("Air Quality Bulletin", () -> updateDailyAQBulletin(new ChannelGroupUID(thingUID, GROUP_AQ_BULLETIN), + new ChannelGroupUID(thingUID, GROUP_AQ_BULLETIN_TOMORROW)), Duration.ofSeconds(2)); + schedule("Episode", () -> updateEpisode(new ChannelGroupUID(thingUID, GROUP_DAILY)), Duration.ofSeconds(3)); } - private void updateDaily() { + private void updatePollens(ChannelGroupUID pollensGroupUID) { + PollensResponse localPollens; try { - Bulletin bulletin = executeUri(PREV_BULLETIN_URI, Bulletin.class); - logger.debug("The bulletin is {}", bulletin.today().dayDescription()); - - Set.of(bulletin.today(), bulletin.tomorrow()).stream().forEach(aq -> { - String groupName = aq.previsionDate().equals(LocalDate.now()) ? GROUP_AQ_BULLETIN - : GROUP_AQ_BULLETIN_TOMORROW + "#"; - updateState(groupName + CHANNEL_COMMENT, - !aq.available() ? UnDefType.UNDEF : new StringType(aq.bulletin().fr())); - aq.concentrations().forEach(measure -> { - String cName = groupName + measure.pollutant().name().toLowerCase(); - updateState(cName + "-min", !aq.available() ? UnDefType.UNDEF - : new QuantityType<>(measure.min(), measure.pollutant().unit)); - updateState(cName + "-max", !aq.available() ? UnDefType.UNDEF - : new QuantityType<>(measure.max(), measure.pollutant().unit)); - }); - }); - - Episode episode = executeUri(EPISODES_URI, Episode.class); - logger.debug("The episode is {}", episode); - - // if (episode.active()) { - // updateState(GROUP_DAILY + "#" + CHANNEL_MESSAGE, new StringType(episode.message().fr())); - // updateState(GROUP_DAILY + "#" + CHANNEL_TOMORROW, new StringType(episode.message().fr())); - // } - - ZonedDateTime tomorrowMorning = ZonedDateTime.now().plusDays(1).truncatedTo(ChronoUnit.DAYS).plusMinutes(1); - long delay = Duration.between(ZonedDateTime.now(), tomorrowMorning).getSeconds(); - logger.debug("Rescheduling daily job tomorrow morning"); - dailyJob = scheduler.schedule(this::updateDaily, delay, TimeUnit.SECONDS); - } catch (AirParifException e) { - logger.warn("Error update pollens data: {}", e.getMessage()); - } - } - - private void updatePollens() { - try { - PollensResponse pollens = executeUri(POLLENS_URI, PollensResponse.class); - - pollens.getComment() - .ifPresent(comment -> updateState(GROUP_POLLENS + "#" + CHANNEL_COMMENT, new StringType(comment))); - pollens.getBeginValidity().ifPresent( - begin -> updateState(GROUP_POLLENS + "#" + CHANNEL_BEGIN_VALIDITY, new DateTimeType(begin))); - pollens.getEndValidity().ifPresent(end -> { - updateState(GROUP_POLLENS + "#" + CHANNEL_END_VALIDITY, new DateTimeType(end)); - logger.info("Pollens bulletin valid until {}", end); - long delay = Duration.between(ZonedDateTime.now(), end).getSeconds(); - if (delay < 0) { - // what if the bulletin was not updated and the delay is passed ? - delay = 3600; - logger.debug("Update time of the bulletin is in the past - will retry in one hour"); - } else { - delay += 60; - } - - pollensJob = scheduler.schedule(this::updatePollens, delay, TimeUnit.SECONDS); - }); - getThing().getThings().stream().map(Thing::getHandler).filter(LocationHandler.class::isInstance) - .map(LocationHandler.class::cast).forEach(locHand -> locHand.setPollens(pollens)); + localPollens = executeUri(POLLENS_URI, PollensResponse.class); } catch (AirParifException e) { logger.warn("Error updating pollens data: {}", e.getMessage()); + return; } + + updateState(new ChannelUID(pollensGroupUID, CHANNEL_COMMENT), Objects.requireNonNull( + localPollens.getComment().map(comment -> (State) new StringType(comment)).orElse(UnDefType.NULL))); + updateState(new ChannelUID(pollensGroupUID, CHANNEL_BEGIN_VALIDITY), Objects.requireNonNull( + localPollens.getBeginValidity().map(begin -> (State) new DateTimeType(begin)).orElse(UnDefType.NULL))); + updateState(new ChannelUID(pollensGroupUID, CHANNEL_END_VALIDITY), Objects.requireNonNull( + localPollens.getEndValidity().map(end -> (State) new DateTimeType(end)).orElse(UnDefType.NULL))); + + long delay = localPollens.getValidityDuration().getSeconds(); + // if delay is null, update in 3600 seconds + delay += delay == 0 ? 3600 : 60; + schedule("Pollens Update", () -> updatePollens(pollensGroupUID), Duration.ofSeconds(delay)); + + // Send pollens information to childs + getThing().getThings().stream().map(Thing::getHandler).filter(LocationHandler.class::isInstance) + .map(LocationHandler.class::cast).forEach(locHand -> locHand.setPollens(localPollens)); + pollens = localPollens; + } + + private void updateDailyAQBulletin(ChannelGroupUID todayGroupUID, ChannelGroupUID tomorrowGroupUID) { + Bulletin bulletin; + try { + bulletin = executeUri(PREV_BULLETIN_URI, Bulletin.class); + } catch (AirParifException e) { + logger.warn("Error updating Air Quality Bulletin: {}", e.getMessage()); + return; + } + + Set.of(bulletin.today(), bulletin.tomorrow()).stream().forEach(aq -> { + ChannelGroupUID groupUID = aq.isToday() ? todayGroupUID : tomorrowGroupUID; + updateState(new ChannelUID(groupUID, CHANNEL_COMMENT), + !aq.available() ? UnDefType.UNDEF : new StringType(aq.bulletin().fr())); + + aq.concentrations().forEach(measure -> { + Pollutant pollutant = measure.pollutant(); + String cName = pollutant.name().toLowerCase() + "-"; + updateState(new ChannelUID(groupUID, cName + "min"), + aq.available() ? new QuantityType<>(measure.min(), pollutant.unit) : UnDefType.UNDEF); + updateState(new ChannelUID(groupUID, cName + "max"), + aq.available() ? new QuantityType<>(measure.max(), pollutant.unit) : UnDefType.UNDEF); + }); + }); + + ZonedDateTime tomorrowMorning = ZonedDateTime.now().plusDays(1).truncatedTo(ChronoUnit.DAYS).plusMinutes(1); + logger.debug("Rescheduling daily air quality bulletin job tomorrow morning"); + schedule("Air Quality Bulletin", () -> updateDailyAQBulletin(todayGroupUID, tomorrowGroupUID), + Duration.between(ZonedDateTime.now(), tomorrowMorning)); + } + + private void updateEpisode(ChannelGroupUID dailyGroupUID) { + Episode episode; + try { + episode = executeUri(EPISODES_URI, Episode.class); + } catch (AirParifException e) { + logger.warn("Error updating Episode: {}", e.getMessage()); + return; + } + + logger.debug("The episode is {}", episode); + + updateState(new ChannelUID(dailyGroupUID, CHANNEL_MESSAGE), new StringType(episode.message().fr())); + updateState(new ChannelUID(dailyGroupUID, CHANNEL_TOMORROW), new StringType(episode.message().fr())); + + // Set.of(episode.today(), episode.tomorrow()).stream().forEach(aq -> { + + // }); + + ZonedDateTime tomorrowMorning = ZonedDateTime.now().plusDays(1).truncatedTo(ChronoUnit.DAYS).plusMinutes(1); + schedule("Episode", () -> updateEpisode(dailyGroupUID), Duration.between(ZonedDateTime.now(), tomorrowMorning)); } public @Nullable Route getConcentrations(String location) { @@ -294,4 +311,34 @@ public class AirParifBridgeHandler extends BaseBridgeHandler { } return null; } + + public Map requestPollens(String department) { + PollensResponse localPollens = pollens; + return localPollens != null ? localPollens.getDepartment(department) : Map.of(); + } + + @Override + public @Nullable Bridge getBridge() { + return super.getBridge(); + } + + @Override + public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) { + super.updateStatus(status, statusDetail, description); + } + + @Override + public ScheduledExecutorService getScheduler() { + return scheduler; + } + + @Override + public Logger getLogger() { + return logger; + } + + @Override + public Map> getJobs() { + return jobs; + } } diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/HandlerUtils.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/HandlerUtils.java new file mode 100644 index 00000000000..df2aec547da --- /dev/null +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/HandlerUtils.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.airparif.internal.handler; + +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BridgeHandler; +import org.slf4j.Logger; + +/** + * The {@link HandlerUtils} defines and implements some common methods for Thing Handlers + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public interface HandlerUtils { + default @Nullable ScheduledFuture cancelFuture(@Nullable ScheduledFuture job) { + if (job != null && !job.isCancelled()) { + job.cancel(true); + } + return null; + } + + @SuppressWarnings("unchecked") + default @Nullable T getBridgeHandler(Class clazz) { + Bridge bridge = getBridge(); + if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) { + BridgeHandler bridgeHandler = bridge.getHandler(); + if (bridgeHandler != null) { + if (bridgeHandler.getClass() == clazz) { + return (T) bridgeHandler; + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/incorrect-bridge"); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/incorrect-bridge"); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, ""); + } + return null; + } + + default void schedule(String jobName, Runnable job, Duration duration) { + ScheduledFuture result = getJobs().remove(jobName); + String operation = "Scheduling"; + if (result != null) { + operation = "Rescheduled"; + cancelFuture(result); + } + getLogger().info("{} {} in {}", operation, jobName, duration); + getJobs().put(jobName, getScheduler().schedule(job, duration.getSeconds(), TimeUnit.SECONDS)); + } + + default void cleanJobs() { + getJobs().values().forEach(job -> cancelFuture(job)); + getJobs().clear(); + } + + void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description); + + @Nullable + Bridge getBridge(); + + ScheduledExecutorService getScheduler(); + + Logger getLogger(); + + Map> getJobs(); +} diff --git a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/LocationHandler.java b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/LocationHandler.java index 55f8484dfd8..3407afe2a09 100755 --- a/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/LocationHandler.java +++ b/bundles/org.openhab.binding.airparif/src/main/java/org/openhab/binding/airparif/internal/handler/LocationHandler.java @@ -12,9 +12,13 @@ */ package org.openhab.binding.airparif.internal.handler; -import static org.openhab.binding.airparif.internal.AirParifBindingConstants.GROUP_POLLENS; +import static org.openhab.binding.airparif.internal.AirParifBindingConstants.*; +import java.time.Duration; +import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -23,8 +27,11 @@ import org.openhab.binding.airparif.internal.api.AirParifDto.PollensResponse; import org.openhab.binding.airparif.internal.api.AirParifDto.Route; import org.openhab.binding.airparif.internal.api.PollenAlertLevel; import org.openhab.binding.airparif.internal.config.LocationConfiguration; +import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelGroupUID; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; @@ -41,11 +48,13 @@ import org.slf4j.LoggerFactory; * @author Gaël L'hopital - Initial contribution */ @NonNullByDefault -public class LocationHandler extends BaseThingHandler { +public class LocationHandler extends BaseThingHandler implements HandlerUtils { private final Logger logger = LoggerFactory.getLogger(LocationHandler.class); + private final Map> jobs = new HashMap<>(); private @Nullable LocationConfiguration config; + private Map myPollens = Map.of(); public LocationHandler(Thing thing) { super(thing); @@ -55,46 +64,85 @@ public class LocationHandler extends BaseThingHandler { public void initialize() { config = getConfigAs(LocationConfiguration.class); updateStatus(ThingStatus.UNKNOWN); + schedule("Local Air Quality", this::getConcentrations, Duration.ofSeconds(2)); + } - scheduler.execute(this::getConcentrations); + @Override + public void dispose() { + logger.debug("Disposing the AirParif bridge handler."); + cleanJobs(); } public void setPollens(PollensResponse pollens) { LocationConfiguration local = config; if (local != null) { - Map alerts = pollens.getDepartment(local.department); - alerts.forEach((pollen, level) -> { - updateState(GROUP_POLLENS + "#" + pollen.name().toLowerCase(), new DecimalType(level.ordinal())); - }); + updatePollenChannels(pollens.getDepartment(local.department)); updateStatus(ThingStatus.ONLINE); } } + private void updatePollenChannels(Map pollens) { + ChannelGroupUID pollensUID = new ChannelGroupUID(thing.getUID(), GROUP_POLLENS); + myPollens = pollens; + pollens.forEach((pollen, level) -> updateState(new ChannelUID(pollensUID, pollen.name().toLowerCase()), + new DecimalType(level.ordinal()))); + } + private void getConcentrations() { - AirParifBridgeHandler apiHandler = getApiBridgeHandler(); + AirParifBridgeHandler apiHandler = getBridgeHandler(AirParifBridgeHandler.class); LocationConfiguration local = config; + long delay = 3600; if (apiHandler != null && local != null) { + if (myPollens.isEmpty()) { + updatePollenChannels(apiHandler.requestPollens(local.department)); + } + Route route = apiHandler.getConcentrations(local.location); + if (route != null) { + route.concentrations().forEach(concentration -> { + ChannelGroupUID groupUID = new ChannelGroupUID(thing.getUID(), + concentration.pollutant().name().toLowerCase()); + updateState(new ChannelUID(groupUID, CHANNEL_TIMESTAMP), new DateTimeType(concentration.date())); + updateState(new ChannelUID(groupUID, CHANNEL_MESSAGE), concentration.getMessage()); + updateState(new ChannelUID(groupUID, CHANNEL_VALUE), + new QuantityType<>(concentration.getValue(), concentration.pollutant().unit)); + + }); + updateStatus(ThingStatus.ONLINE); + } + } else { + delay = 10; } + schedule("Local Air Quality", this::getConcentrations, Duration.ofSeconds(delay)); } @Override public void handleCommand(ChannelUID channelUID, Command command) { // TODO Auto-generated method stub - } - private @Nullable AirParifBridgeHandler getApiBridgeHandler() { - Bridge bridge = this.getBridge(); - if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) { - if (bridge.getHandler() instanceof AirParifBridgeHandler airParifBridgeHandler) { - return airParifBridgeHandler; - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/incorrect-bridge"); - } - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); - } - return null; + @Override + public @Nullable Bridge getBridge() { + return super.getBridge(); + } + + @Override + public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) { + super.updateStatus(status, statusDetail, description); + } + + @Override + public ScheduledExecutorService getScheduler() { + return scheduler; + } + + @Override + public Logger getLogger() { + return logger; + } + + @Override + public Map> getJobs() { + return jobs; } } diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/i18n/airparif.properties b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/i18n/airparif.properties index 448b12d4d94..850db294e30 100755 --- a/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/i18n/airparif.properties +++ b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/i18n/airparif.properties @@ -7,8 +7,14 @@ addon.airparif.description = Air Quality data and forecasts provided by AirParif thing-type.airparif.api.label = AirParif API Portal thing-type.airparif.api.description = Bridge to the AirParif API Portal. In order to receive the data, you must register an account on https://www.airparif.fr/contact and receive your API token. +thing-type.airparif.api.group.aq-bulletin.label = Today's Air Quality Bulletin +thing-type.airparif.api.group.aq-bulletin-tomorrow.label = Tomorrow's Air Quality Bulletin thing-type.airparif.location.label = Department Report thing-type.airparif.location.description = AirParif air quality report for the given location +thing-type.airparif.location.group.no2.label = NO2 Concentration Information +thing-type.airparif.location.group.o3.label = Ozone Concentration Information +thing-type.airparif.location.group.pm10.label = PM10 Concentration Information +thing-type.airparif.location.group.pm25.label = PM2.5 Concentration Information # thing types config @@ -28,6 +34,25 @@ thing-type.config.airparif.location.location.label = Location # channel group types +channel-group-type.airparif.air-quality-bulletin.label = Air Quality Bulletin +channel-group-type.airparif.air-quality-bulletin.channel.comment.label = Message +channel-group-type.airparif.air-quality-bulletin.channel.comment.description = General message for the air quality bulletin +channel-group-type.airparif.air-quality-bulletin.channel.no2-max.label = NO2 Max +channel-group-type.airparif.air-quality-bulletin.channel.no2-max.description = Maximum level of NO2 concentation +channel-group-type.airparif.air-quality-bulletin.channel.no2-min.label = NO2 Min +channel-group-type.airparif.air-quality-bulletin.channel.no2-min.description = Minimum level of NO2 concentation +channel-group-type.airparif.air-quality-bulletin.channel.o3-max.label = O3 Max +channel-group-type.airparif.air-quality-bulletin.channel.o3-max.description = Maximum level of O3 concentation +channel-group-type.airparif.air-quality-bulletin.channel.o3-min.label = O3 Min +channel-group-type.airparif.air-quality-bulletin.channel.o3-min.description = Minimum level of O3 concentation +channel-group-type.airparif.air-quality-bulletin.channel.pm10-max.label = PM 10 Max +channel-group-type.airparif.air-quality-bulletin.channel.pm10-max.description = Maximum level of PM 10 concentation +channel-group-type.airparif.air-quality-bulletin.channel.pm10-min.label = PM 10 Min +channel-group-type.airparif.air-quality-bulletin.channel.pm10-min.description = Minimum level of PM 10 concentation +channel-group-type.airparif.air-quality-bulletin.channel.pm25-max.label = PM 2.5 Max +channel-group-type.airparif.air-quality-bulletin.channel.pm25-max.description = Maximum level of PM 2.5 concentation +channel-group-type.airparif.air-quality-bulletin.channel.pm25-min.label = PM 2.5 Min +channel-group-type.airparif.air-quality-bulletin.channel.pm25-min.description = Minimum level of PM 2.5 concentation channel-group-type.airparif.bridge-pollens.label = Pollen information for the region channel-group-type.airparif.bridge-pollens.channel.begin-validity.label = Begin Validity channel-group-type.airparif.bridge-pollens.channel.begin-validity.description = Current bulletin validity start @@ -35,7 +60,33 @@ channel-group-type.airparif.bridge-pollens.channel.comment.label = Begin Validit channel-group-type.airparif.bridge-pollens.channel.comment.description = Current bulletin validity start channel-group-type.airparif.bridge-pollens.channel.end-validity.label = End Validity channel-group-type.airparif.bridge-pollens.channel.end-validity.description = Current bulletin validity ending +channel-group-type.airparif.daily.label = Daily information for the region +channel-group-type.airparif.daily.channel.message.label = Message +channel-group-type.airparif.daily.channel.message.description = Current bulletin validity start +channel-group-type.airparif.daily.channel.tomorrow.label = Tomorrow +channel-group-type.airparif.daily.channel.tomorrow.description = Current bulletin validity start channel-group-type.airparif.dept-pollens.label = Pollen information for the department +channel-group-type.airparif.pollutant-mpc.label = Pollutant Concentration Information +channel-group-type.airparif.pollutant-mpc.channel.message.label = Message +channel-group-type.airparif.pollutant-mpc.channel.message.description = Polllutant concentration alert message +channel-group-type.airparif.pollutant-mpc.channel.timestamp.label = Timestamp +channel-group-type.airparif.pollutant-mpc.channel.timestamp.description = Timestamp of the measure +channel-group-type.airparif.pollutant-mpc.channel.value.label = Concentration +channel-group-type.airparif.pollutant-mpc.channel.value.description = Concentration of the given pollutant +channel-group-type.airparif.pollutant-ndx.label = Global Pollutant Index +channel-group-type.airparif.pollutant-ndx.channel.message.label = Message +channel-group-type.airparif.pollutant-ndx.channel.message.description = Alert message associated to the value of the index +channel-group-type.airparif.pollutant-ndx.channel.timestamp.label = Timestamp +channel-group-type.airparif.pollutant-ndx.channel.timestamp.description = Timestamp of the evaluation +channel-group-type.airparif.pollutant-ndx.channel.value.label = Value +channel-group-type.airparif.pollutant-ndx.channel.value.description = Value of the global Index +channel-group-type.airparif.pollutant-ppb.label = Pollutant Concentration Information +channel-group-type.airparif.pollutant-ppb.channel.message.label = Message +channel-group-type.airparif.pollutant-ppb.channel.message.description = Polllutant concentration alert message +channel-group-type.airparif.pollutant-ppb.channel.timestamp.label = Timestamp +channel-group-type.airparif.pollutant-ppb.channel.timestamp.description = Timestamp of the measure +channel-group-type.airparif.pollutant-ppb.channel.value.label = Concentration +channel-group-type.airparif.pollutant-ppb.channel.value.description = Concentration of the given pollutant # channel types @@ -85,6 +136,8 @@ channel-type.airparif.linden-level.state.option.0 = None channel-type.airparif.linden-level.state.option.1 = Low channel-type.airparif.linden-level.state.option.2 = Average channel-type.airparif.linden-level.state.option.3 = High +channel-type.airparif.mpc-value.label = Measure +channel-type.airparif.ndx-value.label = Measure channel-type.airparif.oak-level.label = Oak channel-type.airparif.oak-level.state.option.0 = None channel-type.airparif.oak-level.state.option.1 = Low @@ -110,6 +163,7 @@ channel-type.airparif.poplar-level.state.option.0 = None channel-type.airparif.poplar-level.state.option.1 = Low channel-type.airparif.poplar-level.state.option.2 = Average channel-type.airparif.poplar-level.state.option.3 = High +channel-type.airparif.ppb-value.label = Measure channel-type.airparif.ragweed-level.label = Ragweed channel-type.airparif.ragweed-level.state.option.0 = None channel-type.airparif.ragweed-level.state.option.1 = Low diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/api.xml b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/api.xml index b1abb517927..416faef5ed3 100755 --- a/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/api.xml +++ b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/api.xml @@ -13,10 +13,12 @@ - - - - + + + + + + diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/channel-groups.xml b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/channel-groups.xml index 89e8600d7e7..5494219bbcb 100644 --- a/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/channel-groups.xml +++ b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/channel-groups.xml @@ -22,61 +22,115 @@ - - - - - - General message for the air quality bulletin - - - - Minimum level of NO2 concentation - - - - Maximum level of NO2 concentation - - - - Minimum level of O3 concentation - - - - Maximum level of O3 concentation - - - - Minimum level of PM 10 concentation - - - - Maximum level of PM 10 concentation - - - - Minimum level of PM 2.5 concentation - - - - Maximum level of PM 2.5 concentation - - - - - - - - - - Current bulletin validity start - - - - Current bulletin validity start - - - + + + + + + General message for the air quality bulletin + + + + Minimum level of NO2 concentation + + + + Maximum level of NO2 concentation + + + + Minimum level of O3 concentation + + + + Maximum level of O3 concentation + + + + Minimum level of PM 10 concentation + + + + Maximum level of PM 10 concentation + + + + Minimum level of PM 2.5 concentation + + + + Maximum level of PM 2.5 concentation + + + + + + + + + + Polllutant concentration alert message + + + + Timestamp of the measure + + + + Concentration of the given pollutant + + + + + + + + + + Polllutant concentration alert message + + + + Timestamp of the measure + + + + Concentration of the given pollutant + + + + + + + + + + Alert message associated to the value of the index + + + + Timestamp of the evaluation + + + + Value of the global Index + + + + + + + + + + Current bulletin validity start + + + + Current bulletin validity start + + + diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/channels.xml index 10cba528047..e47a4ec856c 100755 --- a/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/channels.xml +++ b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/channels.xml @@ -126,7 +126,7 @@ Number - oh:airparif:urticacea + oh:airparif:urticaceae @@ -291,17 +291,23 @@ - - Number:Dimensionless - - - - - - Number:Density - - - + + Number:Dimensionless + + + + + + Number + + + + + + Number:Density + + + diff --git a/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/thing-types.xml index 49a01933a7e..80003f5c0e9 100755 --- a/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.airparif/src/main/resources/OH-INF/thing/thing-types.xml @@ -14,6 +14,19 @@ + + + + + + + + + + + + + department