Files
pe1hvh dad42503f4 feat(repeater): poll repeater statistics on a configurable interval
feat(repeater): poll repeater statistics on a configurable interval

Configured repeaters are logged into, queried for their status and
logged out again on a configurable interval. Raw values are archived
with the moment of each poll, so battery voltage can be tracked over
multiple days.

New modules
- services/repeater_config_store.py — per-device configuration in
  ~/.meshcore-gui/repeaters/<device>_repeaters.json (directory 0700,
  file 0600). Holds the full 32-byte public key, display name, login
  password, poll interval and enabled flag. A key that is not 64 hex
  characters is rejected: the meshcore library refuses a prefix for
  login, status and logout requests. An absent password means "not
  configured", an empty string is a valid credential.
- services/repeater_stats_archive.py — append-only JSONL at
  ~/.meshcore-gui/archive/<device>_repeater_stats.jsonl, one record per
  poll, retention through the existing daily cleanup task.
- services/repeater_poller.py — send_login_sync, req_status_sync,
  send_logout. The sync variants are used because plain send_login and
  send_statusreq return as soon as the frame is on the wire and say
  nothing about the outcome.
- gui/panels/repeater_stats_panel.py — read-only REPEATERS panel showing
  the latest values, the age of the last successful poll and the last
  error per repeater.

Changed
- config.py: VERSION 1.23.0, REPEATERS_DIR, REPEATER_POLL_ENABLED,
  REPEATER_POLL_INTERVAL (900 s), REPEATER_POLL_CHECK_INTERVAL,
  REPEATER_LOGIN_TIMEOUT, REPEATER_STATUS_TIMEOUT,
  REPEATER_STATS_RETENTION_DAYS.
- ble/worker.py: _main_loop runs the repeater poll alongside the existing
  periodic tasks, following the same interval-guarded pattern;
  _cleanup_old_data applies retention to the new archive.
- __main__.py, gui/dashboard.py, gui/panels/__init__.py: wiring.

Behaviour
- At most one repeater is polled per check, each with its own start
  offset within the interval, so two repeaters are never queried back to
  back. Two repeaters on the default interval land ~7.5 minutes apart.
- A failed login, an absent status response or an exception is recorded
  as a poll attempt with ok=false and a reason, and blocks neither the
  other repeater nor the application. Logout is always sent once a login
  has been attempted, including after a timeout — a login that returned
  no confirmation may still have succeeded on the far side.
- Values are stored exactly as reported: no scaling, rounding or
  averaging. Battery voltage keeps the unit the firmware returns.
- The password is reachable through a single method that only the poller
  calls. Objects handed to the GUI have no password field, and no
  password reaches the archive, the log or any error message.
- Polling stays idle until a repeater is configured, so an installation
  without a configuration file behaves exactly as before.

No change to the archive JSON schema, the public REST API or any
existing public method signature. Worker and dashboard constructors
gained optional keyword arguments only.
2026-09-01 03:32:40 +02:00

56 lines
1.7 KiB
Python

#!/usr/bin/env python3
"""Direct port of the PHP fetchWeatherData() method.
Behaviour matches the original: same URL construction, same disabled TLS
verification, prints the URL and the decoded payload, and returns the raw
response body (or False on error). The API key is passed in as a parameter.
"""
import json
import ssl
import sys
import urllib.error
import urllib.request
def fetch_weather_data(locatie, datum, api_key):
"""Haal de weerdata op van de API.
:param locatie: location query (lat,lon or place name)
:param datum: date as YYYY-MM-DD
:param api_key: WeatherAPI key
:return: raw response body as str, or False on error
"""
url = f"https://api.weatherapi.com/v1/history.json?q={locatie}&dt={datum}&key={api_key}"
print(f"{url}")
# CURLOPT_SSL_VERIFYPEER = false / CURLOPT_SSL_VERIFYHOST = 0
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
request = urllib.request.Request(url, headers={"accept": "application/json"})
try:
with urllib.request.urlopen(request, context=context) as handle:
response = handle.read().decode("utf-8")
except urllib.error.URLError as exc:
print(f"cURL Error: {exc}")
return False
# JSON netjes weergeven
try:
data = json.loads(response)
print(json.dumps(data, indent=4, ensure_ascii=False))
except json.JSONDecodeError as exc:
print(f"JSON Error: {exc}")
return response
if __name__ == "__main__":
if len(sys.argv) != 4:
print(f"usage: {sys.argv[0]} <locatie> <datum> <api_key>")
sys.exit(1)
fetch_weather_data(sys.argv[1], sys.argv[2], sys.argv[3])