;;; bom.el -- Australian weather forecast from the Bureau -*- lexical-binding: t -*- ;; Copyright (C) 2023 Free Software Foundation, Inc. ;; Author: Yuchen Pei ;; Package-Requires: ((emacs "28.2") (web-server "0.0.2")) ;; This file is part of bom.el. ;; bom.el is free software: you can redistribute it and/or modify it ;; under the terms of the GNU Affero General Public License as ;; published by the Free Software Foundation, either version 3 of the ;; License, or (at your option) any later version. ;; bom.el 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 Affero General ;; Public License for more details. ;; You should have received a copy of the GNU Affero General Public ;; License along with bom.el. If not, see . ;;; Commentary: ;; Australian weather forecast from the Bureau. A client that ;; downloads weather forecast and serves them using emacs-web-server. ;;; Code: (require 'hierarchy) (require 'web-server) (defvar bom-state-files '((act . "IDN11060") (nsw . "IDN11060") (nt . "IDD10207") (qld . "IDQ11295") (sa . "IDS10044") (tas . "IDT16710") (vic . "IDV10753") (wa . "IDW14199")) "Alist of states and territories and their corresponding filenames on the BOM FTP server, see .") (defun bom-api-vic () "Get Victoria weather from the BOM FTP." (with-current-buffer (find-file-noselect "/ftp:anonymous@ftp.bom.gov.au:/anon/gen/fwo/IDV10753.xml") (libxml-parse-xml-region (point-min) (point-max)))) (defun bom-get-areas (resp) "Given an API response RESP, get all areas." (dom-children (dom-by-tag resp 'forecast))) (defvar bom-areas nil "List of areas with forecasts.") (defvar bom-area-hierarchy (hierarchy-new) "Hierarchy of areas with forecasts.") (defvar bom-server nil "The BOM server object.") (defun bom-area-parent (area) "Search `bom-areas' for the parent of AREA. Used as parentfn for hierarchy." (let ((parent-aac (dom-attr area 'parent-aac))) (cl-find-if (lambda (area) (equal (dom-attr area 'aac) parent-aac)) bom-areas))) (defun bom-format-area (area level) "Format an AREA of LEVEL." (format "%s %s%s\n" (make-string level ?*) (dom-attr area 'description) (if (dom-children area) (concat "\n" (bom-format-forecasts (dom-children area))) ""))) (defun bom-area-lessp (a1 a2) "Compare two areas A1 and A2 based on their AAC identifier." (string-lessp (dom-attr a1 'aac) (dom-attr a2 'aac))) (defun bom-format-forecasts (forecasts) "Format a list of FORECASTS." (mapconcat 'bom-format-forecast forecasts "\n")) (defun bom-format-forecast (forecast) "Format a FORECAST period." (let ((date (substring (dom-attr forecast 'start-time-local) 5 10)) (data (dom-children forecast)) (min-temp) (max-temp) (precis) (prec-prob)) (dolist (item data) (pcase (dom-attr item 'type) ("air_temperature_minimum" (setq min-temp (dom-text item))) ("air_temperature_maximum" (setq max-temp (dom-text item))) ("probability_of_precipitation" (setq prec-prob (dom-text item))) ("precis" (setq precis (dom-text item))))) (format "- %s :: %s %s %s" date prec-prob precis (bom-format-temp-range min-temp max-temp)))) (defun bom-format-temp-range (min-temp max-temp) "Format a temperature range from MIN-TEMP to MAX-TEMP." (if (or min-temp max-temp) (format "%s - %s" (if min-temp (concat min-temp "C") "") (if max-temp (concat max-temp "C") "")))) (defun bom-format-areas-org (areas) "Format hierarchy of AREAS with forecasts." (concat "#+title: Victoria weather forecast\n\n" (string-join (hierarchy-map 'bom-format-area bom-area-hierarchy 1) "\n"))) (defun bom-format-areas-html (areas) "Format hierarchy of AREAS with forecasts to html." (with-temp-buffer (insert (bom-format-areas-org areas)) (org-export-as 'html))) (defun bom-get-vic-forecasts () "Call BOM API and return hierarchy of VIC areas with forecasts." (let ((areas (hierarchy-new))) (hierarchy-add-trees bom-area-hierarchy (setq bom-areas (bom-get-areas (bom-api-vic))) 'bom-area-parent) (hierarchy-sort bom-area-hierarchy 'bom-area-lessp) areas)) ;;;###autoload (defun bom-start () "Start serving weather forecasts." (when bom-server (bom-stop)) (setq bom-server (ws-start (lambda (request) (with-slots (process headers) request (ws-response-header process 200 '("Content-type" . "text/html")) (process-send-string process (bom-format-areas-html (bom-get-vic-forecasts))))) 9000))) (defun bom-stop () "Stop serving weather forecasts." (ws-stop bom-server)) (provide 'bom) ;;; bom.el ends here