From 530e317160f7331580ee1475bfa7ac2e6de5af5a Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 28 Jul 2026 03:17:50 +0200 Subject: [PATCH] feat(iobroker): surface energy tariff (TOU) read in state tree Add a read-only energy.{SITE_ID}.tariff channel (tariff_id, tariff_content, tariff_content_v2), populated from the getSiteInfo() fields already fetched by fetchSiteData() - the same fields TeslemetryEnergyApi.getTariff() exposes. Mirrors the node-red/API tariff-read scope: read only, no write, no VPP surfaces. --- .changeset/iobroker-tariff-read.md | 5 +++++ packages/iobroker.teslemetry/README.md | 4 ++++ .../iobroker.teslemetry/lib/StateManager.ts | 17 +++++++++++++++++ .../test/EnergyHandler.test.ts | 6 +++++- .../test/StateManager.test.ts | 18 ++++++++++++++++++ 5 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 .changeset/iobroker-tariff-read.md diff --git a/.changeset/iobroker-tariff-read.md b/.changeset/iobroker-tariff-read.md new file mode 100644 index 0000000..79c0b3a --- /dev/null +++ b/.changeset/iobroker-tariff-read.md @@ -0,0 +1,5 @@ +--- +"iobroker.teslemetry": minor +--- + +Surface the site's time-of-use tariff (`tariff_id`, `tariff_content`, `tariff_content_v2`) as read-only states under `energy.{SITE_ID}.tariff`. Read-only, matching the node-red/Homey tariff precedent - no tariff write or VPP surfaces. diff --git a/packages/iobroker.teslemetry/README.md b/packages/iobroker.teslemetry/README.md index d340581..b7ed37a 100644 --- a/packages/iobroker.teslemetry/README.md +++ b/packages/iobroker.teslemetry/README.md @@ -178,6 +178,10 @@ teslemetry.0 │ ├── mode (string, read-write) │ ├── backup_reserve_percent (number, read-write) │ └── off_grid_reserve_percent (number, read-write) + ├── tariff + │ ├── tariff_id (string, read-only) + │ ├── tariff_content (string/JSON, read-only) + │ └── tariff_content_v2 (string/JSON, read-only) └── commands └── storm_mode (boolean, write-only) ``` diff --git a/packages/iobroker.teslemetry/lib/StateManager.ts b/packages/iobroker.teslemetry/lib/StateManager.ts index 641b718..bd673d8 100644 --- a/packages/iobroker.teslemetry/lib/StateManager.ts +++ b/packages/iobroker.teslemetry/lib/StateManager.ts @@ -129,6 +129,12 @@ export class StateManager { await this.createState(`${base}.operation.backup_reserve_percent`, 'Backup Reserve', 'number', 'level.battery', true, true, 20, '%'); await this.createState(`${base}.operation.off_grid_reserve_percent`, 'Off-Grid Reserve', 'number', 'level.battery', true, true, 0, '%'); + // Tariff channel (read-only - rate plan is managed through the utility/Tesla app, not this adapter) + await this.createChannel(`${base}.tariff`, 'Tariff'); + await this.createState(`${base}.tariff.tariff_id`, 'Tariff Plan Name', 'string', 'text', true, false); + await this.createState(`${base}.tariff.tariff_content`, 'Tariff Content (JSON)', 'string', 'json', true, false); + await this.createState(`${base}.tariff.tariff_content_v2`, 'Tariff Content V2 (JSON)', 'string', 'json', true, false); + // Commands channel await this.createChannel(`${base}.commands`, 'Commands'); await this.createState(`${base}.commands.storm_mode`, 'Storm Mode', 'boolean', 'button', false, true, false); @@ -269,6 +275,17 @@ export class StateManager { if (data.off_grid_vehicle_charging_reserve_percent !== undefined) { await this.setStateAsync(`${base}.operation.off_grid_reserve_percent`, data.off_grid_vehicle_charging_reserve_percent); } + + // Update tariff (getSiteInfo() already carries these fields - see TeslemetryEnergyApi.getTariff()) + if (data.tariff_id !== undefined) { + await this.setStateAsync(`${base}.tariff.tariff_id`, data.tariff_id); + } + if (data.tariff_content !== undefined) { + await this.setStateAsync(`${base}.tariff.tariff_content`, JSON.stringify(data.tariff_content)); + } + if (data.tariff_content_v2 !== undefined) { + await this.setStateAsync(`${base}.tariff.tariff_content_v2`, JSON.stringify(data.tariff_content_v2)); + } } catch (error) { this.adapter.log.error(`Error updating energy site data for ${siteId}: ${error}`); } diff --git a/packages/iobroker.teslemetry/test/EnergyHandler.test.ts b/packages/iobroker.teslemetry/test/EnergyHandler.test.ts index fdfdb8f..37a421c 100644 --- a/packages/iobroker.teslemetry/test/EnergyHandler.test.ts +++ b/packages/iobroker.teslemetry/test/EnergyHandler.test.ts @@ -70,7 +70,10 @@ test('fetchSiteData calls getLiveStatus()/getSiteInfo() and updates states from (site as any).getLiveStatus = () => Promise.resolve({ response: { solar_power: 500, battery_power: -100, grid_power: 0, grid_status: 'Active', percentage_charged: 80 } }); - (site as any).getSiteInfo = () => Promise.resolve({ response: { default_real_mode: 'self_consumption', backup_reserve_percent: 20 } }); + (site as any).getSiteInfo = () => + Promise.resolve({ + response: { default_real_mode: 'self_consumption', backup_reserve_percent: 20, tariff_id: 'PGE-EV2-A' }, + }); const { adapter, states } = createFakeAdapter(); const handler = new EnergyHandler(adapter, teslemetry, new StateManager(adapter)); @@ -83,4 +86,5 @@ test('fetchSiteData calls getLiveStatus()/getSiteInfo() and updates states from assert.equal(states.get(`energy.${SITE_ID}.battery.percentage`), 80); assert.equal(states.get(`energy.${SITE_ID}.operation.mode`), 'self_consumption'); assert.equal(states.get(`energy.${SITE_ID}.operation.backup_reserve_percent`), 20); + assert.equal(states.get(`energy.${SITE_ID}.tariff.tariff_id`), 'PGE-EV2-A'); }); diff --git a/packages/iobroker.teslemetry/test/StateManager.test.ts b/packages/iobroker.teslemetry/test/StateManager.test.ts index df19d67..f3e83a8 100644 --- a/packages/iobroker.teslemetry/test/StateManager.test.ts +++ b/packages/iobroker.teslemetry/test/StateManager.test.ts @@ -50,3 +50,21 @@ test('updateEnergySiteData reads the flat getLiveStatus()/getSiteInfo() response assert.equal(states.get('energy.123.live.grid_status'), 'Active'); assert.equal(states.get('energy.123.battery.percentage'), 77); }); + +test('updateEnergySiteData surfaces tariff_id/tariff_content/tariff_content_v2 from the getSiteInfo() response shape', async () => { + const { adapter, states } = createFakeAdapter(); + const stateManager = new StateManager(adapter); + + await stateManager.updateEnergySiteData(123, { + tariff_id: 'PGE-EV2-A', + tariff_content: { code: 'PGE-EV2-A' }, + tariff_content_v2: { code: 'PGE-EV2-A', utility: 'PG&E', currency: 'USD' }, + }); + + assert.equal(states.get('energy.123.tariff.tariff_id'), 'PGE-EV2-A'); + assert.equal(states.get('energy.123.tariff.tariff_content'), JSON.stringify({ code: 'PGE-EV2-A' })); + assert.equal( + states.get('energy.123.tariff.tariff_content_v2'), + JSON.stringify({ code: 'PGE-EV2-A', utility: 'PG&E', currency: 'USD' }) + ); +});