This commit is contained in:
pporcheret
2026-10-26 00:06:24 +01:00
parent c9c3d5f223
commit cd37ac3c0a
207 changed files with 19205 additions and 54 deletions
@@ -0,0 +1,132 @@
"""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
from . import ha_weather
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):
# Weather is a read-only dynamic view; avoid sending routine forecasts to Hermes.
if ha_weather.is_weather_request(text, last_view):
data = ha_weather.get_weather()
return data, ha_weather.describe(data, text)
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