included cloud logging with filters
This commit is contained in:
+37
-4
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
@@ -19,6 +20,9 @@ from Crypto.PublicKey import RSA
|
||||
from Crypto.Util.Padding import pad, unpad
|
||||
|
||||
|
||||
LOG = logging.getLogger("home_control.zyxel")
|
||||
|
||||
|
||||
class ZyxelError(RuntimeError):
|
||||
"""The router rejected a request or returned an unexpected response."""
|
||||
|
||||
@@ -50,6 +54,7 @@ class ZyxelRouter:
|
||||
timeout: float = 10,
|
||||
verify_tls: bool = True,
|
||||
hosts_oid: str = "lanhosts",
|
||||
debug: bool = False,
|
||||
) -> None:
|
||||
if "://" not in host:
|
||||
host = f"http://{host}"
|
||||
@@ -62,9 +67,11 @@ class ZyxelRouter:
|
||||
self.timeout = timeout
|
||||
self.verify_tls = verify_tls
|
||||
self.hosts_oid = hosts_oid
|
||||
self.debug = debug
|
||||
self.session = requests.Session()
|
||||
self._aes_key: bytes | None = None
|
||||
self._session_key: str | None = None
|
||||
self._has_logged_connection = False
|
||||
|
||||
def __enter__(self) -> "ZyxelRouter":
|
||||
self.login()
|
||||
@@ -79,6 +86,8 @@ class ZyxelRouter:
|
||||
try:
|
||||
response = self.session.request(method, f"{self.url}{path}", **kwargs)
|
||||
response.raise_for_status()
|
||||
if self.debug:
|
||||
LOG.info("Zyxel HTTP call: %s %s -> %s", method, path, response.status_code)
|
||||
return response
|
||||
except requests.RequestException as error:
|
||||
raise ZyxelError(f"Zyxel request failed: {error}") from error
|
||||
@@ -120,6 +129,12 @@ class ZyxelRouter:
|
||||
if result.get("result") != "ZCFG_SUCCESS" or not result.get("sessionkey"):
|
||||
raise ZyxelError(f"Zyxel login failed: {result.get('result', 'unknown error')}")
|
||||
self._session_key = str(result["sessionkey"])
|
||||
self._log_first_connection()
|
||||
|
||||
def _log_first_connection(self) -> None:
|
||||
if not self._has_logged_connection:
|
||||
LOG.info("Connected successfully to Zyxel router at %s", self.url)
|
||||
self._has_logged_connection = True
|
||||
|
||||
def _decrypt(self, envelope: dict[str, Any]) -> dict[str, Any]:
|
||||
if "content" not in envelope or "iv" not in envelope:
|
||||
@@ -143,7 +158,10 @@ class ZyxelRouter:
|
||||
response = self._request(
|
||||
"GET", "/cgi-bin/DAL", params={"oid": oid, "sessionkey": self._session_key}
|
||||
)
|
||||
return self._decrypt(self._json(response))
|
||||
result = self._decrypt(self._json(response))
|
||||
if self.debug:
|
||||
LOG.info("Zyxel DAL call: oid=%s -> %s", oid, result.get("result", "no result"))
|
||||
return result
|
||||
|
||||
def get_lan_hosts(self) -> list[LanHost]:
|
||||
result = self.dal_get(self.hosts_oid)
|
||||
@@ -184,9 +202,23 @@ class ZyxelRouter:
|
||||
))
|
||||
return hosts
|
||||
|
||||
def connection_states(self, macs: list[str]) -> dict[str, bool]:
|
||||
"""Check several MAC addresses using one router query."""
|
||||
wanted = [normalize_mac(mac) for mac in macs]
|
||||
active_macs = {host.mac for host in self.get_lan_hosts() if host.active}
|
||||
states = {mac: mac in active_macs for mac in wanted}
|
||||
if self.debug:
|
||||
for mac, connected in states.items():
|
||||
LOG.info(
|
||||
"Zyxel presence result: mac=%s -> %s",
|
||||
mac,
|
||||
"connected" if connected else "absent",
|
||||
)
|
||||
return states
|
||||
|
||||
def is_connected(self, mac: str) -> bool:
|
||||
wanted = normalize_mac(mac)
|
||||
return any(host.mac == wanted and host.active for host in self.get_lan_hosts())
|
||||
return self.connection_states([wanted])[wanted]
|
||||
|
||||
def close(self) -> None:
|
||||
if self._session_key:
|
||||
@@ -204,7 +236,7 @@ def _load_cli_config(path: Path) -> tuple[dict[str, Any], str]:
|
||||
with path.open(encoding="utf-8") as stream:
|
||||
root = yaml.safe_load(stream)
|
||||
arrival = root["arrival_detection"]
|
||||
return arrival["zyxel"], str(arrival["device"]["mac"])
|
||||
return arrival["zyxel"], str(arrival["devices"][0]["mac"])
|
||||
except (OSError, TypeError, KeyError, yaml.YAMLError) as error:
|
||||
raise ZyxelError(f"could not load configuration from {path}: {error}") from error
|
||||
|
||||
@@ -221,7 +253,7 @@ def main() -> int:
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mac",
|
||||
help="MAC address to check (default: arrival_detection.device.mac)",
|
||||
help="MAC address to check (default: first arrival_detection.devices entry)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -235,6 +267,7 @@ def main() -> int:
|
||||
timeout=float(config.get("timeout", 10)),
|
||||
verify_tls=bool(config.get("verify_tls", True)),
|
||||
hosts_oid=str(config.get("hosts_oid", "lanhosts")),
|
||||
debug=bool(config.get("debug", False)),
|
||||
)
|
||||
try:
|
||||
connected = router.is_connected(mac)
|
||||
|
||||
Reference in New Issue
Block a user