Files
autoficher/README.md
omar 5ba5710db2 feat: daemon con bot de Telegram, killswitch y servicio systemd
Pasa de un cron ciego a un servicio en segundo plano. El killswitch lo exige:
para poder cancelar un fichaje pendiente desde el movil tiene que haber un
proceso escuchando durante la espera.

- daemon: bucle propio que duerme exactamente hasta el siguiente evento, con
  precision de decimas. No usa JobQueue de PTB porque APScheduler 3.6 importa
  pkg_resources, que ya no existe en Python 3.13; ademas asi no se añade una
  dependencia. Recupera fichajes perdidos si el equipo estuvo apagado.
- bot: /hoy /saldo /plan /fichar /desfichar /off /on /pausa /reanudar /log.
  Los comandos se registran con setMyCommands para que salgan en el menu del
  chat. Solo atiende al chat_id configurado.
- killswitch: aviso 12 min antes con teclado inline (cancelar / +15 min /
  fichar ya).
- Google Calendar queda solo para ausencias personales; festivos y vacaciones
  salen del calendario de Timenet.
- deploy/autoficher.service: unidad de usuario endurecida (ProtectSystem
  strict, escritura solo en el propio directorio), Restart=always.
- requirements: python-telegram-bot >= 22. La 20.5 tiene un bug de __slots__
  en Updater que rompe el polling en Python 3.13.
- .gitignore: data/, *.db, .env, __pycache__ en cualquier nivel.

tools/test_daemon.py cubre con reloj simulado y sin red: plan, aviso previo,
fichaje a su hora, killswitch, retraso de 15 min, pausa, recuperacion tras
apagon, fallo de red y dia no laborable. 19 comprobaciones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-24 18:51:46 +02:00

119 lines
4.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Autoficher para Timenet
=======================
Ficha y desficha automáticamente en Timenet, controlado desde Telegram.
Cómo calcula Timenet (y por qué importa)
----------------------------------------
Verificado sobre 6 meses de historial (104 días laborables, 376 fichajes):
```
trabajado = Σ min(salida, fin_teórico) − max(entrada, inicio_teórico)
```
Timenet **recorta a la ventana teórica**. La API guarda dos horas por fichaje: `HR`
(la que fichaste) y `H` (la que te cuentan). De 376 fichajes, 136 llevaban ajuste:
**todos salidas, todos recortados hacia atrás**. Ninguna entrada ajustada jamás.
Consecuencias, y son todo el diseño de este programa:
* **Sólo importan las entradas.** Llegar tarde se debita al minuto; salir tarde no
acredita nada. Por eso las entradas se fichan *antes* de la hora y las salidas
*después*.
* **Acabar el mes en positivo es imposible.** 0 días con saldo positivo en 108, y
`timeExtraHours: false` en todos los tipos de jornada. El objetivo es 0 clavado.
* El calendario laboral (festivos, vacaciones, horarios) lo da `cp/calendar`. No hay
que adivinarlo. Google Calendar queda sólo para ausencias personales.
Servicio (recomendado)
----------------------
```bash
pip install -r requirements.txt # requiere python-telegram-bot >= 22
cp sample.config.json config.json # y edítalo
echo "TIMENET_PIN=1234" > .env && chmod 600 .env
cp deploy/autoficher.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now autoficher.service
sudo loginctl enable-linger $USER # para que sobreviva al logout y al reinicio
```
```bash
systemctl --user status autoficher # estado
journalctl --user -u autoficher -f # logs en vivo
systemctl --user restart autoficher # recargar tras tocar config
```
Comandos de Telegram
--------------------
Se registran solos en el menú del chat al arrancar.
| comando | qué hace |
|---|---|
| `/hoy` | plan y estado de hoy, con lo que Timenet ha registrado de verdad |
| `/saldo` | saldo del mes y acumulado |
| `/plan` | plan de hoy, con botón para re-tirar los dados |
| `/fichar` `/desfichar` | fichaje manual inmediato |
| `/off [hoy\|dd/mm\|dd/mm-dd/mm]` | no fichar esos días |
| `/on [fecha]` | volver a fichar ese día |
| `/pausa` `/reanudar` | parar o retomar el servicio |
| `/log` | últimos eventos |
Doce minutos antes de cada fichaje llega un aviso con killswitch:
```
⏳ ENTRADA programada a las 08:59:22 (en 12 min)
[🛑 Cancelar hoy] [⏭ +15 min] [✅ Fichar ya]
```
El bot sólo atiende al `chat_id` de `config.json`; cualquier otro chat se ignora.
Configuración
-------------
Lo esencial va en `config.json` (ver `sample.config.json`). Los valores por defecto
del jitter están en `app/settings.py`:
| perfil | `entry_before` / `exit_after` (s) | presencia extra | colchón mínimo |
|---|---|---|---|
| holgado | 40-150 / 30-130 | 5,67 min/día | 30 s |
| **equilibrado** (por defecto) | 30-100 / 20-70 | 3,55 min/día | 20 s |
| ajustado | 20-70 / 15-45 | 2,70 min/día | 15 s |
Los tres dan saldo 0; cambia cuánta presencia real cuestan y cuánto retraso del
daemon aguantan antes de perder un minuto.
`geo_jitter_meters` desplaza la posición dentro de un disco de ese radio. Sin él,
las coordenadas salen idénticas al sexto decimal en todos los fichajes.
Herramientas
------------
```bash
./venv/bin/python3 tools/tn_inspect.py # calendario y saldo, contra la API real
./venv/bin/python3 tools/dryrun.py # simula el año y verifica que no pierde minutos
./venv/bin/python3 tools/test_daemon.py # 19 pruebas con reloj simulado, sin red
```
Modo antiguo (CLI de un disparo)
--------------------------------
`main.py` sigue funcionando para fichajes puntuales desde consola o cron:
```
python3 main.py -p <pin> [-t <0=Entrada, 1=Salida> | -bt] [-f] [-d]
```
No aplica la corrección del sesgo ni consulta el calendario de Timenet: pierde de 3
a 5 minutos al día. Úsalo sólo como respaldo manual.
Google Calendar
---------------
Para ausencias personales. Descarga el OAuth de la API de Google y déjalo en la raíz
como `googleoauth_grant.json`; el token se guarda en `googleoauth.json`. Se consulta
el calendario `autoficher`: si un evento cubre el día, no se ficha.