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
import 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:
def 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:
def 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
@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 like nix run git+http://code.rix.si/rrix/pollen.
{
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.
{ pkgs ? import <nixpkgs> {}, ... }:
pkgs.writers.writePython3Bin "pollen-count" {
flakeIgnore = ["E302" "W292"];
libraries = with pkgs.python3.pkgs; [
beautifulsoup4
flask
];
} (builtins.readFile ./app.py)The Pollen Counter as a NixOS Module
And include it on NixOS. I oughta make this a gunicorn but meh. i'm the only one that will ever touch it!
{ lib, pkgs, inputs, config, ... }:
with lib;
{
options.services.pollen-count = {
enable = mkOption {
type = types.bool;
default = true;
};
package = mkOption {
type = types.package;
default = inputs.pollen-count.packages.${pkgs.stdenv.hostPlatform.system}.default;
};
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";
};
};
};
}Make the pollen counter available in an Arroyo System Flake
pollen-count = {
url = "git+http://code.rix.si/rrix/pollen";
inputs.nixpkgs.follows = "nixpkgs";
};