diff --git a/.eslint.config.mjs b/.eslint.config.mjs
index ecfb955..898b950 100644
--- a/.eslint.config.mjs
+++ b/.eslint.config.mjs
@@ -112,7 +112,7 @@ const config = [
}
},
{
- "ignores": ["*.js"]
+ "ignores": ["*.js", "components/*.js"]
}
];
diff --git a/.gitignore b/.gitignore
index 3dd7249..71a9168 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
/node_modules/
/MMM-Linky.js
/node_helper.js
-/linkyData.json
+/components/
+/data/*.json
diff --git a/README.md b/README.md
index 03375a5..38f530d 100644
--- a/README.md
+++ b/README.md
@@ -8,18 +8,19 @@ Si vous choisissez de récupérer les données de l'année précédente une comp
Le header est également dynamique et changera en fonction de la période sélectionnée !
-Les données sont actualisées chaque jour entre 14h et 15h.
+Les données sont actualisées chaque jour entre 12h et 12h15.
## ScreenShots
-
-
-
+
+
+
+
Possibilité de choisir entre 4 thèmes de couleur pour le graphique et d'afficher les valeurs dans les barres :
-
-
+
+
## Installation
@@ -30,7 +31,7 @@ cd MMM-Linky
npm run setup
```
-## Using the module
+## Utilisation du module
### Pré-requis
@@ -48,6 +49,8 @@ Pour utiliser ce module, ajoutez-le au tableau modules dans le fichier `config/c
prm: "",
token: "",
periode: 1,
+ apis: ["getDailyConsumption"],
+ affichageInterval: 1000 * 15,
annee_n_minus_1: 1,
couleur: 3,
valuebar: 1,
@@ -69,6 +72,7 @@ Configuration minimale :
config: {
prm: "",
token: "",
+ apis: ["getDailyConsumption"]
},
},
```
@@ -77,18 +81,73 @@ Configuration minimale :
Option|Default|Description
---|---|---
-`debug`|0|Active le mode débogage.
`1` : activer
`0` : Désactiver
+`debug`|0|Active le mode débogage.
`1` : activer
`0` : désactiver
`prm`||Votre numéro PDL Linky [VOIR ICI](https://www.enedis.fr/faq/compteur-linky/ou-trouver-le-numero-point-de-livraison-pdl-du-compteur-linky)
-`token`||Votre token personnel [CONSO API](https://conso.boris.sh/)
+`token`||Votre token personnel [CONSO API](https://conso.boris.sh/)
`periode`|1|Choix de la période:
`1` = Données de la veille
`2` = 3 derniers jours
`3` = 7 derniers jours
-`annee_n_minus_1`|1|Récupérer les données de l'année précédente.
`1` : Activer
`0` : Désactiver
+`apis`|["getDailyConsumption"]|Nom des API à interroger (voir ci-dessous)
+`affichageInterval`|1000 * 15|Intervalle d'affichage des graphiques en ms (si utilisation de plusieurs API)
+`annee_n_minus_1`|1|Récupérer les données de l'année précédente. (uniquement pour les API `getDailyConsumption` et `getDailyProduction`)
`1` : activer
`0` : désactiver
`couleur`|3| `1` : Bleu et Rose
`2` : Jaune et Vert
`3` : Blanc et Bleu
`4` : Orange et Violet
-`valuebar`|1|Affiche les valeurs à l'intérieur des barres.
`1` : Afficher
`0` : Masquer
-`valuebartextcolor`|0|Couleur du texte des valeurs.
`0` : Texte noir
`1` : Texte blanc
-`header`|1|Affiche l'en-tête selon la période selectionné.
`1` : Afficher
`0` : Masquer
-`energie`|1|Affiche l'indicateur de consomation d'énergie.
`1` : Afficher
`0` : Masquer
-`updateDate`|1|Affiche la date de récupération des données.
`1` : Afficher
`0` : Masquer
-`updateNext`|1|Affiche la date du prochain cycle de récupération des données.
`1` : Afficher
`0` : Masquer
+`valuebar`|1|Affiche les valeurs à l'intérieur des barres.
`1` : afficher
`0` : masquer
+`valuebartextcolor`|0|Couleur du texte des valeurs.
`0` : texte noir
`1` : texte blanc
+`header`|1|Affiche l'en-tête selon la période selectionné.
`1` : afficher
`0` : masquer
+`energie`|1|Affiche l'indicateur de consomation d'énergie.
`1` : afficher
`0` : masquer
+`updateDate`|1|Affiche la date de récupération des données.
`1` : afficher
`0` : masquer
+`updateNext`|1|Affiche la date du prochain cycle de récupération des données.
`1` : afficher
`0` : masquer
+
+### APIs
+
+Grâce à `Conso API`, vous pouvez interroger plusieurs API et afficher le graphique correspondant.
+
+* `getDailyConsumption`: Récupère la consommation quotidienne.
+* `getLoadCurve`: Récupère la puissance moyenne consommée de la veille sur un intervalle de 30 min.
+* `getMaxPower`: Récupère la puissance maximale de consommation atteinte quotidiennement.
+
+Il est également possible d'afficher vos données de production d'energie.
+
+* `getDailyProduction`: Récupère la production quotidienne.
+* `getProductionLoadCurve`: Récupère la puissance moyenne produite sur un intervalle de 30 min.
+
+## Mise en cache des données
+
+Afin d'éviter une surcharge de l'API, une mise en cache des données a été mise en place.
+
+De ce fait, lors d'un redémarrage de `MagicMirror²`, `MMM-Linky` utilisera les dernières données reçues de l'API.
+
+La validité de ce cache à été fixée à 10h.
+
+## Effacer le cache des données
+
+Vous pouvez toute fois détruire ce cache avec la commande: `npm run reset:cache`
+
+Il est déconseillé d'utiliser cette commande trop souvent car l'api a un usage limité.
+
+`Conso API` a fixé cette régle:
+
+* Maximum de 5 requêtes par seconde.
+* Maximum de 10 000 requêtes par heure.
+
+⚠ Si vous dépassez une des régles, votre adresse IP sera bloquée sans avertissement !
+
+Malheurement, nous n'avons aucun pouvoir pour la débloquer...
+
+Pour rappel un appel API est une requête. si vous utilisez 2 API en config... c'est donc 2 requêtes !
+
+## Changement de configuration
+
+Afin de générer un nouveau cache, une nouvelle requête sera relancé pour les API suivantes (si utilisées)
+
+↪️ En cas de changement de configuration `periode`
+
+* `getDailyConsumption`
+* `getMaxPower`
+* `getDailyProduction`
+
+↪️ En cas de changement de configuration `annee_n_minus_1`
+
+* `getDailyConsumption`
+* `getDailyProduction`
## Mise à jour
diff --git a/data/.keep b/data/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/installer/cache.js b/installer/cache.js
new file mode 100644
index 0000000..ccc6e17
--- /dev/null
+++ b/installer/cache.js
@@ -0,0 +1,21 @@
+const utils = require("./utils");
+
+async function main () {
+ // Let's start !
+ utils.empty();
+ utils.info(`Delete Cache ${utils.moduleName()} v${utils.moduleVersion()}`);
+ utils.empty();
+ await deleteCache();
+ utils.success("Done!");
+}
+
+async function deleteCache () {
+ utils.info("➤ Cleaning json data files...");
+ if (utils.isWin()) {
+ await utils.execCMD(`del ${utils.getModuleRoot()}\\data\\*.json`);
+ } else {
+ await utils.execCMD(`rm -f ${utils.getModuleRoot()}/data/*.json`);
+ }
+}
+
+main();
diff --git a/installer/functions.js b/installer/functions.js
index 3d2eb3e..444f07a 100644
--- a/installer/functions.js
+++ b/installer/functions.js
@@ -1,3 +1,4 @@
+const path = require("node:path");
const utils = require("./utils");
var packageJSON;
@@ -12,6 +13,8 @@ try {
var options = packageJSON.installer || {};
async function updatePackageInfoLinux () {
+ const apt = options.apt;
+ if (!apt.length) return;
utils.empty();
utils.info("➤ Update package informations");
utils.empty();
@@ -34,14 +37,13 @@ async function updatePackageInfoLinux () {
module.exports.updatePackageInfoLinux = updatePackageInfoLinux;
async function installLinuxDeps () {
+ const apt = options.apt;
+ if (!apt.length) return;
utils.empty();
utils.info("➤ Dependencies installer");
utils.empty();
- const apt = options.apt;
- if (!apt.length) {
- utils.out("No dependecies needed!");
- return;
- }
+ utils.out(`Checking: ${apt}...`);
+ utils.empty();
return new Promise((resolve) => {
utils.check(apt, (result) => {
if (!result.length) {
@@ -70,6 +72,32 @@ async function installLinuxDeps () {
}
module.exports.installLinuxDeps = installLinuxDeps;
+async function postInstall () {
+ if (!options.postInstall) return;
+ utils.empty();
+ utils.info("➤ Post-Install...");
+ utils.empty();
+ const Path = path.resolve(`${utils.getModuleRoot()}`, "installer");
+ const args = utils.getArgs();
+ const command = args.path ? `${options.postInstall} --path=${args.path}` : `${options.postInstall}`;
+ return new Promise((resolve) => {
+ utils.execPathCMD(command, Path, (err) => {
+ if (err) {
+ utils.error("Error Detected!");
+ process.exit(1);
+ }
+ resolve();
+ })
+ .on("stdout", function (data) {
+ utils.out(data.trim());
+ })
+ .on("stderr", function (data) {
+ utils.error(data.trim());
+ });
+ });
+}
+module.exports.postInstall = postInstall;
+
async function installNPMDeps () {
utils.empty();
utils.info("➤ NPM Package installer");
@@ -141,13 +169,10 @@ async function develop () {
module.exports.develop = develop;
async function electronRebuild () {
+ if (!options.rebuild || (utils.isWin() && !options.windowsRebuild)) return;
utils.empty();
utils.info("➤ Rebuild MagicMirror...");
utils.empty();
- if (!options.rebuild || (utils.isWin() && !options.windowsRebuild)) {
- utils.out("electron-rebuild is not needed.");
- return;
- }
return new Promise((resolve) => {
utils.electronRebuild((err) => {
if (err) {
@@ -209,6 +234,7 @@ function setOptions () {
minify: true,
rebuild: false,
apt: [],
+ postInstall: null,
windowsNPMRemove: [],
windowsRebuild: false
};
diff --git a/installer/setup.js b/installer/setup.js
index 55e9923..633a09f 100644
--- a/installer/setup.js
+++ b/installer/setup.js
@@ -25,7 +25,11 @@ async function checkOS () {
}
utils.empty();
await utils.checkRoot();
+ await functions.updatePackageInfoLinux();
+ await functions.installLinuxDeps();
await functions.installNPMDeps();
+ await functions.postInstall();
+ await functions.electronRebuild();
await functions.installFiles();
functions.done();
break;
@@ -38,6 +42,7 @@ async function checkOS () {
case "Windows":
utils.success(`OS Detected: Windows (${sysinfo.name} ${sysinfo.version} ${sysinfo.arch})`);
await functions.installNPMDeps();
+ await functions.postInstall();
await functions.installFiles();
functions.done();
break;
diff --git a/installer/utils.js b/installer/utils.js
index 353ddf4..8bc9e3b 100644
--- a/installer/utils.js
+++ b/installer/utils.js
@@ -9,6 +9,7 @@ var packageJSON = require("../package.json");
var moduleRoot = path.resolve(__dirname, "../");
const installerHome = path.resolve(__dirname, "../installer");
+const bugsounetRoot = path.resolve(__dirname);
// color codes
const reset = "\x1B[0m";
@@ -353,7 +354,8 @@ module.exports.develop = develop;
// electron need to be rebuilded
function electronRebuild (callback = () => {}) {
var emitter = new events.EventEmitter();
- var child = exec("npx electron-rebuild", { cwd: moduleRoot }, function (err) {
+ const cmd = args.path ? `npx electron-rebuild -m ${moduleRoot}` : "npx electron-rebuild";
+ var child = exec(cmd, { cwd: bugsounetRoot }, function (err) {
if (err) {
return callback(err);
}
@@ -392,6 +394,27 @@ function execCMD (command, callback = () => {}, bypass) {
}
module.exports.execCMD = execCMD;
+function execPathCMD (command, path, callback = () => {}) {
+ var emitter = new events.EventEmitter();
+ var child = exec(`${command}`, { cwd: path }, function (err) {
+ if (err) {
+ return callback(err);
+ }
+ return callback();
+ });
+
+ child.stdout.on("data", function (data) {
+ emitter.emit("stdout", data);
+ });
+
+ child.stderr.on("data", function (data) {
+ emitter.emit("stderr", data);
+ });
+
+ return emitter;
+}
+module.exports.execPathCMD = execPathCMD;
+
async function moduleReset () {
info("➤ Cleaning js files and reset git branch...");
if (isWin()) {
diff --git a/package-lock.json b/package-lock.json
index 8da9a00..8a7f979 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "MMM-Linky",
- "version": "1.1.3",
+ "version": "1.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "MMM-Linky",
- "version": "1.1.3",
+ "version": "1.2.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -20,13 +20,13 @@
"node-cron": "^3.0.3"
},
"devDependencies": {
- "@stylistic/eslint-plugin": "^4.1.0",
- "eslint": "^9.21.0",
+ "@stylistic/eslint-plugin": "^4.2.0",
+ "eslint": "^9.22.0",
"eslint-plugin-depend": "^0.12.0",
"eslint-plugin-import-x": "^4.6.1",
- "eslint-plugin-package-json": "^0.26.0",
+ "eslint-plugin-package-json": "^0.26.3",
"markdownlint-cli2": "^0.17.2",
- "stylelint": "^16.14.1",
+ "stylelint": "^16.15.0",
"stylelint-config-standard": "^37.0.0",
"stylelint-prettier": "^5.0.3"
}
@@ -645,6 +645,16 @@
"node": "*"
}
},
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.1.0.tgz",
+ "integrity": "sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
"node_modules/@eslint/core": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz",
@@ -707,9 +717,9 @@
}
},
"node_modules/@eslint/js": {
- "version": "9.21.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz",
- "integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==",
+ "version": "9.22.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.22.0.tgz",
+ "integrity": "sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -927,9 +937,9 @@
}
},
"node_modules/@stylistic/eslint-plugin": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-4.1.0.tgz",
- "integrity": "sha512-bytbL7qiici7yPyEiId0fGPK9kjQbzcPMj2aftPfzTCyJ/CRSKdtI+iVjM0LSGzGxfunflI+MDDU9vyIIeIpoQ==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-4.2.0.tgz",
+ "integrity": "sha512-8hXezgz7jexGHdo5WN6JBEIPHCSFyyU4vgbxevu4YLVS5vl+sxqAAGyXSzfNDyR6xMNSH5H1x67nsXcYMOHtZA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2418,18 +2428,19 @@
}
},
"node_modules/eslint": {
- "version": "9.21.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.21.0.tgz",
- "integrity": "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==",
+ "version": "9.22.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.22.0.tgz",
+ "integrity": "sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.19.2",
+ "@eslint/config-helpers": "^0.1.0",
"@eslint/core": "^0.12.0",
"@eslint/eslintrc": "^3.3.0",
- "@eslint/js": "9.21.0",
+ "@eslint/js": "9.22.0",
"@eslint/plugin-kit": "^0.2.7",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
@@ -2441,7 +2452,7 @@
"cross-spawn": "^7.0.6",
"debug": "^4.3.2",
"escape-string-regexp": "^4.0.0",
- "eslint-scope": "^8.2.0",
+ "eslint-scope": "^8.3.0",
"eslint-visitor-keys": "^4.2.0",
"espree": "^10.3.0",
"esquery": "^1.5.0",
@@ -2559,9 +2570,9 @@
}
},
"node_modules/eslint-plugin-package-json": {
- "version": "0.26.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-package-json/-/eslint-plugin-package-json-0.26.0.tgz",
- "integrity": "sha512-plYuuP7RyL532yHLPvKtQNzK6ncXRmzWPji5EUlV0tXhhfFc84TDWiwJ+OYvv4pDA9AfV+gKYVUwhojaDameNw==",
+ "version": "0.26.3",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-package-json/-/eslint-plugin-package-json-0.26.3.tgz",
+ "integrity": "sha512-HG1JePOD3eQWSO4x3aPGyBKMv9SR8+/5m6GsYTRxgRsJUnD9DV5XD7gDD1qg7N8AUYLLMW2wkQudcLbphatFTg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2572,7 +2583,7 @@
"package-json-validator": "^0.10.0",
"semver": "^7.5.4",
"sort-object-keys": "^1.1.3",
- "sort-package-json": "^2.12.0",
+ "sort-package-json": "^3.0.0",
"validate-npm-package-name": "^6.0.0"
},
"engines": {
@@ -2584,9 +2595,9 @@
}
},
"node_modules/eslint-scope": {
- "version": "8.2.0",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
- "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
+ "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
@@ -5128,9 +5139,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.1",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
- "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
+ "version": "8.5.3",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
+ "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dev": true,
"funding": [
{
@@ -5730,20 +5741,20 @@
"license": "MIT"
},
"node_modules/sort-package-json": {
- "version": "2.14.0",
- "resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-2.14.0.tgz",
- "integrity": "sha512-xBRdmMjFB/KW3l51mP31dhlaiFmqkHLfWTfZAno8prb/wbDxwBPWFpxB16GZbiPbYr3wL41H8Kx22QIDWRe8WQ==",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-3.0.0.tgz",
+ "integrity": "sha512-vfZWx4DnFNB8R9Vg4Dnx21s20auNzWH15ZaCBfADAiyrCwemRmhWstTgvLjMek1DW3+MHcNaqkp86giCF24rMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"detect-indent": "^7.0.1",
- "detect-newline": "^4.0.0",
+ "detect-newline": "^4.0.1",
"get-stdin": "^9.0.0",
"git-hooks-list": "^3.0.0",
"is-plain-obj": "^4.1.0",
- "semver": "^7.6.0",
+ "semver": "^7.7.1",
"sort-object-keys": "^1.1.3",
- "tinyglobby": "^0.2.9"
+ "tinyglobby": "^0.2.12"
},
"bin": {
"sort-package-json": "cli.js"
@@ -5882,9 +5893,9 @@
"integrity": "sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g=="
},
"node_modules/stylelint": {
- "version": "16.14.1",
- "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.14.1.tgz",
- "integrity": "sha512-oqCL7AC3786oTax35T/nuLL8p2C3k/8rHKAooezrPGRvUX0wX+qqs5kMWh5YYT4PHQgVDobHT4tw55WgpYG6Sw==",
+ "version": "16.15.0",
+ "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.15.0.tgz",
+ "integrity": "sha512-OK6Rs7EPdcdmjqiDycadZY4fw3f5/TC1X6/tGjnF3OosbwCeNs7nG+79MCAtjEg7ckwqTJTsku08e0Rmaz5nUw==",
"dev": true,
"funding": [
{
@@ -5911,7 +5922,7 @@
"debug": "^4.3.7",
"fast-glob": "^3.3.3",
"fastest-levenshtein": "^1.0.16",
- "file-entry-cache": "^10.0.5",
+ "file-entry-cache": "^10.0.6",
"global-modules": "^2.0.0",
"globby": "^11.1.0",
"globjoin": "^0.1.4",
@@ -5925,14 +5936,14 @@
"micromatch": "^4.0.8",
"normalize-path": "^3.0.0",
"picocolors": "^1.1.1",
- "postcss": "^8.5.1",
+ "postcss": "^8.5.3",
"postcss-resolve-nested-selector": "^0.1.6",
"postcss-safe-parser": "^7.0.1",
- "postcss-selector-parser": "^7.0.0",
+ "postcss-selector-parser": "^7.1.0",
"postcss-value-parser": "^4.2.0",
"resolve-from": "^5.0.0",
"string-width": "^4.2.3",
- "supports-hyperlinks": "^3.1.0",
+ "supports-hyperlinks": "^3.2.0",
"svg-tags": "^1.0.0",
"table": "^6.9.0",
"write-file-atomic": "^5.0.1"
@@ -6237,17 +6248,20 @@
}
},
"node_modules/tinyglobby": {
- "version": "0.2.10",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz",
- "integrity": "sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==",
+ "version": "0.2.12",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz",
+ "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==",
"dev": true,
"license": "MIT",
"dependencies": {
- "fdir": "^6.4.2",
+ "fdir": "^6.4.3",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/to-regex-range": {
diff --git a/package.json b/package.json
index ccb404a..31889b7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "MMM-Linky",
- "version": "1.1.3",
+ "version": "1.2.0",
"description": "Un module pour récupérer et afficher les données de consommation Linky sur MagicMirror.",
"keywords": [
"MagicMirror",
@@ -35,6 +35,7 @@
"minify": "node installer/minify",
"preinstall": "echo ⚠ Please use: npm run setup && exit 1",
"reset": "node installer/reset",
+ "reset:cache": "node installer/cache",
"setup": "node installer/setup",
"test": "npm run lint",
"test:all": "npm run lint && npm run test:css && npm run test:markdown",
@@ -53,15 +54,15 @@
"node-cron": "^3.0.3"
},
"devDependencies": {
- "@stylistic/eslint-plugin": "^4.1.0",
- "eslint": "^9.21.0",
+ "@stylistic/eslint-plugin": "^4.2.0",
+ "eslint": "^9.22.0",
"eslint-plugin-depend": "^0.12.0",
"eslint-plugin-import-x": "^4.6.1",
- "eslint-plugin-package-json": "^0.26.0",
+ "eslint-plugin-package-json": "^0.26.3",
"markdownlint-cli2": "^0.17.2",
- "stylelint": "^16.14.1",
+ "stylelint": "^16.15.0",
"stylelint-config-standard": "^37.0.0",
"stylelint-prettier": "^5.0.3"
},
- "rev": "250301"
+ "rev": "20250309"
}
diff --git a/src/MMM-Linky.js b/src/MMM-Linky.js
index 895fa4c..36bb564 100644
--- a/src/MMM-Linky.js
+++ b/src/MMM-Linky.js
@@ -7,6 +7,9 @@ Module.register("MMM-Linky", {
debug: 0,
token: "",
prm: "",
+ //apis: ["getDailyConsumption", "getLoadCurve", "getMaxPower", "getDailyProduction", "getProductionLoadCurve"];
+ apis: ["getDailyConsumption"],
+ affichageInterval: 1000 * 15,
periode: 1,
annee_n_minus_1: 1,
couleur: 3,
@@ -21,9 +24,11 @@ Module.register("MMM-Linky", {
start () {
Log.info("[LINKY] MMM-Linky démarré...");
if (this.config.debug) _linky = (...args) => { console.log("[MMM-Linky]", ...args); };
- if (this.config.header) this.data.header = this.getHeaderText();
+ if (this.config.header) this.data.header = "Veuillez patienter, vos données arrivent...";
this.chart = null;
this.ChartJsLoaded = false;
+ this.linkyData = {};
+ this.linkyInterval = null;
this.chartsData = {};
this.timers = [];
this.timers.CRON = null;
@@ -55,10 +60,23 @@ Module.register("MMM-Linky", {
console.error("[LINKY]", payload);
this.displayMessagerie(payload, "warn");
break;
+ case "CONFIG":
+ _linky("Réception de la configuration APIs:", payload);
+ this.config.apis = payload;
+ break;
+ case "INIT":
+ _linky("Réception des premières données:", payload);
+ this.linkyData = payload;
+ this.displayChartInterval();
+ break;
case "DATA":
_linky("Réception des données:", payload);
- this.chartsData = payload;
- this.displayChart();
+ if (payload.getDailyConsumption) this.linkyData.getDailyConsumption = payload.getDailyConsumption;
+ if (payload.getLoadCurve) this.linkyData.getLoadCurve = payload.getLoadCurve;
+ if (payload.getMaxPower) this.linkyData.getMaxPower = payload.getMaxPower;
+ if (payload.getDailyProduction) this.linkyData.getDailyProduction = payload.getDailyProduction;
+ if (payload.getProductionLoadCurve) this.linkyData.getProductionLoadCurve = payload.getProductionLoadCurve;
+ _linky("Mise en place des données:", this.linkyData);
break;
case "TIMERS":
_linky("Réception d'un timer:", payload);
@@ -111,33 +129,73 @@ Module.register("MMM-Linky", {
return wrapper;
},
- getHeaderText () {
+ getHeaderText (type) {
+ if (!this.config.header) return;
+ var text;
const periodTexts = {
- 1: "Consommation électricité de la veille",
- 2: "Consommation électricité des 3 derniers jours",
- 3: "Consommation électricité des 7 derniers jours"
+ 1: "de la veille",
+ 2: "des 3 derniers jours",
+ 3: "des 7 derniers jours"
};
- return periodTexts[this.config.periode] || "Consommation électricité";
+ switch (type) {
+ case "getDailyConsumption":
+ text = `Consommation ${periodTexts[this.config.periode]}`;
+ break;
+ case "getLoadCurve":
+ text = `Consommation ${periodTexts[1]}`;
+ break;
+ case "getMaxPower":
+ text = `Puissance maximale ${periodTexts[this.config.periode]}`;
+ break;
+ case "getDailyProduction":
+ text = `Production ${periodTexts[this.config.periode]}`;
+ break;
+ case "getProductionLoadCurve":
+ text = `Production ${periodTexts[this.config.periode]}`;
+ break;
+ default:
+ text = "Consommation électricité";
+ }
+ return text;
},
- displayChart () {
+ displayChartInterval () {
+ if (this.linkyInterval) return;
+ const call = this.config.apis;
+ this.displayChart(call[0], this.linkyData[call[0]]);
+ if (call.length > 1) {
+ var i = 1;
+ this.linkyInterval = setInterval(() => {
+ if (this.linkyData[call[i]]) {
+ this.displayChart(call[i], this.linkyData[call[i]]);
+ } else {
+ this.displayMessagerie(`Aucune données pour la création du graphique ${call[i]}`, "warn");
+ }
+ i++;
+ i = i % call.length;
+ }, this.config.affichageInterval);
+ }
+ },
+
+ displayChart (type, data) {
const Displayer = document.getElementById("MMM-Linky_Displayer");
Displayer.classList.add("animate__fadeOut");
Displayer.style.setProperty("--animate-duration", "0s");
- if (this.chartsData.labels && this.chartsData.datasets) {
+ if (data?.labels && data?.datasets) {
try {
- this.displayMessagerie(null, null, true);
- this.createChart(this.chartsData.labels, this.chartsData.datasets);
- _linky("Graphique créé avec succès");
- if (this.config.annee_n_minus_1 === 1) this.displayEnergie();
- this.displayUpdate();
+ if (!this.timers.RETRY?.seed) this.displayMessagerie(null, null, true);
+ this.createChart(data.labels, data.datasets, type);
+ _linky(`Graphique créé avec succès pour ${type}`);
+ if (this.config.annee_n_minus_1 === 1) this.displayEnergie(data);
+ this.displayUpdate(data);
} catch (error) {
- console.error("[LINKY] Erreur lors de la création du graphique : ", error);
- this.displayMessagerie("Erreur lors de la création du graphique", "warn");
+ console.error(`[LINKY] Erreur lors de la création du graphique ${type}:`, error);
+ this.displayMessagerie(`Erreur lors de la création du graphique ${type}:`, "warn");
}
} else {
- this.displayMessagerie("Veuillez patienter, vos données arrivent...");
+ console.error(`[LINKY] Erreur de la lecture des données pour ${type}`);
+ this.displayMessagerie(`Erreur de la lecture des données pour ${type}`, "warn");
}
Displayer.classList.remove("animate__fadeOut");
@@ -148,24 +206,23 @@ Module.register("MMM-Linky", {
}, 1000);
},
- displayEnergie () {
- if (this.config.energie === 0) return;
+ displayEnergie (data) {
const Energie = document.getElementById("MMM-Linky_Energie");
- Energie.textContent = this.chartsData.energie.message;
- Energie.className = this.chartsData.energie.color;
+ Energie.textContent = data.energie?.message || "";
+ Energie.className = data.energie?.color;
},
- displayUpdate () {
+ displayUpdate (data) {
if (this.config.updateDate === 0) return;
const Update = document.getElementById("MMM-Linky_Update");
- Update.textContent = this.chartsData.update;
+ Update.textContent = data.update;
},
displayTimer () {
if (this.config.updateNext === 0) return;
const Timer = document.getElementById("MMM-Linky_Timer");
if (this.timers.RETRY?.seed < this.timers.CRON.seed) Timer.textContent = this.timers.RETRY.date;
- else Timer.textContent = this.timers.CRON.date;
+ else Timer.innerText = this.timers.CRON.date;
},
displayMessagerie (text, color, hide) {
@@ -176,9 +233,65 @@ Module.register("MMM-Linky", {
else Messagerie.classList.remove("hidden");
},
- createChart (days, datasets) {
+ createChart (days, datasets, type) {
const chartContainer = document.getElementById("MMM-Linky_Chart");
+ const headerContainer = document.getElementById(this.identifier).getElementsByClassName("module-header")[0];
+ headerContainer.textContent = this.getHeaderText(type);
+
+ var animation = {
+ easing: "easeInOutExpo",
+ duration: 1500
+ };
+ var chartType = "bar";
+
+ if (type === "getLoadCurve" || type === "getProductionLoadCurve") {
+ chartType = "line";
+ }
+
+ if (chartType === "line") {
+ // line animation
+ const totalDuration = 1500;
+ const delayBetweenPoints = totalDuration / days.length;
+ const previousY = (ctx) => (ctx.index === 0 ? ctx.chart.scales.y.getPixelForValue(100) : ctx.chart.getDatasetMeta(ctx.datasetIndex).data[ctx.index - 1].getProps(["y"], true).y);
+ animation = {
+ x: {
+ type: "number",
+ easing: "linear",
+ duration: delayBetweenPoints,
+ from: NaN, // the point is initially skipped
+ delay (ctx) {
+ if (ctx.type !== "data" || ctx.xStarted) {
+ return 0;
+ }
+ ctx.xStarted = true;
+ return ctx.index * delayBetweenPoints;
+ }
+ },
+ y: {
+ type: "number",
+ easing: "linear",
+ duration: delayBetweenPoints,
+ from: previousY,
+ delay (ctx) {
+ if (ctx.type !== "data" || ctx.yStarted) {
+ return 0;
+ }
+ ctx.yStarted = true;
+ return ctx.index * delayBetweenPoints;
+ }
+ }
+ };
+ }
+
+ const displayLegend = () => {
+ if (type === "getLoadCurve") return true;
+ if (type === "getDailyProduction" && this.config.annee_n_minus_1 === 1) return true;
+ if (type === "getProductionLoadCurve") return true;
+ if (type === "getDailyConsumption" && this.config.annee_n_minus_1 === 1) return true;
+ return false;
+ };
+
if (this.chart && typeof this.chart.destroy === "function") {
this.chart.destroy();
}
@@ -187,37 +300,45 @@ Module.register("MMM-Linky", {
Chart.register(ChartDataLabels);
this.chart = new Chart(chartContainer, {
- type: "bar",
+ type: chartType,
data: {
labels: days,
datasets
},
options: {
+ animation,
responsive: true,
plugins: {
legend: {
- display: this.config.annee_n_minus_1 === 1 ? true : false,
+ display: displayLegend(),
labels: { color: "white" }
},
- datalabels: this.config.valuebar === 1
+ datalabels: this.config.valuebar === 1 && chartType !== "line"
? {
color: this.config.valuebartextcolor === 1 ? "white" : "black",
anchor: "center",
align: "center",
rotation: -90,
- formatter: (value) => (value / 1000).toFixed(2)
+ formatter: (value) => {
+ return (value / 1000).toFixed(2);
+ }
}
: false
},
scales: {
y: {
ticks: {
- callback: (value) => `${value / 1000} kWh`,
+ callback: (value) => {
+ if (type === "getLoadCurve") return `${value} W`;
+ if (type === "getProductionLoadCurve") return `${value} W`;
+ if (type === "getMaxPower") return `${value / 1000} kW`;
+ return `${value / 1000} kWh`;
+ },
color: "#fff"
},
title: {
display: true,
- text: "Consommation (kWh)",
+ text: type.includes("Production") ? "Production" : "Consommation",
color: "#fff"
}
},
@@ -228,8 +349,8 @@ Module.register("MMM-Linky", {
}
});
} else {
- console.error("[LINKY] Impossible de créer le graphique : données invalides.");
- this.displayMessagerie("Impossible de créer le graphique : données invalides.", "warn");
+ console.error(`[LINKY] Impossible de créer le graphique ${type}: données invalides.`);
+ this.displayMessagerie(`Impossible de créer le graphique ${type}: données invalides.`, "warn");
}
}
});
diff --git a/src/components/api.js b/src/components/api.js
new file mode 100644
index 0000000..ec5950f
--- /dev/null
+++ b/src/components/api.js
@@ -0,0 +1,70 @@
+var log = () => { /* do nothing */ };
+
+class API {
+ constructor (Tools, config) {
+ this.Linky = null;
+ this.config = config;
+ if (this.config.debug) log = (...args) => { console.log("[LINKY] [API]", ...args); };
+ this.sendError = (error) => Tools.sendError(error);
+ this.api = ["getDailyConsumption", "getLoadCurve", "getMaxPower", "getDailyProduction", "getProductionLoadCurve"];
+
+ }
+
+ // Importation de la librairie linky (dynamic import)
+ async loadLinky () {
+ const loaded = await import("linky");
+ return loaded;
+ }
+
+ // Initialisation de l'api linky
+ async initLinky (callback) {
+ const { Session } = await this.loadLinky();
+ try {
+ this.Linky = new Session(this.config.token, this.config.prm);
+ log("API linky Prête");
+ if (callback) callback();
+ } catch (error) {
+ console.error(`[LINKY] [API] ${error}`);
+ this.sendError(error.message);
+ }
+ }
+
+ // Demande des datas selon l'API
+ request (type, date) {
+ if (this.api.indexOf(type) === -1) {
+ this.sendError(`[API] API non reconnu: ${type}`);
+ return;
+ }
+ return new Promise((resolve) => {
+ if (!this.Linky) {
+ this.initLinky(async () => {
+ resolve(await this.request(type, date));
+ });
+ } else {
+ this.Linky[type](date.startDate, date.endDate)
+ .then((result) => {
+ resolve(result);
+ })
+ .catch((error) => {
+ this.catchError(error, type);
+ resolve({ error: true });
+ });
+ }
+ });
+ }
+
+ catchError (error, type) {
+ var msgError;
+ if (error.response?.status) {
+ console.error(`[LINKY] [API] [${type}] [Erreur ${error.response.status}] ${error.response.message}`);
+ console.error(`[LINKY] [API] [${type}] Description:`, error.response.error);
+ msgError = `(${error.response.status}) ${type}: ${error.response.message}`;
+ } else if (error.message) {
+ console.error(`[LINKY] [API] [Erreur ${error.code}] ${error.message}`);
+ msgError = `(${error.code}) ${type}: ${error.message}`;
+ }
+ if (!msgError) msgError = `${type}: ${error.toString()}`;
+ this.sendError(msgError);
+ }
+}
+module.exports = API;
diff --git a/src/components/chart.js b/src/components/chart.js
new file mode 100644
index 0000000..25163a0
--- /dev/null
+++ b/src/components/chart.js
@@ -0,0 +1,174 @@
+const dayjs = require("dayjs");
+require("dayjs/locale/fr");
+
+var log = () => { /* do nothing */ };
+
+class CHART {
+ constructor (Tools, config) {
+ this.config = config;
+ if (this.config.debug) log = (...args) => { console.log("[LINKY] [CHART]", ...args); };
+ this.sendError = (error) => Tools.sendError(error);
+ this.simpleDay = ["getLoadCurve", "getProductionLoadCurve"];
+ }
+
+ // création des données chartjs
+ setChartValue (type, detail) {
+ const isSimpleDay = this.simpleDay.includes(type);
+ const day = dayjs().subtract(1, "day").locale("fr").format("D MMM YYYY");
+ const days = [];
+ const datasets = [];
+ const colors = this.getChartColors();
+ const { datas, seed } = detail;
+
+ let index = 0;
+ for (const year in datas) {
+ const data = datas[year];
+ const values = data.map((item) => item.value);
+
+ if (index === 0) {
+ if (isSimpleDay) {
+ days.push(...data.map((item) => dayjs(item.date).locale("fr").format("HH:mm")));
+ } else if (type === "getMaxPower") {
+ days.push(...data.map((item) => dayjs(item.date).locale("fr").format("D MMM HH:mm")));
+ } else {
+ days.push(...data.map((item) => dayjs(item.date).locale("fr").format("D MMM")));
+ }
+ }
+
+ datasets.push({
+ label: isSimpleDay ? day : year,
+ data: values,
+ backgroundColor: colors[index],
+ borderColor: colors[index].replace("0.8", "1"),
+ borderWidth: type.includes("Curve") ? 3 : 1,
+ tension: 0.4,
+ pointRadius: 0,
+ pointStyle: false
+ });
+ index++;
+ }
+
+ log("Données des graphiques :", { labels: days, data: datasets });
+
+ if (datasets.length > 1 && datasets[0].data.length !== datasets[1].data.length) {
+ console.warn("[LINKY] [CHART] Il manque des données pour une des 2 années.");
+ console.warn("[LINKY] [CHART] L'affichage risque d'être corrompu.");
+ }
+ const removeEnergie = !(isSimpleDay || type === "getMaxPower");
+
+ return {
+ labels: days,
+ datasets: datasets,
+ energie: removeEnergie && this.config.energie === 1 && this.config.annee_n_minus_1 === 1 ? this.setEnergie(type, datas) : null,
+ update: `Données du ${dayjs(seed).format("DD/MM/YYYY -- HH:mm:ss")}`
+ };
+ }
+
+ // Selection schémas de couleurs
+ getChartColors () {
+ const colorSchemes = {
+ 1: ["rgba(0, 128, 255, 0.8)", "rgba(245, 39, 230, 0.8)"],
+ 2: ["rgba(252, 255, 0, 0.8)", "rgba(13, 255, 0, 0.8)"],
+ 3: ["rgba(255, 255, 255, 0.8)", "rgba(0, 255, 242, 0.8)"],
+ 4: ["rgba(255, 125, 0, 0.8)", "rgba(220, 0, 255, 0.8)"]
+ };
+ return colorSchemes[this.config.couleur] || colorSchemes[1];
+ }
+
+ // cacul des dates périodique
+ calculateDates (type) {
+ const isSimpleDay = this.simpleDay.includes(type);
+ const endDate = dayjs().format("YYYY-MM-DD");
+ var start = dayjs();
+
+ if (isSimpleDay) {
+ start = dayjs(start.subtract(1, "day")).format("YYYY-MM-DD");
+ return { startDate: start, endDate };
+ }
+
+ switch (this.config.periode) {
+ case 1:
+ start = start.subtract(1, "day");
+ break;
+ case 2:
+ start = start.subtract(3, "day");
+ break;
+ case 3:
+ start = start.subtract(7, "day");
+ break;
+ default:
+ console.error(`[LINKY] [CHART] [${type}] Période invalide.`);
+ this.sendError("Période invalide.");
+ return null;
+ }
+
+ if (this.config.annee_n_minus_1 === 1 && type !== "getMaxPower") {
+ start = start.subtract(1, "year");
+ }
+
+ const startDate = dayjs(start).format("YYYY-MM-DD");
+
+ return { startDate, endDate };
+ }
+
+ // Création du message Energie
+ setEnergie (type, data) {
+ const currentYearTotal = this.calculateTotalConsumption(dayjs().get("year"), data);
+ const previousYearTotal = this.calculateTotalConsumption(dayjs().subtract(1, "year").get("year"), data);
+ const isProduction = type.includes("Production") ? true : false;
+
+ var message, color, periodText;
+
+ switch (this.config.periode) {
+ case 1:
+ periodText = "le dernier jour";
+ break;
+ case 2:
+ periodText = "les 3 derniers jours";
+ break;
+ case 3:
+ periodText = "les 7 derniers jours";
+ break;
+ default:
+ periodText = "période inconnue";
+ }
+
+ if (currentYearTotal < previousYearTotal) {
+ if (isProduction) {
+ message = `Attention, votre production d'énergie a baissé sur ${periodText} par rapport à l'année dernière !`;
+ color = "red";
+ } else {
+ message = `Félicitations, votre consomation d'énergie a baissé sur ${periodText} par rapport à l'année dernière !`;
+ color = "green";
+ }
+ } else if (currentYearTotal > previousYearTotal) {
+ if (isProduction) {
+ message = `Félicitations, votre production d'énergie a augmenté sur ${periodText} par rapport à l'année dernière !`;
+ color = "green";
+ } else {
+ message = `Attention, votre consomation d'énergie a augmenté sur ${periodText} par rapport à l'année dernière !`;
+ color = "red";
+ }
+ } else {
+ message = `Votre ${isProduction ? "production" : "consomation"} d'énergie est stable sur ${periodText} par rapport à l'année dernière.`;
+ color = "yellow";
+ }
+
+ return {
+ message: message,
+ color: color
+ };
+ }
+
+ // Calcul de la comsommation totale
+ calculateTotalConsumption (year, datas) {
+ let total = 0;
+ if (datas[year]) {
+ datas[year].forEach((data) => {
+ total += data.value;
+ });
+ }
+ return total;
+ }
+}
+module.exports = CHART;
diff --git a/src/components/fetcher.js b/src/components/fetcher.js
new file mode 100644
index 0000000..3e28008
--- /dev/null
+++ b/src/components/fetcher.js
@@ -0,0 +1,107 @@
+const dayjs = require("dayjs");
+const chart = require("./chart");
+const parser = require("./parser");
+const api = require("./api");
+const files = require("./files");
+
+var log = () => { /* do nothing */ };
+
+class FETCHER {
+ constructor (Tools, config) {
+ this.config = config;
+ if (this.config.debug) log = (...args) => { console.log("[LINKY] [FETCHER]", ...args); };
+ this.sendError = (error) => Tools.sendError(error);
+ this.retryTimer = () => Tools.retryTimer();
+ this.chart = new chart(Tools, this.config);
+ this.parser = new parser(Tools, this.config);
+ this.api = new api(Tools, this.config);
+ this.files = new files(Tools, this.config);
+ this.call = this.config.apis;
+ }
+
+ async refresh () {
+ var datas = {};
+
+ for (const call of this.call) {
+ log("[Refresh] Chargement:", call);
+ datas[call] = await this.getData(call);
+ log("[Refresh] Termimé:", call);
+ }
+ return datas;
+ }
+
+ async loadCache () {
+ var datas = {};
+
+ for (const call of this.call) {
+ log("[Cache] Chargement:", call);
+ const result = await this.files.readData(call);
+ if (!result) datas[call] = await this.getData(call);
+ else {
+ const parsedData = this.parser.parseData(call, result);
+ datas[call] = await this.chart.setChartValue(call, parsedData);
+ log("[Cache] Terminé:", call);
+ }
+ }
+ return datas;
+ }
+
+ async getData (type) {
+ const dates = this.chart.calculateDates(type);
+ if (dates === null) return;
+ log("Dates:", dates);
+
+ var parsedData = {};
+ var error = null;
+
+ const isIgnorePeriode = () => {
+ if (type === "getLoadCurve") return true;
+ if (type === "getProductionLoadCurve") return true;
+ return false;
+ };
+
+ const isIgnoreAnnee_n_minus_1 = () => {
+ if (type === "getLoadCurve") return true;
+ if (type === "getMaxPower") return true;
+ if (type === "getProductionLoadCurve") return true;
+ return false;
+ };
+
+ await this.api.request(type, dates)
+ .then((result) => {
+ if (result.error) {
+ error = true;
+ } else {
+ if (result.start && result.end && result.interval_reading) {
+ result.annee_n_minus_1 = this.config.annee_n_minus_1;
+ result.ignoreAnnee_n_minus_1 = isIgnoreAnnee_n_minus_1();
+ result.periode = this.config.periode;
+ result.ignorePeriode = isIgnorePeriode();
+ result.seed = dayjs().valueOf();
+ result.type = type;
+ log(`[${type}] Données reçues de l'API:`, result);
+ this.files.saveData(type, result);
+ parsedData = this.parser.parseData(type, result);
+ } else {
+ console.error(`[LINKY] [${type}] Format inattendu des données:`, result);
+ if (result.error) {
+ error = `[${type}] ${result.error.error}`;
+ } else {
+ error = `[${type}] Erreur lors de la collecte de données.`;
+ }
+ this.sendError(error);
+ }
+ }
+ });
+
+ if (!error) {
+ log(`[${type}] Données collectées:`, parsedData);
+ const chartData = this.chart.setChartValue(type, parsedData);
+ return chartData;
+ } else {
+ this.retryTimer();
+ return null;
+ }
+ }
+}
+module.exports = FETCHER;
diff --git a/src/components/files.js b/src/components/files.js
new file mode 100644
index 0000000..193d4df
--- /dev/null
+++ b/src/components/files.js
@@ -0,0 +1,104 @@
+const { writeFile, readFile, access, constants } = require("node:fs");
+const path = require("node:path");
+const dayjs = require("dayjs");
+
+var log = () => { /* do nothing */ };
+
+class FILES {
+ constructor (Tools, config) {
+ this.config = config;
+ if (this.config.debug) log = (...args) => { console.log("[LINKY] [FILES]", ...args); };
+ this.dataPath = path.resolve(__dirname, "../data");
+ }
+
+ // Exporte les donnée Charts
+ saveData (type, data) {
+ if (!type) {
+ console.error("[LINKY] [FILES] Type de Données inconnue");
+ return;
+ }
+
+ const file = `${this.dataPath}/${type}.json`;
+
+ if (!data) {
+ console.error(`[LINKY] [FILES] Aucune données à sauvegarder pour ${type}`);
+ return;
+ }
+
+ const jsonData = JSON.stringify(data, null, 2);
+ writeFile(file, jsonData, "utf8", (err) => {
+ if (err) {
+ console.error(`[LINKY] [FILES] [${type}] Erreur lors de l'exportation des données`, err);
+ } else {
+ log(`[${type}] Les données ont été exporté vers`, file);
+ }
+ });
+ }
+
+ // Lecture des fichiers de données Charts
+ readData (type) {
+ if (!type) {
+ console.error("[LINKY] [FILES] Type de Données inconnue");
+ return;
+ }
+
+ const file = `${this.dataPath}/${type}.json`;
+
+ return new Promise((resolve) => {
+ // verifie la presence
+ access(file, constants.F_OK, (error) => {
+ if (error) {
+ log(`[${type}] Pas de fichier cache trouvé`);
+ resolve();
+ return;
+ }
+
+ // lit le fichier
+ readFile(file, (err, data) => {
+ if (err) {
+ console.error(`[LINKY] [FILES] [${type}] Erreur de la lecture du fichier cache!`, err);
+ resolve();
+ return;
+ }
+ const linkyData = JSON.parse(data);
+
+ if (linkyData.type !== type) {
+ console.error(`[LINKY] [FILES] [${type}] Fichier cache invalide!`);
+ resolve();
+ return;
+ }
+
+ if (!linkyData.seed) {
+ console.error(`[LINKY] [FILES] [${type}] Cache invalide!`);
+ resolve();
+ return;
+ }
+
+ if (!linkyData.ignoreAnnee_n_minus_1 && (linkyData.annee_n_minus_1 !== this.config.annee_n_minus_1)) {
+ console.log(`[LINKY] [FILES] [${type}] La configuration annee_n_minus_1 a changé.`);
+ resolve();
+ return;
+ }
+
+ if (!linkyData.ignorePeriode && (linkyData.periode !== this.config.periode)) {
+ console.log(`[LINKY] [FILES] [${type}] La configuration periode a changé.`);
+ resolve();
+ return;
+ }
+
+ const now = dayjs().valueOf();
+ const seed = dayjs(linkyData.seed).format("DD/MM/YYYY -- HH:mm:ss");
+ const next = dayjs(linkyData.seed).add(12, "hour").valueOf();
+ if (now > next) {
+ console.log(`[LINKY] [FILES] [${type}] Les dernières données reçues sont > 12h, utilisation de l'API...`);
+ resolve();
+ } else {
+ console.log(`[LINKY] [FILES] [${type}] Utilisation du cache ${seed}`);
+ resolve(linkyData);
+ }
+ });
+ });
+ });
+ }
+}
+module.exports = FILES;
diff --git a/src/components/parser.js b/src/components/parser.js
new file mode 100644
index 0000000..a3a5627
--- /dev/null
+++ b/src/components/parser.js
@@ -0,0 +1,100 @@
+const dayjs = require("dayjs");
+const isBetween = require("dayjs/plugin/isBetween");
+const isLeapYear = require("dayjs/plugin/isLeapYear");
+
+dayjs.extend(isBetween);
+dayjs.extend(isLeapYear);
+
+var log = () => { /* do nothing */ };
+
+class PARSER {
+ constructor (Tools, config) {
+ this.config = config;
+ if (this.config.debug) log = (...args) => { console.log("[LINKY] [PARSER]", ...args); };
+ }
+
+ parseData (type, result) {
+ log("Démarrage...");
+ var datas = {};
+ var added = 0;
+ var LeapYear = false;
+ const seed = result.seed;
+
+ result.interval_reading.forEach((reading) => {
+ const year = dayjs(reading.date).get("year");
+ const value = parseFloat(reading.value);
+
+ if (!datas[year]) datas[year] = [];
+
+ if (type.includes("getDaily") && this.config.annee_n_minus_1 === 1) {
+ var current = dayjs().set("hour", 0).set("minute", 0).set("second", 0);
+ const currentIsLeapYear = current.isLeapYear();
+ const currentYear = current.year();
+
+ var testDate = current.subtract(1, "day");
+ switch (this.config.periode) {
+ case 1:
+ testDate = testDate.subtract(1, "day");
+ break;
+ case 2:
+ testDate = testDate.subtract(3, "day");
+ break;
+ case 3:
+ testDate = testDate.subtract(7, "day");
+ break;
+ default:
+ testDate = current;
+ break;
+ }
+ if (currentYear !== year) {
+ testDate = testDate.subtract(1, "year");
+ current = current.subtract(1, "day").subtract(1, "year");
+ }
+ const testDateIsLeapYear = testDate.isLeapYear();
+
+ if (dayjs(reading.date).isBetween(testDate, current)) {
+ // LeapYear testing
+ if (testDateIsLeapYear && dayjs(reading.date).month() === 1 && dayjs(reading.date).date() === 29) {
+ log(`Année bissextile: ${year} -> ignore 29/02`);
+ } else {
+ if (currentIsLeapYear && dayjs(reading.date).month() === 1 && dayjs(reading.date).date() === 29) {
+ LeapYear = true;
+ log(`Année bissextile pour ${year}:`, { date: `${year - 1}-02-29`, value: 0 });
+ if (!datas[year - 1]) datas[year - 1] = [];
+ datas[year - 1].push({ date: `${year - 1}-02-29`, value: 0 });
+ added++;
+ }
+ log(`Ajoute pour ${year}:`, { date: reading.date, value });
+ datas[year].push({ date: reading.date, value });
+ added++;
+ }
+ }
+ } else {
+ log(`Ajoute pour ${year}:`, { date: reading.date, value });
+ datas[year].push({ date: reading.date, value });
+ added++;
+ }
+ });
+ if (LeapYear) {
+ // a voir a la prochaine Année bissextile...
+ for (const year in datas) {
+ log(`Classements des dates pour ${year}...`);
+ datas[year].sort((a, b) => {
+ if (a.date < b.date) {
+ return -1;
+ }
+ if (a.date > b.date) {
+ return 1;
+ }
+ return 0;
+ });
+ log(`Suppression des premières données pour ${year}:`, datas[year][0]);
+ datas[year].shift();
+ added--;
+ }
+ }
+ log(`Terminé: ${added} dates trouvées.`);
+ return { datas, seed: seed };
+ }
+}
+module.exports = PARSER;
diff --git a/src/components/rejection.js b/src/components/rejection.js
new file mode 100644
index 0000000..df5c11f
--- /dev/null
+++ b/src/components/rejection.js
@@ -0,0 +1,46 @@
+var log = () => { /* do nothing */ };
+
+class REJECTION {
+ constructor (Tools, config) {
+ this.config = config;
+ if (this.config.debug) log = (...args) => { console.log("[LINKY] [REJECTION]", ...args); };
+ this.sendError = (error) => Tools.sendError(error);
+ }
+
+ catchUnhandledRejection () {
+ log("Live Scan Démarré...");
+ process.on("unhandledRejection", (error) => {
+ // detect any errors of node_helper of MMM-Linky
+ if (error.stack.includes("MMM-Linky/node_helper.js")) {
+ console.error(`[LINKY] [REJECTION] ${this._citation()}`);
+ console.error("[LINKY] [REJECTION] ---------");
+ console.error("[LINKY] [REJECTION] node_helper Error:", error);
+ console.error("[LINKY] [REJECTION] ---------");
+ console.error("[LINKY] [REJECTION] Merci de signaler cette erreur aux développeurs");
+ this.sendError(`[Core Crash] ${error}`);
+ } else {
+ // from other modules (must never happen... but...)
+ console.error("-Other-", error);
+ }
+ });
+ }
+
+ _citation () {
+ let citations = [
+ "J'ai glissé, chef !",
+ "Mirabelle appelle Églantine...",
+ "Mais tremblez pas comme ça, ça fait de la mousse !!!",
+ "C'est dur d'être chef, Chef ?",
+ "Un lapin, chef !",
+ "Fou afez trop chaud ou fou afez trop froid ? ",
+ "Restez groupire!",
+ "On fait pas faire des mouvements respiratoires à un type qu'a les bras cassés !!!",
+ "Si j’connaissais l’con qui a fait sauter l’pont...",
+ "Le fil rouge sur le bouton rouge, le fil bleu sur le bouton bleu."
+ ];
+ const random = Math.floor(Math.random() * citations.length);
+ return citations[random];
+ }
+}
+
+module.exports = REJECTION;
diff --git a/src/components/timers.js b/src/components/timers.js
new file mode 100644
index 0000000..3a8785e
--- /dev/null
+++ b/src/components/timers.js
@@ -0,0 +1,80 @@
+const cron = require("node-cron");
+const { CronExpressionParser } = require("cron-parser");
+const dayjs = require("dayjs");
+
+var log = () => { /* do nothing */ };
+
+class TIMERS {
+ constructor (Tools, config) {
+ this.config = config;
+ if (this.config.debug) log = (...args) => { console.log("[LINKY] [TIMERS]", ...args); };
+ this.sendSocketNotification = (...args) => Tools.sendSocketNotification(...args);
+ this.refreshData = () => Tools.refreshData();
+ this.timers = {};
+ this.timer = null;
+ this.cronExpression = "0 0 12 * * *";
+ }
+
+ // Retry Timer en cas d'erreur, relance la requete 2 heures apres
+ retryTimer () {
+ if (this.timer) this.clearRetryTimer();
+ this.timer = setTimeout(() => {
+ log("Retry-Timer: Démarrage");
+ this.refreshData();
+ }, 1000 * 60 * 60 * 2);
+ let job = dayjs(dayjs() + this.timer._idleNext.expiry);
+ log("Retry-Timer planifié:", job.format("[Le] DD/MM/YYYY -- HH:mm:ss"));
+ this.sendTimer(job.valueOf(), job.format("[Le] DD/MM/YYYY -- HH:mm:ss"), "RETRY");
+ }
+
+ // Retry Timer kill
+ clearRetryTimer () {
+ if (!this.timer) return;
+ log("Retry-Timer: Arrêt");
+ clearTimeout(this.timer);
+ this.timer = null;
+ this.sendTimer(null, null, "RETRY");
+ }
+
+ // Récupération planifié des données
+ scheduleDataFetch () {
+ const randomMinute = Math.floor(Math.random() * 15);
+ const randomSecond = Math.floor(Math.random() * 59);
+
+ this.cronExpression = `${randomSecond} ${randomMinute} 12 * * *`;
+ cron.schedule(this.cronExpression, () => {
+ log("Exécution de la tâche planifiée de récupération des données.");
+ this.refreshData();
+ this.displayNextCron();
+ });
+ this.displayNextCron();
+ }
+
+ // Affiche la prochaine tache Cron
+ displayNextCron () {
+ const next = CronExpressionParser.parse(this.cronExpression, { tz: "Europe/Paris" });
+ let nextCron = dayjs(next.next().toString());
+ log("Prochaine tâche planifiée:", nextCron.format("[Le] DD/MM/YYYY -- HH:mm:ss"));
+ this.sendTimer(nextCron.valueOf(), nextCron.format("[Le] DD/MM/YYYY -- HH:mm:ss"), "CRON");
+ }
+
+ // Envoi l'affichage de la date du prochain update
+ sendTimer (seed, date, type) {
+ let timer = {
+ seed: seed,
+ date: date,
+ type: type
+ };
+ this.timers[type] = timer;
+ this.sendSocketNotification("TIMERS", timer);
+ }
+
+ // envoi l'affichage de tous les timers (server mode)
+ sendTimers () {
+ const timers = Object.values(this.timers);
+ timers.forEach((timer) => {
+ this.sendSocketNotification("TIMERS", timer);
+ });
+ }
+}
+module.exports = TIMERS;
diff --git a/src/node_helper.js b/src/node_helper.js
index d57c731..c111f93 100644
--- a/src/node_helper.js
+++ b/src/node_helper.js
@@ -1,35 +1,17 @@
-const { writeFile, readFile, access, constants } = require("node:fs");
-const path = require("node:path");
-const cron = require("node-cron");
-const { CronExpressionParser } = require("cron-parser");
-const dayjs = require("dayjs");
-const isBetween = require("dayjs/plugin/isBetween");
-
-dayjs.extend(isBetween);
const NodeHelper = require("node_helper");
-var log = () => { /* do nothing */ };
+const timers = require("./components/timers");
+const rejection = require("./components/rejection");
+const fetcher = require("./components/fetcher");
module.exports = NodeHelper.create({
- start () {
- this.Linky = null;
- this.config = null;
- this.dates = [];
- this.timer = null;
- this.consumptionData = {};
- this.cronExpression = "0 0 14 * * *";
- this.error = null;
- this.dataFile = path.resolve(__dirname, "linkyData.json");
- this.timers = {};
- },
-
socketNotificationReceived (notification, payload) {
switch (notification) {
case "INIT":
if (!this.ready) {
this.config = payload;
this.ready = true;
- this.chartData = {};
+ this.data = {};
this.initialize();
} else {
this.initWithCache();
@@ -40,431 +22,64 @@ module.exports = NodeHelper.create({
// intialisation de MMM-Linky
async initialize () {
- this.catchError();
console.log(`[LINKY] MMM-Linky Version: ${require("./package.json").version} Revison: ${require("./package.json").rev}`);
- if (this.config.debug) log = (...args) => { console.log("[LINKY]", ...args); };
- if (this.config.dev) log("Config:", this.config);
+ const apis = ["getDailyConsumption", "getLoadCurve", "getMaxPower", "getDailyProduction", "getProductionLoadCurve"];
+ const Tools = {
+ sendError: (error) => {
+ this.error = error;
+ this.sendSocketNotification("ERROR", this.error);
+ },
+ sendSocketNotification: (...args) => this.sendSocketNotification(...args),
+ retryTimer: () => this.tasks.retryTimer(),
+ refreshData: async () => {
+ this.tasks.clearRetryTimer();
+ this.data = await this.fetcher.refresh();
+ this.sendSocketNotification("DATA", this.data);
+ }
+ };
- await this.readChartData();
- if (Object.keys(this.chartData).length) {
- this.sendSocketNotification("DATA", this.chartData);
- }
- else {
- this.getConsumptionData();
- }
- this.scheduleDataFetch();
- },
+ this.rejection = new rejection(Tools, this.config);
+ this.rejection.catchUnhandledRejection();
- // Initialisation de l'api linky
- async initLinky (callback) {
- const { Session } = await this.loadLinky();
- try {
- this.Linky = new Session(this.config.token, this.config.prm);
- log("API linky Prête");
- if (callback) callback();
- } catch (error) {
- console.error(`[LINKY] ${error}`);
- this.error = error.message;
+ if (!Array.isArray(this.config.apis)) {
+ this.error = "[config] Les APIs doivent être inscrite dans le tableau apis:[]";
this.sendSocketNotification("ERROR", this.error);
- }
- },
-
- // Utilisation du cache interne lors d'une utilisation du "mode Server"
- initWithCache () {
- console.log(`[LINKY] [Cache] MMM-Linky Version: ${require("./package.json").version} Revison: ${require("./package.json").rev}`);
- if (this.error) this.sendSocketNotification("ERROR", this.error);
- if (Object.keys(this.chartData).length) this.sendSocketNotification("DATA", this.chartData);
- if (Object.keys(this.timers).length) this.sendTimers();
- },
-
- // Récupération planifié des données
- scheduleDataFetch () {
- const randomMinute = Math.floor(Math.random() * 59);
- const randomSecond = Math.floor(Math.random() * 59);
-
- this.cronExpression = `${randomSecond} ${randomMinute} 14 * * *`;
- cron.schedule(this.cronExpression, () => {
- log("Exécution de la tâche planifiée de récupération des données.");
- this.getConsumptionData();
- this.displayNextCron();
- });
- this.displayNextCron();
- },
-
- // Récupération des données
- async getConsumptionData () {
- this.Dates = this.calculateDates();
- if (this.Dates === null) return;
- log("Dates:", this.Dates);
-
- if (!this.Linky) {
- this.initLinky(() => this.getConsumptionData());
return;
}
- this.consumptionData = {};
- var error = 0;
-
- await this.sendConsumptionRequest(this.Dates).then((result) => {
- if (result.start && result.end && result.interval_reading) {
- log("Données reçues de l'API :", result);
-
- result.interval_reading.forEach((reading) => {
- const year = dayjs(reading.date).get("year");
- const day = dayjs(reading.date).get("date");
- const month = dayjs(reading.date).get("month") + 1;
- const value = parseFloat(reading.value);
-
- if (!this.consumptionData[year]) this.consumptionData[year] = [];
- if (this.config.annee_n_minus_1 === 1) {
- var current = dayjs().set("hour", 0).set("minute", 0).set("second", 0);
- const currentYear = current.year();
- var testDate = current.subtract(1, "day");
- switch (this.config.periode) {
- case 1:
- testDate = testDate.subtract(1, "day");
- break;
- case 2:
- testDate = testDate.subtract(3, "day");
- break;
- case 3:
- testDate = testDate.subtract(7, "day");
- break;
- default:
- testDate = current;
- break;
- }
- if (currentYear !== year) {
- testDate = testDate.subtract(1, "year");
- current = current.subtract(1, "day").subtract(1, "year");
- }
- if (dayjs(reading.date).isBetween(testDate, current)) {
- this.consumptionData[year].push({ day, month, value });
- }
- } else {
- this.consumptionData[year].push({ day, month, value });
- }
- });
- } else {
- error = 1;
- console.error("[LINKY] Format inattendu des données :", result);
- if (result.error) {
- this.error = result.error.error;
- this.sendSocketNotification("ERROR", this.error);
- } else {
- this.error = "Erreur lors de la collecte de données.";
- this.sendSocketNotification("ERROR", this.error);
- }
- }
- });
-
- if (!error) {
- log("Données de consommation collecté:", this.consumptionData);
- this.error = null;
- this.clearRetryTimer();
- this.setChartValue();
- } else {
- log("Il y a des Erreurs API...");
- this.retryTimer();
+ if (!this.config.apis.length) {
+ this.error = "[config] Veuillez spécifier une API dans apis:[]";
+ this.sendSocketNotification("ERROR", this.error);
+ return;
}
- },
-
- // création des données chartjs
- setChartValue () {
- const days = [];
- const datasets = [];
- const colors = this.getChartColors();
-
- let index = 0;
- for (const year in this.consumptionData) {
- const data = this.consumptionData[year].sort((a, b) => {
- if (a.month === b.month) {
- return a.day - b.day;
- }
- return a.month - b.month;
- });
- const values = data.map((item) => item.value);
+ const uniqAPI = [...new Set(this.config.apis)];
+ this.config.apis = uniqAPI;
- if (index === 0) {
- days.push(
- ...data.map(
- (item) => `${item.day} ${["Error", "janv.", "févr.", "mars", "avr.", "mai", "juin", "juil.", "août", "sept.", "oct.", "nov.", "déc."][item.month]}`
- )
- );
+ for (const api of this.config.apis) {
+ if (!apis.includes(api)) {
+ this.error = `[config.apis] L'api ${api} n'est pas valide.`;
+ this.sendSocketNotification("ERROR", this.error);
+ return;
}
-
- datasets.push({
- label: year,
- data: values,
- backgroundColor: colors[index],
- borderColor: colors[index].replace("0.8", "1"),
- borderWidth: 1
- });
- index++;
}
- log("Données des graphiques :", { labels: days, data: datasets });
- this.chartData = {
- labels: days,
- datasets: datasets,
- energie: this.config.annee_n_minus_1 === 1 ? this.setEnergie() : null,
- update: `Données du ${dayjs().format("DD/MM/YYYY -- HH:mm:ss")}`,
- seed: dayjs().valueOf()
- };
- this.sendSocketNotification("DATA", this.chartData);
- this.saveChartData();
- },
+ this.sendSocketNotification("CONFIG", this.config.apis);
- // Selection schémas de couleurs
- getChartColors () {
- const colorSchemes = {
- 1: ["rgba(245, 234, 39, 0.8)", "rgba(245, 39, 230, 0.8)"],
- 2: ["rgba(252, 255, 0, 0.8)", "rgba(13, 255, 0, 0.8)"],
- 3: ["rgba(255, 255, 255, 0.8)", "rgba(0, 255, 242, 0.8)"],
- 4: ["rgba(255, 125, 0, 0.8)", "rgba(220, 0, 255, 0.8)"]
- };
- return colorSchemes[this.config.couleur] || colorSchemes[1];
- },
+ this.tasks = new timers(Tools, this.config);
+ this.tasks.scheduleDataFetch();
- // Demande des datas selon l'API
- sendConsumptionRequest (date) {
- return new Promise((resolve) => {
- this.Linky.getDailyConsumption(date.startDate, date.endDate).then((result) => {
- resolve(result);
- });
- });
- },
+ this.fetcher = new fetcher(Tools, this.config);
+ this.data = await this.fetcher.loadCache();
- // Importation de la librairie linky (dynamic impor)
- async loadLinky () {
- const loaded = await import("linky");
- return loaded;
+ this.sendSocketNotification("INIT", this.data);
},
- // cacul des dates périodique
- calculateDates () {
- const endDate = dayjs().format("YYYY-MM-DD");
- var start = dayjs();
-
- switch (this.config.periode) {
- case 1:
- start = start.subtract(1, "day");
- break;
- case 2:
- start = start.subtract(3, "day");
- break;
- case 3:
- start = start.subtract(7, "day");
- break;
- default:
- console.error("[LINKY] periode invalide.");
- this.sendSocketNotification("ERROR", "periode invalide.");
- return null;
- }
-
- if (this.config.annee_n_minus_1 === 1) start = start.subtract(1, "year");
- const startDate = dayjs(start).format("YYYY-MM-DD");
-
- return { startDate, endDate };
- },
-
- // Création du message Energie
- setEnergie () {
- const currentYearTotal = this.calculateTotalConsumption(dayjs().get("year"));
- const previousYearTotal = this.calculateTotalConsumption(dayjs().subtract(1, "year").get("year"));
-
- var message, color, periodText;
-
- switch (this.config.periode) {
- case 1:
- periodText = "le dernier jour";
- break;
- case 2:
- periodText = "les 3 derniers jours";
- break;
- case 3:
- periodText = "les 7 derniers jours";
- break;
- default:
- periodText = "période inconnue";
- }
-
- if (currentYearTotal < previousYearTotal) {
- message = `Félicitations, votre consommation d'énergie a baissé sur ${periodText} par rapport à l'année dernière !`;
- color = "green";
- } else if (currentYearTotal > previousYearTotal) {
- message = `Attention, votre consommation d'énergie a augmenté sur ${periodText} par rapport à l'année dernière !`;
- color = "red";
- } else {
- message = `Votre consommation d'énergie est stable sur ${periodText} par rapport à l'année dernière.`;
- color = "yellow";
- }
-
- return {
- message: message,
- color: color
- };
- },
-
- // Calcul de la comsommation totale
- calculateTotalConsumption (year) {
- let total = 0;
- if (this.consumptionData[year]) {
- this.consumptionData[year].forEach((data) => {
- total += data.value;
- });
- }
- return total;
- },
-
- // Retry Timer en cas d'erreur, relance la requete 2 heures apres
- retryTimer () {
- if (this.timer) this.clearRetryTimer();
- this.timer = setTimeout(() => {
- log("Retry-Timer: Démarrage");
- this.getConsumptionData();
- }, 1000 * 60 * 60 * 2);
- let job = dayjs(dayjs() + this.timer._idleNext.expiry);
- log("Retry-Timer planifié:", job.format("[Le] DD/MM/YYYY -- HH:mm:ss"));
- this.sendTimer(job.valueOf(), job.format("[Le] DD/MM/YYYY -- HH:mm:ss"), "RETRY");
- },
-
- // Retry Timer kill
- clearRetryTimer () {
- if (this.timer) log("Retry-Timer: Arrêt");
- clearTimeout(this.timer);
- this.timer = null;
- this.sendTimer(null, null, "RETRY");
- },
-
- // Affiche la prochaine tache Cron
- displayNextCron () {
- const next = CronExpressionParser.parse(this.cronExpression, { tz: "Europe/Paris" });
- let nextCron = dayjs(next.next().toString());
- log("Prochaine tâche planifiée:", nextCron.format("[Le] DD/MM/YYYY -- HH:mm:ss"));
- this.sendTimer(nextCron.valueOf(), nextCron.format("[Le] DD/MM/YYYY -- HH:mm:ss"), "CRON");
- },
-
- // Exporte les donnée Charts vers linkyData.json
- saveChartData () {
- const jsonData = JSON.stringify(this.chartData, null, 2);
- writeFile(this.dataFile, jsonData, "utf8", (err) => {
- if (err) {
- console.error("Erreur lors de l'exportation des données", err);
- } else {
- log("Les données ont été exporté vers", this.dataFile);
- }
- });
- },
-
- // Lecture du fichier linkyData.json
- readChartData () {
- return new Promise((resolve) => {
- // verifie la presence
- access(this.dataFile, constants.F_OK, (error) => {
- if (error) {
- log("Pas de fichier cache trouvé");
- this.chartData = {};
- resolve();
- return;
- }
-
- // lit le fichier
- readFile(this.dataFile, (err, data) => {
- if (err) {
- console.error("[LINKY] Erreur de la lecture du fichier cache!", err);
- this.chartData = {};
- resolve();
- return;
- }
- const linkyData = JSON.parse(data);
- const now = dayjs().valueOf();
- const seed = dayjs(linkyData.seed).format("DD/MM/YYYY -- HH:mm:ss");
- const next = dayjs(linkyData.seed).add(12, "hour").valueOf();
- if (now > next) {
- log("Les dernieres données reçues sont > 12h, utilisation de l'API...");
- this.chartData = {};
- } else {
- log("Utilisation du cache:", seed);
- this.chartData = linkyData;
- }
- resolve();
- });
- });
- });
- },
-
- // Envoi l'affichage de la date du prochain update
- sendTimer (seed, date, type) {
- let timer = {
- seed: seed,
- date: date,
- type: type
- };
- this.timers[type] = timer;
- this.sendSocketNotification("TIMERS", timer);
- },
-
- // envoi l'affichage de tous les timers (server mode)
- sendTimers () {
- const timers = Object.values(this.timers);
- timers.forEach((timer) => {
- this.sendSocketNotification("TIMERS", timer);
- });
- },
-
- catchError () {
- process.on("unhandledRejection", (error) => {
- // catch conso API error and Enedis only
- if (error.stack.includes("MMM-Linky/node_modules/linky/") && error.response) {
- // catch Enedis error
- if (error.response.status && error.response.message && error.response.error) {
- console.error(`[LINKY] [${error.response.status}] ${error.response.message}`);
- this.error = error.response.message;
- this.sendSocketNotification("ERROR", this.error);
- } else {
- // catch Conso API error
- if (error.message) {
- console.error(`[LINKY] [${error.code}] ${error.message}`);
- this.error = `[${error.code}] ${error.message}`;
- this.sendSocketNotification("ERROR", this.error);
- } else {
- // must never Happen...
- console.error("[LINKY]", error);
- }
- }
- this.retryTimer();
- } else {
- // detect any errors of node_helper of MMM-Linky
- if (error.stack.includes("MMM-Linky/node_helper.js")) {
- console.error(`[LINKY] ${this._citation()}`);
- console.error("[LINKY] Merci de signaler cette erreur aux développeurs");
- console.error("[LINKY] ---------");
- console.error("[LINKY] node_helper Error:", error);
- console.error("[LINKY] ---------");
- } else {
- // from other modules (must never happen... but...)
- console.error("-Other-", error);
- }
- }
- });
- },
-
- _citation () {
- let citations = [
- "J'ai glissé, chef !",
- "Mirabelle appelle Églantine...",
- "Mais tremblez pas comme ça, ça fait de la mousse !!!",
- "C'est dur d'être chef, Chef ?",
- "Un lapin, chef !",
- "Fou afez trop chaud ou fou afez trop froid ? ",
- "Restez groupire!",
- "On fait pas faire des mouvements respiratoires à un type qu'a les bras cassés !!!",
- "Si j’connaissais l’con qui a fait sauter l’pont...",
- "Le fil rouge sur le bouton rouge, le fil bleu sur le bouton bleu."
- ];
- const random = Math.floor(Math.random() * citations.length);
- return citations[random];
+ // Utilisation du cache interne lors d'une utilisation du "mode Server"
+ initWithCache () {
+ console.log(`[LINKY] [Cache] MMM-Linky Version: ${require("./package.json").version} Revison: ${require("./package.json").rev}`);
+ if (this.error) this.sendSocketNotification("ERROR", this.error);
+ if (this.data) this.sendSocketNotification("INIT", this.data);
+ if (Object.keys(this.tasks.timers).length) this.tasks.sendTimers();
}
});