Aller au contenu

Annexe A — catalogue des entités et services

Référence de SPECIFICATION.md. Chaque ligne indique l'entité, son état, ses attributs, le palier qui l'alimente et l'origine de la donnée dans pronotepy 2.15.7.

Conventions de lecture :

  • <é> remplace le préfixe de l'appareil de l'enfant. Toutes les entités portent _attr_has_entity_name = True et un translation_key ; le nom affiché est traduit, l'identifiant est stable (§2.4 de la spécification).
  • P = palier de collecte, tel que défini au §5.2.
  • Un attribut en gras est déclaré dans _unrecorded_attributes.
  • « — » signifie que la donnée n'a pas d'équivalent direct dans pronotepy : elle est calculée par l'intégration.

1. Capteurs primitifs — la surface d'automatisation

Ce sont les entités que le §2.1 de la spécification rend obligatoires : un état scalaire, directement utilisable par un déclencheur state ou numeric_state, sans template.

Entité État device_class Attributs P Origine
sensor.<é>_prochain_cours horodatage du début timestamp subject, teachers, classroom, end, canceled timetable Lesson.start
sensor.<é>_fin_des_cours horodatage de la fin du dernier cours du jour timestamp subject timetable Lesson.end
sensor.<é>_fin_de_matinee horodatage de la fin du dernier cours avant la pause de midi timestamp subject, end_inferred, resumes_at, break_minutes timetable Lesson.end
sensor.<é>_prochaine_annulation horodatage du début du prochain cours annulé timestamp subject, end, classroom, end_inferred, items timetable Lesson.canceled
sensor.<é>_prochain_reveil horodatage du réveil calculé timestamp first_lesson, margin_minutes timetable — (premier cours − wake_margin)
sensor.<é>_cours_du_jour nombre de cours lessons, first_start, last_end, canceled_count timetable Client.lessons()
sensor.<é>_devoirs_a_faire nombre de devoirs non faits items, next_due homework Homework.done
sensor.<é>_devoirs_pour_demain nombre de devoirs pour le lendemain items homework Homework.date
sensor.<é>_derniere_note valeur numérique de la note la plus récente subject, out_of, coefficient, date, class_average, status marks Grade.grade
sensor.<é>_moyenne_generale moyenne générale de l'élève out_of (constante 20), period marks Period.overall_average
sensor.<é>_moyenne_de_la_classe moyenne générale de la classe out_of (constante 20), period marks Period.class_overall_average
sensor.<é>_prochain_controle horodatage du prochain cours marqué contrôle timestamp subject, classroom timetable Lesson.test
sensor.<é>_prochaine_punition horodatage du prochain créneau de retenue timestamp nature, duration, giver attendance Punishment.schedule
sensor.<é>_absences_non_justifiees nombre items attendance Absence.justified
sensor.<é>_actualites_non_lues nombre items news Information.read
sensor.<é>_messages_non_lus somme des non-lus items discussions sum(Discussion.unread)
sensor.<é>_periode_en_cours nom de la période start, end, index session Client.current_period

Exigence. sensor.<é>_messages_non_lus vaut sum(d.unread for d in discussions). Discussion.unread est un entier (nbNonLus), pas un booléen : compter les discussions ayant au moins un message non lu donnerait un nombre différent, et le capteur contredirait son propre attribut items.

Exigence. sensor.<é>_prochain_controle est ce qui rend possible « réviser la veille au soir ». Un capteur binaire controle_prevu ne répond qu'à « aujourd'hui » ; sans horodatage, l'automatisation la plus demandée retombe sur un template parcourant attributes.lessons, c'est-à-dire l'échec exact que le §1 de la spécification se donne pour critère.

Exigence. sensor.<é>_periode_en_cours devient indisponible plutôt que faux quand current_period ne peut pas être déterminée : pronotepy retombe sur onglets[0] si l'onglet 198 est absent, ce qui désigne silencieusement la mauvaise période dans un établissement qui ne publie pas les notes.

Exigence. sensor.<é>_derniere_note a un état numérique. Quand la note la plus récente est une sentinelle (|1 à |8), l'état vaut unknown et l'attribut status porte le motif. Un état qui vaudrait tantôt 14.5 tantôt Absent ne serait exploitable ni par un seuil ni par un graphique (§4.3).

Exigence. sensor.<é>_prochain_reveil ignore les cours annulés et les jours sans cours, et n'avance pas au lendemain avant la fin des cours du jour. Le calcul se fait dans le fuseau de l'établissement.

Exigence. sensor.<é>_fin_de_matinee retient le creux le plus long qui commence entre 11h00 et 14h30 dans le fuseau de l'établissement et dure au moins 45 minutes (MIDDAY_BREAK_EARLIEST, MIDDAY_BREAK_LATEST, MIDDAY_BREAK_MIN_MINUTES). Ce n'est délibérément pas « le plus grand creux de la journée » : un enfant qui a un cours le matin et un en fin d'après-midi a un creux de cinq heures qui n'est pas un déjeuner, et répondre 10h00 mettrait un parent sur la route au mauvais moment. La durée minimale est l'autre moitié de la règle — trente minutes à midi est un changement de salle, pas un repas. Comme prochain_reveil, le capteur ne compte que les cours qui placent réellement l'enfant quelque part : les cours annulés et ceux dont il est dispensé sont écartés (_teaching_lessons). Les chevauchements sont traités en suivant la fin la plus lointaine atteinte et non par comparaison de paires consécutives, parce que PRONOTE renvoie des cours qui se chevauchent — un remplaçant arrive alors que l'original est encore là — et qu'un balayage par paires inventerait un creux négatif.

Exigence. L'état de sensor.<é>_fin_de_matinee est unknown quand la journée n'a pas de pause de midi, et c'est l'objet de l'entité, pas une lacune. unknown et non unavailable : la collecte a réussi, la réponse est « pas de pause de midi aujourd'hui ». Il n'y a pas de valeur de repli vers la fin des cours : fin_des_cours répond déjà à l'enfant qui finit à midi sans rien après, et deux entités d'horodatage portant le même instant feraient déclencher deux automatisations pour un seul retour. L'attribut end_inferred vaut ce qu'il vaut pour toute fin de cours — vrai partout où DateDuCoursFin n'est pas envoyé, ce qui est le cas de tous les cours sur certains établissements (§4.1).

Exigence. sensor.<é>_prochaine_annulation retient le prochain cours annulé dont la fin n'est pas passée, et unknown s'il n'y en a pas. Le même filtre end > now sert l'état et l'attribut items : deux filtres finiraient par diverger, et une carte montrerait une liste dont la première entrée n'est pas l'état de l'entité. La conséquence est assumée — pendant le créneau annulé, l'état est légèrement dans le passé, ce qui se lit « cette annulation est encore en cours » et ne demande pas de contournement.

Exigence. La fenêtre de sensor.<é>_prochaine_annulation est celle qui a été collectée — la semaine entière, pas la journée — comme prochain_cours : une annulation à deux jours est exactement celle dont un parent veut être prévenu. Une dispense n'est pas une annulation et n'y figure jamais : le cours a lieu, l'enfant n'est simplement pas tenu d'y être, et l'annoncer comme annulé dirait à un parent que la classe est supprimée.


2. Capteurs de liste — la surface d'affichage

État = décompte, contenu = attribut non enregistré. Destinés aux cartes.

Entité État Attributs P Origine
sensor.<é>_emploi_du_temps_de_demain nombre de cours lessons timetable Client.lessons()
sensor.<é>_emploi_du_temps_de_la_semaine nombre de cours lessons timetable Client.lessons()
sensor.<é>_devoirs nombre total sur l'horizon items homework Client.homework()
sensor.<é>_notes nombre de notes de la période items marks Period.grades
sensor.<é>_moyennes_par_matiere nombre de matières items marks Period.averages
sensor.<é>_absences nombre items attendance Period.absences
sensor.<é>_retards nombre items attendance Period.delays
sensor.<é>_punitions nombre items attendance Period.punishments
sensor.<é>_evaluations nombre items evaluations Period.evaluations
sensor.<é>_actualites nombre items news Client.information_and_surveys()
sensor.<é>_discussions nombre items discussions Client.discussions()
sensor.<é>_menu_du_jour nombre de plats first_meal, main_meal, side_meal, other_meal, cheese, dessert, is_lunch, published menus Menu
sensor.<é>_menu_de_demain nombre de plats idem menus Menu
sensor.<é>_bulletin nombre de matières subjects, comments, period marks Period.report
sensor.<é>_equipe_pedagogique nombre de membres items static Client.get_teaching_staff()
sensor.<é>_classe nom de la classe grade, establishment session ClientInfo.class_name
sensor.<é>_periodes nombre de périodes items, current_index session ClientBase.periods

Exigence. Les trois dernières lignes marquées session ne coûtent aucun appel : class_name, establishment, name et periods se lisent dans func_options et parametres_utilisateur, déjà en main après la connexion. La v1 les rangeait sous static et leur budgétait des appels ; le palier static ne vaut en réalité qu'un seul appel, celui de l'équipe pédagogique.

Exigence. Les éléments de sensor.<é>_moyennes_par_matiere sont clés par Subject.id : Average n'a pas de champ id, et la règle de stabilité (§2.4 de la spécification) interdit d'employer un rang dans la liste.

Exigence. Les deux capteurs de menu portent toujours leurs sept clés de plats, plus published. Un établissement qui ne publie rien un jour donné n'est pas une erreur : la requête aboutit et la semaine revient vide. Or un jeu d'attributs qui apparaît et disparaît est illisible depuis une carte — state_attr(..., 'main_meal') rend None aussi bien quand la cantine ne publie rien que quand le palier n'a jamais tourné, et ces deux phrases ne se disent pas pareil à un parent. Les listes sont donc vides et published porte la distinction ; is_lunch vaut None plutôt que de deviner un service pour un repas qui n'existe pas. L'état, lui, reste unknown : zéro plat serait une affirmation sur un menu existant.

Exigence. Chaque élément de sensor.<é>_devoirs (et des deux capteurs primitifs de devoirs) porte description et description_text. descriptif arrive de PRONOTE en HTML — les professeurs saisissent dans un éditeur riche — et une carte ne peut en faire ni l'un ni l'autre : l'injecter ferait de chaque champ de texte professoral une voie d'entrée dans le tableau de bord, l'afficher tel quel fait lire les balises au parent. La conversion appartient donc au seul module qui sait que le champ est du HTML, une fois, et non à chaque carte — le HTML reste à côté, car il porte l'emphase et les liens.

2.1 Forme des éléments

Les tableaux ci-dessus nomment l'attribut ; ils ne disaient pas ce qu'un élément contient, et c'est un manque qui a coûté. Une carte a lu grade au lieu de value : toutes les notes s'affichaient « — », matière et coefficient corrects, note absente, sans un signe d'erreur — et ses propres tests ne l'ont pas vu, parce que leurs fixtures reprenaient le nom inventé. Un nom de champ deviné se vérifie contre lui-même. Il est donc écrit ici.

Ces noms sont ceux des DTO de models.py et ils sont stables : un champ peut s'ajouter, aucun ne sera renommé sans annonce.

lessons[]id, subject, subject_id, teachers[], classroom, start, end (ISO 8601), canceled, status, test, outing, detention, exempted, memo, background_color, end_inferred.

items[] de devoirsid, subject, description (HTML tel que PRONOTE l'envoie), description_text (le même énoncé en texte simple), due, done, background_color, attachments[], attachment_links[].

attachments[] et attachment_links[] — deux projections d'un seul champ, et l'une ne remplace pas l'autre. attachments[] porte les noms des pièces jointes, toutes, sous forme de chaînes : un gabarit qui fait | join(', ') dessus continue de fonctionner. attachment_links[] répond à une autre question — lesquelles peut-on ouvrir, et à quelle adresse — sous forme d'objets { name, url }.

Les deux clés sont le contrat, et il est arrêté : les adresses ne rejoindront pas attachments[]. Une carte aurait besoin de l'adresse dans attachments[] pour transformer un nom en lien, donc la clé unique lui coûterait moins — mais y mettre des objets change la forme d'un attribut qui porte des chaînes depuis l'origine, et casse le gabarit | join(', ') qui est aussi la façon normale d'écrire « Pièces jointes : a.pdf, b.pdf » dans une notification. Une ligne dans une carte coûte moins que cette rupture-là.

Le rapprochement entre les deux listes se fait par le nom, qui est la même chaîne de part et d'autre. Cette clé de rapprochement a une limite qu'il faut connaître : si un même devoir porte deux pièces de même nom dont une seule est ouvrable, le nom ne les distingue plus, et un consommateur rendra les deux comme ouvrables. C'est le prix assumé de la compatibilité — rare, cosmétique, et à comparer à une forme d'attribut cassée pour tous les lecteurs.

Les deux sortes y figurent, et aucune des deux adresses n'est celle de PRONOTE. C'est le cœur du sujet, parce que PRONOTE range deux choses différentes sous une même clé.

Un lien est une adresse qu'un professeur a collée. Elle est stable, elle n'authentifie personne, et elle est publiée telle quelle : la relayer reviendrait à aller chercher le site d'un tiers avec la session de l'établissement, ce qui n'est pas l'affaire de cette intégration.

Un fichier n'a aucune adresse qui existe indépendamment de la session. pronotepy en construit une de la forme FichiersExternes/<hexadécimal>/<nom>?Session=<n>, où le segment hexadécimal n'identifie pas le document : c'est le couple {"N": id, "Actif": true} chiffré avec la clé et le vecteur d'initialisation de la session, tous deux tirés à chaque connexion. Deux liens vers le même document depuis deux sessions n'ont donc aucun octet commun. La publier écrirait une adresse porteuse d'un droit d'accès dans l'instantané, dans chaque ligne du recorder et dans le téléchargement de diagnostic — ce dont § 8.2 a retiré l'URL iCal — et elle serait morte dans l'heure de toute façon.

Exigence. Un fichier reçoit donc une adresse que Home Assistant sert : un chemin signé et expirant vers le point d'entrée /api/pronote_ng/attachment/ de l'intégration, qui va chercher les octets par le chemin unique vers le réseau — une session, un verrou, une requête facturée — et les relaie.

Quatre propriétés de ce chemin portent le raisonnement, et aucune n'est cosmétique :

  • Il relaie, il ne redirige pas. Une redirection 302 mettrait l'adresse PRONOTE dans la barre d'adresse, l'historique du navigateur, l'onglet réseau et le journal de tout mandataire sur le trajet — donc exactement le droit d'accès qu'il s'agit de ne pas publier.
  • Il est enraciné, pas absolu. Une URL absolue obligerait l'intégration à deviner par quel hôte le navigateur est arrivé, et rendrait l'adresse interne à quelqu'un connecté de l'extérieur.
  • Il nomme une empreinte, pas un identifiant. Un identifiant PRONOTE réel contient un #, délimiteur de fragment qui tronquerait le chemin ; et un attribut part dans le recorder, donc y écrire un identifiant réel le rend durable. L'empreinte est l'idiome que diagnostics.py emploie déjà pour les identifiants d'enfant. Elle désigne un document et non un rang, donc un réordonnancement entre deux collectes ne peut pas servir le mauvais fichier.
  • Il expire en douze heures, et il n'est pas enregistré. Ces deux bornes vont ensemble : douze heures est défendable pour une adresse qui fuit par l'historique d'un navigateur, et ne l'est pas pour une adresse écrite dans la base d'historique à chaque collecte — une base est recopiée dans chaque sauvegarde et parfois collée dans un rapport de bogue. L'exclusion qui l'assure est celle de items, déclarée pour toutes les entités dans UNRECORDED_LIST_ATTRIBUTES : le jeton existe dans l'état vivant et nulle part de durable.

Exigence. L'exclusion porte sur la clé de premier rang qui contient les adresses, et une sous-classe n'a jamais le droit de la redéclarer. Ces deux points ont chacun coûté une version, et ils vont ensemble.

Le recorder ne filtre que le premier rang — recorder.db_schema.shared_attrs_bytes_from_event est une compréhension sur state.attributes.items() — donc nommer attachment_links, qui vit dans items, n'exclut rien. Et Home Assistant ne réunit pas ces ensembles le long d'une hiérarchie : Entity.__init_subclass__ calcule _entity_component_unrecorded_attributes | cls._unrecorded_attributes, où le terme de droite est résolu par recherche d'attribut ordinaire, si bien qu'une déclaration sur une sous-classe remplace celle du parent au lieu de l'étendre. La version 0.0.22 a déclaré attachment_links sur la classe des capteurs en croyant ajouter : elle a rendu au recorder les dix-huit attributs de liste de tous les capteurs, items compris, et les adresses signées ont été mesurées dans l'historique d'une instance vivante. La barrière est aujourd'hui double — un test passe l'état publié dans le filtre réel du recorder, un autre refuse toute déclaration qui ne couvre pas l'ensemble partagé.

Le raisonnement qui a rendu l'exclusion nécessaire vaut aussi d'être retenu, parce qu'il s'est retourné. Le téléchargement de diagnostic était défendu par « ces valeurs ne sont jamais des attributs » — vrai jusqu'à ce que cet attribut existe. Le dump de cette intégration n'a d'ailleurs jamais porté d'attribut d'entité, et ne le porte toujours pas ; c'est l'enregistrement, pas le diagnostic, qui était le vecteur réel. Une propriété vraie d'une architecture cesse de l'être quand l'architecture change, et une exclusion argumentée par une architecture doit être revérifiée à chaque fois qu'on ajoute un attribut.

Ce que l'exclusion ne couvre pas. _unrecorded_attributes ne parle qu'au recorder. Une trace d'automatisation capture l'état déclencheur avec ses attributs et vit dans .storage, donc dans les sauvegardes ; un tableau de bord affiche les attributs dans la boîte de dialogue « plus d'infos », donc une capture d'écran de cette boîte publie un jeton vivant ; et tout compte Home Assistant, administrateur ou non, lit /api/states en entier. La borne réelle est donc l'expiration, pas l'exclusion — celle-ci retire le vecteur durable, elle ne rend pas le jeton privé.

Exigence. Le type de contenu annoncé au navigateur est une liste blanche — PDF, images matricielles, texte simple — et tout le reste est servi en application/octet-stream, donc téléchargé plutôt qu'affiché. La réponse sort de l'origine de Home Assistant : un document renvoyé en text/html exécuterait son propre script avec la session du lecteur, et image/svg+xml est le même danger sous un nom d'image.

Une liste attachment_links[] vide reste possible et n'est pas une lacune : la pièce est alors un lien dont l'adresse est inutilisable — pas de schéma http/https, ou pas d'adresse du tout dans la charge utile, cas où amont retombe sur le nom, ce qui est le piège.

Exigence. L'adresse publiée est en http ou https, jamais autre chose. C'est une liste blanche et non un filtre, parce qu'un consommateur met cette valeur dans un href : javascript: y serait l'injection contre laquelle le champ description est déjà protégé (§ 3.3). Une adresse relative est refusée par le même contrôle, et c'est aussi la bonne réponse — le consommateur ne sait pas de quel hôte elle viendrait, et la résoudre contre celui de Home Assistant fabriquerait un lien mort.

background_color — la couleur que l'établissement associe à la matière, telle que le serveur l'envoie, ou null. Elle apparaît sur les créneaux, sur les devoirs et sur les moyennes par matière : trois étages, une seule sémantique.

Trois précisions, parce que les trois se devinent mal :

  • la clé est toujours là, seule sa valeur peut manquer. null dit « on a demandé, il n'y en a pas » — ce qui n'est pas la même phrase que « cette intégration ne publie pas de couleur ». Une clé qui apparaît et disparaît obligerait chaque gabarit à tester l'appartenance avant la valeur, et celui qui l'oublie écrit None dans un tableau de bord ;
  • aucune valeur n'est inventée. Pas de couleur par défaut, pas de couleur dérivée du nom de la matière, pas de substitut. Une couleur fabriquée est indiscernable d'une vraie : dès qu'il en existe une, plus personne ne peut savoir ce que le serveur envoie, et le consommateur perd le seul signal qui lui permet de décider s'il doit se replier sur une table à lui ;
  • le serveur ne la remplit pas partout. PRONOTE l'envoie sous CouleurFond sur les devoirs et les créneaux, sous couleur sur les moyennes par matière — et pronotepy résout celle des devoirs en mode strict là où celle des créneaux est tolérante, ce qui est la bibliothèque affirmant qu'un devoir en porte toujours une. Un créneau sans couleur n'est donc pas un défaut de collecte.

subject_id — l'identifiant de la matière, à côté de son nom. Une carte qui associe une matière à quelque chose qui lui appartient (une couleur, une icône, un filtre) doit poser cette association sur une clé, et le nom en est une mauvaise : PRONOTE l'écrit en capitales, avec des accents, et un établissement peut le renommer en cours d'année.

grades[]id, subject, value (et non grade — mais la charge de event.<é>_nouvelle_note nomme cette même valeur grade : les deux noms coexistent, value dans les attributs, grade dans l'événement), status, out_of, coefficient, date, class_average, min, max, comment, is_bonus, is_optional. value et status sont exclusifs (§4.3) : l'un des deux est nul. C'est ce qui permet à l'état de sensor.<é>_derniere_note d'être numérique — donc utilisable par un numeric_state et par un graphe — au lieu d'être tantôt 14.5 tantôt Absent.

averages[]subject_id, subject, student (et non average), class_average, min, max, out_of, background_color (décodée ici sous couleur et non CouleurFond — même sémantique, orthographe amont différente, absorbée par la passerelle). Clés par subject_id : Average n'a pas d'identifiant propre et §2.4 interdit d'employer un rang.

Ici out_of est le barème décodé (baremeMoyEleve). Sur sensor.<é>_moyenne_generale et sensor.<é>_moyenne_de_la_classe, l'attribut du même nom est une constante 20 écrite dans le producteur d'attributs, pas une lecture : le même nom de clé porte une mesure sur une entité et une hypothèse sur l'autre.

absences[]id, from_date, to_date, justified, hours (une chaîne, "2h00", telle qu'upstream la donne), days (un entier), reasons[]. Il n'y a pas de minutes ici.

delays[]id, date, minutes (un entier), justified, justification, reasons[]. minutes vit ici et nulle part ailleurs : la spécification v1 le listait sur les deux, ce qui est la raison pour laquelle les deux ont aujourd'hui des entités event distinctes (§4).

punishments[]id, nature, reasons[], giver, exclusion, schedule[]. Chaque créneau de schedule[] porte start et duration_minutes. La durée n'existe qu'au niveau du créneau : Punishment n'en a pas au premier niveau, et ce n'est pas un oubli d'exposition — PRONOTE ne la donne pas autrement.

evaluations[]id, name, subject, date, acquisitions[]. Chaque acquisition porte name, level, abbreviation, domain.

items[] d'actualitésid, title, author, category, read, survey, created. Le contenu est délibérément absent : Information.content est un attribut paresseux qui place une requête à la lecture, et rien de tel ne franchit la passerelle (§3.1).

items[] de discussionsid, subject, creator, unread, closed, messages[], chaque message portant id, author, created.

items[] d'équipe pédagogiquename, role (teacher ou staff, l'interprétation restant celle d'upstream), subjects[].

subjects[] du bulletinid, name, student_average, class_average, coefficient, comments[], teachers[]. Le capteur porte en plus comments au premier niveau et period.

Exigence. Ces formes sont un contrat. Une carte tierce qui les lit doit pouvoir être écrite sans lire le code source de l'intégration, et un champ renommé en silence casse un tableau de bord sans aucun message d'erreur — le défaut se présente comme une donnée manquante, jamais comme une panne.

2.2 Historique des périodes closes

Exigence. Pour chaque période close suivie (option history_periods), les entités suivantes sont créées, suffixées par l'index de période et non par son nom (§2.4 de la spécification) :

sensor.<é>_notes_p<n> · sensor.<é>_moyennes_p<n> · sensor.<é>_moyenne_generale_p<n> · sensor.<é>_absences_p<n> · sensor.<é>_retards_p<n> · sensor.<é>_punitions_p<n> · sensor.<é>_evaluations_p<n> · sensor.<é>_bulletin_p<n>

Le nom affiché contient le libellé de la période, traduit avec un substituteur ({period}), donc lisible ; l'identifiant reste stable si l'établissement renomme « Trimestre 1 » en « Semestre 1 ».


3. Capteurs binaires

Entité on quand device_class P Origine
binary_sensor.<é>_jour_de_classe au moins un cours non annulé aujourd'hui timetable Lesson
binary_sensor.<é>_en_cours l'heure courante est dans un cours timetable Lesson.start/end
binary_sensor.<é>_cours_annules au moins un cours annulé aujourd'hui problem timetable Lesson.canceled
binary_sensor.<é>_sortie_pedagogique une sortie est prévue aujourd'hui timetable Lesson.outing
binary_sensor.<é>_controle_prevu un contrôle est prévu aujourd'hui timetable Lesson.test
binary_sensor.<é>_devoirs_en_retard un devoir non fait a une échéance passée problem homework Homework.done/date
binary_sensor.<é>_absence_en_cours une absence couvre l'heure courante problem attendance Absence.from_date/to_date
binary_sensor.<é>_punition_a_venir une punition est programmée dans le futur problem attendance Punishment.schedule
binary_sensor.<é>_vacances aucun cours dans les 7 jours ou aucun cours de toute la semaine courante timetable
binary_sensor.<compte>_collectes_bridees le limiteur retarde des collectes problem

3.1 Trois jumeaux retirés en v2

binary_sensor.devoirs_a_faire, _actualites_non_lues et _messages_non_lus existaient en v1 à côté des capteurs de décompte du §1. Ils tombent sous le test du jumeau (§2.1 de la spécification) : chacun n'était qu'un numeric_state above: 0 sur un nombre déjà exposé, donc une ligne de YAML sans template — le niveau que fixe l'objectif du projet. Trois entités par enfant qui ne faisaient disparaître aucun template.

Les capteurs binaires conservés encodent tous un prédicat qu'il faudrait sinon écrire à la main : une intersection de créneaux, une comparaison d'échéance, une absence de cours sur une fenêtre.

Exigence. Les entités qui dépendent de l'heure et non des seules données — en_cours, absence_en_cours, et côté capteurs prochain_cours, fin_des_cours, fin_de_matinee, prochaine_annulation, prochain_reveil, prochain_controle, prochaine_punition — sont réévaluées par async_track_point_in_time positionné sur la prochaine bascule connue, jamais par sondage. Sans cela elles changent d'état au rythme de leur palier, avec jusqu'à quinze minutes de retard : sur un capteur de réveil, c'est tout son objet qui disparaît.


4. Entités event — les déclencheurs

Alimentées par delta.py (§2.2 de la spécification). Le event_type distingue la nature du changement ; les attributs portent le contexte.

Entité event_types Attributs du contexte P
event.<é>_nouvelle_note grade_added subject, grade, out_of, coefficient, date, class_average, status, grade_id marks
event.<é>_nouveau_devoir homework_added subject, description, due, id homework
event.<é>_cours_modifie lesson_canceled, lesson_restored, lesson_moved, room_changed, teacher_changed, lesson_status_changed subject, start, end, previous_start, previous_end, classroom, previous_classroom, teachers, previous_teachers, status, canceled, lesson_id timetable
event.<é>_nouvelle_actualite information_added author, title, category, survey, information_id news
event.<é>_nouvelle_absence absence_added from_date, to_date, justified, reasons, hours, days, absence_id attendance
event.<é>_nouveau_retard delay_added date, justified, justification, reasons, minutes, delay_id attendance
event.<é>_nouvelle_punition punishment_added nature, reasons, giver, exclusion, schedule, punishment_id attendance
event.<é>_nouveau_message message_received discussion, author, created, discussion_id, unread discussions
event.<é>_nouvelle_evaluation evaluation_added subject, name, acquisitions, date, evaluation_id evaluations

Deux attributs ne sont pas toujours renseignés. Sur event.<é>_nouveau_message, author et created sont null quand la passerelle n'a pas déplié le fil — plus de fils sont passés en non-lu que le plafond d'expansion n'en autorise. L'évènement part quand même, avec l'identité du fil : perdre « un message est arrivé » serait pire que perdre le nom de l'auteur.

Exigence. Deux règles de détection, selon la collection (§2.2.1 de la spécification) :

  • Toutes les lignes de ce tableau sauf cours_modifie sont des collections en ajout seul : delta sur l'identifiant N, jamais sur le contenu. Un libellé corrigé par un professeur ne produit pas de faux évènement.
  • cours_modifie ne peut pas fonctionner sur N : un changement de salle conserve le même identifiant, et un remplacement en crée un nouveau sans retirer l'ancien. Sa détection est donc : dédoublonnage par créneau en gardant le num maximal, puis comparaison du tuple (canceled, status, classroom, teachers, start, end) sur la clé (date, place, subject_id). Les quatre attributs previous_* viennent de ce tuple précédent — ils n'avaient aucune source dans la v1.

Exigence. Aucun évènement n'est émis lors du premier instantané d'un palier après démarrage ou rechargement, sinon chaque redémarrage rejouerait le trimestre.

Exigence. Un même cycle qui découvre huit nouvelles notes émet huit évènements successifs, pas un évènement agrégé : une automatisation qui notifie par note doit pouvoir le faire, et l'agrégation est triviale à refaire côté utilisateur alors que la séparation ne l'est pas.


5. Agendas, tâches, boutons, image

5.1 calendar

Entité Contenu P Origine
calendar.<é>_emploi_du_temps un évènement par cours dédoublonné, uid = Lesson.id timetable Lesson
calendar.<é>_devoirs un évènement d'une journée par échéance homework Homework.date
calendar.<é>_punitions les créneaux programmés (retenues) attendance Punishment.schedule

Exigence. Un cours annulé reste dans l'agenda, avec son statut en description et le préfixe traduit annulé dans le résumé. Le supprimer donnerait l'illusion qu'il n'a jamais existé.

5.2 todo

Entité Éléments Écriture P
todo.<é>_devoirs un élément par devoir, due = échéance, summary = matière, description = énoncé en texte simple cocher → Homework.set_done(True) homework

Exigence. UPDATE_ITEM n'est annoncé que si write_operations_enabled est vrai (§8.3 de la spécification).

5.3 button

Entité Effet
button.<é>_rafraichir demande un passage prioritaire à l'ordonnanceur, sans contourner le limiteur
button.<é>_rafraichir_les_notes idem, palier marks seulement

5.4 image

Entité Contenu P Origine
image.<é>_photo photo de profil static ClientInfo.profile_picture

Exigence. ClientInfo.profile_picture passe par ClientInfo._cache(), qui court-circuite ClientBase.post et la signature membre du parent. La photo est donc lue par une fonction de la passerelle, sous verrou et après set_child, jamais par une lecture de propriété depuis l'entité — sans quoi un compte parent peut afficher la photo du mauvais enfant.

Exigence. Aucune entité n'expose l'identité détaillée (Identity, Guardian) : date de naissance, adresse, INE, téléphones et courriels sont lus par le palier static uniquement si un service les demande, et n'entrent dans aucun état (§8.1 de la spécification).


6. Services

Service Cible Effet Réponse
pronote.refresh entrée, palier optionnel force une échéance
pronote.get_ical_url entrée rend l'URL iCal SupportsResponse.ONLY
pronote.get_identity entrée rend l'identité et les responsables légaux SupportsResponse.ONLY
pronote.mark_homework_done identifiant de devoir Homework.set_done()
pronote.mark_information_read identifiant d'actualité Information.mark_as_read()
pronote.send_message discussion ou destinataires Discussion.reply() / Client.new_discussion()
pronote.generate_timetable_pdf entrée, jour, orientation rend une URL de PDF SupportsResponse.ONLY
pronote.get_rate_limit_status entrée rend l'état complet du limiteur SupportsResponse.ONLY

Exigence. Les quatre services à réponse ne créent aucun état et ne journalisent pas leur réponse. get_ical_url et get_identity rendent des données que le §8 de la spécification interdit d'exposer autrement.

Exigence. Les services d'écriture lèvent HomeAssistantError avec translation_domain et translation_key — jamais un message construit en français dans le code (§10 de la spécification).


7. Diagnostic du limiteur

Rend le réglage du §6 observable. Toutes ces entités portent entity_category: diagnostic.

Entité État Attributs
sensor.<compte>_appels_du_jour nombre d'appels depuis minuit by_tier, logins, failed_logins
sensor.<compte>_budget_restant appels restants sur le plafond du jour daily_cap, hourly_remaining, tokens
sensor.<compte>_derniere_collecte horodatage (timestamp) tier, duration_ms, calls
sensor.<compte>_prochaine_collecte horodatage (timestamp) tiers_due, overdue_by, failing, boosted, boost_served_at
sensor.<compte>_age_de_la_session âge de la session en secondes session_id_hash, opened_at
sensor.<compte>_duree_de_vie_de_la_session durée de vie mesurée de la session, en minutes samples, last_expiry, strategy
sensor.<compte>_connexions_du_jour nombre de connexions réussies failed, cap
sensor.<compte>_etat_du_limiteur nominal, throttled, backoff, quiet_hours, credentials_hold, bootstrap_failed until, reason, consecutive_failures
binary_sensor.<compte>_collectes_bridees on si des collectes sont retardées since

Ces entités sont attachées à l'appareil compte, pas à un enfant : le budget est partagé (§7.1 de la spécification).

Exigence. session_id_hash est une empreinte tronquée, pas l'identifiant de session. Un diagnostic ne doit pas donner de quoi rejouer une session.

Exigence. Ces neuf entités se rafraîchissent d'elles-mêmes, toutes les trente secondes, sans placer aucune requête : leurs valeurs se lisent dans le limiteur, l'ordonnanceur et la session, déjà en mémoire. Ce n'est pas un détail d'implémentation. Rafraîchies seulement par une collecte — ce qu'elles étaient, _attr_should_poll étant inopérant sur une entité de coordinateur —, elles se taisaient précisément dans la circonstance où on les consulte : un palier dû et en échec laissait « prochaine collecte » figée quarante minutes dans le passé, et le témoin « bridé », dont tout le rôle est d'expliquer une intégration silencieuse (§6.6), pouvait rester allumé après le retour à la normale. La contrepartie est explicite dans LocallyPolledMixin : le scrutin ne doit jamais appeler async_request_refresh, sans quoi une lecture gratuite deviendrait une requête toutes les trente secondes.

Exigence. prochaine_collecte répond aussi à « mon appui sur Rafraîchir a-t-il porté ? », et il faut deux attributs pour ça parce qu'il y a trois réponses. boosted liste les catégories dont la demande est armée et pas encore servie ; boost_served_at donne, par catégorie, l'heure à laquelle la dernière demande a effectivement été suivie d'une collecte.

L'absence des deux ne signifie rien en soi, et la formulation courte — « pas dans boosted, pas dans boost_served_at, donc refusé » — est fausse. Ces attributs portent un état, pas une issue : sur une instance qui vient de démarrer les deux sont vides parce que personne n'a rien pressé, et non parce qu'un plafond a refusé quoi que ce soit. La lecture juste demande une troisième information que l'intégration n'a pas et que l'appelant a : l'instant de l'appui. Si un appui de moins d'un intervalle existe et que la catégorie n'apparaît dans aucun des deux, alors le plafond a refusé — une demande ayant déjà été servie dans l'intervalle, c'est-à-dire « c'est déjà demandé, ça viendra ». Sans cette condition, une interface fraîchement ouverte annonce « refusé » à quelqu'un qui vient d'arriver.

Ni l'un ni l'autre ne redit failing, ni les reports du limiteur : ceux-là répondent à « le limiteur a-t-il refusé », qui est une autre question. Un boost sur une catégorie que le limiteur maintient fait monter les reports sans placer une seule requête. La distinction est ici parce que la confondre a un coût concret : un bouton qui ne peut dire que « la demande est partie » est un bouton qu'on presse deux fois, et un second appui n'est pas gratuit sur une catégorie en échec. L'ordonnanceur connaissait déjà les deux faits ; ils n'atteignaient que le vidage de diagnostics et un service à réponse seule, donc aucun tableau de bord.

Exigence. prochaine_collecte porte une échéance, et non un décompte converti à l'instant de la lecture. Un horodatage passé y est une lecture légitime — un palier est en retard — et overdue_by en donne l'ampleur en secondes tandis que failing nomme les paliers dont la dernière tentative a échoué. Sans eux, « échéance dépassée et rien n'a tourné » et « il tourne et échoue à chaque fois » se lisent pareil sur la tuile.


8. Ce que PRONOTE expose et qui n'est délibérément pas exposé

Traçabilité du périmètre : ces données existent dans pronotepy et ne deviennent pas des entités.

Donnée Classe Raison
Identité complète Identity données personnelles ; service à réponse uniquement
Responsables légaux Guardian idem
Numéro INE Identity.ine_number identifiant national ; jamais exposé
URL iCal Client.export_ical() porteur d'authentification (§8.2)
Jeton mobile export_credentials() secret de connexion
Pièces jointes Attachment.data téléchargement à la demande, pas un état ; URL en attribut
Contenu de cours Lesson.content attribut paresseux coûteux ; résolu seulement pour la semaine courante
Destinataires possibles Recipient utile au seul service send_message, pas un état
Élèves de la classe StudentClass.students() données d'autres élèves
Corbeille et brouillons Discussion.labels filtrés à l'entrée

Exigence. Cette table est maintenue. Toute donnée pronotepy non exposée y figure avec sa raison — sans quoi l'écart entre ce que le protocole rend et ce que l'intégration montre redevient invisible, ce qui est précisément le défaut qu'on corrige.