← Interactions

Moteur d'interaction SMS

Étape 1 : normaliser un SMS et valider son format sans l'interpréter.

Périmètre

Cette première étape réalise uniquement :

  1. la normalisation du texte ;
  2. l’identification d’une commande ou d’une interrogation ;
  3. l’extraction de la pièce et du contenu restant ;
  4. le rejet des formats invalides.

Aucune entité Home Assistant n’est recherchée et aucune action n’est exécutée.

Formats acceptés

Commande :

<Pièce>, <action> <paramètres>

Interrogation :

<Pièce>, <question> ?

Exemples valides :

Salon, allume lumière
Bureau, règle chauffage à 19
Salon, température ?
Garage, état porte ?

Validateur

import re
import unicodedata
from dataclasses import dataclass
from enum import Enum


class SmsType(Enum):
    COMMAND = "command"
    QUERY = "query"
    INVALID = "invalid"


@dataclass
class ValidationResult:
    valid: bool
    sms_type: SmsType
    normalized_message: str
    piece: str | None = None
    payload: str | None = None
    error: str | None = None


COMMAND_RE = re.compile(
    r"^(?P<piece>[a-z0-9][a-z0-9 _-]*)"
    r"\s*,\s*"
    r"(?P<payload>[a-z0-9][a-z0-9 _.,°%+-]*)"
    r"[.!]?$"
)

QUERY_RE = re.compile(
    r"^(?P<piece>[a-z0-9][a-z0-9 _-]*)"
    r"\s*,\s*"
    r"(?P<payload>[a-z0-9][a-z0-9 _.,°%+-]*)"
    r"\s*\?$"
)


def normalize_sms(message: str) -> str:
    message = unicodedata.normalize("NFKD", message)
    message = "".join(
        character
        for character in message
        if not unicodedata.combining(character)
    )
    message = re.sub(r"\s+", " ", message)
    return message.strip().lower()


def validate_sms(message: str) -> ValidationResult:
    normalized = normalize_sms(message)

    if not normalized:
        return ValidationResult(
            valid=False,
            sms_type=SmsType.INVALID,
            normalized_message=normalized,
            error="empty_message",
        )

    match = QUERY_RE.fullmatch(normalized)
    if match:
        return ValidationResult(
            valid=True,
            sms_type=SmsType.QUERY,
            normalized_message=normalized,
            piece=match.group("piece").strip(),
            payload=match.group("payload").strip(),
        )

    match = COMMAND_RE.fullmatch(normalized)
    if match:
        return ValidationResult(
            valid=True,
            sms_type=SmsType.COMMAND,
            normalized_message=normalized,
            piece=match.group("piece").strip(),
            payload=match.group("payload").strip(),
        )

    return ValidationResult(
        valid=False,
        sms_type=SmsType.INVALID,
        normalized_message=normalized,
        error="invalid_format",
    )

Résultats attendus

SMSTypePiècePayload
Salon, allume lumièrecommandsalonallume lumiere
Salon, température ?querysalontemperature
allume lumière saloninvalid——
chaîne videinvalid——

Contrat de sortie

ChampTypeDescription
validboolRésultat de la validation
sms_typeSmsTypecommand, query ou invalid
normalized_messagestrMessage sans accents, en minuscules et avec espaces normalisés
piecestr | NonePièce extraite
payloadstr | NoneContenu restant, non interprété
errorstr | Noneempty_message ou invalid_format

Étape suivante

L’analyse du payload devra produire une intention structurée sans exécuter directement l’action :

payload
  → verbe
  → équipement ou information
  → valeur optionnelle
  → intention validée