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

4.2 KiB
Raw Blame History

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)

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
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

./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.