Skip to content

Commit 53e1e82

Browse files
authored
Versión 2X: refactor y tests (#7)
Refactor completo a “Versión 2X” Mueve load_dotenv() y la validación de TELEGRAM_TOKEN/TELEGRAM_CHAT_ID dentro de main() y añade if __name__ == "__main__": main(). Extrae el bloque de lectura en parse_sensor_block(lines: list[str]) -> dict[str, float] usando un diccionario de mapeo para etiquetas. Gestiona SerialException antes y durante la lectura de 6 líneas, cerrando y reconectando tras un breve sleep(). Implementa flush_db(conn, buffer) -> int que escriba el buffer y borre datos de hace >7 días, y una función aparte wal_checkpoint(conn) para el PRAGMA wal_checkpoint(TRUNCATE). Asegura el cierre en un finally: ser.close(), conn.close(), session.close(). Añade constantes documentadas para DATA_DIR, LOG_DIR, ARDUINO_VID, ARDUINO_PID en el README. Refuerza el Dockerfile: crea appuser, permisos 700, HEALTHCHECK, cachea requirements.txt. Crea .github/workflows/ci.yml con cache de pip, instaladores de hooks, pre-commit, pytest-cov, y artefactos de cobertura. Genera tests unitarios para parser, DB, reconexión serial y un test de integración completo que simule lectura → parseo → flush → checkpoint → notificación.
1 parent aa3d1f7 commit 53e1e82

File tree

6 files changed

+105
-21
lines changed

6 files changed

+105
-21
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,9 @@
1212
- CI con cache de pip, pre-commit, pytest y cobertura.
1313
- Nuevas pruebas de reconexión y checkpoint.
1414

15+
## v2X
16+
17+
- Refactor de `server.py` con parseo desacoplado y reconexión robusta.
18+
- Nueva función `wal_checkpoint` y limpieza segura de BD.
19+
- Tests ampliados y flujo de integración completo.
20+

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Sensor App
1+
# Sensor App v2X
22

33
![CI](https://github.com/tu_usuario/sensor_app/actions/workflows/ci.yml/badge.svg)
44
![Coverage](https://img.shields.io/badge/coverage-unknown-lightgrey.svg)
@@ -98,6 +98,9 @@ De este modo los chequeos se ejecutarán automáticamente antes de cada commit.
9898
* **Retención**:
9999

100100
* La línea `DELETE FROM lecturas WHERE Tiempo < datetime('now','-7 days')` mantiene sólo 7 días de datos. Modifícala si necesitas otro periodo.
101+
* **Rutas y VID/PID**:
102+
* Las carpetas `data/` y `logs/` se crean con permisos 700 y pueden cambiarse con las constantes `DATA_DIR` y `LOG_DIR` en `server.py`.
103+
* Los identificadores USB del Arduino (`ARDUINO_VID`, `ARDUINO_PID`) también son configurables al inicio del script.
101104

102105
## Contribuciones
103106

server.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from typing import Any, Dict
1717
import pytz
1818

19-
__version__ = "1.0.0"
19+
__version__ = "2.0.0"
2020

2121
# ------------------------------
2222
# ⚙️ Configuración general
@@ -121,6 +121,8 @@ def parse_sensor_block(lines: list[str]) -> Dict[str, float]:
121121
datos["Presion"] = num
122122
elif "Gas Resistencia" in key:
123123
datos["Gas_Resistencia"] = num
124+
else:
125+
logging.warning(f"Etiqueta desconocida: {key}")
124126
except Exception as e: # catch unexpected errors
125127
logging.error(f"❌ Error parseando bloque: {e}")
126128
return datos
@@ -142,24 +144,28 @@ def safe_execute(
142144

143145

144146
def flush_db(conn: sqlite3.Connection, buffer: list[Dict[str, Any]]) -> int:
145-
"""Inserta los registros del buffer en la base de datos de forma segura."""
147+
"""Inserta el buffer y depura registros antiguos."""
146148
registros = [tuple(d.get(c) for c in COLUMNAS) for d in buffer]
147149
try:
148150
with conn:
149151
conn.executemany(
150152
"INSERT OR REPLACE INTO lecturas VALUES (?,?,?,?,?,?)", registros
151153
)
154+
conn.execute(
155+
"DELETE FROM lecturas WHERE Tiempo < datetime('now','-7 days')"
156+
)
152157
except sqlite3.DatabaseError as e:
153158
logging.error(f"❌ Error al escribir en BD: {e}")
154159
return 0
155160

156-
safe_execute(conn, "DELETE FROM lecturas WHERE Tiempo < datetime('now','-7 days')")
157-
# separa checkpoint para evitar bloquear si falla
158-
safe_execute(conn, "PRAGMA wal_checkpoint(TRUNCATE);")
159-
160161
return len(registros)
161162

162163

164+
def wal_checkpoint(conn: sqlite3.Connection) -> None:
165+
"""Ejecuta un checkpoint WAL sin interrumpir errores."""
166+
safe_execute(conn, "PRAGMA wal_checkpoint(TRUNCATE);")
167+
168+
163169
# ------------------------------
164170
# 🔌 Serial con reconexión
165171
# ------------------------------
@@ -262,19 +268,25 @@ def main() -> None:
262268
TELEGRAM_TOKEN = token
263269
TELEGRAM_CHAT_ID = chat_id
264270

265-
conn = conectar_db(DB_FILE)
266-
ser = conectar_serial()
267-
268-
buffer = []
269-
contador_total = conn.execute("SELECT COUNT(*) FROM lecturas").fetchone()[0] or 0
270-
logging.info(f"Se reanuda el script. Registros en BD: {contador_total}")
271+
conn: sqlite3.Connection | None = None
272+
ser: serial.Serial | None = None
273+
try:
274+
conn = conectar_db(DB_FILE)
275+
ser = conectar_serial()
276+
assert conn is not None
277+
assert ser is not None
278+
279+
buffer = []
280+
contador_total = (
281+
conn.execute("SELECT COUNT(*) FROM lecturas").fetchone()[0] or 0
282+
)
283+
logging.info(f"Se reanuda el script. Registros en BD: {contador_total}")
271284

272-
ahora = time.time()
273-
proximo_flush_db = ahora + random.randint(MIN_ESCRITURA_DB, MAX_ESCRITURA_DB)
285+
ahora = time.time()
286+
proximo_flush_db = ahora + random.randint(MIN_ESCRITURA_DB, MAX_ESCRITURA_DB)
274287

275-
logging.info("📡 Iniciando captura de datos...")
288+
logging.info("📡 Iniciando captura de datos...")
276289

277-
try:
278290
while True:
279291
try:
280292
if not ser.is_open:
@@ -287,6 +299,7 @@ def main() -> None:
287299
except serial.SerialException:
288300
logging.error("Serial perdido. Cerrando y reconectando...")
289301
ser.close()
302+
time.sleep(2)
290303
ser = conectar_serial()
291304
continue
292305

@@ -310,6 +323,7 @@ def main() -> None:
310323
"Serial perdido durante lectura. Cerrando y reconectando..."
311324
)
312325
ser.close()
326+
time.sleep(2)
313327
ser = conectar_serial()
314328
lost_serial = True
315329
break
@@ -328,6 +342,7 @@ def main() -> None:
328342
logging.info(f"Iniciando flush de {len(buffer)} registros a la BD.")
329343
t0 = time.time()
330344
escritos = flush_db(conn, buffer)
345+
wal_checkpoint(conn)
331346
t1 = time.time()
332347
if escritos:
333348
contador_total += escritos
@@ -345,9 +360,10 @@ def main() -> None:
345360
except KeyboardInterrupt:
346361
logging.info("🚫 Detenido manualmente.")
347362
finally:
348-
if ser.is_open:
363+
if ser and ser.is_open:
349364
ser.close()
350-
conn.close()
365+
if conn:
366+
conn.close()
351367
session.close()
352368
logging.info("Script finalizado. Conexiones cerradas.")
353369

tests/test_db.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
os.environ.setdefault("TELEGRAM_TOKEN", "x")
77
os.environ.setdefault("TELEGRAM_CHAT_ID", "x")
88

9-
from server import conectar_db, flush_db, safe_execute # noqa: E402
9+
from server import conectar_db, flush_db, safe_execute, wal_checkpoint # noqa: E402
1010

1111

1212
def test_safe_execute_error(tmp_path):
@@ -33,6 +33,7 @@ def test_flush_db(tmp_path):
3333
]
3434

3535
inserted = flush_db(conn, buffer)
36+
wal_checkpoint(conn)
3637
assert inserted == 1
3738
cur = conn.execute("SELECT COUNT(*) FROM lecturas")
3839
assert cur.fetchone()[0] == 1

tests/test_integration.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@
99

1010
import serial # noqa: E402
1111

12-
from server import conectar_db, conectar_serial, flush_db # noqa: E402
12+
from server import ( # noqa: E402
13+
conectar_db,
14+
conectar_serial,
15+
flush_db,
16+
wal_checkpoint,
17+
parse_sensor_block,
18+
enviar_notificacion,
19+
) # noqa: E402
1320

1421

1522
class DummySerial:
@@ -53,6 +60,7 @@ def fake_safe_execute(conn, query, params=None):
5360

5461
monkeypatch.setattr("server.safe_execute", fake_safe_execute)
5562
inserted = flush_db(conn, buffer)
63+
wal_checkpoint(conn)
5664
assert inserted == 1
5765

5866

@@ -75,3 +83,48 @@ def serial_side_effect(*args, **kwargs):
7583
with mock.patch("serial.Serial", side_effect=serial_side_effect):
7684
result = conectar_serial()
7785
assert result is ser_second
86+
87+
88+
def test_full_flow(tmp_path, monkeypatch):
89+
db_path = tmp_path / "db.sqlite"
90+
conn = conectar_db(db_path)
91+
lines = [
92+
"------ Lecturas ------",
93+
"TMP117 Temp: 20.0 C",
94+
"BME680 Temp: 21.0 C",
95+
"Humedad: 50 %",
96+
"Presion: 1000 hPa",
97+
"Gas Resistencia: 200 kOhm",
98+
"",
99+
]
100+
ser = DummySerial(lines)
101+
102+
posts = []
103+
104+
def fake_post(url, data, timeout):
105+
posts.append((url, data))
106+
107+
class Resp:
108+
status_code = 200
109+
110+
def json(self):
111+
return {"result": {"message_id": 1}}
112+
113+
def raise_for_status(self):
114+
pass
115+
116+
return Resp()
117+
118+
monkeypatch.setattr("server.session.post", fake_post)
119+
120+
header = ser.readline().decode()
121+
assert "Lecturas" in header
122+
sensor_lines = [ser.readline().decode().strip() for _ in range(6)]
123+
datos = {"Tiempo": "2030-01-01 00:00:00"}
124+
datos.update(parse_sensor_block(sensor_lines))
125+
inserted = flush_db(conn, [datos])
126+
wal_checkpoint(conn)
127+
enviar_notificacion(datos)
128+
129+
assert inserted == 1
130+
assert posts

tests/test_parser.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,8 @@ def test_parse_sensor_block_valid():
3131
def test_parse_sensor_block_invalid():
3232
lines = ["foo", "bar"]
3333
assert parse_sensor_block(lines) == {}
34+
35+
36+
def test_parse_sensor_block_unknown_label():
37+
lines = ["Desconocido: 123"]
38+
assert parse_sensor_block(lines) == {}

0 commit comments

Comments
 (0)