omar f2ed694f98 feat: comando /grafico con cinco visualizaciones
Cinco graficos generados con matplotlib y enviados como foto a Telegram, con
botones para elegir. La paleta es la de referencia del proyecto, validada con el
comprobador de daltonismo en claro y oscuro (peor par CVD delta-E 24.7 / 26.8,
umbral 8).

- saldo        acumulado (linea) + saldo de cada dia (barras), dos paneles
- puntualidad  desviacion de cada entrada en el tiempo. Es el que explica el
               problema de un vistazo: en 6 meses no hay ni un solo punto por
               debajo de cero, o sea que nunca se ha llegado antes de la hora
- horas        histograma de la hora de entrada, historico frente a planificado.
               Las dos distribuciones no se solapan en absoluto
- regalado     minutos al mes presente sin que corra el reloj
- prevision    linea de tiempo de los proximos dias con la franja que cuenta

Un PNG no tiene hover, asi que las cifras que en una pagina resolveria el
tooltip van en el pie de foto y en etiquetas directas sobre las marcas.

Al mirarlos renderizados aparecieron cuatro defectos que el codigo no delata:
titulo y subtitulo pisandose, la etiqueta del peor dia chocando con el eje de
fechas, un dia de +64 min aplastando el resto del eje contra el cero, y un bug
de formato que pintaba "09:64" en vez de "10:04". Corregidos: ejes acotados con
los valores extremos anotados aparte, y las horas formateadas con un helper.

Executor.historico() extrae los dias de varios meses ya parseados; el servicio
lo cachea media hora porque son seis llamadas a la API.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-24 20:05:20 +02:00
2021-10-15 14:41:56 +02:00

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
/perfil [nombre] ver y cambiar el perfil de jitter
/proximos [n] previsión de los próximos días laborables y sus rangos
/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.

Perfiles de jitter

Timenet sólo cuenta dentro de la ventana teórica, así que las horas de fichaje sólo pueden variar hacia el lado seguro, y alejarse cuesta presencia real. Más variedad visible = más minutos ahí. Cada perfil elige un punto de ese compromiso. Los cinco dan saldo 0; se cambian en caliente con /perfil.

perfil min/día madrugón entrada 09:00
discreto 2,8 1,3 2 minutos, moda 75 %
natural (por defecto) 4,1 1,8 5 minutos, moda 51 %
camuflado 5,5 2,1 4 minutos, moda 43 %
disperso 7,9 3,0 6 minutos, moda 29 %
sin madrugar 5,7 1,3 2 minutos, moda 77 %
histórico real — — 4 minutos, moda 38 %

min/día es la presencia total de más. madrugón es la parte de esos minutos que te levantas antes por la entrada de la mañana — el resto se lo come la pausa de comer y la salida, que no alargan el día. Por eso existe sin madrugar: entrada de la mañana pegada a las 9 y toda la variedad en la comida y la salida.

Medido sobre los 226 días laborables del año con tools/perfiles.py. Un perfil puede afinar por tramo con sufijo _w<n>: entry_median_w0 gana a entry_median para el tramo de la mañana.

Configuración

Lo esencial va en config.json (ver sample.config.json). 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
./venv/bin/python3 tools/perfiles.py       # mide cada perfil sobre el calendario real

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.

Description
No description provided
Readme 23 MiB
Languages
Python 100%