diff --git a/README.md b/README.md index 72d7a6a..38840b5 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,13 @@ Run the service; host, port, and URL prefix all come from `config.yaml`: .venv/bin/python wsgi.py ``` +For production, use the Waitress entry point. It reads host, port, thread count, +and all application settings from `config.yaml`: + +```sh +.venv/bin/python serve.py +``` + With the example settings, open . Keep one application worker because the polling scheduler runs inside the service process. Alternatively set `collector.enabled: false` in web workers and invoke this from a system timer: @@ -78,6 +85,42 @@ The service account must have write permission on the log directory. Console logging remains enabled by default so `python wsgi.py` displays its listening address; set `logging.console: false` to use only the logfile. +## systemd installation + +The included `deploy/netatmo.service` runs Waitress from `/srv/service_netatmo` +under a dedicated, non-login `netatmo` account. As root, create the account and +writable data directory: + +```sh +useradd --system --home-dir /srv/service_netatmo --shell /usr/sbin/nologin netatmo +install -d -o netatmo -g netatmo -m 0750 /var/lib/rrd +chown netatmo:netatmo /srv/service_netatmo/config.yaml +chmod 0600 /srv/service_netatmo/config.yaml +``` + +Create the virtual environment and install dependencies as that account: + +```sh +sudo -u netatmo python3 -m venv /srv/service_netatmo/.venv +sudo -u netatmo /srv/service_netatmo/.venv/bin/pip install -r /srv/service_netatmo/requirements.txt +``` + +Install and start the unit: + +```sh +install -o root -g root -m 0644 /srv/service_netatmo/deploy/netatmo.service /etc/systemd/system/netatmo.service +systemctl daemon-reload +systemctl enable --now netatmo.service +systemctl status netatmo.service +``` + +Follow service output and the rotating application log with: + +```sh +journalctl -u netatmo.service -f +tail -f /var/lib/rrd/netatmo_service.log +``` + ## HTTP API ```text diff --git a/config.example.yaml b/config.example.yaml index 11646ac..7379369 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -2,6 +2,7 @@ server: host: 127.0.0.1 port: 30225 + threads: 4 url_prefix: /w storage: @@ -23,8 +24,8 @@ graphs: height: 240 netatmo: - client_id: 6a93db3d7cdb0e40ea0c2655 - client_secret: c6uRbAzZ9e8STPtXas6PYKaZZiD68Y6QHFaKq2GEIa8 + client_id: REPLACE_ME + client_secret: REPLACE_ME redirect_uri: http://www.example.com/w token_file: /var/lib/rrd/netatmo_tokens.json device_id: "" diff --git a/deploy/netatmo.service b/deploy/netatmo.service new file mode 100644 index 0000000..db82fcd --- /dev/null +++ b/deploy/netatmo.service @@ -0,0 +1,25 @@ +[Unit] +Description=Netatmo RRD Flask service (Waitress) +Documentation=file:///srv/service_netatmo/README.md +Wants=network-online.target +After=network-online.target + +[Service] +Type=simple +User=netatmo +Group=netatmo +WorkingDirectory=/srv/service_netatmo +ExecStart=/srv/service_netatmo/.venv/bin/python /srv/service_netatmo/serve.py +Restart=on-failure +RestartSec=10 +TimeoutStopSec=30 +UMask=0077 + +NoNewPrivileges=true +PrivateTmp=true +ProtectHome=true +ProtectSystem=strict +ReadWritePaths=/var/lib/rrd + +[Install] +WantedBy=multi-user.target diff --git a/netatmo_service/config.py b/netatmo_service/config.py index dba3b6d..0472af5 100644 --- a/netatmo_service/config.py +++ b/netatmo_service/config.py @@ -20,7 +20,7 @@ def default_config(base: Path | None = None) -> dict[str, Any]: base = (base or Path.cwd()).resolve() rrd = base / "rrd" return { - "HOST": "0.0.0.0", "PORT": 5000, "URL_PREFIX": "", "RRD_FOLDER": rrd, + "HOST": "0.0.0.0", "PORT": 5000, "THREADS": 4, "URL_PREFIX": "", "RRD_FOLDER": rrd, "LOG_FILE": rrd / "netatmo_service.log", "LOG_LEVEL": "INFO", "LOG_MAX_BYTES": 5242880, "LOG_BACKUP_COUNT": 5, "LOG_CONSOLE": True, "NETATMO_TOKEN_FILE": rrd / "netatmo_tokens.json", @@ -63,6 +63,7 @@ def load_config(filename: str | Path | None = None) -> dict[str, Any]: result.update({ "HOST": str(server.get("host", result["HOST"])), "PORT": int(server.get("port", result["PORT"])), + "THREADS": int(server.get("threads", result["THREADS"])), "URL_PREFIX": _prefix(server.get("url_prefix", result["URL_PREFIX"])), "GRAPH_WIDTH": int(graphs.get("width", result["GRAPH_WIDTH"])), "GRAPH_HEIGHT": int(graphs.get("height", result["GRAPH_HEIGHT"])), diff --git a/netatmo_service/first_time_install.sh b/netatmo_service/first_time_install.sh new file mode 100644 index 0000000..372fbad --- /dev/null +++ b/netatmo_service/first_time_install.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# run as root + +useradd --system --home-dir /srv/service_netatmo --shell /usr/sbin/nologin netatmo +install -d -o netatmo -g netatmo -m 0750 /var/lib/rrd + +chown netatmo:netatmo /srv/service_netatmo/config.yaml +chmod 0600 /srv/service_netatmo/config.yaml + +# sudo -u netatmo python3 -m venv /srv/service_netatmo/.venv +# sudo -u netatmo /srv/service_netatmo/.venv/bin/pip install \ + -r /srv/service_netatmo/requirements.txt + +install -o root -g root -m 0644 \ + /srv/service_netatmo/deploy/netatmo.service \ + /etc/systemd/system/netatmo.service + +systemctl daemon-reload +systemctl enable --now netatmo.service +systemctl status netatmo.service \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index a063b5f..0837c6d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ Flask>=3.1,<4 PyYAML>=6.0,<7 +waitress>=3.0,<4 rrdtool-bindings # if python 3.14 rrdtool # if python 3.12 diff --git a/serve.py b/serve.py new file mode 100644 index 0000000..73ae9c4 --- /dev/null +++ b/serve.py @@ -0,0 +1,14 @@ +"""Production entry point using Waitress and config.yaml.""" + +from waitress import serve + +from wsgi import app + + +if __name__ == "__main__": + serve( + app, + host=app.config["HOST"], + port=app.config["PORT"], + threads=app.config["THREADS"], + ) diff --git a/tests/test_config.py b/tests/test_config.py index f131c8e..6e21f40 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -8,6 +8,7 @@ def test_yaml_config_is_loaded_and_relative_paths_are_resolved(tmp_path): server: host: 127.0.0.1 port: 12345 + threads: 6 url_prefix: weather/ storage: rrd_folder: data @@ -27,6 +28,7 @@ modules: config = load_config(config_file) assert config["HOST"] == "127.0.0.1" assert config["PORT"] == 12345 + assert config["THREADS"] == 6 assert config["URL_PREFIX"] == "/weather" assert config["RRD_FOLDER"] == tmp_path / "data" assert config["LOG_FILE"] == tmp_path / "logs/service.log"