Skip to content

Commit

Permalink
Add CSV import feature
Browse files Browse the repository at this point in the history
  • Loading branch information
bokub committed Feb 27, 2024
1 parent 3922169 commit fea2787
Show file tree
Hide file tree
Showing 11 changed files with 210 additions and 79 deletions.
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 1.4.0 - 27 février 2024

- Ajout de l'import de données historiques via un fichier CSV

## 1.3.1 - 25 février 2024

- Correction de la synchronisation quotidienne qui ne se lançait pas correctement
Expand All @@ -14,5 +18,3 @@
## 1.2.0 - 15 novembre 2023

- Ajout de la synchronisation de la production d'énergie - merci à @cddu33 pour son aide !

![2023-11-15_09-30](https://github.com/bokub/ha-linky/assets/17952318/e6148d10-cd88-40a0-839e-ea9b7ccbf275)
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Pour utiliser cet add-on, il vous faut :

- Un compteur Linky
- Un espace client Enedis
- La collecte de la consommation horaire activée sur votre espace client Enedis ([tutoriel](https://github.com/bokub/ha-linky/wiki/Activer-la-collecte-de-la-consommation-horaire))
- Un token d'accès, à générer sur [Conso API](https://conso.boris.sh/)

## Installation
Expand Down Expand Up @@ -76,7 +77,7 @@ Appliquez les modifications et démarrez / redémarrez l'add-on si ce n'est pas
Une fois l'add-on démarré, rendez-vous dans l'onglet _Journal_ / _Log_ pour suivre la progression de la synchronisation.
Au premier lancement, **HA Linky** essaiera de récupérer toutes les données de consommation depuis la date d'installation de votre compteur Linky.
Au premier lancement, **HA Linky** essaiera de récupérer jusqu'à **1 an** de données historiques (sauf si vous fournissez votre propre export CSV).
Ensuite, il synchronisera les données deux fois par jour tant qu'il n'est pas arrêté :
Expand Down Expand Up @@ -108,7 +109,21 @@ Revenez sur l'onglet _Configuration_ de l'add-on et changez la valeur de `action

Ouvrez ensuite l'onglet _Journal_ / _Log_ pour vérifier que la remise à zéro s'est bien déroulée.

Au prochain démarrage, si `action` est repassé à `sync`, **HA Linky** réimportera à nouveau toutes vos données. Cette manipulation peut surcharger le serveur de **Conso API**, ne l'utilisez donc que si nécessaire pour ne pas risquer un ban !
Au prochain démarrage, si `action` est repassé à `sync`, **HA Linky** réimportera à nouveau vos données historiques, soit via Conso API, soit via un fichier CSV si vous en avez fourni un (voir paragraphe suivant).

### Import d'historique CSV

Lors de l'**initialisation**, HA Linky télécharge jusqu'à **1 an** de données **quotidiennes** via Conso API.

Si vous souhaitez un historique **plus long** ainsi qu'une **précision horaire**, vous pouvez importer un fichier CSV à partir duquel HA Linky pourra extraire les données.

La démarche à suivre est la suivante :

- Téléchargez un export de vos données **horaires** depuis votre espace client Enedis ([tutoriel](https://github.com/bokub/ha-linky/wiki/T%C3%A9l%C3%A9charger-son-historique-au-format-CSV))
- Déposez ce fichier dans le dossier `/addon_configs/cf6b56a3_linky` ([tutoriel](https://github.com/bokub/ha-linky/wiki/Importer-un-fichier-CSV-dans-Home-Assistant))
- Si vous avez déjà importé des données dans Home Assistant, faites une remise à zéro en suivant le paragraphe précédent
- 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.

## Installation standalone

Expand Down
4 changes: 3 additions & 1 deletion config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Linky
description: Sync Energy dashboards with your Linky smart meter
version: 1.3.1
version: 1.4.0
slug: linky
init: false
url: https://github.com/bokub/ha-linky
Expand All @@ -13,6 +13,8 @@ arch:
- i386
homeassistant_api: true
hassio_api: true
map:
- addon_config
options:
meters:
- prm: ''
Expand Down
10 changes: 8 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "ha-linky",
"private": true,
"version": "1.3.1",
"version": "1.4.0",
"type": "module",
"scripts": {
"build": "tsc",
Expand All @@ -12,6 +12,7 @@
"prettier": "@bokub/prettier-config",
"dependencies": {
"chalk": "^5.3.0",
"csv-parse": "^5.5.3",
"dayjs": "^1.11.9",
"linky": "^2.0.2",
"node-cron": "^3.0.2",
Expand Down
25 changes: 25 additions & 0 deletions src/format.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest';
import { formatLoadCurve } from './format.js';

describe('Load curve formatter', () => {
it('Should format the load curve properly', () => {
const result = formatLoadCurve([
{ date: '2022-07-08T01:00:00+02:00', value: '10' },
{ date: '2022-07-08T02:00:00+02:00', value: '15' },
{ date: '2022-07-08T03:00:00+02:00', value: '20' },
{ date: '2024-01-24T22:20:00+01:00', value: '100' },
{ date: '2024-01-24T22:40:00+01:00', value: '100' },
{ date: '2024-01-24T23:00:00+01:00', value: '200' },
{ date: '2024-01-24T23:30:00+01:00', value: '500' },
{ date: '2024-01-25T00:00:00+01:00', value: '700' },
]);

expect(result).toEqual([
{ date: '2022-07-08T00:00:00+02:00', value: 10 },
{ date: '2022-07-08T01:00:00+02:00', value: 15 },
{ date: '2022-07-08T02:00:00+02:00', value: 20 },
{ date: '2024-01-24T22:00:00+01:00', value: 133.33 },
{ date: '2024-01-24T23:00:00+01:00', value: 600 },
]);
});
});
50 changes: 50 additions & 0 deletions src/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import dayjs from 'dayjs';

export type LinkyDataPoint = { date: string; value: number };
export type EnergyDataPoint = { start: string; state: number; sum: number };

export function formatDailyData(data: { value: string; date: string }[]): 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[] {
const formatted = data.map((r) => ({
value: +r.value,
date: dayjs(r.date)
.subtract(parseFloat(r.interval_length?.match(/\d+/)[0] || '1'), 'minute')
.startOf('hour')
.format('YYYY-MM-DDTHH:mm:ssZ'),
}));

const grouped = formatted.reduce(
(acc, cur) => {
const date = cur.date;
if (!acc[date]) {
acc[date] = [];
}
acc[date].push(cur.value);
return acc;
},
{} as { [date: string]: number[] },
);
return Object.entries(grouped).map(([date, values]) => ({
date,
value: Math.round((100 * values.reduce((acc, cur) => acc + cur, 0)) / values.length) / 100,
}));
}

export function formatToEnergy(data: LinkyDataPoint[]): EnergyDataPoint[] {
const result: EnergyDataPoint[] = [];
for (let i = 0; i < data.length; i++) {
result[i] = {
start: data[i].date,
state: data[i].value,
sum: data[i].value + (i === 0 ? 0 : result[i - 1].sum),
};
}

return result;
}
65 changes: 65 additions & 0 deletions src/history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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 dayjs from 'dayjs';

const baseDir = '/config';
const userDir = '/addon_configs/cf6b56a3_linky';

export async function getMeterHistory(prm: string): Promise<EnergyDataPoint[]> {
if (!existsSync(baseDir)) {
debug(`Cannot find folder ${userDir}`);
return;
}
const files = readdirSync(baseDir).filter((file) => file.endsWith('.csv'));
debug(`Found ${files.length} CSV ${files.length > 1 ? 'files' : 'file'} in ${userDir}`);

for (const filename of files) {
try {
const metadata = await readMetadata(filename);

if (metadata['Identifiant PRM'] && metadata['Identifiant PRM'] === prm) {
return readHistory(filename);
}
} catch (e) {
error(`Error while reading ${filename}: ${e.toString()}`);
}
}
return [];
}

async function readMetadata(filename: string): Promise<{ [key: string]: string }> {
const parser = createReadStream(`${baseDir}/${filename}`).pipe(
parse({ bom: true, delimiter: ';', columns: true, toLine: 2 }),
);
for await (const record of parser) {
return record;
}
}

async function readHistory(filename: string): Promise<EnergyDataPoint[]> {
info(`Importing historical data from ${filename}`);

const parser = createReadStream(`${baseDir}/${filename}`).pipe(
parse({ bom: true, delimiter: ';', columns: true, fromLine: 3 }),
);

const records: { date: string; value: string }[] = [];
for await (const record of parser) {
if (record['Horodate'] && record['Valeur']) {
records.push({
date: record['Horodate'],
value: record['Valeur'],
});
}
}

const intervalFrom = dayjs(records[0].date).format('DD/MM/YYYY');
const intervalTo = dayjs(records[records.length - 1].date).format('DD/MM/YYYY');

info(`Found ${records.length} data points from ${intervalFrom} to ${intervalTo} in CSV file`);

const loadCurve = formatLoadCurve(records);
return formatToEnergy(loadCurve);
}
11 changes: 9 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { HomeAssistantClient } from './ha.js';
import { LinkyClient } from './linky.js';
import { getUserConfig, MeterConfig } from './config.js';
import { getMeterHistory } from './history.js';
import { debug, error, info, warn } from './log.js';
import cron from 'node-cron';
import dayjs from 'dayjs';
Expand Down Expand Up @@ -38,11 +39,17 @@ async function main() {

async function init(config: MeterConfig) {
info(
`[${dayjs().format('DD/MM HH:mm')}] New PRM detected, importing as much historical ${
`[${dayjs().format('DD/MM HH:mm')}] New PRM detected, historical ${
config.production ? 'production' : 'consumption'
} data as possible`,
} data import is starting`,
);

const history = await getMeterHistory(config.prm);
if (history.length > 0) {
await haClient.saveStatistics(config.prm, config.name, config.production, history);
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);
Expand Down
20 changes: 7 additions & 13 deletions src/linky.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('LinkyClient', () => {
expect((client as any).session.userAgent).toBe('ha-linky/' + version);
});

it('Fetches all historical data if first parameter is null', async () => {
it('Fetches 1 year of historical data if first parameter is null', async () => {
getLoadCurve.mockReturnValue({
interval_reading: [
{ value: '100', date: '2023-12-31 00:30:00', interval_length: 'PT30M' },
Expand All @@ -34,26 +34,20 @@ describe('LinkyClient', () => {
],
});

getDailyConsumption.mockImplementation((start: string) => {
if (start.startsWith('2022')) {
throw new Error();
}
return { interval_reading: [{ value: '2000', date: start }] };
});
getDailyConsumption.mockImplementation((start: string) => ({ interval_reading: [{ value: '2000', date: start }] }));

const result = await client.getEnergyData(null);

expect(getLoadCurve).toHaveBeenCalledOnce();
expect(getLoadCurve).toHaveBeenCalledWith('2023-12-25', '2024-01-01');

expect(getDailyConsumption).toHaveBeenCalledTimes(3);
expect(getDailyConsumption).toHaveBeenNthCalledWith(1, '2023-07-28', '2023-12-25');
expect(getDailyConsumption).toHaveBeenNthCalledWith(2, '2023-02-28', '2023-07-28');
expect(getDailyConsumption).toHaveBeenNthCalledWith(3, '2022-10-01', '2023-02-28');
expect(getDailyConsumption).toHaveBeenCalledTimes(2);
expect(getDailyConsumption).toHaveBeenNthCalledWith(1, '2023-06-29', '2023-12-25');
expect(getDailyConsumption).toHaveBeenNthCalledWith(2, '2023-01-01', '2023-06-29');

expect(result).toEqual([
{ start: '2023-02-28T00:00:00+01:00', state: 2000, sum: 2000 },
{ start: '2023-07-28T00:00:00+02:00', state: 2000, sum: 4000 },
{ start: '2023-01-01T00:00:00+01:00', state: 2000, sum: 2000 },
{ start: '2023-06-29T00:00:00+02:00', state: 2000, sum: 4000 },
{ start: '2023-12-31T00:00:00+01:00', state: 200, sum: 4200 },
{ start: '2023-12-31T01:00:00+01:00', state: 500, sum: 4700 },
]);
Expand Down
Loading

0 comments on commit fea2787

Please sign in to comment.