mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
Finalizing binding
Signed-off-by: Gaël L'hopital <gael@lhopital.org>
This commit is contained in:
parent
272fb89d33
commit
dc1ac4d86e
@ -45,6 +45,8 @@ public class AirParifBindingConstants {
|
|||||||
public static final String CHANNEL_COMMENT = "comment";
|
public static final String CHANNEL_COMMENT = "comment";
|
||||||
public static final String CHANNEL_MESSAGE = "message";
|
public static final String CHANNEL_MESSAGE = "message";
|
||||||
public static final String CHANNEL_TOMORROW = "tomorrow";
|
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<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(APIBRIDGE_THING_TYPE,
|
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(APIBRIDGE_THING_TYPE,
|
||||||
LOCATION_THING_TYPE);
|
LOCATION_THING_TYPE);
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.openhab.binding.airparif.internal.api;
|
package org.openhab.binding.airparif.internal.api;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
@ -19,6 +20,7 @@ import java.time.format.DateTimeFormatter;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
@ -29,6 +31,9 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
|||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.openhab.binding.airparif.internal.api.AirParifApi.Pollen;
|
import org.openhab.binding.airparif.internal.api.AirParifApi.Pollen;
|
||||||
import org.openhab.binding.airparif.internal.api.AirParifApi.Scope;
|
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;
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
@ -74,6 +79,10 @@ public class AirParifDto {
|
|||||||
public String dayDescription() {
|
public String dayDescription() {
|
||||||
return bulletin.fr;
|
return bulletin.fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isToday() {
|
||||||
|
return previsionDate.equals(LocalDate.now());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record DailyEpisode(//
|
public record DailyEpisode(//
|
||||||
@ -106,28 +115,51 @@ public class AirParifDto {
|
|||||||
private static ZoneId DEFAULT_ZONE = ZoneId.of("Europe/Paris");
|
private static ZoneId DEFAULT_ZONE = ZoneId.of("Europe/Paris");
|
||||||
|
|
||||||
public List<Pollens> data = List.of();
|
public List<Pollens> data = List.of();
|
||||||
|
private @Nullable Set<ZonedDateTime> validities;
|
||||||
|
private @Nullable ZonedDateTime beginValidity;
|
||||||
|
private @Nullable ZonedDateTime endValidity;
|
||||||
|
|
||||||
public Optional<Pollens> getData() {
|
public Optional<Pollens> getData() {
|
||||||
return Optional.ofNullable(data.isEmpty() ? null : data.get(0));
|
return Optional.ofNullable(data.isEmpty() ? null : data.get(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<ZonedDateTime> getValidities() {
|
private Set<ZonedDateTime> getValidities() {
|
||||||
Set<ZonedDateTime> result = new TreeSet<>();
|
Set<ZonedDateTime> local;
|
||||||
getData().ifPresent(pollens -> {
|
if (validities != null) {
|
||||||
Matcher matcher = PATTERN.matcher(pollens.periode);
|
local = validities;
|
||||||
while (matcher.find()) {
|
} else {
|
||||||
result.add(LocalDate.parse(matcher.group(), FORMATTER).atStartOfDay(DEFAULT_ZONE));
|
local = new TreeSet<>();
|
||||||
}
|
getData().ifPresent(pollens -> {
|
||||||
});
|
Matcher matcher = PATTERN.matcher(pollens.periode);
|
||||||
return result;
|
while (matcher.find()) {
|
||||||
|
local.add(LocalDate.parse(matcher.group(), FORMATTER).atStartOfDay(DEFAULT_ZONE));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
validities = local;
|
||||||
|
}
|
||||||
|
|
||||||
|
return local;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<ZonedDateTime> getBeginValidity() {
|
public Optional<ZonedDateTime> getBeginValidity() {
|
||||||
return Optional.ofNullable(getValidities().iterator().next());
|
if (beginValidity == null) {
|
||||||
|
beginValidity = getValidities().iterator().next();
|
||||||
|
}
|
||||||
|
return Optional.ofNullable(beginValidity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<ZonedDateTime> getEndValidity() {
|
public Optional<ZonedDateTime> 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<String> getComment() {
|
public Optional<String> getComment() {
|
||||||
@ -150,17 +182,25 @@ public class AirParifDto {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record Result(//
|
public record Concentration(//
|
||||||
@SerializedName("polluant") Pollutant pollutant, //
|
@SerializedName("polluant") Pollutant pollutant, //
|
||||||
ZonedDateTime date, //
|
ZonedDateTime date, //
|
||||||
@SerializedName("valeurs") double[] values, //
|
@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(//
|
public record Route(//
|
||||||
@SerializedName("dateRequise") ZonedDateTime requestedDate, //
|
@SerializedName("dateRequise") ZonedDateTime requestedDate, //
|
||||||
double[][] longlats, //
|
double[][] longlats, //
|
||||||
@SerializedName("resultats") Result[] results, //
|
@SerializedName("resultats") List<Concentration> concentrations, //
|
||||||
@Nullable Message[] messages) {
|
@Nullable Message[] messages) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -21,12 +21,15 @@ import java.net.URI;
|
|||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
@ -46,6 +49,7 @@ import org.eclipse.jetty.http.HttpStatus;
|
|||||||
import org.eclipse.jetty.http.HttpStatus.Code;
|
import org.eclipse.jetty.http.HttpStatus.Code;
|
||||||
import org.openhab.binding.airparif.internal.AirParifException;
|
import org.openhab.binding.airparif.internal.AirParifException;
|
||||||
import org.openhab.binding.airparif.internal.AirParifIconProvider;
|
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.Bulletin;
|
||||||
import org.openhab.binding.airparif.internal.api.AirParifDto.Episode;
|
import org.openhab.binding.airparif.internal.api.AirParifDto.Episode;
|
||||||
import org.openhab.binding.airparif.internal.api.AirParifDto.ItineraireResponse;
|
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.Route;
|
||||||
import org.openhab.binding.airparif.internal.api.AirParifDto.Version;
|
import org.openhab.binding.airparif.internal.api.AirParifDto.Version;
|
||||||
import org.openhab.binding.airparif.internal.api.ColorMap;
|
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.config.BridgeConfiguration;
|
||||||
import org.openhab.binding.airparif.internal.deserialization.AirParifDeserializer;
|
import org.openhab.binding.airparif.internal.deserialization.AirParifDeserializer;
|
||||||
import org.openhab.core.library.types.DateTimeType;
|
import org.openhab.core.library.types.DateTimeType;
|
||||||
import org.openhab.core.library.types.QuantityType;
|
import org.openhab.core.library.types.QuantityType;
|
||||||
import org.openhab.core.library.types.StringType;
|
import org.openhab.core.library.types.StringType;
|
||||||
import org.openhab.core.thing.Bridge;
|
import org.openhab.core.thing.Bridge;
|
||||||
|
import org.openhab.core.thing.ChannelGroupUID;
|
||||||
import org.openhab.core.thing.ChannelUID;
|
import org.openhab.core.thing.ChannelUID;
|
||||||
import org.openhab.core.thing.Thing;
|
import org.openhab.core.thing.Thing;
|
||||||
import org.openhab.core.thing.ThingStatus;
|
import org.openhab.core.thing.ThingStatus;
|
||||||
import org.openhab.core.thing.ThingStatusDetail;
|
import org.openhab.core.thing.ThingStatusDetail;
|
||||||
|
import org.openhab.core.thing.ThingUID;
|
||||||
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||||
import org.openhab.core.types.Command;
|
import org.openhab.core.types.Command;
|
||||||
|
import org.openhab.core.types.State;
|
||||||
import org.openhab.core.types.UnDefType;
|
import org.openhab.core.types.UnDefType;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@ -78,7 +87,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@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 int REQUEST_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(30);
|
||||||
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
||||||
|
|
||||||
@ -86,11 +95,10 @@ public class AirParifBridgeHandler extends BaseBridgeHandler {
|
|||||||
private final AirParifDeserializer deserializer;
|
private final AirParifDeserializer deserializer;
|
||||||
private final AirParifIconProvider iconProvider;
|
private final AirParifIconProvider iconProvider;
|
||||||
private final HttpClient httpClient;
|
private final HttpClient httpClient;
|
||||||
|
private final Map<String, ScheduledFuture<?>> jobs = new HashMap<>();
|
||||||
|
|
||||||
private BridgeConfiguration config = new BridgeConfiguration();
|
private BridgeConfiguration config = new BridgeConfiguration();
|
||||||
|
private @Nullable PollensResponse pollens;
|
||||||
private @Nullable ScheduledFuture<?> pollensJob;
|
|
||||||
private @Nullable ScheduledFuture<?> dailyJob;
|
|
||||||
|
|
||||||
public AirParifBridgeHandler(Bridge bridge, HttpClient httpClient, AirParifDeserializer deserializer,
|
public AirParifBridgeHandler(Bridge bridge, HttpClient httpClient, AirParifDeserializer deserializer,
|
||||||
AirParifIconProvider iconProvider) {
|
AirParifIconProvider iconProvider) {
|
||||||
@ -112,19 +120,10 @@ public class AirParifBridgeHandler extends BaseBridgeHandler {
|
|||||||
scheduler.execute(this::initiateConnexion);
|
scheduler.execute(this::initiateConnexion);
|
||||||
}
|
}
|
||||||
|
|
||||||
private @Nullable ScheduledFuture<?> cancelFuture(@Nullable ScheduledFuture<?> job) {
|
|
||||||
if (job != null && !job.isCancelled()) {
|
|
||||||
job.cancel(true);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
logger.debug("Disposing the AirParif bridge handler.");
|
logger.debug("Disposing the AirParif bridge handler.");
|
||||||
|
cleanJobs();
|
||||||
pollensJob = cancelFuture(pollensJob);
|
|
||||||
dailyJob = cancelFuture(dailyJob);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized String executeUri(URI uri, HttpMethod method, @Nullable String payload)
|
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) {
|
} else if (statusCode == Code.FORBIDDEN) {
|
||||||
throw new AirParifException("@text/offline.config-error-invalid-apikey");
|
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());
|
throw new AirParifException("Error '%s' requesting: %s", statusCode.getMessage(), uri.toString());
|
||||||
} catch (TimeoutException | ExecutionException e) {
|
} 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) {
|
} catch (InterruptedException e) {
|
||||||
throw new AirParifException(e, "Execution interrupted: %s", e.getMessage());
|
throw new AirParifException(e, "Execution interrupted: %s", e.getMessage());
|
||||||
}
|
}
|
||||||
@ -180,7 +178,7 @@ public class AirParifBridgeHandler extends BaseBridgeHandler {
|
|||||||
|
|
||||||
private void initiateConnexion() {
|
private void initiateConnexion() {
|
||||||
Version version;
|
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);
|
version = executeUri(VERSION_URI, Version.class);
|
||||||
} catch (AirParifException e) {
|
} catch (AirParifException e) {
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||||
@ -195,9 +193,9 @@ public class AirParifBridgeHandler extends BaseBridgeHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
getThing().setProperty("api-version", version.version());
|
thing.setProperty("api-version", version.version());
|
||||||
getThing().setProperty("key-expiration", keyInfo.expiration().toString());
|
thing.setProperty("key-expiration", keyInfo.expiration().toString());
|
||||||
getThing().setProperty("scopes", keyInfo.scopes().stream().map(e -> e.name()).collect(Collectors.joining(",")));
|
thing.setProperty("scopes", keyInfo.scopes().stream().map(e -> e.name()).collect(Collectors.joining(",")));
|
||||||
logger.info("The api key is valid until {}", keyInfo.expiration().toString());
|
logger.info("The api key is valid until {}", keyInfo.expiration().toString());
|
||||||
updateStatus(ThingStatus.ONLINE);
|
updateStatus(ThingStatus.ONLINE);
|
||||||
|
|
||||||
@ -209,73 +207,92 @@ public class AirParifBridgeHandler extends BaseBridgeHandler {
|
|||||||
logger.warn("Error reading ColorMap: {]", e.getMessage());
|
logger.warn("Error reading ColorMap: {]", e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
pollensJob = scheduler.schedule(this::updatePollens, 1, TimeUnit.SECONDS);
|
ThingUID thingUID = thing.getUID();
|
||||||
dailyJob = scheduler.schedule(this::updateDaily, 2, TimeUnit.SECONDS);
|
|
||||||
|
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 {
|
try {
|
||||||
Bulletin bulletin = executeUri(PREV_BULLETIN_URI, Bulletin.class);
|
localPollens = executeUri(POLLENS_URI, PollensResponse.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));
|
|
||||||
} catch (AirParifException e) {
|
} catch (AirParifException e) {
|
||||||
logger.warn("Error updating pollens data: {}", e.getMessage());
|
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) {
|
public @Nullable Route getConcentrations(String location) {
|
||||||
@ -294,4 +311,34 @@ public class AirParifBridgeHandler extends BaseBridgeHandler {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<Pollen, PollenAlertLevel> 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<String, ScheduledFuture<?>> getJobs() {
|
||||||
|
return jobs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 extends BridgeHandler> T getBridgeHandler(Class<T> 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<String, ScheduledFuture<?>> getJobs();
|
||||||
|
}
|
@ -12,9 +12,13 @@
|
|||||||
*/
|
*/
|
||||||
package org.openhab.binding.airparif.internal.handler;
|
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.Map;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
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.AirParifDto.Route;
|
||||||
import org.openhab.binding.airparif.internal.api.PollenAlertLevel;
|
import org.openhab.binding.airparif.internal.api.PollenAlertLevel;
|
||||||
import org.openhab.binding.airparif.internal.config.LocationConfiguration;
|
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.DecimalType;
|
||||||
|
import org.openhab.core.library.types.QuantityType;
|
||||||
import org.openhab.core.thing.Bridge;
|
import org.openhab.core.thing.Bridge;
|
||||||
|
import org.openhab.core.thing.ChannelGroupUID;
|
||||||
import org.openhab.core.thing.ChannelUID;
|
import org.openhab.core.thing.ChannelUID;
|
||||||
import org.openhab.core.thing.Thing;
|
import org.openhab.core.thing.Thing;
|
||||||
import org.openhab.core.thing.ThingStatus;
|
import org.openhab.core.thing.ThingStatus;
|
||||||
@ -41,11 +48,13 @@ import org.slf4j.LoggerFactory;
|
|||||||
* @author Gaël L'hopital - Initial contribution
|
* @author Gaël L'hopital - Initial contribution
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class LocationHandler extends BaseThingHandler {
|
public class LocationHandler extends BaseThingHandler implements HandlerUtils {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(LocationHandler.class);
|
private final Logger logger = LoggerFactory.getLogger(LocationHandler.class);
|
||||||
|
private final Map<String, ScheduledFuture<?>> jobs = new HashMap<>();
|
||||||
|
|
||||||
private @Nullable LocationConfiguration config;
|
private @Nullable LocationConfiguration config;
|
||||||
|
private Map<Pollen, PollenAlertLevel> myPollens = Map.of();
|
||||||
|
|
||||||
public LocationHandler(Thing thing) {
|
public LocationHandler(Thing thing) {
|
||||||
super(thing);
|
super(thing);
|
||||||
@ -55,46 +64,85 @@ public class LocationHandler extends BaseThingHandler {
|
|||||||
public void initialize() {
|
public void initialize() {
|
||||||
config = getConfigAs(LocationConfiguration.class);
|
config = getConfigAs(LocationConfiguration.class);
|
||||||
updateStatus(ThingStatus.UNKNOWN);
|
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) {
|
public void setPollens(PollensResponse pollens) {
|
||||||
LocationConfiguration local = config;
|
LocationConfiguration local = config;
|
||||||
if (local != null) {
|
if (local != null) {
|
||||||
Map<Pollen, PollenAlertLevel> alerts = pollens.getDepartment(local.department);
|
updatePollenChannels(pollens.getDepartment(local.department));
|
||||||
alerts.forEach((pollen, level) -> {
|
|
||||||
updateState(GROUP_POLLENS + "#" + pollen.name().toLowerCase(), new DecimalType(level.ordinal()));
|
|
||||||
});
|
|
||||||
updateStatus(ThingStatus.ONLINE);
|
updateStatus(ThingStatus.ONLINE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updatePollenChannels(Map<Pollen, PollenAlertLevel> 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() {
|
private void getConcentrations() {
|
||||||
AirParifBridgeHandler apiHandler = getApiBridgeHandler();
|
AirParifBridgeHandler apiHandler = getBridgeHandler(AirParifBridgeHandler.class);
|
||||||
LocationConfiguration local = config;
|
LocationConfiguration local = config;
|
||||||
|
long delay = 3600;
|
||||||
if (apiHandler != null && local != null) {
|
if (apiHandler != null && local != null) {
|
||||||
|
if (myPollens.isEmpty()) {
|
||||||
|
updatePollenChannels(apiHandler.requestPollens(local.department));
|
||||||
|
}
|
||||||
|
|
||||||
Route route = apiHandler.getConcentrations(local.location);
|
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
|
@Override
|
||||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||||
// TODO Auto-generated method stub
|
// TODO Auto-generated method stub
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private @Nullable AirParifBridgeHandler getApiBridgeHandler() {
|
@Override
|
||||||
Bridge bridge = this.getBridge();
|
public @Nullable Bridge getBridge() {
|
||||||
if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
|
return super.getBridge();
|
||||||
if (bridge.getHandler() instanceof AirParifBridgeHandler airParifBridgeHandler) {
|
}
|
||||||
return airParifBridgeHandler;
|
|
||||||
} else {
|
@Override
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/incorrect-bridge");
|
public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
|
||||||
}
|
super.updateStatus(status, statusDetail, description);
|
||||||
} else {
|
}
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
|
|
||||||
}
|
@Override
|
||||||
return null;
|
public ScheduledExecutorService getScheduler() {
|
||||||
|
return scheduler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Logger getLogger() {
|
||||||
|
return logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, ScheduledFuture<?>> getJobs() {
|
||||||
|
return jobs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.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.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.label = Department Report
|
||||||
thing-type.airparif.location.description = AirParif air quality report for the given location
|
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
|
# thing types config
|
||||||
|
|
||||||
@ -28,6 +34,25 @@ thing-type.config.airparif.location.location.label = Location
|
|||||||
|
|
||||||
# channel group types
|
# 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.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.label = Begin Validity
|
||||||
channel-group-type.airparif.bridge-pollens.channel.begin-validity.description = Current bulletin validity start
|
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.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.label = End Validity
|
||||||
channel-group-type.airparif.bridge-pollens.channel.end-validity.description = Current bulletin validity ending
|
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.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
|
# 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.1 = Low
|
||||||
channel-type.airparif.linden-level.state.option.2 = Average
|
channel-type.airparif.linden-level.state.option.2 = Average
|
||||||
channel-type.airparif.linden-level.state.option.3 = High
|
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.label = Oak
|
||||||
channel-type.airparif.oak-level.state.option.0 = None
|
channel-type.airparif.oak-level.state.option.0 = None
|
||||||
channel-type.airparif.oak-level.state.option.1 = Low
|
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.1 = Low
|
||||||
channel-type.airparif.poplar-level.state.option.2 = Average
|
channel-type.airparif.poplar-level.state.option.2 = Average
|
||||||
channel-type.airparif.poplar-level.state.option.3 = High
|
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.label = Ragweed
|
||||||
channel-type.airparif.ragweed-level.state.option.0 = None
|
channel-type.airparif.ragweed-level.state.option.0 = None
|
||||||
channel-type.airparif.ragweed-level.state.option.1 = Low
|
channel-type.airparif.ragweed-level.state.option.1 = Low
|
||||||
|
@ -13,10 +13,12 @@
|
|||||||
|
|
||||||
<channel-groups>
|
<channel-groups>
|
||||||
<channel-group id="pollens" typeId="bridge-pollens"/>
|
<channel-group id="pollens" typeId="bridge-pollens"/>
|
||||||
<channel-group id="aq-bulletin" typeId="air-quality-bulletin">
|
<channel-group id="aq-bulletin" typeId="air-quality-bulletin">
|
||||||
<label>Today's Air Quality Bulletin</label></channel-group>
|
<label>Today's Air Quality Bulletin</label>
|
||||||
<channel-group id="aq-bulletin-tomorrow" typeId="air-quality-bulletin">
|
</channel-group>
|
||||||
<label>Tomorrow's Air Quality Bulletin</label></channel-group>
|
<channel-group id="aq-bulletin-tomorrow" typeId="air-quality-bulletin">
|
||||||
|
<label>Tomorrow's Air Quality Bulletin</label>
|
||||||
|
</channel-group>
|
||||||
<channel-group id="daily" typeId="daily"/>
|
<channel-group id="daily" typeId="daily"/>
|
||||||
</channel-groups>
|
</channel-groups>
|
||||||
|
|
||||||
|
@ -22,61 +22,115 @@
|
|||||||
</channels>
|
</channels>
|
||||||
</channel-group-type>
|
</channel-group-type>
|
||||||
|
|
||||||
<channel-group-type id="air-quality-bulletin">
|
<channel-group-type id="air-quality-bulletin">
|
||||||
<label>Air Quality Bulletin</label>
|
<label>Air Quality Bulletin</label>
|
||||||
<channels>
|
<channels>
|
||||||
<channel id="comment" typeId="comment">
|
<channel id="comment" typeId="comment">
|
||||||
<label>Message</label>
|
<label>Message</label>
|
||||||
<description>General message for the air quality bulletin</description>
|
<description>General message for the air quality bulletin</description>
|
||||||
</channel>
|
</channel>
|
||||||
<channel id="no2-min" typeId="ppb-value">
|
<channel id="no2-min" typeId="ppb-value">
|
||||||
<label>NO2 Min</label>
|
<label>NO2 Min</label>
|
||||||
<description>Minimum level of NO2 concentation</description>
|
<description>Minimum level of NO2 concentation</description>
|
||||||
</channel>
|
</channel>
|
||||||
<channel id="no2-max" typeId="ppb-value">
|
<channel id="no2-max" typeId="ppb-value">
|
||||||
<label>NO2 Max</label>
|
<label>NO2 Max</label>
|
||||||
<description>Maximum level of NO2 concentation</description>
|
<description>Maximum level of NO2 concentation</description>
|
||||||
</channel>
|
</channel>
|
||||||
<channel id="o3-min" typeId="ppb-value">
|
<channel id="o3-min" typeId="ppb-value">
|
||||||
<label>O3 Min</label>
|
<label>O3 Min</label>
|
||||||
<description>Minimum level of O3 concentation</description>
|
<description>Minimum level of O3 concentation</description>
|
||||||
</channel>
|
</channel>
|
||||||
<channel id="o3-max" typeId="ppb-value">
|
<channel id="o3-max" typeId="ppb-value">
|
||||||
<label>O3 Max</label>
|
<label>O3 Max</label>
|
||||||
<description>Maximum level of O3 concentation</description>
|
<description>Maximum level of O3 concentation</description>
|
||||||
</channel>
|
</channel>
|
||||||
<channel id="pm10-min" typeId="mpc-value">
|
<channel id="pm10-min" typeId="mpc-value">
|
||||||
<label>PM 10 Min</label>
|
<label>PM 10 Min</label>
|
||||||
<description>Minimum level of PM 10 concentation</description>
|
<description>Minimum level of PM 10 concentation</description>
|
||||||
</channel>
|
</channel>
|
||||||
<channel id="pm10-max" typeId="mpc-value">
|
<channel id="pm10-max" typeId="mpc-value">
|
||||||
<label>PM 10 Max</label>
|
<label>PM 10 Max</label>
|
||||||
<description>Maximum level of PM 10 concentation</description>
|
<description>Maximum level of PM 10 concentation</description>
|
||||||
</channel>
|
</channel>
|
||||||
<channel id="pm25-min" typeId="mpc-value">
|
<channel id="pm25-min" typeId="mpc-value">
|
||||||
<label>PM 2.5 Min</label>
|
<label>PM 2.5 Min</label>
|
||||||
<description>Minimum level of PM 2.5 concentation</description>
|
<description>Minimum level of PM 2.5 concentation</description>
|
||||||
</channel>
|
</channel>
|
||||||
<channel id="pm25-max" typeId="mpc-value">
|
<channel id="pm25-max" typeId="mpc-value">
|
||||||
<label>PM 2.5 Max</label>
|
<label>PM 2.5 Max</label>
|
||||||
<description>Maximum level of PM 2.5 concentation</description>
|
<description>Maximum level of PM 2.5 concentation</description>
|
||||||
</channel>
|
</channel>
|
||||||
</channels>
|
</channels>
|
||||||
</channel-group-type>
|
</channel-group-type>
|
||||||
|
|
||||||
<channel-group-type id="daily">
|
<channel-group-type id="pollutant-mpc">
|
||||||
<label>Daily information for the region</label>
|
<label>Pollutant Concentration Information</label>
|
||||||
<channels>
|
<channels>
|
||||||
<channel id="message" typeId="comment">
|
<channel id="message" typeId="comment">
|
||||||
<label>Message</label>
|
<label>Message</label>
|
||||||
<description>Current bulletin validity start</description>
|
<description>Polllutant concentration alert message</description>
|
||||||
</channel>
|
</channel>
|
||||||
<channel id="tomorrow" typeId="comment">
|
<channel id="timestamp" typeId="timestamp">
|
||||||
<label>Tomorrow</label>
|
<label>Timestamp</label>
|
||||||
<description>Current bulletin validity start</description>
|
<description>Timestamp of the measure</description>
|
||||||
</channel>
|
</channel>
|
||||||
</channels>
|
<channel id="value" typeId="mpc-value">
|
||||||
</channel-group-type>
|
<label>Concentration</label>
|
||||||
|
<description>Concentration of the given pollutant</description>
|
||||||
|
</channel>
|
||||||
|
</channels>
|
||||||
|
</channel-group-type>
|
||||||
|
|
||||||
|
<channel-group-type id="pollutant-ppb">
|
||||||
|
<label>Pollutant Concentration Information</label>
|
||||||
|
<channels>
|
||||||
|
<channel id="message" typeId="comment">
|
||||||
|
<label>Message</label>
|
||||||
|
<description>Polllutant concentration alert message</description>
|
||||||
|
</channel>
|
||||||
|
<channel id="timestamp" typeId="timestamp">
|
||||||
|
<label>Timestamp</label>
|
||||||
|
<description>Timestamp of the measure</description>
|
||||||
|
</channel>
|
||||||
|
<channel id="value" typeId="ppb-value">
|
||||||
|
<label>Concentration</label>
|
||||||
|
<description>Concentration of the given pollutant</description>
|
||||||
|
</channel>
|
||||||
|
</channels>
|
||||||
|
</channel-group-type>
|
||||||
|
|
||||||
|
<channel-group-type id="pollutant-ndx">
|
||||||
|
<label>Global Pollutant Index</label>
|
||||||
|
<channels>
|
||||||
|
<channel id="message" typeId="comment">
|
||||||
|
<label>Message</label>
|
||||||
|
<description>Alert message associated to the value of the index</description>
|
||||||
|
</channel>
|
||||||
|
<channel id="timestamp" typeId="timestamp">
|
||||||
|
<label>Timestamp</label>
|
||||||
|
<description>Timestamp of the evaluation</description>
|
||||||
|
</channel>
|
||||||
|
<channel id="value" typeId="ndx-value">
|
||||||
|
<label>Value</label>
|
||||||
|
<description>Value of the global Index</description>
|
||||||
|
</channel>
|
||||||
|
</channels>
|
||||||
|
</channel-group-type>
|
||||||
|
|
||||||
|
<channel-group-type id="daily">
|
||||||
|
<label>Daily information for the region</label>
|
||||||
|
<channels>
|
||||||
|
<channel id="message" typeId="comment">
|
||||||
|
<label>Message</label>
|
||||||
|
<description>Current bulletin validity start</description>
|
||||||
|
</channel>
|
||||||
|
<channel id="tomorrow" typeId="comment">
|
||||||
|
<label>Tomorrow</label>
|
||||||
|
<description>Current bulletin validity start</description>
|
||||||
|
</channel>
|
||||||
|
</channels>
|
||||||
|
</channel-group-type>
|
||||||
|
|
||||||
<channel-group-type id="dept-pollens">
|
<channel-group-type id="dept-pollens">
|
||||||
<label>Pollen information for the department</label>
|
<label>Pollen information for the department</label>
|
||||||
|
@ -126,7 +126,7 @@
|
|||||||
<channel-type id="urticaceae-level">
|
<channel-type id="urticaceae-level">
|
||||||
<item-type>Number</item-type>
|
<item-type>Number</item-type>
|
||||||
<label>Urticacea</label>
|
<label>Urticacea</label>
|
||||||
<category>oh:airparif:urticacea</category>
|
<category>oh:airparif:urticaceae</category>
|
||||||
<state readOnly="true">
|
<state readOnly="true">
|
||||||
<options>
|
<options>
|
||||||
<option value="0">None</option>
|
<option value="0">None</option>
|
||||||
@ -291,17 +291,23 @@
|
|||||||
</state>
|
</state>
|
||||||
</channel-type>
|
</channel-type>
|
||||||
|
|
||||||
<channel-type id="ppb-value">
|
<channel-type id="ppb-value">
|
||||||
<item-type unitHint="ppb">Number:Dimensionless</item-type>
|
<item-type unitHint="ppb">Number:Dimensionless</item-type>
|
||||||
<label>Measure</label>
|
<label>Measure</label>
|
||||||
<state readOnly="true" pattern="%.0f %unit%"/>
|
<state readOnly="true" pattern="%.0f %unit%"/>
|
||||||
</channel-type>
|
</channel-type>
|
||||||
|
|
||||||
<channel-type id="mpc-value">
|
<channel-type id="ndx-value">
|
||||||
<item-type unitHint="µg/m³">Number:Density</item-type>
|
<item-type>Number</item-type>
|
||||||
<label>Measure</label>
|
<label>Measure</label>
|
||||||
<state readOnly="true" pattern="%.0f %unit%"/>
|
<state readOnly="true"/>
|
||||||
</channel-type>
|
</channel-type>
|
||||||
|
|
||||||
|
<channel-type id="mpc-value">
|
||||||
|
<item-type unitHint="µg/m³">Number:Density</item-type>
|
||||||
|
<label>Measure</label>
|
||||||
|
<state readOnly="true" pattern="%.0f %unit%"/>
|
||||||
|
</channel-type>
|
||||||
|
|
||||||
|
|
||||||
</thing:thing-descriptions>
|
</thing:thing-descriptions>
|
||||||
|
@ -14,6 +14,19 @@
|
|||||||
|
|
||||||
<channel-groups>
|
<channel-groups>
|
||||||
<channel-group id="pollens" typeId="dept-pollens"/>
|
<channel-group id="pollens" typeId="dept-pollens"/>
|
||||||
|
<channel-group id="indice" typeId="pollutant-ndx"/>
|
||||||
|
<channel-group id="o3" typeId="pollutant-ppb">
|
||||||
|
<label>Ozone Concentration Information</label>
|
||||||
|
</channel-group>
|
||||||
|
<channel-group id="no2" typeId="pollutant-ppb">
|
||||||
|
<label>NO2 Concentration Information</label>
|
||||||
|
</channel-group>
|
||||||
|
<channel-group id="pm25" typeId="pollutant-mpc">
|
||||||
|
<label>PM2.5 Concentration Information</label>
|
||||||
|
</channel-group>
|
||||||
|
<channel-group id="pm10" typeId="pollutant-mpc">
|
||||||
|
<label>PM10 Concentration Information</label>
|
||||||
|
</channel-group>
|
||||||
</channel-groups>
|
</channel-groups>
|
||||||
|
|
||||||
<representation-property>department</representation-property>
|
<representation-property>department</representation-property>
|
||||||
|
Loading…
Reference in New Issue
Block a user