Backup automatico script del 2026-08-23 12:04

This commit is contained in:
daniele committed 2026-08-23 12:04:16 +02:00
1 parent 11823447a2
commit 795a15a7b6
66 files changed
+8546

No files matched your search

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
"""Client REST verso app homelab LOOGLE."""
@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
"""Client Loogle Casa — dashboard, meteo, rete."""
import os
from typing import Any, Optional
from .session_client import SessionApiClient
MCP_USERS = ("daniele", "lucia", "davide", "luca")
# Daniele MCP → admin su Loogle Casa
MCP_TO_SERVICE_USER = {
"daniele": "admin",
"lucia": "lucia",
"davide": "davide",
"luca": "luca",
}
def _service_username(mcp_username: str) -> str:
return MCP_TO_SERVICE_USER.get(mcp_username.lower(), mcp_username.lower())
PUBLIC_URL = os.environ.get("LOOGLE_CASA_URL", "https://casa.loogle.it").rstrip("/")
API_URL = os.environ.get("LOOGLE_CASA_API_URL", PUBLIC_URL).rstrip("/")
_client: Optional[SessionApiClient] = None
def _client_instance() -> SessionApiClient:
global _client
if _client is None:
verify = os.environ.get("LOOGLE_CASA_VERIFY_SSL", "true").strip().lower() not in (
"0", "false", "no", "off",
)
client = SessionApiClient(
service="LOOGLE_CASA",
base_url=API_URL,
verify_ssl=verify,
)
client.map_username = _service_username # type: ignore[attr-defined]
_client = client
return _client
def is_configured(username: Optional[str] = None) -> bool:
user = (username or "daniele").lower()
if user in MCP_USERS:
key = f"LOOGLE_CASA_PASSWORD_{user.upper()}"
if os.environ.get(key, "").strip():
return True
if os.environ.get("LOOGLE_CASA_PASSWORD", "").strip():
return True
return user in MCP_USERS
def get_dashboard(username: str) -> dict:
return _client_instance().get("/api/dashboard", username=username)
def get_weather_home(username: str) -> dict:
return _client_instance().get("/api/weather/home", username=username)
def get_network_overview(username: str) -> dict:
return _client_instance().get("/api/network/overview", username=username)
def get_network_failover_status(username: str) -> dict:
return _client_instance().get("/api/network/failover/status", username=username)
def get_network_mcp_status(username: str) -> dict:
return _client_instance().get("/api/network/mcp", username=username)
def get_alerts_cards(username: str) -> Any:
return _client_instance().get("/api/alerts/cards", username=username)
@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
"""Client Home Assistant REST API (read-only)."""
import os
from typing import Any, Optional
from urllib.parse import urljoin
import httpx
PUBLIC_URL = os.environ.get("HA_URL", "https://ha.loogle.it").rstrip("/")
API_URL = os.environ.get("HA_API_URL", PUBLIC_URL).rstrip("/")
HA_TOKEN = os.environ.get("HA_TOKEN", "").strip()
def is_configured() -> bool:
return bool(HA_TOKEN)
def _headers() -> dict:
if not HA_TOKEN:
raise RuntimeError(
"HA_TOKEN non configurato in .env — crea un long-lived token in Home Assistant"
)
return {"Authorization": f"Bearer {HA_TOKEN}", "Content-Type": "application/json"}
def _request(method: str, path: str, *, params: Optional[dict] = None) -> Any:
verify = os.environ.get("HA_VERIFY_SSL", "true").strip().lower() not in (
"0", "false", "no", "off",
)
url = path if path.startswith("http") else urljoin(API_URL + "/", path.lstrip("/"))
with httpx.Client(timeout=30.0, verify=verify) as client:
resp = client.request(method, url, headers=_headers(), params=params)
if resp.status_code >= 400:
raise RuntimeError(f"Home Assistant {path}: HTTP {resp.status_code} {resp.text[:200]}")
return resp.json()
def get_config() -> dict:
return _request("GET", "/api/config")
def get_entity(entity_id: str) -> dict:
return _request("GET", f"/api/states/{entity_id}")
def list_entities(domain: Optional[str] = None, limit: int = 100) -> list:
states = _request("GET", "/api/states")
if domain:
prefix = domain if domain.endswith(".") else f"{domain}."
states = [s for s in states if s.get("entity_id", "").startswith(prefix)]
return states[:limit]
def search_entities(query: str, limit: int = 30) -> list:
q = query.lower()
matches = []
for state in _request("GET", "/api/states"):
eid = state.get("entity_id", "")
name = (state.get("attributes") or {}).get("friendly_name", "")
blob = f"{eid} {name}".lower()
if q in blob:
matches.append({
"entity_id": eid,
"state": state.get("state"),
"friendly_name": name,
"last_changed": state.get("last_changed"),
})
if len(matches) >= limit:
break
return matches
@@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
"""Client Irrigazione Smart — irri.loogle.it."""
import os
from typing import Any, Optional
from .session_client import SessionApiClient
MCP_USERS = ("daniele", "lucia", "davide", "luca")
MCP_TO_SERVICE_USER = {
"daniele": "admin",
"lucia": "lucia",
"davide": "dado",
"luca": "luca",
}
PUBLIC_URL = os.environ.get("IRRIGAZIONE_URL", "https://irri.loogle.it").rstrip("/")
API_URL = os.environ.get("IRRIGAZIONE_API_URL", PUBLIC_URL).rstrip("/")
_client: Optional[SessionApiClient] = None
def _service_username(mcp_username: str) -> str:
return MCP_TO_SERVICE_USER.get(mcp_username.lower(), mcp_username.lower())
def _client_instance() -> SessionApiClient:
global _client
if _client is None:
verify = os.environ.get("IRRIGAZIONE_VERIFY_SSL", "true").strip().lower() not in (
"0", "false", "no", "off",
)
client = SessionApiClient(
service="IRRIGAZIONE",
base_url=API_URL,
verify_ssl=verify,
)
client.map_username = _service_username # type: ignore[attr-defined]
_client = client
return _client
def is_configured(username: Optional[str] = None) -> bool:
user = (username or "daniele").lower()
if os.environ.get(f"IRRIGAZIONE_PASSWORD_{user.upper()}", "").strip():
return True
if os.environ.get("IRRIGAZIONE_PASSWORD", "").strip():
return True
return user in MCP_USERS
def get_status(username: str) -> dict:
return _client_instance().get("/api/status", username=username)
def get_zones(username: str) -> Any:
return _client_instance().get("/api/zones", username=username)
def get_history(username: str, limit: int = 30) -> Any:
data = _client_instance().get("/api/history", username=username)
if isinstance(data, list):
return data[:limit]
if isinstance(data, dict) and "items" in data:
items = data["items"]
return items[:limit] if isinstance(items, list) else data
return data
def get_events(username: str, limit: int = 50) -> Any:
data = _client_instance().get("/api/events", username=username)
if isinstance(data, list):
return data[:limit]
return data
def get_lavori_summary(username: str) -> Any:
return _client_instance().get("/api/lavori/summary", username=username)
@@ -0,0 +1,122 @@
# -*- coding: utf-8 -*-
"""Client HTTP con sessione cookie (Loogle Casa, Irrigazione)."""
import logging
import threading
import time
from typing import Any, Optional
from urllib.parse import urljoin
import httpx
LOGGER = logging.getLogger("loogle_mcp.session_client")
_sessions: dict[str, tuple[str, float]] = {}
_sessions_lock = threading.Lock()
SESSION_TTL = 3600 * 12
class SessionApiClient:
"""Login cookie-based con cache per utente MCP."""
def __init__(
self,
*,
service: str,
base_url: str,
login_path: str = "/api/login",
verify_ssl: bool = True,
) -> None:
self.service = service
self.base_url = base_url.rstrip("/")
self.login_path = login_path
self.verify_ssl = verify_ssl
def _password_for_user(self, username: str) -> Optional[str]:
import os
user = username.lower()
env_key = f"{self.service}_PASSWORD_{user.upper()}"
pwd = os.environ.get(env_key, "").strip()
if pwd:
return pwd
fallback = os.environ.get(f"{self.service}_PASSWORD", "").strip()
if fallback:
return fallback
return user
def _cache_key(self, username: str) -> str:
return f"{self.service}:{username.lower()}"
def _get_cached_cookie(self, username: str) -> Optional[str]:
key = self._cache_key(username)
with _sessions_lock:
row = _sessions.get(key)
if not row:
return None
cookie, expires = row
if time.time() > expires:
_sessions.pop(key, None)
return None
return cookie
def _store_cookie(self, username: str, cookie: str) -> None:
key = self._cache_key(username)
with _sessions_lock:
_sessions[key] = (cookie, time.time() + SESSION_TTL)
def login(self, username: str) -> str:
cached = self._get_cached_cookie(username)
if cached:
return cached
service_user = username
if hasattr(self, "map_username"):
service_user = self.map_username(username) # type: ignore[attr-defined]
password = self._password_for_user(username)
if not password:
raise RuntimeError(
f"Password {self.service} non configurata per {username}. "
f"Imposta {self.service}_PASSWORD_{username.upper()} in .env"
)
url = urljoin(self.base_url + "/", self.login_path.lstrip("/"))
with httpx.Client(timeout=30.0, verify=self.verify_ssl) as client:
resp = client.post(
url, json={"username": service_user, "password": password},
)
if resp.status_code != 200:
raise RuntimeError(
f"Login {self.service} fallito per {username}: HTTP {resp.status_code}"
)
cookie = resp.cookies.get("session")
if not cookie:
raise RuntimeError(f"Login {self.service}: cookie session mancante")
self._store_cookie(username, cookie)
return cookie
def request(
self,
method: str,
path: str,
*,
username: str,
params: Optional[dict] = None,
json_body: Optional[dict] = None,
) -> Any:
cookie = self.login(username)
url = path if path.startswith("http") else urljoin(self.base_url + "/", path.lstrip("/"))
headers = {"Cookie": f"session={cookie}"}
with httpx.Client(timeout=60.0, verify=self.verify_ssl) as client:
resp = client.request(method, url, headers=headers, params=params, json=json_body)
if resp.status_code == 401:
with _sessions_lock:
_sessions.pop(self._cache_key(username), None)
cookie = self.login(username)
headers = {"Cookie": f"session={cookie}"}
resp = client.request(method, url, headers=headers, params=params, json=json_body)
if resp.status_code >= 400:
raise RuntimeError(f"{self.service} {method} {path}: HTTP {resp.status_code} {resp.text[:200]}")
if resp.headers.get("content-type", "").startswith("application/json"):
return resp.json()
return resp.text
def get(self, path: str, *, username: str, params: Optional[dict] = None) -> Any:
return self.request("GET", path, username=username, params=params)
@@ -0,0 +1,208 @@
# -*- coding: utf-8 -*-
"""Client Turni-Live — turni.loogle.it (JWT Bearer)."""
import logging
import os
import threading
import time
from typing import Any, Optional
from urllib.parse import urljoin
import httpx
LOGGER = logging.getLogger("loogle_mcp.turni")
MCP_USERS = ("daniele", "lucia", "davide", "luca")
MCP_TO_SERVICE_USER = {
"daniele": "daniely",
"lucia": "lucia",
"davide": "davide",
"luca": "luca",
}
PUBLIC_URL = os.environ.get("TURNI_URL", "https://turni.loogle.it").rstrip("/")
API_URL = os.environ.get("TURNI_API_URL", PUBLIC_URL).rstrip("/")
_jwt_cache: dict[str, tuple[str, float]] = {}
_jwt_lock = threading.Lock()
JWT_TTL = 3600 * 6
def _service_username(mcp_username: str) -> str:
return MCP_TO_SERVICE_USER.get(mcp_username.lower(), mcp_username.lower())
def _password_for_user(mcp_username: str) -> Optional[str]:
user = mcp_username.lower()
pwd = os.environ.get(f"TURNI_PASSWORD_{user.upper()}", "").strip()
if pwd:
return pwd
return os.environ.get("TURNI_PASSWORD", "").strip() or None
def _jwt_for_user(mcp_username: str) -> Optional[str]:
user = mcp_username.lower()
direct = os.environ.get(f"TURNI_JWT_{user.upper()}", "").strip()
if direct:
return direct
return os.environ.get("TURNI_JWT", "").strip() or None
def is_configured(username: Optional[str] = None) -> bool:
user = (username or "daniele").lower()
if _jwt_for_user(user):
return True
if _password_for_user(user):
return True
return False
def _store_jwt(mcp_username: str, token: str) -> None:
with _jwt_lock:
_jwt_cache[mcp_username.lower()] = (token, time.time() + JWT_TTL)
def _cached_jwt(mcp_username: str) -> Optional[str]:
with _jwt_lock:
row = _jwt_cache.get(mcp_username.lower())
if not row:
return None
token, expires = row
if time.time() > expires:
_jwt_cache.pop(mcp_username.lower(), None)
return None
return token
def login(mcp_username: str) -> str:
cached = _cached_jwt(mcp_username)
if cached:
return cached
preset = _jwt_for_user(mcp_username)
if preset:
_store_jwt(mcp_username, preset)
return preset
password = _password_for_user(mcp_username)
if not password:
raise RuntimeError(
f"Turni non configurato per {mcp_username}. "
f"Imposta TURNI_PASSWORD_{mcp_username.upper()} o TURNI_JWT_{mcp_username.upper()}"
)
service_user = _service_username(mcp_username)
verify = os.environ.get("TURNI_VERIFY_SSL", "true").strip().lower() not in (
"0", "false", "no", "off",
)
url = urljoin(API_URL + "/", "api/auth/login")
with httpx.Client(timeout=30.0, verify=verify) as client:
resp = client.post(url, json={"username": service_user, "password": password})
if resp.status_code != 200:
raise RuntimeError(f"Login Turni fallito: HTTP {resp.status_code}")
data = resp.json()
token = data.get("token")
if not token:
raise RuntimeError("Login Turni: token JWT mancante")
_store_jwt(mcp_username, token)
return token
def _request(
method: str,
path: str,
*,
mcp_username: str,
params: Optional[dict] = None,
) -> Any:
token = login(mcp_username)
verify = os.environ.get("TURNI_VERIFY_SSL", "true").strip().lower() not in (
"0", "false", "no", "off",
)
url = path if path.startswith("http") else urljoin(API_URL + "/", path.lstrip("/"))
headers = {"Authorization": f"Bearer {token}"}
with httpx.Client(timeout=60.0, verify=verify) as client:
resp = client.request(method, url, headers=headers, params=params)
if resp.status_code == 401:
with _jwt_lock:
_jwt_cache.pop(mcp_username.lower(), None)
headers["Authorization"] = f"Bearer {login(mcp_username)}"
resp = client.request(method, url, headers=headers, params=params)
if resp.status_code >= 400:
raise RuntimeError(f"Turni {path}: HTTP {resp.status_code} {resp.text[:200]}")
return resp.json()
def get_status() -> dict:
verify = os.environ.get("TURNI_VERIFY_SSL", "true").strip().lower() not in (
"0", "false", "no", "off",
)
url = urljoin(API_URL + "/", "api/status")
with httpx.Client(timeout=30.0, verify=verify) as client:
resp = client.get(url)
resp.raise_for_status()
return resp.json()
def list_doctors(mcp_username: str) -> Any:
return _request("GET", "/api/doctors", mcp_username=mcp_username)
def get_shift_assignments(
mcp_username: str,
*,
from_date: Optional[str] = None,
to_date: Optional[str] = None,
limit: int = 100,
) -> Any:
params: dict = {}
if from_date:
params["from"] = from_date
if to_date:
params["to"] = to_date
data = _request("GET", "/api/shift-assignments", mcp_username=mcp_username, params=params or None)
if isinstance(data, list):
return data[:limit]
if isinstance(data, dict):
items = data.get("assignments") or data.get("items") or data.get("results")
if isinstance(items, list):
return items[:limit]
return data
def get_my_shifts(
mcp_username: str,
*,
from_date: Optional[str] = None,
to_date: Optional[str] = None,
limit: int = 50,
) -> dict:
"""Turni dell'utente MCP: filtra per doctorId collegato o per nome medico."""
user_info = _request("GET", "/api/users/me", mcp_username=mcp_username)
doctor_id = user_info.get("doctorId")
assignments = get_shift_assignments(
mcp_username, from_date=from_date, to_date=to_date, limit=500,
)
if not isinstance(assignments, list):
return {"user": user_info, "assignments": assignments}
if doctor_id:
mine = [a for a in assignments if a.get("doctorId") == doctor_id or a.get("doctor_id") == doctor_id]
else:
service_user = _service_username(mcp_username)
doctors = list_doctors(mcp_username)
doc_ids = set()
if isinstance(doctors, list):
for doc in doctors:
name = (doc.get("name") or doc.get("fullName") or "").lower()
if service_user.lower() in name or mcp_username.lower() in name:
doc_ids.add(doc.get("id") or doc.get("doctorId"))
mine = [
a for a in assignments
if (a.get("doctorId") or a.get("doctor_id")) in doc_ids
] if doc_ids else assignments[:limit]
return {
"user": {
"username": user_info.get("username"),
"role": user_info.get("role"),
"doctorId": doctor_id,
},
"assignments": mine[:limit],
"count": len(mine),
}