aboutsummaryrefslogtreecommitdiff
path: root/bom.el
diff options
context:
space:
mode:
authorYuchen Pei <id@ypei.org>2023-10-02 16:53:56 +1100
committerYuchen Pei <id@ypei.org>2023-10-02 16:53:56 +1100
commita2ad653df4ac4f3260541f1b0b24efbf8ce9d1ef (patch)
tree96c5062ef7e3e5268bc348dd549f0ecb7bd7080c /bom.el
First commit.
Serving Victoria weather over localhost:9000
Diffstat (limited to 'bom.el')
-rw-r--r--bom.el151
1 files changed, 151 insertions, 0 deletions
diff --git a/bom.el b/bom.el
new file mode 100644
index 0000000..710aa4d
--- /dev/null
+++ b/bom.el
@@ -0,0 +1,151 @@
+;;; bom.el -- Australian weather forecast from the Bureau -*- lexical-binding: t -*-
+
+;; Copyright (C) 2023 Free Software Foundation, Inc.
+
+;; Author: Yuchen Pei <id@ypei.org>
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; 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 <http://www.bom.gov.au/catalogue/data-feeds.shtml>.")
+
+(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 (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