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 -![Conso 7 derniers jours](https://github.com/user-attachments/assets/055eef27-43bb-478c-a2cb-16a451bac5b4) -![Conso 3 derniers jours](https://github.com/user-attachments/assets/6dacfd38-d78e-4cb3-be22-be8aec980729) -![Conso veille](https://github.com/user-attachments/assets/6e965953-0c5d-466e-accd-40d09ae3ab71) +![APIs](https://github.com/user-attachments/assets/e3497a96-6a3a-4322-b9cb-fdd8bcfe9688) +![Conso 7 derniers jours JPG](https://github.com/user-attachments/assets/fd2412cf-6cb1-4171-a3b7-1a9300ab2cc6) +![Conso 3 derniers jours JPG](https://github.com/user-attachments/assets/c6658811-e6f4-4a5d-8e2c-ea0a53189fea) +![Conso veille](https://github.com/user-attachments/assets/2e50eac0-71d2-44b4-9c78-0e9f4f1b80db) Possibilité de choisir entre 4 thèmes de couleur pour le graphique et d'afficher les valeurs dans les barres : -![WithDataLabel](https://github.com/user-attachments/assets/a4196ed6-2289-487d-a4dc-aee6fb35ff06) -![Capture](https://github.com/user-attachments/assets/52c76634-4543-41e0-be96-27326745fa3d) +![WithDataLabel](https://github.com/user-attachments/assets/3ee608d5-3127-4adb-b6c3-2a3a07f6111e) +![Capture](https://github.com/user-attachments/assets/a4d7366f-9f04-4a02-afaf-221caa9e1872) ## 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(); } });