I should make a simple thing that scraps the local pollen count page and outputs a JSON with the counts and the notes. I use this with my Home Assistant cluster to send me morning notifications if the grass pollen yesterday was real bad, to have an idea what today might bring.
Simple Flask server
python source: :tangle ~/Code/pollen/app.py :comments noneimport os import bs4 import urllib.request import itertools from flask import Flask app = Flask(__name__)
This uses BeautifulSoup to try to extract the relevant texts:
python source: :tangle ~/Code/pollen/app.py :comments nonedef fetch_counts(): req = urllib.request.Request( "https://www.oregonallergyassociates.com/pollen-counts/", headers={ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:126.0) " "Gecko/20100101 Firefox/126.0" } ) resp = urllib.request.urlopen(req) assert resp.code == 200 pchtml = resp.read() soup = bs4.BeautifulSoup(pchtml, 'html.parser') headlines = soup.find_all("div", class_="pollent-count-headline") pars = soup.find_all("p") texts = list() sentinel = "Kraig W. Jacobson, MD." for par in pars: if par.text == sentinel: break if "grass" in par.text.lower(): texts.append(par.text) notice = '\n'.join(texts) counts = dict() levels = dict() for desc, num in itertools.zip_longest(headlines[::2], headlines[1::2]): desc = desc.text.lower() num = int(num.text) if "grass" in desc: counts['grass'] = num levels['grass'] = normalize_grass(num) elif "tree" in desc: counts['trees'] = num levels['trees'] = normalize_trees(num) elif "weed" in desc: counts['weeds'] = num else: counts['unknown'] = num return (notice, counts, levels)
These clusterings are brought from the website:
python source: :tangle ~/Code/pollen/app.py :comments nonedef normalize_grass(num): if num <= 4: return "LOW" elif num <= 19: return "MODERATE" elif num <= 199: return "HIGH" return "VERY HIGH" def normalize_trees(num): if num <= 14: return "LOW" elif num <= 89: return "MODERATE" elif num <= 499: return "HIGH" return "VERY HIGH"
It's a simple endpoint to return them to HomeAss
python source: :tangle ~/Code/pollen/app.py :comments none@app.route("/counts") def flask_handler(): notice, counts, levels = fetch_counts() return { "notice": notice, "counts": counts, "levels": levels, } if __name__ == '__main__': listen_port = int(os.environ.get("PC_PORT", "8080")) app.run(host="0.0.0.0", port=listen_port)
Packaging
You can nix run this:
nix source: :tangle ~/Code/pollen/flake.nix :mkdirp yes{ inputs = { # nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; systems.url = "github:nix-systems/default"; }; outputs = { self, nixpkgs, flake-utils, systems, }: flake-utils.lib.eachSystem (import systems) (system: let pkgs = import nixpkgs { inherit system; }; python-with-my-packages = pkgs.python3.withPackages (p: with p; [ beautifulsoup4 flask ]); pc = import ./default.nix { inherit pkgs; }; in { apps = rec { default = pollen-count; pollen-count = flake-utils.lib.mkApp { drv = pc; }; }; packages = flake-utils.lib.flattenTree rec { default = pc; pollen-count = pc; devShells.default = pkgs.mkShell { packages = [ python-with-my-packages ]; }; }; }); }
And nix-build it.
nix source: :tangle ~/Code/pollen/default.nix{ pkgs ? import <nixpkgs> {}, ... }: pkgs.writers.writePython3Bin "pollen-count" { flakeIgnore = ["E302"]; libraries = with pkgs.python3.pkgs; [ beautifulsoup4 flask ]; } (builtins.readFile ./app.py)
And include it on NixOS. I oughta make this a gunicorn but meh. i'm the only one that will ever touch it!
nix source: :tangle ~/arroyo-nix/nixos/pollen-count.nix{ lib, pkgs, config, ... }: with lib; { options.services.pollen-count = { enable = mkOption { type = types.bool; default = true; }; package = mkOption { type = types.package; default = import <projects/pollen/default.nix> { inherit pkgs; }; }; port = mkOption { type = types.port; default = 18567; }; }; config = mkIf config.services.pollen-count.enable { networking.firewall.allowedTCPPorts = [ config.services.pollen-count.port ]; users.groups.python-scripts = {}; users.users.python-scripts = { createHome = true; home = "/var/lib/python-scripts"; group = "python-scripts"; isSystemUser = true; }; systemd.services.pollen-counter = { enable = true; description = "Fetch pollen count from Oregon Allergy Associates"; after = [ "network.target" ]; script = "${config.services.pollen-count.package}/bin/pollen-count"; wantedBy = [ "default.target" ]; environment = { PC_PORT = "${builtins.toString config.services.pollen-count.port}"; }; serviceConfig = { RestartSec = 5; Restart = "on-failure"; User = "python-scripts"; }; }; }; }