Kilometrage des vehicules : releves km, km par kWh, capacite estimee (tables, vues, Grafana, formulaire HA)
This commit is contained in:
@@ -205,6 +205,32 @@ views:
|
|||||||
icon: mdi:calendar-clock
|
icon: mdi:calendar-clock
|
||||||
icon_color: cyan
|
icon_color: cyan
|
||||||
|
|
||||||
|
- type: entities
|
||||||
|
title: Relevé kilométrique (km par kWh et usure des batteries)
|
||||||
|
show_header_toggle: false
|
||||||
|
entities:
|
||||||
|
- entity: input_select.evcs_releve_vehicule
|
||||||
|
name: Véhicule
|
||||||
|
- entity: input_number.evcs_releve_km_valeur
|
||||||
|
name: Compteur (km)
|
||||||
|
- entity: input_number.evcs_releve_niveau
|
||||||
|
name: Niveau batterie (%, -1 = inconnu)
|
||||||
|
- type: button
|
||||||
|
name: Enregistrer le relevé
|
||||||
|
icon: mdi:content-save
|
||||||
|
action_name: Enregistrer
|
||||||
|
tap_action:
|
||||||
|
action: perform-action
|
||||||
|
perform_action: script.evcs_enregistrer_releve_km
|
||||||
|
- type: section
|
||||||
|
label: "Ou par WhatsApp : « JEEP 112694 km 54 % » — toujours avec le niveau"
|
||||||
|
- entity: input_number.victron_evcs_autonomie_jeep_km
|
||||||
|
name: Autonomie de référence JEEP
|
||||||
|
- entity: input_number.victron_evcs_autonomie_vw_km
|
||||||
|
name: Autonomie de référence VW
|
||||||
|
- entity: input_number.victron_evcs_autonomie_zero_km
|
||||||
|
name: Autonomie de référence ZERO
|
||||||
|
|
||||||
- type: entities
|
- type: entities
|
||||||
title: Paramètres utilisateur
|
title: Paramètres utilisateur
|
||||||
show_header_toggle: false
|
show_header_toggle: false
|
||||||
|
|||||||
@@ -579,6 +579,557 @@
|
|||||||
"show": false
|
"show": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "xychart",
|
||||||
|
"pluginVersion": "11.3.0",
|
||||||
|
"title": "Kilometres contre kWh : le nuage des trajets",
|
||||||
|
"description": "Un point = l'intervalle entre deux releves du compteur d'un vehicule. kWh consommes = kWh livres par la borne entre les deux releves + (niveau au 1er - niveau au 2nd) x capacite. Les droites sont les references donnees par le proprietaire (JEEP 50 km, VW 42 km, ZERO 200 km pour une charge pleine). Sous la droite : on a roule moins loin par kWh (conduite, froid, chauffage — ou batterie qui s'use). Les intervalles d'une hybride finis batterie vide sont ecartes (km essence possibles).",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 24,
|
||||||
|
"w": 14,
|
||||||
|
"h": 11
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"editorMode": "code",
|
||||||
|
"rawSql": "WITH i AS (SELECT * FROM km_intervalle WHERE statut = 'valide'),\ncap AS (SELECT DISTINCT ON (vehicule) capacite_kwh AS c FROM releve_km\n WHERE capacite_kwh > 0 ORDER BY vehicule, ts DESC),\nx AS (SELECT round(kwh_consommes::numeric, 2)::float8 AS x, vehicule, km_parcourus::float8 AS km FROM i\n UNION ALL SELECT 0, NULL, NULL\n UNION ALL SELECT c::float8, NULL, NULL FROM cap)\nSELECT x AS \"kWh consommes\",\n CASE WHEN vehicule = 'JEEP' THEN km END AS \"JEEP\",\n CASE WHEN vehicule = 'VW' THEN km END AS \"VW\",\n CASE WHEN vehicule = 'ZERO' THEN km END AS \"ZERO\",\n round((x * (SELECT autonomie_ref_km / capacite_kwh FROM releve_km\n WHERE vehicule = 'JEEP' AND capacite_kwh > 0 AND autonomie_ref_km > 0 ORDER BY ts DESC LIMIT 1))::numeric, 1)::float8 AS \"Reference JEEP\",\n round((x * (SELECT autonomie_ref_km / capacite_kwh FROM releve_km\n WHERE vehicule = 'VW' AND capacite_kwh > 0 AND autonomie_ref_km > 0 ORDER BY ts DESC LIMIT 1))::numeric, 1)::float8 AS \"Reference VW\",\n round((x * (SELECT autonomie_ref_km / capacite_kwh FROM releve_km\n WHERE vehicule = 'ZERO' AND capacite_kwh > 0 AND autonomie_ref_km > 0 ORDER BY ts DESC LIMIT 1))::numeric, 1)::float8 AS \"Reference ZERO\"\nFROM x ORDER BY 1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "lengthkm",
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"show": "points",
|
||||||
|
"pointSize": {
|
||||||
|
"fixed": 10
|
||||||
|
},
|
||||||
|
"pointShape": "circle",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"fillOpacity": 60
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "JEEP"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "green"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "VW"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "blue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "ZERO"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "purple"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Reference JEEP"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "green"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.show",
|
||||||
|
"value": "lines"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.lineStyle",
|
||||||
|
"value": {
|
||||||
|
"fill": "dash",
|
||||||
|
"dash": [
|
||||||
|
10,
|
||||||
|
10
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Reference VW"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "blue"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.show",
|
||||||
|
"value": "lines"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.lineStyle",
|
||||||
|
"value": {
|
||||||
|
"fill": "dash",
|
||||||
|
"dash": [
|
||||||
|
10,
|
||||||
|
10
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Reference ZERO"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "purple"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.show",
|
||||||
|
"value": "lines"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.lineStyle",
|
||||||
|
"value": {
|
||||||
|
"fill": "dash",
|
||||||
|
"dash": [
|
||||||
|
10,
|
||||||
|
10
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"mapping": "manual",
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"frame": {
|
||||||
|
"matcher": {
|
||||||
|
"id": "byIndex",
|
||||||
|
"options": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x": {
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "kWh consommes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"y": {
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "JEEP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": {
|
||||||
|
"matcher": {
|
||||||
|
"id": "byIndex",
|
||||||
|
"options": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x": {
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "kWh consommes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"y": {
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "VW"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": {
|
||||||
|
"matcher": {
|
||||||
|
"id": "byIndex",
|
||||||
|
"options": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x": {
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "kWh consommes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"y": {
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "ZERO"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": {
|
||||||
|
"matcher": {
|
||||||
|
"id": "byIndex",
|
||||||
|
"options": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x": {
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "kWh consommes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"y": {
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Reference JEEP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": {
|
||||||
|
"matcher": {
|
||||||
|
"id": "byIndex",
|
||||||
|
"options": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x": {
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "kWh consommes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"y": {
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Reference VW"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": {
|
||||||
|
"matcher": {
|
||||||
|
"id": "byIndex",
|
||||||
|
"options": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x": {
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "kWh consommes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"y": {
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Reference ZERO"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"legend": {
|
||||||
|
"showLegend": true,
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"title": "Les intervalles entre releves",
|
||||||
|
"description": "Tous les intervalles, y compris ceux ecartes du nuage, avec la raison. « niveau manquant » : redonner le releve avec le niveau (« JEEP 112694 km 54 % »).",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"editorMode": "code",
|
||||||
|
"rawSql": "SELECT vehicule AS \"Vehicule\",\n to_char(debut AT TIME ZONE 'Europe/Paris', 'Dy DD/MM HH24:MI') AS \"Du\",\n to_char(fin AT TIME ZONE 'Europe/Paris', 'Dy DD/MM HH24:MI') AS \"Au\",\n km_parcourus AS \"km\",\n round(kwh_charges::numeric, 2) AS \"kWh borne\",\n niveau_debut || ' -> ' || niveau_fin || ' %' AS \"Niveau\",\n round(kwh_consommes::numeric, 2) AS \"kWh consommes\",\n round(km_par_kwh::numeric, 1) AS \"km/kWh\",\n round(km_par_kwh_ref::numeric, 1) AS \"Reference\",\n statut AS \"Statut\"\nFROM km_intervalle ORDER BY fin DESC"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gridPos": {
|
||||||
|
"x": 14,
|
||||||
|
"y": 24,
|
||||||
|
"w": 10,
|
||||||
|
"h": 11
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"align": "left",
|
||||||
|
"cellOptions": {
|
||||||
|
"type": "auto"
|
||||||
|
},
|
||||||
|
"filterable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "kWh borne"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "unit",
|
||||||
|
"value": "kwatth"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "kWh consommes"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "unit",
|
||||||
|
"value": "kwatth"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"showHeader": true,
|
||||||
|
"cellHeight": "sm",
|
||||||
|
"footer": {
|
||||||
|
"show": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Capacite estimee des batteries (usure)",
|
||||||
|
"description": "Un point = un branchement ou le niveau a ete donne et la charge menee au plein (ou suivie d'un releve « debranche a NN % ») : capacite = kWh livres par la borne depuis le branchement / part manquante. Ce sont des kWh AU COMPTEUR (pertes du chargeur comprises). Une pente descendante sur des mois = usure de la batterie. Le niveau saisi est un entier : sur une petite charge, 1 point d'erreur pese lourd.",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"format": "time_series",
|
||||||
|
"rawQuery": true,
|
||||||
|
"editorMode": "code",
|
||||||
|
"rawSql": "SELECT debranche AS time,\n CASE WHEN vehicule = 'JEEP' THEN capacite_kwh END AS \"JEEP\",\n CASE WHEN vehicule = 'VW' THEN capacite_kwh END AS \"VW\",\n CASE WHEN vehicule = 'ZERO' THEN capacite_kwh END AS \"ZERO\"\nFROM capacite_estimee WHERE capacite_kwh IS NOT NULL AND $__timeFilter(debranche)\nORDER BY 1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 35,
|
||||||
|
"w": 14,
|
||||||
|
"h": 9
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "kwatth",
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"lineWidth": 2,
|
||||||
|
"fillOpacity": 8,
|
||||||
|
"showPoints": "always",
|
||||||
|
"spanNulls": false,
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"gradientMode": "opacity",
|
||||||
|
"drawStyle": "points",
|
||||||
|
"pointSize": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "JEEP"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "green"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "VW"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "blue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "ZERO"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "purple"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true,
|
||||||
|
"calcs": []
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "multi",
|
||||||
|
"sort": "desc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"title": "Les releves du compteur",
|
||||||
|
"description": "Releves recus par WhatsApp (concierge) ou par le tableau EVCS Master. Un releve fait au branchement est date du branchement : le compteur n'avance pas pendant la charge.",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"editorMode": "code",
|
||||||
|
"rawSql": "SELECT to_char(ts AT TIME ZONE 'Europe/Paris', 'Dy DD/MM HH24:MI') AS \"Quand\",\n vehicule AS \"Vehicule\", km AS \"Compteur\",\n coalesce(niveau_pct || ' %', '?') AS \"Niveau\", source AS \"Source\"\nFROM releve_km ORDER BY ts DESC"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gridPos": {
|
||||||
|
"x": 14,
|
||||||
|
"y": 35,
|
||||||
|
"w": 10,
|
||||||
|
"h": 9
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"align": "left",
|
||||||
|
"cellOptions": {
|
||||||
|
"type": "auto"
|
||||||
|
},
|
||||||
|
"filterable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Compteur"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "unit",
|
||||||
|
"value": "lengthkm"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"showHeader": true,
|
||||||
|
"cellHeight": "sm",
|
||||||
|
"footer": {
|
||||||
|
"show": false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
89
packages/evcs_kilometrage.yaml
Normal file
89
packages/evcs_kilometrage.yaml
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# =====================================================
|
||||||
|
# EVCS — KILOMETRAGE DES VEHICULES (04/10/2026, demande du proprietaire)
|
||||||
|
# Releves du compteur kilometrique + niveau, pour mesurer les km par kWh et,
|
||||||
|
# avec les charges menees au plein, la capacite (usure) de chaque batterie.
|
||||||
|
# Calcul : service d'historisation (tools/kilometrage.mjs), Grafana « Les
|
||||||
|
# vehicules ». ⚠️ JEEP et VW hybrides : donner le NIVEAU avec le compteur,
|
||||||
|
# un intervalle fini batterie vide (essence possible) est ecarte.
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
input_text:
|
||||||
|
# Tableau JSON des derniers releves [vehicule, km, niveau|null, epoch_s, source],
|
||||||
|
# ecrit par le concierge WhatsApp ou par le script ci-dessous. Ne pas editer.
|
||||||
|
evcs_releve_km:
|
||||||
|
name: EVCS - Relevés kilométriques (file)
|
||||||
|
max: 255
|
||||||
|
icon: mdi:counter
|
||||||
|
|
||||||
|
input_select:
|
||||||
|
evcs_releve_vehicule:
|
||||||
|
name: "EVCS - Relevé km : véhicule"
|
||||||
|
options:
|
||||||
|
- JEEP
|
||||||
|
- VW
|
||||||
|
- ZERO
|
||||||
|
icon: mdi:car-multiple
|
||||||
|
|
||||||
|
input_number:
|
||||||
|
evcs_releve_km_valeur:
|
||||||
|
name: "EVCS - Relevé km : compteur"
|
||||||
|
min: 0
|
||||||
|
max: 2000000
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: km
|
||||||
|
mode: box
|
||||||
|
icon: mdi:counter
|
||||||
|
# -1 = niveau inconnu (le releve compte, mais l'intervalle restera « niveau manquant »)
|
||||||
|
evcs_releve_niveau:
|
||||||
|
name: "EVCS - Relevé km : niveau batterie (-1 = inconnu)"
|
||||||
|
min: -1
|
||||||
|
max: 100
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: "%"
|
||||||
|
mode: box
|
||||||
|
icon: mdi:battery-50
|
||||||
|
# Autonomie de reference d'une charge pleine, donnee par le proprietaire
|
||||||
|
# (04/10/2026) : droite de reference du nuage km / kWh.
|
||||||
|
victron_evcs_autonomie_jeep_km:
|
||||||
|
name: EVCS - Autonomie de référence JEEP
|
||||||
|
min: 0
|
||||||
|
max: 1000
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: km
|
||||||
|
mode: box
|
||||||
|
victron_evcs_autonomie_vw_km:
|
||||||
|
name: EVCS - Autonomie de référence VW
|
||||||
|
min: 0
|
||||||
|
max: 1000
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: km
|
||||||
|
mode: box
|
||||||
|
victron_evcs_autonomie_zero_km:
|
||||||
|
name: EVCS - Autonomie de référence ZERO
|
||||||
|
min: 0
|
||||||
|
max: 1000
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: km
|
||||||
|
mode: box
|
||||||
|
|
||||||
|
script:
|
||||||
|
evcs_enregistrer_releve_km:
|
||||||
|
alias: "EVCS — enregistrer le relevé kilométrique"
|
||||||
|
mode: queued
|
||||||
|
icon: mdi:content-save
|
||||||
|
sequence:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ states('input_number.evcs_releve_km_valeur') | int(0) > 0 }}"
|
||||||
|
- service: input_text.set_value
|
||||||
|
target:
|
||||||
|
entity_id: input_text.evcs_releve_km
|
||||||
|
data:
|
||||||
|
value: >
|
||||||
|
{% set brut = states('input_text.evcs_releve_km') %}
|
||||||
|
{% set tab = brut | from_json if brut.startswith('[') else [] %}
|
||||||
|
{% set n = states('input_number.evcs_releve_niveau') | int(-1) %}
|
||||||
|
{% set e = [states('input_select.evcs_releve_vehicule'),
|
||||||
|
states('input_number.evcs_releve_km_valeur') | int,
|
||||||
|
n if n >= 0 else none,
|
||||||
|
now().timestamp() | int, 'ha'] %}
|
||||||
|
{{ (tab[-4:] + [e]) | to_json }}
|
||||||
91
tests/kilometrage/run.mjs
Normal file
91
tests/kilometrage/run.mjs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
// Tests du kilometrage (04/10/2026).
|
||||||
|
// node tests/kilometrage/run.mjs -> fonctions pures
|
||||||
|
// node tests/kilometrage/run.mjs --vues -> vues SQL, dans une transaction
|
||||||
|
// ANNULEE (a lancer dans le
|
||||||
|
// conteneur d'historisation)
|
||||||
|
import { lireReleves, ajouterReleve, creerSchemaKm } from '../../tools/kilometrage.mjs';
|
||||||
|
|
||||||
|
let ok = 0, ko = 0;
|
||||||
|
const eq = (nom, a, b) => {
|
||||||
|
const sa = JSON.stringify(a), sb = JSON.stringify(b);
|
||||||
|
if (sa === sb) { ok++; } else { ko++; console.log(`ECHEC ${nom}\n obtenu ${sa}\n attendu ${sb}`); }
|
||||||
|
};
|
||||||
|
const proche = (nom, a, b, tol = 1e-6) => eq(nom, Math.abs(Number(a) - b) <= tol ? b : Number(a), b);
|
||||||
|
|
||||||
|
// --- lireReleves
|
||||||
|
const T = 1759515246;
|
||||||
|
eq('vide', lireReleves('').releves, []);
|
||||||
|
eq('unknown', lireReleves('unknown').releves, []);
|
||||||
|
eq('json illisible', lireReleves('[x').rejets, ['JSON illisible']);
|
||||||
|
const r1 = lireReleves(JSON.stringify([['jeep', 112694, 54, T, 'w'], ['ZERO', 16469, null, T, 'w']]));
|
||||||
|
eq('deux releves', r1.releves.map(r => [r.vehicule, r.km, r.niveau, r.ts.getTime() / 1000]),
|
||||||
|
[['JEEP', 112694, 54, T], ['ZERO', 16469, null, T]]);
|
||||||
|
eq('km decimal refuse', lireReleves(JSON.stringify([['VW', 1.5, null, T, 'w']])).rejets.length, 1);
|
||||||
|
eq('niveau 150 refuse', lireReleves(JSON.stringify([['VW', 1000, 150, T, 'w']])).rejets.length, 1);
|
||||||
|
eq('horodatage absurde refuse', lireReleves(JSON.stringify([['VW', 1000, null, 12, 'w']])).rejets.length, 1);
|
||||||
|
|
||||||
|
// --- ajouterReleve : garde les derniers, tient dans 255 caracteres
|
||||||
|
let b = '';
|
||||||
|
for (let i = 0; i < 10; i++) b = ajouterReleve(b, { vehicule: 'JEEP', km: 112694 + i, niveau: 54, ts: new Date((T + i) * 1000), source: 'whatsapp' });
|
||||||
|
eq('255 car max', b.length <= 255, true);
|
||||||
|
eq('dernier garde', JSON.parse(b).at(-1)[1], 112703);
|
||||||
|
eq('helper corrompu repart a vide', JSON.parse(ajouterReleve('pas du json', { vehicule: 'VW', km: 5, niveau: null, ts: new Date(T * 1000), source: 'ha' })).length, 1);
|
||||||
|
|
||||||
|
if (process.argv.includes('--vues')) {
|
||||||
|
const pg = (await import('pg')).default;
|
||||||
|
const c = new pg.Client(); await c.connect();
|
||||||
|
try {
|
||||||
|
await c.query('BEGIN');
|
||||||
|
await creerSchemaKm(c);
|
||||||
|
// Vehicules fictifs, annee 2020 : rien ne se melange au reel, et tout est annule.
|
||||||
|
const ins = (v, ts, km, n, cap, auto, elec) => c.query(
|
||||||
|
'INSERT INTO releve_km (vehicule, ts, km, niveau_pct, source, capacite_kwh, autonomie_ref_km, tout_electrique) VALUES ($1,$2,$3,$4,\'test\',$5,$6,$7)',
|
||||||
|
[v, ts, km, n, cap, auto, elec]);
|
||||||
|
await ins('TESTH', '2020-01-01T18:00Z', 1000, 54, 8.7, 50, false);
|
||||||
|
await ins('TESTH', '2020-01-02T18:00Z', 1030, 20, 8.7, 50, false); // pas de charge : 34 % x 8,7 = 2,958 kWh
|
||||||
|
await ins('TESTH', '2020-01-04T18:00Z', 1080, 0, 8.7, 50, false); // batterie vide : essence possible
|
||||||
|
await ins('TESTE', '2020-01-01T18:00Z', 500, 80, 15, 200, true);
|
||||||
|
await ins('TESTE', '2020-01-03T18:00Z', 600, 0, 15, 200, true); // electrique : 0 % reste valide
|
||||||
|
await c.query(`INSERT INTO session_charge (debut, fin, terminee, vehicule, vehicule_sur, energie_kwh, energie_station_kwh)
|
||||||
|
VALUES ('2020-01-03T01:00Z','2020-01-03T03:00Z',true,'TESTH',true,4.0,4.2)`);
|
||||||
|
const { rows } = await c.query("SELECT * FROM km_intervalle WHERE vehicule LIKE 'TEST%' ORDER BY vehicule, debut");
|
||||||
|
const h1 = rows.find(r => r.vehicule === 'TESTH' && r.km_parcourus === 30);
|
||||||
|
proche('H1 kWh consommes = 0 + 0,34 x 8,7', h1.kwh_consommes, 2.958);
|
||||||
|
proche('H1 km/kWh', h1.km_par_kwh, 30 / 2.958);
|
||||||
|
proche('ref km/kWh = 50/8,7', h1.km_par_kwh_ref, 50 / 8.7);
|
||||||
|
eq('H1 valide', h1.statut, 'valide');
|
||||||
|
const h2 = rows.find(r => r.vehicule === 'TESTH' && r.km_parcourus === 50);
|
||||||
|
proche('H2 charge de la borne comptee (4,2)', h2.kwh_charges, 4.2);
|
||||||
|
eq('H2 hybride vide au retour', h2.statut, 'batterie vide au retour : km essence possibles');
|
||||||
|
const e1 = rows.find(r => r.vehicule === 'TESTE');
|
||||||
|
proche('E1 kWh = 0,80 x 15', e1.kwh_consommes, 12);
|
||||||
|
eq('E1 electrique a 0 % valide', e1.statut, 'valide');
|
||||||
|
|
||||||
|
// Capacite : branchement 2020-02-01 10:00 -> 14:00, niveau 50 % saisi a
|
||||||
|
// 10:40 (compteur deja a 101,0 : la voiture chargeait), compteur 100,0 au
|
||||||
|
// branchement et 104,5 au debranchement, plein vu a 13:00.
|
||||||
|
const ev = (ts, apres) => c.query(`INSERT INTO evenement (ts, genre, source, resume, detail) VALUES ($1,'vehicule','test','test',$2)`,
|
||||||
|
[ts, JSON.stringify({ entite: 'binary_sensor.victron_evcs_vehicle_connected', apres })]);
|
||||||
|
const cpt = (ts, v) => c.query(`INSERT INTO mesure (ts, cle, valeur) VALUES ($1,'sensor.evcs_hq2239dnt7p_total_energy',$2)`, [ts, v]);
|
||||||
|
await ev('2020-02-01T10:00:00.123456Z', 'on'); await ev('2020-02-01T14:00Z', 'off');
|
||||||
|
await c.query(`INSERT INTO etiquette_branchement (branchement, label, source_airtria) VALUES ('2020-02-01T10:00:00.123Z','TESTH','test')`);
|
||||||
|
await c.query(`INSERT INTO niveau_saisi (saisi_a, niveau_pct, ref_kwh) VALUES ('2020-02-01T10:40Z', 50, 101.0)`);
|
||||||
|
await cpt('2020-02-01T09:59Z', 100.0); await cpt('2020-02-01T13:59Z', 104.5);
|
||||||
|
await c.query(`INSERT INTO decision (ts, etat) VALUES ('2020-02-01T13:00Z','day_vehicle_full_idle')`);
|
||||||
|
const capa = async () => (await c.query("SELECT * FROM capacite_estimee WHERE branche >= '2020-02-01' AND branche < '2020-02-02'")).rows[0];
|
||||||
|
let cap = await capa();
|
||||||
|
proche('energie depuis le branchement, pas la saisie', cap.kwh_livres, 4.5);
|
||||||
|
proche('capacite = 4,5 / 0,5', cap.capacite_kwh, 9.0);
|
||||||
|
eq('vehicule de l etiquette (horodatage a la microseconde pres)', cap.vehicule, 'TESTH');
|
||||||
|
await c.query(`INSERT INTO session_charge (debut, fin, terminee, vehicule, vehicule_sur, energie_kwh, energie_station_kwh)
|
||||||
|
VALUES ('2020-02-01T10:01Z','2020-02-01T13:00Z',true,'TESTX',true,4.4,4.5)`);
|
||||||
|
eq('la session confirmee prime sur l etiquette', (await capa()).vehicule, 'TESTX');
|
||||||
|
await c.query(`DELETE FROM decision WHERE ts = '2020-02-01T13:00Z'`);
|
||||||
|
cap = await capa();
|
||||||
|
eq('pas plein, pas de releve -> pas de capacite', cap.capacite_kwh, null);
|
||||||
|
await ins('TESTX', '2020-02-01T14:20Z', 777, 80, 15, 200, true);
|
||||||
|
proche('releve dans l heure : 4,5 / (80-50) %', (await capa()).capacite_kwh, 15.0);
|
||||||
|
} finally { await c.query('ROLLBACK'); await c.end(); }
|
||||||
|
}
|
||||||
|
console.log(`${ok}/${ok + ko}`);
|
||||||
|
process.exit(ko ? 1 : 0);
|
||||||
@@ -1350,6 +1350,101 @@ ORDER BY date_trunc(pas.unite, debut)"""
|
|||||||
COULEURS_VEHICULES = {"VW": "blue", "JEEP": "green", "ZERO": "purple",
|
COULEURS_VEHICULES = {"VW": "blue", "JEEP": "green", "ZERO": "purple",
|
||||||
"Invite": "yellow", "Non etiquete": "orange"}
|
"Invite": "yellow", "Non etiquete": "orange"}
|
||||||
|
|
||||||
|
# Kilometrage et capacite (04/10/2026) : vues km_intervalle et capacite_estimee
|
||||||
|
# du service d'historisation (tools/kilometrage.mjs). Aucune constante ici : les
|
||||||
|
# references (autonomie / capacite) viennent du releve, donc des helpers HA.
|
||||||
|
KM_REF = """(SELECT autonomie_ref_km / capacite_kwh FROM releve_km
|
||||||
|
WHERE vehicule = '{v}' AND capacite_kwh > 0 AND autonomie_ref_km > 0 ORDER BY ts DESC LIMIT 1)"""
|
||||||
|
KM_NUAGE = """WITH i AS (SELECT * FROM km_intervalle WHERE statut = 'valide'),
|
||||||
|
cap AS (SELECT DISTINCT ON (vehicule) capacite_kwh AS c FROM releve_km
|
||||||
|
WHERE capacite_kwh > 0 ORDER BY vehicule, ts DESC),
|
||||||
|
x AS (SELECT round(kwh_consommes::numeric, 2)::float8 AS x, vehicule, km_parcourus::float8 AS km FROM i
|
||||||
|
UNION ALL SELECT 0, NULL, NULL
|
||||||
|
UNION ALL SELECT c::float8, NULL, NULL FROM cap)
|
||||||
|
SELECT x AS "kWh consommes",
|
||||||
|
CASE WHEN vehicule = 'JEEP' THEN km END AS "JEEP",
|
||||||
|
CASE WHEN vehicule = 'VW' THEN km END AS "VW",
|
||||||
|
CASE WHEN vehicule = 'ZERO' THEN km END AS "ZERO",
|
||||||
|
round((x * """ + KM_REF.format(v="JEEP") + """)::numeric, 1)::float8 AS "Reference JEEP",
|
||||||
|
round((x * """ + KM_REF.format(v="VW") + """)::numeric, 1)::float8 AS "Reference VW",
|
||||||
|
round((x * """ + KM_REF.format(v="ZERO") + """)::numeric, 1)::float8 AS "Reference ZERO"
|
||||||
|
FROM x ORDER BY 1"""
|
||||||
|
|
||||||
|
KM_XY = {"type": "xychart", "pluginVersion": "11.3.0",
|
||||||
|
"title": "Kilometres contre kWh : le nuage des trajets",
|
||||||
|
"description": ("Un point = l'intervalle entre deux releves du compteur d'un vehicule. "
|
||||||
|
"kWh consommes = kWh livres par la borne entre les deux releves + "
|
||||||
|
"(niveau au 1er - niveau au 2nd) x capacite. Les droites sont les "
|
||||||
|
"references donnees par le proprietaire (JEEP 50 km, VW 42 km, ZERO 200 km "
|
||||||
|
"pour une charge pleine). Sous la droite : on a roule moins loin par kWh "
|
||||||
|
"(conduite, froid, chauffage — ou batterie qui s'use). Les intervalles "
|
||||||
|
"d'une hybride finis batterie vide sont ecartes (km essence possibles)."),
|
||||||
|
"datasource": PG, "gridPos": {"x": 0, "y": 24, "w": 14, "h": 11},
|
||||||
|
"targets": [sql("A", KM_NUAGE, fmt="table")],
|
||||||
|
"fieldConfig": {"defaults": {"unit": "lengthkm", "color": {"mode": "palette-classic"},
|
||||||
|
"custom": {"show": "points", "pointSize": {"fixed": 10}, "pointShape": "circle",
|
||||||
|
"lineWidth": 1, "fillOpacity": 60}},
|
||||||
|
"overrides": [sur(v, [{"id": "color", "value": {"mode": "fixed", "fixedColor": COULEURS_VEHICULES[v]}}])
|
||||||
|
for v in ["JEEP", "VW", "ZERO"]] +
|
||||||
|
[sur("Reference " + v, [{"id": "color", "value": {"mode": "fixed", "fixedColor": COULEURS_VEHICULES[v]}},
|
||||||
|
{"id": "custom.show", "value": "lines"},
|
||||||
|
{"id": "custom.lineStyle", "value": {"fill": "dash", "dash": [10, 10]}}])
|
||||||
|
for v in ["JEEP", "VW", "ZERO"]]},
|
||||||
|
"options": {"mapping": "manual",
|
||||||
|
"series": [{"frame": {"matcher": {"id": "byIndex", "options": 0}},
|
||||||
|
"x": {"matcher": {"id": "byName", "options": "kWh consommes"}},
|
||||||
|
"y": {"matcher": {"id": "byName", "options": n}}}
|
||||||
|
for n in ["JEEP", "VW", "ZERO", "Reference JEEP", "Reference VW", "Reference ZERO"]],
|
||||||
|
"legend": {"showLegend": True, "displayMode": "list", "placement": "bottom"},
|
||||||
|
"tooltip": {"mode": "single", "sort": "none"}}}
|
||||||
|
|
||||||
|
KM_INTERVALLES = table("Les intervalles entre releves", [sql("A",
|
||||||
|
"SELECT vehicule AS \"Vehicule\",\n"
|
||||||
|
" to_char(debut AT TIME ZONE 'Europe/Paris', 'Dy DD/MM HH24:MI') AS \"Du\",\n"
|
||||||
|
" to_char(fin AT TIME ZONE 'Europe/Paris', 'Dy DD/MM HH24:MI') AS \"Au\",\n"
|
||||||
|
" km_parcourus AS \"km\",\n"
|
||||||
|
" round(kwh_charges::numeric, 2) AS \"kWh borne\",\n"
|
||||||
|
" niveau_debut || ' -> ' || niveau_fin || ' %' AS \"Niveau\",\n"
|
||||||
|
" round(kwh_consommes::numeric, 2) AS \"kWh consommes\",\n"
|
||||||
|
" round(km_par_kwh::numeric, 1) AS \"km/kWh\",\n"
|
||||||
|
" round(km_par_kwh_ref::numeric, 1) AS \"Reference\",\n"
|
||||||
|
" statut AS \"Statut\"\n"
|
||||||
|
"FROM km_intervalle ORDER BY fin DESC", fmt="table")],
|
||||||
|
PG, 14, 24, 10, 11,
|
||||||
|
"Tous les intervalles, y compris ceux ecartes du nuage, avec la raison. "
|
||||||
|
"« niveau manquant » : redonner le releve avec le niveau (« JEEP 112694 km 54 % »).",
|
||||||
|
overrides=[sur("kWh borne", [{"id": "unit", "value": "kwatth"}]),
|
||||||
|
sur("kWh consommes", [{"id": "unit", "value": "kwatth"}])])
|
||||||
|
|
||||||
|
KM_CAPACITE = ts("Capacite estimee des batteries (usure)", [sql("A",
|
||||||
|
"SELECT debranche AS time,\n"
|
||||||
|
" CASE WHEN vehicule = 'JEEP' THEN capacite_kwh END AS \"JEEP\",\n"
|
||||||
|
" CASE WHEN vehicule = 'VW' THEN capacite_kwh END AS \"VW\",\n"
|
||||||
|
" CASE WHEN vehicule = 'ZERO' THEN capacite_kwh END AS \"ZERO\"\n"
|
||||||
|
"FROM capacite_estimee WHERE capacite_kwh IS NOT NULL AND $__timeFilter(debranche)\n"
|
||||||
|
"ORDER BY 1")],
|
||||||
|
PG, 0, 35, 14, 9, "kwatth",
|
||||||
|
"Un point = un branchement ou le niveau a ete donne et la charge menee au plein "
|
||||||
|
"(ou suivie d'un releve « debranche a NN % ») : capacite = kWh livres par la borne "
|
||||||
|
"depuis le branchement / part manquante. Ce sont des kWh AU COMPTEUR (pertes du "
|
||||||
|
"chargeur comprises). Une pente descendante sur des mois = usure de la batterie. "
|
||||||
|
"Le niveau saisi est un entier : sur une petite charge, 1 point d'erreur pese lourd.",
|
||||||
|
extra={"overrides": [sur(v, [{"id": "color", "value": {"mode": "fixed", "fixedColor": COULEURS_VEHICULES[v]}}])
|
||||||
|
for v in ["JEEP", "VW", "ZERO"]]})
|
||||||
|
KM_CAPACITE["fieldConfig"]["defaults"]["custom"].update(
|
||||||
|
{"drawStyle": "points", "pointSize": 10, "showPoints": "always", "spanNulls": False})
|
||||||
|
|
||||||
|
KM_RELEVES = table("Les releves du compteur", [sql("A",
|
||||||
|
"SELECT to_char(ts AT TIME ZONE 'Europe/Paris', 'Dy DD/MM HH24:MI') AS \"Quand\",\n"
|
||||||
|
" vehicule AS \"Vehicule\", km AS \"Compteur\",\n"
|
||||||
|
" coalesce(niveau_pct || ' %', '?') AS \"Niveau\", source AS \"Source\"\n"
|
||||||
|
"FROM releve_km ORDER BY ts DESC", fmt="table")],
|
||||||
|
PG, 14, 35, 10, 9,
|
||||||
|
"Releves recus par WhatsApp (concierge) ou par le tableau EVCS Master. Un "
|
||||||
|
"releve fait au branchement est date du branchement : le compteur n'avance pas "
|
||||||
|
"pendant la charge.",
|
||||||
|
overrides=[sur("Compteur", [{"id": "unit", "value": "lengthkm"}])])
|
||||||
|
|
||||||
vehicules = dashboard("evcs-vehicules", "Les vehicules", (
|
vehicules = dashboard("evcs-vehicules", "Les vehicules", (
|
||||||
"Ce que chaque voiture encaisse, session par session et sur la periode "
|
"Ce que chaque voiture encaisse, session par session et sur la periode "
|
||||||
"choisie. L'etiquette vient d'un selecteur manuel : elle peut etre fausse, "
|
"choisie. L'etiquette vient d'un selecteur manuel : elle peut etre fausse, "
|
||||||
@@ -1430,6 +1525,7 @@ vehicules = dashboard("evcs-vehicules", "Les vehicules", (
|
|||||||
{"id": "custom.width", "value": 110}]),
|
{"id": "custom.width", "value": 110}]),
|
||||||
sur("dont reseau", [{"id": "unit", "value": "kwatth"}]),
|
sur("dont reseau", [{"id": "unit", "value": "kwatth"}]),
|
||||||
sur("P max", [{"id": "unit", "value": "watt"}])]),
|
sur("P max", [{"id": "unit", "value": "watt"}])]),
|
||||||
|
KM_XY, KM_INTERVALLES, KM_CAPACITE, KM_RELEVES,
|
||||||
])
|
])
|
||||||
|
|
||||||
# =====================================================================
|
# =====================================================================
|
||||||
@@ -1791,7 +1887,7 @@ journee = dashboard(
|
|||||||
# found ». Ici on veut les quatre colonnes, tout le temps.
|
# found ». Ici on veut les quatre colonnes, tout le temps.
|
||||||
barchart("Energie heure par heure",
|
barchart("Energie heure par heure",
|
||||||
[sql("A", energie_sql(prevision=True), fmt="table")],
|
[sql("A", energie_sql(prevision=True), fmt="table")],
|
||||||
PG, 0, 4, 24, 9, "kwatth",
|
PG, 0, 5, 24, 9, "kwatth",
|
||||||
"La forme de la journee : quand le solaire a couvert la maison, quand il "
|
"La forme de la journee : quand le solaire a couvert la maison, quand il "
|
||||||
"a fallu acheter — et, a la suite des barres mesurees, ce que la derniere "
|
"a fallu acheter — et, a la suite des barres mesurees, ce que la derniere "
|
||||||
"prevision annonce pour les heures qui restent. L'heure EN COURS n'a pas "
|
"prevision annonce pour les heures qui restent. L'heure EN COURS n'a pas "
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import fs from 'node:fs';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import pg from 'pg';
|
import pg from 'pg';
|
||||||
|
import { creerSchemaKm, ingererReleves, ingererNiveauSaisi } from './kilometrage.mjs';
|
||||||
import { texteRapport, envoyer, donneesNuit, texteDepuis } from './rapport-nuit.mjs';
|
import { texteRapport, envoyer, donneesNuit, texteDepuis } from './rapport-nuit.mjs';
|
||||||
import { diagnosticPv, etatCellules, productionJour, chargeSolaire, bilanNuit, etatReseau, importJour, minuitLocal, COMPTEURS_IMPORT, COMPTEUR_EDF, consoJour, COMPTEURS_CONSO, projectionSoir, previsionNuit, vrmVersCourbes, nuitMaisonPrevue } from './diagnostics-agent.mjs';
|
import { diagnosticPv, etatCellules, productionJour, chargeSolaire, bilanNuit, etatReseau, importJour, minuitLocal, COMPTEURS_IMPORT, COMPTEUR_EDF, consoJour, COMPTEURS_CONSO, projectionSoir, previsionNuit, vrmVersCourbes, nuitMaisonPrevue } from './diagnostics-agent.mjs';
|
||||||
|
|
||||||
@@ -710,6 +711,9 @@ async function migrer(client) {
|
|||||||
// « montre-moi tous les signalements graves de la semaine ».
|
// « montre-moi tous les signalements graves de la semaine ».
|
||||||
await client.query(
|
await client.query(
|
||||||
'CREATE INDEX IF NOT EXISTS observation_gravite_idx ON observation (gravite, ts DESC)');
|
'CREATE INDEX IF NOT EXISTS observation_gravite_idx ON observation (gravite, ts DESC)');
|
||||||
|
// Kilometrage et capacite des vehicules (04/10/2026) : tables releve_km et
|
||||||
|
// niveau_saisi, vues km_intervalle et capacite_estimee (tools/kilometrage.mjs).
|
||||||
|
await creerSchemaKm(client);
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// REPARATION UNIQUE (13/09/2026) — onze sessions etiquetees « VW » a tort
|
// REPARATION UNIQUE (13/09/2026) — onze sessions etiquetees « VW » a tort
|
||||||
@@ -2128,6 +2132,11 @@ async function cycle(client) {
|
|||||||
ordre: await appliquerOrdreAirtria(sql, client, st).catch((e) => {
|
ordre: await appliquerOrdreAirtria(sql, client, st).catch((e) => {
|
||||||
console.error(`ordre airtria en echec : ${e.message}`); return 0;
|
console.error(`ordre airtria en echec : ${e.message}`); return 0;
|
||||||
}),
|
}),
|
||||||
|
kilometrage: DRY ? 0 : await ingererReleves(sql, (q, v) => client.query(q, v), st, journaliser).catch((e) => {
|
||||||
|
console.error(`releves km en echec : ${e.message}`); return 0;
|
||||||
|
}) + await ingererNiveauSaisi(sql, st).catch((e) => {
|
||||||
|
console.error(`niveau saisi en echec : ${e.message}`); return 0;
|
||||||
|
}),
|
||||||
pvDiagnostic: DRY ? 0 : await publierDiagnosticPv(st).catch((e) => {
|
pvDiagnostic: DRY ? 0 : await publierDiagnosticPv(st).catch((e) => {
|
||||||
console.error(`diagnostic PV en echec : ${e.message}`); return 0;
|
console.error(`diagnostic PV en echec : ${e.message}`); return 0;
|
||||||
}),
|
}),
|
||||||
|
|||||||
247
tools/kilometrage.mjs
Normal file
247
tools/kilometrage.mjs
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
// Kilometrage et capacite des vehicules (04/10/2026, demande du proprietaire).
|
||||||
|
//
|
||||||
|
// Deux mesures distinctes, decidees avec lui :
|
||||||
|
// 1. km par kWh : entre deux RELEVES DU COMPTEUR kilometrique d'un vehicule,
|
||||||
|
// kWh consommes = kWh livres par la borne a ce vehicule entre les deux
|
||||||
|
// relevés + (niveau au 1er releve - niveau au 2nd) x capacite.
|
||||||
|
// Cela mesure surtout la conduite et la saison (chauffage, froid).
|
||||||
|
// 2. capacite estimee : a chaque branchement ou le niveau a ete SAISI et la
|
||||||
|
// charge menee jusqu'au plein, capacite = kWh livres depuis la saisie /
|
||||||
|
// (part manquante). C'est elle qui montrera l'usure de la batterie.
|
||||||
|
//
|
||||||
|
// ⚠️ La JEEP et la VW sont des HYBRIDES : batterie vide, elles roulent a
|
||||||
|
// l'essence et ces kilometres gonfleraient le rendement. Un intervalle n'est
|
||||||
|
// donc retenu que si le niveau au retour est > 0. La ZERO est tout electrique.
|
||||||
|
//
|
||||||
|
// Contrat d'entree (meme modele que l'etiquette du concierge) :
|
||||||
|
// `input_text.evcs_releve_km` = tableau JSON des derniers releves, chacun
|
||||||
|
// [vehicule, km, niveau_pct|null, epoch_s, source]
|
||||||
|
// ecrit par le concierge WhatsApp ou par le script HA du tableau EVCS Master.
|
||||||
|
// Le service les ingere chaque cycle, sans doublon (vehicule, horodatage).
|
||||||
|
|
||||||
|
export const VEHICULES_TOUT_ELECTRIQUES = ['ZERO'];
|
||||||
|
export const RELEVE_HELPER = 'input_text.evcs_releve_km';
|
||||||
|
export const NIVEAU_ENTITY = 'input_number.victron_evcs_vehicle_level_pct';
|
||||||
|
export const NIVEAU_AT_ENTITY = 'input_datetime.victron_evcs_vehicle_level_at';
|
||||||
|
export const NIVEAU_REF_ENTITY = 'input_number.victron_evcs_vehicle_level_ref_kwh';
|
||||||
|
// Etats du controleur qui disent « le vehicule n'accepte plus rien ».
|
||||||
|
export const ETATS_PLEIN = ['day_vehicle_full_idle', 'night_recovery_done_idle',
|
||||||
|
'night_session_done_idle', 'departure_done_idle'];
|
||||||
|
|
||||||
|
// Lit le helper. Renvoie { releves: [...], rejets: [motif...] }. Pure.
|
||||||
|
export function lireReleves(brut) {
|
||||||
|
const s = (brut ?? '').toString().trim();
|
||||||
|
if (!s || s === 'unknown' || s === 'unavailable') return { releves: [], rejets: [] };
|
||||||
|
let tab;
|
||||||
|
try { tab = JSON.parse(s); } catch { return { releves: [], rejets: ['JSON illisible'] }; }
|
||||||
|
if (!Array.isArray(tab)) return { releves: [], rejets: ['pas un tableau'] };
|
||||||
|
const releves = [], rejets = [];
|
||||||
|
for (const e of tab) {
|
||||||
|
if (!Array.isArray(e) || e.length < 4) { rejets.push('entree mal formee'); continue; }
|
||||||
|
const [v, km, niveau, t, source] = e;
|
||||||
|
const vehicule = (v ?? '').toString().trim().toUpperCase();
|
||||||
|
const kmN = Number(km), tN = Number(t);
|
||||||
|
const nivN = niveau === null || niveau === undefined || niveau === '' ? null : Number(niveau);
|
||||||
|
if (!vehicule) { rejets.push('vehicule absent'); continue; }
|
||||||
|
if (!Number.isInteger(kmN) || kmN <= 0 || kmN > 2000000) { rejets.push(`${vehicule} : km invalide (${km})`); continue; }
|
||||||
|
if (nivN !== null && !(Number.isInteger(nivN) && nivN >= 0 && nivN <= 100)) {
|
||||||
|
rejets.push(`${vehicule} : niveau invalide (${niveau})`); continue;
|
||||||
|
}
|
||||||
|
if (!Number.isFinite(tN) || tN < 1.7e9) { rejets.push(`${vehicule} : horodatage invalide`); continue; }
|
||||||
|
releves.push({ vehicule, km: kmN, niveau: nivN, ts: new Date(tN * 1000),
|
||||||
|
source: (source ?? '').toString() || 'inconnue' });
|
||||||
|
}
|
||||||
|
return { releves, rejets };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajoute un releve au tableau existant en gardant les 6 derniers (le helper est
|
||||||
|
// plafonne a 255 caracteres par HA). Pure — utilisee par le concierge.
|
||||||
|
export function ajouterReleve(brut, releve) {
|
||||||
|
let tab = [];
|
||||||
|
try { const t = JSON.parse(brut || '[]'); if (Array.isArray(t)) tab = t; } catch { /* repart a vide */ }
|
||||||
|
tab.push([releve.vehicule, releve.km, releve.niveau ?? null, Math.round(releve.ts.getTime() / 1000), releve.source]);
|
||||||
|
let out = tab.slice(-6);
|
||||||
|
while (JSON.stringify(out).length > 255 && out.length > 1) out = out.slice(1);
|
||||||
|
return JSON.stringify(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function creerSchemaKm(client) {
|
||||||
|
await client.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS releve_km (
|
||||||
|
vehicule text NOT NULL,
|
||||||
|
ts timestamptz NOT NULL,
|
||||||
|
km integer NOT NULL,
|
||||||
|
niveau_pct integer,
|
||||||
|
source text,
|
||||||
|
capacite_kwh double precision,
|
||||||
|
autonomie_ref_km double precision,
|
||||||
|
tout_electrique boolean NOT NULL DEFAULT false,
|
||||||
|
pose_a timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (vehicule, ts)
|
||||||
|
)`);
|
||||||
|
// Chaque saisie du niveau au branchement (concierge ou tableau de bord),
|
||||||
|
// avec le compteur de la borne a cet instant. Le vehicule se deduit de
|
||||||
|
// l'etiquette du branchement, jamais du selecteur.
|
||||||
|
await client.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS niveau_saisi (
|
||||||
|
saisi_a timestamptz PRIMARY KEY,
|
||||||
|
niveau_pct integer NOT NULL,
|
||||||
|
ref_kwh double precision NOT NULL,
|
||||||
|
pose_a timestamptz NOT NULL DEFAULT now()
|
||||||
|
)`);
|
||||||
|
await client.query(SQL_VUE_INTERVALLES);
|
||||||
|
await client.query(SQL_VUE_CAPACITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Un intervalle = deux releves consecutifs du meme vehicule.
|
||||||
|
export const SQL_VUE_INTERVALLES = `
|
||||||
|
DROP VIEW IF EXISTS km_intervalle;
|
||||||
|
CREATE VIEW km_intervalle AS
|
||||||
|
WITH r AS (
|
||||||
|
SELECT vehicule, ts, km, niveau_pct, capacite_kwh, autonomie_ref_km, tout_electrique,
|
||||||
|
lag(ts) OVER w AS ts0,
|
||||||
|
lag(km) OVER w AS km0,
|
||||||
|
lag(niveau_pct) OVER w AS n0
|
||||||
|
FROM releve_km WINDOW w AS (PARTITION BY vehicule ORDER BY ts)
|
||||||
|
), e AS (
|
||||||
|
SELECT r.*, km - km0 AS km_parcourus,
|
||||||
|
(SELECT coalesce(sum(coalesce(s.energie_station_kwh, s.energie_kwh)), 0)
|
||||||
|
FROM session_charge s
|
||||||
|
WHERE s.vehicule = r.vehicule AND s.debut >= r.ts0 AND s.debut < r.ts) AS kwh_charges
|
||||||
|
FROM r WHERE ts0 IS NOT NULL
|
||||||
|
), c AS (
|
||||||
|
SELECT e.*,
|
||||||
|
CASE WHEN n0 IS NOT NULL AND niveau_pct IS NOT NULL AND capacite_kwh > 0
|
||||||
|
THEN kwh_charges + (n0 - niveau_pct) / 100.0 * capacite_kwh END AS kwh_consommes
|
||||||
|
FROM e
|
||||||
|
)
|
||||||
|
SELECT vehicule, ts0 AS debut, ts AS fin, km_parcourus, kwh_charges, n0 AS niveau_debut,
|
||||||
|
niveau_pct AS niveau_fin, capacite_kwh, kwh_consommes,
|
||||||
|
CASE WHEN kwh_consommes > 0 THEN km_parcourus / kwh_consommes END AS km_par_kwh,
|
||||||
|
CASE WHEN capacite_kwh > 0 AND autonomie_ref_km > 0
|
||||||
|
THEN autonomie_ref_km / capacite_kwh END AS km_par_kwh_ref,
|
||||||
|
CASE
|
||||||
|
WHEN km_parcourus < 0 THEN 'compteur en baisse'
|
||||||
|
WHEN km_parcourus = 0 THEN 'aucun km'
|
||||||
|
WHEN n0 IS NULL OR niveau_pct IS NULL THEN 'niveau manquant'
|
||||||
|
WHEN NOT tout_electrique AND niveau_pct <= 0 THEN 'batterie vide au retour : km essence possibles'
|
||||||
|
WHEN kwh_consommes IS NULL OR kwh_consommes <= 0 THEN 'energie nulle'
|
||||||
|
ELSE 'valide' END AS statut
|
||||||
|
FROM c`;
|
||||||
|
|
||||||
|
// Capacite estimee : derniere saisie de chaque branchement termine.
|
||||||
|
// - Branchement = derniere transition 'on' au plus 1 h apres la saisie (la
|
||||||
|
// saisie vaut si elle date de branchement - 1 h ou apres).
|
||||||
|
// - Le niveau saisi est celui du BRANCHEMENT (le proprietaire le lit sur la
|
||||||
|
// voiture avant de brancher, et le dit parfois une heure apres : « c'etait
|
||||||
|
// 53 % a 13h10 ») : l'energie se compte donc du compteur de la borne au
|
||||||
|
// branchement jusqu'au debranchement.
|
||||||
|
// - Niveau de fin = 100 si le controleur a vu le vehicule plein, sinon le
|
||||||
|
// niveau d'un releve km du meme vehicule dans l'heure qui suit le
|
||||||
|
// debranchement (« debranche a 78 % »).
|
||||||
|
// - Vehicule = etiquette CONFIRMEE des sessions du branchement (celle que le
|
||||||
|
// proprietaire corrige), a defaut celle du concierge.
|
||||||
|
export const SQL_VUE_CAPACITE = `
|
||||||
|
DROP VIEW IF EXISTS capacite_estimee;
|
||||||
|
CREATE VIEW capacite_estimee AS
|
||||||
|
WITH tr AS (
|
||||||
|
SELECT ts, detail->>'apres' AS apres FROM evenement
|
||||||
|
WHERE genre = 'vehicule'
|
||||||
|
AND detail->>'entite' = 'binary_sensor.victron_evcs_vehicle_connected'
|
||||||
|
), s AS (
|
||||||
|
SELECT n.*,
|
||||||
|
(SELECT max(ts) FROM tr WHERE apres = 'on' AND tr.ts <= n.saisi_a + interval '1 hour') AS branche
|
||||||
|
FROM niveau_saisi n
|
||||||
|
), s1 AS (
|
||||||
|
SELECT DISTINCT ON (branche) * FROM s WHERE branche IS NOT NULL ORDER BY branche, saisi_a DESC
|
||||||
|
), b AS (
|
||||||
|
SELECT s1.*,
|
||||||
|
(SELECT min(ts) FROM tr WHERE apres = 'off' AND tr.ts > s1.branche) AS debranche
|
||||||
|
FROM s1 WHERE saisi_a >= branche - interval '1 hour'
|
||||||
|
), v AS (
|
||||||
|
SELECT b.*,
|
||||||
|
coalesce(
|
||||||
|
(SELECT sc.vehicule FROM session_charge sc
|
||||||
|
WHERE sc.debut >= b.branche AND sc.debut < b.debranche AND sc.vehicule_sur
|
||||||
|
GROUP BY sc.vehicule ORDER BY sum(coalesce(sc.energie_station_kwh, sc.energie_kwh)) DESC LIMIT 1),
|
||||||
|
(SELECT label FROM etiquette_branchement eb
|
||||||
|
WHERE eb.branchement BETWEEN b.branche - interval '2 seconds' AND b.branche + interval '2 seconds'
|
||||||
|
LIMIT 1)) AS vehicule
|
||||||
|
FROM b WHERE debranche IS NOT NULL
|
||||||
|
), k AS (
|
||||||
|
SELECT v.*,
|
||||||
|
(SELECT valeur FROM mesure m WHERE m.cle = 'sensor.evcs_hq2239dnt7p_total_energy'
|
||||||
|
AND m.ts <= v.debranche ORDER BY m.ts DESC LIMIT 1)
|
||||||
|
- coalesce((SELECT valeur FROM mesure m WHERE m.cle = 'sensor.evcs_hq2239dnt7p_total_energy'
|
||||||
|
AND m.ts <= v.branche ORDER BY m.ts DESC LIMIT 1), ref_kwh) AS kwh_livres,
|
||||||
|
EXISTS (SELECT 1 FROM decision d WHERE d.ts > v.branche AND d.ts <= v.debranche
|
||||||
|
AND d.etat = ANY (ARRAY['${ETATS_PLEIN.join("','")}'])) AS plein,
|
||||||
|
(SELECT r.niveau_pct FROM releve_km r
|
||||||
|
WHERE r.vehicule = v.vehicule AND r.niveau_pct IS NOT NULL
|
||||||
|
AND r.ts >= v.debranche AND r.ts <= v.debranche + interval '1 hour'
|
||||||
|
ORDER BY r.ts LIMIT 1) AS niveau_releve_apres
|
||||||
|
FROM v
|
||||||
|
), f AS (
|
||||||
|
SELECT k.*, CASE WHEN plein THEN 100 ELSE niveau_releve_apres END AS niveau_fin FROM k
|
||||||
|
)
|
||||||
|
SELECT branche, debranche, coalesce(vehicule, '?') AS vehicule, niveau_pct, niveau_fin,
|
||||||
|
kwh_livres, plein,
|
||||||
|
CASE WHEN vehicule IS NOT NULL AND niveau_fin - niveau_pct >= 10 AND kwh_livres > 0
|
||||||
|
THEN kwh_livres / ((niveau_fin - niveau_pct) / 100.0) END AS capacite_kwh
|
||||||
|
FROM f`;
|
||||||
|
|
||||||
|
// Ingestion des releves du helper. Refuse un compteur qui baisse.
|
||||||
|
export async function ingererReleves(sql, lire, st, journaliser) {
|
||||||
|
const { releves, rejets } = lireReleves(st.get(RELEVE_HELPER)?.state);
|
||||||
|
let n = 0;
|
||||||
|
for (const r of releves) {
|
||||||
|
const v = r.vehicule.toLowerCase();
|
||||||
|
const cap = Number(st.get(`input_number.victron_evcs_capacity_${v}_kwh`)?.state);
|
||||||
|
const auto = Number(st.get(`input_number.victron_evcs_autonomie_${v}_km`)?.state);
|
||||||
|
const { rows: prec } = await lire(
|
||||||
|
'SELECT km FROM releve_km WHERE vehicule = $1 AND ts < $2 ORDER BY ts DESC LIMIT 1',
|
||||||
|
[r.vehicule, r.ts]);
|
||||||
|
const { rows: deja } = await lire(
|
||||||
|
'SELECT 1 FROM releve_km WHERE vehicule = $1 AND ts = $2', [r.vehicule, r.ts]);
|
||||||
|
if (deja.length) continue;
|
||||||
|
if (prec.length && r.km < Number(prec[0].km)) {
|
||||||
|
const cle = `${r.vehicule}|${r.ts.toISOString()}`;
|
||||||
|
if (!rejetsJournalises.has(cle)) {
|
||||||
|
rejetsJournalises.add(cle);
|
||||||
|
await journaliser(sql, 'vehicule', `Releve km ${r.vehicule} refuse : ${r.km} km < ${prec[0].km} km du releve precedent`,
|
||||||
|
{ vehicule: r.vehicule, km: r.km, precedent: Number(prec[0].km), source: r.source });
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await sql(
|
||||||
|
`INSERT INTO releve_km (vehicule, ts, km, niveau_pct, source, capacite_kwh, autonomie_ref_km, tout_electrique)
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT DO NOTHING`,
|
||||||
|
[r.vehicule, r.ts, r.km, r.niveau, r.source, cap > 0 ? cap : null, auto > 0 ? auto : null,
|
||||||
|
VEHICULES_TOUT_ELECTRIQUES.includes(r.vehicule)]);
|
||||||
|
await journaliser(sql, 'vehicule',
|
||||||
|
`Releve km ${r.vehicule} : ${r.km} km` + (r.niveau === null ? ', niveau non donne' : `, niveau ${r.niveau} %`) + ` (${r.source})`,
|
||||||
|
{ vehicule: r.vehicule, km: r.km, niveau: r.niveau, ts: r.ts.toISOString(), source: r.source });
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
for (const m of rejets) {
|
||||||
|
if (rejetsJournalises.has(m)) continue;
|
||||||
|
rejetsJournalises.add(m);
|
||||||
|
await journaliser(sql, 'vehicule', `Releve km illisible : ${m}`, { brut: (st.get(RELEVE_HELPER)?.state || '').slice(0, 255) });
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
const rejetsJournalises = new Set();
|
||||||
|
|
||||||
|
// Saisie du niveau : une ligne par horodatage de saisie.
|
||||||
|
export async function ingererNiveauSaisi(sql, st) {
|
||||||
|
// L'attribut `timestamp` d'un input_datetime est en secondes epoch : pas de
|
||||||
|
// fuseau a deviner (l'etat, lui, est une heure locale sans decalage).
|
||||||
|
const at = Number(st.get(NIVEAU_AT_ENTITY)?.attributes?.timestamp) * 1000;
|
||||||
|
const niveau = Number(st.get(NIVEAU_ENTITY)?.state);
|
||||||
|
const ref = Number(st.get(NIVEAU_REF_ENTITY)?.state);
|
||||||
|
if (!Number.isFinite(at) || !Number.isInteger(niveau) || !Number.isFinite(ref) || ref <= 0) return 0;
|
||||||
|
const r = await sql(
|
||||||
|
'INSERT INTO niveau_saisi (saisi_a, niveau_pct, ref_kwh) VALUES ($1,$2,$3) ON CONFLICT DO NOTHING',
|
||||||
|
[new Date(at), niveau, ref]);
|
||||||
|
return r?.rowCount || 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user