introduced config.yaml
This commit is contained in:
@@ -1,30 +0,0 @@
|
|||||||
# Registered Netatmo application: GetNetatmoData v2
|
|
||||||
# Mount every route below this path. Leave empty to serve from /.
|
|
||||||
URL_PREFIX=/w
|
|
||||||
|
|
||||||
NETATMO_CLIENT_ID=6a93db3d7cdb0e40ea0c2655
|
|
||||||
NETATMO_CLIENT_SECRET=c6uRbAzZ9e8STPtXas6PYKaZZiD68Y6QHFaKq2GEIa8
|
|
||||||
NETATMO_REFRESH_TOKEN=
|
|
||||||
# Rotated OAuth tokens are persisted here with mode 0600.
|
|
||||||
NETATMO_TOKEN_FILE=/var/lib/rrd/netatmo_tokens.json
|
|
||||||
# Must exactly match a redirect URI configured for the Netatmo application.
|
|
||||||
NETATMO_REDIRECT_URI=http://www.suy.nl/w
|
|
||||||
# Alternatively, useful for a short-lived test (refresh credentials are preferred):
|
|
||||||
# NETATMO_ACCESS_TOKEN=
|
|
||||||
# NETATMO_DEVICE_ID=
|
|
||||||
|
|
||||||
RRD_FOLDER=/var/lib/rrd
|
|
||||||
LOG_FILE=/var/lib/rrd/netatmo_service.log
|
|
||||||
LOG_LEVEL=INFO
|
|
||||||
LOG_MAX_BYTES=5242880
|
|
||||||
LOG_BACKUP_COUNT=5
|
|
||||||
LOG_CONSOLE=true
|
|
||||||
POLL_INTERVAL=600
|
|
||||||
START_COLLECTOR=true
|
|
||||||
|
|
||||||
# Override these if the names in the Netatmo app differ.
|
|
||||||
MODULE_OUTDOOR=Outdoor
|
|
||||||
MODULE_WIND=Wind
|
|
||||||
MODULE_BEDROOM=Bedroom
|
|
||||||
MODULE_STUDY=Study
|
|
||||||
MODULE_LIVING=Living
|
|
||||||
@@ -2,5 +2,6 @@ __pycache__/
|
|||||||
*.py[cod]
|
*.py[cod]
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.env
|
.env
|
||||||
|
config.yaml
|
||||||
rrd/
|
rrd/
|
||||||
netatmo_tokens.json
|
netatmo_tokens.json
|
||||||
|
|||||||
@@ -26,17 +26,15 @@ but may require the RRDtool development headers and a C compiler.
|
|||||||
```sh
|
```sh
|
||||||
python3 -m venv .venv
|
python3 -m venv .venv
|
||||||
.venv/bin/pip install -r requirements.txt
|
.venv/bin/pip install -r requirements.txt
|
||||||
cp .env.example .env
|
cp config.example.yaml config.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
Add the client ID and secret for the Netatmo application **GetNetatmoData v2**
|
Add the client ID and secret for the Netatmo application **GetNetatmoData v2**
|
||||||
to `.env`. Ensure `NETATMO_REDIRECT_URI` exactly matches a redirect URI configured
|
to the private `config.yaml`. Ensure `netatmo.redirect_uri` exactly matches a
|
||||||
for that application, then perform the initial `read_station` authorization:
|
redirect URI configured for that application, then perform the initial
|
||||||
|
`read_station` authorization:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
set -a
|
|
||||||
. ./.env
|
|
||||||
set +a
|
|
||||||
.venv/bin/python scripts/get_netatmo_token.py
|
.venv/bin/python scripts/get_netatmo_token.py
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -45,43 +43,40 @@ from the browser's address bar back into the prompt. This works even if the loca
|
|||||||
callback page itself cannot be reached. The script exchanges the authorization
|
callback page itself cannot be reached. The script exchanges the authorization
|
||||||
code and writes the token file with mode `0600`.
|
code and writes the token file with mode `0600`.
|
||||||
|
|
||||||
The service loads `NETATMO_TOKEN_FILE` at startup. Before an access token expires,
|
The service loads `netatmo.token_file` at startup. Before an access token expires,
|
||||||
it refreshes it and atomically saves both the new access token and any rotated
|
it refreshes it and atomically saves both the new access token and any rotated
|
||||||
refresh token. Values in the token file take precedence over initial token values
|
refresh token. Values in the token file take precedence over initial token values
|
||||||
in `.env`. Client credentials remain environment-only and are never written to
|
in YAML. Client credentials remain in the private configuration and are never written to
|
||||||
the token file or an RRD.
|
the token file or an RRD.
|
||||||
|
|
||||||
Export the file and run Flask:
|
Run the service; host, port, and URL prefix all come from `config.yaml`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
set -a
|
.venv/bin/python wsgi.py
|
||||||
. ./.env
|
|
||||||
set +a
|
|
||||||
.venv/bin/flask --app wsgi run --host 0.0.0.0
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Open <http://localhost:5000/>. Keep one application worker because the polling
|
With the example settings, open <http://localhost:30225/w/>. Keep one application worker because the polling
|
||||||
scheduler runs inside the service process. Alternatively set
|
scheduler runs inside the service process. Alternatively set
|
||||||
`START_COLLECTOR=false` in web workers and invoke this from a system timer:
|
`collector.enabled: false` in web workers and invoke this from a system timer:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
.venv/bin/flask --app wsgi collect-now
|
.venv/bin/flask --app wsgi collect-now
|
||||||
```
|
```
|
||||||
|
|
||||||
`RRD_FOLDER` configures the database directory (default `./rrd`). The module
|
`storage.rrd_folder` configures the database directory (default `./rrd`). The
|
||||||
variables in `.env.example` allow the five Netatmo display names to be changed.
|
`modules` section allows the five Netatmo display names to be changed.
|
||||||
Set `URL_PREFIX=/w` to mount the dashboard, static assets, and every API endpoint
|
Set `server.url_prefix: /w` to mount the dashboard, static assets, and every API endpoint
|
||||||
below `/w`; leave it empty to serve from the site root. When a prefix is set,
|
below `/w`; leave it empty to serve from the site root. When a prefix is set,
|
||||||
open <http://localhost:5000/w/> instead.
|
open <http://localhost:5000/w/> instead.
|
||||||
|
|
||||||
`LOG_FILE` selects a rotating application log (by default
|
`logging.file` selects a rotating application log (by default
|
||||||
`RRD_FOLDER/netatmo_service.log`). Every Netatmo HTTP attempt records success or
|
`RRD_FOLDER/netatmo_service.log`). Every Netatmo HTTP attempt records success or
|
||||||
failure, HTTP status, endpoint, and duration without credentials. Collector,
|
failure, HTTP status, endpoint, and duration without credentials. Collector,
|
||||||
Flask, uncaught main-thread, and uncaught worker-thread exceptions are written to
|
Flask, uncaught main-thread, and uncaught worker-thread exceptions are written to
|
||||||
the same log. `LOG_MAX_BYTES` defaults to 5 MiB and `LOG_BACKUP_COUNT` to five.
|
the same log. `logging.max_bytes` defaults to 5 MiB and `backup_count` to five.
|
||||||
The service account must have write permission on the log directory. Console
|
The service account must have write permission on the log directory. Console
|
||||||
logging remains enabled by default so `python wsgi.py` displays its listening
|
logging remains enabled by default so `python wsgi.py` displays its listening
|
||||||
address; set `LOG_CONSOLE=false` to use only the logfile.
|
address; set `logging.console: false` to use only the logfile.
|
||||||
|
|
||||||
## HTTP API
|
## HTTP API
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Copy this file to config.yaml. Keep config.yaml private; it contains secrets.
|
||||||
|
server:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 30225
|
||||||
|
url_prefix: /w
|
||||||
|
|
||||||
|
storage:
|
||||||
|
rrd_folder: /var/lib/rrd
|
||||||
|
|
||||||
|
logging:
|
||||||
|
file: /var/lib/rrd/netatmo_service.log
|
||||||
|
level: INFO
|
||||||
|
max_bytes: 5242880
|
||||||
|
backup_count: 5
|
||||||
|
console: true
|
||||||
|
|
||||||
|
collector:
|
||||||
|
enabled: true
|
||||||
|
interval_seconds: 600
|
||||||
|
|
||||||
|
graphs:
|
||||||
|
width: 900
|
||||||
|
height: 240
|
||||||
|
|
||||||
|
netatmo:
|
||||||
|
client_id: 6a93db3d7cdb0e40ea0c2655
|
||||||
|
client_secret: c6uRbAzZ9e8STPtXas6PYKaZZiD68Y6QHFaKq2GEIa8
|
||||||
|
redirect_uri: http://www.example.com/w
|
||||||
|
token_file: /var/lib/rrd/netatmo_tokens.json
|
||||||
|
device_id: ""
|
||||||
|
refresh_token: ""
|
||||||
|
access_token: ""
|
||||||
|
token_url: https://api.netatmo.com/oauth2/token
|
||||||
|
stations_url: https://api.netatmo.com/api/getstationsdata
|
||||||
|
|
||||||
|
modules:
|
||||||
|
outdoor: Outdoor
|
||||||
|
wind: Wind
|
||||||
|
bedroom: Bedroom
|
||||||
|
study: Study
|
||||||
|
living: Living
|
||||||
@@ -10,7 +10,7 @@ from flask import Flask, Response, abort, jsonify, render_template, url_for
|
|||||||
from werkzeug.exceptions import HTTPException
|
from werkzeug.exceptions import HTTPException
|
||||||
|
|
||||||
from .collector import Collector
|
from .collector import Collector
|
||||||
from .config import Config
|
from .config import default_config, load_config
|
||||||
from .netatmo import NetatmoClient
|
from .netatmo import NetatmoClient
|
||||||
from .rrd import ALIASES, PERIODS, RRD_ERROR, RRDStore, SCHEMAS
|
from .rrd import ALIASES, PERIODS, RRD_ERROR, RRDStore, SCHEMAS
|
||||||
|
|
||||||
@@ -85,9 +85,9 @@ def configure_logging(app: Flask) -> None:
|
|||||||
threading.excepthook = uncaught_thread_exception
|
threading.excepthook = uncaught_thread_exception
|
||||||
|
|
||||||
|
|
||||||
def create_app(test_config: dict | None = None) -> Flask:
|
def create_app(test_config: dict | None = None, config_path=None) -> Flask:
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.from_object(Config)
|
app.config.from_mapping(load_config(config_path) if test_config is None else default_config())
|
||||||
if test_config:
|
if test_config:
|
||||||
app.config.update(test_config)
|
app.config.update(test_config)
|
||||||
|
|
||||||
|
|||||||
+80
-40
@@ -1,51 +1,91 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import yaml
|
||||||
def _bool(name: str, default: bool) -> bool:
|
|
||||||
value = os.getenv(name)
|
|
||||||
return default if value is None else value.lower() in {"1", "true", "yes", "on"}
|
|
||||||
|
|
||||||
|
|
||||||
def _prefix(value: str) -> str:
|
def _prefix(value: str) -> str:
|
||||||
value = value.strip()
|
value = str(value).strip()
|
||||||
if not value or value == "/":
|
return "" if not value or value == "/" else "/" + value.strip("/")
|
||||||
return ""
|
|
||||||
return "/" + value.strip("/")
|
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
def _path(value: str | Path, base: Path) -> Path:
|
||||||
URL_PREFIX = _prefix(os.getenv("URL_PREFIX", ""))
|
path = Path(value).expanduser()
|
||||||
RRD_FOLDER = Path(os.getenv("RRD_FOLDER", "./rrd")).expanduser().resolve()
|
return (base / path).resolve() if not path.is_absolute() else path.resolve()
|
||||||
LOG_FILE = Path(
|
|
||||||
os.getenv("LOG_FILE", str(RRD_FOLDER / "netatmo_service.log"))
|
|
||||||
).expanduser().resolve()
|
|
||||||
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
|
|
||||||
LOG_MAX_BYTES = int(os.getenv("LOG_MAX_BYTES", str(5 * 1024 * 1024)))
|
|
||||||
LOG_BACKUP_COUNT = int(os.getenv("LOG_BACKUP_COUNT", "5"))
|
|
||||||
LOG_CONSOLE = _bool("LOG_CONSOLE", True)
|
|
||||||
NETATMO_TOKEN_FILE = Path(
|
|
||||||
os.getenv("NETATMO_TOKEN_FILE", str(RRD_FOLDER / "netatmo_tokens.json"))
|
|
||||||
).expanduser().resolve()
|
|
||||||
GRAPH_WIDTH = int(os.getenv("GRAPH_WIDTH", "900"))
|
|
||||||
GRAPH_HEIGHT = int(os.getenv("GRAPH_HEIGHT", "240"))
|
|
||||||
POLL_INTERVAL = int(os.getenv("POLL_INTERVAL", "600"))
|
|
||||||
START_COLLECTOR = _bool("START_COLLECTOR", True)
|
|
||||||
|
|
||||||
NETATMO_CLIENT_ID = os.getenv("NETATMO_CLIENT_ID", "")
|
|
||||||
NETATMO_CLIENT_SECRET = os.getenv("NETATMO_CLIENT_SECRET", "")
|
def default_config(base: Path | None = None) -> dict[str, Any]:
|
||||||
NETATMO_REFRESH_TOKEN = os.getenv("NETATMO_REFRESH_TOKEN", "")
|
base = (base or Path.cwd()).resolve()
|
||||||
NETATMO_ACCESS_TOKEN = os.getenv("NETATMO_ACCESS_TOKEN", "")
|
rrd = base / "rrd"
|
||||||
NETATMO_DEVICE_ID = os.getenv("NETATMO_DEVICE_ID", "")
|
return {
|
||||||
NETATMO_TOKEN_URL = os.getenv("NETATMO_TOKEN_URL", "https://api.netatmo.com/oauth2/token")
|
"HOST": "0.0.0.0", "PORT": 5000, "URL_PREFIX": "", "RRD_FOLDER": rrd,
|
||||||
NETATMO_STATIONS_URL = os.getenv(
|
"LOG_FILE": rrd / "netatmo_service.log", "LOG_LEVEL": "INFO",
|
||||||
"NETATMO_STATIONS_URL", "https://api.netatmo.com/api/getstationsdata"
|
"LOG_MAX_BYTES": 5242880, "LOG_BACKUP_COUNT": 5, "LOG_CONSOLE": True,
|
||||||
|
"NETATMO_TOKEN_FILE": rrd / "netatmo_tokens.json",
|
||||||
|
"NETATMO_REDIRECT_URI": "http://localhost:5000/",
|
||||||
|
"GRAPH_WIDTH": 900, "GRAPH_HEIGHT": 240, "POLL_INTERVAL": 600,
|
||||||
|
"START_COLLECTOR": True, "NETATMO_CLIENT_ID": "", "NETATMO_CLIENT_SECRET": "",
|
||||||
|
"NETATMO_REFRESH_TOKEN": "", "NETATMO_ACCESS_TOKEN": "", "NETATMO_DEVICE_ID": "",
|
||||||
|
"NETATMO_TOKEN_URL": "https://api.netatmo.com/oauth2/token",
|
||||||
|
"NETATMO_STATIONS_URL": "https://api.netatmo.com/api/getstationsdata",
|
||||||
|
"MODULE_OUTDOOR": "Outdoor", "MODULE_WIND": "Wind", "MODULE_BEDROOM": "Bedroom",
|
||||||
|
"MODULE_STUDY": "Study", "MODULE_LIVING": "Living",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(filename: str | Path | None = None) -> dict[str, Any]:
|
||||||
|
"""Load and normalize the service's single YAML configuration file."""
|
||||||
|
filename = filename or "config.yaml"
|
||||||
|
config_path = Path(filename).expanduser().resolve()
|
||||||
|
if not config_path.is_file():
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Configuration file not found: {config_path}. "
|
||||||
|
"Copy config.example.yaml to config.yaml and edit it."
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
raw = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
|
||||||
|
except (OSError, yaml.YAMLError) as exc:
|
||||||
|
raise RuntimeError(f"Cannot load YAML configuration {config_path}: {exc}") from exc
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
raise RuntimeError(f"YAML configuration {config_path} must contain a mapping")
|
||||||
|
|
||||||
|
sections = {name: raw.get(name, {}) for name in
|
||||||
|
("server", "storage", "logging", "collector", "graphs", "netatmo", "modules")}
|
||||||
|
for name, section in sections.items():
|
||||||
|
if not isinstance(section, dict):
|
||||||
|
raise RuntimeError(f"YAML section '{name}' must be a mapping")
|
||||||
|
base, result = config_path.parent, default_config(config_path.parent)
|
||||||
|
server, storage, log = sections["server"], sections["storage"], sections["logging"]
|
||||||
|
collector, graphs = sections["collector"], sections["graphs"]
|
||||||
|
netatmo, modules = sections["netatmo"], sections["modules"]
|
||||||
|
result.update({
|
||||||
|
"HOST": str(server.get("host", result["HOST"])),
|
||||||
|
"PORT": int(server.get("port", result["PORT"])),
|
||||||
|
"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"])),
|
||||||
|
"POLL_INTERVAL": int(collector.get("interval_seconds", result["POLL_INTERVAL"])),
|
||||||
|
"START_COLLECTOR": bool(collector.get("enabled", result["START_COLLECTOR"])),
|
||||||
|
"LOG_LEVEL": str(log.get("level", result["LOG_LEVEL"])).upper(),
|
||||||
|
"LOG_MAX_BYTES": int(log.get("max_bytes", result["LOG_MAX_BYTES"])),
|
||||||
|
"LOG_BACKUP_COUNT": int(log.get("backup_count", result["LOG_BACKUP_COUNT"])),
|
||||||
|
"LOG_CONSOLE": bool(log.get("console", result["LOG_CONSOLE"])),
|
||||||
|
"NETATMO_CLIENT_ID": str(netatmo.get("client_id", "")),
|
||||||
|
"NETATMO_CLIENT_SECRET": str(netatmo.get("client_secret", "")),
|
||||||
|
"NETATMO_REFRESH_TOKEN": str(netatmo.get("refresh_token", "")),
|
||||||
|
"NETATMO_ACCESS_TOKEN": str(netatmo.get("access_token", "")),
|
||||||
|
"NETATMO_DEVICE_ID": str(netatmo.get("device_id", "")),
|
||||||
|
"NETATMO_REDIRECT_URI": str(netatmo.get("redirect_uri", result["NETATMO_REDIRECT_URI"])),
|
||||||
|
"NETATMO_TOKEN_URL": str(netatmo.get("token_url", result["NETATMO_TOKEN_URL"])),
|
||||||
|
"NETATMO_STATIONS_URL": str(netatmo.get("stations_url", result["NETATMO_STATIONS_URL"])),
|
||||||
|
})
|
||||||
|
result["RRD_FOLDER"] = _path(storage.get("rrd_folder", "rrd"), base)
|
||||||
|
result["LOG_FILE"] = _path(log.get("file", result["RRD_FOLDER"] / "netatmo_service.log"), base)
|
||||||
|
result["NETATMO_TOKEN_FILE"] = _path(
|
||||||
|
netatmo.get("token_file", result["RRD_FOLDER"] / "netatmo_tokens.json"), base
|
||||||
)
|
)
|
||||||
|
for name in ("outdoor", "wind", "bedroom", "study", "living"):
|
||||||
MODULE_OUTDOOR = os.getenv("MODULE_OUTDOOR", "Outdoor")
|
result[f"MODULE_{name.upper()}"] = str(modules.get(name, result[f"MODULE_{name.upper()}"]))
|
||||||
MODULE_WIND = os.getenv("MODULE_WIND", "Wind")
|
return result
|
||||||
MODULE_BEDROOM = os.getenv("MODULE_BEDROOM", "Bedroom")
|
|
||||||
MODULE_STUDY = os.getenv("MODULE_STUDY", "Study")
|
|
||||||
MODULE_LIVING = os.getenv("MODULE_LIVING", "Living")
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
Flask>=3.1,<4
|
Flask>=3.1,<4
|
||||||
|
PyYAML>=6.0,<7
|
||||||
rrdtool-bindings # if python 3.14
|
rrdtool-bindings # if python 3.14
|
||||||
rrdtool # if python 3.12
|
rrdtool # if python 3.12
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
|
||||||
import secrets
|
import secrets
|
||||||
import sys
|
import sys
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
@@ -14,6 +13,7 @@ from pathlib import Path
|
|||||||
# Allow direct execution from a source checkout without installing the package.
|
# Allow direct execution from a source checkout without installing the package.
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
|
from netatmo_service.config import load_config
|
||||||
from netatmo_service.netatmo import NetatmoClient, NetatmoError
|
from netatmo_service.netatmo import NetatmoClient, NetatmoError
|
||||||
|
|
||||||
AUTHORIZE_URL = "https://api.netatmo.com/oauth2/authorize"
|
AUTHORIZE_URL = "https://api.netatmo.com/oauth2/authorize"
|
||||||
@@ -21,16 +21,7 @@ AUTHORIZE_URL = "https://api.netatmo.com/oauth2/authorize"
|
|||||||
|
|
||||||
def arguments() -> argparse.Namespace:
|
def arguments() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument("--client-id", default=os.getenv("NETATMO_CLIENT_ID"))
|
parser.add_argument("--config", type=Path, default=Path("config.yaml"))
|
||||||
parser.add_argument("--client-secret", default=os.getenv("NETATMO_CLIENT_SECRET"))
|
|
||||||
parser.add_argument(
|
|
||||||
"--redirect-uri", default=os.getenv("NETATMO_REDIRECT_URI", "http://localhost:8080")
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--token-file",
|
|
||||||
type=Path,
|
|
||||||
default=Path(os.getenv("NETATMO_TOKEN_FILE", "./rrd/netatmo_tokens.json")),
|
|
||||||
)
|
|
||||||
parser.add_argument("--no-browser", action="store_true")
|
parser.add_argument("--no-browser", action="store_true")
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
@@ -51,15 +42,20 @@ def authorization_code(value: str, expected_state: str) -> str:
|
|||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
args = arguments()
|
args = arguments()
|
||||||
if not args.client_id or not args.client_secret:
|
try:
|
||||||
print("Set NETATMO_CLIENT_ID and NETATMO_CLIENT_SECRET first.", file=sys.stderr)
|
config = load_config(args.config)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
print(exc, file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
if not config["NETATMO_CLIENT_ID"] or not config["NETATMO_CLIENT_SECRET"]:
|
||||||
|
print("Set netatmo.client_id and netatmo.client_secret in config.yaml first.", file=sys.stderr)
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
state = secrets.token_urlsafe(24)
|
state = secrets.token_urlsafe(24)
|
||||||
url = f"{AUTHORIZE_URL}?" + urllib.parse.urlencode(
|
url = f"{AUTHORIZE_URL}?" + urllib.parse.urlencode(
|
||||||
{
|
{
|
||||||
"client_id": args.client_id,
|
"client_id": config["NETATMO_CLIENT_ID"],
|
||||||
"redirect_uri": args.redirect_uri,
|
"redirect_uri": config["NETATMO_REDIRECT_URI"],
|
||||||
"scope": "read_station",
|
"scope": "read_station",
|
||||||
"state": state,
|
"state": state,
|
||||||
"response_type": "code",
|
"response_type": "code",
|
||||||
@@ -73,23 +69,13 @@ def main() -> int:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
code = authorization_code(input("> "), state)
|
code = authorization_code(input("> "), state)
|
||||||
config = {
|
|
||||||
"NETATMO_CLIENT_ID": args.client_id,
|
|
||||||
"NETATMO_CLIENT_SECRET": args.client_secret,
|
|
||||||
"NETATMO_REFRESH_TOKEN": "",
|
|
||||||
"NETATMO_ACCESS_TOKEN": "",
|
|
||||||
"NETATMO_TOKEN_FILE": args.token_file.expanduser().resolve(),
|
|
||||||
"NETATMO_DEVICE_ID": "",
|
|
||||||
"NETATMO_TOKEN_URL": "https://api.netatmo.com/oauth2/token",
|
|
||||||
"NETATMO_STATIONS_URL": "https://api.netatmo.com/api/getstationsdata",
|
|
||||||
}
|
|
||||||
client = NetatmoClient(config)
|
client = NetatmoClient(config)
|
||||||
client.exchange_code(code, args.redirect_uri)
|
client.exchange_code(code, config["NETATMO_REDIRECT_URI"])
|
||||||
except (EOFError, ValueError, NetatmoError) as exc:
|
except (EOFError, ValueError, NetatmoError) as exc:
|
||||||
print(f"Token setup failed: {exc}", file=sys.stderr)
|
print(f"Token setup failed: {exc}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
print(f"Tokens saved to {args.token_file} with owner-only permissions.")
|
print(f"Tokens saved to {config['NETATMO_TOKEN_FILE']} with owner-only permissions.")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from netatmo_service.config import load_config
|
||||||
|
|
||||||
|
|
||||||
|
def test_yaml_config_is_loaded_and_relative_paths_are_resolved(tmp_path):
|
||||||
|
config_file = tmp_path / "config.yaml"
|
||||||
|
config_file.write_text(
|
||||||
|
"""
|
||||||
|
server:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 12345
|
||||||
|
url_prefix: weather/
|
||||||
|
storage:
|
||||||
|
rrd_folder: data
|
||||||
|
logging:
|
||||||
|
file: logs/service.log
|
||||||
|
collector:
|
||||||
|
enabled: false
|
||||||
|
netatmo:
|
||||||
|
client_id: test-client
|
||||||
|
client_secret: test-secret
|
||||||
|
token_file: secrets/tokens.json
|
||||||
|
modules:
|
||||||
|
study: Office
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
config = load_config(config_file)
|
||||||
|
assert config["HOST"] == "127.0.0.1"
|
||||||
|
assert config["PORT"] == 12345
|
||||||
|
assert config["URL_PREFIX"] == "/weather"
|
||||||
|
assert config["RRD_FOLDER"] == tmp_path / "data"
|
||||||
|
assert config["LOG_FILE"] == tmp_path / "logs/service.log"
|
||||||
|
assert config["NETATMO_TOKEN_FILE"] == tmp_path / "secrets/tokens.json"
|
||||||
|
assert config["START_COLLECTOR"] is False
|
||||||
|
assert config["MODULE_STUDY"] == "Office"
|
||||||
Reference in New Issue
Block a user