From 9c4cb5ebcb3547232718887fa8130036889355c8 Mon Sep 17 00:00:00 2001 From: Pierre-Alexandre MARTIN Date: Fri, 15 May 2026 01:42:08 +0200 Subject: [PATCH 1/4] =?UTF-8?q?Am=C3=A9lioration=20v1,=20tests=20=C3=A0=20?= =?UTF-8?q?approfondir=20et=20code=20=C3=A0=20relire?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 16934fd397472018ef0b0e0971eaa7257e665f3e) --- README.md | 24 +- .../sncf_trains/www/sncf-train-card.js | 488 ++++++++++++++---- 2 files changed, 396 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index 6691f9f..2289506 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,11 @@ La carte `sncf-train-card` est **automatiquement disponible** dans le sélecteur Dans un tableau de bord, cliquer sur **+ Ajouter une carte** → chercher **SNCF Train Card**. +La configuration peut ensuite se faire : + +- via l'éditeur visuel Lovelace +- ou via YAML + Ou en YAML : ```yaml @@ -124,6 +129,8 @@ device_id: VOTRE_DEVICE_ID ### 🔍 Trouver le `device_id` +_S'obtient dynamiquement via la configuration visuelle._ + Le `device_id` correspond à l'appareil créé lors de la configuration du trajet. 1. Aller dans **Paramètres → Appareils & services → SNCF Trains** @@ -139,11 +146,14 @@ Le `device_id` correspond à l'appareil créé lors de la configuration du traje | `device_id` | `string` | **obligatoire** | Identifiant de l'appareil SNCF (voir ci-dessus) | | `title` | `string` | `'Trains SNCF'` | Titre affiché en haut de la carte | | `train_lines` | `number` | `3` | Nombre de trains affichés simultanément | -| `train_emoji` | `string` | `'🚅'` | Emoji du train animé sur la barre de progression | -| `train_emoji_axial_symmetry` | `boolean` | `true` | Retourne l'emoji (à utiliser selon son sens) | -| `train_station_emoji` | `string` | `'🚉'` | Emoji affiché à côté des gares | | `animation_duration` | `number` | `30` | Nombre de minutes avant l'arrivée en gare à partir duquel l'animation du train se déclenche (ex : `30` = animation active dans les 30 dernières minutes, `60` = dans la dernière heure) | | `update_interval` | `number` | `30000` | Intervalle de rafraîchissement de la carte en **millisecondes** | +| `train_emoji_axial_symmetry` | `boolean` | `true` | Retourne l'emoji du train horizontalement | +| `train_emoji` | `string` | `'🚅'` | Emoji du train animé sur la barre | +| `show_departure_station` | `boolean` | `true` | Affiche ou masque les informations de départ | +| `departure_station_emoji` | `string` | `''` | Emoji de la station de départ | +| `show_arrival_station` | `boolean` | `true` | Affiche ou masque les informations d'arrivée | +| `arrival_station_emoji` | `string` | `'🚉'` | Emoji de la station d'arrivée | ### Exemple complet @@ -154,13 +164,17 @@ title: "Paris → Lyon" train_lines: 4 train_emoji: "🚆" train_emoji_axial_symmetry: true -train_station_emoji: "🏙️" -animation_duration: 45 +show_departure_station: true +departure_station_emoji: "🚉" +show_arrival_station: true +arrival_station_emoji: "🏙️" +animation_duration: 0 update_interval: 60000 ``` ### Exemple d'affichage +![Exemple d'affichage](./assets/card_example.png) ![Exemple d'affichage](./assets/card_example.png) --- diff --git a/custom_components/sncf_trains/www/sncf-train-card.js b/custom_components/sncf_trains/www/sncf-train-card.js index 5e27af0..fe9ee0e 100644 --- a/custom_components/sncf_trains/www/sncf-train-card.js +++ b/custom_components/sncf_trains/www/sncf-train-card.js @@ -1,53 +1,201 @@ // Ajouter au registre des cartes personnalisées -(window.customCards = window.customCards || []).push({ +globalThis.customCards ||= [] +globalThis.customCards.push({ type: 'sncf-train-card', name: 'SNCF Train Card', - description: 'Carte personnalisée animée pour afficher les trains SNCF en temps réel' + description: 'Carte personnalisée animée pour afficher les trains SNCF en temps réel', + preview: true, + configurable: true }); class SncfTrainCard extends HTMLElement { constructor() { super(); - this.attachShadow({ mode: 'open' }); + this.attachShadow({mode: 'open'}); this.updateInterval = null; this.lastTrainSignature = null; this._lastRenderTime = 0; } + /** + * Méthode héritée
+ * Permet de définir la configuration de la carte, avec validation et gestion des changements de device_id pour forcer une mise à jour immédiate + * @param {Object} config - La configuration de la carte, qui doit inclure au minimum un device_id valide pour fonctionner correctement, et peut inclure d'autres paramètres pour personnaliser l'affichage + * @throws {Error} Si le device_id n'est pas défini, une erreur est levée pour informer l'utilisateur de la nécessité de fournir cette information essentielle + */ setConfig(config) { if (!config.device_id) { throw new Error('You need to define device_id'); } - + const previousDeviceId = this.config ? this.config.device_id : null; const deviceIdChanged = previousDeviceId && previousDeviceId !== config.device_id; - - this.config = { - device_id: config.device_id, - train_lines: config.train_lines || 3, - title: config.title || 'Trains SNCF', - train_emoji: config.train_emoji || '🚅', - train_emoji_axial_symmetry: config.train_emoji_axial_symmetry || true, - train_station_emoji: config.train_station_emoji || '🚉', - animation_duration: config.animation_duration || 30, - update_interval: config.update_interval || 30000, - ...config - }; - + + this.config = config; + // Forcer la mise à jour immédiate si device_id a changé if (deviceIdChanged) { this.stopUpdateTimer(); this.startUpdateTimer(); } - + // Toujours forcer un nouveau rendu this.render(); } + /** + * Méthode héritée
+ * Fournit la configuration du formulaire pour l'éditeur de Lovelace, avec des labels et des aides personnalisés + */ + static getConfigForm() { + return { + schema: [ + { + name: "device_id", + required: true, + selector: { + device: { + filter: { + integration: "sncf_trains" + } + } + } + }, + { + name: "title", + selector: {text: {}}, + }, + { + name: "train_lines", + selector: { + number: { + min: 1, + max: 10, + step: 1, + }, + }, + }, + { + name: "animation_duration", + selector: { + number: { + min: 0, + max: 100, + step: 1, + }, + }, + }, + { + name: "update_interval", + selector: { + number: { + min: 5000, + step: 1000, + }, + }, + }, + { + type: "grid", + name: "", + column_min_width: "150px", + schema: [ + { + name: "train_emoji_axial_symmetry", + selector: {boolean: {}}, + }, + { + name: "train_emoji", + selector: { + icon: {}, + }, + }, + { + name: "show_departure_station", + selector: {boolean: {}}, + }, + { + name: "departure_station_emoji", + selector: { + icon: {}, + }, + }, + { + name: "show_arrival_station", + selector: {boolean: {}}, + }, + { + name: "arrival_station_emoji", + selector: { + icon: {}, + }, + }, + ] + }, + ], + computeLabel: (schema) => { + const labels = { + device_id: "ID du Device (obligatoire)", + title: "Titre de la carte", + train_emoji: "Emoji du train", + train_lines: "Nombre de trains à afficher", + animation_duration: "Durée d'animation (minutes)", + update_interval: "Intervalle de mise à jour (ms)", + departure_station_emoji: "Emoji de la gare de départ", + arrival_station_emoji: "Emoji de la gare d'arrivée", + show_departure_station: "Afficher les informations de départ", + show_arrival_station: "Afficher les informations d'arrivée", + train_emoji_axial_symmetry: "Symétrie axiale du train", + }; + return labels[schema.name] || undefined; + }, + computeHelper: (schema) => { + const helpers = { + device_id: "L'identifiant unique du device SNCF à afficher", + title: "Le titre affiché en haut de la carte", + train_emoji: "L'emoji représentant le train", + train_lines: "Le nombre de trains à afficher (1-10)", + animation_duration: "Nombre de minutes avant le départ pour que le train apparaisse", + update_interval: "Fréquence de rafraîchissement en millisecondes (ex: 30000 pour 30s)", + departure_station_emoji: "L'emoji pour la gare de départ", + arrival_station_emoji: "L'emoji pour la gare d'arrivée", + show_departure_station: "Affiche ou masque la gare de départ", + show_arrival_station: "Affiche ou masque la gare d'arrivée", + train_emoji_axial_symmetry: "Retourner l'emoji du train horizontalement", + }; + return helpers[schema.name] || undefined; + }, + }; + } + + /** + * Méthode héritée
+ * Fournit une configuration par défaut pour le mode aperçu dans l'éditeur de Lovelace + */ + static getStubConfig() { + return { + device_id: '', + title: 'Trains SNCF', + train_lines: 3, + animation_duration: 30, + update_interval: 30000, + train_emoji_axial_symmetry: true, + train_emoji: '🚅', + show_departure_station: true, + departure_station_emoji: '', + show_arrival_station: true, + arrival_station_emoji: '🚉', + }; + } + + /** + * Méthode héritée
+ * Permet de recevoir l'objet Home Assistant et de déclencher une vérification des mises à jour des trains pour éviter les rendus inutiles + * @param {Object} hass - L'objet Home Assistant fourni par le système, utilisé pour accéder aux états et aux services, et pour déclencher des mises à jour de la carte lorsque les données des trains changent + */ set hass(hass) { const previousHass = this._hass; this._hass = hass; - + // Vérifier si les données des trains ont changé if (this.config && previousHass) { this.checkForTrainUpdates(previousHass, hass); @@ -56,14 +204,43 @@ class SncfTrainCard extends HTMLElement { } } + /** + * Méthode héritée
+ * Démarre un timer pour forcer des mises à jour régulières, ce qui est nécessaire pour capturer les changements de données en temps réel + */ + connectedCallback() { + this.startUpdateTimer(); + } + + /** + * Méthode héritée
+ * Arrête le timer de mise à jour pour éviter les fuites de mémoire lorsque la carte est retirée du DOM + */ + disconnectedCallback() { + this.stopUpdateTimer(); + } + + /** + * Méthode héritée
+ * Calcule la taille de la carte en fonction du nombre de lignes de train à afficher, avec une taille minimale pour éviter les problèmes d'affichage + */ + getCardSize() { + return Math.max(3, this.config.train_lines + 1); + } + + /** + * Vérifie si les données des trains ont changé en comparant une signature des données actuelles avec la dernière signature connue, et ne fait un rendu que si nécessaire pour optimiser les performances + * @param {Object} previousHass - L'objet Home Assistant précédent pour comparer les données + * @param {Object} currentHass - L'objet Home Assistant actuel pour récupérer les données fraîches + */ async checkForTrainUpdates(previousHass, currentHass) { try { // Récupérer les entités actuelles const currentTrains = await this.getTrainEntities(); - + // Créer une signature des données actuelles const currentSignature = this.createTrainSignature(currentTrains); - + // Comparer avec la signature précédente if (currentSignature !== this.lastTrainSignature) { this.lastTrainSignature = currentSignature; @@ -71,24 +248,25 @@ class SncfTrainCard extends HTMLElement { } } catch (error) { // En cas d'erreur, faire un rendu quand même + console.error(error); this.render(); } } + /** + * Crée une signature unique pour les données des trains en concaténant les informations clés de chaque train, ce qui permet de détecter facilement les changements sans faire un rendu complet à chaque fois + * @param {Array} trains - Un tableau d'entités de train + * @returns {string} Une chaîne de caractères représentant la signature des données des trains + */ createTrainSignature(trains) { - return trains.map(train => + return trains.map(train => `${train.entity_id}:${train.attributes.departure_time}:${train.attributes.delay_minutes || 0}:${train.attributes.has_delay || false}` ).join('|'); } - connectedCallback() { - this.startUpdateTimer(); - } - - disconnectedCallback() { - this.stopUpdateTimer(); - } - + /** + * Démarre un timer qui force un rendu de la carte à intervalles réguliers, ce qui est nécessaire pour capturer les changements de données en temps réel, surtout pour les données de train qui peuvent changer fréquemment + */ startUpdateTimer() { this.stopUpdateTimer(); this.updateInterval = setInterval(async () => { @@ -100,6 +278,9 @@ class SncfTrainCard extends HTMLElement { }, this.config.update_interval); } + /** + * Arrête le timer de mise à jour pour éviter les fuites de mémoire lorsque la carte est retirée du DOM ou lorsque le device_id change, ce qui est important pour maintenir les performances et éviter les rendus inutiles + */ stopUpdateTimer() { if (this.updateInterval) { clearInterval(this.updateInterval); @@ -107,158 +288,193 @@ class SncfTrainCard extends HTMLElement { } } + /** + * Récupère les entités de train associées au device_id configuré en utilisant l'API WebSocket de Home Assistant pour obtenir des données fraîches, filtre les trains qui ne sont pas encore passés, et trie les résultats par heure de départ pour n'afficher que les trains à venir, ce qui garantit que les informations affichées sont toujours à jour et pertinentes pour l'utilisateur + * @returns {Promise} Un tableau d'entités de train avec des données fraîches + */ async getTrainEntities() { if (!this._hass) return []; - + try { // Utiliser l'API Home Assistant pour récupérer toutes les entités const allEntityRegistry = await this._hass.callWS({ type: 'config/entity_registry/list' }); - + // Filtrer les entités par device_id - const deviceEntities = allEntityRegistry.filter(entityInfo => + const deviceEntities = allEntityRegistry.filter(entityInfo => entityInfo.device_id === this.config.device_id ); - + if (!deviceEntities || deviceEntities.length === 0) { console.warn('⚠️ Aucune entité trouvée pour ce device_id dans le registre'); return []; } - + // Récupérer les états des entités train trouvées avec données fraîches const trainEntities = deviceEntities .filter(entityInfo => entityInfo.entity_id.includes('train')) .map(entityInfo => { // Forcer la récupération de l'état frais - const freshState = this._hass.states[entityInfo.entity_id]; - return freshState; + return this._hass.states[entityInfo.entity_id]; }) - .filter(entity => entity && entity.attributes && entity.attributes.departure_time); - + .filter(entity => entity?.attributes?.departure_time); + // Filtrer les trains qui ne sont pas encore passés const currentTime = new Date(); const upcomingTrains = trainEntities.filter(entity => { const departureTime = this.parseTime(entity.attributes.departure_time); return departureTime >= currentTime; }); - - const sortedEntities = upcomingTrains + + return upcomingTrains .sort((a, b) => { const aTime = this.parseTime(a.attributes.departure_time); const bTime = this.parseTime(b.attributes.departure_time); return aTime - bTime; }) .slice(0, this.config.train_lines); - - return sortedEntities; - + } catch (error) { console.error('❌ Erreur lors de la récupération via API:', error); return []; } } - // Méthode pour parser correctement le format SNCF + /** + * Parse une chaîne de temps au format spécifique de la SNCF (ex: "19/11/2025 - 08:20") et retourne un objet Date, ou une date par défaut si le format est invalide ou si la chaîne est vide, ce qui permet de gérer correctement les données de temps fournies par les entités de train et d'éviter les erreurs d'affichage + * @param {string} departureTime - La chaîne de temps à parser, qui peut être au format SNCF ou un format standard reconnu par JavaScript + * @returns {Date} Un objet Date représentant le temps de départ, ou une date par défaut si le parsing échoue + */ parseTime(departureTime) { if (!departureTime) { return new Date(0); } - + // Format SNCF: "19/11/2025 - 08:20" if (departureTime.includes('/') && departureTime.includes(' - ')) { const parts = departureTime.split(' - '); if (parts.length === 2) { const datePart = parts[0]; // "19/11/2025" const timePart = parts[1]; // "08:20" - + const dateComponents = datePart.split('/'); if (dateComponents.length === 3) { - const day = parseInt(dateComponents[0]); - const month = parseInt(dateComponents[1]) - 1; // Mois 0-indexé - const year = parseInt(dateComponents[2]); - + const day = Number.parseInt(dateComponents[0]); + const month = Number.parseInt(dateComponents[1]) - 1; // Mois 0-indexé + const year = Number.parseInt(dateComponents[2]); + const timeComponents = timePart.split(':'); if (timeComponents.length === 2) { - const hour = parseInt(timeComponents[0]); - const minute = parseInt(timeComponents[1]); - + const hour = Number.parseInt(timeComponents[0]); + const minute = Number.parseInt(timeComponents[1]); + return new Date(year, month, day, hour, minute); } } } } - + // Fallback vers Date classique return new Date(departureTime); } + /** + * Calcule la position du train sur la barre de progression en fonction de l'heure actuelle et de l'heure de départ, en affichant le train 30 minutes avant le départ et en le faisant avancer vers la droite à mesure que l'heure de départ approche, ce qui crée une animation visuelle intuitive pour les utilisateurs afin de suivre l'approche du train vers la gare, et retourne une position en pourcentage (0% = train à gauche, 100% = train arrivé) ou une valeur négative pour indiquer que le train n'est pas encore visible, ce qui permet de gérer l'affichage du train de manière dynamique en fonction du temps restant avant le départ + * @param {string} departureTime - La chaîne de temps de départ à utiliser pour calculer la position du train, qui doit être au format reconnu par la méthode parseTime + * @param {Date} [currentTime] - L'heure actuelle à utiliser pour le calcul, qui peut être fournie pour les tests ou les rendus spécifiques, sinon la date actuelle sera utilisée par défaut + * @returns {number} Un nombre représentant la position du train en pourcentage (0-100) ou une valeur négative si le train n'est pas encore visible + */ calculateTrainPosition(departureTime, currentTime) { if (!departureTime) { return -10; } - + const departure = this.parseTime(departureTime); - - if (isNaN(departure.getTime())) { + + if (Number.isNaN(departure.getTime())) { return -10; } - + const now = currentTime || new Date(); const diffMinutes = (departure - now) / (1000 * 60); - + // Train apparaît 30 minutes avant l'heure + // todo : tester et s'assurer de la véracité / nom du param animation_duration const maxMinutes = this.config.animation_duration; - + if (diffMinutes > maxMinutes) { + if(maxMinutes === 0) { + return 0; + } return -10; // Hors de la barre } if (diffMinutes <= 0) { return 100; // Arrivé à la gare } - + // Position sur la barre (0% = gauche, 100% = droite) return ((maxMinutes - diffMinutes) / maxMinutes) * 100; } + /** + * Formate une chaîne de temps en une heure lisible au format français (ex: "08:20"), ou retourne "N/A" si la chaîne est vide, ou "Format invalide" si le parsing échoue, ce qui permet d'afficher les heures de départ et d'arrivée de manière claire et compréhensible pour les utilisateurs, tout en gérant les cas où les données de temps peuvent être manquantes ou mal formatées + * @param {string} timeString - La chaîne de temps à formater, qui doit être au format reconnu par la méthode parseTime + * @returns {string} Une chaîne représentant l'heure formatée ou un message d'erreur si le format est invalide + */ formatTime(timeString) { if (!timeString) { return 'N/A'; } - + const time = this.parseTime(timeString); - - if (isNaN(time.getTime())) { + + if (Number.isNaN(time.getTime())) { return 'Format invalide'; } - - const result = time.toLocaleTimeString('fr-FR', { - hour: '2-digit', - minute: '2-digit' + + return time.toLocaleTimeString('fr-FR', { + hour: '2-digit', + minute: '2-digit' }); - - return result; } + /** + * Calcule l'heure d'arrivée réelle en ajoutant les minutes de retard à l'heure de départ prévue, et retourne une chaîne formatée de l'heure d'arrivée réelle, ou null si les données nécessaires sont manquantes ou si le train n'a pas de retard. + * @param departureTime - L'heure de départ + * @param delayMinutes - Le temps de retard en minutes + * @returns {string} Une chaîne représentant l'heure avec retard formatée ou null + */ + // TODO : tester si encore utile ? calculateRealArrivalTime(departureTime, delayMinutes) { if (!departureTime || !delayMinutes || delayMinutes === 0) { return null; } - + const originalTime = this.parseTime(departureTime); const realTime = new Date(originalTime.getTime() + (delayMinutes * 60000)); // Ajouter les minutes de retard - - return realTime.toLocaleTimeString('fr-FR', { - hour: '2-digit', - minute: '2-digit' + + return realTime.toLocaleTimeString('fr-FR', { + hour: '2-digit', + minute: '2-digit' }); } + /** + * Calcule la couleur du train en fonction du retard + * @param {number} delayMinutes - Le nombre de minutes de retard + * @param {boolean} hasDelay - Indique si le train a du retard ou non + * @returns {string} La couleur correspondante + */ getTrainColor(delayMinutes, hasDelay) { if (!hasDelay || delayMinutes === 0) return '#4caf50'; // Vert à l'heure return '#f44336'; // Rouge en retard (peu importe le nombre de minutes) } + /** + * Méthode héritée
+ * Génération du rendu de l'ensemble de la carte, incluant le css et l'html + */ async render() { if (!this._hass || !this.config) { return; @@ -272,7 +488,7 @@ class SncfTrainCard extends HTMLElement { this._lastRenderTime = now; const trains = await this.getTrainEntities(); - + if (trains.length === 0) { this.shadowRoot.innerHTML = ` @@ -378,7 +594,7 @@ class SncfTrainCard extends HTMLElement { filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3)); } - .train-emoji-axial-symmetry { + .train-emoji-axial-symmetry-true { transform: translateX(-50%) scaleX(-1); } @@ -387,7 +603,6 @@ class SncfTrainCard extends HTMLElement { flex-direction: row; align-items: center; gap: 8px; - min-width: 120px; } .station-emoji { @@ -459,54 +674,105 @@ class SncfTrainCard extends HTMLElement { `; } + /** + * Rendu des lignes de train en fonction des données fournies, en calculant la position de chaque train sur la barre de progression, en affichant les informations de départ et d'arrivée selon la configuration, et en appliquant des styles différents pour les trains en retards. + * @param {Array} trains - Un tableau d'entités de train à afficher, avec leurs attributs contenant les informations nécessaires pour le rendu + * @param {Date} currentTime - L'heure actuelle à utiliser pour le calcul de la position des trains, ce qui permet de faire avancer les trains vers la droite à mesure que l'heure de départ approche, et d'afficher les informations de retard de manière dynamique en fonction du temps restant avant le départ + * @returns {string} Une chaîne HTML représentant la section complète du train + */ renderTrainLines(trains, currentTime) { return trains.map((train, index) => { const position = this.calculateTrainPosition(train.attributes.departure_time, currentTime); const delayMinutes = train.attributes.delay_minutes || 0; const hasDelay = train.attributes.has_delay || false; const trainColor = this.getTrainColor(delayMinutes, hasDelay); - const formattedTime = this.formatTime(train.attributes.departure_time); - const realArrivalTime = this.calculateRealArrivalTime(train.attributes.departure_time, delayMinutes); - - const html = ` + + let trainPositionHTML = '' + if (position >= 0 && position <= 100) { + trainPositionHTML = ` +
+ ${this.config.train_emoji} +
+ ` + } + + return `
+ ${this.config.show_departure_station ? this.renderDeparture(train.attributes) : ''} +
- ${position >= 0 && position <= 100 ? ` -
- ${this.config.train_emoji} -
- ` : ` - - `} + ${trainPositionHTML}
-
-
${this.config.train_station_emoji}
-
-
- ${hasDelay && realArrivalTime ? ` -
${formattedTime}
-
${realArrivalTime}
- ` : ` -
${formattedTime}
- `} -
-
- ${hasDelay ? `+${delayMinutes}min` : 'À l\'heure'} -
-
-
+ ${this.config.show_arrival_station ? this.renderArrival(train.attributes) : ''}
`; - - return html; }).join(''); } - getCardSize() { - return Math.max(3, this.config.train_lines + 1); + /** + * Rendu de la section de départ pour un train donné, en affichant l'heure de départ prévue, l'heure de départ réelle si le train a du retard. + * @param {object} trainAttributes - Les attributs du train + * @returns {string} Une chaîne HTML représentant la section de départ du train + */ + renderDeparture(trainAttributes) { + const hasDelay = trainAttributes.has_delay || false; + const delayMinutes = trainAttributes.delay_minutes || 0; + const departureTime = this.formatTime(trainAttributes.base_departure_time); + const realDepartureTime = this.formatTime(trainAttributes.departure_time); + + return ` +
+
+
+ ${hasDelay && realDepartureTime ? ` +
${departureTime}
+
${realDepartureTime}
+ ` : ` +
${departureTime}
+ `} +
+
+ ${hasDelay ? `+${delayMinutes}min` : 'À l\'heure'} +
+
+
${this.config.departure_station_emoji}
+
+ ` + } + + /** + * Rendu de la section d'arrivée pour un train donné, en affichant l'heure d'arrivée prévue, l'heure d'arrivée réelle si le train a du retard. + * @param {object} trainAttributes - Les attributs du train + * @returns {string} Une chaîne HTML représentant la section d'arrivée du train + */ + renderArrival(trainAttributes) { + const hasDelay = trainAttributes.has_delay || false; + const delayMinutes = trainAttributes.delay_minutes || 0; + const arrivalTime = this.formatTime(trainAttributes.base_arrival_time); + const realArrivalTime = this.formatTime(trainAttributes.arrival_time); + + return ` +
+
${this.config.arrival_station_emoji}
+
+
+ ${hasDelay && realArrivalTime ? ` +
${arrivalTime}
+
${realArrivalTime}
+ ` : ` +
${arrivalTime}
+ `} +
+
+ ${hasDelay ? `+${delayMinutes}min` : 'À l\'heure'} +
+
+
+ `; } + } // Définir l'élément custom From 9819addf29b4c8d80229724266c1e617d43481fb Mon Sep 17 00:00:00 2001 From: Pierre-Alexandre MARTIN Date: Fri, 15 May 2026 10:28:04 +0200 Subject: [PATCH 2/4] Fix de l'affichage des trains sur la barre --- .../sncf_trains/www/sncf-train-card.js | 60 +++++++++++-------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/custom_components/sncf_trains/www/sncf-train-card.js b/custom_components/sncf_trains/www/sncf-train-card.js index fe9ee0e..04638f1 100644 --- a/custom_components/sncf_trains/www/sncf-train-card.js +++ b/custom_components/sncf_trains/www/sncf-train-card.js @@ -320,17 +320,25 @@ class SncfTrainCard extends HTMLElement { }) .filter(entity => entity?.attributes?.departure_time); + // Source - https://stackoverflow.com/a/1214753 + // Posted by Kip, modified by community. See post 'Timeline' for change history + // Retrieved 2026-05-15, License - CC BY-SA 4.0 + const addMinutes = (date, minutes) => { + return new Date(date.getTime() + minutes*60000); + } + // Filtrer les trains qui ne sont pas encore passés const currentTime = new Date(); const upcomingTrains = trainEntities.filter(entity => { - const departureTime = this.parseTime(entity.attributes.departure_time); - return departureTime >= currentTime; + // TODO : paramétrer le temps d'affichage max d'un train arrivé en gare + const arrivalTime = addMinutes(this.parseTime(entity.attributes.arrival_time), 30); + return arrivalTime >= currentTime; }); return upcomingTrains .sort((a, b) => { - const aTime = this.parseTime(a.attributes.departure_time); - const bTime = this.parseTime(b.attributes.departure_time); + const aTime = this.parseTime(a.attributes.arrival_time); + const bTime = this.parseTime(b.attributes.arrival_time); return aTime - bTime; }) .slice(0, this.config.train_lines); @@ -381,40 +389,41 @@ class SncfTrainCard extends HTMLElement { /** * Calcule la position du train sur la barre de progression en fonction de l'heure actuelle et de l'heure de départ, en affichant le train 30 minutes avant le départ et en le faisant avancer vers la droite à mesure que l'heure de départ approche, ce qui crée une animation visuelle intuitive pour les utilisateurs afin de suivre l'approche du train vers la gare, et retourne une position en pourcentage (0% = train à gauche, 100% = train arrivé) ou une valeur négative pour indiquer que le train n'est pas encore visible, ce qui permet de gérer l'affichage du train de manière dynamique en fonction du temps restant avant le départ - * @param {string} departureTime - La chaîne de temps de départ à utiliser pour calculer la position du train, qui doit être au format reconnu par la méthode parseTime - * @param {Date} [currentTime] - L'heure actuelle à utiliser pour le calcul, qui peut être fournie pour les tests ou les rendus spécifiques, sinon la date actuelle sera utilisée par défaut + * @param {object} trainAttributes - Les attributs du train, qui doivent inclure au minimum une heure de départ valide pour que le calcul fonctionne correctement, et peuvent inclure d'autres informations pour personnaliser l'affichage * @returns {number} Un nombre représentant la position du train en pourcentage (0-100) ou une valeur négative si le train n'est pas encore visible */ - calculateTrainPosition(departureTime, currentTime) { - if (!departureTime) { + calculateTrainPosition(trainAttributes) { + if (!trainAttributes.departure_time || !trainAttributes.arrival_time) { return -10; } - const departure = this.parseTime(departureTime); + const departure = this.parseTime(trainAttributes.departure_time); + const arrival = this.parseTime(trainAttributes.arrival_time); + const travelTime = (arrival - departure) / (1000 * 60); - if (Number.isNaN(departure.getTime())) { + if (Number.isNaN(departure.getTime()) || Number.isNaN(arrival.getTime()) || travelTime < 0) { return -10; } - const now = currentTime || new Date(); - const diffMinutes = (departure - now) / (1000 * 60); + const now = new Date(); + const diffMinutes = (arrival - now) / (1000 * 60); - // Train apparaît 30 minutes avant l'heure - // todo : tester et s'assurer de la véracité / nom du param animation_duration - const maxMinutes = this.config.animation_duration; - - if (diffMinutes > maxMinutes) { - if(maxMinutes === 0) { + if (diffMinutes > travelTime) { + // todo : tester et s'assurer de la véracité / nom du param animation_duration + if (this.config.animation_duration === 0 || this.config.animation_duration > diffMinutes - travelTime) { + // Train apparaît X minutes avant l'heure return 0; } - return -10; // Hors de la barre + // Hors de la barre + return -10; } if (diffMinutes <= 0) { - return 100; // Arrivé à la gare + // Arrivé à la gare + return 100; } // Position sur la barre (0% = gauche, 100% = droite) - return ((maxMinutes - diffMinutes) / maxMinutes) * 100; + return ((travelTime - diffMinutes) / travelTime) * 100; } /** @@ -500,9 +509,7 @@ class SncfTrainCard extends HTMLElement { return; } - const currentTime = new Date(); - - const trainLinesHTML = this.renderTrainLines(trains, currentTime); + const trainLinesHTML = this.renderTrainLines(trains); this.shadowRoot.innerHTML = ` - - -
-
-
${this.config.title}
-
- - ${trainLinesHTML} - -
-
- `; - } - - /** - * Rendu des lignes de train en fonction des données fournies, en calculant la position de chaque train sur la barre de progression, en affichant les informations de départ et d'arrivée selon la configuration, et en appliquant des styles différents pour les trains en retards. - * @param {Array} trains - Un tableau d'entités de train à afficher, avec leurs attributs contenant les informations nécessaires pour le rendu - * @param {Date} currentTime - L'heure actuelle à utiliser pour le calcul de la position des trains, ce qui permet de faire avancer les trains vers la droite à mesure que l'heure de départ approche, et d'afficher les informations de retard de manière dynamique en fonction du temps restant avant le départ - * @returns {string} Une chaîne HTML représentant la section complète du train - */ - renderTrainLines(trains) { - return trains.map((train, index) => { - const position = this.calculateTrainPosition(train.attributes); - const delayMinutes = train.attributes.delay_minutes || 0; - const hasDelay = train.attributes.has_delay || false; - const trainColor = this.getTrainColor(delayMinutes, hasDelay); - - let trainPositionHTML = '' - if (position >= 0 && position <= 100) { - trainPositionHTML = ` -
- ${this.config.train_emoji} -
- ` - } - - return ` -
- ${this.config.show_departure_station ? this.renderDeparture(train.attributes) : ''} - - -
- ${trainPositionHTML} -
- - ${this.config.show_arrival_station ? this.renderArrival(train.attributes) : ''} -
- `; - }).join(''); - } - - /** - * Rendu de la section de départ pour un train donné, en affichant l'heure de départ prévue, l'heure de départ réelle si le train a du retard. - * @param {object} trainAttributes - Les attributs du train - * @returns {string} Une chaîne HTML représentant la section de départ du train - */ - renderDeparture(trainAttributes) { - const hasDelay = trainAttributes.has_delay || false; - const delayMinutes = trainAttributes.delay_minutes || 0; - const departureTime = this.formatTime(trainAttributes.base_departure_time); - const realDepartureTime = this.formatTime(trainAttributes.departure_time); - - return ` -
-
-
- ${hasDelay && realDepartureTime ? ` -
${departureTime}
-
${realDepartureTime}
- ` : ` -
${departureTime}
- `} -
-
- ${hasDelay ? `+${delayMinutes}min` : 'À l\'heure'} -
-
-
${this.config.departure_station_emoji}
-
- ` - } - - /** - * Rendu de la section d'arrivée pour un train donné, en affichant l'heure d'arrivée prévue, l'heure d'arrivée réelle si le train a du retard. - * @param {object} trainAttributes - Les attributs du train - * @returns {string} Une chaîne HTML représentant la section d'arrivée du train - */ - renderArrival(trainAttributes) { - const hasDelay = trainAttributes.has_delay || false; - const delayMinutes = trainAttributes.delay_minutes || 0; - const arrivalTime = this.formatTime(trainAttributes.base_arrival_time); - const realArrivalTime = this.formatTime(trainAttributes.arrival_time); - - return ` -
-
${this.config.arrival_station_emoji}
-
-
- ${hasDelay && realArrivalTime ? ` -
${arrivalTime}
-
${realArrivalTime}
- ` : ` -
${arrivalTime}
- `} -
-
- ${hasDelay ? `+${delayMinutes}min` : 'À l\'heure'} -
-
-
`; } From ce4c0e9974861c683b88c41c4a9368b2fde107c8 Mon Sep 17 00:00:00 2001 From: Pierre-Alexandre MARTIN Date: Fri, 15 May 2026 23:26:55 +0200 Subject: [PATCH 4/4] =?UTF-8?q?Gestion=20des=20ic=C3=B4nes=20g=C3=A9n?= =?UTF-8?q?=C3=A9riques=20HA=20(ex=20:=20mdi:home)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sncf_trains/www/sncf-train-card.js | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/custom_components/sncf_trains/www/sncf-train-card.js b/custom_components/sncf_trains/www/sncf-train-card.js index d6b9695..4e40394 100644 --- a/custom_components/sncf_trains/www/sncf-train-card.js +++ b/custom_components/sncf_trains/www/sncf-train-card.js @@ -409,7 +409,7 @@ class SncfTrainCard extends HTMLElement { const diffMinutes = (arrival - now) / (1000 * 60); if (diffMinutes > travelTime) { - // todo : tester et s'assurer de la véracité / nom du param animation_duration + // TODO : tester et s'assurer de la véracité / nom du param animation_duration if (this.config.animation_duration === 0 || this.config.animation_duration > diffMinutes - travelTime) { // Train apparaît X minutes avant l'heure return 0; @@ -525,6 +525,18 @@ class SncfTrainCard extends HTMLElement { `; } + /** + * Rendu des icônes en fonction de la configuration, en vérifiant si l'icône est un emoji simple ou une icône HA (mdi:, fa:, ic:, ...), et en retournant le HTML approprié pour chaque cas. + * @param icone - La chaîne de caractères représentant l'icône configurée, qui peut être un emoji simple ou une icône HA avec un préfixe spécifique, et qui doit être traitée différemment pour s'assurer qu'elle s'affiche correctement dans la carte + * @return {string} Une chaîne HTML représentant l'icône à afficher, soit en utilisant la balise pour les icônes HA, soit en affichant directement l'emoji pour les emojis simples, ce qui permet de gérer une grande variété d'icônes de manière flexible et personnalisable + */ + renderIcone(icone) { + if (icone?.includes(':')) { + return ``; + } + return icone; + } + /** * Rendu des lignes de train en fonction des données fournies, en calculant la position de chaque train sur la barre de progression, en affichant les informations de départ et d'arrivée selon la configuration, et en appliquant des styles différents pour les trains en retards. * @param {Array} trains - Un tableau d'entités de train à afficher, avec leurs attributs contenant les informations nécessaires pour le rendu @@ -540,24 +552,18 @@ class SncfTrainCard extends HTMLElement { const isArrived = new Date() > this.parseTime(TA.arrival_time) const trainColor = this.getTrainColor(delayMinutes, hasDelay); - let trainPositionHTML = '' - if (position >= 0 && position <= 100) { - trainPositionHTML = ` -
- ${this.config.train_emoji} -
- ` - } - const theme = isArrived ? 'arrived' : hasDelay ? 'delayed' : isRunning ? 'running' : ''; return `
${this.config.show_departure_station ? this.renderDeparture(TA) : ''} -
- ${trainPositionHTML} + ${ position >= 0 ? + `
+ ${this.renderIcone(this.config.train_emoji)} +
` : '' + }
${this.config.show_arrival_station ? this.renderArrival(TA) : ''} @@ -593,7 +599,7 @@ class SncfTrainCard extends HTMLElement { ${hasDelay ? `+${delayMinutes}min` : isGone ? 'Parti' : 'À l\'heure'}
-
${this.config.departure_station_emoji}
+
${this.renderIcone(this.config.departure_station_emoji)}
` } @@ -612,7 +618,7 @@ class SncfTrainCard extends HTMLElement { return `
-
${this.config.arrival_station_emoji}
+
${this.renderIcone(this.config.arrival_station_emoji)}
${hasDelay && realArrivalTime ? ` @@ -755,7 +761,7 @@ class SncfTrainCard extends HTMLElement { z-index: 10; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3)); } - + .train-emoji-axial-symmetry-true { transform: translateX(-50%) scaleX(-1); } @@ -771,7 +777,7 @@ class SncfTrainCard extends HTMLElement { font-size: 1.8em; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2)); } - + .station-info { display: flex; flex-direction: column;