Skip to content

Commit 03c6b60

Browse files
committed
feat(#327): add energy_collection_key option and improve collection auto-detection
Support explicit energy collection binding via energy_collection_key config. Improve default behavior by trying panel-specific key (_energy_<panelUrl>) first, fixing wrong collection binding when navigating between dashboards on HA 2026.4+.
1 parent 20b09f0 commit 03c6b60

File tree

5 files changed

+103
-8
lines changed

5 files changed

+103
-8
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Install through [HACS](https://hacs.xyz/)
2727
| sections | list | | Section-level configuration (sorting, min_width). See [sections object](#sections-object)
2828
| layout | string | auto | Valid options are: 'horizontal' - flow left to right, 'vertical' - flow top to bottom & 'auto' - determine based on available space (based on the section->`min_witdh` option, which defaults to 150)
2929
| energy_date_selection | boolean | false | Integrate with the Energy Dashboard. Filters data based on the [energy-date-selection](https://www.home-assistant.io/dashboards/energy/) card. Use this only for accumulated data sensors (energy/water/gas) and with a `type:energy-date-selection` card. You still need to specify all your entities as HA doesn't know exactly how to connect them but you can use the general kWh entities that you have in the energy dashboard. In the future we may use areas to auto configure the chart. Not compatible with `time_period`
30+
| energy_collection_key | string | | Key of the energy collection to bind to. Auto-detected by default. Set this if you have multiple energy dashboards and the chart binds to the wrong one. The key follows the pattern `_energy_<dashboard_url>`, e.g. `_energy_energy-dashboard`
3031
| title | string | | Optional header title for the card
3132
| unit_prefix | string | | Metric prefix for the unit of measurment. See <https://en.wikipedia.org/wiki/Unit_prefix> . Supported values are m, k, M, G, T, and 'auto'. When 'auto' is used, the appropriate prefix is chosen automatically for each value based on its magnitude (m for values <1, k for values >=1000, etc.)
3233
| round | number | 0 | Round the value to at most N decimal places. May not apply to near zero values, see issue [#29](https://github.com/MindFreeze/ha-sankey-chart/issues/29)

__tests__/energy.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { HomeAssistant } from 'custom-card-helpers';
2+
import { getEnergyDataCollection } from '../src/energy';
3+
4+
const mockCollection = () => ({ subscribe: jest.fn() });
5+
6+
const createHass = (connectionKeys: Record<string, any>, panelUrl = 'energy') =>
7+
({
8+
connection: connectionKeys,
9+
panelUrl,
10+
} as unknown as HomeAssistant);
11+
12+
describe('getEnergyDataCollection', () => {
13+
it('returns panel-specific collection by default (HA 2026.4+)', () => {
14+
const collection = mockCollection();
15+
const hass = createHass({ '_energy_energy': collection }, 'energy');
16+
expect(getEnergyDataCollection(hass)).toBe(collection);
17+
});
18+
19+
it('prefers panel-specific key over other _energy* keys', () => {
20+
const correct = mockCollection();
21+
const wrong = mockCollection();
22+
const hass = createHass({
23+
'_energy_other-dashboard': wrong,
24+
'_energy_my-dashboard': correct,
25+
}, 'my-dashboard');
26+
expect(getEnergyDataCollection(hass)).toBe(correct);
27+
});
28+
29+
it('falls back to legacy _energy key', () => {
30+
const collection = mockCollection();
31+
const hass = createHass({ '_energy': collection }, 'energy');
32+
expect(getEnergyDataCollection(hass)).toBe(collection);
33+
});
34+
35+
it('falls back to prefix scan when panel and legacy keys miss', () => {
36+
const collection = mockCollection();
37+
const hass = createHass({ '_energy_some-other-panel': collection }, 'different-panel');
38+
expect(getEnergyDataCollection(hass)).toBe(collection);
39+
});
40+
41+
it('returns null when no collection exists', () => {
42+
const hass = createHass({}, 'energy');
43+
expect(getEnergyDataCollection(hass)).toBeNull();
44+
});
45+
46+
it('returns explicit collectionKey when found', () => {
47+
const collection = mockCollection();
48+
const hass = createHass({ '_energy_my-dashboard': collection }, 'energy');
49+
expect(getEnergyDataCollection(hass, '_energy_my-dashboard')).toBe(collection);
50+
});
51+
52+
it('returns null when explicit collectionKey is not found', () => {
53+
const hass = createHass({ '_energy_other': mockCollection() }, 'energy');
54+
expect(getEnergyDataCollection(hass, '_energy_missing')).toBeNull();
55+
});
56+
57+
it('explicit collectionKey bypasses auto-detection', () => {
58+
const panelCollection = mockCollection();
59+
const explicitCollection = mockCollection();
60+
const hass = createHass({
61+
'_energy_energy': panelCollection,
62+
'_energy_custom': explicitCollection,
63+
}, 'energy');
64+
expect(getEnergyDataCollection(hass, '_energy_custom')).toBe(explicitCollection);
65+
});
66+
67+
it('skips objects without subscribe method', () => {
68+
const hass = createHass({
69+
'_energy_energy': { notSubscribe: jest.fn() },
70+
'_energy': { data: 'something' },
71+
}, 'energy');
72+
expect(getEnergyDataCollection(hass)).toBeNull();
73+
});
74+
});

src/energy.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,15 +99,34 @@ export interface EnergyCollection extends Collection<EnergyData> {
9999

100100
export const getEnergyDataCollection = (
101101
hass: HomeAssistant,
102+
collectionKey?: string,
102103
): EnergyCollection | null => {
103-
if ((hass.connection as any)['_energy']) {
104-
return (hass.connection as any)['_energy'];
104+
const conn = hass.connection as any;
105+
const isCollection = (obj: any) => obj && typeof obj.subscribe === 'function';
106+
107+
// If an explicit key is provided, use only that key
108+
if (collectionKey) {
109+
return isCollection(conn[collectionKey]) ? conn[collectionKey] : null;
110+
}
111+
112+
// Smart auto-detection: try panel-specific key first (HA 2026.4+)
113+
const panelKey = `_energy_${hass.panelUrl}`;
114+
if (isCollection(conn[panelKey])) {
115+
return conn[panelKey];
105116
}
106-
for (const key of Object.keys(hass.connection)) {
107-
if (key.startsWith('_energy') && typeof (hass.connection as any)[key]?.subscribe === 'function') {
108-
return (hass.connection as any)[key];
117+
118+
// Legacy key (HA < 2026.4)
119+
if (isCollection(conn['_energy'])) {
120+
return conn['_energy'];
121+
}
122+
123+
// Fallback: prefix scan for any _energy* collection
124+
for (const key of Object.keys(conn)) {
125+
if (key.startsWith('_energy') && isCollection(conn[key])) {
126+
return conn[key];
109127
}
110128
}
129+
111130
// HA has not initialized the collection yet and we don't want to interfere with that if energy_date_selection is enabled
112131
return null;
113132
};

src/ha-sankey-chart.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,11 @@ class SankeyChart extends SubscribeMixin(LitElement) {
7676
resolve: (value: EnergyCollection | PromiseLike<EnergyCollection>) => void,
7777
reject: (reason?: any) => void,
7878
) => {
79-
const energyCollection = getEnergyDataCollection(this.hass);
79+
const energyCollection = getEnergyDataCollection(this.hass, this.config.energy_collection_key);
8080
if (energyCollection) {
8181
resolve(energyCollection);
8282
} else if (Date.now() - start > ENERGY_DATA_TIMEOUT) {
83-
console.debug(getEnergyDataCollection(this.hass));
83+
console.debug(getEnergyDataCollection(this.hass, this.config.energy_collection_key));
8484
reject(
8585
new Error('No energy data received. Make sure to add a `type: energy-date-selection` card to this screen.'),
8686
);
@@ -92,7 +92,7 @@ class SankeyChart extends SubscribeMixin(LitElement) {
9292
setTimeout(() => {
9393
if (!this.error && !Object.keys(this.states).length) {
9494
this.error = new Error('Something went wrong. No energy data received.');
95-
console.debug(getEnergyDataCollection(this.hass));
95+
console.debug(getEnergyDataCollection(this.hass, this.config.energy_collection_key));
9696
}
9797
}, ENERGY_DATA_TIMEOUT * 2);
9898
energyPromise.catch(err => {

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export interface SankeyChartConfig extends LovelaceCardConfig {
5353
show_states?: boolean;
5454
show_units?: boolean;
5555
energy_date_selection?: boolean;
56+
energy_collection_key?: string;
5657
min_box_size?: number;
5758
min_box_distance?: number;
5859
throttle?: number;

0 commit comments

Comments
 (0)