Skip to content

salistito/home-os

Repository files navigation

HomeOS

Bot de Telegram para repartir las tareas de la casa entre dos o más personas. Cada mañana asigna las tareas del día al miembro con menos puntos acumulados en el mes, y al completarlas se suman puntos para llevar un balance mensual.

Arquitectura

El proyecto sigue una arquitectura en capas con dependencia unidireccional:

apps/bots/telegram/   ← entrypoint del bot (Starlette + python-telegram-bot)
  │
modules/tasks/        ← lógica de dominio (servicios, repositorio, tipos)
  │
core/                 ← infraestructura compartida (DB, config, identidad, utils)

Reglas de dependencia:

  • core/ no puede importar de modules/ ni apps/.
  • modules/ puede importar solo de core/.
  • apps/ puede importar de ambos.

core/

Archivo Propósito
config.py Carga variables de entorno desde .env usando python-dotenv
utils/date.py Utilidades de fecha: get_today(), format_date(), to_db_date(), next_due_date(), month_key(), arrays DAYS y MONTHS
utils/string.py Utilidades de texto: normalize_string(), html_escape()
db.py Conexión SQLite con row_factory = sqlite3.Row y PRAGMA foreign_keys = ON
schema.sql Esquema de la base de datos (users, tasks, assignments)
identity.py Consulta de usuarios desde la DB
seed.py Carga datos iniciales desde archivos YAML en seed/

modules/tasks/

Archivo Propósito
types.py Dataclasses: Task, Assignment, TaskOperationResult, AssignmentCompletionResult and enums: TaskOperationStatus, AssignmentCompletionStatus
repository.py Consultas SQL (tasks, assignments, puntos por usuario)
service.py Lógica de negocio: asignar tareas, marcar como hechas, balance mensual

apps/bots/telegram/

Archivo Propósito
main.py Punto de entrada: servidor Starlette + Uvicorn, rutas webhook
app.py Construcción de la aplicación python-telegram-bot con handlers
jobs.py Envío de asignaciones diarias a cada usuario
messages_es.py Mensajes de texto en español (i18n listo para agregar otros idiomas)
trigger_daily.py Script CLI para ejecutar asignaciones diarias sin servidor web
handlers/commands.py Comandos /start, /help, /tasks, /add_task, /list_tasks, /edit_task, /delete_task, /assignments, /balance
handlers/messages.py Mensajes de texto y botones inline

Requisitos

  • Python 3.12 o superior
  • Un bot de Telegram (crear con @BotFather)
  • (Opcional) Docker

Configuración inicial

  1. Clona el repositorio y crea un entorno virtual:
git clone <repo>
cd home-os
python -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
  1. Instala el proyecto en modo editable con dependencias de desarrollo:
pip install -e ".[dev]"
  1. Copia el archivo de ejemplo y completa las variables:
cp .env.example .env

Edita .env con los siguientes valores:

Variable Descripción
TELEGRAM_BOT_TOKEN Token del bot (de @BotFather)
SEBASTIAN_TELEGRAM_CHAT_ID Chat ID del primer integrante
ANTONIA_TELEGRAM_CHAT_ID Chat ID del segundo integrante
WEBHOOK_URL URL pública del bot (para webhook)
WEBHOOK_SECRET Token aleatorio para validar requests

Para obtener tu chat_id, envía un mensaje a @userinfobot en Telegram.

Variables de entorno

Variable Defecto Descripción
HOME_OS_DB_PATH ./homeos.db Ruta al archivo SQLite
TZ America/Santiago Zona horaria (usada por core.utils.get_today())
PORT 8080 Puerto del servidor webhook

Ejecución local

python -m apps.bots.telegram.main

Si WEBHOOK_URL y WEBHOOK_SECRET están configurados, levanta un servidor Starlette + Uvicorn en http://0.0.0.0:8080 en modo webhook.
Si no, arranca en modo polling — el bot escucha actualizaciones de Telegram sin necesidad de un servidor accesible públicamente.

Asignación diaria sin servidor web

Para probar o ejecutar la rutina de asignación diaria localmente sin levantar el bot ni configurar un cron:

python -m apps.bots.telegram.trigger_daily

Esto inicializa la base de datos, ejecuta el mismo algoritmo de asignación que usa el cron en producción y envía los mensajes por Telegram a cada integrante.

Verificar instalación

python -c "import core, modules.tasks, apps.bots.telegram; print('imports OK')"

Linter

ruff check .

No hay typechecker configurado. Ruff usa line-length = 100.

Seed data

Al arrancar, el bot inicializa la DB y carga datos desde seed/:

seed/users.yaml

Define los integrantes de la casa. El telegram_chat_id soporta expansión de variables de entorno con sintaxis $VAR_NAME:

users:
  - id: sebastian
    name: Sebastian
    telegram_chat_id: $SEBASTIAN_TELEGRAM_CHAT_ID
  - id: antonia
    name: Antonia
    telegram_chat_id: $ANTONIA_TELEGRAM_CHAT_ID

seed/tasks.yaml

Define las tareas del hogar. Cada tarea puede ser:

  • Recurrente (con frequency_days): se asigna automáticamente cada N días.
  • Ocasional (sin frequency_days): los integrantes la marcan como hecha cuando quieran.

Para tareas recurrentes, el next_due_date se puede definir de dos formas:

  • start_offset_days: se calcula como hoy + offset.
  • next_due_date: fecha exacta en formato YYYY-MM-DD (tiene prioridad sobre start_offset_days).
tasks:
  - name: Lavar la loza
    points: 3
    frequency_days: 2
    next_due_date: "2026-07-13"
  - name: Regar las plantas
    points: 2
    frequency_days: 7
    start_offset_days: 1
  - name: Sacar el reciclaje
    points: 2

Las tareas se cargan una sola vez al crear la DB. Si la tabla tasks ya tiene datos, el seed se salta.

seed/assignments.yaml

Define asignaciones históricas para poblar la DB (útil para migrar datos o reiniciar). Usa task_name y user_name para referenciar por nombre (se resuelven a IDs automáticamente).

assignments:
  - task_name: Lavar la loza
    user_name: Sebastián
    assigned_at: "2026-07-06"
    status: completed
    completed_at: "2026-07-06"
    points_awarded: 6

  - task_name: Regar las plantas
    user_name: Antonia
    assigned_at: "2026-07-06"
    status: completed
    completed_at: "2026-07-06"
    points_awarded: 2

  - task_name: Lavar la loza
    user_name: Sebastián
    assigned_at: "2026-07-08"
    status: failed

Las asignaciones se cargan una sola vez. Si la tabla assignments ya tiene datos, el seed se salta.

Uso del bot

| Comando / Acción | Descripción | |---|---|---| | /start | Mensaje de bienvenida con instrucciones | | /help | Muestra la ayuda (alias de /start) | | /tasks | Explicación de los comandos CRUD de tareas | | /add_task <name> <points> [freq] | Crea una tarea nueva | | /list_tasks | Lista todas las tareas con formato tabla | | /edit_task <name> <field> <value> | Edita nombre, puntos o frecuencia de una tarea | | /delete_task <name> | Elimina una tarea (solo si no tiene asignaciones pendientes) | | /assignments | Muestra las tareas pendientes de hoy con botones | | /balance | Muestra los puntos acumulados este mes | | Escribir nombre de tarea | Marca una tarea como completada (coincidencia exacta, case-insensitive) | | Botón inline | Marca la tarea como completada desde el mensaje de la mañana |

¿Cómo funciona la asignación diaria?

  1. Se marcan como failed las tareas pendientes de días anteriores.
  2. Se buscan tareas recurrentes cuya next_due_date sea hoy o anterior, ordenadas por puntos de mayor a menor.
  3. Se asignan al integrante con menos puntos acumulados en el mes actual. En caso de empate, se elige al azar.
  4. Cada integrante tiene un tope diario de puntos igual a 1.5 × la tarea con más puntos del día. Al alcanzarlo, no recibe más tareas ese día. Las tareas que nadie puede tomar se saltan y quedan pendientes para el próximo ciclo.
  5. Se envía un mensaje a cada integrante con sus tareas y botones para marcar como hecha.
  6. Al marcar como hecha, se recalcula next_due_date = today + frequency_days.

Docker

cp .env.example .env
# completa TELEGRAM_BOT_TOKEN y los chat IDs
docker compose up --build

La base de datos persiste en ./data gracias al volumen definido en docker-compose.yml.

Contrato de la API

La interfaz entre la lógica de dominio (modules/tasks/service.py) y el bot. No se cambia sin conversarlo.

def create_task(name: str, points: int, frequency_days: int | None = None) -> TaskOperationResult

def update_active_task(task_id: int, **kwargs: str | int | None) -> TaskOperationResult

def soft_delete_active_task(task_id: int) -> TaskOperationResult

def get_daily_assignments(day: date) -> list[Assignment] # Si no hay asignaciones para el día, las genera automáticamente

def get_pending_assignments(day: date) -> list[Assignment]

def fail_stale_pending_assignments(day: date) -> int

def mark_assignment_done(text: str, user_id: str, day: date) -> AssignmentCompletionResult

def get_month_balance(month: str) -> dict[str, int]

Producción (Fly.io)

El bot corre en modo webhook sobre Starlette + Uvicorn con dos rutas:

  • POST /telegram — recibe updates de Telegram (validado con header X-Telegram-Bot-Api-Secret-Token).
  • GET|POST /trigger-daily/<WEBHOOK_SECRET> — dispara la asignación diaria. Lo llama un cron externo.

La máquina de Fly.io se apaga automáticamente sin tráfico (auto_stop_machines = 'stop', min_machines_running = 0) y arranca con cada request entrante. Así solo se paga el cómputo cuando el bot está activo (~USD 0.15/mes por el volumen).

Setup inicial en Fly.io

fly launch --no-deploy
fly volumes create homeos_data --region gru --size 1
fly secrets set \
  TELEGRAM_BOT_TOKEN=<token> \
  WEBHOOK_SECRET=$(openssl rand -hex 32) \
  WEBHOOK_URL=https://<tu-app>.fly.dev \
  ANTONIA_TELEGRAM_CHAT_ID=<chat_id> \
  SEBASTIAN_TELEGRAM_CHAT_ID=<chat_id>
fly deploy

Cron del aviso diario

Configurar un cron externo (ej. cron-job.org) que haga una request a las 07:00 hora de Chile:

GET https://<tu-app>.fly.dev/trigger-daily/<WEBHOOK_SECRET>

La zona horaria de Chile es America/Santiago. En horario de verano (septiembre-marzo) son UTC-3; en invierno UTC-4. cron-job.org permite fijar la zona horaria directamente.

Base de datos

SQLite, creada automáticamente al arrancar. Tablas:

  • usersid, name, telegram_chat_id
  • tasksid, name, points, frequency_days, next_due_date, deleted_at (soft delete)
  • assignmentsid, task_id, user_id, assigned_at, completed_at, status (pending|completed|failed), points_awarded

Índices únicos:

  • idx_tasks_unique_active_name — un nombre activo por tarea (WHERE deleted_at IS NULL)
  • idx_assignment_one_pending_per_task — una asignación pendiente por tarea
  • idx_assignment_one_completed_per_day — una asignación completada por tarea por día

El archivo .db no se versiona (en .gitignore). Se regenera solo con datos de seed si no existe.

Backup de la base de datos (producción)

La DB de producción vive en un volumen de Fly.io (/app/data/homeos.db). Para descargar una copia local:

Opción 1: Script automático (PowerShell)

.\scripts\private\backup_db.ps1

Esto:

  1. Inicia la máquina si está detenida
  2. Descarga la DB a data/homeos_<timestamp>.db y data/homeos.db
  3. Verifica la integridad de la copia
  4. Detiene la máquina para ahorrar costos

Para especificar otro directorio de salida:

.\scripts\private\backup_db.ps1 -OutputDir "backups"

Opción 2: Comandos manuales

# Verificar estado
fly status

# Si la máquina está detenida, iniciarla
fly machine start <machine_id>

# Descargar la DB
fly ssh sftp get /app/data/homeos.db data/homeos.db

# Detener la máquina después del backup
fly machine stop <machine_id>

Inspeccionar la DB de producción directamente

Sin descargar la DB, ejecuta inspect_db.py directamente en el servidor de Fly.io:

python scripts/private/inspect_prod_db.py

Esto:

  1. Inicia la máquina si está detenida
  2. Sube el script de inspección a /tmp/inspect.py en el servidor
  3. Lo ejecuta contra la DB de producción
  4. Muestra tablas, conteo de filas y las primeras 25 filas de cada tabla

Inspeccionar la DB local

python scripts/private/inspect_db.py data/homeos.db

Muestra tablas, conteo de filas y las primeras 25 filas de cada tabla.

Notas

  • La DB en data/ está en .gitignore y no se versiona.
  • Ambos scripts (backup_db.ps1 e inspect_prod_db.py) auto-inician la máquina si está detenida y la detienen después de operar.
  • Los scripts están en scripts/private/ (también en .gitignore).

Notas técnicas

  • No hay scheduler en proceso. Las asignaciones diarias las dispara un cron externo, o localmente con python -m apps.bots.telegram.trigger_daily.
  • assignments con status pending de días anteriores se marcan como failed al ejecutar la rutina diaria.
  • Las tareas se asignan al usuario con menor puntaje acumulado en el mes, no aleatoriamente ni por turnos fijos.

About

HomeOS is a modular platform that centralizes household management, including shared tasks, recipes, shopping, nutrition, and smart home automation.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors