105 lines
3.6 KiB
Python
105 lines
3.6 KiB
Python
"""Home Assistant conversation intents for TaskMate (FEAT-12).
|
|
|
|
Registers intent handlers so a voice/text assistant can answer questions like
|
|
"how many chores does Malia have left?" or "how many stars does Alex have?".
|
|
|
|
The default conversation agent matches *sentences* to these intent names; ship
|
|
the example sentences in ``custom_sentences/<lang>/taskmate.yaml`` into your HA
|
|
config (see custom_sentences/README.md). The speech-building logic is kept in
|
|
pure helpers so it is unit-testable without the conversation stack.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
import voluptuous as vol
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers import config_validation as cv
|
|
from homeassistant.helpers import intent
|
|
|
|
from .const import DOMAIN
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
INTENT_CHORES_LEFT = "TaskMateChoresLeft"
|
|
INTENT_POINTS = "TaskMatePoints"
|
|
|
|
|
|
def _get_coordinator(hass: HomeAssistant):
|
|
from .coordinator import TaskMateCoordinator
|
|
for value in hass.data.get(DOMAIN, {}).values():
|
|
if isinstance(value, TaskMateCoordinator):
|
|
return value
|
|
return None
|
|
|
|
|
|
def _find_child(coordinator, name: str):
|
|
"""Case-insensitive child lookup by name."""
|
|
if coordinator is None or not name:
|
|
return None
|
|
target = name.strip().lower()
|
|
for child in coordinator.storage.get_children():
|
|
if child.name.strip().lower() == target:
|
|
return child
|
|
return None
|
|
|
|
|
|
def chores_left_speech(coordinator, name: str) -> str:
|
|
"""Speech for 'how many chores does <name> have left'."""
|
|
child = _find_child(coordinator, name)
|
|
if child is None:
|
|
return f"I couldn't find anyone called {name}."
|
|
count = len(coordinator.get_due_chores_for_child(child.id))
|
|
if count == 0:
|
|
return f"{child.name} has finished all their chores. Nice work!"
|
|
if count == 1:
|
|
return f"{child.name} has 1 chore left today."
|
|
return f"{child.name} has {count} chores left today."
|
|
|
|
|
|
def points_speech(coordinator, name: str) -> str:
|
|
"""Speech for 'how many points does <name> have'."""
|
|
child = _find_child(coordinator, name)
|
|
if child is None:
|
|
return f"I couldn't find anyone called {name}."
|
|
points_name = coordinator.storage.get_points_name()
|
|
return f"{child.name} has {child.points} {points_name}."
|
|
|
|
|
|
class _TaskMateIntentBase(intent.IntentHandler):
|
|
slot_schema = {vol.Required("name"): cv.string}
|
|
|
|
def _speech(self, coordinator, name: str) -> str: # pragma: no cover - overridden
|
|
raise NotImplementedError
|
|
|
|
async def async_handle(self, intent_obj):
|
|
hass = intent_obj.hass
|
|
coordinator = _get_coordinator(hass)
|
|
name = ""
|
|
slot = (intent_obj.slots or {}).get("name") or {}
|
|
name = slot.get("value", "") if isinstance(slot, dict) else str(slot)
|
|
response = intent_obj.create_response()
|
|
response.async_set_speech(self._speech(coordinator, name))
|
|
return response
|
|
|
|
|
|
class ChoresLeftIntentHandler(_TaskMateIntentBase):
|
|
intent_type = INTENT_CHORES_LEFT
|
|
|
|
def _speech(self, coordinator, name: str) -> str:
|
|
return chores_left_speech(coordinator, name)
|
|
|
|
|
|
class PointsIntentHandler(_TaskMateIntentBase):
|
|
intent_type = INTENT_POINTS
|
|
|
|
def _speech(self, coordinator, name: str) -> str:
|
|
return points_speech(coordinator, name)
|
|
|
|
|
|
def async_setup_intents(hass: HomeAssistant) -> None:
|
|
"""Register TaskMate conversation intents (idempotent enough for reloads)."""
|
|
intent.async_register(hass, ChoresLeftIntentHandler())
|
|
intent.async_register(hass, PointsIntentHandler())
|
|
_LOGGER.debug("Registered TaskMate conversation intents")
|