Home Assistant & Mi Boxer

J'avais acheté cette télécommande Mi Boxer il y a quelques mois afin de gérer les éclairages de mon séjour, en me disant que les curseurs seraient plus pratiques que mon Opple avec ses 6 boutons actuelle. Hélas elle n'était reconnue nulle part et avait terminé sa course dans le tiroir aux oubliettes du Zigbee...

C'était sans compter sur la ténacité de quelques amateurs de reverse engineering sur lesquels je suis tombé il y a quelques semaines et qui on fait un travail formidable qui a aboutit à une extension pour Zigbee2MQTT, ce qui rend cette télécommande enfin utilisable, tout au moins partiellement pour l'instant. Mais l'essentiel est là !

Ce qui fonctionne :

  • 7 boutons avec ON et OFF (deux actions et non un toggle). Celui du bas à droite n'est pas actif et je vous déconseille de l'utiliser...
  • 1 bouton avec ON et OFF (en haut)
  • 1 bouton W (si on veut bricoler...)
  • La barre de réglage de la luminosité
  • La remontée de l'état de la batterie

Ce qui ne fonctionne pas (pour l'instant) :

  • La barre de réglage des couleurs
  • La barre de réglage de la température du blanc
  • Les touches RGB
  • Les touches de temporisation (en bas à droite)

Une fois la télécommande reconnue sous Z2M on peut commencer à créer des actions. On remarque tout de suite que l'affaire va manquer de sensor: et que certains ne fonctionnent pas. Pas de panique, il y a une solution comme je vais vous le démonter avec l'automation: qui suit. 

J'ai fait un mélange de choose: / conditions: / trigger.id . C'est un peu long, mais je la colle en entier ce qui vous évitera une fastidieuse saisie. Il faudra tout de même y coller vous id: et entity: ! Ah j'allais oublier, il faut aussi ajouter un petit input_select: ... Voir plus bas EDIT du 11/01/2024 !

J'ai tenté de faire ça avec ControllerX que j'adore et qui me sert pour toutes mes télécommandes, mais pour l'instant c'est un échec !

alias: GUI - Mi Boxer
description: ""
trigger:
  - platform: state
    entity_id:
      - sensor.mi_boxeur_brightness
    id: bright
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_1_on
    discovery_id: 0x003c84fffec29c71_zone_1_button_on
    id: 1on
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_1_off
    discovery_id: 0x003c84fffec29c71_zone_1_button_off
    id: 1off
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_2_on
    discovery_id: 0x003c84fffec29c71_zone_2_button_on
    id: 2on
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_2_off
    discovery_id: 0x003c84fffec29c71_zone_2_button_off
    id: 2off
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_3_on
    discovery_id: 0x003c84fffec29c71_zone_3_button_on
    id: 3on
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_3_off
    discovery_id: 0x003c84fffec29c71_zone_3_button_off
    id: 3off
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_4_on
    discovery_id: 0x003c84fffec29c71_zone_4_button_on
    id: 4on
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_4_off
    discovery_id: 0x003c84fffec29c71_zone_4_button_off
    id: 4off
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_5_on
    discovery_id: 0x003c84fffec29c71_zone_5_button_on
    id: 5on
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_5_off
    discovery_id: 0x003c84fffec29c71_zone_5_button_off
    id: 5off
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_6_on
    discovery_id: 0x003c84fffec29c71_zone_6_button_on
    id: 6on
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_6_off
    discovery_id: 0x003c84fffec29c71_zone_6_button_off
    id: 6off
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_7_on
    discovery_id: 0x003c84fffec29c71_zone_7_button_on
    id: 7on
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_7_off
    discovery_id: 0x003c84fffec29c71_zone_7_button_off
    id: 7off
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_8_on
    discovery_id: 0x003c84fffec29c71_zone_8_button_on
    id: 8on
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_8_off
    discovery_id: 0x003c84fffec29c71_zone_8_button_off
    id: 8off
    
condition: []
action:
  - choose:
      - conditions: "{{ trigger.id in ['1on'] }}"
        sequence:
          - service: light.turn_on
            data: {}
            target:
              entity_id: light.shellydimmer_f3d426
          - service: input_select.select_option
            data:
              option: "1"
            target:
              entity_id: input_select.mi_boxer_select
      - conditions: "{{ trigger.id in ['1off'] }}"
        sequence:
          - service: light.turn_off
            data: {}
            target:
              entity_id: light.shellydimmer_f3d426

      - conditions: "{{ trigger.id in ['2on'] }}"
        sequence:
          - service: light.turn_on
            data: {}
            target:
              entity_id: light.shellydimmer_d3e57c
          - service: input_select.select_option
            data:
              option: "2"
            target:
              entity_id: input_select.mi_boxer_select
      - conditions: "{{ trigger.id in ['2off'] }}"
        sequence:
          - service: light.turn_off
            data: {}
            target:
              entity_id: light.shellydimmer_d3e57c

      - conditions: "{{ trigger.id in ['3on'] }}"
        sequence:
          - service: light.turn_on
            data: {}
            target:
              entity_id: light.ikea_e27_tv
          - service: input_select.select_option
            data:
              option: "3"
            target:
              entity_id: input_select.mi_boxer_select
      - conditions: "{{ trigger.id in ['3off'] }}"
        sequence:
          - service: light.turn_off
            data: {}
            target:
              entity_id: light.ikea_e27_tv

      - conditions: "{{ trigger.id in ['4on'] }}"
        sequence:
          - service: light.turn_on
            data: {}
            target:
              entity_id: light.lidl_led_stand
          - service: input_select.select_option
            data:
              option: "4"
            target:
              entity_id: input_select.mi_boxer_select
      - conditions: "{{ trigger.id in ['4off'] }}"
        sequence:
          - service: light.turn_off
            data: {}
            target:
              entity_id: light.lidl_led_stand

      - conditions: "{{ trigger.id in ['5on'] }}"
        sequence:
          - service: light.turn_on
            data: {}
            target:
              entity_id: light.dimmable_sm309
          - service: input_select.select_option
            data:
              option: "5"
            target:
              entity_id: input_select.mi_boxer_select
      - conditions: "{{ trigger.id in ['5off'] }}"
        sequence:
          - service: light.turn_off
            data: {}
            target:
              entity_id: light.dimmable_sm309

      - conditions: "{{ trigger.id in ['6on'] }}"
        sequence:
          - service: light.turn_on
            data: {}
            target:
              entity_id: light.shelly_bulb_1
          - service: input_select.select_option
            data:
              option: "6"
            target:
              entity_id: input_select.mi_boxer_select
      - conditions: "{{ trigger.id in ['6off'] }}"
        sequence:
          - service: light.turn_off
            data: {}
            target:
              entity_id: light.shelly_bulb_1

      - conditions: "{{ trigger.id in ['7on'] }}"
        sequence:
          - service: light.turn_on
            data: {}
            target:
              entity_id: light.led_strip_1
          - service: input_select.select_option
            data:
              option: "7"
            target:
              entity_id: input_select.mi_boxer_select
      - conditions: "{{ trigger.id in ['7off'] }}"
        sequence:
          - service: light.turn_off
            data: {}
            target:
              entity_id: light.led_strip_1

      - conditions: "{{ trigger.id in ['8on'] }}"
        sequence:
          - service: light.turn_on
            data: {}
            target:
              entity_id: 
                - light.shellydimmer_f3d426
                - light.shellydimmer_d3e57c
                - light.ikea_e27_tv
                - light.dimmable_sm309
                - light.shelly_bulb_1
                - light.lidl_led_stand
                - light.plug_tz_10_switch
                - light.led_strip_1
          - service: input_select.select_option
            data:
              option: "8"
            target:
              entity_id: input_select.mi_boxer_select
      - conditions: "{{ trigger.id in ['8off'] }}"
        sequence:
          - service: light.turn_off
            data: {}
            target:
              entity_id: 
                - light.shellydimmer_f3d426
                - light.shellydimmer_d3e57c
                - light.ikea_e27_tv
                - light.dimmable_sm309
                - light.shelly_bulb_1
                - light.lidl_led_stand
                - light.plug_tz_10_switch
                - light.led_strip_1
              
      - conditions: >-
          {{ is_state('input_select.mi_boxer_select', '1') and trigger.id in ['bright'] }}
        sequence:
          - service: light.turn_on
            data:
              brightness_pct: "{{ trigger.to_state.state }}"
              transition: 0.2
            target:
              entity_id: light.shellydimmer_f3d426
            enabled: true

      - conditions: >-
          {{ is_state('input_select.mi_boxer_select', '2') and trigger.id in ['bright'] }}
        sequence:
          - service: light.turn_on
            data:
              brightness_pct: "{{ trigger.to_state.state }}"
              transition: 0.2
            target:
              device_id: 1ff4112785e14b8b8cba18d45fec3b11
            enabled: true

      - conditions: >-
          {{ is_state('input_select.mi_boxer_select', '3') and trigger.id in ['bright'] }}
        sequence:
          - service: light.turn_on
            data:
              brightness_pct: "{{ trigger.to_state.state }}"
              transition: 0.2
            target:
              entity_id: light.ikea_e27_tv
            enabled: true

      - conditions: >-
          {{ is_state('input_select.mi_boxer_select', '4') and trigger.id in ['bright'] }}
        sequence:
          - service: light.turn_on
            data:
              brightness_pct: "{{ trigger.to_state.state }}"
              transition: 0.2
            target:
              entity_id: light.lidl_led_stand
            enabled: true

      - conditions: >-
          {{ is_state('input_select.mi_boxer_select', '5') and trigger.id in ['bright'] }}
        sequence:
          - service: light.turn_on
            data:
              brightness_pct: "{{ trigger.to_state.state }}"
              transition: 0.2
            target:
              entity_id: light.dimmable_sm309
            enabled: true

      - conditions: >-
          {{ is_state('input_select.mi_boxer_select', '6') and trigger.id in ['bright'] }}
        sequence:
          - service: light.turn_on
            data:
              brightness_pct: "{{ trigger.to_state.state }}"
              transition: 0.2
            target:
              entity_id: light.shelly_bulb_1
            enabled: true

      - conditions: >-
          {{ is_state('input_select.mi_boxer_select', '7') and trigger.id in ['bright'] }}
        sequence:
          - service: light.turn_on
            data:
              brightness_pct: "{{ trigger.to_state.state }}"
              transition: 0.2
            target:
              entity_id: light.led_strip_1
            enabled: true
mode: restart

EDIT 11/01/2024

Avec la dernière mise à jour Zigbee2MQTT (1.35.1-1) il n’y a plus besoin de l’extension. Par contre ce que j’avais fait sur la base de ce qui est proposé ici ne fonctionne plus (bien que toujours dans la doc).

J’ai donc biaisé en faisant du mqtt direct :

Pour la détection (trigger:) des boutons (à dupliquer) :

- platform: mqtt
    topic: zigbee2mqtt/Mi Boxer
    payload: ('on', 101)
    value_template: "{{ value_json.action , value_json.action_group }}"
    id: 1on
  - platform: mqtt
    topic: zigbee2mqtt/Mi Boxer
    payload: ('off', 101)
    value_template: "{{ value_json.action , value_json.action_group }}"
    id: 1off

Et pour la luminosité (il me reste à trouver comment ne prendre en compte que la charge action_level) :

- platform: mqtt
    topic: zigbee2mqtt/Mi Boxer
    id: bright

Ensuite on modifie dans le chose: coté action (à dupliquer bien entendu :

      - conditions:
          - condition: template
            value_template: >-
              {{ is_state('input_select.mi_boxer_select', '2') and trigger.id in ['bright'] }}
        sequence:
          - service: light.turn_on
            data_template:
              entity_id: light.shellydimmer_d3e57c
              brightness_pct: "{{ trigger.payload_json.action_level }}"

EDIT 11/01/2024

Le problème avec la solution de mon EDIT précédent est que le trigger sur la charge MQTT globale génère trop de bruit. On va donc faire deux sensor: basés sur MQTT afin d'isoler la valeur de la luminosité (merci Mathieu) ainsi que le groupe (bouton) correspondant à la light: active, ce qui va nous permettre d'éliminer l'input_select: dans le filtrage à venir :

mqtt:
  sensor:
    - name: "Mi Boxer Bright"
      unique_id: "mi_boxer_bright"
      state_topic: "zigbee2mqtt/Mi Boxer"
      value_template: "{{ value_json.action_level |int }}"

    - name: "Mi Boxxer Group"
      unique_id: "mi_boxer_group"
      state_topic: "zigbee2mqtt/Mi Boxer"
      value_template: "{{ value_json.action_group |int }}"

En trigger: on utilisera l'action : action_brightness_move_to_level qui remonte dans HA :

trigger:
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: action
    subtype: brightness_move_to_level
    discovery_id: 0x003c84fffec29c71 action_brightness_move_to_level
    id: bright

Et on complète notre action déclenchée par l'id: bright et filtrée sur le sensor: correspondant au bon bouton, et j'ai ajouté le light: actif afin de ne pas risquer une fausse manœuvre :

      - conditions:
          - condition: template
            value_template: >-
              {{ is_state('sensor.mi_boxer_group', '101') and trigger.id in ['bright'] and is_state('light.shellydimmer_f3d426', 'on') }}
        sequence:
          - service: light.turn_on
            data_template:
              entity_id: light.shellydimmer_f3d426
              brightness_pct: "{{ states('sensor.mi_boxer_bright') | int}}"

Il ne reste plus qu'à attendre que Z2M remonte les informations liées à la température du blanc et la roue chromatique et on pourra les traiter avec la même méthode.

 
 

 

 

RustDesk

Au fil des ans TeamViewer s’est imposé dans l'IT. Au départ pseudo gratuit ce produit offre maintenant de plus en plus de fonctionnalités (souvent inutiles) et son cout augmente d’années en années. On pourrait imaginer le remplacer par AnyDesk, mais hélas leur politique est quasiment identique et ce serait reculer pour mieux sauter.

Pendant le confinement certains en ont eu assez de se faire “raquetter”. Si le mot vous parait fort, constatez simplement les augmentations successives dont la dernière de 20% que rien ne justifie à mes yeux, l'éditeur se justifiant quant à lui par l'ajout de fonctionnalités qui me sont inutiles. Entendons nous bien, tout a un prix et tout travail ou service mérite une rémunération, mais tout doit rester dans la nuance, même quand on se retrouve dans une position de domination, ce qui est d'ailleurs le cas pour d'autres, comme Microsoft et ses dernières augmentations au niveau 365 ou Azure...

Bref, c'est donc ainsi qu'une petite équipe de développeurs a créé RustDesk. RustDesk offre les mêmes fonctionnalités de base dans le cadre de l’open source. Je teste Rustdesk depuis près d’un an et j’estime que l’heure est venue de franchir le pas.

RustDesk est disponible sous différentes distributions Linux, MacOS, Windows, Android et IOS.

Il existe plusieurs façons d’utiliser RustDesk :

Version isolée sans serveur

La plus simple et totalement gratuite, mais qui présente quelques inconvénients :

  • Pas de chiffrement, cela veut dire que toutes les infos circulent en clair (pas sécure pour les clients, mauvaise pratique).
  • Lenteurs possibles car ça utilise les serveurs publics, ce qui reste limité.
  • Pas de carnet d’adresse synchronisé entre les devices clients. Et là ça bloque car comme beaucoup de monde j'utilise un PC fixe au bureau et un laptop quand je me déplace, sur lequel je souhaite retrouver mon environnement de travail. Ils auraient pu envisager une synchronisation de type "drive", mais cela aurait posé des questions de sécurité car en plus de synchroniser les identifiants il faut stocker des mots de passe.

Version gratuite avec serveur

Toujours gratuit (en dehors du cout d'un serveur VPS (qui peut être mutualisé)), et on résous la question du chiffrement :

  • Le serveur sert de relais (amélioration de la vitesse) et de serveur de chiffrement afin de sécuriser les liens.

Version payante avec serveur privé

La totale qui va couter quelques euros (10 € / mois pour deux utilisateurs, ou 20 € /mois pour 20 utilisateurs + le serveur), mais à mon sens indispensable dans un cadre professionnel

  • Le serveur offre une console qui permet de lister les clients installés et éventuellement les partager
    • Seul l’admin peut activer les partages à ce stade. Partager fait remonter les ID pour un autre utilisateur, mais le mot de passe associé au device reste nécessaire.
  • Avoir des logs, qui se connecte à quoi (dispo pour chaque utilisateur), transferts de fichiers.
  • Changer l’ID d’un client
  • Utiliser un carnet d’adresse qui sera synchronisé entre les clients de chaque utilisateur, mais non accessible à l’admin.
  • Appliquer des stratégies
  • Et d'autres fonctionnalités à venir car le projet est en constante évolution....

Il n'existe pour l'instant pas de version Cloud et je n'ai pas le sentiment que ce soit dans l'ordre des priorités de l'équipe.

Installation

Je me place ici dans le cadre de la version serveur après avoir acquis une licence.

L'installation du serveur se fait sous Linux ou dans un container Docker. La procédure est décrite ici et je ne vais pas la reprendre. J'ai choisit Docker pour la facilité d'un Compose...

Coté client, contrairement à TeamViewer qui fonctionne uniquement avec une infrastructure cloud, RustDesk fonctionne avec une infrastructure privée si l’on veut disposer de toutes ses fonctionnalités. Quand on installe le client il va donc falloir configurer les informations liées au serveur. Ce device sera alors uniquement accessible par des clients également configurés pour ce serveur (tout au moins pour l’instant car il y a des évolutions en cours).

Pour configurer le client afin qu’il soit reconnu par le serveur on a plusieurs solutions :

  • Soit on installe l’exe normalement et on copie la config dans ID / Relay server
Qfi0TWlxGWrUdfqdsfVENxxxxxxxxxxxxxxxxxxxxxxxqfsqdfN3boJye

  • Soit on renomme l’exe d’installation afin d’y intégrer la clé, ce qui va donner quelque chose du genre:
rustdesk-licensed-Qfi0TWlxGWrUkR5Vxxxxxxxdfsdfsdfsdfye.exe
  • Soit on crée un script de déploiement et ainsi appliquer la configuration après l’installation dans le cadre d’un script PowerShell, on dispose alors de ces trois commandes, et on verra plus bas ce à quoi peut ressembler un script de déploiement  :

Se connecter

En général quand on utilise ce genre de produit, souvent pour de l'assistance client ou de la gestion de serveurs, il y a deux types d'installations :

  • Le device client, le mien, sur lequel après avoir configuré le serveur (chapitre précédent), je vais me connecter avec l'identifiant fournit par l'administrateur du serveur : 
  • Le device distant auquel on va affecter (ou pas) un utilisateur

Affecter un device à un utilisateur

Si on affecte un device à un utilisateur, cet utilisateur (et uniquement lui) verra ce device distant monter dans son logiciel client. Il pourra également l’ajouter à son carnet d’adresse personnel. Si on ne l'affecte pas, tous les utilisateurs le verront, pourront l'ajouter à leur carnet d'adresse et s'y connecter (sous réserve de disposer du mot de passe préalablement défini).

Des fonctionnalités de groupe et de partage de groupe sont également prévues. Il est ainsi possible de partager les devises affectés à un groupe à un autre utilisateur. A noter que pour l'instant groupe = utilisateur et que l'on ne peut pas directement affecter des devices à un groupe isolé. La solution consiste donc à créer un utilisateur virtuel qui partagera ses devices avec d'autres utilisateurs. Par exemple j'ai créé un utilisateur virtuel Société A qui partage ses devices avec les deux personnes chargés de la maintenance de Société A. Ca consomme une licence utilisateur mais je n'ai pas trouvé mieux pour l'instant.

Il existe plusieurs façons pour y parvenir :

  • En se connectant avec son user/password (mais c’est une très mauvaise idée que de faire ça sur le PC d’un client)
  • En l’associant dans l’interface d’administrateur (mais seul l’admin peut le faire)
  • En utilisant l’API sur le client dans le cadre d’un déploiement vie un script

Pour ça il faut un token. Les tokens qui permettent l’accès à l’API sont délivrés par l’admin.

PS C:\Program Files\RustDesk>.\RustDesk.exe  –assign –token "rapi_xxxxxxxxdyw==" –user_name “admin” | more

La console

Chaque utilisateur peut voir la liste de ses devices, l’état de la connexion ainsi que diverses infos (par exemple :

ip: 82.65.xxx.xxx;
version: 1.2.3;
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz, 2.53GHz, 4/4 cores; mem: 8GB

Un log est également disponible tant pour les connections que pour les transferts de fichiers. Cette console est destinée à évoluer pour plus de fonctionnalités.

L'administrateur peut quant à lui, outre gérer utilisateurs et devices, affecter des stratégies aux utilisateurs et devices :

Script PowerShell d’installation

On ne dispose pour l'instant pas de .msi et ce script est un peu simpliste (il en existe pour d'autres O/S ici). Pour faire simple je conseille d’ouvrir une session PS en mode admin. Mais il est également possible de faire une élévation dans le script (voir à la fin). Pour l’instant attention à indiquer la version de l'exe sur la bonne ligne…

Version client isolé

$rustdesk_pw="mot_de_passe_permanent"
$ErrorActionPreference= 'silentlycontinue'

################################### Please Do Not Edit Below This Line #########################################

If (!(Test-Path C:\Temp))
{
    New-Item -ItemType Directory -Force -Path C:\Temp > null
}

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

cd C:\Temp
Invoke-WebRequest "https://github.com/rustdesk/rustdesk/releases/download/1.2.3/rustdesk-1.2.3-x86_64.exe" -Outfile "rustdesk.exe"
Start-Process .\rustdesk.exe --silent-install # -wait
$ServiceName = 'Rustdesk'
$arrService = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
if ($arrService -eq $null)
{
    Start-Sleep -seconds 20
}
cd $env:ProgramFiles\RustDesk\
$rustdesk_id = (.\RustDesk.exe --get-id | out-host)

.\RustDesk.exe --password $rustdesk_pw | out-host

Version "mes machines"

################################# VARIABLES #######################################
$rustdesk_pw="PasswordPermanent"  # Celui du device distant
$rustdesk_token="MonTocken"
$rustdesk_cfg="Qfi0TWlxxxxxxxxxxxxxxxxxxxxxxxxxxN3boJye" # Clé de configuration
$ErrorActionPreference= 'silentlycontinue'
################################### SCRIPT ########################################

If (!(Test-Path C:\Temp))
{
    New-Item -ItemType Directory -Force -Path C:\Temp > null
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
cd C:\Temp
Invoke-WebRequest "https://github.com/rustdesk/rustdesk/releases/download/1.2.3/rustdesk-1.2.3-x86_64.exe" -Outfile "rustdesk.exe" # Changer la version le cas échéant

Start-Process .\rustdesk.exe --silent-install # -wait
$ServiceName = 'Rustdesk'
$arrService = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue

if ($arrService -eq $null)
{
    Start-Sleep -seconds 20
}
cd $env:ProgramFiles\RustDesk\
$rustdesk_id = (.\RustDesk.exe --get-id | out-host)

.\RustDesk.exe --config $rustdesk_cfg | out-host
.\RustDesk.exe --password $rustdesk_pw | out-host
Start-Sleep -Seconds 5
.\RustDesk.exe --assign --token $rustdesk_token --user_name manu | out-host

Elévation (le cas échéant à ajouter au début du script)

if (-Not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
    if ([int](Get-CimInstance -Class Win32_OperatingSystem | Select-Object -ExpandProperty BuildNumber) -ge 6000) {
        Start-Process PowerShell -Verb RunAs -ArgumentList "-NoProfile -ExecutionPolicy Bypass -Command `"cd '$pwd'; & '$PSCommandPath';`"";
        Exit;
    }
}

Le cas des serveurs Windows et du RDP

Si on installe RustDesk sur un serveur Windows va se retrouver face à deux possibilités, activer ou pas le partage de session RDP (une problématique identique existe avec TeamViewer qui a choisit de fournir deux ID, cela concerne principalement les administrateurs de serveurs, qu’ils soient premise ou cloud) :

  • Non activé : RustDesk va proposer une ouverture de session, il est possible d’enregistrer le mot de passe dans le client, et cette ouverture de session déconnectera l’éventuel utilisateur connecté en RDP.
  • Activé : Dans ce cas RustDesk partage la connexion RDP active et si on déconnecte la session RDP ça déconnecte la session RustDesk (qui peut toutefois se reconnecter s’il dispose des identifiants Windows.
    • Il existe toutefois un problème, dans ce mode rien ne permet actuellement de choisir la session sur laquelle va s’opérer le partage, il y a donc un risque de se concerter sur la session d’un utilisateur actif dans le cas d’un serveur de bureaux virtuels. (ticket ouvert en cours).

Conclusion

RustDesk est constante évolution et j'y reviendrait certainement ici. En attendant j'ai déployé une trentaine de devices sans observer de problèmes particulier si je compare à mon classique TeamViewer.

EDIT 10/12/2023

J'avais demandé une option afin de simplifier la mise en œuvre et ne pas associer des installations distantes et non contrôlées de façon permanentent à mon serveur. Sur le client on installe simplement Rustdesk de façon interactive, ou on l'installer avec le script simplifié qui permet seulement de définir un mot de passe permanent.

Il est maintenait possible, dans une infrastructure avec un serveur Pro de se connecter à un client isolé tout en bénéficiant du chiffrage du serveur en saisissant son ID@public :

Ainsi la machine sera contrôlée et chiffrée de façon temporaire par le serveur, sans pour autant l'enregistrer de faon permanente dans celui ci. Il sera toutefois possible de l'ajouter à son carnet d'adresse et de lui affecter une étiquette, mais pas de la partager avec d'autres utilisateurs du serveur. Autre avantage, s'autres intervenant pourront se connecter à cette machine, même s'ils ne fort pas partie du périmètre du serveur.

 

 

RDP & Guacamole

Et non, je ne suis hélas pas parti en vacances au Mexique déguster du guacamole arrosé de téquila ! Mais j'ai trouvé la solution que je cherchait depuis longtemps pour sécuriser les accès RDP. Comme chacun le sait il n'est vraiment pas conseillé de laisser ouvert sur internet le port RDP qui est plutôt vulnérable. Hélas pensant le confinement beaucoup de clients ont du mettre en place des solutions dans l'urgence et dans bien des cas il était impossible de verrouiller au minimum ce port sur une IP fixe....

Bien sur Microsoft a une solution (RDP Gateway), couteuse et complexe, donc pas adaptée à de petites entreprises dont les couts IT explosent déjà. Je restait donc en éveil à la recherche d'une solution quand je suis tombé sur le projet open-source Guacamole en HTML5. Et surprise, celui ci fonctionne vraiment bien ! Le principe est simple et reprends celui des autres projets, une machine va servir de proxy afin d'exposer le RDP (mais aussi SSH, Telnet et VNC).

Installation

S'il est possible d'installer Guacamole en natif comme c'est très bien expliqué ici, après avoir testé différentes options, par facilité je vais faire ça sous Docker en utilisant ce projet.

Je vais donc monter une VM Linux Ubuntu et y configurer Docker et Docker Compose. Pour cette partie je vous laisse chercher, d'autres expliquent mieux que moi.

Pour déployer Guacamole on a besoin de 3 services :

  • GUACD : Le cœur de Guacamole qui va servir à connecter les différents services (RDP, SSH, etc...)
  • PostgreSQL : Une base de donnée (qui peut être également MySQL ou encore l'utilisation d'une base externe)
  • Guacamole : Le composant FrontEnd qui va gérer les connections, les services et les utilisateurs.

Docker Compose

L'auteur de l'intégration a eu la bonne idée de nous fournir un script d'installation. On va donc l'utiliser et on se servira de Docker Compose plus tard afin d'ajuster la configuration.

sudo git clone "https://github.com/boschkundendienst/guacamole-docker-compose.git"
cd guacamole-docker-compose
./prepare.sh
sudo docker-compose up -d

Si tout se passe bien on peut se connecter sur https://ip_serveur:8443/guacamole. Le nom d'utilisateur par défaut est guacadmin avec mot de passe guacadmin. La première chose à faire est bien sur de le changer.

On pourra facilement configurer un serveur RDP et s'y connecter pour avoir les premières impressions. Je trouve que le rendu RDP en HTML5 est fluide, même en regardant des vidéos sur YouTube à une bonne résolution. Attention à ne pas perdre de vue que c'est le serveur Guacamole qui doit encaisser le trafic entrant et sortant. Selon l'usage et le nombre de clients il faudra donc le dimensionner correctement.

Je ne vais pas m'étendre sur les différentes options, l'interface et claire, la documentation également et d'autres en parlent très bien.

Sécurisation

Il n'est bien sur pas question d'exposer ce serveur directement sur internet et je vais tester deux solutions de reverse proxy, je commence par éditer le fichier docker-compose.yml afin de supprimer le proxy Nginx préinstallé histoire de ne pas empiler les proxys. J'ajuste également en 8080:8080 pour une utilisation en direct et REMOTE_IP_VALVE_ENABLED: 'true'  pour activer le proxy externe et WEBAPP_CONTEXT: 'ROOT' afin que Guacamole soit accessible en racine :

version: '2.0'
networks:
  guacnetwork_compose:
    driver: bridge

services:
  # guacd
  guacd:
    container_name: guacd_compose
    image: guacamole/guacd
    networks:
      guacnetwork_compose:
    restart: always
    volumes:
    - ./drive:/drive:rw
    - ./record:/record:rw
  # postgres
  postgres:
    container_name: postgres_guacamole_compose
    environment:
      PGDATA: /var/lib/postgresql/data/guacamole
      POSTGRES_DB: guacamole_db
      POSTGRES_PASSWORD: 'ChooseYourOwnPasswordHere1234'
      POSTGRES_USER: guacamole_user
    image: postgres:15.2-alpine
    networks:
      guacnetwork_compose:
    restart: always
    volumes:
    - ./init:/docker-entrypoint-initdb.d:z
    - ./data:/var/lib/postgresql/data:Z

  guacamole:
    container_name: guacamole_compose
    depends_on:
    - guacd
    - postgres
    environment:
      REMOTE_IP_VALVE_ENABLED: 'true'   # On active ici l'utilisation via un proxy externe
      WEBAPP_CONTEXT: 'ROOT'
      GUACD_HOSTNAME: guacd
      POSTGRES_DATABASE: guacamole_db
      POSTGRES_HOSTNAME: postgres
      POSTGRES_PASSWORD: 'ChooseYourOwnPasswordHere1234'
      POSTGRES_USER: guacamole_user
    image: guacamole/guacamole
    volumes:
    - ./custom/server.xml:/home/administrator/guacamole-docker-compose/server.xml
    links:
    - guacd
    networks:
      guacnetwork_compose:
    ports:
    - 8080:8080
    restart: always

Je relance docker :

administrator@guacamole:~/guacamole-docker-compose$ sudo docker-compose up -d

En local je me connecte maintenant en http://ip_serveur:8080 sans SSL car le SSL sera géré par le proxy.

Option 1 : pfsense

J'ai un pfsense installé avec HAProxy et Acme pour gérer les certificats Lets'Encrypt, je vais donc me servir de ca pour publier le service. Sur HAProxy on configure le BackEnd et le FronteEnd qui lui utilisera le certificat préalablement créé avec Acme. Dans ma configuration je partage l'IP avec plusieurs sites et ce qui est important c'est d'activer l'option forwardfor qui permettra de transférer les adresses sources à Guacamole.

Je mets ici le code de configuration qui sera utile à ceux qui n'utilisent pas HAProxy sous pfsense qui lui dispose d'une interface graphique. Comme on peut le constater le HTTP to HTTPS se fait au niveau du HAProxy et c'est lui également qui redirigera les requetés HTTTP vers HTTPS et mon serveur sera accessible sur https://guacamole.mondomaine.tld :

global
  maxconn     10000
  stats socket /tmp/haproxy.socket level admin  expose-fd listeners
  uid     80
  gid     80
  nbproc      1
  nbthread      1
  hard-stop-after   15m
  chroot        /tmp/haproxy_chroot
  daemon
  tune.ssl.default-dh-param 1024
  server-state-file /tmp/haproxy_server_state

listen HAProxyLocalStats
  bind 127.0.0.1:2200 name localstats
  mode http
  stats enable
  stats admin if TRUE
  stats show-legends
  stats uri /haproxy/haproxy_stats.php?haproxystats=1
  timeout client 5000
  timeout connect 5000
  timeout server 5000

frontend Shared_WAN-merged
  bind      x.x.x.x:443 (IP WAN) name x.x.x.x:443 (IP WAN)   ssl crt-list /var/etc/haproxy/Shared_WAN.crt_list  
  mode      http
  log       global
  option    http-keep-alive
  option    forwardfor
  acl https ssl_fc
  http-request set-header   X-Forwarded-Proto http if !https
  http-request set-header   X-Forwarded-Proto https if https
  timeout client    30000
  acl     Admin var(txn.txnhost)      -m str -i admin.domain.tld
  acl     guacamole var(txn.txnhost)  -m str -i guacamole.domain.tld
  http-request set-var(txn.txnhost) hdr(host)
  use_backend Admin_ipvANY  if  Admin 
  use_backend Guacamole_8443_ipvANY  if  guacamole 

frontend http-to-https
  bind            x.x.x.x:80 (IP WAN) name x.x.x.x:80 (IP WAN)
  mode            http
  log             global
  option          http-keep-alive
  timeout client  30000
  http-request redirect code 301 location https://%[hdr(host)]%[path]  

backend Admin_ipvANY
  mode     http
  id       100
  log      global
  timeout  connect   30000
  timeout  server    30000
  retries  3
  server   admin 192.168.55.44:80 id 101  

backend Guacamole_8443_ipvANY
  mode     http
  id       102
  log      global
  timeout  connect   30000
  timeout  server    30000
  retries  3
  server   guacamole 192.168.66.55:8080 id 101

Option 2 : CloudFlared

Cette option est encore plus simple pour ceux qui disposent d'un domaine chez Cloudflare. On va utiliser les possibilités offertes gratuitement par Cloudflared et ajouter une couche de sécurité supplémentaire.

On commence par créer un tunnel CloudFlared avec une instance Docker supplémentaire (le code est fournit par le site de configuration Zero Trust de Cloudflare)

docker run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token eyJhIjoiZjk0YjdkZTBiMzFmYWNkNjZlshhsgfhsfghsgfhgfhssgfhsfgzRiMC00MmNlLWJjMghsfghsghsgfhsgfhsgfhsgmpaR00tyyretu(-yenebybvewWXpGaUxUazVNell0TnpRek56WXhNMlkxWXpFeiJ9

Ensuite une fois que le tunnel est monté

On va publier et pointant sur l'ip privée et le port de notre serveur, cela va automatiquement créer l'entrée DNS dans CloudFlare et gérer la problématique du certificat. Ll sera accessible en SSL. :

Ensuite on va ajouter une couche de sécurité supplémentaire en créant une application de type SelfHosted à laquelle on affecte une policie qui impose un code supplémentaire lors d'une connexion. Ce code sera envoyé sur le mail de l'utilisateur sur une adresse individuelle ou un domaine spécifique. Ce n'est pas tout à fait du MFA, mais on considère que si l'utilisateur reçoit bien le code sur son mail professionnel, il s'agit bien de lui et on peut lui permettre de saisir son login / password pour accéder au service :

C'est la méthode la plus simple et de nombreuses option sont exploitables, de même qu'il est possible d'utiliser de nombreux providers externes d'authentification :

Lorsqu'il souhaite se connecter, l'utilisateur tombe sur un portail que l'on peu personnaliser aux couleurs de l'entreprise.

Voilà, et surtout maintenant on retrouve bien les IP publiques dans le log de Guacamole :

Il n'y a plus qu'a ajuster les différentes options de Guacamole et pour ça la documentation est très bien faite. Et bien sur si on se sert de Guacamole pour exposer des serveurs RDP accessibles sur l'internet on prendra soin de restreindre leur usage à l'IP publique du serveur Guacamole, ou mieux de faire transiter ce flux par un tunnel (Wireguard, Tailscale ou Zerotier par exemple que l'on peu facilement installer sur les deux serveurs).

Home Assistant & Arrosage

Je n'ai pas vraiment la main verte et et mon jardin ressemble souvent à un no man's land, mais au printemps dernier une amie m'a convaincu de créer un carré d'herbes aromatiques, l'idée à fait son chemin et au centre on a même planté des plants de tomates qui produisent bien cet été ! Bon, vous imaginez bien que je ne vais pas ici vous conter ma vie privée mais plutôt la part domotique de cette réalisation !

En Provence, si on veut un peu de verdure et espérer manger ses propres tomates, il n'y a pas de secret, il faut arroser ! J'ai donc commencé par disposer un tuyau poreux sur mon carré. Ensuite j'ai acheté un robinet Zigbee et préparé un petit scheduler qui me permet facilement d'activer ou pas la plage journalière d'arrosage. J'ai fait quelque chose de simple en m'inspirant de ce que j'avais fait pour mon chauffe eau. Je publie ici le code suite à quelques demandes, même si ça n'a pas une grande valeur ajoutée.

Pour l'instant ça ne prends pas en compte les valeurs remontés sur les capteurs de plantes car je n'en suis pas satisfait.

L'offre en matière de capteurs de plantes n'est pas énorme :

  • Le capteur Xiaomi (ou ses copies) qui fonctionnent en Bluetooth et que l'on trouve sur Amazon ou Ali Express : C'est ce qui fonctionne le "mieux", mais cela nécessite un proxy BLE à l'extérieur que l'on peut facilement se bricoler avec un ESP sous ESPHome.
  • Le capteur Rehent en Zigbee que j'ai acheté chez Domadoo : Et là c'est la déconvenue. Il est un peu encombrant mais ne pose pas de soucis particulier pour l'appairer en ZHA ou Z2M (ou sur une passerelle Tuya), sauf que si ce capteur remonte parfaitement la température du sol, l'humidité du sol reste invariablement entre 75% et 85% (voir les sources au bas de cet article). Et bien sur chez Domadoo on est pas chez Amazon, le client a tort et l'éventuel retour est à votre charge. Bref capteur et fournisseur à éviter ! Il semblerait qu'autres séries de ce capteur que l'on trouve chez AliExpress fonctionnent, mais là il faudra aussi oublier le retour...

J'utilise une vanne Zigbee Woox qui alimente le tuyaux poreux (que je devrais remplacer par un un goute à goute). Pour avoir un réseau Zigbee fiable à l'extérieur, j'ai installé sous la toiture de la terrasse des prises Zigbee dont le relais est HS mais qui continuent à remplir parfaitement leur rôle de routeur Zigbee.

Tout cela reste très expérimental... Coté intégration j'utilise Home Assistant Plant et la carte qui va avec.

Automation

Une automation de schedule simple et visuelle :

input_datetime:
  watering_start:
    has_date: false
    has_time: true
  watering_stop:
    has_date: false
    has_time: true

input_boolean:
  watering_day_monday:
    name: "WATERING : Lundi"
    icon: mdi:toggle-switch
  watering_day_tuesday:
    name: "WATERING : Mardi"
    icon: mdi:toggle-switch
  watering_day_wednesday:
    name: "WATERING : Mercredi"
    icon: mdi:toggle-switch
  watering_day_thursday:
    name: "WATERING : Jeudi"
    icon: mdi:toggle-switch
  watering_day_friday:
    name: "WATERING : Vendredi"
    icon: mdi:toggle-switch
  watering_day_saturday:
    name: "WATERING : Samedi"
    icon: mdi:toggle-switch
  watering_day_sunday:
    name: "WATERING : Dimanche"
    icon: mdi:toggle-switch

automation:

- id: 'xx8d0e1-fcb6-4412-abvxx-99c4d37be5xx'
  alias: 'WATERING ON'
  trigger:
  - platform: template
    value_template: '{{ states.sensor.time.state == states.input_datetime.watering_start.state[0:5] }}'
  condition:
    condition: or
    conditions:
      - '{{ (now().strftime("%a") == "Mon") and is_state("input_boolean.watering_day_monday", "on") }}'
      - '{{ (now().strftime("%a") == "Tue") and is_state("input_boolean.watering_day_tuesday", "on") }}'
      - '{{ (now().strftime("%a") == "Wed") and is_state("input_boolean.watering_day_wednesday", "on") }}'
      - '{{ (now().strftime("%a") == "Thu") and is_state("input_boolean.watering_day_thursday", "on") }}'
      - '{{ (now().strftime("%a") == "Fri") and is_state("input_boolean.watering_day_friday", "on") }}'
      - '{{ (now().strftime("%a") == "Sat") and is_state("input_boolean.watering_day_saturday", "on") }}'
      - '{{ (now().strftime("%a") == "Sun") and is_state("input_boolean.watering_day_sunday", "on")}}'
  action:
  - service: switch.turn_on
    entity_id: switch.vanne_woox_switch
  - service: notify.slack_hass_canaletto
    data:
      message: "{{now().strftime('%d/%m/%Y, %H:%M')}} > WATERING | START | Soil : {{ states.sensor.soil_01_soil_moisture.state }}%" 


- id: 'zz9csdfsef-76dd-4fdd-9dzz-40bfsdq158zz'
  alias: 'WATERING OFF'
  trigger:
  - platform: template
    value_template: '{{ states.sensor.time.state == states.input_datetime.watering_stop.state[0:5] }}'
  action:
  - service: switch.turn_off
    entity_id: switch.vanne_woox_switch
  - service: notify.slack_hass_canaletto
    data:
      message: "{{now().strftime('%d/%m/%Y, %H:%M')}} > WATERING | STOP | Soil : {{ states.sensor.soil_01_soil_moisture.state }}%"   

La carte Lovelace :

type: vertical-stack
cards:
  - type: entities
    entities:
      - entities:
          - entity: automation.watering_on
            name: false
          - entity: sensor.energy_total_yearly_1pm_watering
            name: false
            unit: kWh
            format: precision2
          - entity: sensor.soil_01_soil_moisture
            name: false
        entity: switch.vanne_woox_switch
        name: Arrosage
        icon: mdi:watering-can-outline
        show_state: false
        state_color: true
        type: custom:multiple-entity-row
  - type: horizontal-stack
    cards:
      - type: custom:button-card
        color_type: card
        entity: input_boolean.watering_day_monday
        name: Lundi
        show_last_changed: false
        show_state: false
        tap_action:
          action: toggle
        state:
          - value: 'on'
            color: green
            icon: mdi:water-boiler
          - value: 'off'
            color: grey
            icon: mdi:water-boiler-off
        styles:
          card:
            - height: 60px
            - border-radius: 5px
            - font-size: 12px
      - type: custom:button-card
        color_type: card
        entity: input_boolean.watering_day_tuesday
        name: Mardi
        show_last_changed: false
        show_state: false
        tap_action:
          action: toggle
        state:
          - value: 'on'
            color: green
            icon: mdi:water-boiler
          - value: 'off'
            color: grey
            icon: mdi:water-boiler-off
        styles:
          card:
            - height: 60px
            - border-radius: 5px
            - font-size: 12px
      - type: custom:button-card
        color_type: card
        entity: input_boolean.watering_day_wednesday
        name: Mercredi
        show_last_changed: false
        show_state: false
        tap_action:
          action: toggle
        state:
          - value: 'on'
            color: green
            icon: mdi:water-boiler
          - value: 'off'
            color: grey
            icon: mdi:water-boiler-off
        styles:
          card:
            - height: 60px
            - border-radius: 5px
            - font-size: 12px
      - type: custom:button-card
        color_type: card
        entity: input_boolean.watering_day_thursday
        name: Jeudi
        show_last_changed: false
        show_state: false
        tap_action:
          action: toggle
        state:
          - value: 'on'
            color: green
            icon: mdi:water-boiler
          - value: 'off'
            color: grey
            icon: mdi:water-boiler-off
        styles:
          card:
            - height: 60px
            - border-radius: 5px
            - font-size: 12px
      - type: custom:button-card
        color_type: card
        entity: input_boolean.watering_day_friday
        name: Vendredi
        show_last_changed: false
        show_state: false
        tap_action:
          action: toggle
        state:
          - value: 'on'
            color: green
            icon: mdi:water-boiler
          - value: 'off'
            color: grey
            icon: mdi:water-boiler-off
        styles:
          card:
            - height: 60px
            - border-radius: 5px
            - font-size: 12px
      - type: custom:button-card
        color_type: card
        entity: input_boolean.watering_day_saturday
        name: Samedi
        show_last_changed: false
        show_state: false
        tap_action:
          action: toggle
        state:
          - value: 'on'
            color: green
            icon: mdi:water-boiler
          - value: 'off'
            color: grey
            icon: mdi:water-boiler-off
        styles:
          card:
            - height: 60px
            - border-radius: 5px
            - font-size: 12px
      - type: custom:button-card
        color_type: card
        entity: input_boolean.watering_day_sunday
        name: Dimanche
        show_last_changed: false
        show_state: false
        tap_action:
          action: toggle
        state:
          - value: 'on'
            color: green
            icon: mdi:water-boiler
          - value: 'off'
            color: grey
            icon: mdi:water-boiler-off
        styles:
          card:
            - height: 60px
            - border-radius: 5px
            - font-size: 12px
  - type: conditional
    conditions:
      - entity: automation.watering_on
        state: 'on'
    card:
      type: custom:vertical-stack-in-card
      cards:
        - type: horizontal-stack
          cards:
            - type: markdown
              content: '#### <center> Heure de début'
            - type: markdown
              content: '#### <center> Arrosage'
            - type: markdown
              content: '#### <center> Heure de Fin'
        - type: horizontal-stack
          cards:
            - entity: input_datetime.watering_start
              type: custom:time-picker-card
              name: Début
              layout:
                align_controls: center
                embedded: true
              hide:
                name: true
                icon: true
            - type: glance
              show_state: true
              show_name: false
              entities:
                - switch.vanne_woox_switch
            - entity: input_datetime.watering_stop
              type: custom:time-picker-card
              layout:
                align_controls: center
                embedded: true
              hide:
                name: true
                icon: true
  - color_thresholds:
      - color: '#039BE5'
        value: 0
      - color: '#0da035'
        value: 19
      - color: '#e0b400'
        value: 25
      - color: '#e45e65'
        value: 2400
    color_thresholds_transition: hard
    entities:
      - entity: sensor.plant_01_moisture
        name: Humidité du sol
      - entity: sensor.plant_01_temperature
        name: Températire du sol
      - color: rgba(0,0,255,1)
        entity: binary_sensor.night_reworked
        name: Nuit
        show_line: false
        y_axis: secondary
    group: false
    hour24: true
    hours_to_show: 24
    line_width: 2
    name: Humidité et température du sol
    points_per_hour: 4
    show:
      extrema: true
      fill: fade
      icon: true
      labels: false
      name: true
      state: true
    state_map:
      - label: Day
        value: 'off'
      - label: Night
        value: 'on'
    type: custom:mini-graph-card
  - type: custom:flower-card
    entity: plant.jardin
    show_bars:
      - illuminance
      - humidity
      - moisture
      - conductivity
      - temperature
      - dli
    battery_sensor: sensor.demo_battery

Souces

Capteur Rehent

 

Home Assistant & Plug Security

On utilise souvent des prises commandées uniquement pour mesurer la consommation de certains appareils. Ces prises sont toujours en position ON. Ces prises, si elles ne sont pas de trop mauvaise qualité un mécanisme de sécurité intégré qui les passent OFF en cas de surtension ou de surcharge. Et dans la pratique ça arrive parfois et on constate plus tard que le lave linge ou le lave vaisselle s'est arrêté, ou pire dans le cas d'un congélateur.

Pour palier à cet inconvénient on va créer une automation qui va réarmer la prise après quelques minutes en OFF. Jusque là c'est simple, mais il se peut également que le problème soit plus grave et qu'il ne faille pas forcer indéfiniment ce réarmement automatique. Etant donné qu'on ne dispose généralement pas d'information sur la cause de ce passage en sécurité on va considérer qu'au bout de "n" tentatives sur un temps donné on ne réarme plus. Pour ça on va utiliser des compteurs (à créer avec un minimum à 0 et un maximum à 10) et on fera un reset de ceux-ci toutes les nuits...

J'ai fixé ici arbitrairement à 5 le nombre de réarmements possibles.

Attention : Je vous explique ici comment j'ai fait, mais un risque existe toujours et la mise en œuvre de cette solution est à vos risques (et périls). Je me dégage ainsi de toute responsabilité.

On va faire ça avec une seule automation que j'ai voulue la plus concise et qui servira également à notifier :

automation:
- id: '2bd0ertyyf-2687-45f98f-aed0-to-off'
  alias: "Notify Plug Off"
  description: "Notification mise en sécurité des prises"
  mode: single
  trigger:
    - platform: state
      entity_id: switch.bw_1
      to: "off"
      id: "Lave Vaisselle"
    - platform: state
      entity_id: switch.bw_2
      to: "off"
      id: "Lave Linge"
    - platform: state
      entity_id: switch.bw_3
      to: "off"
      id: "Seche Linge"
    - platform: state
      entity_id: switch.bw_4
      to: "off"
      id: "Congelateur"
    - platform: time
      at: "00:05:00"
      id: "Reset"
  condition: []
  action:
  - delay:
      hours: 0
      minutes: 5
      seconds: 0
      milliseconds: 0  
  - choose:
      - conditions: "{{ trigger.id in ['Lave Vaisselle'] and states('counter.plug_bw_1') | int < 5 }}"
        sequence:
          - service: switch.turn_on
            target:
              entity_id: switch.bw_1
          - service: counter.increment
            target:
              entity_id: counter.plug_bw_1
          - service: notify.slack_hass_mondon
            data:
              message: "{{now().strftime('%d/%m/%Y, %H:%M:%S')}} > Réarmement n°{{ states('counter.plug_bw_1')}} du {{ trigger.id }}"               

      - conditions: "{{ trigger.id in ['Lave Linge'] and states('counter.plug_bw_2') | int < 5 }}"
        sequence:
          - service: switch.turn_on
            target:
              entity_id: switch.bw_2
          - service: counter.increment
            target:
              entity_id: counter.plug_bw_2
          - service: notify.slack_hass_mondon
            data:
              message: "{{now().strftime('%d/%m/%Y, %H:%M:%S')}} > Réarmement n°{{ states('counter.plug_bw_2')}} du {{ trigger.id }}" 

      - conditions: "{{ trigger.id in ['Seche Linge'] and states('counter.plug_bw_3') | int < 5 }}"
        sequence:
          - service: switch.turn_on
            target:
              entity_id: switch.bw_3
          - service: counter.increment
            target:
              entity_id: counter.plug_bw_3
          - service: notify.slack_hass_mondon
            data:
              message: "{{now().strftime('%d/%m/%Y, %H:%M:%S')}} > Réarmement n°{{ states('counter.plug_bw_3')}} du {{ trigger.id }}" 

      - conditions: "{{ trigger.id in ['Congelateur'] and states('counter.plug_bw_4') | int < 5 }}"
        sequence:
          - service: switch.turn_on
            target:
              entity_id: switch.bw_4
          - service: counter.increment
            target:
              entity_id: counter.plug_bw_3
          - service: notify.slack_hass_mondon
            data:
              message: "{{now().strftime('%d/%m/%Y, %H:%M:%S')}} > Réarmement n°{{ states('counter.plug_bw_4')}} du {{ trigger.id }}" 

      - conditions: 
          - condition: or
            conditions:
              - "{{ trigger.id in ['Lave Vaisselle'] and states('counter.plug_bw_1') | int >= 5 }}"
              - "{{ trigger.id in ['Lave Linge'] and states('counter.plug_bw_2') | int >= 5 }}"
              - "{{ trigger.id in ['Seche Linge'] and states('counter.plug_bw_3') | int >= 5 }}"
              - "{{ trigger.id in ['Congelateur'] and states('counter.plug_bw_4') | int >= 5 }}"
        sequence:
          - service: notify.slack_hass_mondon
            data:
              message: "{{now().strftime('%d/%m/%Y, %H:%M:%S')}} > Problème avec le {{ trigger.id }}. On ne réarme plus, une vérification s'impose." 

      - conditions: "{{ trigger.id in ['Reset'] }}"
        sequence:
          - service: counter.reset
            target:
              entity_id:
                - counter.plug_bw_1
                - counter.plug_bw_2
                - counter.plug_bw_3
                - counter.plug_bw_4

Tailscale Access Controls

J'ai souvent parlé de Zerotier que j'utilise au quotidien, mais qui dans sa version gratuite comporte quelques limitations en terme de contrôle d'accès dans un environnement mixant du site-to-site et des clients isolés. J'ai donc voulut voir ce que proposait Tailscale, qui est un VPN managé relativement similaire reposant sur Wireguard. Tous deux offrent des fonctionnalités gratuites et payantes. Si payer ne doit pas être un problème dans un environnement de production, c'est un peu différent en mode home lab.... d'autant plus que certaines fonctionnalités de la version gratuite ne se retrouvent que dans la version Premium à $18 /mois/utilisateur.

La version gratuite des Tailscale est plutôt généreuse, elle propose 100 nodes et 3 utilisateurs (à condition de disposer d'un "custom domain" (je vais utiliser des comptes Microsoft 365, mais plusieurs autres possibilités sont disponibles : SSO Identity Providers). On peut donc disposer de 3 comptes sur un même domaine. Je parle de comptes car nativement (et normalement) le contrôle d'accès se fait sur l'utilisateur. Manque de chance j'ai besoin de gérer 4 utilisateurs, et je ne suis pas prêt à payer 4 x $18, ce qui me ferait plus de $ 800 par an !

J'ai donc cherché à contourner et je vais finalement gérer mes autorisations à partir des machines Tailscale que j'ai approuvées (mode NFS pour ceux qui se souviennent). Dans cet usage je ne cherche pas faire communiquer les clients Tailscale entre eux mais à leur permettre d'accéder à des ressources situées sur différents subnets sur lesquels je souhaite restreindre les possibilités.

Pour la suite on considère que le réseaux Tailscale est configuré avec les nodes qui servent à joindre les subnets.

Je vais créer deux utilisateurs :

[email protected]
[email protected]

Le premier compte qui sera également le mien va également me permettre de configurer et approuver les machines clientes.

Le second servira à connecter mes utilisateurs et je pourrais approuver leurs machines. Ce compte étant protégé en MFA, il nécessitera une validation de ma part. Une fois la machine connectée, je désactive l'expiration de la clé et on passe au contrôle s'accès :

{
  "acls": [
    {"action": "accept", "src": ["[email protected]"], "dst": ["*:*"]}, // Ici l'admin dispose de tous les droits
    
    // Autorisation par machines
    {
      "action": "accept",
      "src": ["azura", "lydia"],      // Ici deux clients Tailscale
      "dst": [
        "pipa:25,53",                 // On autorise le host qui supporte le DNS si on veut utiliser les noms courts...
        "mixa:80,443,445,3389",
        "nona:80,443,445,1443,3389",  // Ports autorisés

        "lab-1:*",

        "192.168.2.12:80,443,9100,5001,3389", // Par IP + ports
        "192.168.6.0/24:*",                   // Par subnet
      ],
    },
  ],
  "hosts": {
    "azura": "100.1.2.3",        // Client Tailscale
    "lydia": "100.5.2.3",        // Client Tailscale

    "mixa":  "192.168.1.4",      // Host on subnet
    "nona":  "192.168.2.8",      // Host on subnet
    "pipa":  "192.168.2.1",      // Host on subnet

    "lab-1": "192.168.6.0/24", // subnet
  },

}

Dans l'ordre :

  1. J'autorise l'admin à tout voir.
  2. Ensuite j'autorise les machines azura et lydia à accéder uniquement aux hosts que j'aurais défini avec une restriction sur certains ports. C'est possible en définissant les hosts plus loin ou en se servant directement des adresses IP. L'ICMP (ping) est ouvert automatiquement vers les destinations autorisées.
  3. Je défini les clients et serveurs.

Voilà. Je n'ai pas creusé toutes les possibilité offertes par le contrôle d'accès proposé par Tailscale, la liste est longue et le documentation plutôt bien faire. Idées bienvenues !

Home Assistant & Alarm, encore...

J'ai souvent parlé d'alarme ici, de télécommandes, de claviers et de sirènes. Je pense cette fois être parvenu à quelque chose de fonctionnel et je vais vous en parler. Pour faire face aux différentes contraintes, notamment la gestion bizarre des télécommandes sous ZHA, il va falloir un peu ruser. Avant que vous me demandiez pourquoi je m'obstine sous ZHA alors que ces mêmes télécommandes sont bien gérées sous Z2M, je vous répondrait que mon objectif est la simplification et donc d'éviter le plus possible les adons qui complexifient une installation, pas forcément pour moi ou je dispose de toutes les passerelles possibles, mais sur des installations que je gère à distance ou je me dois de faire simple et fiable.

Attention :  on parle ici d'un système de sécurité DIY, donc non agrée par les assurances et autres... Ca ne veut pas dire que ça ne fonctionne pas, juste que ça ne répond pas aux normes NF en vigueur dans ce domaine

Afin de gérer toutes les contraintes, je vais principalement m'appuyer sur deux automations:, quelques scripts: et un alarm_control_panel: virtuel.

L'objectif est de gérer :

  • Un mode absent (away) (avec une variable pour simuler un peu de présence).
  • Un mode présent/nuit armé (home) (plus ou moins utile mais mais ça fait partie de tout système d'alarme face au home jacking.
  • Un mode alerte (emergency) qui va permettre de déclencher une action d'urgence en appuyant sur un bouton de télécommande ou médaillon (je pense à mes vieux jours).

De l'armement / désarmement vont également découler :

  • La fermeture complète ou partielle des volets roulants, leur réouverture cohérente avec la programmation en cours et éventuellement le passage en manuel des volets des chambres des enfants / invités s'ils sont présents.
  • La gestion du climatiseur et des convecteurs avec passage en mode éco ou absent en fonction du type d'armement).
  • La gestion du chauffe eau en cas d'absence de longue durée.
  • La gestion des éclairages et la mise en veille des écrans et ordinateurs.
  • ...

Etant donné que j'ai deux systèmes d'alarme qui fonctionnent en parallèle (Alarmo et Visonic), j'ai commencé par créer une alarme virtuelle et je me servirait d'elle pour les différentes actions.

alarm_control_panel:
  - platform: manual
    name: Home Alarm Command
    code: 1234
    code_arm_required: false
    disarm_after_trigger: false
    arming_time: 0
    delay_time: 0
    trigger_time: 600
    disarmed:
      trigger_time: 0
    armed_home:
      arming_time: 0
      delay_time: 0

L'Armement / Désarmement

L'armement / désarmement va pouvoir se faire de plusieurs façons :

  • Clavier à code
  • Télécommande dédiée
  • Application mobile
  • Tag RFID (pour désarmer dans mon cas)

J'ai volontairement écarté les télécommandes radio en 433 Mhz dont j'avais parlé ici, d'une part parce qu'elles sont facilement piratables, et d'autre part parce qu'elles ne permettent pas de savoir qui a désarmé le système.

Je vais utiliser deux automations et les découper ce dessous/

  • La première pour gérer les armements / désarmements
  • La seconde pour gérer le désarmement des télécommandes, claviers à code afin d'intégrer les particularités ZHA (en attendant une amélioration de la part des développeurs de Home Assistant qui pour l'instant font la sourde oreille, parfois plus concentrés sur les nouveautés que l'affinage de l'existant).

Gestion de l'armement / désarmement et des boutons d'urgence

Les déclencheurs : Ici on va empiler nos déclencheurs d'armement, qu'ils soient gérés par ZHA, Z2M ou toute autre source. 3 par fonction, armed_away, armed_home, triggered, mais cela peut également être un bouton virtuel pour Lovelace, voire n'importe quelle télécommande ou bouton poussoir.

- id: 078fsdg412-3545-4f37-ba02-bccsfds5646sf
  alias: "Alarm : Global ON/OFF"
  mode: restart
  trigger:
    - platform: device
      device_id: c036b6547a0347878dcc6d06152267ab
      domain: alarm_control_panel
      entity_id: alarm_control_panel.lk_zb_keypad
      type: armed_away
      id: "KeyPad Away"
    - platform: device
      device_id: c036b6547a0347878dcc6d06152267ab
      domain: alarm_control_panel
      entity_id: alarm_control_panel.lk_zb_keypad
      type: armed_home
      id: "KeyPad Home"
    - platform: device
      device_id: c036b6547a0347878dcc6d06152267ab
      domain: alarm_control_panel
      entity_id: alarm_control_panel.lk_zb_keypad
      type: triggered
      id: "KeyPad Triggered"
    - platform: state
      entity_id: input_button.arm_alarm_away
      id: "Push Button Away"

Et un déclencheur de désarmement activé par une seconde automation que l'on verra plus loin :

    - platform: state
      entity_id:
        - alarm_control_panel.home_alarm_command
      to: disarmed
      id: "Remote or Keypad"

Les conditions : Dans mon cas je n'en ai pas utilisé. Chacun adaptera.

  condition:

Les actions

Je commence par arrêter les éventuels scripts en cours. Cela est principalement utile quand on a fermé la porte, armé le système et que l'on doit rapidement désarmer car on a oublié quelque chose dans la maison.

  action:
    - service: homeassistant.turn_off
      target:
        entity_id:
          - script.privacy_on
          - script.privacy_off
          - script.alarmo_after_arm
          - script.alarmo_after_disarm
          - script.alarm_actions_on_leave_common_tasks
          - script.alarm_actions_on_return_common_tasks

Ensuite on va utiliser chose: pour exécuter les différentes actions en fonction de la source et de l'état.

On commence par l'armement en mode absent :

  • On envoie les bips d'armement sur la sirène. En MQTT pour l'instant car elle aussi est très mal gérée sous ZHA
  • On arme Alarmo
  • On arme Visonic (en mode Home sans les PIR qui déraillent).
  • On arme notre panneau d'alarme virtuel qui nous servira au désarmement
  • On arme également le clavier, ça ne change rien à l'usage mais quand on se présentera pour désarmé il apparaitra comme étant armé. Dans la pratique ça relance de fait l'automation avec la même automation, et donc ça fausse le message, j'ai donc retiré cette option pour l'instant. (D'ailleurs si vous êtes sous Z2M il le développeur d'Alarmo a fait un joli travail pour ce clavier).
  • On inscrit un message texte dans un input_text: que l'on affiche dans Lovelace et qui nous permet de savoir qui fait quoi. A terme je remplacerait les id: par le prénom de l'utilisateur pour plus de lisibilité.
    - choose:
        - conditions: "{{ trigger.id in ['KeyPad Away', 'Heiman RC1 Away', 'Heiman RC2 Away', 'Woox RC1 Away'] }}"
          sequence:
            - service: mqtt.publish
              data:
                topic: zigbee2mqtt/Sirène SMaBiT/set
                payload: >-
                  {"squawk": {"state": "system_is_armed", "level": "veryhigh", "strobe": "true"}}     
            - service: alarm_control_panel.alarm_arm_away # ALARMO Away
              data:
                code: !secret alarm_code
              entity_id: alarm_control_panel.alarmo
            - service: alarm_control_panel.alarm_arm_home # VISONIC Home (ne jamais armer AWAY à cause des détecteurs HS)
              data:
                code: !secret alarm_code_visonic
              entity_id: alarm_control_panel.visonic_alarm
            - service: script.alarm_actions_on_leave_common_tasks
            - service: alarm_control_panel.alarm_arm_away # FAKE ALARM PANEL Away
              target:
                entity_id: alarm_control_panel.home_alarm_command
            - service: alarm_control_panel.alarm_arm_away # On arme le clavier afin qu'il apparaisse armé
              data:
                code: !secret alarm_code_zha
              target:
                entity_id: 
                  - alarm_control_panel.lk_zb_keypad
            - service: input_text.set_value
              target:
                entity_id: input_text.last_arm
              data:
                value: "{{now().strftime('%d/%m/%Y, %H:%M:%S')}} > Arm Away by : {{ trigger.id }}"                            

On fait la même chose pour le mode home. Je fais l'impasse sur Visonic, par contre j'émet un son de confirmation sur un buzzer (on verra plus loin l'utilisation possible d'un TTS sur une enceinte.

        - conditions: "{{ trigger.id in ['KeyPad Home', 'Heiman RC1 Home', 'Heiman RC2 Home', 'Woox RC1 Home'] }}"
          sequence:
            - service: alarm_control_panel.alarm_arm_home # ALARMO Home
              data:
                code: !secret alarm_code
              entity_id: alarm_control_panel.alarmo
            - service: alarm_control_panel.alarm_arm_home # FAKE ALARM PANEL Home
              target:
                entity_id: alarm_control_panel.home_alarm_command
            - service: alarm_control_panel.alarm_arm_home # On arme le clavier afin qu'il apparaisse armé
              data:
                code: !secret alarm_code_zha
              target:
                entity_id: 
                  - alarm_control_panel.lk_zb_keypad

            - service: button.press
              target:
                entity_id: button.up_chime_play_buzzer
            - service: input_text.set_value
              target:
                entity_id: input_text.last_arm
              data:
                value: "{{now().strftime('%d/%m/%Y, %H:%M:%S')}} > Arm Home by : {{ trigger.id }}"                                    

Ensuite on passe au désarmement du mode absent. On remarque que l'on écrit pas d'input_text: car ici on ne sait pas qui a fait quoi car l'ordre vient de la seconde automation que l'on verra plus loi. C'est donc elle qui écrira. Par contre on émet le son de désarmement sur la sirène.

        - conditions: "{{ trigger.id in ['Remote or Keypad'] and is_state('alarm_control_panel.alarmo', 'armed_away') }}"
          sequence:              
            - service: alarm_control_panel.alarm_disarm
              data:
                code: !secret alarm_code
              entity_id:  
                - alarm_control_panel.alarmo
                - alarm_control_panel.visonic_alarm
            - service: mqtt.publish
              data:
                topic: zigbee2mqtt/Sirène SMaBiT/set
                payload: >-
                  {"squawk": {"state": "system_is_disarmed", "level": "veryhigh", "strobe": "true"}}       

Pareil pour le mode Home. Ici on joue un son sur le buzzer.

        - conditions: 
            - "{{ trigger.id in ['Push Button'] }}"
            - "{{ is_state('alarm_control_panel.alarmo', 'armed_home') }}"
          sequence:              
            - service: alarm_control_panel.alarm_disarm
              data:
                code: !secret alarm_code
              entity_id:  
                - alarm_control_panel.alarmo
            - service: button.press
              target:
                entity_id: button.up_chime_play_buzzer

On continue avec la gestion du bouton d'urgence :

  • On joue un son pour confirmer
  • On arme notre panneau d'alarme virtuel dans un mode inutilisé (Nuit) pour ensuite pouvoir désarmer.
  • On envoie un SMS d'Au Secours, ou toute autre action à imaginer.
  • On écrit un input_text: pour avoir une trace.
        - conditions: "{{ trigger.id in ['KeyPad Triggered', 'Heiman RC1 Triggered', 'Heiman RC2 Triggered', 'Woox RC1 Triggered'] and is_state('alarm_control_panel.alarmo', 'disarmed') }}"
          sequence:              
            - service: button.press
              target:
                entity_id: button.up_chime_play_chime
            - service: alarm_control_panel.alarm_arm_night # FAKE ALARM PANEL Night pour pouvoir désarmer
              target:
                entity_id: alarm_control_panel.home_alarm_command
            - service: notify.free_mobile_andre
              data:
                message: "{{ states.sensor.date_time.state}} > TEST Alerte au secours par télécommande TEST"
            - service: input_text.set_value
              target:
                entity_id: input_text.last_arm
              data:
                value: "{{now().strftime('%d/%m/%Y, %H:%M:%S')}} > Triggered by : {{ trigger.id }}"   

Enfin, on désarme cette alerte. A ce stade on peut aussi envoyer un SMS pour dire que c'était une fausse alerte... Je vais me contenter de jouer du buzzer...

        - conditions: 
            - "{{ trigger.id in ['Remote or Keypad'] and is_state('alarm_control_panel.alarmo', 'disarmed') }}"
          sequence:              
            - service: button.press
              target:
                entity_id: button.up_chime_play_buzzer

Le cas des tag RFID

Les tags RFID c'est sympa, mais hormis en coller deux cote à cote on ne pourra basiquement gérer qu'un seule information : tag_scanned

Hors, l'idée est de se servir d'un même tag collé à la porte pour armer ou désarmer. S'il s'agissait d'un switch on ferait un switch.toggle, mais pas pour une alarme. Il va falloir ruser.

Bien sur on pourrait mettre une condition pour savoir connaitre l'état de l'alarme, mais je ne veux pas utiliser de condition et uniquement un trigger avec son id:. J'ai passé des heures à essayer de faire un trigger template pour combiner le scan du tag + l'état de alarm_control_panel, mais sans succès car le tag n'est pas un device ou un entity ou l'on peut facilement lire l'état.

La solution consiste à créer un binary_sensor: temporisé :

template:
  - trigger:
      - platform: event
        event_type: tag_scanned
        event_data:
          tag_id: bf5018c7-c1a9-4220-a01a-66f885176795
#          name: Tag Green
#          device_id: 1d3ddfgy45805354ce090774e3f22510
    binary_sensor:
      name: Tag Green
      state: "{{ trigger.platform == 'event' }}"
      auto_off:
        seconds: 1

A noter que name: peut être utilisé à la place de tag_id: et que l'on peut jouer avec les autres informations que l'on trouve dans l'event afin par exemple de savoir quel appareil a scanné le tag et ainsi savoir qui a armé ou désarmé... 

Et ensuite il sera facile de créer un trigger basé sur un petit template et se servir de son id: pour action dans le chose:

  - platform: template
    value_template: "{{ is_state('binary_sensor.tag_green', 'on') and is_state('alarm_control_panel.alarmo', 'armed_home') }}"
    id: tag_to_disarm

Gestion des commandes de désarmement

Au risque de me répéter, cette partie aurait pu être intégrée à la première automation, si ZHA ne posait pas problème... Je le ferait probablement, mais pour l'exemple on va pour l'instant scinder cette partie.

Donc, pour les télécommandes d'alarme sous ZHA on va devoir écouter un event:, s'en servir pour désarmer nos système d'alarme mais également pour la désarmer et désarmer ses copines ainsi que le clavier. Simple ! J'avais exploré dans un précédent article plusieurs pistes afin de ne prendre en compte qu'un seul de ses "events" (en + elle en envoie 3 ou 4 identiques selon l'humeur et le modèle). J'ai un peu avancé et on ne va pas les écouter dans l'automation, mais se servir de l'état temporaire d'un binary_sensor: que l'on va pouvoir temporiser et ainsi ne prendre en compte qu'un seul event:

template:
  - trigger:
      - platform: event
        event_type: zha_event
        event_data:
          device_ieee: a4:c1:38:96:0b:cf:c9:61 # Woox RC1
          command: 'arm'
          args:
            arm_mode: 0
            arm_mode_description: 'Disarm'
            code: ''
            zone_id: 0
    binary_sensor:
      name: Woox RC1 to Disarmed
      icon: "{{ (trigger.platform == 'event') | iif('mdi:remote-off', 'mdi:remote') }}"
      state: "{{ trigger.platform == 'event' }}"
      auto_off:
        seconds: 5

Les déclencheurs : Ici on va écouter des actions de désarmement, que cela provienne d'un clavier, une télécommande (via leur binary_sensor:), ou en direct pour une source classique (tag RFID, MQTT, ...).

- id: '2bd0ertyyf-43fa-45f98f-aed0-disarm'
  alias: "Alarm @ Remote Disarm"
  description: 'Disarm Remote Control panel'
  mode: single
  trigger:
    - platform: state
      entity_id: binary_sensor.heiman_rc1_to_disarmed
      to: "on"
      id: "Heiman RC 1"  
    - platform: tag
      tag_id: 2b855225-c999-4b5558123-6c7d12345641
      id: "TAG Jaune"
    - platform: device
      domain: mqtt
      device_id: 139eb4ce27246465b0d5aa7560a953601
      type: action
      subtype: disarm
      discovery_id: 0x84fd27fffe915905 action_disarm
      id: "Heiman RC2"

Les conditions : Aucune, mais on pourrait imagine un flag de blocage activé à distance après une alerte...

condition:

Les actions : On utilise un chose: et on commence par :

  • Désarmer le panneau d'alarme virtuel, ce qui déclenchera l'action idoine dans la première automation.
  • Désarmer les télécommandes et claviers qui sont également des panneaux d'alarme, avec cette particularité ZHA qui fait qu'il faut un code pour désarmer une télécommande...
  • Ecrire un input_text: afin de savoir qui a désarmé.
  • J'envoie un message dans Slack qui me sert de log. Ca va également m'informer à distance que quelqu'un est arrivé et a désarmé 
  action:
    - choose: # DISARM
        - conditions: "{{ trigger.id in ['Push Button', 'KeyPad', 'Linkind RC', 'Heiman RC 1', 'Woox RC 1', 'TAG Porte', 'TAG Jaune', 'TAG Clé', 'TAG Carte', 'Heiman RC2' ] }}"
          sequence:
            - service: alarm_control_panel.alarm_disarm   # FAKE ALARM PANEL to Command
              data:
                code: "1234"
              target:
                entity_id: alarm_control_panel.home_alarm_command
            - service: alarm_control_panel.alarm_disarm
              data:
                code: !secret alarm_code_zha
              target:
                entity_id: 
                  - alarm_control_panel.lk_zb_keypad
                  - alarm_control_panel.lk_zb_remote
                  - alarm_control_panel.heiman_remote
                  - alarm_control_panel.woox_01
            - service: input_text.set_value
              target:
                entity_id: input_text.last_disarm
              data:
                value: "{{now().strftime('%d/%m/%Y, %H:%M:%S')}} > Disarm by : {{ trigger.id }}"
            - service: notify.slack_hass_canaletto
              data:
                message: "{{now().strftime('%d/%m/%Y, %H:%M:%S')}} > Disarmed by : {{ trigger.id }}" 

On peut également faire une place à une fonctionnalité détournée du clavier. En effet au delà de l'amenée Home, Away et Emergency, on peut également lui faire avaler n'importe quel code et effectuer une action précise en fonction de celui ci. Ici si je saisit 33 et valide je vais allumer les projecteurs.

    - choose:
        - conditions: "{{ trigger.id in ['KeyPad 33'] }}"
          sequence:
            - service: light.turn_off
              target:
                entity_id:
                  - light.groupe_de_projecteurs          

Enfin j'ai quelques actions par défaut, comme éteindre les sirènes au cas ou... (ça sent le debug...).

    - service: mqtt.publish
      data:
        topic: zigbee2mqtt/Sirène SMaBiT/set
        payload: >-
          {"warning": {"mode": "stop"}}
    - service: siren.turn_off
      target:
        entity_id:
          - siren.heiman_sirene_1
          - siren.heiman_sirene_2
          - siren.sirene_terrasse                

Et surtout on ajoute un délai car ces télécommandes ont la fâcheuse idée d'envoyer 3 ou 4 event: identique pour chaque appui. Donc combiné au mode: single de cette automation, on va en capturer qu'un seul. Ce délais est bien sur à ajuster plus ou moins au pif... Maintenant que je passe par des binary_sensor: temporisés, je pense que ce délai est devenu inutile.

    - delay : '00:00:08'

Les actions POST Armement / Désarmement

On va utiliser deux scripts qui seront exécutés par Alarmo après l'armement et après le désarmement. Chez moi ils vont servir à :

  • Jouer une annonce vocale en TTS sur une enceinte extérieure pour annoncer l'état armé ou désarmé.
  • Activer ou désactiver la vidéo surveillance.
  • Passer les volets en mode manuel et automatique au retour (voir l'automatisation des volets).
  • Gérer le mode ECO/Absent du climatiseur et des convecteurs.
  • Gérer les éclairages et le mode veille de mon PC.
  • Et plus encore selon votre imagination....

Les notifications d'alarme et sirènes

  • Notifications SMS pour l'intrusion, la détection incendie ou fuite d'eau. (notifications gérées par Alarmo).
  • Sirènes et projecteurs en cas d'intrusion :
    • la législation autorise au maximum 3 minutes pour chaque lancement de sirène.
    • Pas de limite pour les projecteurs. Et éclairer comme en plein jour peut également faire fuir et permettra d'avoir des images bien nettes sur les caméras.
    • Si vous avez un système audio multi room bien puissant, vous pouvez également envoyer une bonne salve d'AC-DC, ça peu perturber... ou inciter les voisins à appeler la maréchaussée.
    • Les armes, explosifs et autres gaz toxiques sont à proscrire.

Pour cela on va utiliser des scripts: qui seront lancés par Alarmo selon les conditions choisies (voir ici).

Etant donné que j'ai des détecteurs d'ouverture sur mes volets roulants, je peux également imaginer l'éclairage extérieur avant de déclencher l'alarme si un de ces volets est touché. Il faut être prévenant et éclairer l'intrus. Cela peut se gréer dans une partition secondaire d'Alarmo, ou simple une automation. Les caméras Unifi Protect (G4) font également de la détection de personne qui remonte dans Home Assistant.

Voilà, ce n'est pas exhaustif, vos idées son les bienvenues, ici ou sur HACF.

 

 

VMWare ESXi sur Nuc 9 & SSD firmware...

Comme je vous en avait parlé ici, j'ai remplacé mon serveur ESXi HP ML350p par un Nuc 9. Depuis plus de 6 mois ça fonctionne très bien.

Dans ce Nuc j'ai installé deux SSD NVMe Samsung 980 Pro. Les performance sont au top, mais voilà que plusieurs utilisateurs se plaignent que leurs SSD s'usent prématurément et finissent par passer en R/O. C'est d'autant plus fâcheux que sous ESX on ne dispose de rien pour surveiller les disques sur un NUC, celui ci ne remontant pas de données IPML comme le ferait un vrai serveur. Après avoir fait un peu la sourde oreille, Samsung a fini par sortir la mise à jour idoine.

Sous Windows il est très facile de mettre à jour (ou de migrer) un SSD, il suffit de lancer Samsung Magician et le tour est joué ! Sous VMWare ESXi rien de tout ça et il va falloir le faire à la mano. Heureusement pour cela Samsung fournit également les firmawares sous la forme d'une ISO, il suffit, en principe, de le griller sur une clé USB et de booter avec et de faire la mise à jour. Mais ça c'est la théorie. Dans la pratique j'ai appris à mes dépends que cette clé basée sur un vieux Linux était incapable de booter sur du matériel moderne, et quand elle boote certains disent même qu'elle ne reconnait que les claviers PS2... Allons donc !

Après quelques recherches j'ai découvert qu'il existait un contournement possible mis en pratique par la communauté Linux, mais nullement supportée par Samsung qui doit considérer que ses SSD sont destinés uniquement à l'environnement Windows, tout comme VMWare qui a sa liste de hardware supporté...

L'astuce consiste à booter sous Linux, un Live CD Ubuntu. Facile, non pas si simple car ce live CD ne voulait lui non plus pas booter. Le bios de ce Nuc n'étant pas de toute fraicheur j'en profite pour le mettre à jour sans plus de succès.

J'ai fini par trouver que pour booter sous Ubuntu il fallait désactiver TPM et SGX. Ca ne s'invente pas mais j'ai fini par pouvoir booter mon Ubuntu Live.

Une fois sur le desktop de mon Ubuntu je copie l'ISO de mise à jour (Samsung_SSD_980_PRO_5B2QGXA7.iso) que j'ai au préalable préparée sur une autre clé USB vers le répertoire Home et je monte le volume. Ensuite toujours depuis l'interface graphique j'extrait le fichier initrd un répertoire (/home/samsung par exemple). Ensuite je vais jusqu'à /home/samsung/initrd/root/fumagician et je peux lancer la mise à jour de mes SSD :

sudo ./fumagicien

Je fais la mise à jour de mes deux SSD et je relance le Nuc. Attention, le bon numéro de version n'apparaitra qu'âpres le prochain redémarrage. Donc même si c'est un peu fastidieux, je vous conseille de relancer le Live CD pour vérifier. De même si vous avez d'autres machines à mettre à jour, vous pouvez sauvegarder fumagician sur une clé pour éviter l'étape de l'extraction. 

Il existe une autre méthode en CLI pour les barbus, elle demandera toutefois de connecter le Live CE au réseau si on veut éviter l'étape de la clé USB, mais in fine ça revient au même :

wget https://semiconductor.samsung.com/resources/software-resources/Samsung_SSD_980_PRO_5B2QGXA7.iso
apt-get -y install gzip unzip wget cpio
mkdir /mnt/iso
sudo mount -o loop ./Samsung_SSD_980_PRO_5B2QGXA7.iso /mnt/iso/
mkdir /tmp/fwupdate
cd /tmp/fwupdate
gzip -dc /mnt/iso/initrd | cpio -idv --no-absolute-filenames
cd root/fumagician/
sudo ./fumagician

Voilà comment occuper (perdre) un après-midi d'hiver !

Attention toutefois, si vous avez créé un volume RAID, ce qui est discutable avec des SSD, il semblerait que vous ne puissiez les voir sans repasser le bios en AHCI.

Sources

 

Home Assistant & Sirènes

J'ai déjà parlé ici du système d'alarme Alarmo pour Home Assistant et notamment des commandes possibles pour armer et désarmer le système. Aujourd'hui il s'agira des sirènes.

J'ai trois modèles

  • La plus basique est juste branchée sur une prise commandée Zigbee ondulée. Donc un switch: vu en tant que siren:.
  • Une sirène Heiman qui sous ZHA décroche régulièrement et que j'ai du appairer à ma seconde passerelle sous Z2M.
    • Sous ZHA elle était vue en tant que siren:.
    • Sous Z2M elle est vue en tant que rien du tout...
  • Une sirène SMaBit sous Z2M également vue en rien du tout...

Pour commander une sirène via ZHA HA propose :

    - service: siren.turn_on
      target:
        entity_id:
          - siren.heiman_sirene

Pour commander une sirène sous Z2M il faut lui envoyer le payload correspondant :

    - service: mqtt.publish
      data:
        topic: zigbee2mqtt/Sirène SMaBiT/set
        payload: >-
          {"warning": {"mode": "fire", "level": "very_high", "strobe_level": "low", "strobe": "false", "strobe_duty_cycle": "10", "duration": "360"}}

Un peu plus compliqué et moins facile à mémoriser, mais ça fonctionne.

Mon objectif a donc été de faire en sorte qu'une sirène Z2M soit vue par Home Assistant comme une sirène ZHA et ainsi pouvoir la commander par un siren.turn_on comme les autres... Et voici ce que ça donne :

mqtt:
  siren:
    - unique_id: heiman_sirene_1s
      name: Heiman Sirène 1s
      state_topic: "zigbee2mqtt/Heiman Sirène 1/set"
      command_topic: "zigbee2mqtt/Heiman Sirène 1/set"
      optimistic: false
      qos: 0
      retain: true
      state_value_template: "{{ value_json.warning['mode'] }}"
      command_template: '{"warning": {"duration": 2, "mode": "burglar", "level": "very_high", "strobe": true}}'
      command_off_template: '{"warning": {"duration": 2, "mode": "stop", "level": "very_high", "strobe": true}}'
      state_on: "burglar"
      state_off: "stop"

Problème, car il y en a un, la sirène Heiman ne retourne aucun état sous Z2M. Il y a beaucoup d'articles à ce sujet, et l'intégration ne semble pas vraiment terminée. Hors dans l'activation il y a une durée, le principe étant d'activer la sirène pour x secondes. Et donc comme elle ne retourne pas son état, son commutateur restera sur ON alors que le délais d'activation est terminé et la prochaine action n'aura aucun effet... De plus certaines sirènes proposent plusieurs fonctions et tonalités (les bips de délais d'armement et désarmement par exemple).

Donc contrairement à ZHA ou l'intégration siren: peut gérer tous les modes, chercher à gérer une sirène Z2M en tant que siren: n'a pas de sens... (hormis peut être de bricoler un template: qui en fonction de la durée d'activation repasserait le commutateur à off, on peut aussi le faire via une automation: mais là ça va alourdir la chose...).

Alternative

S'agissant d'envoyer un payload à une sirène Z2M, ce que j'ai trouvé de plus logique est un mqtt:/button: et il faut donc en faire deux :

mqtt:
  button:
    - unique_id: siren_heiman_1_warning
      name: "Sirène Heiman 1 : Waraning"
      command_topic: "zigbee2mqtt/Heiman Sirène 1/set"
      payload_press: '{"warning": {"duration": 2000, "mode": "burglar", "level": "very_high", "strobe": true}}'
      qos: 0
      retain: false
      entity_category: "config"
      device_class: "restart"

    - unique_id: siren_heiman_1_stopped
      name: "Sirène Heiman 1 : Stopped"
      command_topic: "zigbee2mqtt/Heiman Sirène 1/set"
      payload_press: '{"warning": {"duration": 2, "mode": "stop", "level": "very_high", "strobe": true}}'
      qos: 0
      retain: false
      entity_category: "config"
      device_class: "restart"   

Ou plusieurs pour gérer les différents mode de la SMaBit :

mqtt:
  button:
    - unique_id: siren_smabit_armed
      name: "Sirène SMaBit : Armed"
      command_topic: "zigbee2mqtt/Sirène SMaBiT/set"
      payload_press: '{"squawk": {"state": "system_is_armed", "level": "veryhigh", "strobe": "true"}}'
      qos: 0
      retain: false
      entity_category: "config"
      device_class: "restart"

    - unique_id: siren_smabit_disarmed
      name: "Sirène SMaBit : Disarmed"
      command_topic: "zigbee2mqtt/Sirène SMaBiT/set"
      payload_press: '{"squawk": {"state": "system_is_disarmed", "level": "veryhigh", "strobe": "true"}}'
      qos: 0
      retain: false
      entity_category: "config"
      device_class: "restart"

    - unique_id: siren_smabit_warning
      name: "Sirène SMaBit : Warning"
      command_topic: "zigbee2mqtt/Sirène SMaBiT/set"
      payload_press: '{"warning": {"mode": "fire", "level": "very_high", "strobe_level": "low", "strobe": "false", "strobe_duty_cycle": "10", "duration": "360"}}'
      qos: 0
      retain: false
      entity_category: "config"
      device_class: "restart"

    - unique_id: siren_smabit_stopped
      name: "Sirène SMaBit : Stopped"
      command_topic: "zigbee2mqtt/Sirène SMaBiT/set"
      payload_press: '{"warning": {"mode": "stop"}}'
      qos: 0
      retain: false
      entity_category: "config"
      device_class: "restart"

Vous l'aurez compris, il s'agit plus d'un exercice de style qu'autre chose. Mais ça m'aura permis de comprendre comment créer des devices MQTT non reconnus par le discovery:. Merci @Mathieu pour ta participation !

Sources

 

Sony mon amour !

Amour déçu comme on le verra plus loin ! Depuis très jeune j'ai toujours été un afficionado de la marque Sony. Et si d'aucune pourraient trouver cela déplacé, il est des domaines ou je suis fidèle ! Coté TV après avoir eu quelques Trinitrons cathodiques célèbres tel le Profeel, je me suis offert jadis le premier moniteur plasma pro de la marque, qui avait du me couter un an de salaire et que je me suis résolu récemment à le porter à la déchetterie, le pauvre il trainait dans le garage depuis bientôt 20 ans !

Je ne vais pas vous faire la liste des écrans Sony que j'ai eu successivement, ni leur cout car ça me déprimerait, mais juste vous parler des déboires de mon dernier TV. Il y a un an j'ai craqué pour le A80J, un Oled ! Un TV intelligent sous Google TV, même si d'intelligence il n'a que la pub et les traqueurs que cela induit !

Je considère Google TV / Android TV comme étant le meilleur choix car il me permet d'installer les application qui me sont utiles, en opposition aux écosystèmes fragmentés de la concurrence qui ne proposent que l'essentiel bien commercial ! C'est du business, on le sait et on l'accepte car ça nous apporte du confort.

Autres points que les pseudo journalistes testeurs oublient souvent, c'est la télécommande et l'implémentation du CEC. Celles de Sony m'ont toujours satisfait alors que je déteste certaines d'autres marques. Mais Sony est radin, et sur le A80J la télécommande n'est pas même rétroéclairée ! Qu'à cela ne tienne il suffit de commander sur Amazon celle du A90J qui elle est premium et rétro éclairée... Quant au CEC il a toujours été très bien chez Sony.

Je ne vais pas vous raconter ce que vaut cette TV, plusieurs sites l'ont déjà fait plus ou moins bien. Enfin, je parle de TV, mais je ne regarde même pas la télé et aucune antenne n'y est raccordée. Je rêve d'une version moniteur !

Domotique

Oh le gros mot ! Encore un client qui veut faire des choses louches ! Louches car je ne me plie pas à la domotique commerciale et forcément limitative (Amazon, Google, Apple, Tuya, etc...) mais que ma domotique est libre, Home Assistant.

Et pourquoi faire ? Pas grand chose, juste inclure ce TV dans me scénarios (scènes). Et on va faire simple !

  • Play / Pause : on éteint et on rallume progressivement les éclairages comme au cinéma.
  • On / Off : on mets sur pause la musique qui était diffusée.

Ce n'est pas sorcier et très simple à faire. Mais pour ca il faut pouvoir extraire les informations du TV. Ca tombe bien ce TV est reconnu sous Home Assistant par l'intégration Bravia ou HomeKit. Dès lors on dispose des information idoines et ça fonctionne très bien, il suffit d'agir en fonction de l'état remonté.

Mais alors il se plaint de quoi le râleur ? Et oui il y a un "mais" !

Consommation

Pour accéder à ces fonctions il faut que le contrôle à distance ou / et HomeKit soit activé. C'est d'ailleurs le cas par défaut. Et les ingés de chez Sony, dont je ne doute pas des prouesses en audio / vidéo s'avèrent des brèles en réseau ! Pour que ces fonctions soient accessibles ils laissent simplement la partie réseau allumée quand le téléviseur est en veille. Et comme leur carte réseau doit dater du siècle dernier (elle a du mal à accrocher le réseau et est limitée à du 10/100 en 2022 ou on est censés passer de la full 4K !), elle consomme en veille pas moins de 26/28 W alors que si ces fonctions sont désactivées la veille est à seulement 0.9 W.

Cela pose deux constats :

  • Il est inacceptable qu'un TV en veille consomme 27 W en 2022 (c-a-d + ou - 50 € / an).
  • Il est inacceptable que ces fonctionnalités soient activées par défaut et induisent cette surconsommation ! Et certainement contraire aux directives européennes en la matière. Il est en effet peu probable qu'un consommateur lambda ne pense à mesurer et désactiver ces fonctions.

En l'état il est donc inenvisageable d'utiliser ces fonctionnalités qui ajoutent un plus au téléviseur. Et il faut noter que tout pilotage par les applis standard, Chromecast, Alexa, etc pose le même problème...

Le pire étant que si on désactive ces fonctions, quand on allume le téléviseur il mets bien trop longtemps pour accrocher le réseau, que ce soit en Ethernet ou en WI-FI. Je sais c'est fait pour regarder la télé avec un râteau sur la toiture !

Alternative

Avant j'utilisait un LCD Sony bien plus bête et une NVidia Shield que je pilotait très bien sans que cela ne grève mon budget. Le Shield allumait le TV en CEC et tout était parfait. Je vais peut être en racheter un, encore que mon Shield de 2015 fonctionne encore très bien dans ma chambre. Un bel exemple de non obsolescence qui mérite d'être signalé !

Et les autres ?

Je ne sais pas comment ça se passe chez les autres constructeurs de TV car je ne suis pas client. On me remonte 1 à 2 W avec les options de pilotage activées chez Samsung ou Panasonic, mais je n'ai pas vérifié. Par contre j'ai un bel élevage de Sonos, et là non plus ce n'est pas très joli à voir : aucun progrès depuis le début, conso identique en veille sur les anciens Play 1 et 3 et les nouveaux One (gen 1 et 2), entre 3 et 4 W voire plus de 8 à 11 W sur un AMP ou 6 W pour une Beam !

Moralité

La domotique n'est pas vraiment écologique, elle crée un énorme talon de consommation de part son infrastructure (caméras, nvr, switches, micro modules et autres prise connectées). Elle apporte plus de confort qu’elle ne fait réaliser des économies (les économies c’est le bobard que racontent certains à leur compagne quand c’est elle qui tiens les cordons de la bourse, car, hélas, la domotique reste un truc de mec, comme le BBQ, n’en déplaise à Sandrine).

Un panneau solaire va devoir s'imposer à moi !

Sources