Home Assistant & UPS

Vous allez me demander pourquoi gérer les onduleurs sur Home Assistant ? Simplement pour être avertit en cas de coupure électrique via une notification (si on est en déplacement il peut être intéressant d'envoyer une personne de confiance réarmer en cas d'une défaillance locale, histoire de ne pas perdre le contenu du congélateur (vécu)), ensuite exécuter des actions sur ce qui est secouru et en dernier ressort éteindre le serveur. Les informations ainsi remontées donneront également des indication sur l'état des batteries afin de les changer préventivement.

J'ai souvent des coupures à la campagne, j'ai deux gros onduleurs réseau de marque APC et je me suis dès mes débuts intéressé à eux avec ce que j'ai alors trouvé de plus simple.

APC UPS Daemon

ApcUpsd s'installe sous la forme d'un addon et la suite est intégrée à Home Assistant.

On ajoute aux addons ce repository : https://github.com/korylprince/hassio-apcupsd et on installe la version IP ou USB qui nous convient. Et on configure.

name: APC UPS
cable: ether
type: snmp
device: 192.168.210.6

Ensuite on ajoute ça dans configuration.yaml :

apcupsd:

Ainsi que quelques sensors :

- platform: apcupsd
  resources:
    - apc
    - date
    - hostname
    - version
    - upsname
    - cable
    - driver
    - upsmode
    - starttime
    - model
    - status
    - linev
    - loadpct
    - bcharge
    - timeleft
    - mbattchg
    - mintimel
    - maxtime
    - maxlinev
    - minlinev
    - outputv

Et on termine avec un binary :

- platform: apcupsd
  name: UPS

Les paquets sont marqués obsolètes, mais pour l'instant j'utilisait ça depuis un an et ça fonctionne très bien.

SNMP

Si on dispose d'un onduleur réseau, ici APC, voici un "package" (mode pakage de HA) pour le faire en SNMP. Le gros avantage c'est qu'il n'y a absolument rien à installer, SNMP étant supporté de base dans Home Assistant. Attention c'est un peu long...

sensor:
  - platform: snmp
    name: ups_smart_750_capacity
    host: 192.168.210.7
    baseoid: 1.3.6.1.4.1.318.1.1.1.2.2.1.0
    accept_errors: true
    unit_of_measurement: '%'
  - platform: snmp
    name: ups_smart_750_runtime_remaining
    host: 192.168.210.7
    baseoid: 1.3.6.1.4.1.318.1.1.1.2.2.3.0
    accept_errors: true
    value_template: >-
      {% set time = (value | int) / 100 | int %}
      {% set minutes = ((time % 3600) / 60) | int %}
      {% set hours = ((time % 86400) / 3600) | int %}
      {% set days = (time / 86400) | int %}
    
      {%- if time < 60 -%}
        Less than a minute
        {%- else -%}
        {%- if days > 0 -%}
          {{ days }}d
        {%- endif -%}
        {%- if hours > 0 -%}
          {%- if days > 0 -%}
            {{ ' ' }}
          {%- endif -%}
          {{ hours }}h
        {%- endif -%}
        {%- if minutes > 0 -%}
          {%- if days > 0 or hours > 0 -%}
            {{ ' ' }}
          {%- endif -%}
          {{ minutes }}m
        {%- endif -%}
      {%- endif -%}
  - platform: snmp
    name: ups_smart_750_load_percentage
    host: 192.168.210.7
    baseoid: 1.3.6.1.4.1.318.1.1.1.4.2.3.0
    accept_errors: true
    unit_of_measurement: '%'
  - platform: snmp
    name: ups_smart_750_battery_temperature
    host: 192.168.210.7
    baseoid: 1.3.6.1.4.1.318.1.1.1.2.2.2.0
    accept_errors: true
    unit_of_measurement: '°C'
  - platform: snmp
    name: ups_smart_750_battery_status
    host: 192.168.210.7
    baseoid: 1.3.6.1.4.1.318.1.1.1.2.2.4.0
    accept_errors: true
    value_template: >-
      {%if value == '1' %}
        Good
      {% elif value == '2' %}
        Failed
      {% endif %}
  - platform: snmp
    name: ups_smart_750_type
    host: 192.168.210.7
    baseoid: 1.3.6.1.4.1.318.1.1.1.1.1.1.0
    accept_errors: true
  - platform: snmp
    name: ups_smart_750_input_voltage
    host: 192.168.210.7
    baseoid: 1.3.6.1.4.1.318.1.1.1.3.2.1.0
    accept_errors: true
    unit_of_measurement: 'V'
  - platform: snmp
    name: ups_smart_750_last_transfer_reason
    host: 192.168.210.7
    baseoid: 1.3.6.1.4.1.318.1.1.1.3.2.5.0
    accept_errors: true
    value_template: >-
      {%if value == '1' %}
        No events
      {% elif value == '2' %}
        High line voltage
      {% elif value == '3' %}
        Brownout
      {% elif value == '4' %}
        Loss of mains power
      {% elif value == '5' %}
        Small temporary power drop
      {% elif value == '6' %}
        Large temporary power drop
      {% elif value == '7' %}
        Small spike
      {% elif value == '8' %}
        Large spike
      {% elif value == '9' %}
        UPS self test
      {% elif value == '10' %}
        Excessive input voltage fluctuation
      {% endif %}
  - platform: snmp
    name: ups_smart_750_output_load
    host: 192.168.210.7
    baseoid: 1.3.6.1.4.1.318.1.1.1.4.2.3.0
    accept_errors: true
    unit_of_measurement: '%'
  - platform: snmp
    name: ups_smart_750_output_current
    host: 192.168.210.7
    baseoid: 1.3.6.1.4.1.318.1.1.1.4.2.4.0
    accept_errors: true
    unit_of_measurement: 'A'
  - platform: snmp
    name: ups_smart_750_last_self_test_result
    host: 192.168.210.7
    baseoid: 1.3.6.1.4.1.318.1.1.1.7.2.3.0
    accept_errors: true
    value_template: >-
      {%if value == '1' %}
        OK
      {% elif value == '2' %}
        Failed
      {% elif value == '3' %}
        Invalid Test
      {% elif value == '4' %}
        Test In Progress
      {% endif %}
  - platform: snmp
    name: ups_smart_750_last_self_test_date
    host: 192.168.210.7
    baseoid: 1.3.6.1.4.1.318.1.1.1.7.2.4.0
    accept_errors: true
  - platform: snmp
    name: ups_smart_750_communication_status
    host: 192.168.210.7
    baseoid: 1.3.6.1.4.1.318.1.1.1.8.1.0
    accept_errors: true
    value_template: >-
      {%if value == '1' %}
        on
      {% elif value == '2' %}
        off
      {% endif %}
  - platform: snmp
    name: ups_smart_750_status
    host: 192.168.210.7
    baseoid: 1.3.6.1.4.1.318.1.1.1.4.1.1.0
    accept_errors: true
    value_template: >-
      {%if value == '1' %}
        Unknown
      {% elif value == '2' %}
        On Line
      {% elif value == '3' %}
        On Battery
      {% elif value == '4' %}
        On Smart Boost
      {% elif value == '5' %}
        Timed Sleeping
      {% elif value == '6' %}
        Software Bypass
      {% elif value == '7' %}
        Off
      {% elif value == '8' %}
        Rebooting
      {% elif value == '9' %}
        Switched Bypass
      {% elif value == '10' %}
        Hardware Failure Bypass
      {% elif value == '11' %}
        Sleeping Until Power Returns
      {% elif value == '12' %}
        On Smart Trim
      {% endif %}

homeassistant:
  customize:
    sensor.ups_smart_750_status:
      device_class: connectivity
      friendly_name: Smart-UPS status
    sensor.ups_smart_750_capacity:
      device_class: battery

NUT

Enfin on peut aussi utiliser NUT (Network UPS Tools) que beaucoup ici connaissent et qui est une légende dans le monde des UPS.

Pour ça il faut installer le serveur NUT que l'on trouvera dans les addons et qui communiquera avec vos onduleurs. Voici deux configurations à adapter selon que l'onduleur soit connecté en USB ou en IP. La documentation de NUT est une véritable bible des UPS et vous y trouverez ce qui vous manque.

USB

users:
  - username: nut
    password: MyPassword
    instcmds:
      - all
    actions: []
devices:
  - name: apc
    driver: usbhid-ups
    port: auto
    config:
      - desc = "APC Back-UPS 600VA"
      - vendorid = 051d
mode: netserver
shutdown_host: 'false'

IP

users:
  - username: nut
    password: MyPassword
    instcmds:
      - all
    actions: []
devices:
  - name: apc_1400
    driver: snmp-ups
    port: 192.168.210.6
    community: public
    snmp_version: v1
    pollfreq: 15
    config:
      - desc = "APC Smart-UPS 1400VA"
  - name: apc_750
    driver: snmp-ups
    port: 192.168.210.7
    community: public
    snmp_version: v1
    pollfreq: 15
    config:
      - desc = "APC Smart-UPS 750VA"
mode: netserver
shutdown_host: 'false'

A partir de là il faut aller dans les intégrations, ajouter l'intégration NUT, la configurer avec ces informations :

  • Host : a0d7b954-nut
  • Port : 3493
  • Username : nut
  • Password : MyPassword

Et ensuite choisir les seniors que l'on souhaite créer, utiliser et superviser.

Automations

Une fois que l'on dispose des sensors il sera possible d'effectuer des actions selon le comportement de ceux ci. Libre à vous d'imaginer, ou par exemple utiliser l'intégration Alert pour recevoir des notifications répétées.

ups_1:
  name: UPS APC 1
  entity_id: binary_sensor.ups
  state: 'off'   # Optional, 'on' is the default value
  repeat:
    - 10
    - 30
    - 60
    - 300
  can_acknowledge: true  # Optional, default is true
  skip_first: true  # Optional, false is the default
  message: "{{ states.sensor.date_time.state}} > A L A R M  U P S | Coupure EDF" 
  done_message: "{{ states.sensor.date_time.state}} > A L A R M  U P S | Rétablissement EDF"
  notifiers:
    - slack_hass_canaletto
    - Free_Mobile

Voilà, un peu soporifique, je suis d'accord, mais le sujet n'est pas rigolo !

 

 
 

 

 

Télétravail, RDP & VPN

Par les temps qui courent, le télétravail est de mise, mais tout le monde n’est pas placé à la même enseigne. Il y a les grandes entreprises ou les cadres sont équipées d’ordinateurs portables et ou les infrastructures de sécurité existent et ou il suffit juste d’extrapoler pour les salariés qui ne sont pas équipés. Et puis il y a les petites entreprises, voire très petites ou rien n’existe et ou bien souvent le salarié sera contraint dans l'urgence de travailler sur son ordinateur personnel.

Et dans ce cas on peu se retrouver dans des situations très précaires, en termes de sécurité ou de praticité, car l’ordinateur familial est généralement utilisé par d’autres personnes, souvent les enfants, ce qui peut rapidement poser des problèmes. On pourrait bien sur isoler une session, mais c’est ingérable et par définition l’intervention sur le PC personnel que l'on réalise en avec une prise de contrôle à distance doit se limiter au strict minimum.

La solution bien souvent utilisée conste à permettre au télétravailleur de travailler distance en se connectant à son ordinateur de bureau, et ainsi conserver son environnement habituel. Pour ce faire on pense d’abord aux solutions de type AnyDesk, TeamViewer, voire VNC, solutions simples à mettre en œuvre mais qui offrent peu de confort à l’usage. La seule vraie solution confortable est d'utiliser le bureau à distance qui fonctionne avec le protocole RDP. Le problème du RDP c’est sa sécurisation car ce protocole directement exposé sur internet est une véritable passoire dont les méchants hackers sont friands si on se content de simples redirections de port. Microsoft ne s’est jamais occupé de ce problème pour cet usage, la seule solution proposée est RDS (Remote Desktop Services), une usine à gaz certes efficace mais totalement inadaptée aux TPE. Royal TS Gateway peut constituer une alternative (il y en a d’autres), mais pas pour de très petites entreprises qui n'ont que peux d'infra et généralement pas d'IT.

L’autre alternative reste l’utilisation d’un VPN en équipant les postes clients. Il y a plusieurs façons de faire, mais je voulais quelque chose de transparent, facilement administrable, ne m’imposant pas d’intervention ultérieure sur le poste client et ne nécessitant pas l’installation d’un serveur. Je vais donc une fois de plus utiliser Zerotier qui répond à mon besoin et qui est gratuit jusqu'à 50 clients.

  • Pas de serveur à déployer
  • Installation minimale sur le poste client
  • Gestion des ACL centralisée

Ce n’est pas la façon de faire la plus sécurisée (le port RDP est ouvert entre le client et son PC de bureau), mais on va limiter le risque avec un bon équilibre risque / coût / praticité.

Zerotier

Je vais faire l’impasse sur la mise en œuvre, j’en ai déjà parlé. Ici on va installer le client sur les deux postes à associer et leur figer une IP sur la console d’admin (Zerotier supporte Windows, MacOS, Linux et même Android et IOS). Ensuite on va s’assurer que seul le trafic RDP du poste client soit autorisé à se connecter au PC de bureau en utilisant les règles dans la console.

# Allow only IPv4, IPv4 ARP, and IPv6 Ethernet frames.
drop
	not ethertype ipv4
	and not ethertype arp
	and not ethertype ipv6
;
accept ipprotocol tcp and dport 443 or dport 80;

accept dport 3389 and ipsrc 10.147.1.20/32 and ipdest 10.147.1.30/32; # André
accept dport 3389 and ipsrc 10.147.1.21/32 and ipdest 10.147.1.31/32; # Carole
accept dport 3389 and ipsrc 10.147.1.23/32 and ipdest 10.147.1.33/32; # Bernard

drop chr tcp_syn and not chr tcp_ack; # No new TCP connections
;
# Drop TCP SYN,!ACK packets (new connections) not explicitly whitelisted above
break                     # break can be overridden by a capability
  chr tcp_syn             # TCP SYN (TCP flags will never match non-TCP packets)
  and not chr tcp_ack     # AND not TCP ACK
;
cap superuser
  id 2000 accept;
accept; # Accept what's left, returning RDP traffic

Coté poste au bureau on s'assure que le PC ne se met pas en veille (veille et verrouillage de l'écran uniquement) et on autorise RDP (le bureau à distance) sur le poste. Pour ça il faut un Windows Pro (mise à niveau à envisager parfois car les TPE qui vont acheter leur PC à la Fnac ou sur Amazon se retrouvent souvent avec une édition Famille de Windows).

Le client RDP

Do coté du poste client, après s'être assurée que la machine est à jour (on ne fait pas ça sur un vieux PC sous XP) et pas vérolée, il va nous falloir un client RDP et surtout interdire la mémorisation du mot de passe afin que n'importe qui ne puisse pas se connecter. Pour ça il y a une stratégie (Policie) à configurer ou une modification de registry, sauf que le PC personnel est une édition familiale il faudra ruser pour avoir accès aux stratégies...

Registry Hive HKEY_CURRENT_USER
Registry Path SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services
Value Name DisablePasswordSaving
Value Type REG_DWORD
Enabled Value 1
Disabled Value 0

Tant sur Windows que sur Mac il existe plusieurs clients RDP proposés par Microsoft avec des possibilités inégales.

  • Client RDP de base Windows : Il supporte les redirections d'imprimantes, mais visiblement il est impossible malgré la stratégie d'interdire la mémorisation du mot de passe de session.
  • Client RDP enrichi que l'on peut télécharger sur le Windows Store pour Windows ou Apple Store MacOS : Il ne supporte pas la redirection des imprimantes, par contre il prend bien en compte la stratégie d'interdiction de mémorisation du mot de passe.

Tout ça se passe plutôt bien bien si le PC personnel est sous Windows, par contre si c'est un Mac vous imaginez bien que Microsoft ne s'est pas préoccupé du mappage du clavier qui est différent sur chez Apple. Et ça c'est juste insupportable à l'usage. Me voici contraint de chercher une alternative, alternative que je vais finalement utiliser sous Windows également car elle est plus sécurisée et bien plus fonctionnelle.

Royal TS/TSX

Royal TS (ou TSX sur Mac) est un client multi protocoles que j'utilise depuis des années pour gérer des douzaines de serveurs. Il inclus bien sur le RDP et une multitude d'options dont le mappage du clavier entre un Mac et un PC, la gestion des imprimantes et la possibilité de lancer des commandes avant et après une session. Et autre avantage il va être possible de chiffrer toutes les informations contenue dans ce client. De plus le fichier de configuration peut être partagé entre un PC et un Mac via un drive.

Royal TS/TSX n'est pas gratuit pour gérer une multitude de serveurs, mais cerise sur le gâteau il existe une version gratuite limitée à 10 connections. Juste ce dont on a besoin ici.

On installe sur le PC du salarié, on crée le profil (que l'on peut créer à l'avance) correspondant à son poste de travail avec l'option plein écran, on redirige les imprimantes et sur un Mac on gère le mappage. On n'hésite pas à enregistrer les identifiants car ici c'est le mot de passe de chiffrage de l'application qui sécurisera toutes les connections enregistrées.

A partir de là le salarié lance Royal TS/TSX, saisit le mot de passe de l'application et a accès à son PC de bureau. A noter qu'il est également possible de lui donner accès dans cette même application à d'autres environnements (RDP, VNC, SSH, PS, Terminal, ou des web apps sois IE ou Chromium). Et comme on peut batcher des process avant et après une ouverture de session on peut même imaginer de lancer et désactiver Zerotier avant et après.

Autre avantage de cette solution, il est possible de créer des fichiers de configuration chiffrés et facilement déployables (envoi par mail par exemple).

Voilà, ce n'est pas parfait, mais dans le contexte actuel rien n'est parfait. Et cette solution permettra le télétravail à peu de frais avec un minimum de sécurité et de confort.
 

Installer Ghost sur aaPanel avec Docker

Il y a quelques semaines je vous avait vaguement expliqué comment installer aaPanel pour gérer un linux. Avec aaPanel on peut installer en quelques clics un site web, un Wordpress ou un autre CMS. Le hic c'est que celui que je veux tester n'est pas dans la liste, mais heureusement aaPanel gère les container Docker et justement Ghost est disponible préinstallé dans ce mode.

Je prends ici l'exemple de Ghost, mais ce qui suit s'applique à n'importe quel container d'application web.

Docker

Dans aaPanel vous pouvez ajouter Docker Manager depuis l'AppStore, celui-ci s'avèrera suffisant pour installer quelques containers, mais trop léger pour faire la majorité des manipulations. L'avantage à l'installer c'est que ça ajoutera au serveur tout ce qui est nécessaire de base pour Docker/

L'alternative consiste à installer Portainer qui lui offre une interface complète pour Docker, dans un container (il existe des alternatives).

docker volume create portainer_data
docker run -d -p 8000:8000 -p 9000:9000 --name=portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce

Mais au final le plus simple est encore Docker Compose ou d'y aller directement en SSH, ou plus simplement avec le terminal intégré à aaPanel.

Il y a plusieurs choses importantes à paramétrer pour utiliser Ghost dans un Docker :

  • Le port que l'on restituera ou pas sur le host (la machine qui supporte Docker). Si on ne le configure pas Ghost sera uniquement accessible en local via l'IP du container (http://172.17.0.2:2368 par exemple) et non sur le réseau local et non sur le réseau local qui héberge le serveur. Sur une machine isolée, un VPS par exemple, ce n'est pas gênant car on servira de NGINX pour accéder au site en reverse proxy au passage gérer le certificat Lets'Encrypt. Par contre si le point d'entrée passe par un firewall sur une autre IP du LAN, il sera important d'exposer un port sur le host afin d'éviter le passage par un NGINX local, inutile dans ce cas.
  • Le second point consiste à paramétrer le répertoire qui contiendra les données (db, images, etc...) de notre site. Si on ne le fait pas Docker va créer un répertoire arbitraire pour y stocker les données du container tel que cela aura été défini par celui qui a créé le Docker (dans l'absolu ces données pourraient très bien rester dans le container, mais ce ne serait pas du tout une bonne idée). Sur un VPS on peut par exemple faire en sorte que les données soient dans un sous répertoire de l'utilisateur.
  • Enfin, et si l'on veut éviter d'avoir à le faire plus tard, il faut définir l'url du site que l'on va créer. C'est un point important car comme dans Wordpress, Ghost à besoin de connaitre l'url sur lequel il tourne pour la navigation. Si on ne le précise pas et que l'on redirige le site via un reverse proxy (par exemple : https://www.domain.tld > http://172.17.0.2:2368) cela semblera fonctionner mais certains liens seront incorrect puisqu'ils pointeront sur l'IP du container.

Mise en pratique

Création du container minimaliste :

docker run -d --name ghost ghost

Création du container avec la gestion des ports et de l'url :

docker run -d --name ghost -e url=https://www.domain.tld -p 3001:2368 ghost

Création du container avec la gestion des ports, de l'url en précisant du répertoire contenant les données, une option de redémarrage et une image particulière (pour l'exemple car idéalement on utilise celle par défaut qui est la dernière :latest) :

docker run --name ghost -p 3001:2368 -e url=https://www.domain.tld -v /home/lionel/ghost/content:/var/lib/ghost/content --restart=always -d ghost:1.21.1-alpine

En fait cette dernière possibilité, bien que proposée ne fonctionne pas, ou plus, au niveau de l'url. Je vais donc créer mon docker sans l'url et en profiter pour mettre mes données dans un répertoire un peu plus parlant. Pour ça je vais au préalable créer un volume.

docker volume create canaletto_data
docker run --name canaletto -p 3001:2368 -v canaletto_data:/var/lib/ghost/content --restart=always -d ghost

Tout ça c'est bien beau si on a qu'un seul container et que l'on ne veut pas exposer les ports. Le problème dans cette configuration c'est que mes containers vont s'attribuer par défaut des IP par ordre de démarrage. Par défaut le host docker à 172.17.0.1 et le premier container à être lancé prendra la suivante et ainsi de suite... Sauf que mon reverse proxy aura besoin de connaitre une IP fixe.  Il me faut donc trouver la solution pour figer leur IP. Simple il dit lui ! J'ai fini par trouver une méthode simple qui semble faire le job :

On crée un nouveau réseau Docker que je vais appeler my_bridge :

docker network create --subnet=172.18.0.0/16 --gateway=172.18.0.1 my_bridge
docker network ls  (ls pour voir les réseaux, rm pour en supprimer un)

Ensuite on crée le container en spécifiant le réseau à utiliser et l'IP à lui affecter et cela permet également d'isoler les container entre eux (entre temps j'ai bien sur crée une image de mon précédent container pour ne pas le perdre) :

docker run --name Ghost-2 --net my_bridge --ip 172.18.0.10 -p 3010:2368 -v canaletto_data:/var/lib/ghost/content --restart=always -d ghost:2

Ensuite je vais aller éditer le fichier de configuration Ghost qui se trouve dans le docker.

docker ps pour repéréer l'ID
docker container exec -it b23f1490ccfb /bin/bash

Et une fois dans le docker

apt-get update
apt-get install nano
nano config.production.json

Et là je vais modifier l'url qui se présente ainsi "url": "http://localhost:2368", en "url": "https://ghost.canaletto.fr",. Attention à bien sortir avec un exit si vous souhaitez y revenir... (ce point sur l'url n'est pas clair dans ma tête).

Une fois notre docker installé on a plusieurs choix possibles pour y accéder

  • En direct si on a exposé le port 80 ou sur le port 2368 qui est le port par défaut de Ghost dans le container, et à condition d'ouvrir le port dans aaPanel. C'est clairement plus que déconseillé car d'une part il n'y aura pas de SSL et d'autre part la sécurité minimale impose un reverse proxy et votre site exposé directement risque de se faire rapidement descendre. De plus ça limiterait à un site par IP.
  • Via le reverse proxy d'un autre firewall sur le LAN. Dans mon cas j'utilise pfsense avec HAProxy et Acme pour gérer les certificats. Je vais donc faire pointer HAProxy sur l'IP et le port exposée sur mon host Docker (https://www.domain.tld > http://192.168.1.10:3001 (IP LAN de mon serveur Docker qui est dans une VM))
  • Enfin si on est par exemple dans un VPS on va y accéder via un site créé avec NGINX, dans lequel on va gérer le certificat via Let's Encrypt et configurer la partie reverse proxy qui va pointer sur l'IP et le port du container. C'ets ce que l'on va voir dans le chapitre suivant.

NIGNX

On va ici faire les choses via aaPanel qui nous sert d'interface. Mais ceux connaissent peuvent bien sur éditer les fichiers de configuration idoines. NIGNX s'installe via l'App Store de aaPanel en un clic. Ensuite on va aller créer un site via le menu Website avec le nom de domaine que l'on souhaite utiliser, ici ghost.canaletto.fr sur le port par defaut qui est le 80. On associe pas de FTP (on pourra le faire plus tard et ainsi le faire pointer sur le répertoire des données Ghost, pas de Database car elle est gérée par Ghost, pas de PHP car c'est ici inutile. Pour le SSL il faudra de toutes façons renseigner plus tard la méthode (vérification par fichier ou par DNS via l'API CloudFlare si le DNS du domaine est géré par eux).

Ensuite on va aller ajouter un reverse proxy dans NIGNX et ainsi rediriger les requêtes vers l'IP et le port du container qui contiens Ghost en n'oubliant pas de préciser le nom du domaine utilisé. Attention, pour je ne sais quelle raison il faut configurer le certificat avant de mettre en place le reverse proxy. C'est probablement une limitation (autre nom pour un bug) liée à aaPanel.

A ce stade Ghost est normalement accessible et on peut commencer à le configurer via l'url https://www.domain.tld/ghost. La suite coule de source et est expliquée dans la documentation Ghost par ailleurs très complète.

Bonus

Le gros avantage de Ghost est que l'on peut lui injecter du code. Soit dans l'admin pour des fonctionnalités globales, soit dans une page ou un article, par exemple pour y ajouter directement un bouton Paypal si l'article présente un produit que vous vendez, ou encore un formulaire ou une vidéo externe.

Au niveau global on peut injecter du code dans deux parties, Site Header et Site Footer.

Zoom

Imaginons que je veuille agrandir les images en cliquant dessus. Je vais alors utiliser ce composant (il en existe d'autres) et ajouter deux scripts dans la partie "footer". Le premier le chargera via un CDN tandis que le second permettra son exécution.

<script async src="https://cdn.jsdelivr.net/gh/coreysnyder04/[email protected]a01/fluidbox-ghost-blog-plugin.min.js"></script>
<script>
    window.fluidboxGhostConfig = {
      theme: 'image-backdrop', // Options: light, dark, image-backdrop, hsla(262, 100%, 82%, 0.6)
      showCaption: true, // Sets whether to capture the caption and show it below the image when expanded
    }
</script>
Le code

Comme je publie régulièrement du code je vais ajouter Prism afin d'obtenir une présentation élégante des différents langages affichés. Je commence par le header et on termine par le footer. Au passage je vais également chercher une police chez Google et j'ajoute des modification de style pour la présentation du code (au début) mais également afin d'utiliser ma nouvelle police à la place de celle proposée par le thème par défaut. Et au passage dans le footer je vais également ajouter de quoi numéroter les lignes de mon code et ouvrir les liens dans une nouvelle fenêtre.

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.16.0/themes/prism-okaidia.min.css" integrity="sha256-Ykz0nNWK7w4QWJUYR7OraN4773aMB/11aMt1nZyrhuQ=" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.16.0/plugins/line-numbers/prism-line-numbers.min.css">

<link href='https://fonts.googleapis.com/css?family=Encode+Sans:300&display=swap' rel='stylesheet'>

	<style type="text/css" media="screen">
        .post-full-content pre strong {
            color: white;
        }
        .post-full-content pre {
            line-height: 0.9;
        }
        .post-full-content pre code {
            white-space: pre-wrap;
            hyphens: auto;
            line-height: 1.6;
            font-size: 0.7em;
        }
        pre[class*=language-] {
        	margin: 1.75em 0;
    	}
        .post-template p {
 			text-align: justify;
		}
  
  		p { 
            font-family: 'Encode Sans', sans-serif; 
        }      
		li { 
            font-family: 'Encode Sans', sans-serif; 
        }      
        blockquote {
            font-family: 'Encode Sans', sans-serif; 
        }      

    </style>
<script>
    window.addEventListener('DOMContentLoaded', (event) => {
        document.querySelectorAll('pre[class*=language-]').forEach(function(node) {
            node.classList.add('line-numbers');
        });
        Prism.highlightAll();
    });
</script>
<script type='text/javascript'>
  $( document ).ready(function() {
  	$(".post-content a").attr("target","moredetail");
  });
</script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.16.0/prism.min.js" integrity="sha256-NFZVyNmS1YlmiklazBA+TALYJlJtZj/y/i/oADk6CVE=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.16.0/components/prism-markup-templating.min.js" integrity="sha256-41PtHfb57czcvRtAYtUhYcSaLDZ3ahSDmVZarE0uWPo=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.16.0/components/prism-javascript.min.js" integrity="sha256-KxieZ8/m0L2wDwOE1+F76U3TMFw4wc55EzHvzTC6Ej8=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.16.0/components/prism-css.min.js" integrity="sha256-49Y45o2obU1Yv4zkYDpMDyAa+D9sgKNbNy4ZYGRl/ls=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.16.0/components/prism-php.min.js" integrity="sha256-gJj4RKQeXyXlVFu2I8jQACQZsii/YzVMhcDT99lr45I=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.16.0/components/prism-sql.min.js" integrity="sha256-zgHnuWPEbzVKrT72LUtMObJgbwkv0VESwRfz7jpdsq0=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.16.0/components/prism-yaml.min.js" integrity="sha256-JoqiKM2GipZjbGjNyl62d6qjQY1F9QTLriWOe4N76wE=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.16.0/components/prism-sass.min.js" integrity="sha256-3oigyyaPovKMS9Ktg4ahAD1R6fOSMGASuA03DT8IrvU=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.16.0/components/prism-json.min.js" integrity="sha256-18m89UBQcWGjPHHo64UD+sQx4SpMxiRI1F0MbefKXWw=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.16.0/components/prism-bash.min.js" integrity="sha256-0W9ddRPtgrjvZVUxGhU/ShLxFi3WGNV2T7A7bBTuDWo=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.16.0/components/prism-python.min.js" integrity="sha256-zXSwQE9cCZ8HHjjOoy6sDGyl5/3i2VFAxU8XxJWfhC0=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.16.0/components/prism-ruby.min.js" integrity="sha256-SGBXZakPP3Fv0P4U6jksuwZQU5FlC22ZAANstHSSp3k=" crossorigin="anonymous"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.16.0/plugins/line-numbers/prism-line-numbers.min.js"></script>
Edition

L'éditeur de Ghost fonctionne en Markdown qui est devenu un standard de fait. Mais il est également possible de créer ses articles avec un éditeur externe comme Zettlr! ou zNote (il en existe plein d'autres). Personnellement je préfère TinyMCE à Markdown, mais c'est surement parce que je suis vieux et que j'ai été lobotomisé par Word...

Traduction

Ghost est livré en anglais. Mais il est prévu pour être traduisible. Pour autant la chose n'est pas forcément simple et va demander un peu de patience. Cela se fait au niveau du thème, et donc chaque traduction est propre à chaque thème. C'est plutôt bien expliqué ici et on trouve des thèmes (payant) qui sont traduits. Dans l'absolu il est même possible de traduire le backoffice, de même qu'il sera possible de configurer Ghost en multi langage (ça c'est plus compliqué).

J'ai commencé à traduire le thème de base, Casper. Donc on commence par faire une copie du thème, on ouvre le .zip et on le renomme. Ensuite il va falloir ajouter une répertoire /locales dans le thème et y ajouter à minima un fichier fr.json (qui suit). Le plus fastidieux sera d'aller dans les templates ajouter {{t à gauche des tous les textes et de terminer par }} (donc Page suivante devient {{t page suivante }}). Fastidieux mais pas énorme non plus.

Explications à venir... (ajout des autres ficjiers)

{
    "Back": "Retour",
    "Newer Posts": "Articles plus récents",
    "Older Posts": "Articles plus anciens",
    "Page {page} of {pages}": "Page {page} sur {pages}",
    "Subscribe": "S’abonner",
    "Subscribe to {blogtitle}": "S’abonner à {blogtitle}",
    "Subscribed!": "Abonné !",
    "with the email address": "avec l’adresse e-mail",
    "Your email address": "Votre adresse e-mail",
    "You’ve successfully subscribed to": "Vous vous êtes abonné avec succès à",
    "A collection of posts": "Une catégorie d’articles",
    "A collection of 1 post": "Une catégorie avec un article",
    "A collection of % posts": "Une catégorie avec % articles",
    "Get the latest posts delivered right to your inbox": "Recevez les derniers articles directement dans votre boîte aux lettres.",
    "Go to the front page": "Aller sur la page d’accueil",
    "Latest Posts": "Derniers articles",
    "Message:": "Message :",
    "<a href='{url}'>More posts</a> by {name}": "<a href='{url}'>Plus d’articles</a> par {name}",
    "No posts": "Aucun article",
    " (Page %)": " (Page %)",
    "Read More": "En savoir plus",
    "Read <a href='{url}'>more posts</a> by this author": "Lire <a href='{url}'>plus d’articles</a> de cet auteur",
    "Ref:": "Réf. :",
    "See all % posts": "Voir les % articles",
    "Share this": "Partager",
    "Stay up to date! Get all the latest & greatest posts delivered straight to your inbox": "Restez à jour ! Recevez tous les derniers articles directement dans votre boîte aux lettres.",
    "This post was a collaboration between": "Cet article est une collaboration entre",
    "[email protected]": "[email protected]",
    "1 post": "Un article",
    "% posts": "% articles",
    "1 min read": "1 min de lecture",
    "% min read": "% min de lecture"
}
Les intégrations

Ghost supporte beaucoup d'intégrations de base et il est possible d'en ajouter simplement en modifiant le code. Par exemple si on souhaite ajouter des commentaires Disqus il suffit d'ajouter ce code dans le thème à l'endroit idoine :

<div id="disqus_thread"></div>
<script>
    var disqus_config = function () {
        this.page.url = "{{url absolute="true"}}";
        this.page.identifier = "ghost-{{comment_id}}"
    };
    (function() {
    var d = document, s = d.createElement('script');
    s.src = 'https://EXAMPLE.disqus.com/embed.js';
    s.setAttribute('data-timestamp', +new Date());
    (d.head || d.body).appendChild(s);
    })();
</script>

Mais si on préfère que les commentaires soient gérés par Telegram et être notifié par ce canal on configurera cette app et on collera le script au même endroit. Il existe d'autre solutions comme Cove ou mieux Commento qui lui devra être installé localement, dans un autre container Docker par exemple), mais qui présente l'avantage de l'autonomie et de ne pas partager les données des commentateurs.

Pour le reste il est bien sur possible de créer ses propres intégrations en codant un peu... La documentation et l'importante communauté via le forum seront des bonnes sources.

Epilogue

Voila, je cherchait à m'affranchir du mammouth (Wordpress que je déteste) et je crois que j'ai enfin trouvé avec Ghost la bonne solution alternative. En plus comme Ghost est multi auteurs on va peut être en faire un blog communautaire. L'exemple est ici https://ghost.canaletto.fr ! Pour l'instant j'utilise BlogEngine pour ce site et peut être qu'un jour je tenterait la migration, d'autant plus que certains l'ont fait et qu'il existe des convertisseurs.

Enjoy !

Sources

 

Home Assistant & Planification, Schedy !

l'inconvénient du Scheduler que je vous avait présenté ici c'est qu'il ne fait pas de replanification et il se contente uniquement d'une action (avec ses contraintes) en début de plage, ce qui impose de devoir programmer la plage suivante. De fait si pour une raison quelconque l'appareil ou le thermostat n'a pas reçu l'ordre, c'est raté. De même il est impossible d'imposer une contrainte supplémentaire après le départ, la notion d'humidité utile pour la clim par exemple, ou encore l'arrivée d'une personne qui ne supporte pas la clim. Pour résumer le Scheduler c'est une action avec des contraintes en début de plage uniquement, là ou Schedy va savoir replanifier dynamiquement en fonction des nouvelles contraintes pendant le déroulement d'une plage. Et ça change tout !

Je vous passe l'installation, ça sous entend app_daemon et j'en ai déjà parlé ici. L'utilisation de Schedy est très simple. Tout se passe dans un seul fichier .yaml et si vous êtes le seul occupant de la demeure ce sera parfait car il suffit d'aller changer les valeur et de sauvegarder pour que les modifications soient prises en compte dynamiquement. Ce mode dynamique est le gros plus des applications app_daemon. Je vous la fait courte car la doc est très bien faite et vous y trouverez d'autres exemples, qui d'ailleurs ne s'appliquent pas uniquement au chauffage. Voici la partie principale du fichier de configuration de Schedy :

      schedule:
      - v: 20
        rules:
        - weekdays: 1-5
          rules:
          - rules:
            - x: "Next() if heating_mode() == 'Normal' else Break()"
            - { start: "06:00", end: "07:30" }
            - { start: "15:00", end: "22:30" }
          - rules:
            - x: "Next() if heating_mode() != 'Normal' else Break()"
            - { start: "08:00", end: "23:30" }
        - weekdays: 6-7
          rules:
          - { start: "08:00", end: "23:30" }

Si vous voulez changer un horaire, un mois ou une saison, il suffit le le faire ici.

Un GUI pour Schedy...

A la base Schedy ne dispose pas d'une interface et n'a pas été développé dans ce sens. Le produit est très stable et son auteur n'a plus trop le temps de le faire évoluer, pour l'heure il se contente de la maintenance.

L'idée ici n'est pas de fournir une planification plug & play pour l'administrateur, mais que celui-ci permette à un utilisateur lambda de modifier les plages et la température de garde associée à chacune d'elles. On va gérer ici 4 plages par thermostat (matin, midi, soir et nuit), sachant que l'on pourrait juste les numéroter et en créer plus ou moins. On dispose également d'une consigne pour la température qui sera appliquée en dehors de ces plages (et qui n'a rien à voir avec le mode hors gel qui lui se gère sur le thermostat.

On va se servir du mode mode "package" de Home Assistant, ce qui va nous permettre d'avoir dans un seul fichier .yaml tous les composants nécessaires pour accompagner notre thermostat. Pour ça il vous faudra faire une petite modification dans le fichier de configuration :

homeassistant:
  packages: !include_dir_named packages

Schedy n'a pas de GUI mais dispose toutefois d'une mince ouverture pour communiquer avec Lovelace et on va ainsi pouvoir lui adjoindre une interface minimale, mais sur mesure. On aurait pu penser à ce qu'il utilise des input_datetime: pour les horaires, mais non la seule possibilité réside dans des input_number: ou sensor: au travers desquels on va faire passer les températures de consigne et les heures de début et de fin des plages. Et pour les horaires (ça m'a pris du temps à comprendre) il faut lui donner l'heure sous la forme d'un nombre en minutes écoulées depuis minuit. Donc si on veut qu'une plage débute à 02:00 il faut lui donner 120. Ca ne s'invente pas !

Il va donc nous falloir traduire les input_datetime: qui vont permettre la saisie d'un horaire (à dupliquer par le nombre de plages à gérer) :

input_datetime:
  ac_start_1:
    has_date: false
    has_time: true
  ac_stop_1:
    has_date: false
    has_time: true

En sensor: afin de ne pas avoir à saisir le nombre de minutes... (Merçi @mathieu !) (à dupliquer par le nombre de plages à gérer) :

sensor:
  platform: template
  sensors:
    ac_start_1:
      friendly_name: "AC Start 1"
      icon_template: mdi:timer-sand
      value_template: "{{ (state_attr('input_datetime.ac_start_1', 'timestamp') / 60)|int }}"
    ac_stop_1:
      friendly_name: "AC Stop 1"
      icon_template: mdi:timer-sand
      value_template: "{{ (state_attr('input_datetime.ac_stop_1', 'timestamp') / 60)|int }}"

On va également créer des input_number: pour les températures de consigne (à dupliquer par le nombre de plages à gérer, sans oublier la température hors plages) :

input_number:
  ac_max_1_temp:
    name: Ac Temp 1
    min: 14
    max: 26
    step: 0.5
    unit_of_measurement: °C

On ajoute un input_boolean: pat plage, il va nous permettre d'activer ou désactiver la plage :

input_boolean:
  ac_1:
    name: AC 1
    icon: mdi:account-check

De façon plus globale on se servira également d'un binary_sensor:  sur lequel reposera l'activation du chauffage, un autre pour la climatisation (je pense que ça peut également être des input_boolean:). Au chapitre des contraintes j'ai déjà un input_boolean: qui me sert au mode absent. Il est également possible d'utiliser workday: dans les contraintes et ainsi définir des plages qui s'exécuteront soit les jours de travail, soit le week-end. Mais on peut s'en passer et gérer ça dans Schedy, l'avantage restant à workday: qui lui gère les jours fériés.

Voici la première partie du fichier de configuration de la partie Schedy :

schedy_heating:  # This is our app instance name.
  module: hass_apps_loader
  class: SchedyApp

  actor_type: thermostat

  expression_environment: |
    def time_between(start, end):
        current = time.hour * 60 + time.minute + 1
        if start >= end:
            return current >= start or current < end
        return current >= start and current < end
  
  schedule_prepend:
  - x: "Mark(OFF, Mark.OVERLAY) if not is_empty(filter_entities('binary_sensor', state='on', window_room=room_name)) else Next()"
  - x: "OFF if is_off('binary_sensor.heating_enabled') else Next()"

  watched_entities:
  - binary_sensor.heating_enabled

La première partie  concerne la déclaration de l'application app_daemon. Ensuite le type d'utilisation et l'environnement qui va nous permettre de récupérer les informations de planification depuis Lovelace. On trouve ensuite la gestion des ouvertures (Voir plus bas en détail) et du mode OFF du chauffage. La dernière ligne concerne la déclation des entités HA que nous utilisons dans cette partie, ici le binary_sensor: du ON/OFF.

La suite concerne les pièces avec leurs thermostats associés :

rooms:
    bureau:
      rescheduling_delay: 120
      actors:
        climate.thermostat_bureau:
       
      watched_entities:
      - binary_sensor.bureau_door_delayed
      - input_number.day_temperature
      - sensor.ac_start_1
      - sensor.ac_stop_1
      - input_boolean.thermostats_away        # Mode Absent qui pourrait également être dans les paramètres globaux
      - input_boolean.ac_1
      # - binary_sensor.workday_sensor

      schedule:
      - months: 1-4
        weekdays: 1-6
        rules:
        - x: state("input_number.day_temperature") if (state("input_boolean.ac_1") == "on") and (state("binary_sensor.bureau_door_delayed") == "off") and  (state("input_boolean.thermostats_away") == "off") and time_between(int(state("sensor.ac_start_1")), int(state("sensor.ac_stop_1"))) else Next()
      - v: 19

Outre le thermostat on déclare ici les différentes entités que l'on va utiliser.

La ligne importante est la plus longue à la fin. On commence par la température, l'activation ou nom de ce planning, la gestion de l'ouverture, le mode absent qu'il me faudra déplacer dans les paramètres globaux et l'heure de début et de fin. On remarque également que je n'ai ici pas utilisé workday: mais que j'ai choisit de laisser (pour l'exemple) les jours et mois ou cette planification peut s'exécuter (on peut également utiliser la saison). La dernière ligne concerne la température de consigne qui sera appliquée en dehors des plages. Il est également possible de la gérer depuis Lovelace avec l'input_number: idoine.

Les ouvertures

Curieusement l'auteur a prévu de gérer les ouvertures, mais il n'a pas intégré la notion de délai. Ainsi de base si on ouvre une porte ou une fenêtre on coupe le convecteur immédiatement, ce qui n'a pas de sens si on ne fait que rentrer ou sortir, alors que ça en aurait si on ouvre une fenêtre pour aérer une pièce plus longuement. D'ailleurs si ça n'a pas d'impact sur un convecteur, le résultat sera bien différent pour un climatiseur pour lequel les changements d'état on souvent une grande latence. On va donc devoir jouer des avec les templates et ainsi créer des retardateurs pour les ouvertures (et non je ne pousserait pas le vice jusqu'à aller gérer les delais dans Lovelace...) (la partie icon_template: est vraiment là pour l'exemple car il n'y aucun intérêt à afficher ça dans Lovelace) :

binary_sensor:
  - platform: template
    sensors:
      garage_door_delayed:
        friendly_name: "Delayed Garage Door"
        #window_room: bedroom
        delay_on: 
          seconds: 180
        delay_off:
          seconds: 360
        value_template: >-
          {{ is_state('binary_sensor.porte_garage', 'on') }}
        icon_template: >-
          {% if is_state('binary_sensor.porte_garage', 'on') %}
            mdi:door-open
          {% else %}
            mdi:door-closed
          {% endif %}

Le froid

Toute cette partie peut s'appliquer à des convecteurs ou climatiseurs en mode chauffage. Pour le mode froid le fonctionnement est un peu différent, on va gérer un seuil de déclenchement et une température de maintient, alors qu'en dehors des plage on éteindra le climatiseur (ici dans la dernière partie).

Epilogue...

Pour cet article, qui m'a pris un peu trop de temps, je me suis bien sur inspiré de mon expérience puisée dans le documentation de Schedy et le fil de discutions du forum Home Assistant. Mais j'ai également trouvé le GitHub d'un utilisateur ou il présente sa configuration. Je vous invite à aller y puiser des idées, et vous remarquerez qu'il a choisit une présentation un peu différente pour la saisie des plages horaires à l'aide de sliders. En ce qui me concerne j'ai préféré compacter au maximum cette partie, même si toutes les fantaisies sont possibles. Pensez à revenir car j'enrichirait cet article dès que j'aurais le temps de continuer.

Replay. La suite...

J'avais un peu laissé en plan cette idée mais mon climatiseur Daikin perd parfois le WI-FI, donc la notion de replanification de Schedy prend toute son importance. Entre temps @PYG a publié un script qui simplifie grandement la création des différentes entités nécessaires, script à lancer en SSH et que l'on pourra adapter à souhait, ce que j'ai fait ici pour mon climatiseur en mode chauffe, et qu'il me faudra adapter en mode froid (on gère les pièces en ligne 10 et le nombre de périodes en 12).

/bin/bash
test -d /config/packages || mkdir /config/packages
cd /config/packages
cat >heating_global.yaml<<EOF
input_boolean:
  heating_enabled:
    name: Heating Global
    icon: mdi:toggle-switch
EOF
for room in hall_ac
do
for period in {1..4}
do
cat >${room}_heating_period_${period}.yaml<<EOF
input_datetime:
  ${room}_heating_period_${period}_start:
    name: "Heating Period ${period} Start Time"
    has_date: false
    has_time: true
  ${room}_heating_period_${period}_end:
    name: "Heating Period ${period} End Time"
    has_date: false
    has_time: true
sensor:
  platform: template
  sensors:
    ${room}_heating_period_${period}_start:
      value_template: "{{ (state_attr('input_datetime.${room}_heating_period_${period}_start', 'timestamp') / 60)|int }}"
    ${room}_heating_period_${period}_end:
      value_template: "{{ (state_attr('input_datetime.${room}_heating_period_${period}_end', 'timestamp') / 60)|int }}"
input_number:
  ${room}_heating_period_${period}_temperature:
    name: Heating Period ${period} Temperature
    min: 18
    max: 25
    step: 1
    unit_of_measurement: °C
    icon: 'mdi:thermometer-lines'
input_boolean:
  ${room}_heating_period_${period}:
    name: Heating Period ${period} Enabled
    icon: mdi:toggle-switch
EOF
done
done
exit

Ensuite j'ai un peu modifié sa config Schedy afin de l'adapter à mon besoin...

schedy_heating:
  module: hass_apps_loader
  class: SchedyApp
  
  actor_type: thermostat
  
  actor_templates:
    default:
      send_retry_interval: 30
      send_retries: 10
      supports_hvac_modes: true
      off_temp: 18
  
  watched_entities:
  - input_boolean.homeoffice
  - binary_sensor.workday_sensor
  # - binary_sensor.holiday_sensor
  - input_boolean.heating_enabled
  
  expression_environment: |
    def homeoffice():
      return is_on("input_boolean.homeoffice")
    def workday():
      return is_on("binary_sensor.workday_sensor")
    # def holiday():
      # return is_on("binary_sensor.holiday_sensor")
    def time_between(start, end):
        start = int(state(start))
        end = int(state(end))
        current = time.hour * 60 + time.minute + 1
        if start >= end:
            return current >= start or current < end
        return current >= start and current < end

  schedule_prepend:
  - x: "14 if is_off('input_boolean.heating_enabled') else Next()"
  
  rooms:
    hall_ac:
      allow_manual_changes: true
      rescheduling_delay: 1
      actors:
        climate.daikin:
          template: default
      watched_entities:
      - input_number.hall_ac_heating_period_1_temperature
      - input_boolean.hall_ac_heating_period_1
      - sensor.hall_ac_heating_period_1_start
      - sensor.hall_ac_heating_period_1_end
      - input_number.hall_ac_heating_period_2_temperature
      - input_boolean.hall_ac_heating_period_2
      - sensor.hall_ac_heating_period_2_start
      - sensor.hall_ac_heating_period_2_end
      - input_number.hall_ac_heating_period_3_temperature
      - input_boolean.hall_ac_heating_period_3
      - sensor.hall_ac_heating_period_3_start
      - sensor.hall_ac_heating_period_3_end
      - input_number.hall_ac_heating_period_4_temperature
      - input_boolean.hall_ac_heating_period_4
      - sensor.hall_ac_heating_period_4_start
      - sensor.hall_ac_heating_period_4_end
      schedule:
      - rules:
        # not workday
        - rules:
          - x: "Break() if workday() else Next()"
          - x: >
              state("input_number.hall_ac_heating_period_1_temperature")
              if (is_on("input_boolean.hall_ac_heating_period_1")
              and time_between("sensor.hall_ac_heating_period_1_start", "sensor.hall_ac_heating_period_1_end"))
              else Next()
          - x: >
              state("input_number.hall_ac_heating_period_2_temperature")
              if (is_on("input_boolean.hall_ac_heating_period_2")
              and time_between("sensor.hall_ac_heating_period_2_start", "sensor.hall_ac_heating_period_2_end"))
              else Next()
          - x: "Break(2)"
        # workday
        - rules:
          - x: >
              state("input_number.hall_ac_heating_period_3_temperature")
              if (is_on("input_boolean.hall_ac_heating_period_3")
              and time_between("sensor.hall_ac_heating_period_3_start", "sensor.hall_ac_heating_period_3_end"))
              else Next()
          - x: >
              state("input_number.hall_ac_heating_period_4_temperature")
              if (is_on("input_boolean.hall_ac_heating_period_4")
              and time_between("sensor.hall_ac_heating_period_4_start", "sensor.hall_ac_heating_period_4_end"))
              else Next()
          - x: "Break(2)"
      # default
      - v: 18

Et j'ai créé la carte Lovelace qui va avec...

type: grid
square: true
cards:
  - type: vertical-stack
    cards:
      - type: entities
        entities:
          - entity: binary_sensor.heating_enabled
            name: Etat du chauffage
          - entity: input_boolean.heating_enabled
            name: Activation du chauffage
          - entity: binary_sensor.workday_sensor
            name: Jour de semaine
        theme: teal
        title: AC Daikin
      - type: entities
        entities:
          - entity: input_boolean.hall_ac_heating_period_1
          - entity: input_datetime.hall_ac_heating_period_1_start
          - entity: input_datetime.hall_ac_heating_period_1_end
          - entity: input_number.hall_ac_heating_period_1_temperature
          - entity: input_boolean.hall_ac_heating_period_2
          - entity: input_datetime.hall_ac_heating_period_2_start
          - entity: input_datetime.hall_ac_heating_period_2_end
          - entity: input_number.hall_ac_heating_period_2_temperature
        title: Week-End & Jours fériés
        show_header_toggle: false
        theme: teal
        state_color: true
      - type: entities
        entities:
          - entity: input_boolean.hall_ac_heating_period_3
          - entity: input_datetime.hall_ac_heating_period_3_start
          - entity: input_datetime.hall_ac_heating_period_3_end
          - entity: input_number.hall_ac_heating_period_3_temperature
          - entity: input_boolean.hall_ac_heating_period_4
          - entity: input_datetime.hall_ac_heating_period_4_start
          - entity: input_datetime.hall_ac_heating_period_4_end
          - entity: input_number.hall_ac_heating_period_4_temperature
        title: Semaine
        show_header_toggle: false
        theme: teal
        state_color: true
columns: 1

Et voilà !

 

 
 

 

 

Windows Terminal & SSH

Après nous avoir vanté pendant des décennies l'avantage d'un O/S Windows ou tout se passe en mode clic clic, force est de constater ces dernières années que le CLI revient en force, et ce notamment avec PowerShell. Et puis ces dernières années Microsoft nous a pondu Windows Terminal (ou mieux celle-ci) qui supporte bien sur PowerShell, Dos, mais également le client SSH maintenant intégré (timidement) à Windows.

Et c'est celui ci qui m'a intéressé pour remplacer mes anciens clients SSH (j'utilisait Putty ou Bitvise SSH) avec des mots de passe. Sauf que ce client SSH calqué sur ceux que l'on trouve sur Linux (OpenSSH) ne permet bien sur pas de stocker les mots de passe. Et c'est normal. Il va donc falloir apprendre à générer et utiliser des paires de clés SSH, ce qui avec ma culture Windows n'a pas été une mince affaire.

Si ce n'est pas fait on installe le client SSH après avoir installé Windows Terminal (le client SSH peut aussi fonctionner seul, et tout ce qui se rapporte ici à SSH n'est bien sur pas lié à Windows...).

PS C:\> Add-WindowsCapability -Online -Name OpenSSH.Client*

On va donc commencer par générer une paire de clés. Idéalement on se place dans le répertoire .ssh qui se trouve dans le répertoire utilisateur. Cela nous évitera d'avoir à saisir un chemin, car au final les clés devront se trouver dans se répertoire pour simplifier la suite. Il est possible de protéger les clés par une phrase de passe, ou pas.

PS C:\Users\Lionel> cd .ssh
PS C:\Users\Lionel\.ssh> ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (C:\Users\Lionel/.ssh/id_rsa): test
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in test.
Your public key has been saved in test.pub.
The key fingerprint is:
SHA256:vAwnkX2iNyxyqFBVnNVHpzPbdvy6g1VoE/RU8o7+hJ0 [email protected]
The key's randomart image is:
+---[RSA 2048]----+
|    .o.o.. ...+ +|
|   .  oo  . .o.= |
|  .   o o ..+  oo|
| .   . = o   =++.|
|.   o * S   ..+o+|
| . . o B o   o.+o|
|  .     o    ooE+|
|            . .+ |
|              oo.|
+----[SHA256]-----+

On dispose maintenant de notre paire de clés, test qui est la clé privée et test.pub qui est la clé publique. Si je ne l'avais pas nommée ainsi on se serait retrouvé avec id_rsa et id_rsa.pub qui est le nom de la clé par défaut. Peu importe le nommage, ce qui compte c'est de bien identifier ces deux fichiers.

A partir de là il va falloir installer la clé publique sur le serveur de destination.

A ce stade il est donc nécessaire que le service SSH soit activé sur le serveur distant avec la possibilité de se connecter avec un mot de passe, voire en root (je sais c'est pas bien). Pour cela on édite le fichier qui va bien avec sudo nano /etc/ssh/sshd_config et on redémare le service :

On remplace PermitRootLogin prohibit-password par PermitRootLogin yes 
On remplace PasswordAuthentication no par PasswordAuthentication yes

A noter que quand on a installé une surcouche comme aaPanel, celui ci se charge du SSH au dessus de l'O/S, et on peut télécharger la clé privée à utiliser coté client directement depuis l'interface, et le cas échéant désactiver l'authentification par mot de passe. Inconvénient, pour l'instant aaPanel ne gère que le root qu'il convient donc de sécuriser.

Et c'est là que ça se complique, car si sous Linux il existe le petit outil idoine pour faire ça :

ssh-copy-id [email protected]_host

Rien de tel sous Windows et il va falloir faire avec ce sont on dispose, donc le client SSH et voici la méthode que j'ai trouvée pour installer la clé publique sur le serveur :

PS C:\> cat ~/.ssh/id_rsa.pub | ssh [email protected] "mkdir -p ~/.ssh && touch ~/.ssh/authorized_keys && chmod -R go= ~/.ssh && cat >> ~/.ssh/authorized_keys"

Une confirmation sera demandée (pour ajouter ce nouveau host à la liste locale) ainsi que le mot de passe SSH du serveur distant. 

A ce stade il est possible de se connecter avec notre clé et il sera possible de désactiver (sans se précipiter) l'authentification par mot de passe en éditant le fichier sshd_config comme vu précédemment.

PS C:\> ssh [email protected]

Et si j'ai plusieurs clés destinées à plusieurs serveurs ?

C'est possible, de deux façons, la première en cli :

PS C:\> ssh -i ~/.ssh/ma_cle_perso [email protected]

Ou plus simplement :

PS C:\> ssh MonServeur

A condition d'avoir créé un fichier config dans /.ssh

Host MonServeur
  User root
  HostName 192.168.20.20
    Port 22
  IdentityFile ~/.ssh/ma_cle_perso

Intégration au menu Windows Terminal

Windows Terminal dispose d'un menu facilement paramétrable auquel on va pouvoir ajouter des raccourcis vers nos serveurs favoris. En cliquant sur paramètres on va directement ouvrir le fichier de paramètres et ajouter une entrée correspondant à un nouveau serveur SSH distant (attention le GUID doit être unique et on peut en générer sur plusieurs sites comme ici).

   {
     "guid":  "{232b7eac-3f44-4560-9250-eee6e97ca4e3}", // Random GUID
       "hidden":  false,
     "name":  "MonServeur",
  "icon":  "c:/users/lionel/pictures/terminal/ps.png", // facultatif
     "commandline":  "ssh 192.168.20.20"
   },

Ainsi il sera très facile d'accéder à un serveur. Dans ce fichier il est également possible de modifier tous les paramètres, mais pour ça je vous laisse lire les mes sources ci-dessous. Et comme il y a du Windows dans l'air, certains n'ont pas pu résister à ajouter des icones dans le terminal...

C'est bien sur anecdotique, mais je suis sur que ça va en amuser certains, voici la marche à suivre :

  1. On télécharge ces polices et on installe la police Caskaydia*
  2. Dans PowerShell on exécute Install-Module Terminal-Icons -Scope CurrentUser
  3. Toujours dans PS : code $profile, et on ajoute cette ligne Import-Module Terminal-Icons
  4. Avec RegEdit : Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Console\TrueTypeFont: 000=CaskaydiaCove Nerd Font
  5. Dans les paramètres de Windows Terminal on ajoute "fontFace": "CaskaydiaCove Nerd Font" dans profiles > defaults.

Bonus

Si vous êtes vraiment allergiques à Microsoft (que faites vous sous Windows), il y a une très bonne alternative, Fluent Terminal.

Voilà ! Je n'ai rien inventé et cet article a surtout pour but de me permettre de mémoriser tout ça. Et si ça peut vous aider.... Et si d'aucune ont quelque chose à y ajouter, n'hésitez pas !

Enjoy ;-)

Sources

 

Gérer son serveur Linux avec aaPanel

Comme beaucoup ici le savent à la base je n'ai pas une culture Linux/Unix mais plutôt Windows depuis que j'ai reçu une pile de disquettes Windows 1.03. Et très très longtemps j'ai cru que c'était le graal au point de ne pas trop regarder ce qu'il se passait à coté. Hors si je reste encore convaincu que Linux a peu de chances coté desktop (j'essaie régulièrement), Linux s'est au fil des années affirmé coté serveur, malgré le combat acharné des lobbyistes de Redmond pour laisser penser le contraire et nous laisser croire qu'ils soutiennent l'open source. On ne va pas relancer le débat, ce n'est pas l'objet.

Du coup sous Linux bien souvent je tâtonne pour des choses qui sont pourtant simples. Au fil du temps je suis devenu le roi du copié / collé SSH, tout en essayant de comprendre, ce qui n'est pas toujours évident (au passage je vous conseille vivement Windows Terminal (initiative de quelques convaincus chez MS) qui pour moi est le meilleur client SSH/PS sous Windows).

Bref, pour la faire courte je trouve que Linux manque d'interface. Je ne parle pas d'interface graphique, ce qui alourdirait pour rien un serveur, mais d'interface pour installer et superviser. Alors bien sur il y a cPanel que l'on retrouve chez certains loueurs de VPS, mais d'une part ce n'est pas gratuit et d'autre part pas le plus simple à installer sur un petit serveur isolé.

Et puis un jour j'ai découvert aaPanel. aaPanel c'est une sorte de cPanel minimaliste gratuit et open source, mais qui va permettre de faire en quelques clics de souris les choses essentielles sur un serveur Linux (installer un serveur web, un Wordpress, un FTP, cron, surveillance, etc...). Pour installer aaPanel, une seule ligne de commande suffit et je vous recommande de le faire dès le début sur une Install Linux clean ou rien n'a encore été déployé. Ca prend quelques minutes et vous trouverez plus de détails, notament pour d'autres distributions ici.

Ubuntu :

wget -O install.sh http://www.aapanel.com/script/install-ubuntu_6.0_en.sh && sudo bash install.sh aapanel

Debian :

wget -O install.sh http://www.aapanel.com/script/install-ubuntu_6.0_en.sh && bash install.sh aapanel

A partir de là ça va mouliner un peu et le système vous donnera les informations de connexion (à changer), et la suite se passera bien sur dans votre navigateur préféré :

Je vous laisse explorer l'interface qui est plutôt intuitive. Je vais juste prendre un exemple. Imaginons que je veuille installer un Wordpress. Il me suffit d'aller dans l'App Store et de choisir One Click Deployement et de sélectionner Wordpress et aaPanel fera le reste après avoir choisit quelques options de personnalisation :

Ensuite il me sera conseillé de créer une destination de sauvegarde (GDrive, S3, FTP...) et de faire un cron de sauvegarde. De même qu'en deux clics je vais pouvoir gérer mes certificats Let's Encrypt. Bien sur il est possible d'héberger  et de gérer plusieurs service sur une même instance aaPanel, que ce soit en direct, voire dans des container Docker. C'est donc la solution idéale à installer sur un VPS...  aaPanel est une solution qui s'enrichit de semaines en semaines. Et si vous voulez poser des questions, il y a bien sur un forum dédié.

Vous allez me dire que ce n'est pas avec ça que je vais apprendre à tout faire en CLI ! Ce n'est pas mon but, et tout ce qui peut me faire gagner du temps, je prends. Et surtout ça va me permettre d'être moins dépendant et ainsi de faire facilement des choses que j'étais obligé de confier à des collègues, tout en comprenant mieux ce que je met en œuvre. Enjoy !

 

 

Home Assistant, restauration d'état après une coupure secteur

Comme d'aucuns ont pu le remarquer, les ampoules connectées s'allument après une coupure secteur (Mi, Hue, Ikea, Tuya, etc...)., c'est logique car si on les actionne depuis un inter d'origine il faut bien que l'ampoule répondre en dehors de tout contexte domotique, bien que certaines, Shelly par exemple, permettent comme les prises commandées et modules de choisir cet état.

Pour palier à ce problème et ne pas retrouver toutes mes ampoules allumées quand je rentre à cause d'une coupure ayant eu lieu dans la journée, j'ai fait un groupe d'ampoules (dans light.yaml) à éteindre après que l'onduleur ait signalé la reprise électrique (ça sous entend bien sur d'avoir configuré un addon sur l'onduleur afin d'avoir le binay_sensor: idoine).

- platform: group
  name: Groupe de Coupure
  entities:
    - light.tuya_cour_1
    - light.tuya_cour_2
    - light.tuya_chevet
    - light.tuya_strip_antoine
    - light.hall_1
    - light.hall_2
    - light.cuisine_led
    - light.sejour_led
    - light.cuisine_lampe_de_table
    - light.bureau_led_bt
    - light.ikea_r14_entree
    - light.ikea_e27_tv

Et une petite automation :

- alias: P - Light OFF après reprise électrique
  description: ''
  trigger:
  - platform: state
    entity_id: binary_sensor.ups
    from: 'off'
    to: 'on'
  condition: []
  action:
  - service: light.turn_off
    data: {}
    entity_id: light.groupe_de_coupure

Ca fonctionne mais ce n'est pas satisfaisant car ça éteint toutes les ampoules, et pas seulement celles qui étaient éteintes avant la coupure. J'ai commencé par me dire qu'idéalement il faudrait connaitre leur état avant la coupure, le stocker dans des input quelque chose et faire une automation de malade à exécuter lors du rétablissement électrique. Ca m'a donné mal au crane et j'ai procrastiné la chose...

Et puis, en discutant dans notre groupe préféré et confidentiel, la lumière jaillit ! Il y a une fonctionnalité souvent peu utilisée dans Home Assistant, ce sont les scènes. En général on crée une scène, genre j'allume des ampoules à telle ou telle intensité ou couleur, je baisse les stores, et je lance un film sur Netflix... Mais, si on lit la doc jusqu'au bout on découvrira que l'on peut également créer des scènes à la volée, un peu comme un snapshoot de l'état de certaines entités.

Et là ça devient très simple. Si une ampoule n'est plus alimentée, elle ne signale pas son état à Home Assistant. Il suffit alors d'enregistrer son état dans une scène dès lors que le binary de l'onduleur passe à OFF :

- alias: P - Sauve l'état des lampes lors d'une coupure
  trigger:
  - platform: state
    entity_id: binary_sensor.ups
    from: 'on'
    to: 'off'
  condition: []
  action:
  - service: scene.create
    data:
      scene_id: light_state
      snapshot_entities:
      - light.tuya_cour_1
      - light.tuya_cour_2
      - light.tuya_chevet
      - light.tuya_strip_antoine
      - light.hall_1
      - light.hall_2
      - light.cuisine_led
      - light.sejour_led
      - light.cuisine_lampe_de_table
      - light.bureau_led_bt
      - light.ikea_r14_entree
      - light.ikea_e27_tv

Et de restaurer la scène quand celui ci passe à ON :

- alias: P - Restaure l'état des lampes lors d'une coupure
  trigger:
  - platform: state
    entity_id: binary_sensor.ups
    from: 'off'
    to: 'on'
  condition: []
  action:
  - delay : '00:00:60' # On ajoute une temporisation afin que les ampoules se reconnectent
  - service: scene.turn_on
    data:
      entity_id: scene.light_state

Et on retrouve l'état de l'éclairage avant qu'Enedis ait sévit. Car si les coupures électriques sont peu fréquentes en ville, à la campagne le moindre aléa climatique en provoque, ce qui en dit long sur l'état du réseau. 

J'ai pris ici l'exemple de l'éclairage lors d'une coupure secteur. Mais il est tout à fait possible de restaurer ainsi l'état d'autres entités, mais également de s'en servir dans d'autres contextes, par exemple sauvegarder un état d'éclairage avant de fermer les volets ou de regarder un film, et de le restaurer ensuite. Avec Emby (et surement d'autres triggers) on peu par exemple jouer deux états selon que l'on appuie sur PLAY ou PAUSE... La suite n'étant qu'une question d'imagination ;-)

 

 

Home Assistant & Planification, la suite !

Ce qui déroute souvent quand on débute avec Home Assistant c'est l'absence de planificateur intégré, alors même que la chose parrait évidente. Il y a tout de même plusieurs façons de planifier des évènements et on a déjà examiné ici plusieurs solutions :

  • En codant des des automation basées sur des input_time, etc... On peut obtenir quelque chose de très personnalisé. Mais il faut pas mal d'huile de coude.
  • Avec des agendas externes comme Google Agenda ou Microsoft 365, ou encore des agendas plus geeks... (ics, etc).

Il y a une autre solution qui bien que dépourvue d'interface est vraiment très puissante, mais un peu compliquée à appréhender. Je veux parler de Shedy. Ca impose d'installer AppDaemon et ensuite ça se passe en YAML, à ceci près que sous AppDaemon la prise en compte des modifications apportées au code YAML est dynamique, et ça c'est très sympa.  AppDaemon c'est un addon qui s'installe dans la section addon de HA et je m'en sert également pour ControlerX. Dans toutes ces solutions, seul Shedy sait faire de la replanification dynamique, c-a-d que toutes les x minutes il reconsidère l'état de tous les paramètres, là ou les autres se contentent de faire ON au début et OFF à la fin (ou augmenter la consigne et la baisser).

Depuis peu il y a aussi les BluePrint, ça va changer beaucoup de choses sous HA et surement permettre de proposer des planifications simples et prêtes à l'emploi.

On a peut être aussi des possibilités avec NodeRed, mais pour moi c'est non, vous le savez, je n'aime pas. Bon, blague à part, c'est bien et même intéressant, surtout pour ceux qui ne réussissent pas à entrer dans la logique yaml, ce qui se comprends très bien. Mais dans l'absolu ça n'a pas besoin de HA, à part pour l'interface, ah oui NR n'a pas d'interface utilisateur, juste une interface dev. Bon, après chacun son truc.

Et puis il y a le Scheduler dont je veux vous parler aujourd'hui et pour lequel je vous conseille de suivre cette conversation.

L'idée de son auteur est de créer un scheduler entièrement graphique, donc à l'opposé de Schedy. A vrai dire j'aurais préféré qu'il fasse une interface pour Schedy. Mais les développeurs sont ce qu'ils sont et lui est parti sur deux modules, un composant moteur et une carte d'interface. Du coup si l'interface est très réussie il reste encore des choses à voir coté motorisation pour arriver au niveau de Schedy, qui lui gère notamment la notion de re planification dynamique. Attention, ce n'est pas un reproche, Niels a fait un travail remarquable en très peu de temps, et comme c'est un perfectionniste, je ne doute pas qu'au fil des semaines cet outil, déjà totalement utilisable, gagnera en fonctionnalités.

Je vous passe les détails de l'installations décrits ici, et , en gros on installe le composant depuis HACS, on redémarre et on installe la carte. Ensuite on va créer une carte à laquelle on attribuera des objets à planifier avec des conditions. Par exemple, je veux que le thermostat de la cuisine passe à 21° tous les matins à 07:15 et qu'il repasse à 19.5° à 09:00 (avec plusieurs plages possibles dans la journée), mais à condition que nous soyons en semaine, que le commutateur qui autorise le chauffage soit à ON et que je ne soit pas en voyage, etc...

On va donc créer une carte avec les objets dont on a besoin que l'on peut choisir par type ou mieux individuellement. Par exemple un thermostat, et les input_bolean: Chauffage ON/OFF et Mode Absent ON/OFF : 

Je prends l'exemple des thermostats qui me préoccupait, mais on peut bien sur planifier tout ce qui est actionnable, directement, via des scripts ou des scènes.

Il est bien sur possible de faire ça par le code, ce qui permet en plus d'ajouter des options de présentation de la carte. Personnellement j'ai choisit d'utiliser une carte de type "Vertical Stack" "Panel Mode", ce qui sera bien plus agréable à utiliser pour planifier une journée. Ce mode n'est pas très adapté au mobile, mais c'est le genre de choses que je fais depuis mon PC.

type: vertical-stack
cards:
  - type: 'custom:scheduler-card'
    discover_existing: false
    display_options:
      primary_info:
        - '<b>{entity} / {name}</b>'
      secondary_info:
        - >-
          <b>Next</b> : {relative-time} ({days} {time}) | <b>Action</b> :
          {action} ({additional-tasks})
      icon: 'mdi:radiator'
    include:
      - climate.daikin
      - input_boolean.thermostats_ac_on_off
      - input_boolean.thermostats_away
      - input_boolean.thermostats_on_off
      - input_select.chauffage
    title: AC Daikin
  - type: 'custom:scheduler-card'
    discover_existing: false
    display_options:
      primary_info:
        - '<b>{entity} / {name}</b>'
        - '<b>Next</b> : {relative-time} | <b>Action</b> : {action}'
        - additional-tasks
      secondary_info:
        - '{days} {time}'
      icon: 'mdi:radiator'
    include:
      - climate.thermostat_lionel
      - input_boolean.thermostats_ac_on_off
      - input_boolean.thermostats_away
      - input_boolean.thermostats_on_off
      - input_select.chauffage
    title: Thermostat Chambre Lionel

J'utilise ici une carte de la pile pour chaque thermostat et dans chaque carte je vais pouvoir ajoute plusieurs planifications. Par exemple une pour la semaine et une pour les week-end et jours fériés. A noter que workday: est supporté, ce qui simplifie grandement la gestion des jours férié. A droite de chaque planification on a un interrupteur qui permet d'activer ou désactiver la planification, fonction également possible par une automation. On peut également la laisser activée et la soumettre à une condition pour peu qu'on ait associé l'objet idoine, un input_bolean: par exemple...

Quand on ajoute une entrée on choisit l'objet sur lequel on peut agir, ici un thermostat, on précise l'entité et le type d'action unique à exécuter (allumer, éteindre plus adapté à d'autres équipements, ou mieux de faire un schéma, ce qui sera pleinement adapté à un thermostat :

Par exemple ici j'ai choisit de chauffer le séjour, le week-end, de 08:00 à 22:30 à 21.5°. Sauf que si on laisse la chose en l'état à 22:30 le thermostat continuera à chauffer à 21.5°. La logique actuelle veut que l'on crée une plage suivante avec une autre température, mais également une précédente, contrairement à Schedy qui lui va considérer une température de base (Eco) et uniquement augmenter les plages définies pour revenir ensuite à sa température de base.

Le développeur me répond qu'il ne veut pas faire un planificateur trop spécifique au chauffage. Mais je pense l'avoir convaincu de changer sa logique, et pour chaque plage définie ainsi prévoir une action de début et une action de fin, ce qui me paraitrait un bon début.

Il n'y a pas ici d'options de replanification dynamique. Si Home Assistant redémarre, le thermostat conservera son état (à condition de ne pas utiliser l'option initial_hvac_mode:), par contre face à d'éventuelles coupures secteur, il faut utiliser des actionneurs capables de retrouver leur état avant coupure et de les configurer ainsi. C'est les cas des Shelly ou des cartes IPX800, mais certains actionneurs resteront muets après coupure et ne sont donc pas conseillés pour cette utilisation.

Avec le bouton Options en bas à droite on va pouvoir ajouter des conditions supplémentaires basées sur l'état des entités que l'on aura choisies, ici un input_bolean: qui conditionne le fonctionnement du chauffage. Donc si cette condition n'est pas remplie la planification ne s'exécutera pas, la validation se faisant en début de planification. 

J'ai par exemple un input_bolean: qui conditionne le chauffage des chambres de ma fille et mon fils qui vivent à l'autre bout de la France. Ce commutateur s'actionne soit manuellement, soit en en fonction de leur date d'arrivée que j'aurais saisie (et peut être un jour automatisée en fonction des billets TGV reçus... ou en fonction de leur localisation...).

Voilà pout un petit tour rapide. Alors vous allez me demander pourquoi utiliser une solution plutôt qu'une autre ? Il y a plusieurs questions à se poser, la plus évidente étant de savoir si vous êtes le seul à agir ou si vous voulez un système plus WAF accessible à tous les membre de la maison.

Par exemple j'ai monté un Home Assistant chez mon frère, ce n'est pas un geek et je n'avais pas l'intention d'intervenir chaque fois qu'il souhaite ajuter ses plages de températures. Au départ j'avais basé ses planifications sur Google Agenda, une solution qui fonctionne très bien, mais pas idéale coté expérience utilisateur, en effet cela nécessitait deux interfaces, celle de HA et celle de Google Agenda. De plus les jours fériés n'étaient pas pris en compte (il aurait fallut faire des automations plus complexes). J'ai donc remplacé tout ça par ce Scheduler et ça lui va très bien.

En ce qui me concerne j'ai le nez dans le code et j'avais dès le début fait pas mal d'automations pour gérer les différentes situations (présent, absent, j'ai du monde à diner, à coucher, des amis passent le week-end ici, etc...) et pour l'instant ça reste ainsi. Mais mon installation est également un labo et j'ai installé Schedy pour le tester en live, notamment au niveau des conditions et situations particulières. J'aimerais aussi pouvoir le personnaliser avec un minium de GUI. Je ne suis pas un bon exemple à suivre...

En conclusion je dirais que ce Scheduler est vraiment ce que tout le monde attendait et qu'il couvrira la majorité des besoins. Après il y avait un vague projet de scheduler intégré au core, mais pour l'heure ça reste une arlésienne.

 

Home Assistant & Tuya zéro cloud

On le sait tous, la domotique nous fait acheter beaucoup de bricoles qui souvent deviennent obsolètes, bref il y a toujours mieux. Par exemple les prises et autres objets Tuya ou Sonoff qui nécessitent le cloud, alors qu'un prise Shelly dispose de base de MQTT, peut être flashée avec ce que l'on souhaite et dans tous les cas fonctionne en local. Bien sur on ne va pas parler des prises en RF433 genre DIO mise au rebus faute de retour d'état, pas plus que des solutions Zigbee qui se traitent autrement. Tuya c'est du matériel OEM disponibles en marque blanche que des entreprises peuvent distribuer sous leur étiquette. Je trouve d'ailleurs scandaleux qu'une marques comme Konyks soit labelisée French Tech alors qu'il se contente de distribuer de produits chinois sous leurs marque sans aucune R&D... Bref !

Nous avons donc en Wi-Fi...

  • L'écosystème Sonoff : en dehors de quelques modèles DIY, il faudra en général ouvrir et souder pour flasher. Heureusement il existe une intégration qui permet leur utilisation en local / cloud. En gros l'intégration mets à jour à intervalles réguliers ce qu'il trouve sur le cloud Sonoff et les actions sont ensuite locales.
  • L'écosystème Tuya : de base 100% cloud et géré nativement par HA, mais on va voir plus loin comment contourner ça.
  • L'écosystème Shelly : Cloud ou 100% nativement local et MQTT, flashable si vous aimez ça. C'est clairement ce que je vous conseille pour de nouveaux achats.

Tuya sans cloud

Heureusement il y a des petits malins qui adorent faire du reverse engineering, ce qui va nous permettre d'utiliser nos produits Tuya localement avec bien souvent plus de fonctionnalités, comme par exemple la remontée des information de consommation qui ne se fait pas de base sous HA.

Dans tous les cas, comme pour les produits Xiaomi/Aqara, il faudra trouver la clé... Au fil du temps les méthodes changent, mais aujourd'hui ça n'a jamais été aussi facile. Je vous conseille donc de récupérer vos clés, même si vous ne comptez pas les utiliser tout de suite.

On part du principe que vous avez un compte Tuya/ SmartLife (ou une application OEM comme celle de Konyks, c'est la même chose) et que vos équipements y sont enregistrés. En substance on va passer par la plateforme IoT de Tuya sur lequel vous allez vous créer un compte développeur et suivre les instructions de cet article dont je me suis inspiré. (Je ne suis pas sur que tout soit utile, vous me direz...).

  1. Créez un nouveau compte sur iot.tuya.com et assurez-vous que vous êtes connecté. Allez dans Cloud -> Projet dans le tiroir de navigation de gauche et cliquez sur "Create". Après avoir créé un nouveau projet, cliquez dessus. L'ID d'accès et la clé d'accès sont équivalents à la clé d'API et aux valeurs secrètes d'API requises.
  2. Allez dans App -> App SDK -> Development dans le tiroir de navigation. Cliquez sur "Create" et entrez ce que vous voulez pour les noms de package et Channel ID (pour le nom du package Android, vous devez entrer une chaîne commençant par com.). Prenez note de l' ID de chaîne que vous avez entré. Cela équivaut à la valeur schema requise plus loin. Ignorez les clés d'application et les valeurs secrètes d'application que vous voyez dans cette section car elles ne sont pas utilisées.
  3. Allez dans Cloud -> Projet et cliquez sur le projet que vous avez créé précédemment. Cliquez ensuite sur "Link Device". Cliquez sur l'onglet «Link devices by Apps», puis sur «Add Apps». Vérifiez l'application que vous venez de créer et cliquez sur "Ok".
  4. Toujours dans Cloud -> Projet et cliquez sur le projet que vous avez créé précédemment. Cliquez ensuite sur "Link Device". Cliquez sur l'onglet «Link devices by App Account», puis Add App Account et ajouter votre compte Tuya/SmartLife et vous devriez retrouver vos petits....
  5. Sur la même page, cliquez sur "Groupe d'API" sur le côté gauche. Modifiez le statut en Ouvert pour les trois groupes d'API suivants en cliquant sur "Appliquer" pour chaque ligne, en saisissant une raison quelconque et en cliquant sur "OK": "Authorization Management", "Device Management", "Device Control", "User Management", "Network Management", "Data Service", "Home Management", "Device User Mangement" and "Device Statistics".. Cela peut prendre 10 à 15 minutes pour que ces modifications prennent effet.
  6. Ensuite sur Device List / App Account / Europe vous listez vos devices et vous récupérez les devices ID
  7. Enfin on va dans API Explorer / Get Device details, on choisit Europe et on rentre le device ID et par magie la local_key apparaitra.
{
  "result": {
    "active_time": 1609598200,
    "biz_type": 0,
    "category": "cz",
    "create_time": 1609598200,
    "icon": "smart/icon/154028815822y4yx2k5jz_0.jpg",
    "id": "871720fsdfsdfsfd8e997fec",
    "ip": "70.22.16.3",
    "local_key": "8dfsdfsdfsgggsg10ecb6",
    "name": "Konyks Priska Plus 6",
    "online": true,
    "owner_id": "19956557",
    "product_id": "j6cVsdgfsdfgsdfsdfeYpli",
    "product_name": "Konyks Priska Plus",
    "status": [
      {
        "code": "switch_1",
        "value": false
      },
      {
        "code": "countdown_1",
        "value": 0
      },
      {
        "code": "cur_current",
        "value": 0
      },
      {
        "code": "cur_power",
        "value": 0
      },
      {
        "code": "cur_voltage",
        "value": 2339
      }
    ],
    "sub": false,
    "time_zone": "+01:00",
    "uid": "eu15345fgsdfsdfdsf9crK3G",
    "update_time": 1609565564,
    "uuid": "8717244465sd6f46sdf7fec"
  },
  "success": true,
  "t": 1609606041296
}

Ceux qui sont familiers de Node JS pourront lancer le script comme décrit ici et obtenir toute les clés en une seule manipulation.

Ensuite on a deux solutions :

  • La première, Trade Face Tuya Gateway est indépendante de la solution domotique choisie car full MQTT, elle peut s'installer dans un Docker et nos amis encore sous Jeedom pourront ainsi en profiter.
  • La seconde, Local Tuya, est une simple intégration pour Home Assistant qui permettra de reconnaitre nos équipements en quelques clics et s'installe depuis HACS. Le discovery fera le reste et il faudra juste tâtonner un peu quand on configure un équipement Tuya pour faire correspondre les bonnes valeurs, mais c'est expliqué ici.

Vous aurez compris, ce qui compte encore une fois c'est d'avoir les clés. Ensuite il existe surement d'autre solutions pour exploiter. Attention : Pour toutes ces manips pensez à fermer l'application mobile (Tuya/SmartLife) car un équipement Tuya ne supporte qu'une seule connexion simultanée.

Merci Yvon pour cette exploration nocturne et conjointe d'un jour de l'an sous couvre-feu !

EDIT 04/01/2021 : Il y a un soucis dans le v3.2 ou on perd certains équipement au reboot. Revenir à la 3.1 résout le problème en attendant mieux.

EDIT 21/02/2021 : Contrairement aux Shelly les appareils Tuya ne fournissent que la consommation instantanée. Donc pas exploitable directement avec un utility_meter: par exemple. Il va donc falloir ruser un peu :

  1. Sortir la consommation instantanée (j'ai laissé le reste en exemple) disponible en attribut pour en faire un sensor :
    sensor:
      - platform: template
        sensors:
          # tuya-sw01_voltage:
          #   value_template: >-
          #     {{ states.switch.sw01.attributes.voltage }}
          #   unit_of_measurement: 'V'
          # tuya-sw01_current:
          #   value_template: >-
          #     {{ states.switch.sw01.attributes.current }}
          #   unit_of_measurement: 'mA'
          tuya_plug_1_current_consumption:
            value_template: >-
              {{ states.switch.tuya_plug_1.attributes.current_consumption }}
            unit_of_measurement: 'W'
  2. Utiliser l'intégration Riemann pour convertir la la consommation instantanée (en Watts) en consommation cumulée (en kW/h)
    sensor:
      - platform: integration
        source: sensor.tuya_plug_1_current_consumption
        name: 'Tuya Plug 1 : Cumul'
        unit_prefix: k
        round: 2
        method: left
  3. Si l'on souhaite conserver l'historique (ici année et année précédente) on va utiliser un utility_meter:
    utility_meter:
      tuya_plug_1_energy_yearly:
        source: sensor.tuya_plug_1_cumul
        cycle: yearly


Enjoy ;-)

 

 

 
 

 

 

Home Assistant & Notifications d'état

Avant, je planifiait mon chauffage avec des automations qui s'appuyaient sur des input_date, input_time pour actionner et input_number pour sélectionner la température de consigne souhaitée et je notifiait dans un journal sur Slack, ce qui me permet un debug facile. J'en avait largement parlé ici, mais maintenant il existe un vrai scheduler et je vais changer ma façon de faire. Sauf que ce scheduler, dont je vous parlerais bientôt, ne dispose pas d'une option de notification. Ca viendra peut être, et ce travail sera peut être à jeter, bien que le principe puisse être adapté à d'autre notifications.

Le scheduler pilotant dans mon cas les thermostats, je vais donc me baser sur les changement d'était du thermostat pour notifier l'état à Slack (voire faire autre chose...). Pour cela je vais stocker les états qui ont changés dans des input_number et des input_text et m'en servir pour notifier avec les bonnes variables.

La liste des courses

J'ai essayé de simplifier en minimisant les besoins, mais il nous faut des input_number: et des input_text:

input_number
  trigger_source_notify_temp_current:
    name: Température Ambiante
    mode: box
    min: 0
    max: 30
    unit_of_measurement: "°C"

  trigger_source_notify_temp_target:
    name: Température de consigne
    mode: box
    min: 0
    max: 30
    unit_of_measurement: "°C"

input_text:
  trigger_source_notify_name:
    name: Trigger Source Name

  trigger_source_notify_hvac_mode:
    name: Trigger Source HVAC Mode

  trigger_source_notify_hvac_action:
    name: Trigger Source HVAC Action
  
  trigger_source_notify_preset_mode:
    name: Trigger Source Preset Mode

  trigger_source_notify_state:
    name: Trigger Source State

Ces "inputs" vont servir à stocker temporairement les valeurs que je vais afficher dans mes notifications.

Une seule automation

Ensuite on va avoir besoin d'une automation qui va :

  1. Se déclencher (trigger) sur les changements d'état pour lesquels on souhaite envoyer une notification. Ici les attributs de deux thermostats.
  2. Ecrire (action) les valeurs liées à la source de déclenchement dans les input_number: et input_text:
  3. Notifier (action) en se servant des valeurs écrite précédemment.
- alias: Notifications des Thermostats
  trigger:
  - platform: template
    value_template: "{{ state_attr('climate.daikin','temperature')  }}"
  - platform: template
    value_template: "{{ state_attr('climate.daikin','preset_mode')  }}"
  - platform: template
    value_template: "{{ state_attr('climate.daikin','hvac_modes')  }}"

  - platform: template
    value_template: "{{ state_attr('climate.thermostat_antoine','temperature')  }}"
  - platform: template
    value_template: "{{ state_attr('climate.thermostat_antoine','preset_mode')  }}"
  - platform: template
    value_template: "{{ state_attr('climate.thermostat_antoine','hvac_modes')  }}"

  action:
  - data_template:
      entity_id: input_number.trigger_source_notify_temp_target
      value: '{{ trigger.to_state.attributes.temperature }}'
    service: input_number.set_value
  - data_template:
      entity_id: input_number.trigger_source_notify_temp_current
      value: '{{ trigger.to_state.attributes.current_temperature }}'
    service: input_number.set_value
  - data_template:
      entity_id: input_text.trigger_source_notify_name
      value: '{{ trigger.to_state.attributes.friendly_name }}'
    service: input_text.set_value
  - data_template:
      entity_id: input_text.trigger_source_notify_hvac_action
      value: '{{ trigger.to_state.attributes.hvac_action }}'
    service: input_text.set_value
  - data_template:
      entity_id: input_text.trigger_source_notify_preset_mode
      value: '{{ trigger.to_state.attributes.preset_mode }}'
    service: input_text.set_value
  - data_template:
      entity_id: input_text.trigger_source_notify_state
      value: '{{ trigger.to_state.state }}'
    service: input_text.set_value

  - service: notify.slack_hass_canaletto
    data:
      message: "{{ states.sensor.date_time.state}} > {{ states.input_text.trigger_source_notify_name.state }} : Consigne à {{ states.input_number.trigger_source_notify_temp_target.state }}° |  
                Température ambiante : {{ states('input_number.trigger_source_notify_temp_current') }}° | 
                Etat : {{ states.input_text.trigger_source_notify_hvac_action.state }} / {{ states.input_text.trigger_source_notify_preset_mode.state }} / {{ states.input_text.trigger_source_notify_state.state }}

Et on obtient une notification dans le journal Slack

2020-12-06, 20:24 > Thermostat : Antoine : Consigne à 13.0° | Température ambiante : 17.1° | Etat : idle / away / heat

Bonus

On peut s'amuser à traduire les messages d'état et ainsi les insérer en clair et en français dans la notification... En bricolant quelque chose du genre :

  {% if is_state('input_text.trigger_source_notify_hvac_action', 'heating') %}
    Etat : Chauffe
  {%-elif is_state('input_text.trigger_source_notify_hvac_action', 'cooling') %}
    Climatise
  {%-elif is_state('input_text.trigger_source_notify_hvac_action', 'dry') %}
    Désumidifie
  {%-elif is_state('input_text.trigger_source_notify_hvac_action', 'fan_only') %}
    Ventile
  {%-elif is_state('input_text.trigger_source_notify_hvac_action', 'heat_cold') %}
    Auto
  {% else %}
    Etat : Inactif
  {% endif %}

  {% if is_state('input_text.trigger_source_notify_preset_mode', 'away') %}
    Préréglage : Absent
  {%-elif is_state('input_text.trigger_source_notify_preset_mode', 'eco') %}
    Préréglage : Eco
  {%-elif is_state('input_text.trigger_source_notify_preset_mode', 'boost') %}
    Préréglage : Boost
  {% else %}
    Préréglage : Normal
  {% endif %}

  {% if is_state('input_text.trigger_source_notify_state', 'heat') %}
    Mode : Chauffage
  {%-elif is_state('input_text.trigger_source_notify_state', 'cold') %}
    Mode : Climatisation
  {%-elif is_state('input_text.trigger_source_notify_state', 'dry') %}
    Mode : Désumidifiation
  {%-elif is_state('input_text.trigger_source_notify_state', 'fan_only') %}
    Mode : Ventilation
  {%-elif is_state('input_text.trigger_source_notify_state', 'heat_cold') %}
    Mode : Auto
  {% else %}
    Mode : Arrêt
  {% endif %}

Voilà, bien entendu tout ça est adaptable à d'autres besoins....