128 lines
6.9 KiB
Python
128 lines
6.9 KiB
Python
"""Fast voice routing for visual actions; Hermes still handles general tasks."""
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
from datetime import datetime, timedelta, time
|
||
from zoneinfo import ZoneInfo
|
||
|
||
from . import ha_views as ha
|
||
from . import alfred_intents as intents
|
||
|
||
TZ = ZoneInfo(os.getenv('ALFRED_TIMEZONE','Europe/Paris'))
|
||
|
||
|
||
def _label(s,e):
|
||
return f"du {s:%d/%m} au {(e-timedelta(days=1)):%d/%m}"
|
||
|
||
|
||
def _event_summary(events, question):
|
||
count=len(events)
|
||
if count == 0:return "Non, je n'ai trouvé aucun événement pour cette période." if question else 'Aucun événement prévu sur cette période.'
|
||
names=', '.join(ev['title'] for ev in events[:3])
|
||
if question:
|
||
return f"Oui, {count} événement{'s' if count>1 else ''} prévu{'s' if count>1 else ''}. {names}." if count<=3 else f"Oui, {count} événements sont prévus. Notamment : {names}."
|
||
return f"Voici ton planning avec {count} événement{'s' if count>1 else ''}."
|
||
|
||
|
||
def _calendar(text, question=False):
|
||
start,end,view=intents.period(text)
|
||
payload=ha.get_events(start,end)
|
||
content={'type':'calendar','view':view,'title':'Planning','start':start.isoformat(),'end':end.isoformat(),
|
||
'events':payload['events'],'calendars':payload['calendars'],'errors':payload['errors']}
|
||
speech=_event_summary(payload['events'],question)
|
||
if payload['errors']: speech+=' Certains calendriers sont indisponibles.'
|
||
return content,speech
|
||
|
||
|
||
def _create(text):
|
||
calendars=ha.list_calendars()
|
||
details, complete=intents.extract_create(text,calendars)
|
||
if not complete:
|
||
return {'type':'calendar_form','values':details,'calendars':calendars},'Je prépare le rendez-vous. Complète ou vérifie les informations à l’écran avant de l’enregistrer.'
|
||
# No calendar mutation until a second explicit confirmation.
|
||
intents.set_create(details)
|
||
pretty=datetime.fromisoformat(details['date']).strftime('%d/%m')
|
||
speech=f"Je vais ajouter {details['title']} le {pretty} à {details['time']}. Dis confirme pour enregistrer, ou annule."
|
||
return {'type':'calendar_form','values':details,'calendars':calendars,'awaiting_confirmation':True},speech
|
||
|
||
|
||
def _delete(text):
|
||
now=datetime.now(TZ)
|
||
day=intents.parse_day(text,now)
|
||
if day:
|
||
start=datetime.combine(day,time.min,TZ); end=start+timedelta(days=1)
|
||
else:
|
||
start=datetime.combine(now.date(),time.min,TZ);end=start+timedelta(days=45)
|
||
data=ha.get_events(start,end)
|
||
needle=intents.extract_search(text)
|
||
events=[e for e in data['events'] if intents.simplify(needle) in intents.simplify(e['title'])] if needle else data['events']
|
||
if not events:
|
||
return {'type':'calendar','view':'days','start':start.isoformat(),'end':end.isoformat(),**data},'Je ne trouve pas cet événement. Aucun événement n’a été supprimé.'
|
||
if len(events)!=1:
|
||
return {'type':'delete_candidates','events':events[:30]},f"J'ai trouvé {len(events)} événements correspondants. Choisis celui à supprimer sur l'écran."
|
||
ev=events[0]
|
||
if not ev['can_delete']:
|
||
return {'type':'delete_candidates','events':[ev]},'Ce calendrier ne permet pas la suppression de cet événement depuis Alfred.'
|
||
intents.set_delete(ev)
|
||
warning=' Attention, si cet événement est récurrent, cela peut supprimer toute la série.' if not ev['recurrence_id'] else ''
|
||
return {'type':'delete_candidates','events':[ev], 'awaiting_confirmation':True},f"Confirme la suppression de {ev['title']}, ou dis annule.{warning}"
|
||
|
||
|
||
def _confirm_delete():
|
||
e=intents.current_delete()
|
||
if not e:return {'type':'text','text':'Confirmation expirée.'},'La demande de suppression a expiré.'
|
||
# Recheck existence in HA to avoid deleting an event that changed meanwhile.
|
||
start=datetime.fromisoformat(e['start'])-timedelta(days=1)
|
||
end=datetime.fromisoformat(e['end'])+timedelta(days=1)
|
||
found=ha.get_events(start,end,[e['calendar_id']])['events']
|
||
if not any(x['id']==e['id'] and x['start']==e['start'] and x['recurrence_id']==e['recurrence_id'] for x in found):
|
||
intents.clear_delete()
|
||
return {'type':'text','text':'Cet événement a changé. Suppression annulée.'},'Cet événement a changé. Rien n’a été supprimé.'
|
||
ha.delete_event(e['calendar_id'],e['id'],e['recurrence_id'] or None)
|
||
intents.clear_delete()
|
||
content,speech=_calendar('planning sur 7 jours')
|
||
return content,'Événement supprimé. '+speech
|
||
|
||
|
||
def _confirm_create():
|
||
data=intents.current_create()
|
||
if not data:return {'type':'text','text':'Confirmation expirée.'},'La demande de création a expiré.'
|
||
if os.getenv('ALFRED_ALLOW_CALENDAR_WRITES','false').lower() not in ('1','true','yes'):
|
||
return {'type':'calendar_form','values':data,'calendars':ha.list_calendars()},'L’écriture est désactivée. Active ALFRED_ALLOW_CALENDAR_WRITES dans la configuration.'
|
||
start=datetime.fromisoformat(data['date']+'T'+data['time']).replace(tzinfo=TZ)
|
||
end=start+timedelta(minutes=int(data.get('duration_minutes',60)))
|
||
ha.create_event(data['calendar_id'],data['title'],start,end)
|
||
intents.clear_create()
|
||
content,speech=_calendar('planning sur 7 jours')
|
||
return content,'Événement ajouté. '+speech
|
||
|
||
|
||
def route(text, last_view=None):
|
||
intent=intents.classify(text)
|
||
# Short temporal follow-ups to a displayed calendar: 'Et demain ?', 'Ce matin ?'
|
||
if intent is None and last_view == 'calendar' and len(text.strip()) < 45:
|
||
t=intents.simplify(text)
|
||
if any(word in t for word in ('demain','matin','apres-midi','soir','journee','semaine','mois','jours','novembre','decembre','janvier','fevrier','mars','avril','mai','juin','juillet','aout','septembre','octobre')):
|
||
intent={'kind':'calendar','question':True}
|
||
if intent is None:return None
|
||
kind=intent['kind']
|
||
if kind=='home':return {'type':'home','title':'Alfred','text':'À ton écoute.'},'Je reviens à l’accueil.'
|
||
if kind=='calendar':return _calendar(text, intent.get('question',False))
|
||
if kind=='create':return _create(text)
|
||
if kind=='delete':return _delete(text)
|
||
if kind=='confirm_delete':
|
||
if os.getenv('ALFRED_ALLOW_CALENDAR_WRITES','false').lower() not in ('1','true','yes'):
|
||
return {'type':'text','text':'Écriture calendrier désactivée.'},'La suppression est désactivée dans la configuration.'
|
||
return _confirm_delete()
|
||
if kind=='confirm_create':return _confirm_create()
|
||
if kind=='cancel_pending':
|
||
intents.cancel_pending()
|
||
return {'type':'home','text':'Demande annulée.'},'D’accord, aucune modification effectuée.'
|
||
if kind=='temperatures':
|
||
temps=ha.get_temperatures()
|
||
return {'type':'temperatures','items':temps},f"Voici {len(temps)} mesures de température provenant de Home Assistant."
|
||
if kind=='cameras':
|
||
cameras=ha.list_cameras()
|
||
return {'type':'cameras','items':cameras},f"Voici {len(cameras)} caméras Home Assistant disponibles."
|
||
return None
|