[RELEASE] US AQI from Open Weather with coversions

I have greatly improved on @Byrin 's OWM Air quality driver.

OWM returns information in EU format (mg/m^3) and only has the EU AQI value.

It now returns the calculated (to the best of my ability) US AQI number as well as the EU AQI (Which is provided by OWM) and detailed information in US standard units.

Please credit Byrin and myself if you modify.

/*
	OpenWeatherMap-Air Quality - Detailed
    Modified from Byrin's original

0.1.5 - Added user editable lat/long, user editable tile width
0.1.4 - Added AQI tile
0.1.3 - Changed the output. Now default output is AirQuality in US format. 2022-10-04
0.1.2 - Fixed "then" statement. (I'm getting too old for this.") 2022-10-03
0.1.1 - Changed Hex value returned to have # in front. Easier to assign to a light. 2022-10-01
*/
static String version()	{  return '0.1.5'  }
import groovy.transform.Field


metadata {
    definition (name: "OpenWeatherMap-Air Quality", namespace: "AC7SS", author: "SJ") {
    	capability "AirQuality"
        attribute "CO", "float"
        attribute "NO", "float"
        attribute "NO2", "float"
        attribute "O3", "float"
        attribute "SO2", "float"
        attribute "PM2.5", "float"
        attribute "PM10", "float"
        attribute "NH3", "float"
        // attribute "AQI", "int"
        attribute "AQIColorCode", "string"
        attribute "PrimaryFactor", "string"
        attribute "AlertTile", "string"

        command 'pollData'
	} 
    preferences {
        input 'apiKey', 'text', required: true, title: 'Type OpenWeatherMap.org API Key Here', defaultValue: null
        input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true
        input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true
        input 'locLat', 'text', title: 'Location Latitude', defaultValue: location.latitude
        input 'locLon', 'text', title: 'Location Longitude', defaultValue: location.longitude
        input 'tileWidth', 'int', title: 'Tile width', defaultValue: '125'
    }
}

void pollAQI() {
	if( apiKey == null ) {
		return
	}
	Map ParamsAQI
	ParamsAQI = [ 
        uri: 'https://api.openweathermap.org/data/2.5/air_pollution?lat=' + (String)locLat + '&lon=' + (String)locLon + '&appid=' + (String)apiKey, 
        timeout: 20 ]
    if (logEnable) log.debug "ParamsAQI:${ParamsAQI}"
	asynchttpGet('pollAQIHandler', ParamsAQI)
}

void pollAQIHandler(resp, data) {
	if(resp.getStatus() == 200 || resp.getStatus() == 207) {
		Map aqi = parseJson(resp.data)
		if(aqi.toString()==sNULL) {
			pauseExecution(30000) //5 minute pause
			pollAQI()
			return
		}
		def name = 'airQualityIndex'
		def value = aqi.list[0].main.aqi
        
		def descriptionText = "${device.displayName} ${name} is ${value}"
//		if (txtEnable) log.info "${descriptionText}"
//		sendEvent(name: name,value: value,descriptionText: descriptionText,unit: unit)
        
        // Here begins my modification (SJ)
        def int aQItemp =0
        def int aQI = 0
        def aQIfactor = "none"
        name = 'CO'
        value = aqi.list[0].components.co
		def convertedVal = Math.round(value/114.5)/10
        if (convertedVal<4.5){
            aQItemp = (50/4.4)*(convertedVal)
        }else if (convertedVal<9.5){
            aQItemp = (49/4.9)*(convertedVal-4.5)+51
        }else if (convertedVal<12.5){
            aQItemp = (49/2.9)*(convertedVal-9.5)+101
        }else if (convertedVal<15.5){
            aQItemp = (49/2.9)*(convertedVal-12.5)+151
        }else if (convertedVal<30.5){
            aQItemp = (99/14.9)*(convertedVal-15.5)+201
        }else if (convertedVal<40.5){
            aQItemp = (99/9.9)*(convertedVal-30.5)+301
        }else {
            aQItemp = (99/9.9)*(convertedVal-40.5)+401
        }
        aQI=aQItemp
        aQIfactor = name
        
        descriptionText = "${device.displayName} ${name} is ${convertedVal} AQI ${aQItemp}"
		if (txtEnable) log.info "${descriptionText}"
		sendEvent(name: name,value: convertedVal,descriptionText: descriptionText,unit: "ppm")
        
        name = 'NO2'
		value = aqi.list[0].components.no2
		convertedVal = Math.round(value/1.88)
        if (convertedVal<54){
            aQItemp = (50/53)*(convertedVal)
        }else if (convertedVal<101){
            aQItemp = (49/46)*(convertedVal-54)+51
        }else if (convertedVal<361){
            aQItemp = (49/259)*(convertedVal-101)+101
        }else if (convertedVal<650){
            aQItemp = (49/288)*(convertedVal-361)+151
        }else if (convertedVal<1250){
            aQItemp = (99/599)*(convertedVal-650)+201
        }else if (convertedVal<1650){
            aQItemp = (99/399)*(convertedVal-1250)+301
        }else {
            aQItemp = (99/399)*(convertedVal-1650)+401
        }
        if (aQItemp>aQI){
            aQI=aQItemp
            aQIfactor = name
        }
        descriptionText = "${device.displayName} ${name} is ${convertedVal} AQI ${aQItemp}"
		if (txtEnable) log.info "${descriptionText}"
		sendEvent(name: name,value: convertedVal,descriptionText: descriptionText,unit: "ppb")
        
        name = 'O3'
		value = aqi.list[0].components.o3
		convertedVal = Math.round(value/2)/1000
        if (convertedVal<0.055){
            aQItemp = (50/0.054)*(convertedVal)
        }else if (convertedVal<0.071){
            aQItemp = (49/0.15)*(convertedVal-0.054)+51
        }else if (convertedVal<0.165){
            aQItemp = (49/0.093)*(convertedVal-0.071)+101
        }else if (convertedVal<0.205){
            aQItemp = (49/0.039)*(convertedVal-0.165)+151
        }else if (convertedVal<0.405){
            aQItemp = (99/0.199)*(convertedVal-0.205)+201
        }else if (convertedVal<0.505){
            aQItemp = (99/0.099)*(convertedVal-0.405)+301
        }else {
            aQItemp = (99/0.099)*(convertedVal-0.505)+401
        }
        if (aQItemp>aQI){
            aQI=aQItemp
            aQIfactor = name
        }
        descriptionText = "${device.displayName} ${name} is ${convertedVal} AQI ${aQItemp}"
		if (txtEnable) log.info "${descriptionText}"
		sendEvent(name: name,value: convertedVal,descriptionText: descriptionText,unit: "ppm")
        
        name = 'SO2'
		value = aqi.list[0].components.so2
		convertedVal = Math.round(value/2.62)
        if (convertedVal<36){
            aQItemp = (50/35)*(convertedVal)
        }else if (convertedVal<76){
            aQItemp = (49/39)*(convertedVal-36)+51
        }else if (convertedVal<186){
            aQItemp = (49/109)*(convertedVal-76)+101
        }else if (convertedVal<305){
            aQItemp = (49/118)*(convertedVal-186)+151
        }else if (convertedVal<605){
            aQItemp = (99/299)*(convertedVal-305)+201
        }else if (convertedVal<805){
            aQItemp = (99/199)*(convertedVal-605)+301
        }else {
            aQItemp = (99/199)*(convertedVal-805)+401
        }
        if (aQItemp>aQI){
            aQI=aQItemp
            aQIfactor = name
        }
        descriptionText = "${device.displayName} ${name} is ${convertedVal} AQI ${aQItemp}"
		if (txtEnable) log.info "${descriptionText}"
		sendEvent(name: name,value: convertedVal,descriptionText: descriptionText,unit: "ppb")
        
        name = 'PM2.5'
		value = Math.round(aqi.list[0].components.pm2_5*10)/10
		convertedVal = value
        if (convertedVal<12.1){
            aQItemp = (50/12.0)*(convertedVal)
        }else if (convertedVal<35.5){
            aQItemp = (49/23.3)*(convertedVal-12.1)+51
        }else if (convertedVal<55.5){
            aQItemp = (49/19.9)*(convertedVal-35.5)+101
        }else if (convertedVal<150.5){
            aQItemp = (49/94.9)*(convertedVal-55.5)+151
        }else if (convertedVal<250.5){
            aQItemp = (99/99.9)*(convertedVal-150.5)+201
        }else if (convertedVal<350.5){
            aQItemp = (99/99.9)*(convertedVal-250.5)+301
        }else {
            aQItemp = (99/149.9)*(convertedVal-350.5)+401
        }
        if (aQItemp>aQI){
            aQI=aQItemp
            aQIfactor = name
        }
        descriptionText = "${device.displayName} ${name} is ${convertedVal} AQI ${aQItemp}"
        if (txtEnable) log.info "${descriptionText}"
		sendEvent(name: name,value: value,descriptionText: descriptionText,unit: "mg/m^3")
        
        name = 'PM10'
		value = Math.round(aqi.list[0].components.pm10)
		convertedVal = value
        if (convertedVal<55){
            aQItemp = (50/54)*(convertedVal)
        }else if (convertedVal<155){
            aQItemp = (49/99)*(convertedVal-55)+51
        }else if (convertedVal<255){
            aQItemp = (49/99)*(convertedVal-155)+101
        }else if (convertedVal<355){
            aQItemp = (49/99)*(convertedVal-255)+151
        }else if (convertedVal<425){
            aQItemp = (99/69)*(convertedVal-355)+201
        }else if (convertedVal<505){
            aQItemp = (99/79)*(convertedVal-425)+301
        }else {
            aQItemp = (99/99)*(convertedVal-505)+401
        }
        if (aQItemp>aQI){
            aQI=aQItemp
            aQIfactor = name
        }
        descriptionText = "${device.displayName} ${name} is ${convertedVal} AQI ${aQItemp}"
        if (txtEnable) log.info "${descriptionText}"
		sendEvent(name: name,value: value,descriptionText: descriptionText,unit: "mg/m^3")
        
        name = 'airQualityIndex'
//		def value = aqi.list[0].main.aqi
        
//		def descriptionText = "${device.displayName} ${name} is ${value}"
//		if (txtEnable) log.info "${descriptionText}"
//		sendEvent(name: name,value: value,descriptionText: descriptionText,unit: unit)
//		name = 'AQI'
        descriptionText = "${device.displayName} ${name} is ${aQI} Condition is ${aQIfactor}"
		if (txtEnable) log.info "${descriptionText}"
        sendEvent(name: name,value: aQI,descriptionText: 'US AQI',unit: aQIfactor)
        
        name = 'PrimaryFactor'
        descriptionText = "${device.displayName} ${name} is ${aQIfactor}"
		if (txtEnable) log.info "${descriptionText}"
        sendEvent(name: name,value: aQIfactor,descriptionText: 'AQI Factor',unit: unit)
        

        
        
        name = 'AQIColorCode'
        def aQIcolor = "#7E0023"
//        def aQIText = ""
        
        if (aQI>1&&aQI<51) {
            aQIcolor = '#00E400'
        }else if (aQI<101&&aQI>50) {
            aQIcolor = '#FFFF00'
        }else if (aQI<151&&aQI>100) {
            aQIcolor = '#FF7E00'
        }else if (aQI<201&&aQI>150) {
            aQIcolor = '#FF0000'
        }else if (aQI<301&&aQI>200) {
            aQIcolor = '#8F3F97'
        }
        descriptionText = "${device.displayName} ${name} is ${aQIcolor}"
		if (txtEnable) log.info "${descriptionText}"
        sendEvent(name: 'AQIColorCode',value: aQIcolor ,descriptionText: 'US AQI Color Code',unit: "RGB")
        
        
        // th{padding: 10px;}
        String aTile
        aTile = '<style>h3 {text-align: center;font-size:250%;color:Black;}p {text-align: center;font-size:100%;color:Black;}</style><table width = '
        aTile += tileWidth
        aTile +=' height = 70 align = "center" style="background-color:'
        aTile += aQIcolor
        aTile += '"><tr><th><h3>'
        aTile += aQI
        aTile += '</th></tr><tr><td><p>'
        aTile += aQIfactor
        aTile += '</td></tr></table>' 
        
        descriptionText = "${device.displayName} Tile is ${aTile}"
		if (txtEnable) log.info "${descriptionText}"

        sendEvent(name: 'AlertTile', value: aTile )
	}
}

void refresh() {
}

void installed() {
    schedule("0 0/30 * 1/1 * ? *", pollAQI) //every half hour
}

void uninstalled() {
    unschedule()
}

void pollData() {
	pollAQI()
}

It's a little late for smoke season, but in time for next year!

5 Likes

Fixed a small error. ("then" statement. doesn't exist here.) 0.1.2

Default reporting is now US AQI as stated in the documentation, and returning the primary factor. 0.1.3

Next up is returning a tile, if I can find out how that is done.
I am intending on a color block based on the warning level (The color hex returned) with a prominent AQI number above the primary pollutant.

0.1.5 - Added user editable lat/long, user editable tile width
0.1.4 - Added AQI tile

Now with configurable locations and working tile!

I've installed this driver and have created a device named AQI.
As I'll use this as a tile on a SharpTools dashboard, I've added capability as a sensor so that SharpTools can find it.

Unfortunately it is not working, and logging errors.

dev:21702022-10-21 09:23:44.900 PMerrororg.codehaus.groovy.runtime.metaclass.MissingMethodExceptionNoStack: No signature of method: user_driver_AC7SS_OpenWeatherMap_Air_Quality_1515.pollData() is applicable for argument types: () values: []
Possible solutions: pollAQI(), collect(), collect(groovy.lang.Closure), collect(java.util.Collection, groovy.lang.Closure) (method pollData)

Do you think the errors are caused by the addition of sensor capability? I won't be able to use it without this capability included.

Thanks!

UPDATE;
It suddenly began to work. No errors being logged. If you fixed it - THANKS!. If you didn't fix it, we have a new episode of Twilight Zone...

UPDATE 2:
It finally dawned on me that I was the one who fixed the problem. Sometime in the day I updated my hub. I doubt the new hub code was responsible for the fix, but rather the reboot that takes place in the update process.

A few tweaks and AQI will be working perfectly on my SharpTools dashboards. Thanks for your work.

1 Like

This is great, and I plan to use this driver. Is it safe to assume that a 'free' account API key on Open Weather will suffice to get the AQI?

Answered by own question. The fact that clicking on "poll data" didn't do anything for a while caught me off guard. This is fantastic, thank you for posting this.

Why should I switch to this when we already have a AirNow.gov driver? It already provides US values. Is there something wrong with what they report?

I know you must first create an account on AirNow.gov and in the 3 years I have done so I have not received any emails or spam from then.

I did not find that one. That's the main reason i didn't use it.

I also wanted the practice of writing my own diver.

1 Like

Now I get it. :grin:

Sorry if my words came off too strong. I started to wonder that airnow.gov was not as accurate. It's good to have more than one. I have recently moved to the wundergroud driver because I was thinking (not scientific) Open Weather temperatures and forecast for me in Columbus Ohio are not accurate or close to what local tv/news reports.

I like the 3-day forecast tile of Open Weather so much I added that feature to the wunderground driver and got a fair amount of practice/learning as well.

1 Like

No worries. It's user generated code that makes HE better. I rarely find projects like this that i can do.

I will look at the AirNow and may adapt it to my needs. I like setting a lamp to the warning level color (one of the outputs on my app was designed for that.) As well as having a compact tile.

I like side uses for hubitat, i need information on forecasts and air quality for work, so i can use things like this for triggers and get notifications on my phone when certain predictions are reached.

1 Like

Which WU driver are you using?
So far, this Open Weather (using Lat/Long) has been more accurate for me vs the same with Airnow (running both drivers right now). But wondering if WU is even more?

I use IQAir on my mobile phone which seems the most accurate as it aggregates purpleair and their own sensors.

AirNow displays the highest reading for it's area. In Seattle, it is most of western King County, whereas OWM is a little more granular. I haven't looked at the API for AirNow, and may move to that next year.

(I have a state requirement to notify personnel if the actual or forecast is above certain levels, I can use this to text me the forecast during smoke season.)

Yeah. I'm using both your excellent driver here as well as this PurpleAir Driver for King County.

I tried this other AirNow driver, but not happy with my local results.

I realize I'm seeing an older thread - still, being an avid OWM user I was hoping to condense a bit. I use AirNow for my required monitoring but I wanted to compare it with what OWM is reporting.
I have a fairly accurate (down to the 10m) lat/long for both centered on my city hall which has a reporting station. it's about 2 blocks from me.
I'm getting a fair discrepancy between AirNow and OWM. One reports 82PPM the other 41ppm. I let this run for an hour to be sure it's not an update thing - and thoughts? AirNow is using 2.5 as I believe OWM is for particulate size. What else could be wrong?

I'm also using OWM and AirNow and seeing the same thing. Sometimes they are very close, and sometimes not so much. I've been wondering why also. (Note: the pink line is inside the house.)

wow. thats a useful graph. can you see delta for like 10 days? 30? how far apart are they? I'm wondering the ecowitt doens't jive at all - whats it measureing? 10? 2.5?