Backup automatico script del 2026-08-23 12:04
This commit is contained in:
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),
|
||||
}
|
||||
Reference in new issue
Block a user