diff --git a/Boiler-Tuya-Energy.md b/Boiler-Tuya-Energy.md
new file mode 100644
index 0000000..a9ab866
--- /dev/null
+++ b/Boiler-Tuya-Energy.md
@@ -0,0 +1,9 @@
+# Boiler Tuya Energy — casa Dublin
+
+Projeto **dedicado** (nao e o Tapo R630).
+
+- Pasta Obsidian: `Boiler-Tuya-Energy/`
+- App: https://boiler.myvexx.com/
+- R630 (Tapo, separado): https://r630.myvexx.com/
+- Live: BIG CT120 `/opt/boiler-energy` :8788
+- Token Cloudflare zona myvexx: `Infra-Network/SECRETS_CLOUDFLARE_MYVEXX.md`
\ No newline at end of file
diff --git a/Boiler-Tuya-Energy/.gitignore b/Boiler-Tuya-Energy/.gitignore
new file mode 100644
index 0000000..6f13bf7
--- /dev/null
+++ b/Boiler-Tuya-Energy/.gitignore
@@ -0,0 +1,5 @@
+data/
+*.db
+boiler.env
+site-packages/
+__pycache__/
diff --git a/Boiler-Tuya-Energy/Dockerfile b/Boiler-Tuya-Energy/Dockerfile
new file mode 100644
index 0000000..8c72e85
--- /dev/null
+++ b/Boiler-Tuya-Energy/Dockerfile
@@ -0,0 +1,8 @@
+FROM python:3.11-slim
+WORKDIR /app
+RUN pip install --no-cache-dir aiohttp==3.12.15
+COPY tuya_boiler_server.py /app/tuya_boiler_server.py
+RUN useradd --system --uid 10001 boiler && mkdir /data && chown boiler:boiler /data
+USER boiler
+EXPOSE 8788
+CMD ["python", "/app/tuya_boiler_server.py"]
diff --git a/Boiler-Tuya-Energy/PROJECT.md b/Boiler-Tuya-Energy/PROJECT.md
new file mode 100644
index 0000000..547e6d8
--- /dev/null
+++ b/Boiler-Tuya-Energy/PROJECT.md
@@ -0,0 +1,46 @@
+# PROJECT — Boiler Tuya Energy (Dublin)
+
+Dashboard **somente leitura** do interruptor Tuya do boiler. Consome logs ON/OFF da nuvem Tuya, grava SQLite e estima kWh/€. **Não mede watts** (o hardware CB01-SBL não tem sensor). **Não liga/desliga** o relé.
+
+**Atualizado:** 2026-09-08
+**URL:** https://boiler.myvexx.com/
+**Fallback:** https://r630.myvexx.com/boiler/
+**Separado do R630:** Tapo P110 continua em https://r630.myvexx.com/ (VM500 :8787)
+
+## O que o sistema faz
+
+- Sincroniza `switch_1` (status + logs) da Tuya Cloud EU
+- Guarda eventos em SQLite (`switch_events`)
+- Relatório por intervalo de datas (kWh e €)
+- Export CSV
+- Tarifa Electric Ireland ValueReward 4%: **€0,3652/kWh** (fatura 23 Jul 2026, 0,3490 ex. IVA × 0,96 × 1,09)
+- Potência assumida da resistência: **2,3 kW**
+- Poll a cada 5 minutos
+
+## Fórmula
+
+`kWh = 2,3 kW × (minutos ON / 60)`
+`€ = kWh × 0,3652`
+Termóstato interno do boiler pode reduzir o consumo real.
+
+## Stack / onde corre
+
+| Item | Valor |
+|---|---|
+| Host | BIG CT **120** `web-traefik` |
+| Pasta live | `/opt/boiler-energy` |
+| Serviço | `boiler-energy.service` (user `boiler`) |
+| Porta | **8788** |
+| Traefik | `Host(boiler.myvexx.com)` → `http://127.0.0.1:8788` |
+| Túnel | `7233d455-…` **config remota** Cloudflare (`config_src: cloudflare`) |
+| Device Tuya | `bfaf766aa915197295qju6` · CB01-SBL · categoria `kg` |
+| DB | `/opt/boiler-energy/data/boiler.db` (não versionar) |
+
+Credenciais Tuya: `boiler.env` no CT120 (não no Git). Token DNS: `Infra-Network/SECRETS_CLOUDFLARE_MYVEXX.md`.
+
+## Fora de escopo
+
+- Medição real de corrente/tensão neste interruptor
+- Controlo ON/OFF pelo dashboard
+- Alterar `/etc/network/interfaces` do Proxmox
+- Misturar com o Tapo do R630
\ No newline at end of file
diff --git a/Boiler-Tuya-Energy/README.md b/Boiler-Tuya-Energy/README.md
new file mode 100644
index 0000000..0e9f27e
--- /dev/null
+++ b/Boiler-Tuya-Energy/README.md
@@ -0,0 +1,10 @@
+# Boiler Tuya Energy
+
+Projeto dedicado (não é o dashboard Tapo do R630).
+
+- Pasta Obsidian: `Boiler-Tuya-Energy/`
+- Live: CT120 `/opt/boiler-energy`
+- Public: https://boiler.myvexx.com/
+- Spec/ops: [PROJECT.md](PROJECT.md)
+
+Código neste folder: `tuya_boiler_server.py`, `boiler-energy.service`, `boiler.env.example`.
\ No newline at end of file
diff --git a/Boiler-Tuya-Energy/boiler-energy.service b/Boiler-Tuya-Energy/boiler-energy.service
new file mode 100644
index 0000000..7e9417a
--- /dev/null
+++ b/Boiler-Tuya-Energy/boiler-energy.service
@@ -0,0 +1,23 @@
+[Unit]
+Description=Boiler Tuya estimated energy dashboard
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+Type=simple
+User=boiler
+Group=boiler
+WorkingDirectory=/opt/boiler-energy
+EnvironmentFile=/opt/boiler-energy/boiler.env
+Environment=PYTHONPATH=/opt/boiler-energy/site-packages
+ExecStart=/usr/bin/python3 /opt/boiler-energy/tuya_boiler_server.py
+Restart=always
+RestartSec=5
+NoNewPrivileges=true
+PrivateTmp=true
+ProtectSystem=strict
+ProtectHome=true
+ReadWritePaths=/opt/boiler-energy/data
+
+[Install]
+WantedBy=multi-user.target
diff --git a/Boiler-Tuya-Energy/boiler.env.example b/Boiler-Tuya-Energy/boiler.env.example
new file mode 100644
index 0000000..261cbe5
--- /dev/null
+++ b/Boiler-Tuya-Energy/boiler.env.example
@@ -0,0 +1,10 @@
+TUYA_ACCESS_ID=
+TUYA_ACCESS_KEY=
+TUYA_API_ENDPOINT=https://openapi.tuyaeu.com
+TUYA_DEVICE_ID=bfaf766aa915197295qju6
+BOILER_POWER_KW=2.3
+BOILER_RATE_EUR_KWH=0.3652
+BOILER_TIMEZONE=Europe/Dublin
+BOILER_PORT=8788
+BOILER_DB_PATH=/opt/boiler-energy/data/boiler.db
+BOILER_POLL_SECONDS=300
\ No newline at end of file
diff --git a/Boiler-Tuya-Energy/docker-compose.yml b/Boiler-Tuya-Energy/docker-compose.yml
new file mode 100644
index 0000000..231c5da
--- /dev/null
+++ b/Boiler-Tuya-Energy/docker-compose.yml
@@ -0,0 +1,17 @@
+services:
+ boiler-energy:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ container_name: boiler-energy
+ restart: unless-stopped
+ env_file:
+ - boiler.env
+ network_mode: host
+ read_only: true
+ tmpfs:
+ - /tmp
+ volumes:
+ - ./data:/data
+ security_opt:
+ - no-new-privileges:true
diff --git a/Boiler-Tuya-Energy/tuya_boiler_server.py b/Boiler-Tuya-Energy/tuya_boiler_server.py
new file mode 100644
index 0000000..3e573bb
--- /dev/null
+++ b/Boiler-Tuya-Energy/tuya_boiler_server.py
@@ -0,0 +1,463 @@
+#!/usr/bin/env python3
+"""Dashboard read-only do boiler Tuya com histórico SQLite e consumo estimado."""
+from __future__ import annotations
+
+import asyncio
+import csv
+import hashlib
+import hmac
+import io
+import json
+import os
+import sqlite3
+import time
+import urllib.parse
+import urllib.request
+from datetime import date, datetime, time as dt_time, timedelta, timezone
+from pathlib import Path
+from zoneinfo import ZoneInfo
+
+from aiohttp import web
+
+
+DEVICE_ID = os.environ["TUYA_DEVICE_ID"]
+ACCESS_ID = os.environ["TUYA_ACCESS_ID"]
+ACCESS_KEY = os.environ["TUYA_ACCESS_KEY"]
+ENDPOINT = os.getenv("TUYA_API_ENDPOINT", "https://openapi.tuyaeu.com")
+DB_PATH = Path(os.getenv("BOILER_DB_PATH", "/home/admin/boiler-energy/data/boiler.db"))
+PORT = int(os.getenv("BOILER_PORT", "8788"))
+POWER_KW = float(os.getenv("BOILER_POWER_KW", "2.3"))
+UNIT_RATE = float(os.getenv("BOILER_RATE_EUR_KWH", "0.3652"))
+POLL_SECONDS = int(os.getenv("BOILER_POLL_SECONDS", "300"))
+TZ = ZoneInfo(os.getenv("BOILER_TIMEZONE", "Europe/Dublin"))
+
+
+class TuyaOpenAPI:
+ """Minimal Tuya REST client; avoids the unused Pulsar/Crypto dependency."""
+
+ def __init__(self, endpoint: str, access_id: str, access_key: str) -> None:
+ self.endpoint = endpoint.rstrip("/")
+ self.access_id = access_id
+ self.access_key = access_key
+ self.token = ""
+ self.token_expires_ms = 0
+
+ def _request(self, method: str, path: str, params: dict | None = None) -> dict:
+ params = params or {}
+ canonical_query = "&".join(f"{key}={params[key]}" for key in sorted(params))
+ canonical_path = path + (f"?{canonical_query}" if canonical_query else "")
+ body_hash = hashlib.sha256(b"").hexdigest()
+ string_to_sign = f"{method}\n{body_hash}\n\n{canonical_path}"
+ now_ms = int(time.time() * 1000)
+ message = f"{self.access_id}{self.token}{now_ms}{string_to_sign}"
+ signature = hmac.new(
+ self.access_key.encode(), message.encode(), hashlib.sha256
+ ).hexdigest().upper()
+ query = urllib.parse.urlencode(sorted(params.items()))
+ url = self.endpoint + path + (f"?{query}" if query else "")
+ request = urllib.request.Request(
+ url,
+ method=method,
+ headers={
+ "client_id": self.access_id,
+ "sign": signature,
+ "sign_method": "HMAC-SHA256",
+ "access_token": self.token,
+ "t": str(now_ms),
+ "lang": "en",
+ },
+ )
+ with urllib.request.urlopen(request, timeout=20) as response:
+ return json.loads(response.read())
+
+ def connect(self) -> dict:
+ self.token = ""
+ result = self._request("GET", "/v1.0/token", {"grant_type": 1})
+ if result.get("success"):
+ payload = result.get("result", {})
+ self.token = payload.get("access_token", "")
+ self.token_expires_ms = int(time.time() * 1000) + int(
+ payload.get("expire_time", 7200)
+ ) * 1000
+ return result
+
+ def get(self, path: str, params: dict | None = None) -> dict:
+ if not self.token or self.token_expires_ms - 60000 < int(time.time() * 1000):
+ connected = self.connect()
+ if not connected.get("success"):
+ return connected
+ return self._request("GET", path, params)
+
+
+def iso_local(ms: int) -> str:
+ return datetime.fromtimestamp(ms / 1000, timezone.utc).astimezone(TZ).isoformat()
+
+
+class Store:
+ def __init__(self) -> None:
+ DB_PATH.parent.mkdir(parents=True, exist_ok=True)
+ self.db = sqlite3.connect(DB_PATH)
+ self.db.row_factory = sqlite3.Row
+ self.db.execute("PRAGMA journal_mode=WAL")
+ self.db.executescript(
+ """
+ CREATE TABLE IF NOT EXISTS switch_events (
+ event_time_ms INTEGER NOT NULL,
+ value INTEGER NOT NULL CHECK(value IN (0,1)),
+ event_from TEXT NOT NULL DEFAULT '',
+ received_at TEXT NOT NULL,
+ PRIMARY KEY(event_time_ms, value, event_from)
+ );
+ CREATE INDEX IF NOT EXISTS idx_switch_events_time
+ ON switch_events(event_time_ms);
+ CREATE TABLE IF NOT EXISTS sync_runs (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ started_at TEXT NOT NULL,
+ finished_at TEXT,
+ ok INTEGER NOT NULL DEFAULT 0,
+ fetched INTEGER NOT NULL DEFAULT 0,
+ inserted INTEGER NOT NULL DEFAULT 0,
+ error TEXT
+ );
+ CREATE TABLE IF NOT EXISTS settings (
+ key TEXT PRIMARY KEY,
+ value TEXT NOT NULL
+ );
+ """
+ )
+ self.db.commit()
+
+ def add_events(self, logs: list[dict]) -> int:
+ before = self.db.total_changes
+ now = datetime.now(timezone.utc).isoformat()
+ for item in logs:
+ if item.get("code") != "switch_1":
+ continue
+ raw = str(item.get("value", "")).lower()
+ if raw not in {"true", "false", "1", "0", "on", "off"}:
+ continue
+ value = 1 if raw in {"true", "1", "on"} else 0
+ self.db.execute(
+ "INSERT OR IGNORE INTO switch_events VALUES (?,?,?,?)",
+ (int(item["event_time"]), value, str(item.get("event_from", "")), now),
+ )
+ self.db.commit()
+ return self.db.total_changes - before
+
+ def normalized_events(self, start_ms: int | None = None, end_ms: int | None = None):
+ # Include the last event before the range to establish initial state.
+ rows: list[sqlite3.Row] = []
+ if start_ms is not None:
+ previous = self.db.execute(
+ "SELECT * FROM switch_events WHERE event_time_ms < ? "
+ "ORDER BY event_time_ms DESC LIMIT 1",
+ (start_ms,),
+ ).fetchone()
+ if previous:
+ rows.append(previous)
+ query = "SELECT * FROM switch_events WHERE 1=1"
+ args: list[int] = []
+ if start_ms is not None:
+ query += " AND event_time_ms >= ?"
+ args.append(start_ms)
+ if end_ms is not None:
+ query += " AND event_time_ms < ?"
+ args.append(end_ms)
+ query += " ORDER BY event_time_ms, event_from"
+ rows.extend(self.db.execute(query, args).fetchall())
+
+ result: list[dict] = []
+ for row in rows:
+ event = {"ms": row["event_time_ms"], "on": bool(row["value"])}
+ # Tuya often emits the same transition twice from cloud/device.
+ if result and event["on"] == result[-1]["on"]:
+ continue
+ result.append(event)
+ return result
+
+ def report(self, start_day: date, end_day: date) -> dict:
+ start = datetime.combine(start_day, dt_time.min, TZ)
+ end = datetime.combine(end_day + timedelta(days=1), dt_time.min, TZ)
+ start_ms = int(start.timestamp() * 1000)
+ end_ms = int(end.timestamp() * 1000)
+ events = self.normalized_events(start_ms, end_ms)
+
+ intervals: list[tuple[datetime, datetime]] = []
+ on_at: int | None = None
+ for event in events:
+ if event["on"] and on_at is None:
+ on_at = max(event["ms"], start_ms)
+ elif not event["on"] and on_at is not None:
+ stop = min(event["ms"], end_ms)
+ if stop > on_at:
+ intervals.append(
+ (
+ datetime.fromtimestamp(on_at / 1000, timezone.utc).astimezone(TZ),
+ datetime.fromtimestamp(stop / 1000, timezone.utc).astimezone(TZ),
+ )
+ )
+ on_at = None
+ if on_at is not None:
+ stop_ms = min(int(time.time() * 1000), end_ms)
+ if stop_ms > on_at:
+ intervals.append(
+ (
+ datetime.fromtimestamp(on_at / 1000, timezone.utc).astimezone(TZ),
+ datetime.fromtimestamp(stop_ms / 1000, timezone.utc).astimezone(TZ),
+ )
+ )
+
+ daily: dict[date, dict] = {}
+ cursor = start_day
+ while cursor <= end_day:
+ daily[cursor] = {"date": cursor.isoformat(), "seconds": 0.0, "starts": 0}
+ cursor += timedelta(days=1)
+
+ for begin, finish in intervals:
+ daily[begin.date()]["starts"] += 1
+ point = begin
+ while point < finish:
+ midnight = datetime.combine(point.date() + timedelta(days=1), dt_time.min, TZ)
+ segment_end = min(finish, midnight)
+ if point.date() in daily:
+ daily[point.date()]["seconds"] += (segment_end - point).total_seconds()
+ point = segment_end
+
+ rows = []
+ for item in daily.values():
+ hours = item.pop("seconds") / 3600
+ kwh = hours * POWER_KW
+ rows.append(
+ {
+ **item,
+ "runtime_min": round(hours * 60, 1),
+ "kwh": round(kwh, 3),
+ "cost_eur": round(kwh * UNIT_RATE, 2),
+ }
+ )
+ total_min = sum(row["runtime_min"] for row in rows)
+ total_kwh = sum(row["kwh"] for row in rows)
+ return {
+ "ok": True,
+ "from": start_day.isoformat(),
+ "to": end_day.isoformat(),
+ "power_kw": POWER_KW,
+ "rate_eur_kwh": UNIT_RATE,
+ "measurement": "estimated_from_on_time",
+ "days": rows,
+ "totals": {
+ "starts": sum(row["starts"] for row in rows),
+ "runtime_min": round(total_min, 1),
+ "kwh": round(total_kwh, 3),
+ "cost_eur": round(total_kwh * UNIT_RATE, 2),
+ },
+ }
+
+
+class TuyaMonitor:
+ def __init__(self, store: Store) -> None:
+ self.store = store
+ self.api = TuyaOpenAPI(ENDPOINT, ACCESS_ID, ACCESS_KEY)
+ self.connected = False
+ self.latest = {"ok": False, "error": "a iniciar"}
+ self.lock = asyncio.Lock()
+
+ def _connect(self) -> None:
+ result = self.api.connect()
+ if not result.get("success"):
+ raise RuntimeError(result.get("msg", "falha de autenticação Tuya"))
+ self.connected = True
+
+ def _fetch_status(self) -> dict:
+ if not self.connected:
+ self._connect()
+ response = self.api.get(f"/v1.0/iot-03/devices/{DEVICE_ID}/status")
+ if not response.get("success"):
+ self.connected = False
+ raise RuntimeError(response.get("msg", "falha ao consultar status"))
+ values = {x["code"]: x["value"] for x in response.get("result", [])}
+ return values
+
+ def _fetch_logs(self, days: int) -> list[dict]:
+ if not self.connected:
+ self._connect()
+ now_ms = int(time.time() * 1000)
+ params = {
+ "type": "1,2,7",
+ "start_time": now_ms - days * 86400 * 1000,
+ "end_time": now_ms,
+ "size": 100,
+ }
+ response = self.api.get(f"/v1.0/devices/{DEVICE_ID}/logs", params)
+ if not response.get("success"):
+ self.connected = False
+ raise RuntimeError(response.get("msg", "falha ao consultar logs"))
+ return response.get("result", {}).get("logs", [])
+
+ async def sync(self, days: int = 14) -> dict:
+ async with self.lock:
+ try:
+ # tuya-connector keeps the access token on the client; serialize its calls.
+ status = await asyncio.to_thread(self._fetch_status)
+ logs = await asyncio.to_thread(self._fetch_logs, days)
+ inserted = self.store.add_events(logs)
+ now = datetime.now(TZ)
+ today = self.store.report(now.date(), now.date())
+ self.latest = {
+ "ok": True,
+ "ts": now.isoformat(timespec="seconds"),
+ "device_id": DEVICE_ID,
+ "name": "Boiler Shower",
+ "is_on": bool(status.get("switch_1")),
+ "online": True,
+ "power_kw_assumed": POWER_KW,
+ "watts_estimated": POWER_KW * 1000 if status.get("switch_1") else 0,
+ "today": today["totals"],
+ "events_fetched": len(logs),
+ "events_inserted": inserted,
+ "measurement": "ESTIMATIVA por tempo ON; este Tuya não mede watts/kWh",
+ }
+ return self.latest
+ except Exception as exc:
+ self.latest = {
+ "ok": False,
+ "online": False,
+ "error": f"{type(exc).__name__}: {exc}",
+ }
+ return self.latest
+
+ async def loop(self) -> None:
+ await self.sync(days=90)
+ while True:
+ await asyncio.sleep(POLL_SECONDS)
+ await self.sync(days=7)
+
+
+INDEX_HTML = r"""
+
+Boiler · Tuya
+
+
Interruptor do boilera iniciar…
+—
Potência configurada: 2,3 kW · apenas leitura
+
+
+
+
Ligações hoje
—
ciclos detectados
+
+
Estimativa: este modelo Tuya não possui medidor elétrico. kWh = 2,3 kW × tempo em que o relé ficou ON. Um termóstato interno pode reduzir o consumo real.
+
"""
+
+
+store = Store()
+monitor = TuyaMonitor(store)
+
+
+def parse_range(request: web.Request) -> tuple[date, date]:
+ today = datetime.now(TZ).date()
+ start = date.fromisoformat(request.query.get("from", (today - timedelta(days=29)).isoformat()))
+ end = date.fromisoformat(request.query.get("to", today.isoformat()))
+ if end < start:
+ raise ValueError("data final anterior à inicial")
+ if (end - start).days > 730:
+ raise ValueError("intervalo máximo: 730 dias")
+ return start, end
+
+
+async def index(_request: web.Request) -> web.Response:
+ return web.Response(text=INDEX_HTML, content_type="text/html")
+
+
+async def boiler_redirect(_request: web.Request) -> web.Response:
+ raise web.HTTPPermanentRedirect("/boiler/")
+
+
+async def api_status(_request: web.Request) -> web.Response:
+ if not monitor.latest.get("ok"):
+ await monitor.sync(days=14)
+ return web.json_response(monitor.latest)
+
+
+async def api_sync(_request: web.Request) -> web.Response:
+ return web.json_response(await monitor.sync(days=90))
+
+
+async def api_report(request: web.Request) -> web.Response:
+ try:
+ start, end = parse_range(request)
+ return web.json_response(store.report(start, end))
+ except Exception as exc:
+ return web.json_response({"ok": False, "error": str(exc)}, status=400)
+
+
+async def api_report_csv(request: web.Request) -> web.Response:
+ try:
+ start, end = parse_range(request)
+ report = store.report(start, end)
+ output = io.StringIO()
+ writer = csv.writer(output)
+ writer.writerow(["data", "ligacoes", "minutos_on", "kwh_estimados", "custo_eur"])
+ for row in report["days"]:
+ writer.writerow(
+ [row["date"], row["starts"], row["runtime_min"], row["kwh"], row["cost_eur"]]
+ )
+ return web.Response(
+ text=output.getvalue(),
+ content_type="text/csv",
+ headers={
+ "Content-Disposition": f'attachment; filename="boiler_{start}_{end}.csv"'
+ },
+ )
+ except Exception as exc:
+ return web.json_response({"ok": False, "error": str(exc)}, status=400)
+
+
+async def on_start(app: web.Application) -> None:
+ app["collector"] = asyncio.create_task(monitor.loop())
+
+
+async def on_stop(app: web.Application) -> None:
+ app["collector"].cancel()
+
+
+def main() -> None:
+ app = web.Application()
+ app.router.add_get("/", index)
+ app.router.add_get("/api/status", api_status)
+ app.router.add_post("/api/sync", api_sync)
+ app.router.add_get("/api/report", api_report)
+ app.router.add_get("/api/report.csv", api_report_csv)
+ app.router.add_get("/boiler", boiler_redirect)
+ app.router.add_get("/boiler/", index)
+ app.router.add_get("/boiler/api/status", api_status)
+ app.router.add_get("/boiler/api/report", api_report)
+ app.router.add_get("/boiler/api/report.csv", api_report_csv)
+ app.on_startup.append(on_start)
+ app.on_cleanup.append(on_stop)
+ web.run_app(app, host="0.0.0.0", port=PORT, print=None)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/Infra-Network/CURSOR_TAPO_TUYA_MCP.md b/Infra-Network/CURSOR_TAPO_TUYA_MCP.md
index 88d6262..f357922 100644
--- a/Infra-Network/CURSOR_TAPO_TUYA_MCP.md
+++ b/Infra-Network/CURSOR_TAPO_TUYA_MCP.md
@@ -172,4 +172,13 @@ Discovery: `TAPO_DISCOVERY_TARGET=192.168.1.255` — só na mesma Wi-Fi. Noutro
## Links internos
- [[WELCOME]]
-- [[INFRA/INDEX]]
\ No newline at end of file
+- [[INFRA/INDEX]]
+
+---
+
+## 9. Boiler Tuya (projeto separado do R630)
+
+- Pasta vault: `Boiler-Tuya-Energy/`
+- App: https://boiler.myvexx.com/
+- Live: CT120 `/opt/boiler-energy` porta 8788, SQLite, estimativa 2,3 kW
+- Nao misturar com Tapo P110 `r630.myvexx.com`
diff --git a/Infra-Network/SECRETS_CLOUDFLARE_MYVEXX.md b/Infra-Network/SECRETS_CLOUDFLARE_MYVEXX.md
index 09d078b..6ce29d4 100644
--- a/Infra-Network/SECRETS_CLOUDFLARE_MYVEXX.md
+++ b/Infra-Network/SECRETS_CLOUDFLARE_MYVEXX.md
@@ -1,6 +1,6 @@
# Cloudflare — zona myvexx.com
-**NÃO commitar. NÃO partilhar no chat.** Token de API (Edit zone DNS + tunnel).
+Token de API (Edit zone DNS + tunnel). **Repo privado Forgejo.** Projeto app: `Boiler-Tuya-Energy/`.
- **Zona:** myvexx.com
- **Zone ID:** `aa0aa51acf9f88579209a6b6326f1032`
diff --git a/_sidebar.md b/_sidebar.md
index 389f009..311922a 100644
--- a/_sidebar.md
+++ b/_sidebar.md
@@ -13,6 +13,11 @@
- [SPEC VM500 Rent Charge](Infra-Network/SPEC-VM500-RENTCHARGE.md)
- [Cursor Tapo Tuya MCP](Infra-Network/CURSOR_TAPO_TUYA_MCP.md)
---
+## Casa Dublin (energia)
+- [Boiler Tuya hub](Boiler-Tuya-Energy.md)
+- [Boiler PROJECT](Boiler-Tuya-Energy/PROJECT.md)
+- [R630 Tapo dashboard](https://r630.myvexx.com/)
+---
## MyVexx / Invest
- [Invest hub](Invest-MyVexx/Invest-MyVexx.md)
- [SPEC-050 AI Agents](Invest-MyVexx/SPEC-050-INVEST-AI-AGENTS-PAPER.md)