Adding Air Quality index

Signed-off-by: Gaël L'hopital <gael@lhopital.org>
This commit is contained in:
Gaël L'hopital 2024-11-19 12:22:41 +01:00
parent 8fcac57da6
commit 9aa3255ada
44 changed files with 216 additions and 140 deletions

View File

@ -47,6 +47,7 @@ public class AirParifBindingConstants {
public static final String CHANNEL_TOMORROW = "tomorrow";
public static final String CHANNEL_TIMESTAMP = "timestamp";
public static final String CHANNEL_VALUE = "value";
public static final String CHANNEL_ALERT = "alert";
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(APIBRIDGE_THING_TYPE,
LOCATION_THING_TYPE);

View File

@ -41,14 +41,12 @@ import org.osgi.service.component.annotations.Reference;
public class AirParifHandlerFactory extends BaseThingHandlerFactory {
private final AirParifDeserializer deserializer;
private final HttpClient httpClient;
private final AirParifIconProvider iconProvider;
@Activate
public AirParifHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
final @Reference AirParifDeserializer deserializer, final @Reference AirParifIconProvider iconProvider) {
final @Reference AirParifDeserializer deserializer) {
this.httpClient = httpClientFactory.getCommonHttpClient();
this.deserializer = deserializer;
this.iconProvider = iconProvider;
}
@Override
@ -61,7 +59,7 @@ public class AirParifHandlerFactory extends BaseThingHandlerFactory {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
return APIBRIDGE_THING_TYPE.equals(thingTypeUID)
? new AirParifBridgeHandler((Bridge) thing, httpClient, deserializer, iconProvider)
? new AirParifBridgeHandler((Bridge) thing, httpClient, deserializer)
: LOCATION_THING_TYPE.equals(thingTypeUID) ? new LocationHandler(thing) : null;
}
}

View File

@ -26,7 +26,6 @@ import java.util.Set;
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.ColorMap;
import org.openhab.binding.airparif.internal.api.PollenAlertLevel;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.ui.icon.IconProvider;
@ -50,15 +49,14 @@ import org.slf4j.LoggerFactory;
public class AirParifIconProvider implements IconProvider {
private static final String NEUTRAL_COLOR = "#3d3c3c";
private static final String DEFAULT_LABEL = "Air Parif Icons";
private static final String AQ_ICON = "aq";
private static final String DEFAULT_DESCRIPTION = "Icons illustrating air quality levels provided by AirParif";
private static final List<String> ICONS = List.of("average", "bad", "degrated", "extremely-bad", "good", "pollen");
private static final List<String> POLLEN_ICONS = Pollen.AS_SET.stream().map(Pollen::name).map(String::toLowerCase)
.toList();
private final Logger logger = LoggerFactory.getLogger(AirParifIconProvider.class);
private final TranslationProvider i18nProvider;
private final Bundle bundle;
private @Nullable ColorMap colorMap;
@Activate
public AirParifIconProvider(final BundleContext context, final @Reference TranslationProvider i18nProvider) {
@ -87,24 +85,31 @@ public class AirParifIconProvider implements IconProvider {
@Override
public @Nullable Integer hasIcon(String category, String iconSetId, Format format) {
return Format.SVG.equals(format) && iconSetId.equals(BINDING_ID)
&& (ICONS.contains(category) || POLLEN_ICONS.contains(category)) ? 0 : null;
&& (category.equals(AQ_ICON) || POLLEN_ICONS.contains(category)) ? 0 : null;
}
@Override
public @Nullable InputStream getIcon(String category, String iconSetId, @Nullable String state, Format format) {
URL iconResource = bundle.getEntry("icon/%s.svg".formatted(category));
int ordinal = -1;
try {
ordinal = state != null ? Integer.valueOf(state) : -1;
} catch (NumberFormatException ignore) {
}
String iconName = "icon/%s.svg".formatted(category);
if (category.equals(AQ_ICON) && ordinal != -1) {
iconName = iconName.replace(".", "-%d.".formatted(ordinal));
}
URL iconResource = bundle.getEntry(iconName);
String result;
try (InputStream stream = iconResource.openStream()) {
result = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
if (POLLEN_ICONS.contains(category) && state != null) {
try {
int ordinal = Integer.valueOf(state);
PollenAlertLevel alertLevel = PollenAlertLevel.valueOf(ordinal);
result = result.replaceAll(NEUTRAL_COLOR, alertLevel.color);
} catch (NumberFormatException ignore) {
}
if (POLLEN_ICONS.contains(category)) {
PollenAlertLevel alertLevel = PollenAlertLevel.valueOf(ordinal);
result = result.replaceAll(NEUTRAL_COLOR, alertLevel.color);
}
} catch (IOException e) {
logger.warn("Unable to load ressource '{}': {}", iconResource.getPath(), e.getMessage());
@ -113,8 +118,4 @@ public class AirParifIconProvider implements IconProvider {
return result.isEmpty() ? null : new ByteArrayInputStream(result.getBytes());
}
public void setColorMap(ColorMap map) {
this.colorMap = map;
}
}

View File

@ -62,7 +62,7 @@ public class AirParifApi {
AVERAGE("Moyen"),
DEGRATED("Dégradé"),
BAD("Mauvais"),
REALLY_BAD("Très Mauvais"),
VERY_BAD("Très Mauvais"),
EXTREMELY_BAD("Extrêmement Mauvais"),
UNKNOWN("");

View File

@ -27,10 +27,13 @@ import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.measure.Unit;
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.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
@ -63,6 +66,22 @@ public class AirParifDto {
Pollutant pollutant, //
int min, //
int max) {
private State getQuantity(int value) {
Unit<?> unit = pollutant.unit;
if (unit != null) {
return new QuantityType<>(value, unit);
}
return UnDefType.NULL;
}
public State getMin() {
return getQuantity(min);
}
public State getMax() {
return getQuantity(max);
}
}
public record PollutantEpisode(//
@ -185,9 +204,18 @@ public class AirParifDto {
return message != null ? new StringType(message.fr()) : UnDefType.NULL;
}
public State getQuantity() {
Unit<?> unit = pollutant.unit;
return unit != null ? new QuantityType<>(getValue(), unit) : UnDefType.NULL;
}
public double getValue() {
return values[0];
}
public int getAlertLevel() {
return pollutant.getAppreciation(getValue()).ordinal();
}
}
public record Route(//

View File

@ -17,6 +17,8 @@ import java.util.EnumSet;
import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.airparif.internal.api.AirParifApi.Appreciation;
import org.openhab.core.library.unit.Units;
import com.google.gson.annotations.SerializedName;
@ -28,29 +30,37 @@ import com.google.gson.annotations.SerializedName;
*/
@NonNullByDefault
public enum Pollutant {
// Concentration thresholds per pollutant are available here:
// https://www.airparif.fr/sites/default/files/pdf/guide_calcul_nouvel_indice_fedeAtmo_14122020.pdf
@SerializedName("pm25")
PM25(Units.MICROGRAM_PER_CUBICMETRE),
PM25(Units.MICROGRAM_PER_CUBICMETRE, new int[] { 10, 20, 25, 50, 75 }),
@SerializedName("pm10")
PM10(Units.MICROGRAM_PER_CUBICMETRE),
PM10(Units.MICROGRAM_PER_CUBICMETRE, new int[] { 20, 40, 50, 100, 150 }),
@SerializedName("no2")
NO2(Units.PARTS_PER_BILLION),
NO2(Units.MICROGRAM_PER_CUBICMETRE, new int[] { 40, 90, 120, 230, 340 }),
@SerializedName("o3")
O3(Units.PARTS_PER_BILLION),
O3(Units.MICROGRAM_PER_CUBICMETRE, new int[] { 50, 100, 130, 240, 380 }),
@SerializedName("so2")
SO2(Units.MICROGRAM_PER_CUBICMETRE, new int[] { 100, 200, 350, 500, 750 }),
@SerializedName("indice")
INDICE(Units.PERCENT),
INDICE(null, new int[] {}),
UNKNOWN(Units.PERCENT);
UNKNOWN(null, new int[] {});
public static final EnumSet<Pollutant> AS_SET = EnumSet.allOf(Pollutant.class);
public final Unit<?> unit;
public final @Nullable Unit<?> unit;
private final int[] thresholds;
Pollutant(Unit<?> unit) {
Pollutant(@Nullable Unit<?> unit, int[] thresholds) {
this.unit = unit;
this.thresholds = thresholds;
}
public static Pollutant safeValueOf(String searched) {
@ -60,4 +70,21 @@ public enum Pollutant {
return Pollutant.UNKNOWN;
}
}
public boolean hasUnit() {
return unit != null;
}
public Appreciation getAppreciation(double concentration) {
if (thresholds.length == 0) {
return Appreciation.UNKNOWN;
}
for (int i = 0; i < thresholds.length; i++) {
if (concentration <= thresholds[i]) {
return Appreciation.values()[i];
}
}
return Appreciation.EXTREMELY_BAD;
}
}

View File

@ -48,7 +48,6 @@ import org.eclipse.jetty.http.HttpMethod;
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;
@ -57,13 +56,11 @@ import org.openhab.binding.airparif.internal.api.AirParifDto.KeyInfo;
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;
@ -97,17 +94,14 @@ public class AirParifBridgeHandler extends BaseBridgeHandler implements HandlerU
private final Logger logger = LoggerFactory.getLogger(AirParifBridgeHandler.class);
private final Map<String, ScheduledFuture<?>> jobs = new HashMap<>();
private final AirParifDeserializer deserializer;
private final AirParifIconProvider iconProvider;
private final HttpClient httpClient;
private BridgeConfiguration config = new BridgeConfiguration();
private @Nullable PollensResponse pollens;
public AirParifBridgeHandler(Bridge bridge, HttpClient httpClient, AirParifDeserializer deserializer,
AirParifIconProvider iconProvider) {
public AirParifBridgeHandler(Bridge bridge, HttpClient httpClient, AirParifDeserializer deserializer) {
super(bridge);
this.deserializer = deserializer;
this.iconProvider = iconProvider;
this.httpClient = httpClient;
}
@ -202,14 +196,6 @@ public class AirParifBridgeHandler extends BaseBridgeHandler implements HandlerU
logger.info("The api key is valid until {}", keyInfo.expiration().toString());
updateStatus(ThingStatus.ONLINE);
try {
ColorMap map = executeUri(PREV_COLORS_URI, ColorMap.class);
logger.debug("The color map is {}", map.toString());
iconProvider.setColorMap(map);
} catch (AirParifException e) {
logger.warn("Error reading ColorMap: {}", e.getMessage());
}
ThingUID thingUID = thing.getUID();
schedule(POLLENS_JOB, () -> updatePollens(new ChannelGroupUID(thingUID, GROUP_POLLENS)), Duration.ofSeconds(1));
@ -257,15 +243,15 @@ public class AirParifBridgeHandler extends BaseBridgeHandler implements HandlerU
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.available() ? UnDefType.NULL : 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);
aq.available() ? measure.getMin() : UnDefType.NULL);
updateState(new ChannelUID(groupUID, cName + "max"),
aq.available() ? new QuantityType<>(measure.max(), pollutant.unit) : UnDefType.UNDEF);
aq.available() ? measure.getMax() : UnDefType.NULL);
});
});

View File

@ -15,6 +15,7 @@ package org.openhab.binding.airparif.internal.handler;
import static org.openhab.binding.airparif.internal.AirParifBindingConstants.*;
import java.time.Duration;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
@ -23,13 +24,14 @@ import java.util.concurrent.ScheduledFuture;
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.AirParifDto.Concentration;
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.api.Pollutant;
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;
@ -100,14 +102,22 @@ public class LocationHandler extends BaseThingHandler implements HandlerUtils {
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));
int maxAlert = route.concentrations().stream().filter(conc -> conc.pollutant().hasUnit())
.map(Concentration::getAlertLevel).max(Comparator.comparing(Integer::valueOf)).get();
route.concentrations().stream().forEach(concentration -> {
Pollutant pollutant = concentration.pollutant();
ChannelGroupUID groupUID = new ChannelGroupUID(thing.getUID(), pollutant.name().toLowerCase());
updateState(new ChannelUID(groupUID, CHANNEL_MESSAGE), concentration.getMessage());
if (!pollutant.hasUnit()) {
updateState(new ChannelUID(groupUID, CHANNEL_TIMESTAMP),
new DateTimeType(concentration.date()));
updateState(new ChannelUID(groupUID, CHANNEL_ALERT), new DecimalType(maxAlert));
} else {
updateState(new ChannelUID(groupUID, CHANNEL_VALUE), concentration.getQuantity());
updateState(new ChannelUID(groupUID, CHANNEL_ALERT),
new DecimalType(concentration.getAlertLevel()));
}
});
updateStatus(ThingStatus.ONLINE);
}

View File

@ -67,26 +67,19 @@ 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.alert.label = Alert Level
channel-group-type.airparif.pollutant-mpc.channel.alert.description = Alert Level associated to pollutant concentration
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.alert.label = Alert Level
channel-group-type.airparif.pollutant-ndx.channel.alert.description = Alert Level associated to highest pollutant concentration
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
@ -95,6 +88,13 @@ channel-type.airparif.alder-level.state.option.0 = None
channel-type.airparif.alder-level.state.option.1 = Low
channel-type.airparif.alder-level.state.option.2 = Average
channel-type.airparif.alder-level.state.option.3 = High
channel-type.airparif.appreciation.label = Air Quality
channel-type.airparif.appreciation.state.option.0 = Good
channel-type.airparif.appreciation.state.option.1 = Average
channel-type.airparif.appreciation.state.option.2 = Degrated
channel-type.airparif.appreciation.state.option.3 = Bad
channel-type.airparif.appreciation.state.option.4 = Very Bad
channel-type.airparif.appreciation.state.option.5 = Extremely Bad
channel-type.airparif.ash-level.label = Ash
channel-type.airparif.ash-level.state.option.0 = None
channel-type.airparif.ash-level.state.option.1 = Low
@ -163,7 +163,6 @@ 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
@ -191,6 +190,24 @@ channel-type.airparif.wormwood-level.state.option.1 = Low
channel-type.airparif.wormwood-level.state.option.2 = Average
channel-type.airparif.wormwood-level.state.option.3 = High
# channel group types
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-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-type.airparif.ppb-value.label = Measure
# thing types
thing-type.airparif.location.channel.end-validity.label = End Of Validity

View File

@ -29,19 +29,19 @@
<label>Message</label>
<description>General message for the air quality bulletin</description>
</channel>
<channel id="no2-min" typeId="ppb-value">
<channel id="no2-min" typeId="mpc-value">
<label>NO2 Min</label>
<description>Minimum level of NO2 concentation</description>
</channel>
<channel id="no2-max" typeId="ppb-value">
<channel id="no2-max" typeId="mpc-value">
<label>NO2 Max</label>
<description>Maximum level of NO2 concentation</description>
</channel>
<channel id="o3-min" typeId="ppb-value">
<channel id="o3-min" typeId="mpc-value">
<label>O3 Min</label>
<description>Minimum level of O3 concentation</description>
</channel>
<channel id="o3-max" typeId="ppb-value">
<channel id="o3-max" typeId="mpc-value">
<label>O3 Max</label>
<description>Maximum level of O3 concentation</description>
</channel>
@ -71,31 +71,13 @@
<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 id="alert" typeId="appreciation">
<label>Alert Level</label>
<description>Alert Level associated to pollutant concentration</description>
</channel>
</channels>
</channel-group-type>
@ -111,9 +93,9 @@
<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 id="alert" typeId="appreciation">
<label>Alert Level</label>
<description>Alert Level associated to highest pollutant concentration</description>
</channel>
</channels>
</channel-group-type>

View File

@ -291,12 +291,6 @@
</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="ndx-value">
<item-type>Number</item-type>
<label>Measure</label>
@ -309,5 +303,21 @@
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="appreciation">
<item-type>Number</item-type>
<label>Air Quality</label>
<category>oh:airparif:aq</category>
<state readOnly="true">
<options>
<option value="0">Good</option>
<option value="1">Average</option>
<option value="2">Degrated</option>
<option value="3">Bad</option>
<option value="4">Very Bad</option>
<option value="5">Extremely Bad</option>
</options>
</state>
</channel-type>
</thing:thing-descriptions>

View File

@ -15,10 +15,10 @@
<channel-groups>
<channel-group id="pollens" typeId="dept-pollens"/>
<channel-group id="indice" typeId="pollutant-ndx"/>
<channel-group id="o3" typeId="pollutant-ppb">
<channel-group id="o3" typeId="pollutant-mpc">
<label>Ozone Concentration Information</label>
</channel-group>
<channel-group id="no2" typeId="pollutant-ppb">
<channel-group id="no2" typeId="pollutant-mpc">
<label>NO2 Concentration Information</label>
</channel-group>
<channel-group id="pm25" typeId="pollutant-mpc">

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 98.15 95">
<circle cx="47.85" cy="48.04" r="34.72" fill="#fff" stroke="#8bd1f5" stroke-miterlimit="10" stroke-width="14" />
<path
d="M47.45 65.74c-4.39 0-9.68-2.37-12.06-6.57-2.18-3.85 2.56-3.66 2.56-3.66h20.84s4.81-.25 2.56 3.66c-2.41 4.19-7.68 6.57-12.06 6.57h-1.83z"
fill="none" stroke="#1e1e1c" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" />
<circle cx="58.74" cy="41.74" r="3.57" class="prefix__prefix__cls-3" />
<circle cx="36.27" cy="41.74" r="3.57" class="prefix__prefix__cls-3" />
</svg>

After

Width:  |  Height:  |  Size: 585 B

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 98.15 95">
<circle cx="47.85" cy="48.04" r="34.72" fill="#fff" stroke="#65bba2" stroke-miterlimit="10" stroke-width="14" />
<circle cx="59.23" cy="41.74" r="3.57" class="prefix__prefix__cls-2" />
<circle cx="36.76" cy="41.74" r="3.57" class="prefix__prefix__cls-2" />
<path d="M35.27 59.3l26.81-.05" fill="none" stroke="#1e1e1c" stroke-linecap="round" stroke-linejoin="round"
stroke-width="3" />
</svg>

After

Width:  |  Height:  |  Size: 467 B

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 98.15 95">
<circle cx="47.85" cy="48.04" r="34.72" fill="#fff" stroke="#ffe00a" stroke-miterlimit="10" stroke-width="14" />
<path d="M34.29 63.64s4.34-8.68 13.28-8.68 12.34 4.86 14.81 8.43" fill="none" stroke="#1e1e1c" stroke-linecap="round"
stroke-linejoin="round" stroke-width="3" />
<circle cx="59.32" cy="41.74" r="3.57" class="prefix__cls-2" />
<circle cx="36.85" cy="41.74" r="3.57" class="prefix__cls-2" />
</svg>

After

Width:  |  Height:  |  Size: 485 B

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 98.15 95">
<circle cx="47.85" cy="48.04" r="34.72" stroke-miterlimit="10" fill="#fff" stroke="#ea5553" stroke-width="14" />
<path d="M34.46 63.64s4.34-8.68 13.28-8.68 12.34 4.86 14.81 8.43" fill="none" stroke="#1e1e1c"
stroke-linecap="round" stroke-width="3" stroke-linejoin="round" />
<circle cx="59.48" cy="41.74" r="3.57" class="prefix__cls-3" />
<circle cx="37.01" cy="41.74" r="3.57" class="prefix__cls-3" />
<path d="M32.51 30.33l16.13 7.93 15.59-7.93" stroke-miterlimit="10" fill="none" stroke="#1e1e1c"
stroke-linecap="round" stroke-width="3" />
</svg>

After

Width:  |  Height:  |  Size: 667 B

View File

@ -0,0 +1,9 @@
<svg viewBox="0 0 98.15 95" xmlns="http://www.w3.org/2000/svg">
<circle cx="47.85" cy="48.04" r="34.72" fill="#fff" stroke="#961638" stroke-miterlimit="10" stroke-width="14" />
<path
d="M41.99 35.85c3.181 4.84-5.864 10.379-8.17 6.64m18.58-6.64c-2.401 5.325 6.835 9.434 8.17 6.64m3.61 17.43s-2.68-3.27-7.14-3.27c-4.46 0-5.48 6.864-9.05 7.164-3.57.3-4.63-7.164-8.2-7.164-3.57 0-4.025 1.431-8.62 3.57"
class="prefix__cls-1" />
<path
d="M67.67 17.15c-2.39 2.82-6.78 8.61-6.78 11.52 0 3.78 3.04 6.86 6.78 6.86s6.78-3.08 6.78-6.86c0-2.91-4.39-8.7-6.78-11.52z"
fill="#fff" stroke="#1e1e1c" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" />
</svg>

After

Width:  |  Height:  |  Size: 678 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 98.15 95">
<circle cx="47.85" cy="48.04" r="34.72" fill="#fff" stroke="#7b2681" stroke-miterlimit="10" stroke-width="14" />
<path
d="M63.81 60.51l-4.15-4.06-4.15 3.99-4.07-4.23-3.83 4.23h-.01l-4.15-4.07-4.15 3.99-4.07-4.23-3.83 4.23m3.96-27.64l8.17 5.62-8.17 6.64m24.75-12.26l-8.17 5.62 8.17 6.64"
class="prefix__cls-1" />
</svg>

After

Width:  |  Height:  |  Size: 394 B

View File

@ -0,0 +1,7 @@
<svg viewBox="0 0 98.15 95" xmlns="http://www.w3.org/2000/svg">
<circle cx="47.85" cy="48.04" r="34.72" fill="#fff" stroke-miterlimit="10" stroke-width="14" stroke="#adadad" />
<circle cx="59.23" cy="41.74" r="3.57" class="prefix__prefix__prefix__prefix__cls-2" />
<circle cx="36.76" cy="41.74" r="3.57" class="prefix__prefix__prefix__prefix__cls-2" />
<path d="M35.27 59.3l26.81-.05" fill="none" stroke="#1e1e1c" stroke-linecap="round" stroke-linejoin="round"
stroke-width="3" />
</svg>

After

Width:  |  Height:  |  Size: 499 B

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 98.15 95">
<circle cx="47.85" cy="48.04" r="34.72" style="fill:#fff;stroke:#65bba2;stroke-miterlimit:10;stroke-width:14px"/>
<circle cx="59.23" cy="41.74" r="3.57" class="cls-2"/>
<circle cx="36.76" cy="41.74" r="3.57" class="cls-2"/>
<path d="m35.27 59.3 26.81-.05"
style="fill:none;stroke:#1e1e1c;stroke-linecap:round;stroke-linejoin:round;stroke-width:3px"/>
</svg>

Before

Width:  |  Height:  |  Size: 452 B

View File

@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 98.15 95">
<circle cx="47.85" cy="48.04" r="34.72" style="stroke-miterlimit:10;fill:#fff;stroke:#ea5553;stroke-width:14px"/>
<path d="M34.46 63.64s4.34-8.68 13.28-8.68 12.34 4.86 14.81 8.43"
style="fill:none;stroke:#1e1e1c;stroke-linecap:round;stroke-width:3px;stroke-linejoin:round"/>
<circle cx="59.48" cy="41.74" r="3.57" class="cls-3"/>
<circle cx="37.01" cy="41.74" r="3.57" class="cls-3"/>
<path d="m32.51 30.33 16.13 7.93 15.59-7.93"
style="stroke-miterlimit:10;fill:none;stroke:#1e1e1c;stroke-linecap:round;stroke-width:3px"/>
</svg>

Before

Width:  |  Height:  |  Size: 635 B

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 98.15 95">
<circle cx="47.85" cy="48.04" r="34.72" style="fill:#fff;stroke:#ffe00a;stroke-miterlimit:10;stroke-width:14px"/>
<path d="M34.29 63.64s4.34-8.68 13.28-8.68 12.34 4.86 14.81 8.43"
style="fill:none;stroke:#1e1e1c;stroke-linecap:round;stroke-linejoin:round;stroke-width:3px"/>
<circle cx="59.32" cy="41.74" r="3.57" class="cls-2"/>
<circle cx="36.85" cy="41.74" r="3.57" class="cls-2"/>
</svg>

Before

Width:  |  Height:  |  Size: 486 B

View File

@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 98.15 95">
<circle cx="47.85" cy="48.04" r="34.72" style="fill:#fff;stroke:#7b2681;stroke-miterlimit:10;stroke-width:14px"/>
<path
d="m63.81 60.51-4.15-4.06-4.15 3.99-4.07-4.23-3.83 4.23h-.01l-4.15-4.07-4.15 3.99-4.07-4.23-3.83 4.23M35.36 32.72l8.17 5.62-8.17 6.64M60.11 32.72l-8.17 5.62 8.17 6.64"
class="cls-1"/>
</svg>

Before

Width:  |  Height:  |  Size: 401 B

View File

@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 98.15 95">
<circle cx="47.85" cy="48.04" r="34.72" style="fill:#fff;stroke:#8bd1f5;stroke-miterlimit:10;stroke-width:14px"/>
<path
d="M47.45 65.74c-4.39 0-9.68-2.37-12.06-6.57-2.18-3.85 2.56-3.66 2.56-3.66h20.84s4.81-.25 2.56 3.66c-2.41 4.19-7.68 6.57-12.06 6.57h-1.83Z"
style="fill:none;stroke:#1e1e1c;stroke-linecap:round;stroke-linejoin:round;stroke-width:3px"/>
<circle cx="58.74" cy="41.74" r="3.57" class="cls-3"/>
<circle cx="36.27" cy="41.74" r="3.57" class="cls-3"/>
</svg>

Before

Width:  |  Height:  |  Size: 566 B

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB