From c53f6ac5b3eb602911b497dfabd6e14d4048afd5 Mon Sep 17 00:00:00 2001 From: Boris Kubiak Date: Tue, 23 Jul 2024 15:08:57 +0200 Subject: [PATCH] Add costs computing feature --- CONTRIBUTING.md | 2 ++ README.md | 90 ++++++++++++++++++++++++++++++++++++++++++++-- config.yaml | 12 ++++++- src/config.test.ts | 12 +++++-- src/config.ts | 44 +++++++++++++++++++++-- src/cost.test.ts | 79 ++++++++++++++++++++++++++++++++++++++++ src/cost.ts | 72 +++++++++++++++++++++++++++++++++++++ src/format.ts | 15 +++++--- src/ha.ts | 49 +++++++++++++------------ src/history.ts | 12 ++++--- src/index.ts | 79 +++++++++++++++++++++++++++++++--------- src/linky.ts | 8 ++--- 12 files changed, 413 insertions(+), 61 deletions(-) create mode 100644 src/cost.test.ts create mode 100644 src/cost.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a229ad6..c6a25af 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,6 +8,8 @@ - Click Terminal > Run Task > Start Home Assistant - Open [localhost:7123](http://localhost:7123) - Install the add-on and configure it +- To access the add-on logs, open the terminal in VSCode, and run `docker logs addon_local_linky -f` +- Click the "rebuild" button in the add-on configuration to rebuild the add-on - Enjoy! ## Building locally diff --git a/README.md b/README.md index 31aa8fd..1230bf5 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Pour utiliser cet add-on, il vous faut : ## Configuration -Une fois l'add-on installé, rendez-vous dans l'onglet _Configuration_. +Une fois l'add-on installé, rendez-vous dans l'onglet _Configuration_ puis dans l'encadré `meters` La configuration YAML de base comporte 2 compteurs : @@ -93,6 +93,7 @@ Pour visualiser les données de **HA Linky** dans vos tableaux de bord d'énergi - Cliquez [ici](https://my.home-assistant.io/redirect/config_energy/), ou ouvrez le menu _Paramètres_ / _Settings_, puis _Tableaux de bord_ / _Dashboards_, puis _Énergie_ / _Energy_ - Dans la section _Réseau électrique_ / _Electricity grid_, cliquez sur _Ajouter une consommation_ / _Add consumption_ - Choisissez la statistique correspondant au `name` que vous avez choisi à l'étape de configuration +- Si vous avez configuré une tarification, cliquez sur _Utiliser une entité de suivi des coûts totaux_, et choisissez la statistique équivalente dont le nom finit par _(costs)_ - Cliquez sur _Enregistrer_ / _Save_ ### Bon à savoir @@ -125,6 +126,90 @@ La démarche à suivre est la suivante : - Repassez l'action du compteur à `sync` et redémarrez l'add-on - Si un fichier CSV correspondant à votre PRM est trouvé, HA Linky l'utilisera pour initialiser les données au lieu d'appeler l'API. +### Calcul des coûts + +À partir de la version **1.5.0**, vous pouvez fournir une configuration de tarification pour que HA Linky calcule le coût de votre consommation. + +La configuration des tarifs est optionnelle, et s'écrit dans l'encadré `costs` de l'onglet _Configuration_, sous forme de liste de tarifs + +Chaque item de la liste peut recevoir les paramètres suivants : + +| Paramètre | Description | Optionnel | +| ------------ | --------------------------------------------------------------------------------------- | --------- | +| `price` | Coût du kWh en € | **Non** | +| `prm` | Numéro de PRM en consommation. Par défaut, tous les PRMs en consommation sont concernés | Oui | +| `after` | Heure à partir de laquelle ce tarif est valable, au format _"HH:00"_ | Oui | +| `before` | Heure à partir de laquelle ce tarif n'est plus valable, au format _"HH:00"_ | Oui | +| `weekday` | Jours de la semaine pour lesquels ce tarif est valabe (voir exemple ci-dessous) | Oui | +| `start_date` | Date à partir de laquelle ce tarif est valable, au format _"YYYY-MM-DD"_ | Oui | +| `end_date` | Date à partir de laquelle ce tarif n'est plus valable, au format _"YYYY-MM-DD"_ | Oui | + +#### Exemples + +Configuration la plus simple : `0,23 € / kWh` quelle que soit la date ou l'heure + +```yaml +- price: 0.23 +``` + +Configuration HP/HC : `0,21 € / kWh` de 22h à 6h et de 12h à 14h, et `0,25 €` / kWh le reste du temps. + +**N.B :** Il faut configurer séparément la période minuit - 6h et la période 22h - minuit + +```yaml +- price: 0.21 + before: '06:00' +- price: 0.25 + after: '06:00' + before: '12:00' +- price: 0.21 + after: '12:00' + before: '14:00' +- price: 0.25 + after: '14:00' + before: '22:00' +- price: 0.21 + after: '22:00' +``` + +Configuration par jour de la semaine : `0,24 € / kWh` la semaine, `0,22 € / kWh` le week-end + +```yaml +- price: 0.24 + weekday: + - mon + - tue + - wed + - thu + - fri +- price: 0.22 + weekday: + - sat + - sun +``` + +Tarif qui évolue au cours du temps : `0,21 € / kWh` jusqu'au 30 juin inclus, `0,22 € / kWh` en juillet et août, puis `0,23 € / kWh` à partir du 1 septembre + +```yaml +- price: 0.21 + end_date: '2024-07-01' +- price: 0.22 + start_date: '2024-07-01' + end_date: '2024-09-01' +- price: 0.23 + start_date: '2024-09-01' +``` + +#### Notes concernant le calcul des coûts + +- Vous pouvez combiner **tous** les paramètres (horaires, jours de la semaine, dates, prm), pour personnaliser au maximum le calcul des coûts +- L'ajout des coûts au tableau de bord Énergie s'effectue en choisissant _Utiliser une entité de suivi des coûts totaux_ dans la fenêtre de configuration de la consommation +- Le calcul des coûts est effectué en même temps que la consommation est importée dans Home Assistant. Il faudra faire une remise à zéro si vous souhaitez recalculer le coût des consommations déjà importées. +- La configuration des horaires ne fonctionne que pour les heures piles, autrement dit, les minutes différentes de `:00` n'auront aucun effet +- Si plusieurs items de la liste sont valides au même moment (chevauchement d'horaires ou de dates par exemple), HA Linky choisira l'item le plus haut placé dans la liste +- Assurez-vous d'entourer les heures et les dates par des guillemets doubles `"` pour être certain que celles-ci soient bien interprétées par HA Linky +- Vous pouvez vérifier le coût calculé d'une heure en particulier en vous rendant dans _Outils de développement_, onglet _Statistiques_, puis en cliquant sur l'icône la plus à droite de la ligne qui vous intéresse (flèche montante) + ## Installation standalone Si votre installation de Home Assistant ne vous permet pas d'accéder au système d'add-ons, il est également possible de lancer HA Linky en utilisant Docker @@ -162,7 +247,8 @@ Créez ensuite un fichier nommé `options.json`, au format suivant, puis suivez "action": "sync", "production": true } - ] + ], + "costs": [] } ``` diff --git a/config.yaml b/config.yaml index ae80917..5390a1a 100644 --- a/config.yaml +++ b/config.yaml @@ -1,6 +1,6 @@ name: Linky description: Sync Energy dashboards with your Linky smart meter -version: 1.4.0 +version: 1.5.0 slug: linky init: false url: https://github.com/bokub/ha-linky @@ -27,6 +27,7 @@ options: name: 'Linky production' action: 'sync' production: true + costs: [] schema: meters: - prm: str? @@ -34,3 +35,12 @@ schema: name: str? action: list(sync|reset) production: bool? + costs: + - price: float + prm: str? + after: str? + before: str? + weekday: + - str? + start_date: str? + end_date: str? diff --git a/src/config.test.ts b/src/config.test.ts index ca2cac1..bcd2782 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -16,11 +16,19 @@ describe('getUserConfig', () => { "meters": [ { "prm": "123", "token": "ccc", "name": "Conso", "action": "sync" }, { "prm": "123", "token": "ppp", "name": "Prod", "action": "reset", "production": true } - ] + ], + "costs": [{ "price": 0.1, "start_date": "2024-07-01", "prm": "123" }] }`); expect(getUserConfig()).toEqual({ meters: [ - { action: 'sync', name: 'Conso', prm: '123', production: false, token: 'ccc' }, + { + action: 'sync', + name: 'Conso', + prm: '123', + production: false, + token: 'ccc', + costs: [{ price: 0.1, start_date: '2024-07-01' }], + }, { action: 'reset', name: 'Prod', prm: '123', production: true, token: 'ppp' }, ], }); diff --git a/src/config.ts b/src/config.ts index 1b500d4..1baa5b5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -6,12 +6,22 @@ export type MeterConfig = { name: string; action: 'sync' | 'reset'; production: boolean; + costs?: CostConfig[]; +}; + +export type CostConfig = { + price: number; + after?: string; + before?: string; + weekday?: Array<'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | 'sun'>; + start_date?: string; + end_date?: string; }; export type UserConfig = { meters: MeterConfig[] }; export function getUserConfig(): UserConfig { - let parsed: { meters?: any[] } = {}; + let parsed: { meters?: any[]; costs?: any } = {}; try { parsed = JSON.parse(readFileSync('/data/options.json', 'utf8')); @@ -24,13 +34,41 @@ export function getUserConfig(): UserConfig { if (parsed.meters && Array.isArray(parsed.meters) && parsed.meters.length > 0) { for (const meter of parsed.meters) { if (meter.prm && meter.token) { - result.meters.push({ + const resultMeter: MeterConfig = { prm: meter.prm.toString(), token: meter.token, name: meter.name || 'Linky', action: meter.action === 'reset' ? 'reset' : 'sync', production: meter.production === true, - }); + }; + if (!resultMeter.production && Array.isArray(parsed.costs)) { + const prmCostConfigs = parsed.costs.filter((cost) => !cost.prm || cost.prm === meter.prm); + if (prmCostConfigs.length > 0) { + resultMeter.costs = []; + for (const cost of prmCostConfigs) { + if (cost.price && typeof cost.price === 'number') { + const resultCost: CostConfig = { price: cost.price }; + if (cost.after && typeof cost.after === 'string') { + resultCost.after = cost.after; + } + if (cost.before && typeof cost.before === 'string') { + resultCost.before = cost.before; + } + if (cost.weekday && Array.isArray(cost.weekday)) { + resultCost.weekday = cost.weekday; + } + if (cost.start_date && typeof cost.start_date === 'string') { + resultCost.start_date = cost.start_date; + } + if (cost.end_date && typeof cost.end_date === 'string') { + resultCost.end_date = cost.end_date; + } + resultMeter.costs.push(resultCost); + } + } + } + } + result.meters.push(resultMeter); } } } diff --git a/src/cost.test.ts b/src/cost.test.ts new file mode 100644 index 0000000..1c7c818 --- /dev/null +++ b/src/cost.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from 'vitest'; +import { computeCosts } from './cost.js'; + +describe('Cost computer', () => { + it('Should take start and end dates in account', () => { + const result = computeCosts( + [ + { start: '2024-01-15T00:00:00+01:00', state: 1000, sum: 0 }, + { start: '2024-01-16T00:00:00+01:00', state: 2000, sum: 0 }, + { start: '2024-01-17T00:00:00+01:00', state: 3000, sum: 0 }, + { start: '2024-01-18T00:00:00+01:00', state: 4000, sum: 0 }, + { start: '2024-01-19T00:00:00+01:00', state: 5000, sum: 0 }, + { start: '2024-01-20T00:00:00+01:00', state: 6000, sum: 0 }, + ], + [ + { price: 0.1, end_date: '2024-01-16' }, + { price: 1, start_date: '2024-01-17', end_date: '2024-01-19' }, + { price: 10, start_date: '2024-01-19' }, + ], + ); + + expect(result).toEqual([ + { start: '2024-01-15T00:00:00+01:00', state: 0.1, sum: 0.1 }, + { start: '2024-01-17T00:00:00+01:00', state: 3, sum: 3 + 0.1 }, + { start: '2024-01-18T00:00:00+01:00', state: 4, sum: 4 + 3 + 0.1 }, + { start: '2024-01-19T00:00:00+01:00', state: 50, sum: 50 + 4 + 3 + 0.1 }, + { start: '2024-01-20T00:00:00+01:00', state: 60, sum: 60 + 50 + 4 + 3 + 0.1 }, + ]); + }); + + it('Should take weekday in account', () => { + const result = computeCosts( + [ + { start: '2024-01-01T00:00:00+01:00', state: 1000, sum: 0 }, + { start: '2024-01-02T00:00:00+01:00', state: 2000, sum: 0 }, + { start: '2024-01-03T00:00:00+01:00', state: 3000, sum: 0 }, + { start: '2024-01-04T00:00:00+01:00', state: 4000, sum: 0 }, + { start: '2024-01-05T00:00:00+01:00', state: 5000, sum: 0 }, + ], + [ + { price: 2, weekday: ['mon', 'sun'] }, + { price: 10, weekday: ['wed', 'thu'] }, + ], + ); + + expect(result).toEqual([ + { start: '2024-01-01T00:00:00+01:00', state: 2, sum: 2 }, + { start: '2024-01-03T00:00:00+01:00', state: 30, sum: 30 + 2 }, + { start: '2024-01-04T00:00:00+01:00', state: 40, sum: 40 + 30 + 2 }, + ]); + }); + + it('Should take time in account', () => { + const result = computeCosts( + [ + { start: '2024-01-01T00:00:00+01:00', state: 500, sum: 0 }, + { start: '2024-01-01T01:00:00+01:00', state: 1000, sum: 0 }, + { start: '2024-01-01T02:00:00+01:00', state: 2000, sum: 0 }, + { start: '2024-01-01T03:00:00+01:00', state: 3000, sum: 0 }, + { start: '2024-01-01T04:00:00+01:00', state: 4000, sum: 0 }, + { start: '2024-01-01T05:00:00+01:00', state: 5000, sum: 0 }, + ], + [ + { price: 0.1, before: '01:00' }, + { price: 1, after: '01:00', before: '03:00' }, + { price: 10, after: '03:00', before: '04:00' }, + { price: 100, after: '05:00' }, + ], + ); + + expect(result).toEqual([ + { start: '2024-01-01T00:00:00+01:00', state: 0.05, sum: 0.05 }, + { start: '2024-01-01T01:00:00+01:00', state: 1, sum: 1 + 0.05 }, + { start: '2024-01-01T02:00:00+01:00', state: 2, sum: 2 + 1 + 0.05 }, + { start: '2024-01-01T03:00:00+01:00', state: 30, sum: 30 + 2 + 1 + 0.05 }, + { start: '2024-01-01T05:00:00+01:00', state: 500, sum: 500 + 30 + 2 + 1 + 0.05 }, + ]); + }); +}); diff --git a/src/cost.ts b/src/cost.ts new file mode 100644 index 0000000..10d1224 --- /dev/null +++ b/src/cost.ts @@ -0,0 +1,72 @@ +import { StatisticDataPoint } from './format.js'; +import { CostConfig } from './config.js'; +import dayjs from 'dayjs'; +import { info } from './log.js'; + +export function computeCosts(energy: StatisticDataPoint[], costConfigs: CostConfig[]): StatisticDataPoint[] { + const result: StatisticDataPoint[] = []; + + for (const point of energy) { + const matchingCostConfig = findMatchingCostConfig(point, costConfigs); + if (matchingCostConfig) { + const cost = Math.round(matchingCostConfig.price * point.state) / 1000; + result.push({ + start: point.start, + state: cost, + sum: Math.round(1000 * ((result.length === 0 ? 0 : result[result.length - 1].sum) + cost)) / 1000, + }); + } + } + + if (result.length > 0) { + const intervalFrom = dayjs(result[0].start).format('DD/MM/YYYY'); + const intervalTo = dayjs(result[result.length - 1].start).format('DD/MM/YYYY'); + info(`Successfully computed the cost of ${result.length} data points from ${intervalFrom} to ${intervalTo}`); + } + + return result; +} + +function findMatchingCostConfig(point: StatisticDataPoint, configs: CostConfig[]): CostConfig { + return configs.find((config) => { + if (!config.price || typeof config.price !== 'number') { + return false; + } + const pointStart = dayjs(point.start); + if (config.start_date) { + const configStartDate = dayjs(config.start_date); + if (pointStart.isBefore(configStartDate)) { + return false; + } + } + if (config.end_date) { + const configEndDate = dayjs(config.end_date); + if (pointStart.isAfter(configEndDate) || pointStart.isSame(configEndDate)) { + return false; + } + } + + if (config.weekday && config.weekday.length > 0) { + const weekday = pointStart.format('ddd').toLowerCase(); + if (!(config.weekday as string[]).includes(weekday)) { + return false; + } + } + + if (config.after) { + const afterHour = +config.after.split(':')[0]; + if (isNaN(afterHour) || pointStart.hour() < afterHour) { + return false; + } + } + + if (config.before) { + const beforeHour = +config.before.split(':')[0]; + if (isNaN(beforeHour) || pointStart.hour() >= beforeHour) { + return false; + } + } + + return true; + }); +} diff --git a/src/format.ts b/src/format.ts index 7ca794d..7afa0cc 100644 --- a/src/format.ts +++ b/src/format.ts @@ -1,16 +1,17 @@ import dayjs from 'dayjs'; +export type LinkyRawPoint = { value: string; date: string; interval_length?: string }; export type LinkyDataPoint = { date: string; value: number }; -export type EnergyDataPoint = { start: string; state: number; sum: number }; +export type StatisticDataPoint = { start: string; state: number; sum: number }; -export function formatDailyData(data: { value: string; date: string }[]): LinkyDataPoint[] { +export function formatDailyData(data: LinkyRawPoint[]): LinkyDataPoint[] { return data.map((r) => ({ value: +r.value, date: dayjs(r.date).format('YYYY-MM-DDTHH:mm:ssZ'), })); } -export function formatLoadCurve(data: { value: string; date: string; interval_length?: string }[]): LinkyDataPoint[] { +export function formatLoadCurve(data: LinkyRawPoint[]): LinkyDataPoint[] { const formatted = data.map((r) => ({ value: +r.value, date: dayjs(r.date) @@ -36,8 +37,8 @@ export function formatLoadCurve(data: { value: string; date: string; interval_le })); } -export function formatToEnergy(data: LinkyDataPoint[]): EnergyDataPoint[] { - const result: EnergyDataPoint[] = []; +export function formatAsStatistics(data: LinkyDataPoint[]): StatisticDataPoint[] { + const result: StatisticDataPoint[] = []; for (let i = 0; i < data.length; i++) { result[i] = { start: data[i].date, @@ -48,3 +49,7 @@ export function formatToEnergy(data: LinkyDataPoint[]): EnergyDataPoint[] { return result; } + +export function incrementSums(data: StatisticDataPoint[], value: number): StatisticDataPoint[] { + return data.map((item) => ({ ...item, sum: item.sum + value })); +} diff --git a/src/ha.ts b/src/ha.ts index 35846c0..c68bff5 100644 --- a/src/ha.ts +++ b/src/ha.ts @@ -3,6 +3,7 @@ import { MSG_TYPE_AUTH_INVALID, MSG_TYPE_AUTH_OK, MSG_TYPE_AUTH_REQUIRED } from import { auth } from 'home-assistant-js-websocket/dist/messages.js'; import { debug, warn } from './log.js'; import dayjs from 'dayjs'; +import { StatisticDataPoint } from './format.js'; const WS_URL = process.env.WS_URL || 'ws://supervisor/core/websocket'; const TOKEN = process.env.SUPERVISOR_TOKEN; @@ -23,8 +24,9 @@ export type ErrorMessage = { export type ResultMessage = SuccessMessage | ErrorMessage; -function getStatisticId(prm: string, isProduction: boolean) { - return `${isProduction ? 'linky_prod' : 'linky'}:${prm}`; +function getStatisticId(args: { prm: string; isProduction: boolean; isCost?: boolean }): string { + const { prm, isProduction, isCost } = args; + return `${isProduction ? 'linky_prod' : 'linky'}:${prm}${isCost ? '_cost' : ''}`; } export class HomeAssistantClient { @@ -92,30 +94,32 @@ export class HomeAssistantClient { }); } - public async saveStatistics( - prm: string, - name: string, - isProduction: boolean, - stats: { start: string; state: number; sum: number }[], - ) { - const statisticId = getStatisticId(prm, isProduction); + public async saveStatistics(args: { + prm: string; + name: string; + isProduction: boolean; + isCost?: boolean; + stats: StatisticDataPoint[]; + }) { + const { prm, name, isProduction, isCost, stats } = args; + const statisticId = getStatisticId({ prm, isProduction, isCost }); await this.sendMessage({ type: 'recorder/import_statistics', metadata: { has_mean: false, has_sum: true, - name, + name: isCost ? `${name} (costs)` : name, source: statisticId.split(':')[0], statistic_id: statisticId, - unit_of_measurement: 'Wh', + unit_of_measurement: isCost ? '€' : 'Wh', }, stats, }); } - public async isNewPRM(prm: string, isProduction: boolean) { - const statisticId = getStatisticId(prm, isProduction); + public async isNewPRM(args: { prm: string; isProduction: boolean; isCost?: boolean }) { + const statisticId = getStatisticId(args); const ids = await this.sendMessage({ type: 'recorder/list_statistic_ids', statistic_type: 'sum', @@ -123,23 +127,23 @@ export class HomeAssistantClient { return !ids.result.find((statistic: any) => statistic.statistic_id === statisticId); } - public async findLastStatistic( - prm: string, - isProduction: boolean, - ): Promise { - const isNew = await this.isNewPRM(prm, isProduction); + const { prm, isProduction, isCost } = args; + const isNew = await this.isNewPRM({ prm, isProduction, isCost }); if (isNew) { - warn(`PRM ${prm} not found in Home Assistant statistics`); + if (!isCost) { + warn(`PRM ${prm} not found in Home Assistant statistics`); + } return null; } - const statisticId = getStatisticId(prm, isProduction); + const statisticId = getStatisticId({ prm, isProduction, isCost }); // Loop over the last 52 weeks for (let i = 0; i < 52; i++) { @@ -167,12 +171,13 @@ export class HomeAssistantClient { } public async purge(prm: string, isProduction: boolean) { - const statisticId = getStatisticId(prm, isProduction); + const statisticId = getStatisticId({ prm, isProduction, isCost: false }); + const statisticIdWithCost = getStatisticId({ prm, isProduction, isCost: true }); warn(`Removing all statistics for PRM ${prm}`); await this.sendMessage({ type: 'recorder/clear_statistics', - statistic_ids: [statisticId], + statistic_ids: [statisticId, statisticIdWithCost], }); } } diff --git a/src/history.ts b/src/history.ts index 6da5747..35698fa 100644 --- a/src/history.ts +++ b/src/history.ts @@ -1,13 +1,13 @@ import { readdirSync, createReadStream, existsSync } from 'node:fs'; import { debug, info, error } from './log.js'; import { parse } from 'csv-parse'; -import { EnergyDataPoint, formatLoadCurve, formatToEnergy } from './format.js'; +import { StatisticDataPoint, formatLoadCurve, formatAsStatistics } from './format.js'; import dayjs from 'dayjs'; const baseDir = '/config'; const userDir = '/addon_configs/cf6b56a3_linky'; -export async function getMeterHistory(prm: string): Promise { +export async function getMeterHistory(prm: string): Promise { if (!existsSync(baseDir)) { debug(`Cannot find folder ${userDir}`); return []; @@ -38,7 +38,7 @@ async function readMetadata(filename: string): Promise<{ [key: string]: string } } } -async function readHistory(filename: string): Promise { +async function readHistory(filename: string): Promise { info(`Importing historical data from ${filename}`); const parser = createReadStream(`${baseDir}/${filename}`).pipe( @@ -56,10 +56,12 @@ async function readHistory(filename: string): Promise { } const intervalFrom = dayjs(records[0].date).format('DD/MM/YYYY'); - const intervalTo = dayjs(records[records.length - 1].date).format('DD/MM/YYYY'); + const intervalTo = dayjs(records[records.length - 1].date) + .subtract(1, 'minute') + .format('DD/MM/YYYY'); info(`Found ${records.length} data points from ${intervalFrom} to ${intervalTo} in CSV file`); const loadCurve = formatLoadCurve(records); - return formatToEnergy(loadCurve); + return formatAsStatistics(loadCurve); } diff --git a/src/index.ts b/src/index.ts index 0a7b461..092afa0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,8 @@ import { HomeAssistantClient } from './ha.js'; import { LinkyClient } from './linky.js'; import { getUserConfig, MeterConfig } from './config.js'; import { getMeterHistory } from './history.js'; +import { incrementSums } from './format.js'; +import { computeCosts } from './cost.js'; import { debug, error, info, warn } from './log.js'; import cron from 'node-cron'; import dayjs from 'dayjs'; @@ -44,15 +46,37 @@ async function main() { } data import is starting`, ); - const history = await getMeterHistory(config.prm); - if (history.length > 0) { - await haClient.saveStatistics(config.prm, config.name, config.production, history); + let energyData = await getMeterHistory(config.prm); + + if (energyData.length === 0) { + const client = new LinkyClient(config.token, config.prm, config.production); + energyData = await client.getEnergyData(null); + } + + if (energyData.length === 0) { + warn(`No history found for PRM ${config.prm}`); return; } - const client = new LinkyClient(config.token, config.prm, config.production); - const energyData = await client.getEnergyData(null); - await haClient.saveStatistics(config.prm, config.name, config.production, energyData); + await haClient.saveStatistics({ + prm: config.prm, + name: config.name, + isProduction: config.production, + stats: energyData, + }); + + if (config.costs) { + const costs = computeCosts(energyData, config.costs); + if (costs.length > 0) { + await haClient.saveStatistics({ + prm: config.prm, + name: config.name, + isProduction: config.production, + isCost: true, + stats: costs, + }); + } + } } async function sync(config: MeterConfig) { @@ -62,7 +86,10 @@ async function main() { } data`, ); - const lastStatistic = await haClient.findLastStatistic(config.prm, config.production); + const lastStatistic = await haClient.findLastStatistic({ + prm: config.prm, + isProduction: config.production, + }); if (!lastStatistic) { warn(`Data synchronization failed, no previous statistic found in Home Assistant`); return; @@ -76,8 +103,30 @@ async function main() { const client = new LinkyClient(config.token, config.prm, config.production); const firstDay = dayjs(lastStatistic.start).add(1, 'day'); const energyData = await client.getEnergyData(firstDay); - incrementSums(energyData, lastStatistic.sum); - await haClient.saveStatistics(config.prm, config.name, config.production, energyData); + await haClient.saveStatistics({ + prm: config.prm, + name: config.name, + isProduction: config.production, + stats: incrementSums(energyData, lastStatistic.sum), + }); + + if (config.costs) { + const costs = computeCosts(energyData, config.costs); + if (costs.length > 0) { + const lastCostStatistic = await haClient.findLastStatistic({ + prm: config.prm, + isProduction: config.production, + isCost: true, + }); + await haClient.saveStatistics({ + prm: config.prm, + name: config.name, + isProduction: config.production, + isCost: true, + stats: incrementSums(costs, lastCostStatistic?.sum || 0), + }); + } + } } // Initialize or sync data @@ -85,7 +134,10 @@ async function main() { if (config?.action === 'sync') { info(`PRM ${config.prm} found in configuration for ${config.production ? 'production' : 'consumption'}`); - const isNew = await haClient.isNewPRM(config.prm, config.production); + const isNew = await haClient.isNewPRM({ + prm: config.prm, + isProduction: config.production, + }); if (isNew) { await init(config); } else { @@ -118,13 +170,6 @@ async function main() { }); } -function incrementSums(data: { sum: number }[], value: number) { - return data.map((item) => { - item.sum += value; - return item; - }); -} - try { await main(); } catch (e) { diff --git a/src/linky.ts b/src/linky.ts index a373cd3..107caa6 100644 --- a/src/linky.ts +++ b/src/linky.ts @@ -4,9 +4,9 @@ import { debug, info, warn } from './log.js'; import { formatDailyData, formatLoadCurve, - type EnergyDataPoint, + formatAsStatistics, + type StatisticDataPoint, type LinkyDataPoint, - formatToEnergy, } from './format.js'; export class LinkyClient { @@ -20,7 +20,7 @@ export class LinkyClient { this.session.userAgent = 'ha-linky/1.4.0'; } - public async getEnergyData(firstDay: null | Dayjs): Promise { + public async getEnergyData(firstDay: null | Dayjs): Promise { const history: LinkyDataPoint[][] = []; let offset = 0; let limitReached = false; @@ -101,7 +101,7 @@ export class LinkyClient { info(`Data import returned ${dataPoints.length} data points from ${intervalFrom} to ${intervalTo}`); } - return formatToEnergy(dataPoints); + return formatAsStatistics(dataPoints); } }