Intégrer PRONOTE à son home Assistant

Pronote est une application génialissime de la société Index Education qui équipe une majorité des établissements scolaires.

Si vous ne connaissez pas, passez votre chemin, c’est que vous n’avez pas besoin de l’intégrer à votre domotique.

 

Attention, ce Retex est dépassé, en effet depuis Juin 2023, une intégration (merci vient Delphiki) s’occuper de tout ce qui est décrit ci-dessous. Certains liens ou fichiers de mon HA risque de ne plus correspondre avec la suite de ce texte. Je conseille évidemment d’utiliser l’intégration.

Voici le dernier Retex : Je travaille sur les cartes HA de Pronote

 

Cette intégration est basé sur le script de Dathosim (Merci beaucoup !!)
https://github.com/dathosim/Pronote2Homeassistant

J’ai modifié quelques lignes du code, soit pour adapter à mon utilisation soit pour adapter aux nouvelles normes de HA (platform: template/sensors: devenue template:/sensor par exemple)

Installation de la librairie PronotePy

Il s’agit d’une librairie python développé en open source : pronotepy

Pour installer cette librairie, lancer en ligne de commande :

pip install pronotepy

ou pour avoir la dernière version (déconseillé) :

pip install https://github.com/bain3/pronotepy/archive/refs/heads/master.zip

De temps en temps, il faut penser à lancer un petit coup de :

pip install –upgrade pronotepy

Installation du script python de Dathosim

Aller dans le dossier /config de votre HA et y créer un nouveau dossier qui peut s’appeler python_scripts et qui contiendra vos scripts personnalisés.

Dans ce dossier /config/python_scripts, y placer les fichiers pronote.py et config.ini de la distribution Pronote2Homeassistant.

Configurer le script Python

Il est nécessaire d’éditer le fichier config.ini et d’y ajouter les informations spécifiques.

Configuration spécifique Sigalou

J’ai rencontré des soucis dans l’identification avec l’ENT atrium_sud. Pour une raison que j’ignore, j’ai dû modifier le script Python. Je vais transmettre ces informations à Dathosim pour voir ce qu’il se passe. Je donne ici mes modifications, elles pourront être utiles. J’ai modifié la ligne 57.

# client = (pronotepy.ParentClient if type_compte == 'parent' else pronotepy.Client)(url, username, password, ent)
client = (pronotepy.ParentClient if type_compte == 'parent' else pronotepy.Client)('https://0xx0xxxx.index-education.net/pronote/mobile.parent.html', username = 'xxxxx', password='yyyyy', ent=atrium_sud)
  • Pour avoir l’URL, je suis allé sur pronote regarder l’url proposée lorsqu’on flash le QR code.
  • Pour xxxx il faut mettre le nom d’utilisateur et yyyy le mot de passe.

Avec cela, le script fonctionne parfaitement. Si vous avez un peu plus de chance que moi, tout devrait bien fonctionner avec le script fourni en complétant le fichier de configuration avec vos identifiants.

Lancer le script

C’est là qu’il faut croiser les doigts pour vérifier que tout se passe bien.

Il faut se connecter en ligne de commande SSH, aller dans le dossier phyton_scripts précédemment créé avec un :

cd /config/python_scripts

cd /usr/share/hassio/homeassistant/python_scripts/ (chez Dathosim)

puis lancer le script avec :

python3 pronote.py

S’il ne se passe rien de spécial, pas de message d’erreur c’est que c’est OK, le script a bien déroulé.

Quand tout s’est bien passé, un fichier pronote_prenomenfant.json est créé dans /config/www

C’est ce fichier qui sera interrogé par HA pour récupérer les informations.

Automatiser le lancement du script

Il y a la méthode classique, probablement préférée des puristes Linux, c’est à dire d’ajouter un crontab 

*/10 * * * * /usr/bin/python3 /usr/share/hassio/homeassistant/python_scripts/pronote.py > /tmp/pronote.log 2>&1

ou en fonction de votre système :

*/10 * * * * /config/python_scripts/pronote.py > /tmp/pronote.log 2>&1

Ce n’est pas la méthode que j’ai choisie, j’ai souhaiter utiliser à 100% Home Assistant et dans ma phase d’apprentissage, j’ai cherché comment lancer un cron et comment lancer un script Python.

Ajout d’une automatisation qui lance une commande shell

HA appelle cela un timer pattern

J’ajoute un fichier shell_command.yaml à mon fichier configuration.yaml 

Dans ce fichier shell_command.yaml, j’ajoute :

update_pronote : python3 /config/python_scripts/pronote.py

Dans Automatisations, j’ajoute une nouvelle automatisation qui va lancer le script update_pronote 

Le déclencheur est un déclenchement toutes les 10 minutes

et l’action est une commande shell qui lance update_pronote

En mode YAML, cela donne :

Toutes les 10 minutes, j’ai donc le fichier /config/www/pronote_enfant.json qui est généré.

Récupérer les données

C’est ensuite HA qui va lancer des requêtes REST pour aller chercher les données

Dathosim donne toutes les requêtes à lancer, elles sont sur :
https://github.com/dathosim/Pronote2Homeassistant/blob/main/configuration.yaml

Il faut bien sur remplacer:

  • demo par le prénom de l’enfant
  • 192.168.XX.XX par l’adresse ip de HA

J’ai modifié quelques syntaxes ou capteurs (les templates), cf mon fichier templates.yaml

Créer l’écran Pronote

La dernière étape consiste à afficher les sensors qui débutent par pronote… sur des cartes pour faire un bel écran.

Voici le code de l’écran de Dathosim : https://github.com/dathosim/Pronote2Homeassistant/blob/main/lovelace.yaml

Voici mon code :

type: horizontal-stack
columns: 2
square: false
cards:
  - type: vertical-stack
    cards:
      - type: markdown
        content: |-
          <div>Emploi du temps d'aujourd'hui </div>
          <table>
              <tbody>
                  {%-for attr in states.sensor.pronote_edt_coralie_aujourdhui.attributes.edt_aujourdhui -%}
                  <tr style="background-color:#FF0000"><td>
                      {%- if state_attr('sensor.pronote_edt_coralie_aujourdhui', 'edt_aujourdhui')[loop.index-1]['annulation'] == false -%}
                          <mark>
                          {{state_attr('sensor.pronote_edt_coralie_aujourdhui', 'edt_aujourdhui')[loop.index-1]['heure']}}
                          </mark>
                      {%- else -%}
                          <span>
                          {{state_attr('sensor.pronote_edt_coralie_aujourdhui', 'edt_aujourdhui')[loop.index-1]['heure']}}
                          </span>
                      {% endif %}</td>
                      <td>{{state_attr('sensor.pronote_edt_coralie_aujourdhui', 'edt_aujourdhui')[loop.index-1]['heure_fin']}}</td>
                      <td>
                      {{state_attr('sensor.pronote_edt_coralie_aujourdhui', 'edt_aujourdhui')[loop.index-1]['cours']}}
                      {% if not state_attr('sensor.pronote_edt_coralie_aujourdhui', 'edt_aujourdhui')[loop.index-1]['status'] == None %}<span>{{state_attr('sensor.pronote_edt_coralie_aujourdhui', 'edt_aujourdhui')[loop.index-1]['status']}}</span>
                      {% endif %}</td>
                      <td>{{state_attr('sensor.pronote_edt_coralie_aujourdhui', 'edt_aujourdhui')[loop.index-1]['salle']}}</td>
                  </tr>
                  {% endfor %}
            </tbody>
          </table>
        card_mod:
          style:
            .: |
              ha-card ha-markdown {
                padding:0px
              }
              ha-card ha-markdown.no-header {
                padding:0px
              }
            ha-markdown$: |
              div {
                  background-color:rgb(100, 100, 100);
                  padding: 12px 12px;
                  color:white;
                  font-weight:normal;
                  font-size:1.2em;
                  border-top-left-radius: 5px; 
                  border-top-right-radius: 5px; 
              }
              table{
                border-collapse: collapse;
                font-size: 0.9em;
                font-family: Roboto;
                width: 100%;
                outline: 0px solid #393c3d;
                margin-top:5px;
              } caption {
                  text-align: center;
                  font-weight: bold;
                  font-size: 1.2em;
              } td {
                  padding: 5px 10px 5px 10px;
                  text-align: left;
                  border-bottom: 0px solid #1c2020;
              }
              tr {
                  border-bottom: 0px solid #1c2020;
              }

              tr:nth-of-type(even) {
                  background-color: rgb(54, 54, 54, 0.3);
              }
              tr:last-of-type {
                  border-bottom: transparent;          }*
              mark {
                  background: #009767;
                  color: #222627;
                  border-radius: 5px;
                  padding: 5px;
              }
              span {
                  background: #EC4B34;
                  color: #222627;
                  border-radius: 5px;
                  padding: 5px;
              }
              span {
                  padding: 5px;
              }
              tr:nth-child(n+2) > td:nth-child(2) {
                text-align: left;
              }
      - type: markdown
        content: >-
          <div>Emploi du temps du :
          {{state_attr('sensor.pronote_edt_coralie_prochainjour',
          'edt_prochainjour')[0]['date']}}</div>

          <table>
              <tbody>
                  {%-for attr in states.sensor.pronote_edt_coralie_prochainjour.attributes.edt_prochainjour -%}
                  <tr style="background-color:#FF0000"><td>
                      {%- if state_attr('sensor.pronote_edt_coralie_prochainjour', 'edt_prochainjour')[loop.index-1]['annulation'] == false -%}
                          <mark>
                          {{state_attr('sensor.pronote_edt_coralie_prochainjour', 'edt_prochainjour')[loop.index-1]['heure']}}
                          </mark>
                      {%- else -%}
                          <span>
                          {{state_attr('sensor.pronote_edt_coralie_prochainjour', 'edt_prochainjour')[loop.index-1]['heure']}}
                          </span>
                      {% endif %}</td>
                      <td>{{state_attr('sensor.pronote_edt_coralie_prochainjour', 'edt_prochainjour')[loop.index-1]['heure_fin']}}</td>
                      <td>
                      {{state_attr('sensor.pronote_edt_coralie_prochainjour', 'edt_prochainjour')[loop.index-1]['cours']}}
                      {% if not state_attr('sensor.pronote_edt_coralie_prochainjour', 'edt_prochainjour')[loop.index-1]['status'] == None %}<span>{{state_attr('sensor.pronote_edt_coralie_prochainjour', 'edt_prochainjour')[loop.index-1]['status']}}</span>
                      {% endif %}</td>
                      <td>{{state_attr('sensor.pronote_edt_coralie_prochainjour', 'edt_prochainjour')[loop.index-1]['salle']}}</td>
                  </tr>
                  {% endfor %}
            </tbody>
          </table>
        card_mod:
          style:
            .: |
              ha-card ha-markdown {
                padding:0px
              }
              ha-card ha-markdown.no-header {
                padding:0px
              }
            ha-markdown$: |
              div {
                  background-color:rgb(100, 100, 100);
                  padding: 12px 12px;
                  color:white;
                  font-weight:normal;
                  font-size:1.2em;
                  border-top-left-radius: 5px; 
                  border-top-right-radius: 5px; 
              }
              table{
                border-collapse: collapse;
                font-size: 0.9em;
                font-family: Roboto;
                width: 100%;
                outline: 0px solid #393c3d;
                margin-top:5px;
              } caption {
                  text-align: center;
                  font-weight: bold;
                  font-size: 1.2em;
              } td {
                  padding: 5px 10px 5px 10px;
                  text-align: left;
                  border-bottom: 0px solid #1c2020;
              }
              tr {
                  border-bottom: 0px solid #1c2020;
              }

              tr:nth-of-type(even) {
                  background-color: rgb(54, 54, 54, 0.3);
              }
              tr:last-of-type {
                  border-bottom: transparent;          }*
              mark {
                  background: #009767;
                  color: #222627;
                  border-radius: 5px;
                  padding: 5px;
              }
              span {
                  background: #EC4B34;
                  color: #222627;
                  border-radius: 5px;
                  padding: 5px;
              }
              span {
                  padding: 5px;
              }
              tr:nth-child(n+2) > td:nth-child(2) {
                text-align: left;
              }
  - type: markdown
    content: |-
      <div>Devoirs</div>
      <table>
          <tbody>
              {%-for attr in states.sensor.pronote_devoir_coralie.attributes.devoir -%}
              <tr>
                  <td>
                  {%- if state_attr('sensor.pronote_devoir_coralie', 'devoir')[loop.index-1]['done'] == true -%}
                      <mark>
                      {{state_attr('sensor.pronote_devoir_coralie', 'devoir')[loop.index-1]['date']}}
                      </mark>
                  {%- else -%}
                      <span>
                      {{state_attr('sensor.pronote_devoir_coralie', 'devoir')[loop.index-1]['date']}}
                      </span>
                  {% endif %}</td>              
                  <td>{{state_attr('sensor.pronote_devoir_coralie', 'devoir')[loop.index-1]['title']}}</td>
                  <td>{{state_attr('sensor.pronote_devoir_coralie', 'devoir')[loop.index-1]['description']}}</td>
              </tr>
              {% endfor %}
        </tbody>
      </table>
    card_mod:
      style:
        .: |
          ha-card ha-markdown {
            padding:0px
          }
          ha-card ha-markdown.no-header {
            padding:0px
          }
        ha-markdown$: |
          h1 {
              font-weight: normal;
              font-size: 24px;
          }
          div {
              background-color:rgb(100, 100, 100);
              padding: 12px 12px;
              color:white;
              font-weight:normal;
              font-size:1.2em;
                border-top-left-radius: 5px; 
                border-top-right-radius: 5px; 
          }
          table{
            border-collapse: collapse;
            font-size: 0.9em;
            font-family: Roboto;
            width: auto;
            outline: 0px solid #393c3d;
            margin-top: 10px;
          } caption {
              text-align: center;
              font-weight: bold;
              font-size: 1.2em;
          } td {
              padding: 5px 5px 5px 5px;
              text-align: left;
              border-bottom: 0px solid #1c2020;
          }
          tr {
              border-bottom: 0px solid #1c2020;
          }

          tr:nth-of-type(even) {
              background-color: rgb(54, 54, 54, 0.3);
          }
          tr:last-of-type {
              border-bottom: transparent;
          }
          mark {
              background: #009767;
              color: #222627;
              border-radius: 5px;
              padding: 5px;
          }
          span {
              background: #EC4B34;
              color: #222627;
              border-radius: 5px;
              padding: 5px;
          }
          span {
              padding: 5px;
          }
          tr:nth-child(n+2) > td:nth-child(2) {
            text-align: left;
          }
  - type: markdown
    content: |-
      <div>Evaluations</div>
      <table width='100%'>
        <tbody>
        {%-for attr in states.sensor.pronote_eval_coralie.attributes.evaluation -%}
        {% if loop.index < 40  %}
        <tr>
        <td width='10%'>{{attr['date_courte']}}</td>
        <td width='70%'>{{attr['eval']}}</td>
        <td width='20%'>
        {%for attr2 in attr.acquisitions-%}{% if attr2['acquisition_niveau'] == "A+"  %} 💚 {% elif attr2['acquisition_niveau'] == "A"  %} 🟢 {% elif attr2['acquisition_niveau'] == "B+"  %} ⚪️+ {% elif attr2['acquisition_niveau'] == "B"  %} ⚪️ {% elif attr2['acquisition_niveau'] == "C+"  %} 🟡+ {% elif attr2['acquisition_niveau'] == "C"  %} 🟡 {% elif attr2['acquisition_niveau'] == "D+"  %} ⚪️+ {% elif attr2['acquisition_niveau'] == "D"  %} ⚪️ {% elif attr2['acquisition_niveau'] == "E"  %} 🔴 {% else %}{{ attr2['acquisition_niveau']}}  {% endif %}
        {% endfor %}
        </td>
        </tr>
        {% endif %}
        {% endfor %}
      </tbody>
      </table>
    card_mod:
      style:
        .: |
          ha-card ha-markdown {
            padding:0px
          }
          ha-card ha-markdown.no-header {
            padding:0px
          }
        ha-markdown$: |
          h1 {
              font-weight: normal;
              font-size: 24px;
          }
          div {
              background-color:rgb(100, 100, 100);
              padding: 12px 12px;
              color:white;
              font-weight:normal;
              font-size:1.2em;
              border-top-left-radius: 5px; 
              border-top-right-radius: 5px; 
          }
          table{
            border-collapse: collapse;
            font-size: 0.9em;
            font-family: Roboto;
            width: auto;
            outline: 0px solid #393c3d;
            margin-top: 10px;
          } caption {
              text-align: center;
              font-weight: bold;
              font-size: 1.2em;
          } td {
              padding: 5px 5px 5px 5px;
              text-align: left;
              border-bottom: 0px solid #1c2020;
          }
          tr {
              border-bottom: 0px solid #1c2020;
          }
          tr:nth-of-type(even) {
              background-color: rgb(54, 54, 54, 0.3);
          }
          tr:last-of-type {
              border-bottom: transparent;
          }
          mark {
              background: #009767;
              color: #222627;
              border-radius: 10px;
              padding: 5px;
          }
          span {
              background: #EC4B34;
              color: #222627;
              border-radius: 10px;
              padding: 5px;
          }
          span {
              padding: 5px;
          }
          tr:nth-child(n+2) > td:nth-child(2) {
            text-align: left;
          }

A noter :

J’ai modifier la couleur des points car la correspondance n’était pas bonne avec la configuration du pronote de l’établissement de ma fille. Pour le vert plus, j’ai mis un coeur vert.

Bonus

Ajout d’une carte de mise à jour

Dans l’objectif de diminuer la mise à jour, de la passer de toutes les 10 min à toutes les 4 heures, j’ai ajouté la carte de l’image ci-dessus.

Ainsi, on a le délai depuis la dernière mise à jour et si on veut une mise à jour immédiate, on appui dessus et on lance EXECUTER

Code de la carte :

- type: custom:auto-entities
  card:
    type: entities
  filter:
    include:
      - entity_id: automation.mise_a_jour_pronote
        options:
          secondary_info: last-triggered

Limiter la mise à jour aux jours ouvrables

Après avoir basculé de 10 min à 4 heures la tempo de mise à jour, j’ai cherché à limiter cette mise à jour aux jours ouvrables.

Il existe une intégration qui créé un binary_sensor qui nous donne cette information, il s’agit de Workday.

Je n’entre pas dans son installation, la doc est archi-simple.

J’ai juste ajouté ensuite dans l’automatisation une condition :

Voici le code complet de l’automatisation :

alias: Mise à jour Pronote
description: ""
trigger:
  - platform: time_pattern
    hours: /4
condition:
  - type: is_on
    condition: device
    device_id: 64bbc75616d4864596e1fd861c865904
    entity_id: binary_sensor.workday
    domain: binary_sensor
action:
  - service: shell_command.update_pronote
    data: {}
mode: single

Edit : un nouveau tuto permet de ne déclencher la mise à jour Pronote que les jours ouvrables et hors vacances scolaires.

Lier Pronote et le déclenchement du reveil Alexa

Ce sera la prochaine étape

Mais je programmerai cela un dimanche pour avoir une heure de début des cours demain