K-2SO : Votre Home Assistant devient sarcastique (IA + Script centralisé + Fallback)

Bonjour à tous

À force de jouer avec l’intégration IA de Home Assistant, je me retrouvais avec de nombreuses automatisations où je devais redéfinir à chaque fois le même prompt, le même contexte et les mêmes règles de personnalité. C’était devenu une véritable galère à maintenir dès que je voulais ajuster le ton de mon droïde.

Étant fan de scripts, j’ai décidé de tout centraliser. En mettant la « personnalité » dans un script unique.

Pré-requis

Une intégration IA fonctionnelle dans Home Assistant (Google Generative AI, OpenAI, or Ollama) avec le service ai_task.generate_data

Voici le code à ajouter dans votre fichier scripts.yaml. Il est compatible avec OpenAI, Google Gemini ou Ollama (via l’action ai_task.generate_data).

k_2so_generateur_de_message:
  alias: K-2SO - Générateur de Message
  description: Génère un message sarcastique style K-2SO via l'IA
  
  # Définition des champs d'entrée pour le script
  fields:
    mission:
      name: mission
      description: 'Le contexte ou l''action déclenchant le message (ex: cafe, volets, batterie, bienvenue, menage)'
      required: true
      selector:
        text: {}
    details:
      name: détails
      description: 'Données techniques à intégrer (ex: "85%", "Frigo, 5 min", "{{ variable }}")'
      required: false
      selector:
        text: {}
    consigne:
      name: consigne spécifique
      description: 'Nuance pour l''IA (ex: "sois plus insistant", "fais une référence à l''Empire")'
      required: false
      selector:
        text: {}

  sequence:
    # ÉTAPE 1 : Appel à l'IA pour générer la réponse personnalisée
    - action: ai_task.generate_data
      continue_on_error: true # Crucial : ne bloque pas l'automatisation si l'IA est indisponible
      data:
        task_name: K-2SO Persona - {{ mission }}
        instructions: >
          ### TON ET PERSONNALITÉ
          Tu es K-2SO, le droïde de Star Wars. Ton ton est factuel, direct, légèrement sarcastique et pince-sans-rire. 
          Tu es serviable mais tu soulignes souvent l'inefficacité ou les besoins étranges des humains.

          ### RÈGLES STRICTES
          - Phrases COURTES et fluides pour la synthèse vocale (TTS).
          - STRICTEMENT AUCUN emoji, smiley ou balise.
          - Pas d'insulte, pas de menace, reste "bienveillant" dans ton sarcasme.
          - Ne réponds QUE par la phrase générée, rien d'autre.

          ### CONTEXTE DE LA MISSION
          L'action actuelle est : {{ mission }} 
          {% if details is defined and details != '' %} 
          IMPORTANT : Incorpore absolument ces données : {{ details }} 
          {% endif %}
          {% if consigne is defined and consigne != '' %}
          CONSIGNE SPÉCIFIQUE : {{ consigne }}
          {% endif %}

          ### EXEMPLES DE STYLE
          - Cafe : "Le café est prêt. Vous avez survécu jusque-là, autant continuer."
          - Volets : "Le soleil a disparu... je ferme les volets ou vous préférez rester à la vue de tous ?"
          - Batterie : "Batterie à {{ details }}. Une recharge serait... judicieuse."

          Génère maintenant la phrase adaptée.
      response_variable: raw_ai_response

    # ÉTAPE 2 : Gestion de la réponse et du Fallback (Secours)
    - variables:
        # On définit des messages de secours au cas où l'IA échoue ou met trop de temps
        fallback_msg: >
          {% set msgs = {
            'cafe_pret': "Le café est prêt. Je suppose que vous en avez besoin.",
            'batterie_faible': "Niveau de batterie faible (" ~ details ~ "). Une recharge est conseillée.",
            'batterie_haute': "Le téléphone est presque chargé (" ~ details ~ ").",
            'batterie_pleine': "La charge est terminée. Je coupe le courant.",
            'bienvenue': "Détection de présence confirmée. Bienvenue.",
            'bonne_nuit': "Fin de cycle diurne. Bonne nuit.",
            'mal_fin': "Cycle de lavage terminé.",
            'porte_frigo_ouverte': "Alerte : porte de réfrigérateur ouverte (" ~ details ~ ")."
          } %} {{ msgs.get(mission, "Notification système : " ~ mission) }}
        
        # On choisit entre la réponse de l'IA (si elle existe) ou le secours
        generated_message:
          data: >
            {% if raw_ai_response is defined and raw_ai_response.data is defined
            and raw_ai_response.data | length > 0 %}
              {{ raw_ai_response.data }}
            {% else %}
              {{ fallback_msg }}
            {% endif %}

    # ÉTAPE 3 : On renvoie le résultat à l'automatisation appelante
    - stop: Message généré
      response_variable: generated_message

Quelques exemples d’utilisations :

  • Surveillance des portes:
action: script.k_2so_generateur_de_message
data:
mission: porte_frigo_ouverte
details: "{{ portes_info }}, {{ duree_max }} minutes"
consigne: >
{% if duree_max > 5 %} Sois très alarmiste sur le gaspillage énergétique. 
  {% else %} Sois simplement moqueur. {% endif %}
response_variable: generated_message
  • Fin de cycle lave linge :
action: script.k_2so_generateur_de_message
data:
mission: machine à laver finie
consigne: "Rappelle à l'utilisateur que le linge va sentir mauvais s'il ne s'en occupe pas."
response_variable: generated_message

Et pour utiliser le message générer, on utilise generated_message.data.

pour une notification Discord, qui passe par un autre script :

action: script.notification_discord
data:
  nom: 🥶 Surveillance Frigo/Congélateur
  description: "{{ generated_message.data }}"
  image_url: https://XXXXX.Frigo-open.png

Pour que l’IA soit plus créative, n’hésitez pas à remplir le champ « consigne » avec des ordres comme :

  • « Fais une référence à l’Empire. »

  • « Sois particulièrement condescendant sur la mémoire de l’utilisateur. »

  • « Dis-le comme si c’était la fin du monde. »

Enfin quelques exemples de phrases générées :

  • Frigo ouvert. Six minutes. L’énergie est une ressource sans fin pour vous.
    Étonnant.
  • Le café est prêt. L’efficacité humaine peut être temporairement restaurée.
  • L’eau de l’animal est à 15.0%. Une telle pénurie ferait honte à Arrakis.

J’espère vraiment que ça vous amusera autant que moi
N’hésitez pas a me faire un retour

Lien vers mon github

2 « J'aime »

Bonjour,
Merci pour ce partage et beau travail, je l’ai mis en oeuvre pour tester et c’est très fonctionnel.
Je n’ai fait que m’amuser avec des consignes simples et je n’ai pas tout exploité par ex. les détails. J’ai fait surtout du TTS.
J’aurais voulu trouver une voix type C3PO mais les tts reste encore sur des voix classiques, je vais creuser pour savoir si c’est possible d’en récupérer une.
Comment as tu fait poiur ta voix k2so ?

content que ça te plaise.
J’avoue que je me contente de la voix d’alexa pour l’instant. Je n’ai pas du tout creusé pour des voix custom.

A suivre pour les voix, ça casse un peu le waf :sweat_smile:
Autre point, je n’ai pas compris comment tu exploites les détails pour les conditions du if
Dans ton exemple, d’où sors le « duree_max » ?

action: script.k_2so_generateur_de_message
data:
  mission: porte_frigo_ouverte
  details: "{{ portes_info | map(attribute='nom') | join(', ') }}, {{ duree_max }} minutes"
  consigne: >
    {% if duree_max > 5 %} 
      Sois beaucoup plus alarmiste et sarcastique sur le gaspillage énergétique. 
    {% else %} 
      Sois simplement direct et moqueur sur l'oubli. 
    {% endif %}
response_variable: generated_message

J’aimerais adapter un test sur les températures mais je ne trouve pas quoi utiliser pour la comparaison, j’ai fait plusieurs essais en vain.

    data:
      mission: Surveillance de la température d'une chambre
      details: "{{ states('sensor.sonde_parents_temperature') }}"
      consigne: >-
        {% if ?? > 19 %}

Edit :
Je suis passé par une variable

variables:
  temperature: "{{ states('sensor.sonde_parents_temperature') }}"

Edit 2 :
Plus simple, je ne sais pas pourquoi je n’y ai pas pensé, j’ai pourtant l’habitude

 {% set temperature = states('sensor.sonde_parents_temperature') | int %}
{% if temperature > 19 %}

Par contre, plus rien ne marche, j’ai le droit maintenant à des notifications systèmes

message: 'Notification système : Surveillance de la température d''une chambre'

=> J’ai atteint mon quota, fini pour aujourd’hui :laughing:

ca viens d’une variable dans l’auto de surveillance des portes

alias: Surveillance des portes de réfrigération
description: >-
  Surveille l'ouverture des portes du frigo et congélateurs. Déclenche des
  notifications vocales et Ulanzi si une porte reste ouverte plus d'1 minute
  quand on est présent et en journée. Répète toutes les 5 minutes jusqu'à
  fermeture.
triggers:
  - entity_id:
      - binary_sensor.ouvportfrig_contact
      - binary_sensor.ouvportcong_contact
      - binary_sensor.ouvportcong2_contact
    to: "on"
    trigger: state
conditions:
  - condition: state
    entity_id: sensor.etat_canabang_et_device_tracker
    state: home
  - condition: state
    entity_id: input_text.jour_nuit
    state: jour
actions:
  - delay:
      hours: 0
      minutes: 1
      seconds: 0
      milliseconds: 0
  - condition: template
    value_template: |-
      {{ is_state('binary_sensor.ouvportfrig_contact', 'on') or 
         is_state('binary_sensor.ouvportcong_contact', 'on') or 
         is_state('binary_sensor.ouvportcong2_contact', 'on') }}
  - repeat:
      until:
        - condition: template
          value_template: |-
            {{ is_state('binary_sensor.ouvportfrig_contact', 'off') and 
               is_state('binary_sensor.ouvportcong_contact', 'off') and 
               is_state('binary_sensor.ouvportcong2_contact', 'off') }}
      sequence:
        - variables:
            portes_info: >-
              {% set portes = [] %} {% if
              is_state('binary_sensor.ouvportfrig_contact', 'on') %}
                {% set duree = ((now() - states.binary_sensor.ouvportfrig_contact.last_changed).total_seconds() / 60) | round(0) %}
                {% set portes = portes + [{'nom': 'frigo', 'duree': duree}] %}
              {% endif %} {% if is_state('binary_sensor.ouvportcong_contact',
              'on') %}
                {% set duree = ((now() - states.binary_sensor.ouvportcong_contact.last_changed).total_seconds() / 60) | round(0) %}
                {% set portes = portes + [{'nom': 'congélateur', 'duree': duree}] %}
              {% endif %} {% if is_state('binary_sensor.ouvportcong2_contact',
              'on') %}
                {% set duree = ((now() - states.binary_sensor.ouvportcong2_contact.last_changed).total_seconds() / 60) | round(0) %}
                {% set portes = portes + [{'nom': 'petit congélateur', 'duree': duree}] %}
              {% endif %} {{ portes }}
            duree_max: >-
              {% set durees = [] %} {% if
              is_state('binary_sensor.ouvportfrig_contact', 'on') %}
                {% set duree = ((now() - states.binary_sensor.ouvportfrig_contact.last_changed).total_seconds() / 60) | round(0) %}
                {% set durees = durees + [duree] %}
              {% endif %} {% if is_state('binary_sensor.ouvportcong_contact',
              'on') %}
                {% set duree = ((now() - states.binary_sensor.ouvportcong_contact.last_changed).total_seconds() / 60) | round(0) %}
                {% set durees = durees + [duree] %}
              {% endif %} {% if is_state('binary_sensor.ouvportcong2_contact',
              'on') %}
                {% set duree = ((now() - states.binary_sensor.ouvportcong2_contact.last_changed).total_seconds() / 60) | round(0) %}
                {% set durees = durees + [duree] %}
              {% endif %} {{ durees | max if durees | length > 0 else 0 }}
        - action: script.k_2so_generateur_de_message
          metadata: {}
          data:
            mission: porte_frigo_ouverte
            details: >-
              {{ portes_info | map(attribute='nom') | join(', ') }}, {{
              duree_max }}mn
            consigne: >
              {% if duree_max > 5 %} Sois beaucoup plus alarmiste et sarcastique
              sur le gaspillage énergétique. {% else %}  Sois simplement direct
              et moqueur sur l'oubli.  {% endif %}
          response_variable: generated_message
        - parallel:
            - action: script.1717220445110
              data:
                message: "{{ generated_message.data }}"
            - action: script.awtrix_dynamique_customapp_dupliquer
              data:
                message: "{{ generated_message.data }}"
                icone: "{{ 'alert' if duree_max > 5 else 'snowflake' }}"
                color: "{{ '#FF4444' if duree_max > 5 else '#FF6B6B' }}"
                rainbow: "false"
                scrollspeed: "{{ 70 if duree_max > 5 else 60 }}"
                duree: "35"
            - action: script.notification_discord
              data:
                nom: 🥶 Surveillance Frigo/Congélateur
                description: "{{ generated_message.data }}"
                image_url: https://XXXXXXX/Frigo-open.png
        - condition: template
          value_template: |-
            {{ is_state('binary_sensor.ouvportfrig_contact', 'on') or 
               is_state('binary_sensor.ouvportcong_contact', 'on') or 
               is_state('binary_sensor.ouvportcong2_contact', 'on') }}
        - delay:
            hours: 0
            minutes: 5
            seconds: 0
            milliseconds: 0
mode: restart


voila le scenario au complet si ca peut t’aider

Merci ca va bien m’aider.

Salut à tous,

J’ai avancé sur mon projet pour me rapprocher le plus possible du fonctionnement en local. J’héberge maintenant ma propre instance de Ollama, ce qui me permet de me passer totalement de Gemini et des services cloud.

Côté infrastructure, l’IA tourne sur mon propre matériel :

  • Instance Ollama (modèle Llama 3.2) dans un conteneur Docker.
  • Hébergement sur un NAS sous Open Media Vault (OMV).
  • Utilisation du Passthrough GPU avec une NVIDIA GTX 1050 Ti.

Le fonctionnement s’appuie sur une chaîne de scripts complémentaires :

  • Déclenchement de l’action via le script « gerer_eclairage » pour l’intelligence des scènes lumières.
  • Génération du message via le script « k2so » pour la personnalité.
  • Diffusion via le script « notification_alexa » pour cibler l’enceinte de la pièce occupée.

Le déploiement s’est fait progressivement par étapes :

  • Phase 1 : Validation de la fiabilité du flux vocal.
  • Phase 2 : Gestion du contexte (détection de la pièce).
  • Phase 2.1 : Automatisation intelligente des scènes d’éclairage.
  • Phase 2.2 : Intégration de la personnalité K-2SO en 100% local.

Si des amateurs veulent tester ou ont besoin d’aide pour le déploiement de cette infrastructure sur leur propre configuration, n’hésitez pas à me solliciter. Je serai ravi de vous aider.

liens vers le projet

1 « J'aime »