diff --git a/.gitignore b/.gitignore index bd6aa86..518d7e9 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,9 @@ url/ bookmarks request/ +# Weather forecast +forecast + # Custom file custom.el diff --git a/screens/dashboard.el b/screens/dashboard.el index 22d9320..8f53fd3 100644 --- a/screens/dashboard.el +++ b/screens/dashboard.el @@ -113,7 +113,7 @@ ("RSS" elfeed "r") ("Terminal" theurgy-bottom-shell "t") ("Gomuks" theurgy-gomuks-workspace "G") - ("Weather" theurgy-show-weather "w"))))) + ("Weather" theurgy-weather "w"))))) :align center :width 20)) (grid-make-box `(:content ,(concat @@ -144,7 +144,7 @@ (grid-make-column (list (grid-make-box `(:content ,(concat (enlight-menu '(("Userland" - ("Dired" (dired "~") "d") + ("Weather" theurgy-weather "w") ("RSS" elfeed "r") ("Terminal" theurgy-bottom-shell "t") )))) diff --git a/userland/weather.el b/userland/weather.el new file mode 100644 index 0000000..5833d99 --- /dev/null +++ b/userland/weather.el @@ -0,0 +1,162 @@ +;;; weather.el --- Fetch and display the weather from bom.gov.au -*- lexical-binding: t -*- + + +;; 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 . + + +;;; Commentary: + +;; Reverse-engineered API docs are available here https://trickypr.github.io/bom-weather-docs/ +;; This code depends on request.el + +;;; Code: + +(defcustom theurgy-geohash + "" + "The geohash used for fetching forecast data from BOM. You can manually find this by going to https://api.weather.bom.gov.au/v1/locations?search=, or you can get a nicer interface for it with \\[theurgy-weather-find-geohash]." + :type 'string + :group 'theurgy + :group 'theurgy-weather) + +(defvar bom-api "https://api.weather.bom.gov.au/v1/") + +(defun theurgy-weather-find-geohash (search-term) + "Search for SEARCH-TERM and then return possible geohash candidates." + (interactive "sSearch term (Suburb or Postcode): ") + (when (> 4 (length search-term)) + (error "Search term must be greater than 3 characters (don't ask me)")) + (request (concat bom-api "locations?search=" search-term) + :parser 'json-read + :success (cl-function + (lambda (&key data &allow-other-keys) + (let* ((res (cdr (assoc 'data data))) + (candidates (mapcar (lambda (alst) + (cons (concat + (cdr (assoc 'name alst)) ", " + (cdr (assoc 'state alst)) ", " + (cdr (assoc 'postcode alst))) + (cdr (assoc 'geohash alst)))) + res))) + (customize-set-variable 'theurgy-geohash (cdr (assoc (completing-read "Select Location: " candidates) candidates)))))))) + +(defvar forecast-timer nil) +(defvar forecast-location "forecast") ;; The location of the cached forecast, related to the emacs user directory + +(defun theurgy-weather-fetch-forecast () + "Fetch and save the weather forecast." + (interactive) + (request + (concat bom-api "locations/" theurgy-geohash "/forecasts/daily") + :parser 'json-read + :success (cl-function + (lambda (&key data &allow-other-keys) + (save-excursion + (find-file (concat user-emacs-directory forecast-location)) + (delete-region (point-min) (point-max)) + (insert (format "%S" (mapcar (lambda (alst) + (cons (let ((d (parse-time-string (cdr (assoc 'date alst))))) + (list (nth 3 d) (nth 4 d) (nth 5 d))) + (assq-delete-all 'date alst))) + (cdr (assoc 'data data))))) + (save-buffer) + (kill-buffer)))))) + +(defun current-date () + "Get the current date as a list." + (let ((d (decode-time (current-time)))) + (list (nth 3 d) (nth 4 d) (nth 5 d)))) + +(defun today+ (days) + "Date list for today + DAYS." + (let* ((sec (+ (* days 86400) (time-convert (current-time) 'integer))) + (d (decode-time sec))) + (list (nth 3 d) (nth 4 d) (nth 5 d)))) + +(defun prompt-date () + "Prompt the user for a date." + (let ((d (parse-time-string (org-read-date)))) + (list (nth 3 d) (nth 4 d) (nth 5 d)))) + +(defun theurgy-weather-get-for-date (date) + "Get weather for a particular DATE." + (let* ((forecast-data (read (with-temp-buffer + (insert-file-contents (concat user-emacs-directory forecast-location)) + (buffer-string)))) + (day (assoc date + forecast-data))) + day)) + +(defun theurgy-quick-forecast (date) + "Quick forecast for DATE." + (let ((forecast (theurgy-weather-get-for-date date))) + (message (format "%s - %s degrees. %s" + (cdr (assoc 'temp_min forecast)) + (cdr (assoc 'temp_max forecast)) + (cdr (assoc 'short_text forecast)))))) + +(defun theurgy-weather-quick () + "Show the forecast as a message." + (interactive) + (theurgy-quick-forecast (prompt-date))) + +(defun theurgy-start-forecast-timer () + "Start the timer for periodically retrieving the weather forecast." + (interactive) + (theurgy-weather-fetch-forecast) + (unless forecast-timer + (when (timerp forecast-timer) + (cancel-timer forecast-timer)) + (setq forecast-timer (run-at-time t (* 60 30) (theurgy-weather-fetch-forecast))))) + +(theurgy-start-forecast-timer) + +(defun theurgy-weather-insert-forecast (fc) + "Insert provided forecast FC in the current buffer, as org markup." + (let ((date (car fc))) + (insert (format "* %s/%s/%s - %s\n" (nth 0 date) (nth 1 date) (nth 2 date) (cdr (assoc 'short_text fc)))) + (insert (format "%s\n" (cdr (assoc 'extended_text fc)))) + (insert (format "- %s-%s°C\n" + (cdr (assoc 'temp_min fc)) + (cdr (assoc 'temp_max fc)))) + (insert (format "- %s%% Chance of Rain (%s-%smm)\n" + (cdr (assoc 'chance (assoc 'rain fc))) + (cdr (assoc 'lower_range (assoc 'amount (assoc 'rain fc)))) + (cdr (assoc 'upper_range (assoc 'amount (assoc 'rain fc)))))) + (insert (format "- %s UV\n" + (cdr (assoc 'category (assoc 'uv fc))))) + (insert (format "- %s Fire Danger\n" + (cdr (assoc 'fire_danger fc)))) + (insert "\n"))) + +(defun theurgy-weather () + "Show weather information in a new buffer." + (interactive) + (let ((buf (generate-new-buffer "*Weather*"))) + (with-current-buffer buf + (delete-region (point-min) (point-max)) + (org-mode) + (let* ((today (theurgy-weather-get-for-date (current-date))) + (tomorrow (theurgy-weather-get-for-date (today+ 1))) + (after-tomorrow (theurgy-weather-get-for-date (today+ 2)))) + (theurgy-weather-insert-forecast today) + (theurgy-weather-insert-forecast tomorrow) + (theurgy-weather-insert-forecast after-tomorrow)) + (setq buffer-read-only t)) + (switch-to-buffer buf))) + +(provide 'weather) + +;;; weather.el ends here