aqi/aqi.el

277 lines
9.5 KiB
EmacsLisp

;;; aqi.el --- Air quality data from the World Air Quality Index -*- lexical-binding: t; -*-
;; Copyright 2020 FoAM
;;
;; Author: nik gaffney <nik@fo.am>
;; Created: 2020-02-02
;; Version: 0.1
;; Package-Requires: ((emacs "25.1") (request "0.3") (let-alist "0.0"))
;; Keywords: air quality, AQI, pollution, weather, data
;; URL: https://github.com/zzkt/aqi
;; This file is not part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; View air quality data from the World Air Quality Index.
;;
;; The simplest way to view AQI info is with 'M-x aqi-report' which
;; displays air quality info for your algorithmically derived location
;; (equivalent to the location "here") or the name of a place. A place
;; can be the name of a city (in which case the nearest monitoring
;; station is used) or the name of specific monitoring station.
;;
;; To use the data programmatically, the functions 'aqi-report-full'
;; and 'aqi-report-brief' return the report as a string. The function
;; 'aqi-city-aqi' will return the AQI for a given city as a number.
;;; Code:
(require 'request)
(require 'let-alist)
(defgroup aqi nil
"Fetch and display air quality data from WAQI."
:prefix "aqi-"
:group 'external)
(defcustom aqi-api-key "demo"
"A valid API key from http://aqicn.org/data-platform/token/ to access WAQI."
:type 'string)
;; Data from WAQI can be cached, since it's usually refreshed hourly.
(defcustom aqi-use-cache nil
"When set to t will use cached data, otherwise get new data on each call."
:type 'boolean)
(defcustom aqi-cache-refresh-period 0
"Cached data can be refreshed at a given interval (in minutes).
Set to nil to never refresh."
:type 'number)
(defvar aqi-cached-data '(("None" . "None"))
"Data is cached as an alist of city names and results.")
(defun aqi--city-cache-clear (&optional city)
"Clear the cached data, optionally only for a given CITY."
(if city
(setq aqi-cached-data
(assq-delete-all city aqi-cached-data))
(setq aqi-cached-data '(("None" . "None")))))
(defun aqi--city-cache-update (city)
"Add or update cached data for a given CITY."
(aqi--city-cache-clear city)
(push (cons city (aqi-request city))
aqi-cached-data))
(defun aqi--city-cache-get (city)
"Add or update cached data for a given CITY."
(unless (aqi--cached-city? city)
(aqi--city-cache-update city))
(assoc-default city aqi-cached-data))
(defun aqi--cached-city? (city)
"Return t if AQI data from CITY has been cached."
(if (assoc-default city aqi-cached-data) t nil))
;; data munging
(defmacro aqi--make-city-raw-accessor (name aref)
"Macro to create accessor NAME with binding `let-alist' AREF (or function)."
`(fset ,name
(lambda (city)
(aqi-request city)
(let-alist (assoc-default city aqi-cached-data) ,aref))))
(defmacro aqi--make-city-format-accessor (name aref)
"Macro to create accessor NAME with binding `let-alist' AREF (or function)."
`(fset ,name
(lambda (city)
(aqi-request city)
(format "%s" (let-alist (assoc-default city aqi-cached-data) ,aref)))))
;; various accessors (added as needed...)
;; Function to return the AQI for a city (by name) as a number
(aqi--make-city-raw-accessor 'aqi-city-aqi .aqi)
;; Function to return the coordinates of a city (by name) as a string
(aqi--make-city-format-accessor 'aqi-city-lonlat
(format "%s, %s" (elt .city.geo 0) (elt .city.geo 1)))
;; API requests for AQI info
(defun aqi-request (city)
"Request details for CITY from WAQI."
(request
(format "https://api.waqi.info/feed/%s/" city)
:sync t
:params `(("token" . ,aqi-api-key))
:parser 'json-read
:success (cl-function
(lambda (&key data &allow-other-keys)
(pcase (assoc-default 'status data)
("ok" (push (cons city (assoc-default 'data data))
aqi-cached-data))
("error" (push (cons city (format "Request error: %s" (assoc-default 'data data)))
aqi-cached-data)))))
:error (cl-function
(lambda (&rest args &key error-thrown &allow-other-keys)
(message "WAQI error: %s" error-thrown)))))
(defun aqi-request-geo (latitude longitude)
"Request details by LATITUDE and LONGITUDE from WAQI."
(request
(format "https://api.waqi.info/feed/geo:%s;%s/" latitude longitude)
:sync t
:params `(("token" . ,aqi-api-key))
:parser 'json-read
:success (cl-function
(lambda (&key data &allow-other-keys)
(message "200: %s" data)))
:error (cl-function
(lambda (&rest args &key error-thrown &allow-other-keys)
(message "WAQI error: %s" error-thrown)))))
(defun aqi-request-cached (city)
"Request details for CITY from cached data or direct from WAQI."
(if (aqi--cached-city? city)
(aqi--city-cache-get city)
(aqi-request city)))
(defun aqi-search (name)
"Search for the nearest stations (if any) matching a given NAME."
(request
(format "https://api.waqi.info/search/?keyword=%s&" name)
:sync t
:params `(("token" . ,aqi-api-key))
:parser 'json-read
:success (cl-function
(lambda (&key data &allow-other-keys)
(pcase (assoc-default 'status data)
("ok" (message "Search: %s" (assoc-default 'data data)))
("error" (message "Search error: %s" (assoc-default 'data data))))))
:error (cl-function
(lambda (&rest args &key error-thrown &allow-other-keys)
(message "WAQI error: %s" error-thrown)))))
;; printing, formatting and presenting.
;;;###autoload
(defun aqi-report-brief (&optional place)
"General air quality info from PLACE as a string."
(let ((city (if (and (string< "" place) place) place "here")))
(if aqi-use-cache
(aqi-request-cached city)
(aqi-request city))
(let-alist (aqi--city-cache-get city)
(format "Air Quality Index in %s is %s and the dominant pollutant is %s%s"
.city.name .aqi .dominentpol
(if aqi-use-cache " (cached)" "")))))
;;;###autoload
(defun aqi-report-full (&optional place)
"Detailed air quality info from PLACE as a string."
(let ((city (if (and (string< "" place) place) place "here")))
(if aqi-use-cache
(aqi-request-cached city)
(aqi-request city))
(let ((data (aqi--city-cache-get city)))
;; simple typecheck -> error handling since semantic errors are cached as strings.
(if (stringp data)
(format "%s (%s)" data city)
(let-alist data
(format
(if (fboundp 'org-mode)
;; org mode formatted report
"* Air Quality index in %s is %s
\nMost recent report at %s (UTC%s).\n
| Dominant pollutant | %s |
| PM2.5 (fine particulate matter) | %s |
| PM10 (respirable particulate matter) | %s |
| NO2 (Nitrogen Dioxide) | %s |
| CO (Carbon Monoxide) | %s |
| | |
| Temperature (Celsius) | %s |
| Humidity | %s |
| Air pressure | %s |
| Wind | %s |
\nFurther details can be found at [[%s][aqicn]].
\nData provided by %s and %s%s"
;; text mode
"Air Quality index in %s is %s as of %s (UTC%s).
\nDominant pollutant is %s
PM2.5 (fine particulate matter): %s
PM10 (respirable particulate matter): %s
NO2 (Nitrogen Dioxide): %s
CO (Carbon Monoxide): %s
\nTemperature (Celsius): %s
Humidity: %s
Air pressure: %s
Wind: %s
\nFurther details can be found at [[%s][aqicn]].
\nData provided by %s and %s%s")
.city.name
.aqi
.time.s
.time.tz
.dominentpol
.iaqi.pm25.v
.iaqi.pm10.v
.iaqi.no2.v
.iaqi.co.v
.iaqi.t.v
.iaqi.h.v
.iaqi.p.v
.iaqi.wg.v
.city.url
(let-alist (elt .attributions 0) .name)
(let-alist (elt .attributions 1) .name)
(if aqi-use-cache " (cached)" "")))))))
;;;###autoload
(defun aqi-report (&optional place type)
"General air quality info from PLACE (or `here' if no PLACE is given).
Report TYPE can be `brief' or `full'."
(interactive "sName of city or monitoring station (RET for \"here\"): ")
(let* ((city (if (and (string< "" place) place) place "here"))
(aqi-output (get-buffer-create (format "*Air Quality - %s*" city))))
(with-current-buffer aqi-output
(goto-char (point-min))
(erase-buffer)
(pcase type
('brief (insert (aqi-report-brief city)))
('full (insert (aqi-report-full city)))
('nil (insert (aqi-report-full city)))
(other (warn "Unknown report type: '%s. Try using 'full or 'brief" other)))
(goto-char (point-max))
(insert "")
(org-mode))
(display-buffer aqi-output))
t)
(provide 'aqi)
;;; aqi.el ends here