Skip to content
This repository has been archived by the owner on Feb 19, 2021. It is now read-only.

Updated Code to Pull PM10 Data #1

Open
edfrahm opened this issue Sep 27, 2020 · 1 comment
Open

Updated Code to Pull PM10 Data #1

edfrahm opened this issue Sep 27, 2020 · 1 comment
Assignees

Comments

@edfrahm
Copy link

edfrahm commented Sep 27, 2020

James-

Thanks so much for building this DTH. I'm new to SmartThings and deployed this to supplement an in-home sensor I have that only monitors PM2.5 and VOCs. I wanted to pull AirNow data to track PM10 and turn my HVAC fan on under certain conditions using WebCore.

The code you deployed didn't report PM10, but I noticed it was a part of the AirNow API. I'm not a coder, but I copied your syntax and changed the 2.5 references to 10. It appears to be working, but I'd appreciate your more skilled eyes to check it over. Here's the updated code:

/**

*/
metadata {
definition (name: "AirNow Virtual Sensor", namespace: "jschlackman", author: "[email protected]") {
capability "Sensor"
capability "Polling"

	attribute "combined", "number" // Combined AQI value (worst of either Ozone or PM2.5)
	attribute "combinedCategory", "number" // Combined AQI category number
	attribute "combinedCategoryName", "string" // Combined AQI category name
	attribute "O3", "number" // Ozone AQI value
	attribute "O3Category", "number" // Ozone AQI category number
	attribute "O3CategoryName", "string" // Ozone AQI category name
	attribute "Pm25", "number" // PM2.5 AQI value
	attribute "Pm25Category", "number" // PM2.5 AQI category number
	attribute "Pm25CategoryName", "string" // PM2.5 AQI category name
    attribute "Pm10", "number" // PM2.5 AQI value
	attribute "Pm10Category", "number" // PM2.5 AQI category number
	attribute "Pm10CategoryName", "string" // PM2.5 AQI category name
	attribute "reportingLocation", "string" // City or area name of observed data, with 2-letter state code. Also used to display errors in the mobile app UI.
	attribute "dateObserved", "string" // Date of observation (yyyy-mm-dd)
	attribute "hourObserved", "number" // Hour of observation (00-23)
	attribute "latitude", "number" // Latitude of observation area in decimal degrees.
	attribute "longitude", "number" // Longitude of observation area in decimal degrees.

	command "refresh"
}

preferences {
	input name: "zipCode", type: "text", title: "Zip Code (optional)", required: false
	input name: "airNowKey", type: "text", title: "AirNow API Key", required: true, description: "Register at airnowapi.org"
	input name: "distance", type: "number", title: "Max. Observation Distance (miles)", required: false, description: "Default: 25 miles."
	input name: "about", type: "paragraph", element: "paragraph", title: "AirNow Virtual Sensor 1.1", description: "By James Schlackman <[email protected]>"
}

tiles(scale: 2) {
	
	// Combined AQI number tile, used only for Things view
	standardTile("mainTile", "device.combined", width: 2, height: 2, decoration: "flat", canChangeIcon: true) {
		state "default", label:'${currentValue}', icon: "st.Outdoor.outdoor25", // Defaults to tree icon from ST Outdoor category
			backgroundColors:[
				[value: 25, color: "#44b621"],
				[value: 60, color: "#f1d801"],
				[value: 110, color: "#d04e00"],
				[value: 165, color: "#bc2323"],
				[value: 220, color: "#693591"],
				[value: 320, color: "#7e0023"],
			]
		}

	// Ozone AQI category
	standardTile("O3CategoryName", "device.O3CategoryName", width: 4, height: 2, decoration: "flat") {
		state "default", label:'Ozone Level: ${currentValue}'
	}
	
	// Ozone AQI value
	valueTile("O3", "device.O3", width: 2, height: 2) {
		state "default", label:'${currentValue}',
			backgroundColors:[
				[value: 25, color: "#44b621"],
				[value: 60, color: "#f1d801"],
				[value: 110, color: "#d04e00"],
				[value: 165, color: "#bc2323"],
				[value: 220, color: "#693591"],
				[value: 320, color: "#7e0023"],
			]
	}

	// PM2.5 AQI category
	standardTile("Pm25CategoryName", "device.Pm25CategoryName", width: 4, height: 2, decoration: "flat") {
		state "default", label:'PM₂.₅ Level: ${currentValue}'
	}
	
	// PM2.5 AQI value
	valueTile("Pm25", "device.Pm25", width: 2, height: 2) {
		state "default", label:'${currentValue}',
			backgroundColors:[
				[value: 25, color: "#44b621"],
				[value: 60, color: "#f1d801"],
				[value: 110, color: "#d04e00"],
				[value: 165, color: "#bc2323"],
				[value: 220, color: "#693591"],
				[value: 320, color: "#7e0023"],
			]
	}
	// PM10 AQI category
	standardTile("Pm10CategoryName", "device.Pm10CategoryName", width: 4, height: 2, decoration: "flat") {
		state "default", label:'PM10 Level: ${currentValue}'
	}
	
	// PM10 AQI value
	valueTile("Pm10", "device.Pm10", width: 2, height: 2) {
		state "default", label:'${currentValue}',
			backgroundColors:[
				[value: 25, color: "#44b621"],
				[value: 60, color: "#f1d801"],
				[value: 110, color: "#d04e00"],
				[value: 165, color: "#bc2323"],
				[value: 220, color: "#693591"],
				[value: 320, color: "#7e0023"],
			]
	}
	// Observation location
	standardTile("reportingArea", "device.reportingLocation", width: 4, height: 2, decoration: "flat") {
		state "default", label:'Observation Location: ${currentValue}'
	}

	// Refresh button
	standardTile("refresh", "device.combined", width: 2, height: 2, decoration: "flat") {
		state "default", label: "", action: "refresh", icon:"st.secondary.refresh"
	}

	// Tile layout for main listing and details screen
	main("mainTile")
	details("aqi", "O3CategoryName", "O3", "Pm25CategoryName", "Pm25", "PM10CategoryName", "PM10", "reportingArea", "refresh")
}

}

// Parse events into attributes. This will never be called but needs to be present in the DTH code.
def parse(String description) {
log.debug("AirNow: Parsing '${description}'")
}

def installed() {
runEvery1Hour(poll)
poll()
}

def updated() {
poll()
}

def uninstalled() {
unschedule()
}

// handle commands
def poll() {
log.debug("Polling AirNow for air quality data, location: ${location.name}")

if(airNowKey) {
	
	def airZip = null
	def airDistance = 0

	// Use hub zipcode if user has not defined their own
	if(zipCode) {
		airZip = zipCode
	} else {
		airZip = location.zipCode
	}
	
	// Set the user's requested observation distance, or use a default of 25 miles
	if(distance) {
		airDistance = distance
	} else {
		airDistance = 25
	}

	// Set up the AirNow API query
	def params = [
		uri: 'http://www.airnowapi.org/aq/',
		path: 'observation/zipCode/current/',
		contentType: 'application/json',
		query: [format:'application/json', zipCode: airZip, distance: airDistance, API_KEY: airNowKey]
	]

	try {
	// Send query to the AirNow API
		httpGet(params) {resp ->
			def newCombined = -1
			def newCombinedCategory = -1
			def newCombinedCategoryName = ''

			// Parse the observation data array
			resp.data.each {observation ->
				
				// Only parse this data if the AQI figure is in a sensible range (accounts for occasional API bugs)
				if ((observation.AQI >= 0) && (observation.AQI <= 2000)) {
				
					if (observation.ParameterName == "O3") {
						send(name: "O3", value: observation.AQI)
						send(name: "O3Category", value: observation.Category.Number)
						send(name: "O3CategoryName", value: observation.Category.Name)
					}
					else if (observation.ParameterName == "PM2.5") {
						send(name: "Pm25", value: observation.AQI)
						send(name: "Pm25Category", value: observation.Category.Number)
						send(name: "Pm25CategoryName", value: observation.Category.Name)
					}	
					else if (observation.ParameterName == "PM10") {
						send(name: "Pm10", value: observation.AQI)
						send(name: "Pm10Category", value: observation.Category.Number)
						send(name: "Pm10CategoryName", value: observation.Category.Name)
					}	
                    
					// Check if the observation currently being parsed has the highest AQI and should therefore be the combined AQI
					if ((observation.AQI > newCombined) || (observation.Category.Number > newCombinedCategory)) {
						newCombined = observation.AQI
						newCombinedCategory = observation.Category.Number
						newCombinedCategoryName = observation.Category.Name
					}
				} else {
					log.error("AirNow returned an AQI of ${observation.AQI} for ${observation.ParameterName}. Ignoring as this is probably invalid.")
				}
			}
			
			// If we got valid data for at least one observation, send the combined AQI and reporting data
			if (newCombined > -1) {
			
				// Send the combined AQI
				send(name: "combined", value: newCombined)
				send(name: "combinedCategory", value: newCombinedCategory)
				send(name: "combinedCategoryName", value: newCombinedCategoryName)

				// Send the first reporting area (it will be the same for both observations)
				send(name: "reportingLocation", value: "${resp.data[0].ReportingArea} ${resp.data[0].StateCode}")
				send(name: "latitude", value: resp.data[0].Latitude)
				send(name: "longitude", value: resp.data[0].Longitude)
				send(name: "dateObserved", value: resp.data[0].DateObserved)
				send(name: "hourObserved", value: resp.data[0].HourObserved)

				log.debug("Successfully retrieved air quality data from AirNow.")
			} else {
				log.debug("Failed to retrieve valid air quality data from AirNow.")
			}
		}


	}
	catch (SocketTimeoutException e) {
		log.error("Connection to AirNow API timed out.")
		send(name: "reportingLocation", value: "Connection timed out while retrieving data from AirNow")
	}
	catch (e) {
		log.error("Could not retrieve AirNow data: $e")
		send(name: "reportingLocation", value: "Could not retrieve data: check API key in device settings")
	}

}

else {
	log.warn "No AirNow API key specified."
	send(name: "reportingLocation", value: "No AirNow API key specified in device settings")
}

}

def refresh() {
poll()
}

def configure() {
poll()
}

private send(map) {
//log.debug "AirNow: event: $map"
sendEvent(map)
}

@jschlackman jschlackman self-assigned this Oct 11, 2020
@jschlackman
Copy link
Owner

jschlackman commented Oct 11, 2020

@edfrahm, you were pretty much spot on here, the only changes I've made from your suggestions were to update some comments and use subscript for PM₁₀

The only reason I missed this originally is that PM₁₀ data isn't published in my region, so I never saw example data for it. I've committed v1.2, so please go ahead and test. Unfortunately it will only be visible in the SmartThings Classic app which is being sunset very soon, but data should still be visible in WebCore until SmartThings drop support for custom DTHs as well.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

2 participants