Skip to content

Commit

Permalink
Add costs computing feature
Browse files Browse the repository at this point in the history
  • Loading branch information
bokub committed Jul 23, 2024
1 parent 4628151 commit c53f6ac
Show file tree
Hide file tree
Showing 12 changed files with 413 additions and 61 deletions.
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
90 changes: 88 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 :

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -162,7 +247,8 @@ Créez ensuite un fichier nommé `options.json`, au format suivant, puis suivez
"action": "sync",
"production": true
}
]
],
"costs": []
}
```

Expand Down
12 changes: 11 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.4.0
version: 1.5.0
slug: linky
init: false
url: https://github.com/bokub/ha-linky
Expand All @@ -27,10 +27,20 @@ options:
name: 'Linky production'
action: 'sync'
production: true
costs: []
schema:
meters:
- prm: str?
token: str?
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?
12 changes: 10 additions & 2 deletions src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
],
});
Expand Down
44 changes: 41 additions & 3 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand All @@ -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);
}
}
}
Expand Down
79 changes: 79 additions & 0 deletions src/cost.test.ts
Original file line number Diff line number Diff line change
@@ -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 },
]);
});
});
72 changes: 72 additions & 0 deletions src/cost.ts
Original file line number Diff line number Diff line change
@@ -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;
});
}
Loading

0 comments on commit c53f6ac

Please sign in to comment.