- google_calendar: RefreshError se referenciaba como google.oauth2.credentials.exceptions.RefreshError, ruta que no existe. Al fallar un refresh de token saltaba un AttributeError que ocultaba el error real y rompia el re-auth. Ahora se importa de google.auth.exceptions. - google_calendar: getEvent() comprobaba si habia un evento activo en ese instante. El daemon planifica de madrugada, asi que una ausencia de 09:00 a 18:00 no se detectaba y se fichaba igual. Ahora acepta un dia y comprueba solapamiento. Sin argumento mantiene el comportamiento anterior. - google_calendar: datetime.utcnow() esta deprecado en Python 3.12+. - timenet_manager: "args.basedtime and not args.type" ignoraba -t 0 (Entrada) al combinarlo con -bt, porque not 0 es True. Ahora es "args.type is None". - timenet_manager: updateVersion() asignaba el objeto Response al header Tn-V en vez de su texto. - timenet_manager: geo.accuracy se parseaba y se podia sobrescribir por CLI pero nunca se enviaba en el payload de cp/checks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
241 lines
12 KiB
Python
241 lines
12 KiB
Python
from google.auth.exceptions import RefreshError
|
|
import logging
|
|
|
|
from google.auth.transport.requests import Request
|
|
from google.oauth2.credentials import Credentials
|
|
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
from googleapiclient.discovery import build
|
|
from googleapiclient.errors import HttpError
|
|
|
|
from datetime import datetime, timezone
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
import os
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class GoogleCalendar:
|
|
def __init__(self):
|
|
try:
|
|
logger.info("Inicializando GoogleCalendar...")
|
|
self.creds = None
|
|
self.service = None
|
|
self.SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']
|
|
self.authfile = os.path.join(os.path.dirname(__file__), '../googleoauth.json')
|
|
self.grantfile = os.path.join(os.path.dirname(__file__), '../googleoauth_grant.json')
|
|
|
|
logger.info(f"Auth file: {self.authfile}, Grant file: {self.grantfile}")
|
|
|
|
if os.path.exists(self.authfile):
|
|
try:
|
|
logger.info("Cargando credenciales desde archivo...")
|
|
self.creds = Credentials.from_authorized_user_file(self.authfile, self.SCOPES)
|
|
logger.info("Credenciales cargadas correctamente")
|
|
except ValueError as error:
|
|
logger.error(f"Error al cargar credenciales: {error}", exc_info=True)
|
|
print(f"Error al cargar credenciales: {error}")
|
|
except Exception as e:
|
|
logger.error(f"Error inesperado al cargar credenciales: {str(e)}", exc_info=True)
|
|
print(f"Error inesperado al cargar credenciales: {str(e)}")
|
|
else:
|
|
logger.info("No se encontró archivo de credenciales, se solicitará autenticación")
|
|
|
|
logger.info("Verificando autenticación...")
|
|
self.checkAuth()
|
|
logger.info("Construyendo servicio...")
|
|
self.buildService()
|
|
logger.info("GoogleCalendar inicializado correctamente")
|
|
except Exception as e:
|
|
logger.error(f"Error al inicializar GoogleCalendar: {str(e)}", exc_info=True)
|
|
print(f"Error al inicializar GoogleCalendar: {str(e)}")
|
|
raise
|
|
|
|
def serverAuth(self):
|
|
try:
|
|
logger.info("Iniciando autenticación del servidor...")
|
|
if not os.path.exists(self.grantfile):
|
|
error_msg = f"Archivo de grant no encontrado: {self.grantfile}"
|
|
logger.error(error_msg)
|
|
raise FileNotFoundError(error_msg)
|
|
|
|
flow = InstalledAppFlow.from_client_secrets_file(self.grantfile, self.SCOPES)
|
|
self.creds = flow.run_local_server(port=0)
|
|
logger.info("Autenticación del servidor completada")
|
|
except FileNotFoundError as e:
|
|
logger.error(f"Archivo de grant no encontrado: {str(e)}", exc_info=True)
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error durante autenticación del servidor: {str(e)}", exc_info=True)
|
|
raise
|
|
|
|
def saveAuth(self):
|
|
try:
|
|
logger.info("Guardando credenciales...")
|
|
with open(self.authfile, 'w') as token:
|
|
token.write(self.creds.to_json())
|
|
logger.info("Credenciales guardadas correctamente")
|
|
except Exception as e:
|
|
logger.error(f"Error al guardar credenciales: {str(e)}", exc_info=True)
|
|
raise
|
|
|
|
def refreshAuth(self):
|
|
try:
|
|
logger.info("Refrescando credenciales...")
|
|
self.creds.refresh(Request())
|
|
logger.info("Credenciales refrescadas correctamente")
|
|
except RefreshError as e:
|
|
logger.warning(f"Error al refrescar credenciales: {str(e)}")
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error inesperado al refrescar credenciales: {str(e)}", exc_info=True)
|
|
raise
|
|
|
|
def checkAuth(self):
|
|
try:
|
|
logger.info("Verificando estado de autenticación...")
|
|
if not self.creds or not self.creds.valid:
|
|
logger.info("Credenciales no válidas o no existentes")
|
|
if self.creds and self.creds.expired and self.creds.refresh_token:
|
|
try:
|
|
logger.info("Intentando refrescar credenciales expiradas...")
|
|
self.refreshAuth()
|
|
except RefreshError as rerror:
|
|
logger.warning(f"No se pudo refrescar credenciales: {str(rerror)}, solicitando nueva autenticación")
|
|
self.serverAuth()
|
|
except Exception as e:
|
|
logger.error(f"Error inesperado al refrescar: {str(e)}", exc_info=True)
|
|
self.serverAuth()
|
|
else:
|
|
logger.info("Solicitando nueva autenticación...")
|
|
self.serverAuth()
|
|
|
|
logger.info("Guardando credenciales después de autenticación...")
|
|
self.saveAuth()
|
|
else:
|
|
logger.info("Credenciales válidas")
|
|
except Exception as e:
|
|
logger.error(f"Error durante checkAuth: {str(e)}", exc_info=True)
|
|
raise
|
|
|
|
def buildService(self):
|
|
try:
|
|
logger.info("Construyendo servicio de Google Calendar...")
|
|
self.service = build('calendar', 'v3', credentials=self.creds)
|
|
logger.info("Servicio de Google Calendar construido correctamente")
|
|
except Exception as e:
|
|
logger.error(f"Error al construir servicio de Google Calendar: {str(e)}", exc_info=True)
|
|
raise
|
|
|
|
def getNow(self, day=None):
|
|
d = day or datetime.now(timezone.utc)
|
|
return d.strftime('%Y-%m-%dT00:00:00Z')
|
|
|
|
def getNowAfterOneDay(self, day=None):
|
|
d = day or datetime.now(timezone.utc)
|
|
return (d + relativedelta(days=1)).strftime('%Y-%m-%dT00:00:00Z')
|
|
|
|
@staticmethod
|
|
def _affects(start, end, day):
|
|
"""Con `day`: el evento solapa ese dia. Sin `day`: esta activo ahora mismo."""
|
|
s0 = datetime.fromisoformat(start)
|
|
e0 = datetime.fromisoformat(end)
|
|
if day is None:
|
|
return s0.timestamp() <= datetime.now().timestamp() <= e0.timestamp()
|
|
d0 = datetime(day.year, day.month, day.day, tzinfo=s0.tzinfo)
|
|
d1 = d0 + relativedelta(days=1)
|
|
return s0 < d1 and e0 > d0
|
|
|
|
def getEvent(self, day=None):
|
|
"""Ausencia personal que afecta a `day` (por defecto, ahora mismo).
|
|
|
|
Con `day` se pregunta si el evento CUBRE ese dia, no si esta activo en este
|
|
instante: el daemon planifica de madrugada y una ausencia de 09:00 a 18:00
|
|
tiene que detectarse igual.
|
|
"""
|
|
try:
|
|
logger.info("Obteniendo eventos de Google Calendar...")
|
|
now = self.getNow(day)
|
|
now_after_one_day = self.getNowAfterOneDay(day)
|
|
logger.info(f"Buscando eventos entre {now} y {now_after_one_day}")
|
|
|
|
try:
|
|
logger.info("Consultando calendario autoficher...")
|
|
events_result_autoficher = self.service.events().list(
|
|
calendarId='e51d3bfb6b352c48afb8bd7a5d494599cc3eaa686800a856796306f50c6d72fd@group.calendar.google.com',
|
|
timeMin=now,
|
|
timeMax=now_after_one_day, maxResults=10, singleEvents=True,
|
|
orderBy='startTime').execute()
|
|
logger.info(f"Eventos de autoficher obtenidos: {len(events_result_autoficher.get('items', []))}")
|
|
except HttpError as e:
|
|
logger.error(f"Error HTTP al consultar calendario autoficher: {str(e)}", exc_info=True)
|
|
events_result_autoficher = {'items': []}
|
|
except Exception as e:
|
|
logger.error(f"Error inesperado al consultar calendario autoficher: {str(e)}", exc_info=True)
|
|
events_result_autoficher = {'items': []}
|
|
|
|
try:
|
|
logger.info("Consultando calendario de festivos...")
|
|
events_result_holiday = self.service.events().list(
|
|
calendarId='es.spain#holiday@group.v.calendar.google.com',
|
|
timeMin=now,
|
|
timeMax=now_after_one_day,
|
|
maxResults=10, singleEvents=True,
|
|
orderBy='startTime').execute()
|
|
logger.info(f"Eventos de festivos obtenidos: {len(events_result_holiday.get('items', []))}")
|
|
except HttpError as e:
|
|
logger.warning(f"Error HTTP al consultar calendario de festivos (puede ser normal): {str(e)}")
|
|
events_result_holiday = {'items': []}
|
|
except Exception as e:
|
|
logger.warning(f"Error inesperado al consultar calendario de festivos: {str(e)}")
|
|
events_result_holiday = {'items': []}
|
|
|
|
events = events_result_autoficher.get('items', []) + events_result_holiday.get('items', [])
|
|
logger.info(f"Total de eventos encontrados: {len(events)}")
|
|
|
|
if not events:
|
|
logger.info("No se encontraron eventos")
|
|
return False
|
|
|
|
# Prints the start and name of the next 10 events
|
|
for event in events:
|
|
try:
|
|
start = event['start'].get('dateTime', event['start'].get('date'))
|
|
end = event['end'].get('dateTime', event['end'].get('date'))
|
|
logger.debug(f"Evaluando evento: {event.get('summary', 'Sin título')} - {start} a {end}")
|
|
|
|
if self._affects(start, end, day):
|
|
logger.info(f"Evento activo encontrado: {event.get('summary', 'Sin título')}")
|
|
if event.get('organizer', {}).get('displayName') == 'autoficher':
|
|
result = 'Forzado desde autoficher - ' + event.get("summary", "Sin Título")
|
|
logger.info(f"Evento de autoficher activo: {result}")
|
|
return result
|
|
|
|
#Desactivado por que no funcionaba muy bien ese calendario... se han puesto en el calendario autoficher los dias festivos correctos
|
|
#if 'description' in event and ("Cataluña" in event['description'] or 'Día festivo' in event[
|
|
# 'description'] or 'Celebración\n' in event['description']) and not 'Cambio de horario' in event[
|
|
# 'summary']:
|
|
# return event['summary']
|
|
except KeyError as e:
|
|
logger.warning(f"Evento sin campo requerido: {str(e)}, evento: {event.get('summary', 'Sin título')}")
|
|
continue
|
|
except ValueError as e:
|
|
logger.warning(f"Error al parsear fecha del evento: {str(e)}, evento: {event.get('summary', 'Sin título')}")
|
|
continue
|
|
except Exception as e:
|
|
logger.warning(f"Error al procesar evento: {str(e)}, evento: {event.get('summary', 'Sin título')}")
|
|
continue
|
|
|
|
logger.info("No se encontraron eventos activos")
|
|
return False
|
|
except HttpError as error:
|
|
error_msg = f'Error HTTP de Google Calendar: {error}'
|
|
logger.error(error_msg, exc_info=True)
|
|
print(f'An error occurred: {error}')
|
|
return 'Error de llamada a Google Calendar'
|
|
except Exception as e:
|
|
error_msg = f'Error inesperado en getEvent: {str(e)}'
|
|
logger.error(error_msg, exc_info=True)
|
|
print(f'Error inesperado: {error_msg}')
|
|
return 'Error de llamada a Google Calendar'
|