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_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<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(APIBRIDGE_THING_TYPE,
|
||||
LOCATION_THING_TYPE);
|
||||
|
@ -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<Pollens> data = List.of();
|
||||
private @Nullable Set<ZonedDateTime> validities;
|
||||
private @Nullable ZonedDateTime beginValidity;
|
||||
private @Nullable ZonedDateTime endValidity;
|
||||
|
||||
public Optional<Pollens> getData() {
|
||||
return Optional.ofNullable(data.isEmpty() ? null : data.get(0));
|
||||
}
|
||||
|
||||
private Set<ZonedDateTime> getValidities() {
|
||||
Set<ZonedDateTime> 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<ZonedDateTime> 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<ZonedDateTime> getBeginValidity() {
|
||||
return Optional.ofNullable(getValidities().iterator().next());
|
||||
if (beginValidity == null) {
|
||||
beginValidity = getValidities().iterator().next();
|
||||
}
|
||||
return Optional.ofNullable(beginValidity);
|
||||
}
|
||||
|
||||
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() {
|
||||
@ -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<Concentration> concentrations, //
|
||||
@Nullable Message[] messages) {
|
||||
|
||||
}
|
||||
|
@ -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<String, ScheduledFuture<?>> 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<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;
|
||||
|
||||
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<String, ScheduledFuture<?>> jobs = new HashMap<>();
|
||||
|
||||
private @Nullable LocationConfiguration config;
|
||||
private Map<Pollen, PollenAlertLevel> 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<Pollen, PollenAlertLevel> 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<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() {
|
||||
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<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.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
|
||||
|
@ -13,10 +13,12 @@
|
||||
|
||||
<channel-groups>
|
||||
<channel-group id="pollens" typeId="bridge-pollens"/>
|
||||
<channel-group id="aq-bulletin" typeId="air-quality-bulletin">
|
||||
<label>Today'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="aq-bulletin" typeId="air-quality-bulletin">
|
||||
<label>Today'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-groups>
|
||||
|
||||
|
@ -22,61 +22,115 @@
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="air-quality-bulletin">
|
||||
<label>Air Quality Bulletin</label>
|
||||
<channels>
|
||||
<channel id="comment" typeId="comment">
|
||||
<label>Message</label>
|
||||
<description>General message for the air quality bulletin</description>
|
||||
</channel>
|
||||
<channel id="no2-min" typeId="ppb-value">
|
||||
<label>NO2 Min</label>
|
||||
<description>Minimum level of NO2 concentation</description>
|
||||
</channel>
|
||||
<channel id="no2-max" typeId="ppb-value">
|
||||
<label>NO2 Max</label>
|
||||
<description>Maximum level of NO2 concentation</description>
|
||||
</channel>
|
||||
<channel id="o3-min" typeId="ppb-value">
|
||||
<label>O3 Min</label>
|
||||
<description>Minimum level of O3 concentation</description>
|
||||
</channel>
|
||||
<channel id="o3-max" typeId="ppb-value">
|
||||
<label>O3 Max</label>
|
||||
<description>Maximum level of O3 concentation</description>
|
||||
</channel>
|
||||
<channel id="pm10-min" typeId="mpc-value">
|
||||
<label>PM 10 Min</label>
|
||||
<description>Minimum level of PM 10 concentation</description>
|
||||
</channel>
|
||||
<channel id="pm10-max" typeId="mpc-value">
|
||||
<label>PM 10 Max</label>
|
||||
<description>Maximum level of PM 10 concentation</description>
|
||||
</channel>
|
||||
<channel id="pm25-min" typeId="mpc-value">
|
||||
<label>PM 2.5 Min</label>
|
||||
<description>Minimum level of PM 2.5 concentation</description>
|
||||
</channel>
|
||||
<channel id="pm25-max" typeId="mpc-value">
|
||||
<label>PM 2.5 Max</label>
|
||||
<description>Maximum level of PM 2.5 concentation</description>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
<channel-group-type id="air-quality-bulletin">
|
||||
<label>Air Quality Bulletin</label>
|
||||
<channels>
|
||||
<channel id="comment" typeId="comment">
|
||||
<label>Message</label>
|
||||
<description>General message for the air quality bulletin</description>
|
||||
</channel>
|
||||
<channel id="no2-min" typeId="ppb-value">
|
||||
<label>NO2 Min</label>
|
||||
<description>Minimum level of NO2 concentation</description>
|
||||
</channel>
|
||||
<channel id="no2-max" typeId="ppb-value">
|
||||
<label>NO2 Max</label>
|
||||
<description>Maximum level of NO2 concentation</description>
|
||||
</channel>
|
||||
<channel id="o3-min" typeId="ppb-value">
|
||||
<label>O3 Min</label>
|
||||
<description>Minimum level of O3 concentation</description>
|
||||
</channel>
|
||||
<channel id="o3-max" typeId="ppb-value">
|
||||
<label>O3 Max</label>
|
||||
<description>Maximum level of O3 concentation</description>
|
||||
</channel>
|
||||
<channel id="pm10-min" typeId="mpc-value">
|
||||
<label>PM 10 Min</label>
|
||||
<description>Minimum level of PM 10 concentation</description>
|
||||
</channel>
|
||||
<channel id="pm10-max" typeId="mpc-value">
|
||||
<label>PM 10 Max</label>
|
||||
<description>Maximum level of PM 10 concentation</description>
|
||||
</channel>
|
||||
<channel id="pm25-min" typeId="mpc-value">
|
||||
<label>PM 2.5 Min</label>
|
||||
<description>Minimum level of PM 2.5 concentation</description>
|
||||
</channel>
|
||||
<channel id="pm25-max" typeId="mpc-value">
|
||||
<label>PM 2.5 Max</label>
|
||||
<description>Maximum level of PM 2.5 concentation</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="pollutant-mpc">
|
||||
<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="mpc-value">
|
||||
<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">
|
||||
<label>Pollen information for the department</label>
|
||||
|
@ -126,7 +126,7 @@
|
||||
<channel-type id="urticaceae-level">
|
||||
<item-type>Number</item-type>
|
||||
<label>Urticacea</label>
|
||||
<category>oh:airparif:urticacea</category>
|
||||
<category>oh:airparif:urticaceae</category>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="0">None</option>
|
||||
@ -291,17 +291,23 @@
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="ppb-value">
|
||||
<item-type unitHint="ppb">Number:Dimensionless</item-type>
|
||||
<label>Measure</label>
|
||||
<state readOnly="true" pattern="%.0f %unit%"/>
|
||||
</channel-type>
|
||||
<channel-type id="ppb-value">
|
||||
<item-type unitHint="ppb">Number:Dimensionless</item-type>
|
||||
<label>Measure</label>
|
||||
<state readOnly="true" pattern="%.0f %unit%"/>
|
||||
</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>
|
||||
<channel-type id="ndx-value">
|
||||
<item-type>Number</item-type>
|
||||
<label>Measure</label>
|
||||
<state readOnly="true"/>
|
||||
</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>
|
||||
|
@ -14,6 +14,19 @@
|
||||
|
||||
<channel-groups>
|
||||
<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>
|
||||
|
||||
<representation-property>department</representation-property>
|
||||
|
Loading…
Reference in New Issue
Block a user