diff --git a/addons/binding/org.openhab.binding.airquality/.classpath b/addons/binding/org.openhab.binding.airquality/.classpath new file mode 100644 index 0000000000000..a95e0906ca013 --- /dev/null +++ b/addons/binding/org.openhab.binding.airquality/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/addons/binding/org.openhab.binding.airquality/.project b/addons/binding/org.openhab.binding.airquality/.project new file mode 100644 index 0000000000000..a6278b95ed034 --- /dev/null +++ b/addons/binding/org.openhab.binding.airquality/.project @@ -0,0 +1,33 @@ + + + org.openhab.binding.airquality + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.pde.ds.core.builder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/addons/binding/org.openhab.binding.airquality/ESH-INF/binding/binding.xml b/addons/binding/org.openhab.binding.airquality/ESH-INF/binding/binding.xml new file mode 100644 index 0000000000000..cea5f821b0232 --- /dev/null +++ b/addons/binding/org.openhab.binding.airquality/ESH-INF/binding/binding.xml @@ -0,0 +1,11 @@ + + + + Air Quality Binding + Measure Air Quality Index and details about pollution particles for a given location + Kuba Wolanin + + diff --git a/addons/binding/org.openhab.binding.airquality/ESH-INF/thing/thing-types.xml b/addons/binding/org.openhab.binding.airquality/ESH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..c7794d1cc7c5c --- /dev/null +++ b/addons/binding/org.openhab.binding.airquality/ESH-INF/thing/thing-types.xml @@ -0,0 +1,168 @@ + + + + + + + + Provides various air quality data from the World Air Quality Project. + In order to receive the data, you must register an account on http://aqicn.org/data-platform/token/ and get your API token. + + + + + + + + + + + + + + + + + + + + + + password + + Data-platform token to access the AQIcn.org service + + + + Your geo coordinates separated with comma (e.g. "37.8,-122.4"). + + + + Fill only in case you want to receive data from the specific station + + + + Specifies the refresh interval in minutes. + 60 + + + + + + Number + + + Air Quality Index + + + + + String + + + AQI Description + + + + + Number + + Fine particles pollution level + PM2.5 + + + + + Number + + Coarse dust particles pollution level + PM10 + + + + + Number + + Ozone level + O3 + + + + + Number + + Nitrogen dioxide level + NO2 + + + + + Number + + Carbon monoxide level + CO + + + + + String + + Nearest measuring station location + Location + + + + + Location + + Latitude/Longitude of the measuring station + Station Location + + + + + Number + + Unique measuring station ID + Station ID + + + + + DateTime + + Observation date and time + Observation time + + + + + Number + + Temperature in Celsius degrees + Temperature (°C) + + + + + Number + + + Pressure level + + + + + Number + + + Humidity level + + + + + + diff --git a/addons/binding/org.openhab.binding.airquality/META-INF/MANIFEST.MF b/addons/binding/org.openhab.binding.airquality/META-INF/MANIFEST.MF new file mode 100644 index 0000000000000..d032e8caf3d88 --- /dev/null +++ b/addons/binding/org.openhab.binding.airquality/META-INF/MANIFEST.MF @@ -0,0 +1,27 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: AirQuality Binding +Bundle-SymbolicName: org.openhab.binding.airquality;singleton:=true +Bundle-Vendor: openHAB +Bundle-Version: 2.1.0.qualifier +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ClassPath: . +Import-Package: + com.google.common.collect, + com.google.gson, + com.google.gson.annotations, + org.apache.commons.io, + org.apache.commons.lang, + org.eclipse.smarthome.config.core, + org.eclipse.smarthome.core.library.types, + org.eclipse.smarthome.core.thing, + org.eclipse.smarthome.core.thing.binding, + org.eclipse.smarthome.core.thing.binding.builder, + org.eclipse.smarthome.core.thing.type, + org.eclipse.smarthome.core.types, + org.openhab.binding.airquality, + org.openhab.binding.airquality.handler, + org.slf4j +Service-Component: OSGI-INF/*.xml +Export-Package: org.openhab.binding.airquality, + org.openhab.binding.airquality.handler diff --git a/addons/binding/org.openhab.binding.airquality/OSGI-INF/AirQualityHandlerFactory.xml b/addons/binding/org.openhab.binding.airquality/OSGI-INF/AirQualityHandlerFactory.xml new file mode 100644 index 0000000000000..d2a558b925133 --- /dev/null +++ b/addons/binding/org.openhab.binding.airquality/OSGI-INF/AirQualityHandlerFactory.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/addons/binding/org.openhab.binding.airquality/README.md b/addons/binding/org.openhab.binding.airquality/README.md new file mode 100644 index 0000000000000..7e800a1d7949b --- /dev/null +++ b/addons/binding/org.openhab.binding.airquality/README.md @@ -0,0 +1,198 @@ +--- +layout: documentation +--- + +{% include base.html %} + +# Air Quality Binding + +This binding uses the [AQIcn.org service](https://www.wunderground.com/AirQuality/api/) for providing air quality information for any location worldwide. + +The World Air Quality Index project is a social enterprise project started in 2007. Its mission is to promote Air Pollution awareness and provide a unified Air Quality information for the whole world. + +The project is proving a transparent Air Quality information for more than 70 countries, covering more than 9000 stations in 600 major cities, via those two websites: [aqicn.org](http://aqicn.org) and [waqi.info](http://waqi.info). + +To use this binding, you first need to [register and get your API token](http://aqicn.org/data-platform/token/). + +## Supported Things + +There is exactly one supported thing type, which represents the air quality information for an observation location. It has the `aqi` id. Of course, you can add multiple Things, e.g. for measuring AQI for different locations. + +## Discovery + +There is no discovery implemented. You have to create your things manually. + +## Binding Configuration + +The binding has no configuration options, all configuration is done at Thing level. + +## Thing Configuration + +The thing has a few configuration parameters: + +| Parameter | Description | +|-----------|------------------------------------------------------------------------- | +| apikey | Data-platform token to access the AQIcn.org service. Mandatory. | +| location | Geo coordinates to be considered by the service. | +| stationId | Unique ID of the measuring station. | +| refresh | Refresh interval in minutes. Optional, the default value is 60 minutes. | + +For the location parameter, the following syntax is allowed (comma separated latitude and longitude): + +`37.8,-122.4` +`37.8255,-122.456` + +If you always want to receive data from specific station and you know its unique ID, you can enter it +instead of the coordinates. + + +## Channels + +The AirQuality information that is retrieved is available as these channels: + + +| Channel ID | Item Type | Description | +|------------|--------------|------------------------- | +| aqiLevel | Number | Air Quality Index | +| aqiDescription | String | AQI Description | +| locationName | String | Nearest measuring station location | +| stationId | Number | Measuring station ID | +| stationLocation | Location | Latitude/longitude of measuring station | +| pm25 | Number | Fine particles pollution level (PM2.5) | +| pm10 | Number | Coarse dust particles pollution level (PM10) | +| o3 | Number | Ozone level (O3) | +| no2 | Number | Nitrogen Dioxide level (NO2) | +| co | Number | Carbon monoxide level (CO) | +| observationTime | DateTime | Observation date and time | +| temperature | Number | Temperature in Celsius degrees | +| pressure | Number | Pressure level | +| humidity | Number | Humidity level | + +`AQI Description` item provides a human-readable output that can be interpreted e.g. by MAP transformation. + +*Note that channels like* `pm25`, `pm10`, `o3`, `no2`, `co` *can sometimes return* `UNDEF` *value due to the fact that some stations don't provide measurements for them.* + + +## Full Example + +airquality.map: + +``` +-=- +UNDEF=No data +NULL=No data +NO_DATA=No data +GOOD=Good +MODERATE=Moderate +UNHEALTHY_FOR_SENSITIVE=Unhealthy for sensitive groups +UNHEALTHY=Unhealthy +VERY_UNHEALTHY=Very unhealthy +HAZARDOUS=Hazardous +``` + +airquality.things: + +``` +airquality:aqi:home "AirQuality" @ "Krakow" [ apikey="XXXXXXXXXXXX", location="50.06465,19.94498", refresh=60 ] +airquality:aqi:warsaw "AirQuality in Warsaw" [ apikey="XXXXXXXXXXXX", location="52.22,21.01", refresh=60 ] +airquality:aqi:brisbane "AirQuality in Brisbane" [ apikey="XXXXXXXXXXXX", stationId=5115 ] +``` + +airquality.items: + +``` +Group AirQuality + +Number Aqi_Level "Air Quality Index" (AirQuality) { channel="airquality:aqi:home:aqiLevel" } +String Aqi_Description "AQI Level [MAP(airquality.map):%s]" (AirQuality) { channel="airquality:aqi:home:aqiDescription" } + +Number Aqi_Pm25 "PM\u2082\u2085 Level" (AirQuality) { channel="airquality:aqi:home:pm25" } +Number Aqi_Pm10 "PM\u2081\u2080 Level" (AirQuality) { channel="airquality:aqi:home:pm10" } +Number Aqi_O3 "O\u2083 Level" (AirQuality) { channel="airquality:aqi:home:o3" } +Number Aqi_No2 "NO\u2082 Level" (AirQuality) { channel="airquality:aqi:home:no2" } +Number Aqi_Co "CO Level" (AirQuality) { channel="airquality:aqi:home:co" } + +String Aqi_LocationName "Measuring Location" (AirQuality) { channel="airquality:aqi:home:locationName" } +Location Aqi_StationGeo "Station Location" (AirQuality) { channel="airquality:aqi:home:stationLocation" } +Number Aqi_StationId "Station ID" (AirQuality) { channel="airquality:aqi:home:stationId" } +DateTime Aqi_ObservationTime "Time of observation [%1$tH:%1$tM]" (AirQuality) { channel="airquality:aqi:home:observationTime" } + +Number Aqi_Temperature "Temperature" (AirQuality) { channel="airquality:aqi:home:temperature" } +Number Aqi_Pressure "Pressure" (AirQuality) { channel="airquality:aqi:home:pressure" } +Number Aqi_Humidity "Humidity" (AirQuality) { channel="airquality:aqi:home:humidity" } +``` + +airquality.sitemap: + +``` +sitemap airquality label="Air Quality" { + Frame { + Text item=Aqi_Level valuecolor=[ + Aqi_Level=="-"="lightgray", + Aqi_Level>=300="#7e0023", + >=201="#660099", + >=151="#cc0033", + >=101="#ff9933", + >=51="#ffde33", + >=0="#009966" + ] + Text item=Aqi_Description valuecolor=[ + Aqi_Description=="HAZARDOUS"="#7e0023", + =="VERY_UNHEALTHY"="#660099", + =="UNHEALTHY"="#cc0033", + =="UNHEALTHY_FOR_SENSITIVE"="#ff9933", + =="MODERATE"="#ffde33", + =="GOOD"="#009966" + ] + } + + Frame { + Text item=Aqi_Pm25 + Text item=Aqi_Pm10 + Text item=Aqi_O3 + Text item=Aqi_No2 + Text item=Aqi_Co + } + + Frame { + Text item=Aqi_LocationName + Text item=Aqi_ObservationTime + Text item=Aqi_Temperature + Text item=Aqi_Pressure + Text item=Aqi_Humidity + } + + Frame label="Station Location" { + Mapview item=Aqi_StationGeo height=10 + } +} + +``` + +airquality.rules: + +``` +rule "Change lamp color to reflect Air Quality" +when + Item Aqi_Description changed +then + var String hsb + + switch Aqi_Description.state { + case "HAZARDOUS": + hsb = "343,100,49" + case "VERY_UNHEALTHY": + hsb = "280,100,60" + case "UNHEALTHY": + hsb = "345,100,80" + case "UNHEALTHY_FOR_SENSITIVE": + hsb = "30,80,100" + case "MODERATE": + hsb = "50,80,100" + case "GOOD": + hsb = "160,100,60" + } + + sendCommand(Lamp_Color, hsb) +end +``` diff --git a/addons/binding/org.openhab.binding.airquality/about.html b/addons/binding/org.openhab.binding.airquality/about.html new file mode 100644 index 0000000000000..eff01772105b9 --- /dev/null +++ b/addons/binding/org.openhab.binding.airquality/about.html @@ -0,0 +1,51 @@ + + + + +About + + +

About This Content

+ +

<February 15, 2017>

+

License

+ +

The openHAB community makes available all content in this plug-in ("Content"). Unless otherwise +indicated below, the Content is provided to you under the terms and conditions of the +Eclipse Public License Version 1.0 ("EPL"). A copy of the EPL is available +at http://www.eclipse.org/legal/epl-v10.html. +For purposes of the EPL, "Program" will mean the Content.

+ +

If you did not receive this Content directly from the openHAB community, the Content is +being redistributed by another party ("Redistributor") and different terms and conditions may +apply to your use of any object code in the Content. Check the Redistributor's license that was +provided with the Content. If no such license exists, contact the Redistributor. Unless otherwise +indicated below, the terms and conditions of the EPL still apply to any source code in the Content +and such source code may be obtained at openhab.org.

+ + +

Third Party Content

+

The Content includes items that have been sourced from third parties as set out below. If you + did not receive this Content directly from the openHAB community, the following is provided + for informational purposes only, and you should look to the Redistributor's license for + terms and conditions of use.

+

+ AQIcn.org API

+ The World Air Quality Index project is an Internet service + offered by US Embassies in China that provides weather information for any location worldwide. + The usage of the API are subjected to a "acceptable usage" policy: +

+
    +
  • All the API are provided for free.
  • +
  • A valid key must be used for accessing the API.
  • +
  • All the API are subjected to quota.
  • +
  • The data from the API can not be sold and included in sold packages.
  • +
  • The data from the API can not be used in paid application or service.
  • +
  • Attribution to the World Air Quality Index Project as well as originating EPA is mandatory.
  • +
  • Public usage by for-profit corporations requires explict agreement wit the World Air Quality Index team.
  • +
  • Public usage by non-profit organization requires prior notification (by email) to the World Air Quality Index team.
  • +
+ + + diff --git a/addons/binding/org.openhab.binding.airquality/build.properties b/addons/binding/org.openhab.binding.airquality/build.properties new file mode 100644 index 0000000000000..66e21b90751a7 --- /dev/null +++ b/addons/binding/org.openhab.binding.airquality/build.properties @@ -0,0 +1,6 @@ +source.. = src/main/java/ +output.. = target/classes +bin.includes = META-INF/,\ + .,\ + OSGI-INF/,\ + ESH-INF/ \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.airquality/pom.xml b/addons/binding/org.openhab.binding.airquality/pom.xml new file mode 100644 index 0000000000000..cd1bc4f39e2d1 --- /dev/null +++ b/addons/binding/org.openhab.binding.airquality/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.binding + pom + 2.1.0-SNAPSHOT + + + org.openhab.binding.airquality + + AirQuality Binding + eclipse-plugin + + diff --git a/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/AirQualityBindingConstants.java b/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/AirQualityBindingConstants.java new file mode 100644 index 0000000000000..9656f2933e56d --- /dev/null +++ b/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/AirQualityBindingConstants.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.airquality; + +import java.util.Set; + +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +import com.google.common.collect.ImmutableSet; + +/** + * The {@link AirQualityBinding} class defines common constants, which are + * used across the whole binding. + * + * @author Kuba Wolanin - Initial contribution + * @author Łukasz Dywicki + */ +public class AirQualityBindingConstants { + + public static final String BINDING_ID = "airquality"; + + // List of all Thing Type UIDs + public final static ThingTypeUID THING_TYPE_AQI = new ThingTypeUID(BINDING_ID, "aqi"); + + // List of all Channel id's + public final static String AQI = "aqiLevel"; + public final static String AQIDESCRIPTION = "aqiDescription"; + public final static String PM25 = "pm25"; + public final static String PM10 = "pm10"; + public final static String O3 = "o3"; + public final static String NO2 = "no2"; + public final static String CO = "co"; + public final static String LOCATIONNAME = "locationName"; + public final static String STATIONLOCATION = "stationLocation"; + public final static String STATIONID = "stationId"; + public final static String OBSERVATIONTIME = "observationTime"; + public final static String TEMPERATURE = "temperature"; + public final static String PRESSURE = "pressure"; + public final static String HUMIDITY = "humidity"; + + public final static Set SUPPORTED_THING_TYPES_UIDS = ImmutableSet.of(THING_TYPE_AQI); + public final static Set SUPPORTED_CHANNEL_IDS = ImmutableSet.of(AQI, AQIDESCRIPTION, PM25, PM10, O3, NO2, + CO, LOCATIONNAME, STATIONLOCATION, STATIONID, OBSERVATIONTIME, TEMPERATURE, PRESSURE, HUMIDITY); + +} diff --git a/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/handler/AirQualityHandler.java b/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/handler/AirQualityHandler.java new file mode 100644 index 0000000000000..5f30fe35f843a --- /dev/null +++ b/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/handler/AirQualityHandler.java @@ -0,0 +1,322 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.airquality.handler; + +import java.io.IOException; +import java.math.BigDecimal; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.util.Calendar; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.eclipse.smarthome.core.library.types.DateTimeType; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.airquality.AirQualityBindingConstants; +import org.openhab.binding.airquality.internal.AirQualityConfiguration; +import org.openhab.binding.airquality.internal.json.AirQualityJsonResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; + +/** + * The {@link AirQualityHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Kuba Wolanin - Initial contribution + * @author Łukasz Dywicki + */ +public class AirQualityHandler extends BaseThingHandler { + + private Logger logger = LoggerFactory.getLogger(AirQualityHandler.class); + + private static final String URL = "http://api.waqi.info/feed/%QUERY%/?token=%apikey%"; + + private static final int DEFAULT_REFRESH_PERIOD = 30; + + private ScheduledFuture refreshJob; + + AirQualityJsonResponse aqiResponse; + + private Gson gson; + + public AirQualityHandler(Thing thing) { + super(thing); + gson = new Gson(); + } + + @Override + public void initialize() { + logger.debug("Initializing Air Quality handler."); + super.initialize(); + + AirQualityConfiguration config = getConfigAs(AirQualityConfiguration.class); + logger.debug("config apikey = (omitted from logging)"); + logger.debug("config location = {}", config.location); + logger.debug("config stationId = {}", config.stationId); + logger.debug("config refresh = {}", config.refresh); + + boolean validConfig = true; + String errorMsg = null; + + if (StringUtils.trimToNull(config.apikey) == null) { + errorMsg = "Parameter 'apikey' is mandatory and must be configured"; + validConfig = false; + } + if (StringUtils.trimToNull(config.location) == null && config.stationId == null) { + errorMsg = "Parameter 'location' or 'stationId' is mandatory and must be configured"; + validConfig = false; + } + if (config.refresh != null && config.refresh < 5) { + errorMsg = "Parameter 'refresh' must be at least 5 minutes"; + validConfig = false; + } + + if (validConfig) { + startAutomaticRefresh(); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMsg); + } + } + + /** + * Start the job refreshing the Air Quality data + */ + private void startAutomaticRefresh() { + if (refreshJob == null || refreshJob.isCancelled()) { + Runnable runnable = new Runnable() { + @Override + public void run() { + try { + // Request new air quality data to the aqicn.org service + aqiResponse = updateAirQualityData(); + + // Update all channels from the updated AQI data + for (Channel channel : getThing().getChannels()) { + updateChannel(channel.getUID().getId(), aqiResponse); + } + } catch (Exception e) { + logger.error("Exception occurred during execution: {}", e.getMessage(), e); + } + } + }; + + AirQualityConfiguration config = getConfigAs(AirQualityConfiguration.class); + int delay = (config.refresh != null) ? config.refresh.intValue() : DEFAULT_REFRESH_PERIOD; + refreshJob = scheduler.scheduleWithFixedDelay(runnable, 0, delay, TimeUnit.MINUTES); + } + } + + @Override + public void dispose() { + logger.debug("Disposing the Air Quality handler."); + + if (refreshJob != null && !refreshJob.isCancelled()) { + refreshJob.cancel(true); + refreshJob = null; + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + updateChannel(channelUID.getId(), aqiResponse); + } else { + logger.debug("The Air Quality binding is read-only and can not handle command {}", command); + } + } + + /** + * Update the channel from the last Air Quality data retrieved + * + * @param channelId the id identifying the channel to be updated + */ + private void updateChannel(String channelId, AirQualityJsonResponse aqiResponse) { + if (isLinked(channelId)) { + Object value; + try { + value = getValue(channelId, aqiResponse); + } catch (Exception e) { + logger.debug("Station doesn't provide {} measurement", channelId.toUpperCase()); + return; + } + + State state = null; + if (value == null) { + state = UnDefType.UNDEF; + } else if (value instanceof Calendar) { + state = new DateTimeType((Calendar) value); + } else if (value instanceof BigDecimal) { + state = new DecimalType((BigDecimal) value); + } else if (value instanceof Integer) { + state = new DecimalType(BigDecimal.valueOf(((Integer) value).longValue())); + } else if (value instanceof String) { + state = new StringType(value.toString()); + } else { + logger.warn("Update channel {}: Unsupported value type {}", channelId, + value.getClass().getSimpleName()); + } + logger.debug("Update channel {} with state {} ({})", channelId, (state == null) ? "null" : state.toString(), + (value == null) ? "null" : value.getClass().getSimpleName()); + + // Update the channel + if (state != null) { + updateState(channelId, state); + } + } + } + + /** + * Get new data from aqicn.org service + * + * @return {AirQualityJsonResponse} + */ + private AirQualityJsonResponse updateAirQualityData() { + AirQualityConfiguration config = getConfigAs(AirQualityConfiguration.class); + return getAirQualityData(StringUtils.trimToEmpty(config.location), config.stationId); + } + + /** + * Request new air quality data to the aqicn.org service + * + * @param location geo-coordinates from config + * @param stationId station ID from config + * + * @return the air quality data object mapping the JSON response or null in case of error + */ + private AirQualityJsonResponse getAirQualityData(String location, Integer stationId) { + AirQualityJsonResponse result = null; + boolean resultOk = false; + String errorMsg = null; + + try { + + // Build a valid URL for the aqicn.org service + AirQualityConfiguration config = getConfigAs(AirQualityConfiguration.class); + + String geoStr = "geo:"; + geoStr += location.replace(" ", "").replace(",", ";").replace("\"", "").replace("'", "").trim(); + + String urlStr = URL.replace("%apikey%", StringUtils.trimToEmpty(config.apikey)); + + if (stationId == null) { + urlStr = urlStr.replace("%QUERY%", geoStr); + } else { + urlStr = urlStr.replace("%QUERY%", "@" + stationId); + } + + logger.debug("URL = {}", urlStr); + + // Run the HTTP request and get the JSON response from aqicn.org + URL url = new URL(urlStr); + URLConnection connection = url.openConnection(); + + try { + String response = IOUtils.toString(connection.getInputStream()); + logger.debug("aqiResponse = {}", response); + + // Map the JSON response to an object + result = gson.fromJson(response, AirQualityJsonResponse.class); + } finally { + IOUtils.closeQuietly(connection.getInputStream()); + } + + if (result == null) { + errorMsg = "no data returned"; + } else if (result.getData() != null && result.getStatus() != "error") { + resultOk = true; + } else { + errorMsg = "missing data sub-object"; + } + + if (!resultOk) { + logger.warn("Error in aqicn.org (Air Quality) response: {}", errorMsg); + } + + } catch (MalformedURLException e) { + errorMsg = e.getMessage(); + logger.warn("Constructed url is not valid: {}", errorMsg); + } catch (JsonSyntaxException e) { + errorMsg = "Configuration is incorrect"; + logger.warn("Error running aqicn.org (Air Quality) request: {}", errorMsg); + } catch (IOException | IllegalStateException e) { + errorMsg = e.getMessage(); + } + + // Update the thing status + if (resultOk && result != null) { + String attributions = result.getData().getAttributions(); + updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, attributions); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, errorMsg); + } + + return resultOk ? result : null; + } + + public static Object getValue(String channelId, AirQualityJsonResponse data) throws Exception { + String[] fields = StringUtils.split(channelId, "#"); + + if (data == null) { + return null; + } + + String fieldName = fields[0]; + + switch (fieldName) { + case AirQualityBindingConstants.AQI: + return data.getData().getAqi(); + case AirQualityBindingConstants.AQIDESCRIPTION: + return data.getData().getAqiDescription(); + case AirQualityBindingConstants.PM25: + return data.getData().getIaqi().getPm25(); + case AirQualityBindingConstants.PM10: + return data.getData().getIaqi().getPm10(); + case AirQualityBindingConstants.O3: + return data.getData().getIaqi().getO3(); + case AirQualityBindingConstants.NO2: + return data.getData().getIaqi().getNo2(); + case AirQualityBindingConstants.CO: + return data.getData().getIaqi().getCo(); + case AirQualityBindingConstants.LOCATIONNAME: + return data.getData().getCity().getName(); + case AirQualityBindingConstants.STATIONID: + return data.getData().getStationId(); + case AirQualityBindingConstants.STATIONLOCATION: + return data.getData().getCity().getGeo(); + case AirQualityBindingConstants.OBSERVATIONTIME: + return data.getData().getTime().getDateString(); + case AirQualityBindingConstants.TEMPERATURE: + return data.getData().getIaqi().getT(); + case AirQualityBindingConstants.PRESSURE: + return data.getData().getIaqi().getP(); + case AirQualityBindingConstants.HUMIDITY: + return data.getData().getIaqi().getH(); + } + + return null; + } + +} diff --git a/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/AirQualityConfiguration.java b/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/AirQualityConfiguration.java new file mode 100644 index 0000000000000..4d20cfdacb20c --- /dev/null +++ b/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/AirQualityConfiguration.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.airquality.internal; + +/** + * The {@link AirQualityConfiguration} is the class used to match the + * thing configuration. + * + * @author Kuba Wolanin - Initial contribution + */ +public class AirQualityConfiguration { + + public String apikey; + + public String location; + + public Integer stationId; + + public Integer refresh; + +} \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/AirQualityHandlerFactory.java b/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/AirQualityHandlerFactory.java new file mode 100644 index 0000000000000..e6b2c5866ecf7 --- /dev/null +++ b/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/AirQualityHandlerFactory.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.airquality.internal; + +import static org.openhab.binding.airquality.AirQualityBindingConstants.THING_TYPE_AQI; + +import java.util.Collections; +import java.util.Set; + +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.openhab.binding.airquality.handler.AirQualityHandler; + +/** + * The {@link AirQualityHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Kuba Wolanin - Initial contribution + */ +public class AirQualityHandlerFactory extends BaseThingHandlerFactory { + + private final static Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_AQI); + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected ThingHandler createHandler(Thing thing) { + + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (thingTypeUID.equals(THING_TYPE_AQI)) { + return new AirQualityHandler(thing); + } + + return null; + } + +} diff --git a/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/json/AirQualityJsonCity.java b/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/json/AirQualityJsonCity.java new file mode 100644 index 0000000000000..fed3657d01bf2 --- /dev/null +++ b/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/json/AirQualityJsonCity.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.airquality.internal.json; + +import java.util.ArrayList; +import java.util.List; + +/** + * The {@link AirQualityJsonCity} is responsible for storing + * the "city" node from the waqi.org JSON response + * + * @author Kuba Wolanin - Initial contribution + */ +public class AirQualityJsonCity { + + private String name; + private String url; + private List geo; + + public AirQualityJsonCity() { + + } + + public String getName() { + return name; + } + + public String getUrl() { + return url; + } + + public String getGeo() { + List list = new ArrayList(); + for (int i = 0; i < geo.size(); i++) { + list.add(geo.get(i).toString()); + } + return String.join(",", list); + } + +} diff --git a/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/json/AirQualityJsonData.java b/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/json/AirQualityJsonData.java new file mode 100644 index 0000000000000..7ec0b3c17185f --- /dev/null +++ b/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/json/AirQualityJsonData.java @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.airquality.internal.json; + +import java.util.ArrayList; +import java.util.List; + +/** + * The {@link AirQualityJsonData} is responsible for storing + * the "data" node from the waqi.org JSON response + * + * @author Kuba Wolanin - Initial contribution + */ +public class AirQualityJsonData { + + private int aqi; + private int idx; + + private final static String GOOD = "GOOD"; + private final static String MODERATE = "MODERATE"; + private final static String UNHEALTHY_FOR_SENSITIVE = "UNHEALTHY_FOR_SENSITIVE"; + private final static String UNHEALTHY = "UNHEALTHY"; + private final static String VERY_UNHEALTHY = "VERY_UNHEALTHY"; + private final static String HAZARDOUS = "HAZARDOUS"; + private final static String NO_DATA = "NO_DATA"; + + private AirQualityJsonTime time; + private AirQualityJsonCity city; + private List attributions; + private AirQualityJsonIaqi iaqi; + + public AirQualityJsonData() { + } + + /** + * Air Quality Index + * + * @return {Integer} + */ + public int getAqi() { + return aqi; + } + + /** + * Measuring Station ID + * + * @return {Integer} + */ + public int getStationId() { + return idx; + } + + /** + * Receives "time" node from the "data" object in JSON response + * + * @return {AirQualityJsonTime} + */ + public AirQualityJsonTime getTime() { + return time; + } + + /** + * Receives "city" node from the "data" object in JSON response + * + * @return {AirQualityJsonCity} + */ + public AirQualityJsonCity getCity() { + return city; + } + + /** + * Collects a list of attributions (vendors making data available) + * and transforms it into readable string. + * Currently displayed in Thing Status description when ONLINE + * + * @return {String} + */ + public String getAttributions() { + List list = new ArrayList(); + for (int i = 0; i < attributions.size(); i++) { + list.add(attributions.get(i).getName()); + } + return "Attributions: " + String.join(", ", list); + } + + /** + * Receives "iaqi" node from the "data" object in JSON response + * + * @return {AirQualityJsonIaqi} + */ + public AirQualityJsonIaqi getIaqi() { + return iaqi; + } + + /** + * Interprets the current aqi value within the ranges; + * Returns AQI in a human readable format + * + * @return + */ + public String getAqiDescription() { + if (aqi > 0 && aqi <= 50) { + return GOOD; + } else if (aqi >= 51 && aqi <= 100) { + return MODERATE; + } else if (aqi >= 101 && aqi <= 150) { + return UNHEALTHY_FOR_SENSITIVE; + } else if (aqi >= 151 && aqi <= 200) { + return UNHEALTHY; + } else if (aqi >= 201 && aqi < 300) { + return VERY_UNHEALTHY; + } else if (aqi >= 300) { + return HAZARDOUS; + } + + return NO_DATA; + } + +} diff --git a/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/json/AirQualityJsonIaqi.java b/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/json/AirQualityJsonIaqi.java new file mode 100644 index 0000000000000..0ba713afb4505 --- /dev/null +++ b/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/json/AirQualityJsonIaqi.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.airquality.internal.json; + +import java.math.BigDecimal; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link AirQualityJsonIaqi} is responsible for storing + * "iaqi" node from the waqi.org JSON response + * It contains information on air pollution particles + * as well as some basic weather metrics. + * + * @author Kuba Wolanin - Initial contribution + * @author Łukasz Dywicki + */ +public class AirQualityJsonIaqi { + + private AirQualityValue pm25; + private AirQualityValue pm10; + private AirQualityValue o3; + private AirQualityValue no2; + private AirQualityValue co; + private AirQualityValue t; + + @SerializedName("p") + private AirQualityValue pressure; + private AirQualityValue h; + + public AirQualityJsonIaqi() { + + } + + public BigDecimal getPm25() { + return pm25.getValue(); + } + + public BigDecimal getPm10() { + return pm10.getValue(); + } + + public BigDecimal getO3() { + return o3.getValue(); + } + + public BigDecimal getNo2() { + return no2.getValue(); + } + + public BigDecimal getCo() { + return co.getValue(); + } + + public BigDecimal getT() { + return t.getValue(); + } + + public BigDecimal getP() { + return pressure.getValue(); + } + + public BigDecimal getH() { + return h.getValue(); + } + +} diff --git a/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/json/AirQualityJsonResponse.java b/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/json/AirQualityJsonResponse.java new file mode 100644 index 0000000000000..947d23dc26309 --- /dev/null +++ b/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/json/AirQualityJsonResponse.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.airquality.internal.json; + +/** + * The {@link AirQualityJsonResponse} is the Java class used to map the JSON + * response to the aqicn.org request. + * + * @author Kuba Wolanin - Initial contribution + */ +public class AirQualityJsonResponse { + + private String status; + + private AirQualityJsonData data; + + public AirQualityJsonResponse() { + } + + public String getStatus() { + return status; + } + + public AirQualityJsonData getData() { + return data; + } + +} diff --git a/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/json/AirQualityJsonTime.java b/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/json/AirQualityJsonTime.java new file mode 100644 index 0000000000000..4eea94a7dde9e --- /dev/null +++ b/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/json/AirQualityJsonTime.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.airquality.internal.json; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link AirQualityJsonTime} is responsible for storing + * the "time" node from the waqi.org JSON response + * + * @author Kuba Wolanin - Initial contribution + */ +public class AirQualityJsonTime { + + @SerializedName("s") + private String dateString; + + @SerializedName("tz") + private String timeZone; + + private final static SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ssZ"); + + /** + * Get Time zone from the JSON Response + * in following format: "+0100" + * + * @return {String} + */ + public String getTimeZone() { + return timeZone; + } + + /** + * Get observation time + * + * @return {Calendar} + * @throws Exception + */ + public Calendar getDateString() throws Exception { + Calendar calendar = Calendar.getInstance(); + Date date = SDF.parse(dateString + timeZone.replace(":", "")); + calendar.setTime(date); + return calendar; + } + +} diff --git a/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/json/AirQualityValue.java b/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/json/AirQualityValue.java new file mode 100644 index 0000000000000..5c71dfb213827 --- /dev/null +++ b/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/json/AirQualityValue.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.airquality.internal.json; + +import com.google.gson.annotations.SerializedName; + +/** + * Wrapper type around values reported by aqicn index values. + * + * @author Łukasz Dywicki + */ +public class AirQualityValue { + + @SerializedName("v") + private T value; + + public void setValue(T value) { + this.value = value; + } + + public T getValue() { + return value; + } +} diff --git a/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/json/Attribute.java b/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/json/Attribute.java new file mode 100644 index 0000000000000..80d26844e09ad --- /dev/null +++ b/addons/binding/org.openhab.binding.airquality/src/main/java/org/openhab/binding/airquality/internal/json/Attribute.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.airquality.internal.json; + +/** + * Attribute representation. + * + * @author Łukasz Dywicki + */ +public class Attribute { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/addons/binding/pom.xml b/addons/binding/pom.xml index fb60e32d698fc..4e984997dc1b1 100644 --- a/addons/binding/pom.xml +++ b/addons/binding/pom.xml @@ -16,6 +16,7 @@ pom + org.openhab.binding.airquality org.openhab.binding.allplay org.openhab.binding.amazondashbutton org.openhab.binding.astro diff --git a/features/openhab-addons/src/main/feature/feature.xml b/features/openhab-addons/src/main/feature/feature.xml index f42fac05b0db0..ff6c437ea64bb 100644 --- a/features/openhab-addons/src/main/feature/feature.xml +++ b/features/openhab-addons/src/main/feature/feature.xml @@ -13,6 +13,11 @@ + + openhab-runtime-base + mvn:org.openhab.binding/org.openhab.binding.airquality/${project.version} + + openhab-runtime-base mvn:org.openhab.binding/org.openhab.binding.allplay/${project.version}