diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100755
index 00000000..5ee7abd8
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1 @@
+pnpm exec lint-staged
diff --git a/custom_components/hass_datapoints/hass-datapoints-cards.js b/custom_components/hass_datapoints/hass-datapoints-cards.js
index e3e65a61..69a923ff 100644
--- a/custom_components/hass_datapoints/hass-datapoints-cards.js
+++ b/custom_components/hass_datapoints/hass-datapoints-cards.js
@@ -735,7 +735,7 @@
}
//#endregion
//#region custom_components/hass_datapoints/src/cards/action/action.styles.ts
- var styles$78 = i$5`
+ var styles$79 = i$5`
:host {
display: block;
}
@@ -844,7 +844,7 @@
];
//#endregion
//#region custom_components/hass_datapoints/src/atoms/display/color-swatch/color-swatch.styles.ts
- var styles$77 = i$5`
+ var styles$78 = i$5`
:host {
display: block;
}
@@ -1021,13 +1021,13 @@
`;
}
};
- _defineProperty(ColorSwatch, "styles", styles$77);
+ _defineProperty(ColorSwatch, "styles", styles$78);
__decorate([n$1({ type: String })], ColorSwatch.prototype, "color", null);
__decorate([n$1({ type: String })], ColorSwatch.prototype, "label", null);
customElements.define("color-swatch", ColorSwatch);
//#endregion
//#region custom_components/hass_datapoints/src/atoms/display/feedback-banner/feedback-banner.styles.ts
- var styles$76 = i$5`
+ var styles$77 = i$5`
:host {
display: block;
}
@@ -1059,14 +1059,14 @@
var _kind_accessor_storage = /* @__PURE__ */ new WeakMap();
var _text_accessor_storage$2 = /* @__PURE__ */ new WeakMap();
var _visible_accessor_storage$2 = /* @__PURE__ */ new WeakMap();
- var _variant_accessor_storage = /* @__PURE__ */ new WeakMap();
+ var _variant_accessor_storage$1 = /* @__PURE__ */ new WeakMap();
var FeedbackBanner = class extends i$2 {
constructor(..._args) {
super(..._args);
_classPrivateFieldInitSpec(this, _kind_accessor_storage, "");
_classPrivateFieldInitSpec(this, _text_accessor_storage$2, "");
_classPrivateFieldInitSpec(this, _visible_accessor_storage$2, false);
- _classPrivateFieldInitSpec(this, _variant_accessor_storage, "default");
+ _classPrivateFieldInitSpec(this, _variant_accessor_storage$1, "default");
}
get kind() {
return _classPrivateFieldGet2(_kind_accessor_storage, this);
@@ -1087,10 +1087,10 @@
_classPrivateFieldSet2(_visible_accessor_storage$2, this, value);
}
get variant() {
- return _classPrivateFieldGet2(_variant_accessor_storage, this);
+ return _classPrivateFieldGet2(_variant_accessor_storage$1, this);
}
set variant(value) {
- _classPrivateFieldSet2(_variant_accessor_storage, this, value);
+ _classPrivateFieldSet2(_variant_accessor_storage$1, this, value);
}
render() {
if (!this.text) return A;
@@ -1104,7 +1104,7 @@
`;
}
};
- _defineProperty(FeedbackBanner, "styles", styles$76);
+ _defineProperty(FeedbackBanner, "styles", styles$77);
__decorate([n$1({ type: String })], FeedbackBanner.prototype, "kind", null);
__decorate([n$1({ type: String })], FeedbackBanner.prototype, "text", null);
__decorate([n$1({ type: Boolean })], FeedbackBanner.prototype, "visible", null);
@@ -1112,7 +1112,7 @@
customElements.define("feedback-banner", FeedbackBanner);
//#endregion
//#region custom_components/hass_datapoints/src/cards/action/action-targets/action-targets.styles.ts
- var styles$75 = i$5`
+ var styles$76 = i$5`
:host {
display: block;
}
@@ -1124,7 +1124,7 @@
`;
//#endregion
//#region custom_components/hass_datapoints/src/molecules/chip-group/chip-group.styles.ts
- var styles$74 = i$5`
+ var styles$75 = i$5`
:host {
display: block;
}
@@ -1141,7 +1141,7 @@
`;
//#endregion
//#region custom_components/hass_datapoints/src/atoms/form/entity-chip/entity-chip.styles.ts
- var styles$73 = i$5`
+ var styles$74 = i$5`
:host {
display: inline-flex;
}
@@ -1272,7 +1272,7 @@
`;
}
};
- _defineProperty(EntityChip, "styles", styles$73);
+ _defineProperty(EntityChip, "styles", styles$74);
__decorate([n$1({ type: String })], EntityChip.prototype, "type", null);
__decorate([n$1({
type: String,
@@ -1346,7 +1346,7 @@
`;
}
};
- _defineProperty(ChipGroup, "styles", styles$74);
+ _defineProperty(ChipGroup, "styles", styles$75);
__decorate([n$1({ type: Array })], ChipGroup.prototype, "items", null);
__decorate([n$1({ type: Object })], ChipGroup.prototype, "hass", null);
__decorate([n$1({ type: Boolean })], ChipGroup.prototype, "removable", null);
@@ -1433,7 +1433,7 @@
`;
}
};
- _defineProperty(CardActionTargets, "styles", styles$75);
+ _defineProperty(CardActionTargets, "styles", styles$76);
__decorate([n$1({ attribute: false })], CardActionTargets.prototype, "hass", null);
__decorate([n$1({
type: Boolean,
@@ -1796,7 +1796,7 @@
_feedbackText: { state: true },
_feedbackVisible: { state: true }
});
- _defineProperty(HassDatapointsActionCard, "styles", styles$78);
+ _defineProperty(HassDatapointsActionCard, "styles", styles$79);
__decorate([e$4("#msg")], HassDatapointsActionCard.prototype, "_msgEl", null);
__decorate([e$4("#btn")], HassDatapointsActionCard.prototype, "_btnEl", null);
__decorate([e$4("#ann")], HassDatapointsActionCard.prototype, "_annEl", null);
@@ -2223,10 +2223,10 @@
];
//#endregion
//#region custom_components/hass_datapoints/src/cards/action/i18n/fi.ts
- var fi_exports$28 = /* @__PURE__ */ __exportAll({ translations: () => translations$167 });
- var translations$167;
- var init_fi$28 = __esmMin((() => {
- translations$167 = {
+ var fi_exports$29 = /* @__PURE__ */ __exportAll({ translations: () => translations$173 });
+ var translations$173;
+ var init_fi$29 = __esmMin((() => {
+ translations$173 = {
General: "Yleiset",
"Related items": "Liittyvät kohteet",
"Datapoint Appearance": "Datapisteen ulkoasu",
@@ -2243,20 +2243,20 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/history/history-chart/i18n/fi.ts
- var fi_exports$27 = /* @__PURE__ */ __exportAll({ translations: () => translations$166 });
- var translations$166;
- var init_fi$27 = __esmMin((() => {
- translations$166 = {
+ var fi_exports$28 = /* @__PURE__ */ __exportAll({ translations: () => translations$172 });
+ var translations$172;
+ var init_fi$28 = __esmMin((() => {
+ translations$172 = {
"Date window:": "Aikaikkuna:",
"Actual:": "Todellinen:"
};
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/history/i18n/fi.ts
- var fi_exports$26 = /* @__PURE__ */ __exportAll({ translations: () => translations$165 });
- var translations$165;
- var init_fi$26 = __esmMin((() => {
- translations$165 = {
+ var fi_exports$27 = /* @__PURE__ */ __exportAll({ translations: () => translations$171 });
+ var translations$171;
+ var init_fi$27 = __esmMin((() => {
+ translations$171 = {
General: "Yleiset",
Entity: "Entiteetti",
"Multiple entities": "Useita entiteettejä",
@@ -2273,10 +2273,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/list/i18n/fi.ts
- var fi_exports$25 = /* @__PURE__ */ __exportAll({ translations: () => translations$164 });
- var translations$164;
- var init_fi$25 = __esmMin((() => {
- translations$164 = {
+ var fi_exports$26 = /* @__PURE__ */ __exportAll({ translations: () => translations$170 });
+ var translations$170;
+ var init_fi$26 = __esmMin((() => {
+ translations$170 = {
"Search datapoints…": "Hae datapisteitä…",
"Delete record": "Poista tietue",
Delete: "Poista",
@@ -2294,10 +2294,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/quick/i18n/fi.ts
- var fi_exports$24 = /* @__PURE__ */ __exportAll({ translations: () => translations$163 });
- var translations$163;
- var init_fi$24 = __esmMin((() => {
- translations$163 = {
+ var fi_exports$25 = /* @__PURE__ */ __exportAll({ translations: () => translations$169 });
+ var translations$169;
+ var init_fi$25 = __esmMin((() => {
+ translations$169 = {
General: "Yleiset",
"Icon & colour": "Kuvake ja väri",
"Related items": "Liittyvät kohteet",
@@ -2315,10 +2315,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/sensor/i18n/fi.ts
- var fi_exports$23 = /* @__PURE__ */ __exportAll({ translations: () => translations$162 });
- var translations$162;
- var init_fi$23 = __esmMin((() => {
- translations$162 = {
+ var fi_exports$24 = /* @__PURE__ */ __exportAll({ translations: () => translations$168 });
+ var translations$168;
+ var init_fi$24 = __esmMin((() => {
+ translations$168 = {
Entity: "Entiteetti",
Display: "Näyttö",
"Records list": "Tietueiden luettelo",
@@ -2338,10 +2338,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/chart/i18n/fi.ts
- var fi_exports$22 = /* @__PURE__ */ __exportAll({ translations: () => translations$161 });
- var translations$161;
- var init_fi$22 = __esmMin((() => {
- translations$161 = {
+ var fi_exports$23 = /* @__PURE__ */ __exportAll({ translations: () => translations$167 });
+ var translations$167;
+ var init_fi$23 = __esmMin((() => {
+ translations$167 = {
"⚠️ Anomaly Insight": "⚠️ Poikkeavuushavainto",
"⚠️ Multi-method Anomaly": "⚠️ Monimenetelmäinen poikkeavuus",
"Click the highlighted circle to add an annotation.": "Klikkaa korostettua ympyrää lisätäksesi huomautuksen.",
@@ -2376,10 +2376,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/ha/i18n/fi.ts
- var fi_exports$21 = /* @__PURE__ */ __exportAll({ translations: () => translations$160 });
- var translations$160;
- var init_fi$21 = __esmMin((() => {
- translations$160 = {
+ var fi_exports$22 = /* @__PURE__ */ __exportAll({ translations: () => translations$166 });
+ var translations$166;
+ var init_fi$22 = __esmMin((() => {
+ translations$166 = {
"Confirm delete": "Vahvista poisto",
"Are you sure you want to delete this item?": "Oletko varma, että haluat poistaa tämän kohteen?",
Cancel: "Peruuta",
@@ -2394,10 +2394,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/history-page/i18n/fi.ts
- var fi_exports$20 = /* @__PURE__ */ __exportAll({ translations: () => translations$159 });
- var translations$159;
- var init_fi$20 = __esmMin((() => {
- translations$159 = {
+ var fi_exports$21 = /* @__PURE__ */ __exportAll({ translations: () => translations$165 });
+ var translations$165;
+ var init_fi$21 = __esmMin((() => {
+ translations$165 = {
"entity_id={0}": "entity_id={0}",
"device_id={0}": "device_id={0}",
"area_id={0}": "area_id={0}",
@@ -2495,20 +2495,68 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/timeline/i18n/fi.ts
- var fi_exports$19 = /* @__PURE__ */ __exportAll({ translations: () => translations$158 });
- var translations$158;
- var init_fi$19 = __esmMin((() => {
- translations$158 = {
+ var fi_exports$20 = /* @__PURE__ */ __exportAll({ translations: () => translations$164 });
+ var translations$164;
+ var init_fi$20 = __esmMin((() => {
+ translations$164 = {
Wk: "Vk",
"Week of": "Viikko"
};
}));
//#endregion
+ //#region custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/fi.ts
+ var fi_exports$19 = /* @__PURE__ */ __exportAll({ translations: () => translations$163 });
+ var translations$163;
+ var init_fi$19 = __esmMin((() => {
+ translations$163 = {
+ "Aggregate series": "Koosteet",
+ "Count changes to a state": "Laske muutokset tilaan",
+ "Count all state changes": "Laske kaikki tilamuutokset",
+ "Time in a state": "Aika tilassa",
+ "Percentage of time in a state": "Tilassa vietetyn ajan osuus prosentteina",
+ Hourly: "Tunneittain",
+ Daily: "Päivittäin",
+ Weekly: "Viikoittain",
+ Monthly: "Kuukausittain",
+ On: "Päällä",
+ Off: "Pois",
+ Open: "Auki",
+ Closed: "Kiinni",
+ Detected: "Havaittu",
+ Clear: "Vapaa",
+ Occupied: "Varattu",
+ Unlocked: "Avattu",
+ Locked: "Lukittu",
+ Mean: "Keskiarvo",
+ Minimum: "Pienin",
+ Maximum: "Suurin",
+ Median: "Mediaani",
+ "First reading": "Ensimmäinen lukema",
+ "Last reading": "Viimeisin lukema",
+ "All event types": "Kaikki tapahtumatyypit",
+ Consumption: "Kulutus",
+ "Meter reset detected": "Mittarin nollaus havaittu",
+ "Ambiguous meter reset, not counted": "Epäselvä mittarin nollaus, ei laskettu",
+ Cumulative: "Kumulatiivinen",
+ "Reset total to zoomed range": "Nollaa kokonaismäärä zoomattuun alueeseen",
+ "Partial period": "Osittainen jakso",
+ "Incomplete history": "Puutteellinen historia",
+ "Dashed outlines mark partial periods cut by the selected range; hatched or dotted marks show periods with gaps in recorded history.": "Katkoviivareunat osoittavat valitun aikavälin reunoilla katkenneet osittaiset jaksot; viivoitetut tai pisteviivamerkinnät osoittavat jaksot, joiden tallennetussa historiassa on aukkoja.",
+ "Counts only include activity in retained history; earlier activity is not shown.": "Laskennat sisältävät vain säilytetyn historian toiminnan; aiempaa toimintaa ei näytetä.",
+ "Count events": "Laske tapahtumat",
+ Interval: "Jakso",
+ State: "Tila",
+ "Every minute": "Joka minuutti",
+ "Every 15 minutes": "Joka 15. minuutti",
+ "Every 30 minutes": "Joka 30. minuutti"
+ };
+ }));
+ //#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-anomaly-group/i18n/fi.ts
- var fi_exports$18 = /* @__PURE__ */ __exportAll({ translations: () => translations$157 });
- var translations$157;
+ var fi_exports$18 = /* @__PURE__ */ __exportAll({ translations: () => translations$162 });
+ var translations$162;
var init_fi$18 = __esmMin((() => {
- translations$157 = {
+ translations$162 = {
Direction: "Suunta",
Both: "Molemmat",
Above: "Yläpuolella",
@@ -2556,10 +2604,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-delta-group/i18n/fi.ts
- var fi_exports$17 = /* @__PURE__ */ __exportAll({ translations: () => translations$156 });
- var translations$156;
+ var fi_exports$17 = /* @__PURE__ */ __exportAll({ translations: () => translations$161 });
+ var translations$161;
var init_fi$17 = __esmMin((() => {
- translations$156 = {
+ translations$161 = {
"Show delta vs selected date window": "Näytä delta vs. valittu aikaikkuna",
"Select a date window tab to enable delta analysis.": "Valitse aikaikkuna-välilehti ottaaksesi delta-analyysin käyttöön.",
"Show delta in tooltip": "Näytä delta työkaluvihjeessä",
@@ -2568,10 +2616,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-rate-group/i18n/fi.ts
- var fi_exports$16 = /* @__PURE__ */ __exportAll({ translations: () => translations$155 });
- var translations$155;
+ var fi_exports$16 = /* @__PURE__ */ __exportAll({ translations: () => translations$160 });
+ var translations$160;
var init_fi$16 = __esmMin((() => {
- translations$155 = {
+ translations$160 = {
"Show rate of change": "Näytä muutosnopeus",
"Show rate of change crosshairs": "Näytä muutosnopeuden tähtäin",
"Rate window": "Muutosikkuna",
@@ -2583,10 +2631,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-sample-group/i18n/fi.ts
- var fi_exports$15 = /* @__PURE__ */ __exportAll({ translations: () => translations$154 });
- var translations$154;
+ var fi_exports$15 = /* @__PURE__ */ __exportAll({ translations: () => translations$159 });
+ var translations$159;
var init_fi$15 = __esmMin((() => {
- translations$154 = {
+ translations$159 = {
Downsampling: "Alasnäytteistys",
Interval: "Väli",
Aggregate: "Kooste",
@@ -2618,20 +2666,20 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-summary-group/i18n/fi.ts
- var fi_exports$14 = /* @__PURE__ */ __exportAll({ translations: () => translations$153 });
- var translations$153;
+ var fi_exports$14 = /* @__PURE__ */ __exportAll({ translations: () => translations$158 });
+ var translations$158;
var init_fi$14 = __esmMin((() => {
- translations$153 = {
+ translations$158 = {
"Show min / max / mean": "Näytä min / max / keskiarvo",
"Show range shading": "Näytä aluevarjostus"
};
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-threshold-group/i18n/fi.ts
- var fi_exports$13 = /* @__PURE__ */ __exportAll({ translations: () => translations$152 });
- var translations$152;
+ var fi_exports$13 = /* @__PURE__ */ __exportAll({ translations: () => translations$157 });
+ var translations$157;
var init_fi$13 = __esmMin((() => {
- translations$152 = {
+ translations$157 = {
"Show threshold analysis": "Näytä kynnysanalyysi",
"Shade threshold area": "Varjosta kynnysalue",
Threshold: "Kynnys",
@@ -2642,10 +2690,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-trend-group/i18n/fi.ts
- var fi_exports$12 = /* @__PURE__ */ __exportAll({ translations: () => translations$151 });
- var translations$151;
+ var fi_exports$12 = /* @__PURE__ */ __exportAll({ translations: () => translations$156 });
+ var translations$156;
var init_fi$12 = __esmMin((() => {
- translations$151 = {
+ translations$156 = {
"Show trend lines": "Näytä trendiviivat",
"Show trend crosshairs": "Näytä trenditähtäin",
"Trend method": "Trendimenetelmä",
@@ -2663,10 +2711,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/anomaly-monitor-wizard/i18n/fi.ts
- var fi_exports$11 = /* @__PURE__ */ __exportAll({ translations: () => translations$150 });
- var translations$150;
+ var fi_exports$11 = /* @__PURE__ */ __exportAll({ translations: () => translations$155 });
+ var translations$155;
var init_fi$11 = __esmMin((() => {
- translations$150 = {
+ translations$155 = {
"Add all series from chart": "Lisää kaikki sarjat kaaviosta",
"Add from current chart": "Lisää nykyisestä kaaviosta",
"Create anomaly monitor": "Luo poikkeamamonitori",
@@ -2676,17 +2724,17 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/comparison-tab-rail/i18n/fi.ts
- var fi_exports$10 = /* @__PURE__ */ __exportAll({ translations: () => translations$149 });
- var translations$149;
+ var fi_exports$10 = /* @__PURE__ */ __exportAll({ translations: () => translations$154 });
+ var translations$154;
var init_fi$10 = __esmMin((() => {
- translations$149 = { "Add date window": "Lisää aikaikkuna" };
+ translations$154 = { "Add date window": "Lisää aikaikkuna" };
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/date-window-dialog/i18n/fi.ts
- var fi_exports$9 = /* @__PURE__ */ __exportAll({ translations: () => translations$148 });
- var translations$148;
+ var fi_exports$9 = /* @__PURE__ */ __exportAll({ translations: () => translations$153 });
+ var translations$153;
var init_fi$9 = __esmMin((() => {
- translations$148 = {
+ translations$153 = {
"A date window saves a named date range as a tab, so you can quickly preview it against the selected range or jump the chart back to it later.": "Aikaikkuna tallentaa nimetyn päivävälin välilehteksi, jotta voit nopeasti esikatsella sitä suhteessa valittuun alueeseen tai palata kaavion kyseiseen ajanjaksoon.",
Name: "Nimi",
"e.g. Heating season start": "esim. Lämmityskausi alkaa",
@@ -2703,10 +2751,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/sidebar-options/sections/i18n/fi.ts
- var fi_exports$8 = /* @__PURE__ */ __exportAll({ translations: () => translations$147 });
- var translations$147;
+ var fi_exports$8 = /* @__PURE__ */ __exportAll({ translations: () => translations$152 });
+ var translations$152;
var init_fi$8 = __esmMin((() => {
- translations$147 = {
+ translations$152 = {
Datapoints: "Datapisteet",
"Choose which annotation datapoints appear on the chart.": "Valitse, mitkä huomautusten datapisteet näkyvät kaaviossa.",
"Linked to selected targets": "Linkitetty valittuihin kohteisiin",
@@ -2716,10 +2764,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/target-row/i18n/fi.ts
- var fi_exports$7 = /* @__PURE__ */ __exportAll({ translations: () => translations$146 });
- var translations$146;
+ var fi_exports$7 = /* @__PURE__ */ __exportAll({ translations: () => translations$151 });
+ var translations$151;
var init_fi$7 = __esmMin((() => {
- translations$146 = {
+ translations$151 = {
"Analysis configured": "Analyysi määritetty",
"Configure analysis": "Määritä analyysi",
"Stepped series": "Porrastettu sarja",
@@ -2731,17 +2779,17 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/target-row-list/i18n/fi.ts
- var fi_exports$6 = /* @__PURE__ */ __exportAll({ translations: () => translations$145 });
- var translations$145;
+ var fi_exports$6 = /* @__PURE__ */ __exportAll({ translations: () => translations$150 });
+ var translations$150;
var init_fi$6 = __esmMin((() => {
- translations$145 = {};
+ translations$150 = {};
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/components/ai-query-brief-dialog/i18n/fi.ts
- var fi_exports$5 = /* @__PURE__ */ __exportAll({ translations: () => translations$144 });
- var translations$144;
+ var fi_exports$5 = /* @__PURE__ */ __exportAll({ translations: () => translations$149 });
+ var translations$149;
var init_fi$5 = __esmMin((() => {
- translations$144 = {
+ translations$149 = {
"AI query brief": "AI-kyselytiivistelma",
"This brief is intended for another AI to fetch raw Home Assistant history and hass_datapoints anomaly details. Review it, then copy it into your AI tool of choice.": "Tama tiivistelma on tarkoitettu toiselle AI:lle Home Assistantin raakadat historian ja hass_datapointsin poikkeamien lisatietojen hakemista varten. Tarkista se ja kopioi sitten haluamaasi AI-tyokaluun.",
"Copy-ready brief": "Kopiointivalmis tiivistelma",
@@ -2753,10 +2801,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/components/history-targets/i18n/fi.ts
- var fi_exports$4 = /* @__PURE__ */ __exportAll({ translations: () => translations$143 });
- var translations$143;
+ var fi_exports$4 = /* @__PURE__ */ __exportAll({ translations: () => translations$148 });
+ var translations$148;
var init_fi$4 = __esmMin((() => {
- translations$143 = {
+ translations$148 = {
Targets: "Kohteet",
"Each row controls one chart series.": "Jokainen rivi ohjaa yhtä kaaviosarjaa.",
"Add target": "Lisää kohde",
@@ -2765,10 +2813,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/components/panel-shell/i18n/fi.ts
- var fi_exports$3 = /* @__PURE__ */ __exportAll({ translations: () => translations$142 });
- var translations$142;
+ var fi_exports$3 = /* @__PURE__ */ __exportAll({ translations: () => translations$147 });
+ var translations$147;
var init_fi$3 = __esmMin((() => {
- translations$142 = {
+ translations$147 = {
"Loading Datapoints…": "Ladataan Datapoints…",
Datapoints: "Datapoints",
"Page options": "Sivun asetukset",
@@ -2783,10 +2831,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/components/range-toolbar/i18n/fi.ts
- var fi_exports$2 = /* @__PURE__ */ __exportAll({ translations: () => translations$141 });
- var translations$141;
+ var fi_exports$2 = /* @__PURE__ */ __exportAll({ translations: () => translations$146 });
+ var translations$146;
var init_fi$2 = __esmMin((() => {
- translations$141 = {
+ translations$146 = {
"Toggle sidebar": "Vaihda sivupalkki",
Start: "Alku",
End: "Loppu",
@@ -2811,16 +2859,17 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/i18n/fi.ts
- var fi_exports$1 = /* @__PURE__ */ __exportAll({ translations: () => translations$140 });
- var translations$140;
+ var fi_exports$1 = /* @__PURE__ */ __exportAll({ translations: () => translations$145 });
+ var translations$145;
var init_fi$1 = __esmMin((() => {
- translations$140 = { "Selected range": "Valittu aikaväli" };
+ translations$145 = { "Selected range": "Valittu aikaväli" };
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/i18n/locales/fi.ts
var fi_exports = /* @__PURE__ */ __exportAll({ templates: () => templates$5 });
var modules$5, merged$5, templates$5;
var init_fi = __esmMin((() => {
+ init_fi$29();
init_fi$28();
init_fi$27();
init_fi$26();
@@ -2850,16 +2899,17 @@
init_fi$2();
init_fi$1();
modules$5 = /* #__PURE__ */ Object.assign({
- "../../../cards/action/i18n/fi.ts": fi_exports$28,
- "../../../cards/history/history-chart/i18n/fi.ts": fi_exports$27,
- "../../../cards/history/i18n/fi.ts": fi_exports$26,
- "../../../cards/list/i18n/fi.ts": fi_exports$25,
- "../../../cards/quick/i18n/fi.ts": fi_exports$24,
- "../../../cards/sensor/i18n/fi.ts": fi_exports$23,
- "../../chart/i18n/fi.ts": fi_exports$22,
- "../../ha/i18n/fi.ts": fi_exports$21,
- "../../history-page/i18n/fi.ts": fi_exports$20,
- "../../timeline/i18n/fi.ts": fi_exports$19,
+ "../../../cards/action/i18n/fi.ts": fi_exports$29,
+ "../../../cards/history/history-chart/i18n/fi.ts": fi_exports$28,
+ "../../../cards/history/i18n/fi.ts": fi_exports$27,
+ "../../../cards/list/i18n/fi.ts": fi_exports$26,
+ "../../../cards/quick/i18n/fi.ts": fi_exports$25,
+ "../../../cards/sensor/i18n/fi.ts": fi_exports$24,
+ "../../chart/i18n/fi.ts": fi_exports$23,
+ "../../ha/i18n/fi.ts": fi_exports$22,
+ "../../history-page/i18n/fi.ts": fi_exports$21,
+ "../../timeline/i18n/fi.ts": fi_exports$20,
+ "../../../molecules/analysis-aggregate-group/i18n/fi.ts": fi_exports$19,
"../../../molecules/analysis-anomaly-group/i18n/fi.ts": fi_exports$18,
"../../../molecules/analysis-delta-group/i18n/fi.ts": fi_exports$17,
"../../../molecules/analysis-rate-group/i18n/fi.ts": fi_exports$16,
@@ -2885,10 +2935,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/action/i18n/fr.ts
- var fr_exports$28 = /* @__PURE__ */ __exportAll({ translations: () => translations$139 });
- var translations$139;
- var init_fr$28 = __esmMin((() => {
- translations$139 = {
+ var fr_exports$29 = /* @__PURE__ */ __exportAll({ translations: () => translations$144 });
+ var translations$144;
+ var init_fr$29 = __esmMin((() => {
+ translations$144 = {
General: "Général",
"Related items": "Éléments liés",
"Datapoint Appearance": "Apparence du point de données",
@@ -2905,20 +2955,20 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/history/history-chart/i18n/fr.ts
- var fr_exports$27 = /* @__PURE__ */ __exportAll({ translations: () => translations$138 });
- var translations$138;
- var init_fr$27 = __esmMin((() => {
- translations$138 = {
+ var fr_exports$28 = /* @__PURE__ */ __exportAll({ translations: () => translations$143 });
+ var translations$143;
+ var init_fr$28 = __esmMin((() => {
+ translations$143 = {
"Date window:": "Fenêtre de dates :",
"Actual:": "Réel :"
};
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/history/i18n/fr.ts
- var fr_exports$26 = /* @__PURE__ */ __exportAll({ translations: () => translations$137 });
- var translations$137;
- var init_fr$26 = __esmMin((() => {
- translations$137 = {
+ var fr_exports$27 = /* @__PURE__ */ __exportAll({ translations: () => translations$142 });
+ var translations$142;
+ var init_fr$27 = __esmMin((() => {
+ translations$142 = {
General: "Général",
Entity: "Entité",
"Multiple entities": "Plusieurs entités",
@@ -2935,10 +2985,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/list/i18n/fr.ts
- var fr_exports$25 = /* @__PURE__ */ __exportAll({ translations: () => translations$136 });
- var translations$136;
- var init_fr$25 = __esmMin((() => {
- translations$136 = {
+ var fr_exports$26 = /* @__PURE__ */ __exportAll({ translations: () => translations$141 });
+ var translations$141;
+ var init_fr$26 = __esmMin((() => {
+ translations$141 = {
"Search datapoints…": "Rechercher des points de données…",
"Delete record": "Supprimer l’enregistrement",
Delete: "Supprimer",
@@ -2956,10 +3006,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/quick/i18n/fr.ts
- var fr_exports$24 = /* @__PURE__ */ __exportAll({ translations: () => translations$135 });
- var translations$135;
- var init_fr$24 = __esmMin((() => {
- translations$135 = {
+ var fr_exports$25 = /* @__PURE__ */ __exportAll({ translations: () => translations$140 });
+ var translations$140;
+ var init_fr$25 = __esmMin((() => {
+ translations$140 = {
General: "Général",
"Icon & colour": "Icône et couleur",
"Related items": "Éléments liés",
@@ -2977,10 +3027,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/sensor/i18n/fr.ts
- var fr_exports$23 = /* @__PURE__ */ __exportAll({ translations: () => translations$134 });
- var translations$134;
- var init_fr$23 = __esmMin((() => {
- translations$134 = {
+ var fr_exports$24 = /* @__PURE__ */ __exportAll({ translations: () => translations$139 });
+ var translations$139;
+ var init_fr$24 = __esmMin((() => {
+ translations$139 = {
Entity: "Entité",
Display: "Affichage",
"Records list": "Liste des enregistrements",
@@ -3000,10 +3050,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/chart/i18n/fr.ts
- var fr_exports$22 = /* @__PURE__ */ __exportAll({ translations: () => translations$133 });
- var translations$133;
- var init_fr$22 = __esmMin((() => {
- translations$133 = {
+ var fr_exports$23 = /* @__PURE__ */ __exportAll({ translations: () => translations$138 });
+ var translations$138;
+ var init_fr$23 = __esmMin((() => {
+ translations$138 = {
"⚠️ Anomaly Insight": "⚠️ Analyse d’anomalie",
"⚠️ Multi-method Anomaly": "⚠️ Anomalie multi-méthodes",
"Click the highlighted circle to add an annotation.": "Cliquez sur le cercle en surbrillance pour ajouter une annotation.",
@@ -3038,10 +3088,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/ha/i18n/fr.ts
- var fr_exports$21 = /* @__PURE__ */ __exportAll({ translations: () => translations$132 });
- var translations$132;
- var init_fr$21 = __esmMin((() => {
- translations$132 = {
+ var fr_exports$22 = /* @__PURE__ */ __exportAll({ translations: () => translations$137 });
+ var translations$137;
+ var init_fr$22 = __esmMin((() => {
+ translations$137 = {
"Confirm delete": "Confirmer la suppression",
"Are you sure you want to delete this item?": "Voulez-vous vraiment supprimer cet élément ?",
Cancel: "Annuler",
@@ -3056,10 +3106,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/history-page/i18n/fr.ts
- var fr_exports$20 = /* @__PURE__ */ __exportAll({ translations: () => translations$131 });
- var translations$131;
- var init_fr$20 = __esmMin((() => {
- translations$131 = {
+ var fr_exports$21 = /* @__PURE__ */ __exportAll({ translations: () => translations$136 });
+ var translations$136;
+ var init_fr$21 = __esmMin((() => {
+ translations$136 = {
"entity_id={0}": "entity_id={0}",
"device_id={0}": "device_id={0}",
"area_id={0}": "area_id={0}",
@@ -3157,20 +3207,68 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/timeline/i18n/fr.ts
- var fr_exports$19 = /* @__PURE__ */ __exportAll({ translations: () => translations$130 });
- var translations$130;
- var init_fr$19 = __esmMin((() => {
- translations$130 = {
+ var fr_exports$20 = /* @__PURE__ */ __exportAll({ translations: () => translations$135 });
+ var translations$135;
+ var init_fr$20 = __esmMin((() => {
+ translations$135 = {
Wk: "Sem.",
"Week of": "Semaine du"
};
}));
//#endregion
+ //#region custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/fr.ts
+ var fr_exports$19 = /* @__PURE__ */ __exportAll({ translations: () => translations$134 });
+ var translations$134;
+ var init_fr$19 = __esmMin((() => {
+ translations$134 = {
+ "Aggregate series": "Séries agrégées",
+ "Count changes to a state": "Compter les passages à un état",
+ "Count all state changes": "Compter tous les changements d'état",
+ "Time in a state": "Durée dans un état",
+ "Percentage of time in a state": "Pourcentage du temps dans un état",
+ Hourly: "Par heure",
+ Daily: "Par jour",
+ Weekly: "Par semaine",
+ Monthly: "Par mois",
+ On: "Allumé",
+ Off: "Éteint",
+ Open: "Ouvert",
+ Closed: "Fermé",
+ Detected: "Détecté",
+ Clear: "Libre",
+ Occupied: "Occupé",
+ Unlocked: "Déverrouillé",
+ Locked: "Verrouillé",
+ Mean: "Moyenne",
+ Minimum: "Minimum",
+ Maximum: "Maximum",
+ Median: "Médiane",
+ "First reading": "Première mesure",
+ "Last reading": "Dernière mesure",
+ "All event types": "Tous les types d'événement",
+ Consumption: "Consommation",
+ "Meter reset detected": "Réinitialisation du compteur détectée",
+ "Ambiguous meter reset, not counted": "Réinitialisation du compteur ambiguë, non comptée",
+ Cumulative: "Cumulé",
+ "Reset total to zoomed range": "Réinitialiser le total sur la plage zoomée",
+ "Partial period": "Période partielle",
+ "Incomplete history": "Historique incomplet",
+ "Dashed outlines mark partial periods cut by the selected range; hatched or dotted marks show periods with gaps in recorded history.": "Les contours en pointillés signalent les périodes partielles coupées par la plage sélectionnée ; les marques hachurées ou pointillées indiquent des périodes avec des lacunes dans l'historique enregistré.",
+ "Counts only include activity in retained history; earlier activity is not shown.": "Les décomptes n'incluent que l'activité de l'historique conservé ; l'activité antérieure n'est pas affichée.",
+ "Count events": "Compter les événements",
+ Interval: "Intervalle",
+ State: "État",
+ "Every minute": "Chaque minute",
+ "Every 15 minutes": "Toutes les 15 minutes",
+ "Every 30 minutes": "Toutes les 30 minutes"
+ };
+ }));
+ //#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-anomaly-group/i18n/fr.ts
- var fr_exports$18 = /* @__PURE__ */ __exportAll({ translations: () => translations$129 });
- var translations$129;
+ var fr_exports$18 = /* @__PURE__ */ __exportAll({ translations: () => translations$133 });
+ var translations$133;
var init_fr$18 = __esmMin((() => {
- translations$129 = {
+ translations$133 = {
Direction: "Sens",
Both: "Les deux",
Above: "Au-dessus",
@@ -3218,10 +3316,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-delta-group/i18n/fr.ts
- var fr_exports$17 = /* @__PURE__ */ __exportAll({ translations: () => translations$128 });
- var translations$128;
+ var fr_exports$17 = /* @__PURE__ */ __exportAll({ translations: () => translations$132 });
+ var translations$132;
var init_fr$17 = __esmMin((() => {
- translations$128 = {
+ translations$132 = {
"Show delta vs selected date window": "Afficher le delta par rapport à la fenêtre de dates sélectionnée",
"Select a date window tab to enable delta analysis.": "Sélectionnez un onglet de fenêtre de dates pour activer l’analyse delta.",
"Show delta in tooltip": "Afficher le delta dans l’infobulle",
@@ -3230,10 +3328,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-rate-group/i18n/fr.ts
- var fr_exports$16 = /* @__PURE__ */ __exportAll({ translations: () => translations$127 });
- var translations$127;
+ var fr_exports$16 = /* @__PURE__ */ __exportAll({ translations: () => translations$131 });
+ var translations$131;
var init_fr$16 = __esmMin((() => {
- translations$127 = {
+ translations$131 = {
"Show rate of change": "Afficher le taux de variation",
"Show rate of change crosshairs": "Afficher les repères du taux de variation",
"Rate window": "Fenêtre de variation",
@@ -3245,10 +3343,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-sample-group/i18n/fr.ts
- var fr_exports$15 = /* @__PURE__ */ __exportAll({ translations: () => translations$126 });
- var translations$126;
+ var fr_exports$15 = /* @__PURE__ */ __exportAll({ translations: () => translations$130 });
+ var translations$130;
var init_fr$15 = __esmMin((() => {
- translations$126 = {
+ translations$130 = {
Downsampling: "Rééchantillonnage",
Interval: "Intervalle",
Aggregate: "Agrégat",
@@ -3280,20 +3378,20 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-summary-group/i18n/fr.ts
- var fr_exports$14 = /* @__PURE__ */ __exportAll({ translations: () => translations$125 });
- var translations$125;
+ var fr_exports$14 = /* @__PURE__ */ __exportAll({ translations: () => translations$129 });
+ var translations$129;
var init_fr$14 = __esmMin((() => {
- translations$125 = {
+ translations$129 = {
"Show min / max / mean": "Afficher min / max / moyenne",
"Show range shading": "Afficher l’ombrage de la plage"
};
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-threshold-group/i18n/fr.ts
- var fr_exports$13 = /* @__PURE__ */ __exportAll({ translations: () => translations$124 });
- var translations$124;
+ var fr_exports$13 = /* @__PURE__ */ __exportAll({ translations: () => translations$128 });
+ var translations$128;
var init_fr$13 = __esmMin((() => {
- translations$124 = {
+ translations$128 = {
"Show threshold analysis": "Afficher l’analyse de seuil",
"Shade threshold area": "Ombrer la zone du seuil",
Threshold: "Seuil",
@@ -3304,10 +3402,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-trend-group/i18n/fr.ts
- var fr_exports$12 = /* @__PURE__ */ __exportAll({ translations: () => translations$123 });
- var translations$123;
+ var fr_exports$12 = /* @__PURE__ */ __exportAll({ translations: () => translations$127 });
+ var translations$127;
var init_fr$12 = __esmMin((() => {
- translations$123 = {
+ translations$127 = {
"Show trend lines": "Afficher les lignes de tendance",
"Show trend crosshairs": "Afficher les repères de tendance",
"Trend method": "Méthode de tendance",
@@ -3325,10 +3423,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/anomaly-monitor-wizard/i18n/fr.ts
- var fr_exports$11 = /* @__PURE__ */ __exportAll({ translations: () => translations$122 });
- var translations$122;
+ var fr_exports$11 = /* @__PURE__ */ __exportAll({ translations: () => translations$126 });
+ var translations$126;
var init_fr$11 = __esmMin((() => {
- translations$122 = {
+ translations$126 = {
"Add all series from chart": "Ajouter toutes les séries du graphique",
"Add from current chart": "Ajouter depuis le graphique actuel",
"Create anomaly monitor": "Créer un moniteur d'anomalies",
@@ -3338,17 +3436,17 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/comparison-tab-rail/i18n/fr.ts
- var fr_exports$10 = /* @__PURE__ */ __exportAll({ translations: () => translations$121 });
- var translations$121;
+ var fr_exports$10 = /* @__PURE__ */ __exportAll({ translations: () => translations$125 });
+ var translations$125;
var init_fr$10 = __esmMin((() => {
- translations$121 = { "Add date window": "Ajouter une fenêtre de dates" };
+ translations$125 = { "Add date window": "Ajouter une fenêtre de dates" };
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/date-window-dialog/i18n/fr.ts
- var fr_exports$9 = /* @__PURE__ */ __exportAll({ translations: () => translations$120 });
- var translations$120;
+ var fr_exports$9 = /* @__PURE__ */ __exportAll({ translations: () => translations$124 });
+ var translations$124;
var init_fr$9 = __esmMin((() => {
- translations$120 = {
+ translations$124 = {
"A date window saves a named date range as a tab, so you can quickly preview it against the selected range or jump the chart back to it later.": "Une fenêtre de dates enregistre une plage de dates nommée comme onglet afin que vous puissiez la prévisualiser rapidement par rapport à la plage sélectionnée ou y revenir plus tard dans le graphique.",
Name: "Nom",
"e.g. Heating season start": "ex. Début de la saison de chauffe",
@@ -3365,10 +3463,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/sidebar-options/sections/i18n/fr.ts
- var fr_exports$8 = /* @__PURE__ */ __exportAll({ translations: () => translations$119 });
- var translations$119;
+ var fr_exports$8 = /* @__PURE__ */ __exportAll({ translations: () => translations$123 });
+ var translations$123;
var init_fr$8 = __esmMin((() => {
- translations$119 = {
+ translations$123 = {
Datapoints: "Points de données",
"Choose which annotation datapoints appear on the chart.": "Choisissez quels points de données d’annotation apparaissent sur le graphique.",
"Linked to selected targets": "Liés aux cibles sélectionnées",
@@ -3378,10 +3476,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/target-row/i18n/fr.ts
- var fr_exports$7 = /* @__PURE__ */ __exportAll({ translations: () => translations$118 });
- var translations$118;
+ var fr_exports$7 = /* @__PURE__ */ __exportAll({ translations: () => translations$122 });
+ var translations$122;
var init_fr$7 = __esmMin((() => {
- translations$118 = {
+ translations$122 = {
"Analysis configured": "Analyse configurée",
"Configure analysis": "Configurer l’analyse",
"Stepped series": "Série en escalier",
@@ -3393,17 +3491,17 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/target-row-list/i18n/fr.ts
- var fr_exports$6 = /* @__PURE__ */ __exportAll({ translations: () => translations$117 });
- var translations$117;
+ var fr_exports$6 = /* @__PURE__ */ __exportAll({ translations: () => translations$121 });
+ var translations$121;
var init_fr$6 = __esmMin((() => {
- translations$117 = {};
+ translations$121 = {};
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/components/ai-query-brief-dialog/i18n/fr.ts
- var fr_exports$5 = /* @__PURE__ */ __exportAll({ translations: () => translations$116 });
- var translations$116;
+ var fr_exports$5 = /* @__PURE__ */ __exportAll({ translations: () => translations$120 });
+ var translations$120;
var init_fr$5 = __esmMin((() => {
- translations$116 = {
+ translations$120 = {
"AI query brief": "Brief de requete IA",
"This brief is intended for another AI to fetch raw Home Assistant history and hass_datapoints anomaly details. Review it, then copy it into your AI tool of choice.": "Ce brief est destine a une autre IA afin de recuperer l'historique brut de Home Assistant et les details d'anomalie de hass_datapoints. Relisez-le, puis copiez-le dans l'outil IA de votre choix.",
"Copy-ready brief": "Brief pret a copier",
@@ -3415,10 +3513,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/components/history-targets/i18n/fr.ts
- var fr_exports$4 = /* @__PURE__ */ __exportAll({ translations: () => translations$115 });
- var translations$115;
+ var fr_exports$4 = /* @__PURE__ */ __exportAll({ translations: () => translations$119 });
+ var translations$119;
var init_fr$4 = __esmMin((() => {
- translations$115 = {
+ translations$119 = {
Targets: "Cibles",
"Each row controls one chart series.": "Chaque ligne contrôle une série du graphique.",
"Add target": "Ajouter une cible",
@@ -3427,10 +3525,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/components/panel-shell/i18n/fr.ts
- var fr_exports$3 = /* @__PURE__ */ __exportAll({ translations: () => translations$114 });
- var translations$114;
+ var fr_exports$3 = /* @__PURE__ */ __exportAll({ translations: () => translations$118 });
+ var translations$118;
var init_fr$3 = __esmMin((() => {
- translations$114 = {
+ translations$118 = {
"Loading Datapoints…": "Chargement des points de données…",
Datapoints: "Points de données",
"Page options": "Options de la page",
@@ -3445,10 +3543,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/components/range-toolbar/i18n/fr.ts
- var fr_exports$2 = /* @__PURE__ */ __exportAll({ translations: () => translations$113 });
- var translations$113;
+ var fr_exports$2 = /* @__PURE__ */ __exportAll({ translations: () => translations$117 });
+ var translations$117;
var init_fr$2 = __esmMin((() => {
- translations$113 = {
+ translations$117 = {
"Toggle sidebar": "Basculer la barre latérale",
Start: "Début",
End: "Fin",
@@ -3473,16 +3571,17 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/i18n/fr.ts
- var fr_exports$1 = /* @__PURE__ */ __exportAll({ translations: () => translations$112 });
- var translations$112;
+ var fr_exports$1 = /* @__PURE__ */ __exportAll({ translations: () => translations$116 });
+ var translations$116;
var init_fr$1 = __esmMin((() => {
- translations$112 = { "Selected range": "Période sélectionnée" };
+ translations$116 = { "Selected range": "Période sélectionnée" };
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/i18n/locales/fr.ts
var fr_exports = /* @__PURE__ */ __exportAll({ templates: () => templates$4 });
var modules$4, merged$4, templates$4;
var init_fr = __esmMin((() => {
+ init_fr$29();
init_fr$28();
init_fr$27();
init_fr$26();
@@ -3512,16 +3611,17 @@
init_fr$2();
init_fr$1();
modules$4 = /* #__PURE__ */ Object.assign({
- "../../../cards/action/i18n/fr.ts": fr_exports$28,
- "../../../cards/history/history-chart/i18n/fr.ts": fr_exports$27,
- "../../../cards/history/i18n/fr.ts": fr_exports$26,
- "../../../cards/list/i18n/fr.ts": fr_exports$25,
- "../../../cards/quick/i18n/fr.ts": fr_exports$24,
- "../../../cards/sensor/i18n/fr.ts": fr_exports$23,
- "../../chart/i18n/fr.ts": fr_exports$22,
- "../../ha/i18n/fr.ts": fr_exports$21,
- "../../history-page/i18n/fr.ts": fr_exports$20,
- "../../timeline/i18n/fr.ts": fr_exports$19,
+ "../../../cards/action/i18n/fr.ts": fr_exports$29,
+ "../../../cards/history/history-chart/i18n/fr.ts": fr_exports$28,
+ "../../../cards/history/i18n/fr.ts": fr_exports$27,
+ "../../../cards/list/i18n/fr.ts": fr_exports$26,
+ "../../../cards/quick/i18n/fr.ts": fr_exports$25,
+ "../../../cards/sensor/i18n/fr.ts": fr_exports$24,
+ "../../chart/i18n/fr.ts": fr_exports$23,
+ "../../ha/i18n/fr.ts": fr_exports$22,
+ "../../history-page/i18n/fr.ts": fr_exports$21,
+ "../../timeline/i18n/fr.ts": fr_exports$20,
+ "../../../molecules/analysis-aggregate-group/i18n/fr.ts": fr_exports$19,
"../../../molecules/analysis-anomaly-group/i18n/fr.ts": fr_exports$18,
"../../../molecules/analysis-delta-group/i18n/fr.ts": fr_exports$17,
"../../../molecules/analysis-rate-group/i18n/fr.ts": fr_exports$16,
@@ -3547,10 +3647,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/action/i18n/de.ts
- var de_exports$28 = /* @__PURE__ */ __exportAll({ translations: () => translations$111 });
- var translations$111;
- var init_de$28 = __esmMin((() => {
- translations$111 = {
+ var de_exports$29 = /* @__PURE__ */ __exportAll({ translations: () => translations$115 });
+ var translations$115;
+ var init_de$29 = __esmMin((() => {
+ translations$115 = {
General: "Allgemein",
"Related items": "Verknüpfte Elemente",
"Datapoint Appearance": "Darstellung des Datenpunkts",
@@ -3567,20 +3667,20 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/history/history-chart/i18n/de.ts
- var de_exports$27 = /* @__PURE__ */ __exportAll({ translations: () => translations$110 });
- var translations$110;
- var init_de$27 = __esmMin((() => {
- translations$110 = {
+ var de_exports$28 = /* @__PURE__ */ __exportAll({ translations: () => translations$114 });
+ var translations$114;
+ var init_de$28 = __esmMin((() => {
+ translations$114 = {
"Date window:": "Datumsfenster:",
"Actual:": "Tatsächlich:"
};
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/history/i18n/de.ts
- var de_exports$26 = /* @__PURE__ */ __exportAll({ translations: () => translations$109 });
- var translations$109;
- var init_de$26 = __esmMin((() => {
- translations$109 = {
+ var de_exports$27 = /* @__PURE__ */ __exportAll({ translations: () => translations$113 });
+ var translations$113;
+ var init_de$27 = __esmMin((() => {
+ translations$113 = {
General: "Allgemein",
Entity: "Entität",
"Multiple entities": "Mehrere Entitäten",
@@ -3597,10 +3697,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/list/i18n/de.ts
- var de_exports$25 = /* @__PURE__ */ __exportAll({ translations: () => translations$108 });
- var translations$108;
- var init_de$25 = __esmMin((() => {
- translations$108 = {
+ var de_exports$26 = /* @__PURE__ */ __exportAll({ translations: () => translations$112 });
+ var translations$112;
+ var init_de$26 = __esmMin((() => {
+ translations$112 = {
"Search datapoints…": "Datenpunkte suchen…",
"Delete record": "Eintrag löschen",
Delete: "Löschen",
@@ -3618,10 +3718,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/quick/i18n/de.ts
- var de_exports$24 = /* @__PURE__ */ __exportAll({ translations: () => translations$107 });
- var translations$107;
- var init_de$24 = __esmMin((() => {
- translations$107 = {
+ var de_exports$25 = /* @__PURE__ */ __exportAll({ translations: () => translations$111 });
+ var translations$111;
+ var init_de$25 = __esmMin((() => {
+ translations$111 = {
General: "Allgemein",
"Icon & colour": "Symbol und Farbe",
"Related items": "Verknüpfte Elemente",
@@ -3639,10 +3739,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/sensor/i18n/de.ts
- var de_exports$23 = /* @__PURE__ */ __exportAll({ translations: () => translations$106 });
- var translations$106;
- var init_de$23 = __esmMin((() => {
- translations$106 = {
+ var de_exports$24 = /* @__PURE__ */ __exportAll({ translations: () => translations$110 });
+ var translations$110;
+ var init_de$24 = __esmMin((() => {
+ translations$110 = {
Entity: "Entität",
Display: "Anzeige",
"Records list": "Eintragsliste",
@@ -3662,10 +3762,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/chart/i18n/de.ts
- var de_exports$22 = /* @__PURE__ */ __exportAll({ translations: () => translations$105 });
- var translations$105;
- var init_de$22 = __esmMin((() => {
- translations$105 = {
+ var de_exports$23 = /* @__PURE__ */ __exportAll({ translations: () => translations$109 });
+ var translations$109;
+ var init_de$23 = __esmMin((() => {
+ translations$109 = {
"⚠️ Anomaly Insight": "⚠️ Anomalie-Einblick",
"⚠️ Multi-method Anomaly": "⚠️ Anomalie mit mehreren Methoden",
"Click the highlighted circle to add an annotation.": "Klicken Sie auf den hervorgehobenen Kreis, um eine Anmerkung hinzuzufügen.",
@@ -3700,10 +3800,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/ha/i18n/de.ts
- var de_exports$21 = /* @__PURE__ */ __exportAll({ translations: () => translations$104 });
- var translations$104;
- var init_de$21 = __esmMin((() => {
- translations$104 = {
+ var de_exports$22 = /* @__PURE__ */ __exportAll({ translations: () => translations$108 });
+ var translations$108;
+ var init_de$22 = __esmMin((() => {
+ translations$108 = {
"Confirm delete": "Löschen bestätigen",
"Are you sure you want to delete this item?": "Möchten Sie dieses Element wirklich löschen?",
Cancel: "Abbrechen",
@@ -3718,10 +3818,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/history-page/i18n/de.ts
- var de_exports$20 = /* @__PURE__ */ __exportAll({ translations: () => translations$103 });
- var translations$103;
- var init_de$20 = __esmMin((() => {
- translations$103 = {
+ var de_exports$21 = /* @__PURE__ */ __exportAll({ translations: () => translations$107 });
+ var translations$107;
+ var init_de$21 = __esmMin((() => {
+ translations$107 = {
"entity_id={0}": "entity_id={0}",
"device_id={0}": "device_id={0}",
"area_id={0}": "area_id={0}",
@@ -3819,20 +3919,68 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/timeline/i18n/de.ts
- var de_exports$19 = /* @__PURE__ */ __exportAll({ translations: () => translations$102 });
- var translations$102;
- var init_de$19 = __esmMin((() => {
- translations$102 = {
+ var de_exports$20 = /* @__PURE__ */ __exportAll({ translations: () => translations$106 });
+ var translations$106;
+ var init_de$20 = __esmMin((() => {
+ translations$106 = {
Wk: "KW",
"Week of": "Woche von"
};
}));
//#endregion
+ //#region custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/de.ts
+ var de_exports$19 = /* @__PURE__ */ __exportAll({ translations: () => translations$105 });
+ var translations$105;
+ var init_de$19 = __esmMin((() => {
+ translations$105 = {
+ "Aggregate series": "Aggregierte Serien",
+ "Count changes to a state": "Wechsel in einen Zustand zählen",
+ "Count all state changes": "Alle Zustandswechsel zählen",
+ "Time in a state": "Zeit in einem Zustand",
+ "Percentage of time in a state": "Zeitanteil in einem Zustand in Prozent",
+ Hourly: "Stündlich",
+ Daily: "Täglich",
+ Weekly: "Wöchentlich",
+ Monthly: "Monatlich",
+ On: "An",
+ Off: "Aus",
+ Open: "Offen",
+ Closed: "Geschlossen",
+ Detected: "Erkannt",
+ Clear: "Frei",
+ Occupied: "Belegt",
+ Unlocked: "Entsperrt",
+ Locked: "Gesperrt",
+ Mean: "Mittelwert",
+ Minimum: "Minimum",
+ Maximum: "Maximum",
+ Median: "Median",
+ "First reading": "Erster Messwert",
+ "Last reading": "Letzter Messwert",
+ "All event types": "Alle Ereignistypen",
+ Consumption: "Verbrauch",
+ "Meter reset detected": "Zählerrücksetzung erkannt",
+ "Ambiguous meter reset, not counted": "Unklare Zählerrücksetzung, nicht gezählt",
+ Cumulative: "Kumulativ",
+ "Reset total to zoomed range": "Summe auf den gezoomten Bereich zurücksetzen",
+ "Partial period": "Teilperiode",
+ "Incomplete history": "Unvollständiger Verlauf",
+ "Dashed outlines mark partial periods cut by the selected range; hatched or dotted marks show periods with gaps in recorded history.": "Gestrichelte Umrandungen kennzeichnen Teilperioden am Rand des gewählten Bereichs; schraffierte oder gepunktete Markierungen zeigen Zeiträume mit Lücken im aufgezeichneten Verlauf.",
+ "Counts only include activity in retained history; earlier activity is not shown.": "Zählungen enthalten nur Aktivität im gespeicherten Verlauf; frühere Aktivität wird nicht angezeigt.",
+ "Count events": "Ereignisse zählen",
+ Interval: "Intervall",
+ State: "Zustand",
+ "Every minute": "Jede Minute",
+ "Every 15 minutes": "Alle 15 Minuten",
+ "Every 30 minutes": "Alle 30 Minuten"
+ };
+ }));
+ //#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-anomaly-group/i18n/de.ts
- var de_exports$18 = /* @__PURE__ */ __exportAll({ translations: () => translations$101 });
- var translations$101;
+ var de_exports$18 = /* @__PURE__ */ __exportAll({ translations: () => translations$104 });
+ var translations$104;
var init_de$18 = __esmMin((() => {
- translations$101 = {
+ translations$104 = {
Direction: "Richtung",
Both: "Beide",
Above: "Oberhalb",
@@ -3880,10 +4028,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-delta-group/i18n/de.ts
- var de_exports$17 = /* @__PURE__ */ __exportAll({ translations: () => translations$100 });
- var translations$100;
+ var de_exports$17 = /* @__PURE__ */ __exportAll({ translations: () => translations$103 });
+ var translations$103;
var init_de$17 = __esmMin((() => {
- translations$100 = {
+ translations$103 = {
"Show delta vs selected date window": "Delta gegenüber dem ausgewählten Datumsfenster anzeigen",
"Select a date window tab to enable delta analysis.": "Wählen Sie einen Tab für ein Datumsfenster, um die Delta-Analyse zu aktivieren.",
"Show delta in tooltip": "Delta im Tooltip anzeigen",
@@ -3892,10 +4040,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-rate-group/i18n/de.ts
- var de_exports$16 = /* @__PURE__ */ __exportAll({ translations: () => translations$99 });
- var translations$99;
+ var de_exports$16 = /* @__PURE__ */ __exportAll({ translations: () => translations$102 });
+ var translations$102;
var init_de$16 = __esmMin((() => {
- translations$99 = {
+ translations$102 = {
"Show rate of change": "Änderungsrate anzeigen",
"Show rate of change crosshairs": "Fadenkreuz für Änderungsrate anzeigen",
"Rate window": "Ratenfenster",
@@ -3907,10 +4055,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-sample-group/i18n/de.ts
- var de_exports$15 = /* @__PURE__ */ __exportAll({ translations: () => translations$98 });
- var translations$98;
+ var de_exports$15 = /* @__PURE__ */ __exportAll({ translations: () => translations$101 });
+ var translations$101;
var init_de$15 = __esmMin((() => {
- translations$98 = {
+ translations$101 = {
Downsampling: "Downsampling",
Interval: "Intervall",
Aggregate: "Aggregation",
@@ -3942,20 +4090,20 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-summary-group/i18n/de.ts
- var de_exports$14 = /* @__PURE__ */ __exportAll({ translations: () => translations$97 });
- var translations$97;
+ var de_exports$14 = /* @__PURE__ */ __exportAll({ translations: () => translations$100 });
+ var translations$100;
var init_de$14 = __esmMin((() => {
- translations$97 = {
+ translations$100 = {
"Show min / max / mean": "Min / Max / Mittelwert anzeigen",
"Show range shading": "Bereichsschattierung anzeigen"
};
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-threshold-group/i18n/de.ts
- var de_exports$13 = /* @__PURE__ */ __exportAll({ translations: () => translations$96 });
- var translations$96;
+ var de_exports$13 = /* @__PURE__ */ __exportAll({ translations: () => translations$99 });
+ var translations$99;
var init_de$13 = __esmMin((() => {
- translations$96 = {
+ translations$99 = {
"Show threshold analysis": "Schwellwertanalyse anzeigen",
"Shade threshold area": "Schwellwertbereich schattieren",
Threshold: "Schwellenwert",
@@ -3966,10 +4114,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-trend-group/i18n/de.ts
- var de_exports$12 = /* @__PURE__ */ __exportAll({ translations: () => translations$95 });
- var translations$95;
+ var de_exports$12 = /* @__PURE__ */ __exportAll({ translations: () => translations$98 });
+ var translations$98;
var init_de$12 = __esmMin((() => {
- translations$95 = {
+ translations$98 = {
"Show trend lines": "Trendlinien anzeigen",
"Show trend crosshairs": "Fadenkreuz für Trend anzeigen",
"Trend method": "Trendmethode",
@@ -3987,10 +4135,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/anomaly-monitor-wizard/i18n/de.ts
- var de_exports$11 = /* @__PURE__ */ __exportAll({ translations: () => translations$94 });
- var translations$94;
+ var de_exports$11 = /* @__PURE__ */ __exportAll({ translations: () => translations$97 });
+ var translations$97;
var init_de$11 = __esmMin((() => {
- translations$94 = {
+ translations$97 = {
"Add all series from chart": "Alle Reihen aus dem Diagramm hinzufügen",
"Add from current chart": "Aus aktuellem Diagramm hinzufügen",
"Create anomaly monitor": "Anomaliemonitor erstellen",
@@ -4000,17 +4148,17 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/comparison-tab-rail/i18n/de.ts
- var de_exports$10 = /* @__PURE__ */ __exportAll({ translations: () => translations$93 });
- var translations$93;
+ var de_exports$10 = /* @__PURE__ */ __exportAll({ translations: () => translations$96 });
+ var translations$96;
var init_de$10 = __esmMin((() => {
- translations$93 = { "Add date window": "Datumsfenster hinzufügen" };
+ translations$96 = { "Add date window": "Datumsfenster hinzufügen" };
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/date-window-dialog/i18n/de.ts
- var de_exports$9 = /* @__PURE__ */ __exportAll({ translations: () => translations$92 });
- var translations$92;
+ var de_exports$9 = /* @__PURE__ */ __exportAll({ translations: () => translations$95 });
+ var translations$95;
var init_de$9 = __esmMin((() => {
- translations$92 = {
+ translations$95 = {
"A date window saves a named date range as a tab, so you can quickly preview it against the selected range or jump the chart back to it later.": "Ein Datumsfenster speichert einen benannten Datumsbereich als Registerkarte, damit Sie ihn schnell mit dem ausgewählten Bereich vergleichen oder später im Diagramm wieder dorthin springen können.",
Name: "Name",
"e.g. Heating season start": "z. B. Beginn der Heizsaison",
@@ -4027,10 +4175,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/sidebar-options/sections/i18n/de.ts
- var de_exports$8 = /* @__PURE__ */ __exportAll({ translations: () => translations$91 });
- var translations$91;
+ var de_exports$8 = /* @__PURE__ */ __exportAll({ translations: () => translations$94 });
+ var translations$94;
var init_de$8 = __esmMin((() => {
- translations$91 = {
+ translations$94 = {
Datapoints: "Datenpunkte",
"Choose which annotation datapoints appear on the chart.": "Wählen Sie aus, welche Anmerkungs-Datenpunkte im Diagramm angezeigt werden.",
"Linked to selected targets": "Mit ausgewählten Zielen verknüpft",
@@ -4040,10 +4188,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/target-row/i18n/de.ts
- var de_exports$7 = /* @__PURE__ */ __exportAll({ translations: () => translations$90 });
- var translations$90;
+ var de_exports$7 = /* @__PURE__ */ __exportAll({ translations: () => translations$93 });
+ var translations$93;
var init_de$7 = __esmMin((() => {
- translations$90 = {
+ translations$93 = {
"Analysis configured": "Analyse konfiguriert",
"Configure analysis": "Analyse konfigurieren",
"Stepped series": "Stufenserie",
@@ -4055,17 +4203,17 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/target-row-list/i18n/de.ts
- var de_exports$6 = /* @__PURE__ */ __exportAll({ translations: () => translations$89 });
- var translations$89;
+ var de_exports$6 = /* @__PURE__ */ __exportAll({ translations: () => translations$92 });
+ var translations$92;
var init_de$6 = __esmMin((() => {
- translations$89 = {};
+ translations$92 = {};
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/components/ai-query-brief-dialog/i18n/de.ts
- var de_exports$5 = /* @__PURE__ */ __exportAll({ translations: () => translations$88 });
- var translations$88;
+ var de_exports$5 = /* @__PURE__ */ __exportAll({ translations: () => translations$91 });
+ var translations$91;
var init_de$5 = __esmMin((() => {
- translations$88 = {
+ translations$91 = {
"AI query brief": "KI-Abfragebriefing",
"This brief is intended for another AI to fetch raw Home Assistant history and hass_datapoints anomaly details. Review it, then copy it into your AI tool of choice.": "Dieses Briefing ist fuer eine andere KI gedacht, damit sie rohe Home-Assistant-Verlaeufe und hass_datapoints-Anomaliedetails abrufen kann. Pruefe es und kopiere es dann in dein KI-Werkzeug deiner Wahl.",
"Copy-ready brief": "Kopierfertiges Briefing",
@@ -4077,10 +4225,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/components/history-targets/i18n/de.ts
- var de_exports$4 = /* @__PURE__ */ __exportAll({ translations: () => translations$87 });
- var translations$87;
+ var de_exports$4 = /* @__PURE__ */ __exportAll({ translations: () => translations$90 });
+ var translations$90;
var init_de$4 = __esmMin((() => {
- translations$87 = {
+ translations$90 = {
Targets: "Ziele",
"Each row controls one chart series.": "Jede Zeile steuert eine Diagrammserie.",
"Add target": "Ziel hinzufügen",
@@ -4089,10 +4237,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/components/panel-shell/i18n/de.ts
- var de_exports$3 = /* @__PURE__ */ __exportAll({ translations: () => translations$86 });
- var translations$86;
+ var de_exports$3 = /* @__PURE__ */ __exportAll({ translations: () => translations$89 });
+ var translations$89;
var init_de$3 = __esmMin((() => {
- translations$86 = {
+ translations$89 = {
"Loading Datapoints…": "Datenpunkte werden geladen…",
Datapoints: "Datenpunkte",
"Page options": "Seitenoptionen",
@@ -4107,10 +4255,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/components/range-toolbar/i18n/de.ts
- var de_exports$2 = /* @__PURE__ */ __exportAll({ translations: () => translations$85 });
- var translations$85;
+ var de_exports$2 = /* @__PURE__ */ __exportAll({ translations: () => translations$88 });
+ var translations$88;
var init_de$2 = __esmMin((() => {
- translations$85 = {
+ translations$88 = {
"Toggle sidebar": "Seitenleiste umschalten",
Start: "Start",
End: "Ende",
@@ -4135,16 +4283,17 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/i18n/de.ts
- var de_exports$1 = /* @__PURE__ */ __exportAll({ translations: () => translations$84 });
- var translations$84;
+ var de_exports$1 = /* @__PURE__ */ __exportAll({ translations: () => translations$87 });
+ var translations$87;
var init_de$1 = __esmMin((() => {
- translations$84 = { "Selected range": "Ausgewählter Zeitraum" };
+ translations$87 = { "Selected range": "Ausgewählter Zeitraum" };
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/i18n/locales/de.ts
var de_exports = /* @__PURE__ */ __exportAll({ templates: () => templates$3 });
var modules$3, merged$3, templates$3;
var init_de = __esmMin((() => {
+ init_de$29();
init_de$28();
init_de$27();
init_de$26();
@@ -4174,16 +4323,17 @@
init_de$2();
init_de$1();
modules$3 = /* #__PURE__ */ Object.assign({
- "../../../cards/action/i18n/de.ts": de_exports$28,
- "../../../cards/history/history-chart/i18n/de.ts": de_exports$27,
- "../../../cards/history/i18n/de.ts": de_exports$26,
- "../../../cards/list/i18n/de.ts": de_exports$25,
- "../../../cards/quick/i18n/de.ts": de_exports$24,
- "../../../cards/sensor/i18n/de.ts": de_exports$23,
- "../../chart/i18n/de.ts": de_exports$22,
- "../../ha/i18n/de.ts": de_exports$21,
- "../../history-page/i18n/de.ts": de_exports$20,
- "../../timeline/i18n/de.ts": de_exports$19,
+ "../../../cards/action/i18n/de.ts": de_exports$29,
+ "../../../cards/history/history-chart/i18n/de.ts": de_exports$28,
+ "../../../cards/history/i18n/de.ts": de_exports$27,
+ "../../../cards/list/i18n/de.ts": de_exports$26,
+ "../../../cards/quick/i18n/de.ts": de_exports$25,
+ "../../../cards/sensor/i18n/de.ts": de_exports$24,
+ "../../chart/i18n/de.ts": de_exports$23,
+ "../../ha/i18n/de.ts": de_exports$22,
+ "../../history-page/i18n/de.ts": de_exports$21,
+ "../../timeline/i18n/de.ts": de_exports$20,
+ "../../../molecules/analysis-aggregate-group/i18n/de.ts": de_exports$19,
"../../../molecules/analysis-anomaly-group/i18n/de.ts": de_exports$18,
"../../../molecules/analysis-delta-group/i18n/de.ts": de_exports$17,
"../../../molecules/analysis-rate-group/i18n/de.ts": de_exports$16,
@@ -4209,10 +4359,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/action/i18n/es.ts
- var es_exports$28 = /* @__PURE__ */ __exportAll({ translations: () => translations$83 });
- var translations$83;
- var init_es$28 = __esmMin((() => {
- translations$83 = {
+ var es_exports$29 = /* @__PURE__ */ __exportAll({ translations: () => translations$86 });
+ var translations$86;
+ var init_es$29 = __esmMin((() => {
+ translations$86 = {
General: "General",
"Related items": "Elementos relacionados",
"Datapoint Appearance": "Apariencia del punto de datos",
@@ -4229,20 +4379,20 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/history/history-chart/i18n/es.ts
- var es_exports$27 = /* @__PURE__ */ __exportAll({ translations: () => translations$82 });
- var translations$82;
- var init_es$27 = __esmMin((() => {
- translations$82 = {
+ var es_exports$28 = /* @__PURE__ */ __exportAll({ translations: () => translations$85 });
+ var translations$85;
+ var init_es$28 = __esmMin((() => {
+ translations$85 = {
"Date window:": "Ventana de fechas:",
"Actual:": "Real:"
};
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/history/i18n/es.ts
- var es_exports$26 = /* @__PURE__ */ __exportAll({ translations: () => translations$81 });
- var translations$81;
- var init_es$26 = __esmMin((() => {
- translations$81 = {
+ var es_exports$27 = /* @__PURE__ */ __exportAll({ translations: () => translations$84 });
+ var translations$84;
+ var init_es$27 = __esmMin((() => {
+ translations$84 = {
General: "General",
Entity: "Entidad",
"Multiple entities": "Varias entidades",
@@ -4259,10 +4409,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/list/i18n/es.ts
- var es_exports$25 = /* @__PURE__ */ __exportAll({ translations: () => translations$80 });
- var translations$80;
- var init_es$25 = __esmMin((() => {
- translations$80 = {
+ var es_exports$26 = /* @__PURE__ */ __exportAll({ translations: () => translations$83 });
+ var translations$83;
+ var init_es$26 = __esmMin((() => {
+ translations$83 = {
"Search datapoints…": "Buscar puntos de datos…",
"Delete record": "Eliminar registro",
Delete: "Eliminar",
@@ -4280,10 +4430,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/quick/i18n/es.ts
- var es_exports$24 = /* @__PURE__ */ __exportAll({ translations: () => translations$79 });
- var translations$79;
- var init_es$24 = __esmMin((() => {
- translations$79 = {
+ var es_exports$25 = /* @__PURE__ */ __exportAll({ translations: () => translations$82 });
+ var translations$82;
+ var init_es$25 = __esmMin((() => {
+ translations$82 = {
General: "General",
"Icon & colour": "Icono y color",
"Related items": "Elementos relacionados",
@@ -4301,10 +4451,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/sensor/i18n/es.ts
- var es_exports$23 = /* @__PURE__ */ __exportAll({ translations: () => translations$78 });
- var translations$78;
- var init_es$23 = __esmMin((() => {
- translations$78 = {
+ var es_exports$24 = /* @__PURE__ */ __exportAll({ translations: () => translations$81 });
+ var translations$81;
+ var init_es$24 = __esmMin((() => {
+ translations$81 = {
Entity: "Entidad",
Display: "Visualización",
"Records list": "Lista de registros",
@@ -4324,10 +4474,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/chart/i18n/es.ts
- var es_exports$22 = /* @__PURE__ */ __exportAll({ translations: () => translations$77 });
- var translations$77;
- var init_es$22 = __esmMin((() => {
- translations$77 = {
+ var es_exports$23 = /* @__PURE__ */ __exportAll({ translations: () => translations$80 });
+ var translations$80;
+ var init_es$23 = __esmMin((() => {
+ translations$80 = {
"⚠️ Anomaly Insight": "⚠️ Información de anomalía",
"⚠️ Multi-method Anomaly": "⚠️ Anomalía multimétodo",
"Click the highlighted circle to add an annotation.": "Haz clic en el círculo resaltado para añadir una anotación.",
@@ -4362,10 +4512,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/ha/i18n/es.ts
- var es_exports$21 = /* @__PURE__ */ __exportAll({ translations: () => translations$76 });
- var translations$76;
- var init_es$21 = __esmMin((() => {
- translations$76 = {
+ var es_exports$22 = /* @__PURE__ */ __exportAll({ translations: () => translations$79 });
+ var translations$79;
+ var init_es$22 = __esmMin((() => {
+ translations$79 = {
"Confirm delete": "Confirmar eliminación",
"Are you sure you want to delete this item?": "¿Seguro que quieres eliminar este elemento?",
Cancel: "Cancelar",
@@ -4380,10 +4530,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/history-page/i18n/es.ts
- var es_exports$20 = /* @__PURE__ */ __exportAll({ translations: () => translations$75 });
- var translations$75;
- var init_es$20 = __esmMin((() => {
- translations$75 = {
+ var es_exports$21 = /* @__PURE__ */ __exportAll({ translations: () => translations$78 });
+ var translations$78;
+ var init_es$21 = __esmMin((() => {
+ translations$78 = {
"entity_id={0}": "entity_id={0}",
"device_id={0}": "device_id={0}",
"area_id={0}": "area_id={0}",
@@ -4481,20 +4631,68 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/timeline/i18n/es.ts
- var es_exports$19 = /* @__PURE__ */ __exportAll({ translations: () => translations$74 });
- var translations$74;
- var init_es$19 = __esmMin((() => {
- translations$74 = {
+ var es_exports$20 = /* @__PURE__ */ __exportAll({ translations: () => translations$77 });
+ var translations$77;
+ var init_es$20 = __esmMin((() => {
+ translations$77 = {
Wk: "Sem.",
"Week of": "Semana del"
};
}));
//#endregion
+ //#region custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/es.ts
+ var es_exports$19 = /* @__PURE__ */ __exportAll({ translations: () => translations$76 });
+ var translations$76;
+ var init_es$19 = __esmMin((() => {
+ translations$76 = {
+ "Aggregate series": "Series agregadas",
+ "Count changes to a state": "Contar cambios a un estado",
+ "Count all state changes": "Contar todos los cambios de estado",
+ "Time in a state": "Tiempo en un estado",
+ "Percentage of time in a state": "Porcentaje de tiempo en un estado",
+ Hourly: "Por hora",
+ Daily: "Diario",
+ Weekly: "Semanal",
+ Monthly: "Mensual",
+ On: "Encendido",
+ Off: "Apagado",
+ Open: "Abierto",
+ Closed: "Cerrado",
+ Detected: "Detectado",
+ Clear: "Libre",
+ Occupied: "Ocupado",
+ Unlocked: "Desbloqueado",
+ Locked: "Bloqueado",
+ Mean: "Media",
+ Minimum: "Mínimo",
+ Maximum: "Máximo",
+ Median: "Mediana",
+ "First reading": "Primera lectura",
+ "Last reading": "Última lectura",
+ "All event types": "Todos los tipos de evento",
+ Consumption: "Consumo",
+ "Meter reset detected": "Reinicio del contador detectado",
+ "Ambiguous meter reset, not counted": "Reinicio del contador ambiguo, no contabilizado",
+ Cumulative: "Acumulado",
+ "Reset total to zoomed range": "Reiniciar el total al rango ampliado",
+ "Partial period": "Periodo parcial",
+ "Incomplete history": "Historial incompleto",
+ "Dashed outlines mark partial periods cut by the selected range; hatched or dotted marks show periods with gaps in recorded history.": "Los contornos discontinuos marcan los periodos parciales cortados por el rango seleccionado; las marcas rayadas o punteadas indican periodos con huecos en el historial registrado.",
+ "Counts only include activity in retained history; earlier activity is not shown.": "Los recuentos solo incluyen la actividad del historial conservado; la actividad anterior no se muestra.",
+ "Count events": "Contar eventos",
+ Interval: "Intervalo",
+ State: "Estado",
+ "Every minute": "Cada minuto",
+ "Every 15 minutes": "Cada 15 minutos",
+ "Every 30 minutes": "Cada 30 minutos"
+ };
+ }));
+ //#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-anomaly-group/i18n/es.ts
- var es_exports$18 = /* @__PURE__ */ __exportAll({ translations: () => translations$73 });
- var translations$73;
+ var es_exports$18 = /* @__PURE__ */ __exportAll({ translations: () => translations$75 });
+ var translations$75;
var init_es$18 = __esmMin((() => {
- translations$73 = {
+ translations$75 = {
Direction: "Dirección",
Both: "Ambos",
Above: "Por encima",
@@ -4542,10 +4740,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-delta-group/i18n/es.ts
- var es_exports$17 = /* @__PURE__ */ __exportAll({ translations: () => translations$72 });
- var translations$72;
+ var es_exports$17 = /* @__PURE__ */ __exportAll({ translations: () => translations$74 });
+ var translations$74;
var init_es$17 = __esmMin((() => {
- translations$72 = {
+ translations$74 = {
"Show delta vs selected date window": "Mostrar delta frente a la ventana de fechas seleccionada",
"Select a date window tab to enable delta analysis.": "Selecciona una pestaña de ventana de fechas para habilitar el análisis delta.",
"Show delta in tooltip": "Mostrar delta en la información sobre herramientas",
@@ -4554,10 +4752,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-rate-group/i18n/es.ts
- var es_exports$16 = /* @__PURE__ */ __exportAll({ translations: () => translations$71 });
- var translations$71;
+ var es_exports$16 = /* @__PURE__ */ __exportAll({ translations: () => translations$73 });
+ var translations$73;
var init_es$16 = __esmMin((() => {
- translations$71 = {
+ translations$73 = {
"Show rate of change": "Mostrar tasa de cambio",
"Show rate of change crosshairs": "Mostrar guías de la tasa de cambio",
"Rate window": "Ventana de tasa",
@@ -4569,10 +4767,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-sample-group/i18n/es.ts
- var es_exports$15 = /* @__PURE__ */ __exportAll({ translations: () => translations$70 });
- var translations$70;
+ var es_exports$15 = /* @__PURE__ */ __exportAll({ translations: () => translations$72 });
+ var translations$72;
var init_es$15 = __esmMin((() => {
- translations$70 = {
+ translations$72 = {
Downsampling: "Submuestreo",
Interval: "Intervalo",
Aggregate: "Agregado",
@@ -4604,20 +4802,20 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-summary-group/i18n/es.ts
- var es_exports$14 = /* @__PURE__ */ __exportAll({ translations: () => translations$69 });
- var translations$69;
+ var es_exports$14 = /* @__PURE__ */ __exportAll({ translations: () => translations$71 });
+ var translations$71;
var init_es$14 = __esmMin((() => {
- translations$69 = {
+ translations$71 = {
"Show min / max / mean": "Mostrar mín. / máx. / media",
"Show range shading": "Mostrar sombreado del rango"
};
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-threshold-group/i18n/es.ts
- var es_exports$13 = /* @__PURE__ */ __exportAll({ translations: () => translations$68 });
- var translations$68;
+ var es_exports$13 = /* @__PURE__ */ __exportAll({ translations: () => translations$70 });
+ var translations$70;
var init_es$13 = __esmMin((() => {
- translations$68 = {
+ translations$70 = {
"Show threshold analysis": "Mostrar análisis de umbral",
"Shade threshold area": "Sombrear área del umbral",
Threshold: "Umbral",
@@ -4628,10 +4826,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-trend-group/i18n/es.ts
- var es_exports$12 = /* @__PURE__ */ __exportAll({ translations: () => translations$67 });
- var translations$67;
+ var es_exports$12 = /* @__PURE__ */ __exportAll({ translations: () => translations$69 });
+ var translations$69;
var init_es$12 = __esmMin((() => {
- translations$67 = {
+ translations$69 = {
"Show trend lines": "Mostrar líneas de tendencia",
"Show trend crosshairs": "Mostrar guías de tendencia",
"Trend method": "Método de tendencia",
@@ -4649,10 +4847,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/anomaly-monitor-wizard/i18n/es.ts
- var es_exports$11 = /* @__PURE__ */ __exportAll({ translations: () => translations$66 });
- var translations$66;
+ var es_exports$11 = /* @__PURE__ */ __exportAll({ translations: () => translations$68 });
+ var translations$68;
var init_es$11 = __esmMin((() => {
- translations$66 = {
+ translations$68 = {
"Add all series from chart": "Añadir todas las series del gráfico",
"Add from current chart": "Añadir desde el gráfico actual",
"Create anomaly monitor": "Crear monitor de anomalías",
@@ -4662,17 +4860,17 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/comparison-tab-rail/i18n/es.ts
- var es_exports$10 = /* @__PURE__ */ __exportAll({ translations: () => translations$65 });
- var translations$65;
+ var es_exports$10 = /* @__PURE__ */ __exportAll({ translations: () => translations$67 });
+ var translations$67;
var init_es$10 = __esmMin((() => {
- translations$65 = { "Add date window": "Añadir ventana de fechas" };
+ translations$67 = { "Add date window": "Añadir ventana de fechas" };
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/date-window-dialog/i18n/es.ts
- var es_exports$9 = /* @__PURE__ */ __exportAll({ translations: () => translations$64 });
- var translations$64;
+ var es_exports$9 = /* @__PURE__ */ __exportAll({ translations: () => translations$66 });
+ var translations$66;
var init_es$9 = __esmMin((() => {
- translations$64 = {
+ translations$66 = {
"A date window saves a named date range as a tab, so you can quickly preview it against the selected range or jump the chart back to it later.": "Una ventana de fechas guarda un rango de fechas con nombre como pestaña, para que puedas previsualizarlo rápidamente frente al rango seleccionado o volver más tarde a él en el gráfico.",
Name: "Nombre",
"e.g. Heating season start": "p. ej., inicio de la temporada de calefacción",
@@ -4689,10 +4887,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/sidebar-options/sections/i18n/es.ts
- var es_exports$8 = /* @__PURE__ */ __exportAll({ translations: () => translations$63 });
- var translations$63;
+ var es_exports$8 = /* @__PURE__ */ __exportAll({ translations: () => translations$65 });
+ var translations$65;
var init_es$8 = __esmMin((() => {
- translations$63 = {
+ translations$65 = {
Datapoints: "Puntos de datos",
"Choose which annotation datapoints appear on the chart.": "Elige qué puntos de datos de anotación aparecen en el gráfico.",
"Linked to selected targets": "Vinculados a los objetivos seleccionados",
@@ -4702,10 +4900,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/target-row/i18n/es.ts
- var es_exports$7 = /* @__PURE__ */ __exportAll({ translations: () => translations$62 });
- var translations$62;
+ var es_exports$7 = /* @__PURE__ */ __exportAll({ translations: () => translations$64 });
+ var translations$64;
var init_es$7 = __esmMin((() => {
- translations$62 = {
+ translations$64 = {
"Analysis configured": "Análisis configurado",
"Configure analysis": "Configurar análisis",
"Stepped series": "Serie escalonada",
@@ -4717,17 +4915,17 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/target-row-list/i18n/es.ts
- var es_exports$6 = /* @__PURE__ */ __exportAll({ translations: () => translations$61 });
- var translations$61;
+ var es_exports$6 = /* @__PURE__ */ __exportAll({ translations: () => translations$63 });
+ var translations$63;
var init_es$6 = __esmMin((() => {
- translations$61 = {};
+ translations$63 = {};
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/components/ai-query-brief-dialog/i18n/es.ts
- var es_exports$5 = /* @__PURE__ */ __exportAll({ translations: () => translations$60 });
- var translations$60;
+ var es_exports$5 = /* @__PURE__ */ __exportAll({ translations: () => translations$62 });
+ var translations$62;
var init_es$5 = __esmMin((() => {
- translations$60 = {
+ translations$62 = {
"AI query brief": "Resumen de consulta para IA",
"This brief is intended for another AI to fetch raw Home Assistant history and hass_datapoints anomaly details. Review it, then copy it into your AI tool of choice.": "Este resumen esta pensado para que otra IA obtenga el historial sin procesar de Home Assistant y los detalles de anomalias de hass_datapoints. Revisalo y luego copialo en la herramienta de IA que prefieras.",
"Copy-ready brief": "Resumen listo para copiar",
@@ -4739,10 +4937,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/components/history-targets/i18n/es.ts
- var es_exports$4 = /* @__PURE__ */ __exportAll({ translations: () => translations$59 });
- var translations$59;
+ var es_exports$4 = /* @__PURE__ */ __exportAll({ translations: () => translations$61 });
+ var translations$61;
var init_es$4 = __esmMin((() => {
- translations$59 = {
+ translations$61 = {
Targets: "Objetivos",
"Each row controls one chart series.": "Cada fila controla una serie del gráfico.",
"Add target": "Añadir objetivo",
@@ -4751,10 +4949,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/components/panel-shell/i18n/es.ts
- var es_exports$3 = /* @__PURE__ */ __exportAll({ translations: () => translations$58 });
- var translations$58;
+ var es_exports$3 = /* @__PURE__ */ __exportAll({ translations: () => translations$60 });
+ var translations$60;
var init_es$3 = __esmMin((() => {
- translations$58 = {
+ translations$60 = {
"Loading Datapoints…": "Cargando puntos de datos…",
Datapoints: "Puntos de datos",
"Page options": "Opciones de la página",
@@ -4769,10 +4967,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/components/range-toolbar/i18n/es.ts
- var es_exports$2 = /* @__PURE__ */ __exportAll({ translations: () => translations$57 });
- var translations$57;
+ var es_exports$2 = /* @__PURE__ */ __exportAll({ translations: () => translations$59 });
+ var translations$59;
var init_es$2 = __esmMin((() => {
- translations$57 = {
+ translations$59 = {
"Toggle sidebar": "Alternar barra lateral",
Start: "Inicio",
End: "Fin",
@@ -4797,16 +4995,17 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/i18n/es.ts
- var es_exports$1 = /* @__PURE__ */ __exportAll({ translations: () => translations$56 });
- var translations$56;
+ var es_exports$1 = /* @__PURE__ */ __exportAll({ translations: () => translations$58 });
+ var translations$58;
var init_es$1 = __esmMin((() => {
- translations$56 = { "Selected range": "Intervalo seleccionado" };
+ translations$58 = { "Selected range": "Intervalo seleccionado" };
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/i18n/locales/es.ts
var es_exports = /* @__PURE__ */ __exportAll({ templates: () => templates$2 });
var modules$2, merged$2, templates$2;
var init_es = __esmMin((() => {
+ init_es$29();
init_es$28();
init_es$27();
init_es$26();
@@ -4836,16 +5035,17 @@
init_es$2();
init_es$1();
modules$2 = /* #__PURE__ */ Object.assign({
- "../../../cards/action/i18n/es.ts": es_exports$28,
- "../../../cards/history/history-chart/i18n/es.ts": es_exports$27,
- "../../../cards/history/i18n/es.ts": es_exports$26,
- "../../../cards/list/i18n/es.ts": es_exports$25,
- "../../../cards/quick/i18n/es.ts": es_exports$24,
- "../../../cards/sensor/i18n/es.ts": es_exports$23,
- "../../chart/i18n/es.ts": es_exports$22,
- "../../ha/i18n/es.ts": es_exports$21,
- "../../history-page/i18n/es.ts": es_exports$20,
- "../../timeline/i18n/es.ts": es_exports$19,
+ "../../../cards/action/i18n/es.ts": es_exports$29,
+ "../../../cards/history/history-chart/i18n/es.ts": es_exports$28,
+ "../../../cards/history/i18n/es.ts": es_exports$27,
+ "../../../cards/list/i18n/es.ts": es_exports$26,
+ "../../../cards/quick/i18n/es.ts": es_exports$25,
+ "../../../cards/sensor/i18n/es.ts": es_exports$24,
+ "../../chart/i18n/es.ts": es_exports$23,
+ "../../ha/i18n/es.ts": es_exports$22,
+ "../../history-page/i18n/es.ts": es_exports$21,
+ "../../timeline/i18n/es.ts": es_exports$20,
+ "../../../molecules/analysis-aggregate-group/i18n/es.ts": es_exports$19,
"../../../molecules/analysis-anomaly-group/i18n/es.ts": es_exports$18,
"../../../molecules/analysis-delta-group/i18n/es.ts": es_exports$17,
"../../../molecules/analysis-rate-group/i18n/es.ts": es_exports$16,
@@ -4871,10 +5071,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/action/i18n/pt.ts
- var pt_exports$28 = /* @__PURE__ */ __exportAll({ translations: () => translations$55 });
- var translations$55;
- var init_pt$28 = __esmMin((() => {
- translations$55 = {
+ var pt_exports$29 = /* @__PURE__ */ __exportAll({ translations: () => translations$57 });
+ var translations$57;
+ var init_pt$29 = __esmMin((() => {
+ translations$57 = {
General: "Geral",
"Related items": "Itens relacionados",
"Datapoint Appearance": "Aspeto do ponto de dados",
@@ -4891,20 +5091,20 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/history/history-chart/i18n/pt.ts
- var pt_exports$27 = /* @__PURE__ */ __exportAll({ translations: () => translations$54 });
- var translations$54;
- var init_pt$27 = __esmMin((() => {
- translations$54 = {
+ var pt_exports$28 = /* @__PURE__ */ __exportAll({ translations: () => translations$56 });
+ var translations$56;
+ var init_pt$28 = __esmMin((() => {
+ translations$56 = {
"Date window:": "Janela de datas:",
"Actual:": "Real:"
};
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/history/i18n/pt.ts
- var pt_exports$26 = /* @__PURE__ */ __exportAll({ translations: () => translations$53 });
- var translations$53;
- var init_pt$26 = __esmMin((() => {
- translations$53 = {
+ var pt_exports$27 = /* @__PURE__ */ __exportAll({ translations: () => translations$55 });
+ var translations$55;
+ var init_pt$27 = __esmMin((() => {
+ translations$55 = {
General: "Geral",
Entity: "Entidade",
"Multiple entities": "Múltiplas entidades",
@@ -4921,10 +5121,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/list/i18n/pt.ts
- var pt_exports$25 = /* @__PURE__ */ __exportAll({ translations: () => translations$52 });
- var translations$52;
- var init_pt$25 = __esmMin((() => {
- translations$52 = {
+ var pt_exports$26 = /* @__PURE__ */ __exportAll({ translations: () => translations$54 });
+ var translations$54;
+ var init_pt$26 = __esmMin((() => {
+ translations$54 = {
"Search datapoints…": "Pesquisar pontos de dados…",
"Delete record": "Eliminar registo",
Delete: "Eliminar",
@@ -4942,10 +5142,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/quick/i18n/pt.ts
- var pt_exports$24 = /* @__PURE__ */ __exportAll({ translations: () => translations$51 });
- var translations$51;
- var init_pt$24 = __esmMin((() => {
- translations$51 = {
+ var pt_exports$25 = /* @__PURE__ */ __exportAll({ translations: () => translations$53 });
+ var translations$53;
+ var init_pt$25 = __esmMin((() => {
+ translations$53 = {
General: "Geral",
"Icon & colour": "Ícone e cor",
"Related items": "Itens relacionados",
@@ -4963,10 +5163,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/sensor/i18n/pt.ts
- var pt_exports$23 = /* @__PURE__ */ __exportAll({ translations: () => translations$50 });
- var translations$50;
- var init_pt$23 = __esmMin((() => {
- translations$50 = {
+ var pt_exports$24 = /* @__PURE__ */ __exportAll({ translations: () => translations$52 });
+ var translations$52;
+ var init_pt$24 = __esmMin((() => {
+ translations$52 = {
Entity: "Entidade",
Display: "Visualização",
"Records list": "Lista de registos",
@@ -4986,10 +5186,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/chart/i18n/pt.ts
- var pt_exports$22 = /* @__PURE__ */ __exportAll({ translations: () => translations$49 });
- var translations$49;
- var init_pt$22 = __esmMin((() => {
- translations$49 = {
+ var pt_exports$23 = /* @__PURE__ */ __exportAll({ translations: () => translations$51 });
+ var translations$51;
+ var init_pt$23 = __esmMin((() => {
+ translations$51 = {
"⚠️ Anomaly Insight": "⚠️ Informação de anomalia",
"⚠️ Multi-method Anomaly": "⚠️ Anomalia multimétodo",
"Click the highlighted circle to add an annotation.": "Clique no círculo destacado para adicionar uma anotação.",
@@ -5024,10 +5224,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/ha/i18n/pt.ts
- var pt_exports$21 = /* @__PURE__ */ __exportAll({ translations: () => translations$48 });
- var translations$48;
- var init_pt$21 = __esmMin((() => {
- translations$48 = {
+ var pt_exports$22 = /* @__PURE__ */ __exportAll({ translations: () => translations$50 });
+ var translations$50;
+ var init_pt$22 = __esmMin((() => {
+ translations$50 = {
"Confirm delete": "Confirmar eliminação",
"Are you sure you want to delete this item?": "Tem a certeza de que pretende eliminar este item?",
Cancel: "Cancelar",
@@ -5042,10 +5242,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/history-page/i18n/pt.ts
- var pt_exports$20 = /* @__PURE__ */ __exportAll({ translations: () => translations$47 });
- var translations$47;
- var init_pt$20 = __esmMin((() => {
- translations$47 = {
+ var pt_exports$21 = /* @__PURE__ */ __exportAll({ translations: () => translations$49 });
+ var translations$49;
+ var init_pt$21 = __esmMin((() => {
+ translations$49 = {
"entity_id={0}": "entity_id={0}",
"device_id={0}": "device_id={0}",
"area_id={0}": "area_id={0}",
@@ -5143,20 +5343,68 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/timeline/i18n/pt.ts
- var pt_exports$19 = /* @__PURE__ */ __exportAll({ translations: () => translations$46 });
- var translations$46;
- var init_pt$19 = __esmMin((() => {
- translations$46 = {
+ var pt_exports$20 = /* @__PURE__ */ __exportAll({ translations: () => translations$48 });
+ var translations$48;
+ var init_pt$20 = __esmMin((() => {
+ translations$48 = {
Wk: "Sem.",
"Week of": "Semana de"
};
}));
//#endregion
+ //#region custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/pt.ts
+ var pt_exports$19 = /* @__PURE__ */ __exportAll({ translations: () => translations$47 });
+ var translations$47;
+ var init_pt$19 = __esmMin((() => {
+ translations$47 = {
+ "Aggregate series": "Séries agregadas",
+ "Count changes to a state": "Contar mudanças para um estado",
+ "Count all state changes": "Contar todas as mudanças de estado",
+ "Time in a state": "Tempo num estado",
+ "Percentage of time in a state": "Percentagem de tempo num estado",
+ Hourly: "Por hora",
+ Daily: "Diário",
+ Weekly: "Semanal",
+ Monthly: "Mensal",
+ On: "Ligado",
+ Off: "Desligado",
+ Open: "Aberto",
+ Closed: "Fechado",
+ Detected: "Detetado",
+ Clear: "Livre",
+ Occupied: "Ocupado",
+ Unlocked: "Desbloqueado",
+ Locked: "Bloqueado",
+ Mean: "Média",
+ Minimum: "Mínimo",
+ Maximum: "Máximo",
+ Median: "Mediana",
+ "First reading": "Primeira leitura",
+ "Last reading": "Última leitura",
+ "All event types": "Todos os tipos de evento",
+ Consumption: "Consumo",
+ "Meter reset detected": "Reposição do contador detetada",
+ "Ambiguous meter reset, not counted": "Reposição do contador ambígua, não contabilizada",
+ Cumulative: "Acumulado",
+ "Reset total to zoomed range": "Repor o total no intervalo ampliado",
+ "Partial period": "Período parcial",
+ "Incomplete history": "Histórico incompleto",
+ "Dashed outlines mark partial periods cut by the selected range; hatched or dotted marks show periods with gaps in recorded history.": "Os contornos tracejados indicam períodos parciais cortados pelo intervalo selecionado; as marcas tracejadas finas ou pontilhadas indicam períodos com lacunas no histórico registado.",
+ "Counts only include activity in retained history; earlier activity is not shown.": "As contagens só incluem a atividade do histórico retido; a atividade anterior não é mostrada.",
+ "Count events": "Contar eventos",
+ Interval: "Intervalo",
+ State: "Estado",
+ "Every minute": "A cada minuto",
+ "Every 15 minutes": "A cada 15 minutos",
+ "Every 30 minutes": "A cada 30 minutos"
+ };
+ }));
+ //#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-anomaly-group/i18n/pt.ts
- var pt_exports$18 = /* @__PURE__ */ __exportAll({ translations: () => translations$45 });
- var translations$45;
+ var pt_exports$18 = /* @__PURE__ */ __exportAll({ translations: () => translations$46 });
+ var translations$46;
var init_pt$18 = __esmMin((() => {
- translations$45 = {
+ translations$46 = {
Direction: "Direção",
Both: "Ambos",
Above: "Acima",
@@ -5204,10 +5452,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-delta-group/i18n/pt.ts
- var pt_exports$17 = /* @__PURE__ */ __exportAll({ translations: () => translations$44 });
- var translations$44;
+ var pt_exports$17 = /* @__PURE__ */ __exportAll({ translations: () => translations$45 });
+ var translations$45;
var init_pt$17 = __esmMin((() => {
- translations$44 = {
+ translations$45 = {
"Show delta vs selected date window": "Mostrar delta face à janela de datas selecionada",
"Select a date window tab to enable delta analysis.": "Selecione um separador de janela de datas para ativar a análise delta.",
"Show delta in tooltip": "Mostrar delta na dica",
@@ -5216,10 +5464,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-rate-group/i18n/pt.ts
- var pt_exports$16 = /* @__PURE__ */ __exportAll({ translations: () => translations$43 });
- var translations$43;
+ var pt_exports$16 = /* @__PURE__ */ __exportAll({ translations: () => translations$44 });
+ var translations$44;
var init_pt$16 = __esmMin((() => {
- translations$43 = {
+ translations$44 = {
"Show rate of change": "Mostrar taxa de variação",
"Show rate of change crosshairs": "Mostrar guias da taxa de variação",
"Rate window": "Janela da taxa",
@@ -5231,10 +5479,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-sample-group/i18n/pt.ts
- var pt_exports$15 = /* @__PURE__ */ __exportAll({ translations: () => translations$42 });
- var translations$42;
+ var pt_exports$15 = /* @__PURE__ */ __exportAll({ translations: () => translations$43 });
+ var translations$43;
var init_pt$15 = __esmMin((() => {
- translations$42 = {
+ translations$43 = {
Downsampling: "Reamostragem",
Interval: "Intervalo",
Aggregate: "Agregado",
@@ -5266,20 +5514,20 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-summary-group/i18n/pt.ts
- var pt_exports$14 = /* @__PURE__ */ __exportAll({ translations: () => translations$41 });
- var translations$41;
+ var pt_exports$14 = /* @__PURE__ */ __exportAll({ translations: () => translations$42 });
+ var translations$42;
var init_pt$14 = __esmMin((() => {
- translations$41 = {
+ translations$42 = {
"Show min / max / mean": "Mostrar mín. / máx. / média",
"Show range shading": "Mostrar sombreamento do intervalo"
};
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-threshold-group/i18n/pt.ts
- var pt_exports$13 = /* @__PURE__ */ __exportAll({ translations: () => translations$40 });
- var translations$40;
+ var pt_exports$13 = /* @__PURE__ */ __exportAll({ translations: () => translations$41 });
+ var translations$41;
var init_pt$13 = __esmMin((() => {
- translations$40 = {
+ translations$41 = {
"Show threshold analysis": "Mostrar análise de limiar",
"Shade threshold area": "Sombrear área do limiar",
Threshold: "Limiar",
@@ -5290,10 +5538,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-trend-group/i18n/pt.ts
- var pt_exports$12 = /* @__PURE__ */ __exportAll({ translations: () => translations$39 });
- var translations$39;
+ var pt_exports$12 = /* @__PURE__ */ __exportAll({ translations: () => translations$40 });
+ var translations$40;
var init_pt$12 = __esmMin((() => {
- translations$39 = {
+ translations$40 = {
"Show trend lines": "Mostrar linhas de tendência",
"Show trend crosshairs": "Mostrar guias da tendência",
"Trend method": "Método de tendência",
@@ -5311,10 +5559,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/anomaly-monitor-wizard/i18n/pt.ts
- var pt_exports$11 = /* @__PURE__ */ __exportAll({ translations: () => translations$38 });
- var translations$38;
+ var pt_exports$11 = /* @__PURE__ */ __exportAll({ translations: () => translations$39 });
+ var translations$39;
var init_pt$11 = __esmMin((() => {
- translations$38 = {
+ translations$39 = {
"Add all series from chart": "Adicionar todas as séries do gráfico",
"Add from current chart": "Adicionar do gráfico atual",
"Create anomaly monitor": "Criar monitor de anomalias",
@@ -5324,17 +5572,17 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/comparison-tab-rail/i18n/pt.ts
- var pt_exports$10 = /* @__PURE__ */ __exportAll({ translations: () => translations$37 });
- var translations$37;
+ var pt_exports$10 = /* @__PURE__ */ __exportAll({ translations: () => translations$38 });
+ var translations$38;
var init_pt$10 = __esmMin((() => {
- translations$37 = { "Add date window": "Adicionar janela de datas" };
+ translations$38 = { "Add date window": "Adicionar janela de datas" };
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/date-window-dialog/i18n/pt.ts
- var pt_exports$9 = /* @__PURE__ */ __exportAll({ translations: () => translations$36 });
- var translations$36;
+ var pt_exports$9 = /* @__PURE__ */ __exportAll({ translations: () => translations$37 });
+ var translations$37;
var init_pt$9 = __esmMin((() => {
- translations$36 = {
+ translations$37 = {
"A date window saves a named date range as a tab, so you can quickly preview it against the selected range or jump the chart back to it later.": "Uma janela de datas guarda um intervalo de datas com nome como separador, para que possa pré-visualizá-lo rapidamente face ao intervalo selecionado ou regressar mais tarde a ele no gráfico.",
Name: "Nome",
"e.g. Heating season start": "ex.: início da época de aquecimento",
@@ -5351,10 +5599,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/sidebar-options/sections/i18n/pt.ts
- var pt_exports$8 = /* @__PURE__ */ __exportAll({ translations: () => translations$35 });
- var translations$35;
+ var pt_exports$8 = /* @__PURE__ */ __exportAll({ translations: () => translations$36 });
+ var translations$36;
var init_pt$8 = __esmMin((() => {
- translations$35 = {
+ translations$36 = {
Datapoints: "Pontos de dados",
"Choose which annotation datapoints appear on the chart.": "Escolha quais os pontos de dados de anotação que aparecem no gráfico.",
"Linked to selected targets": "Ligados aos alvos selecionados",
@@ -5364,10 +5612,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/target-row/i18n/pt.ts
- var pt_exports$7 = /* @__PURE__ */ __exportAll({ translations: () => translations$34 });
- var translations$34;
+ var pt_exports$7 = /* @__PURE__ */ __exportAll({ translations: () => translations$35 });
+ var translations$35;
var init_pt$7 = __esmMin((() => {
- translations$34 = {
+ translations$35 = {
"Analysis configured": "Análise configurada",
"Configure analysis": "Configurar análise",
"Stepped series": "Série em degraus",
@@ -5379,17 +5627,17 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/molecules/target-row-list/i18n/pt.ts
- var pt_exports$6 = /* @__PURE__ */ __exportAll({ translations: () => translations$33 });
- var translations$33;
+ var pt_exports$6 = /* @__PURE__ */ __exportAll({ translations: () => translations$34 });
+ var translations$34;
var init_pt$6 = __esmMin((() => {
- translations$33 = {};
+ translations$34 = {};
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/components/ai-query-brief-dialog/i18n/pt.ts
- var pt_exports$5 = /* @__PURE__ */ __exportAll({ translations: () => translations$32 });
- var translations$32;
+ var pt_exports$5 = /* @__PURE__ */ __exportAll({ translations: () => translations$33 });
+ var translations$33;
var init_pt$5 = __esmMin((() => {
- translations$32 = {
+ translations$33 = {
"AI query brief": "Resumo de consulta para IA",
"This brief is intended for another AI to fetch raw Home Assistant history and hass_datapoints anomaly details. Review it, then copy it into your AI tool of choice.": "Este resumo foi feito para outra IA obter o historico bruto do Home Assistant e os detalhes de anomalias do hass_datapoints. Revise-o e depois copie-o para a ferramenta de IA de sua escolha.",
"Copy-ready brief": "Resumo pronto para copiar",
@@ -5401,10 +5649,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/components/history-targets/i18n/pt.ts
- var pt_exports$4 = /* @__PURE__ */ __exportAll({ translations: () => translations$31 });
- var translations$31;
+ var pt_exports$4 = /* @__PURE__ */ __exportAll({ translations: () => translations$32 });
+ var translations$32;
var init_pt$4 = __esmMin((() => {
- translations$31 = {
+ translations$32 = {
Targets: "Alvos",
"Each row controls one chart series.": "Cada linha controla uma série do gráfico.",
"Add target": "Adicionar alvo",
@@ -5413,10 +5661,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/components/panel-shell/i18n/pt.ts
- var pt_exports$3 = /* @__PURE__ */ __exportAll({ translations: () => translations$30 });
- var translations$30;
+ var pt_exports$3 = /* @__PURE__ */ __exportAll({ translations: () => translations$31 });
+ var translations$31;
var init_pt$3 = __esmMin((() => {
- translations$30 = {
+ translations$31 = {
"Loading Datapoints…": "A carregar pontos de dados…",
Datapoints: "Pontos de dados",
"Page options": "Opções da página",
@@ -5431,10 +5679,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/components/range-toolbar/i18n/pt.ts
- var pt_exports$2 = /* @__PURE__ */ __exportAll({ translations: () => translations$29 });
- var translations$29;
+ var pt_exports$2 = /* @__PURE__ */ __exportAll({ translations: () => translations$30 });
+ var translations$30;
var init_pt$2 = __esmMin((() => {
- translations$29 = {
+ translations$30 = {
"Toggle sidebar": "Alternar barra lateral",
Start: "Início",
End: "Fim",
@@ -5459,16 +5707,17 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/panels/datapoints/i18n/pt.ts
- var pt_exports$1 = /* @__PURE__ */ __exportAll({ translations: () => translations$28 });
- var translations$28;
+ var pt_exports$1 = /* @__PURE__ */ __exportAll({ translations: () => translations$29 });
+ var translations$29;
var init_pt$1 = __esmMin((() => {
- translations$28 = { "Selected range": "Intervalo selecionado" };
+ translations$29 = { "Selected range": "Intervalo selecionado" };
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/i18n/locales/pt.ts
var pt_exports = /* @__PURE__ */ __exportAll({ templates: () => templates$1 });
var modules$1, merged$1, templates$1;
var init_pt = __esmMin((() => {
+ init_pt$29();
init_pt$28();
init_pt$27();
init_pt$26();
@@ -5498,16 +5747,17 @@
init_pt$2();
init_pt$1();
modules$1 = /* #__PURE__ */ Object.assign({
- "../../../cards/action/i18n/pt.ts": pt_exports$28,
- "../../../cards/history/history-chart/i18n/pt.ts": pt_exports$27,
- "../../../cards/history/i18n/pt.ts": pt_exports$26,
- "../../../cards/list/i18n/pt.ts": pt_exports$25,
- "../../../cards/quick/i18n/pt.ts": pt_exports$24,
- "../../../cards/sensor/i18n/pt.ts": pt_exports$23,
- "../../chart/i18n/pt.ts": pt_exports$22,
- "../../ha/i18n/pt.ts": pt_exports$21,
- "../../history-page/i18n/pt.ts": pt_exports$20,
- "../../timeline/i18n/pt.ts": pt_exports$19,
+ "../../../cards/action/i18n/pt.ts": pt_exports$29,
+ "../../../cards/history/history-chart/i18n/pt.ts": pt_exports$28,
+ "../../../cards/history/i18n/pt.ts": pt_exports$27,
+ "../../../cards/list/i18n/pt.ts": pt_exports$26,
+ "../../../cards/quick/i18n/pt.ts": pt_exports$25,
+ "../../../cards/sensor/i18n/pt.ts": pt_exports$24,
+ "../../chart/i18n/pt.ts": pt_exports$23,
+ "../../ha/i18n/pt.ts": pt_exports$22,
+ "../../history-page/i18n/pt.ts": pt_exports$21,
+ "../../timeline/i18n/pt.ts": pt_exports$20,
+ "../../../molecules/analysis-aggregate-group/i18n/pt.ts": pt_exports$19,
"../../../molecules/analysis-anomaly-group/i18n/pt.ts": pt_exports$18,
"../../../molecules/analysis-delta-group/i18n/pt.ts": pt_exports$17,
"../../../molecules/analysis-rate-group/i18n/pt.ts": pt_exports$16,
@@ -5533,10 +5783,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/action/i18n/zh-hans.ts
- var zh_hans_exports$28 = /* @__PURE__ */ __exportAll({ translations: () => translations$27 });
- var translations$27;
- var init_zh_hans$28 = __esmMin((() => {
- translations$27 = {
+ var zh_hans_exports$29 = /* @__PURE__ */ __exportAll({ translations: () => translations$28 });
+ var translations$28;
+ var init_zh_hans$29 = __esmMin((() => {
+ translations$28 = {
General: "常规",
"Related items": "关联项",
"Datapoint Appearance": "数据点外观",
@@ -5553,20 +5803,20 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/history/history-chart/i18n/zh-hans.ts
- var zh_hans_exports$27 = /* @__PURE__ */ __exportAll({ translations: () => translations$26 });
- var translations$26;
- var init_zh_hans$27 = __esmMin((() => {
- translations$26 = {
+ var zh_hans_exports$28 = /* @__PURE__ */ __exportAll({ translations: () => translations$27 });
+ var translations$27;
+ var init_zh_hans$28 = __esmMin((() => {
+ translations$27 = {
"Date window:": "日期窗口:",
"Actual:": "实际:"
};
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/history/i18n/zh-hans.ts
- var zh_hans_exports$26 = /* @__PURE__ */ __exportAll({ translations: () => translations$25 });
- var translations$25;
- var init_zh_hans$26 = __esmMin((() => {
- translations$25 = {
+ var zh_hans_exports$27 = /* @__PURE__ */ __exportAll({ translations: () => translations$26 });
+ var translations$26;
+ var init_zh_hans$27 = __esmMin((() => {
+ translations$26 = {
General: "常规",
Entity: "实体",
"Multiple entities": "多个实体",
@@ -5583,10 +5833,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/list/i18n/zh-hans.ts
- var zh_hans_exports$25 = /* @__PURE__ */ __exportAll({ translations: () => translations$24 });
- var translations$24;
- var init_zh_hans$25 = __esmMin((() => {
- translations$24 = {
+ var zh_hans_exports$26 = /* @__PURE__ */ __exportAll({ translations: () => translations$25 });
+ var translations$25;
+ var init_zh_hans$26 = __esmMin((() => {
+ translations$25 = {
"Search datapoints…": "搜索数据点…",
"Delete record": "删除记录",
Delete: "删除",
@@ -5604,10 +5854,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/quick/i18n/zh-hans.ts
- var zh_hans_exports$24 = /* @__PURE__ */ __exportAll({ translations: () => translations$23 });
- var translations$23;
- var init_zh_hans$24 = __esmMin((() => {
- translations$23 = {
+ var zh_hans_exports$25 = /* @__PURE__ */ __exportAll({ translations: () => translations$24 });
+ var translations$24;
+ var init_zh_hans$25 = __esmMin((() => {
+ translations$24 = {
General: "常规",
"Icon & colour": "图标和颜色",
"Related items": "关联项",
@@ -5625,10 +5875,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/cards/sensor/i18n/zh-hans.ts
- var zh_hans_exports$23 = /* @__PURE__ */ __exportAll({ translations: () => translations$22 });
- var translations$22;
- var init_zh_hans$23 = __esmMin((() => {
- translations$22 = {
+ var zh_hans_exports$24 = /* @__PURE__ */ __exportAll({ translations: () => translations$23 });
+ var translations$23;
+ var init_zh_hans$24 = __esmMin((() => {
+ translations$23 = {
Entity: "实体",
Display: "显示",
"Records list": "记录列表",
@@ -5648,10 +5898,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/chart/i18n/zh-hans.ts
- var zh_hans_exports$22 = /* @__PURE__ */ __exportAll({ translations: () => translations$21 });
- var translations$21;
- var init_zh_hans$22 = __esmMin((() => {
- translations$21 = {
+ var zh_hans_exports$23 = /* @__PURE__ */ __exportAll({ translations: () => translations$22 });
+ var translations$22;
+ var init_zh_hans$23 = __esmMin((() => {
+ translations$22 = {
"⚠️ Anomaly Insight": "⚠️ 异常洞察",
"⚠️ Multi-method Anomaly": "⚠️ 多方法异常",
"Click the highlighted circle to add an annotation.": "点击高亮圆圈以添加注释。",
@@ -5686,10 +5936,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/ha/i18n/zh-hans.ts
- var zh_hans_exports$21 = /* @__PURE__ */ __exportAll({ translations: () => translations$20 });
- var translations$20;
- var init_zh_hans$21 = __esmMin((() => {
- translations$20 = {
+ var zh_hans_exports$22 = /* @__PURE__ */ __exportAll({ translations: () => translations$21 });
+ var translations$21;
+ var init_zh_hans$22 = __esmMin((() => {
+ translations$21 = {
"Confirm delete": "确认删除",
"Are you sure you want to delete this item?": "确定要删除此项目吗?",
Cancel: "取消",
@@ -5704,10 +5954,10 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/history-page/i18n/zh-hans.ts
- var zh_hans_exports$20 = /* @__PURE__ */ __exportAll({ translations: () => translations$19 });
- var translations$19;
- var init_zh_hans$20 = __esmMin((() => {
- translations$19 = {
+ var zh_hans_exports$21 = /* @__PURE__ */ __exportAll({ translations: () => translations$20 });
+ var translations$20;
+ var init_zh_hans$21 = __esmMin((() => {
+ translations$20 = {
"entity_id={0}": "entity_id={0}",
"device_id={0}": "device_id={0}",
"area_id={0}": "area_id={0}",
@@ -5805,12 +6055,60 @@
}));
//#endregion
//#region custom_components/hass_datapoints/src/lib/timeline/i18n/zh-hans.ts
+ var zh_hans_exports$20 = /* @__PURE__ */ __exportAll({ translations: () => translations$19 });
+ var translations$19;
+ var init_zh_hans$20 = __esmMin((() => {
+ translations$19 = {
+ Wk: "周",
+ "Week of": "所在周"
+ };
+ }));
+ //#endregion
+ //#region custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/zh-hans.ts
var zh_hans_exports$19 = /* @__PURE__ */ __exportAll({ translations: () => translations$18 });
var translations$18;
var init_zh_hans$19 = __esmMin((() => {
translations$18 = {
- Wk: "周",
- "Week of": "所在周"
+ "Aggregate series": "聚合序列",
+ "Count changes to a state": "统计变为某状态的次数",
+ "Count all state changes": "统计所有状态变化次数",
+ "Time in a state": "处于某状态的时长",
+ "Percentage of time in a state": "处于某状态的时间百分比",
+ Hourly: "每小时",
+ Daily: "每天",
+ Weekly: "每周",
+ Monthly: "每月",
+ On: "开",
+ Off: "关",
+ Open: "打开",
+ Closed: "关闭",
+ Detected: "已检测到",
+ Clear: "无人",
+ Occupied: "有人",
+ Unlocked: "已解锁",
+ Locked: "已锁定",
+ Mean: "平均值",
+ Minimum: "最小值",
+ Maximum: "最大值",
+ Median: "中位数",
+ "First reading": "首次读数",
+ "Last reading": "最后读数",
+ "All event types": "所有事件类型",
+ Consumption: "用量",
+ "Meter reset detected": "检测到电表重置",
+ "Ambiguous meter reset, not counted": "重置情况不明确,未计入",
+ Cumulative: "累计",
+ "Reset total to zoomed range": "将累计值重置为缩放范围",
+ "Partial period": "部分时段",
+ "Incomplete history": "历史记录不完整",
+ "Dashed outlines mark partial periods cut by the selected range; hatched or dotted marks show periods with gaps in recorded history.": "虚线轮廓表示被所选范围截断的部分时段;阴影或点状标记表示已记录历史中存在缺口的时段。",
+ "Counts only include activity in retained history; earlier activity is not shown.": "计数仅包含保留历史中的活动;更早的活动不会显示。",
+ "Count events": "统计事件数",
+ Interval: "间隔",
+ State: "状态",
+ "Every minute": "每分钟",
+ "Every 15 minutes": "每 15 分钟",
+ "Every 30 minutes": "每 30 分钟"
};
}));
//#endregion
@@ -6131,6 +6429,7 @@
var zh_hans_exports = /* @__PURE__ */ __exportAll({ templates: () => templates });
var modules, merged, templates;
var init_zh_hans = __esmMin((() => {
+ init_zh_hans$29();
init_zh_hans$28();
init_zh_hans$27();
init_zh_hans$26();
@@ -6160,16 +6459,17 @@
init_zh_hans$2();
init_zh_hans$1();
modules = /* #__PURE__ */ Object.assign({
- "../../../cards/action/i18n/zh-hans.ts": zh_hans_exports$28,
- "../../../cards/history/history-chart/i18n/zh-hans.ts": zh_hans_exports$27,
- "../../../cards/history/i18n/zh-hans.ts": zh_hans_exports$26,
- "../../../cards/list/i18n/zh-hans.ts": zh_hans_exports$25,
- "../../../cards/quick/i18n/zh-hans.ts": zh_hans_exports$24,
- "../../../cards/sensor/i18n/zh-hans.ts": zh_hans_exports$23,
- "../../chart/i18n/zh-hans.ts": zh_hans_exports$22,
- "../../ha/i18n/zh-hans.ts": zh_hans_exports$21,
- "../../history-page/i18n/zh-hans.ts": zh_hans_exports$20,
- "../../timeline/i18n/zh-hans.ts": zh_hans_exports$19,
+ "../../../cards/action/i18n/zh-hans.ts": zh_hans_exports$29,
+ "../../../cards/history/history-chart/i18n/zh-hans.ts": zh_hans_exports$28,
+ "../../../cards/history/i18n/zh-hans.ts": zh_hans_exports$27,
+ "../../../cards/list/i18n/zh-hans.ts": zh_hans_exports$26,
+ "../../../cards/quick/i18n/zh-hans.ts": zh_hans_exports$25,
+ "../../../cards/sensor/i18n/zh-hans.ts": zh_hans_exports$24,
+ "../../chart/i18n/zh-hans.ts": zh_hans_exports$23,
+ "../../ha/i18n/zh-hans.ts": zh_hans_exports$22,
+ "../../history-page/i18n/zh-hans.ts": zh_hans_exports$21,
+ "../../timeline/i18n/zh-hans.ts": zh_hans_exports$20,
+ "../../../molecules/analysis-aggregate-group/i18n/zh-hans.ts": zh_hans_exports$19,
"../../../molecules/analysis-anomaly-group/i18n/zh-hans.ts": zh_hans_exports$18,
"../../../molecules/analysis-delta-group/i18n/zh-hans.ts": zh_hans_exports$17,
"../../../molecules/analysis-rate-group/i18n/zh-hans.ts": zh_hans_exports$16,
@@ -6240,7 +6540,7 @@
}
//#endregion
//#region custom_components/hass_datapoints/src/cards/action/editor.styles.ts
- var styles$72 = i$5`
+ var styles$73 = i$5`
.note {
font-size: 0.78rem;
color: var(--secondary-text-color);
@@ -6248,7 +6548,7 @@
`;
//#endregion
//#region custom_components/hass_datapoints/src/molecules/editor-base/editor-base.styles.ts
- var styles$71 = i$5`
+ var styles$72 = i$5`
:host {
display: block;
}
@@ -6299,7 +6599,7 @@
this._config = cfg;
this._fire(cfg);
}
- }, _defineProperty(_EditorBase, "styles", styles$71), _EditorBase);
+ }, _defineProperty(_EditorBase, "styles", styles$72), _EditorBase);
__decorate([r$2()], EditorBase.prototype, "_config", null);
__decorate([n$1({ type: Object })], EditorBase.prototype, "hass", null);
EditorBase = __decorate([localized()], EditorBase);
@@ -6377,7 +6677,7 @@
}
//#endregion
//#region custom_components/hass_datapoints/src/atoms/display/section-heading/section-heading.styles.ts
- var styles$70 = i$5`
+ var styles$71 = i$5`
:host {
display: block;
}
@@ -6407,12 +6707,12 @@
return b`
${this.text}
`;
}
};
- _defineProperty(SectionHeading, "styles", styles$70);
+ _defineProperty(SectionHeading, "styles", styles$71);
__decorate([n$1({ type: String })], SectionHeading.prototype, "text", null);
customElements.define("section-heading", SectionHeading);
//#endregion
//#region custom_components/hass_datapoints/src/atoms/form/editor-text-field/editor-text-field.styles.ts
- var styles$69 = i$5`
+ var styles$70 = i$5`
:host {
display: block;
}
@@ -6489,7 +6789,7 @@
>`;
}
};
- _defineProperty(EditorTextField, "styles", styles$69);
+ _defineProperty(EditorTextField, "styles", styles$70);
__decorate([n$1({ type: String })], EditorTextField.prototype, "label", null);
__decorate([n$1({ type: String })], EditorTextField.prototype, "value", null);
__decorate([n$1({ type: String })], EditorTextField.prototype, "type", null);
@@ -6498,7 +6798,7 @@
customElements.define("editor-text-field", EditorTextField);
//#endregion
//#region custom_components/hass_datapoints/src/atoms/form/editor-switch/editor-switch.styles.ts
- var styles$68 = i$5`
+ var styles$69 = i$5`
:host {
display: block;
}
@@ -6596,14 +6896,14 @@
`;
}
};
- _defineProperty(EditorSwitch, "styles", styles$68);
+ _defineProperty(EditorSwitch, "styles", styles$69);
__decorate([n$1({ type: String })], EditorSwitch.prototype, "label", null);
__decorate([n$1({ type: Boolean })], EditorSwitch.prototype, "checked", null);
__decorate([n$1({ type: String })], EditorSwitch.prototype, "tooltip", null);
customElements.define("editor-switch", EditorSwitch);
//#endregion
//#region custom_components/hass_datapoints/src/atoms/form/editor-icon-picker/editor-icon-picker.styles.ts
- var styles$67 = i$5`
+ var styles$68 = i$5`
:host {
display: block;
}
@@ -6661,7 +6961,7 @@
>`;
}
};
- _defineProperty(EditorIconPicker, "styles", styles$67);
+ _defineProperty(EditorIconPicker, "styles", styles$68);
__decorate([n$1({ type: String })], EditorIconPicker.prototype, "label", null);
__decorate([n$1({ type: String })], EditorIconPicker.prototype, "value", null);
__decorate([n$1({ type: Object })], EditorIconPicker.prototype, "hass", null);
@@ -6778,7 +7078,7 @@
`;
}
};
- _defineProperty(HassDatapointsActionCardEditor, "styles", [EditorBase.styles, styles$72]);
+ _defineProperty(HassDatapointsActionCardEditor, "styles", [EditorBase.styles, styles$73]);
__decorate([e$4("#target-picker")], HassDatapointsActionCardEditor.prototype, "_targetPickerEl", null);
__decorate([r$1("ha-selector")], HassDatapointsActionCardEditor.prototype, "_selectorEls", null);
//#endregion
@@ -7022,7 +7322,7 @@
}
//#endregion
//#region custom_components/hass_datapoints/src/cards/dev-tool/dev-tool.styles.ts
- var styles$66 = i$5`
+ var styles$67 = i$5`
:host {
display: block;
}
@@ -7097,7 +7397,7 @@
_defineProperty(HassDatapointsDevToolCardEditor, "styles", [EditorBase.styles]);
//#endregion
//#region custom_components/hass_datapoints/src/cards/dev-tool/dev-tool-results/dev-tool-results.styles.ts
- var styles$65 = i$5`
+ var styles$66 = i$5`
:host {
display: block;
margin-top: 18px;
@@ -7464,7 +7764,7 @@
`;
}
};
- _defineProperty(CardDevToolResults, "styles", styles$65);
+ _defineProperty(CardDevToolResults, "styles", styles$66);
__decorate([n$1({ attribute: false })], CardDevToolResults.prototype, "results", null);
__decorate([n$1({ type: Boolean })], CardDevToolResults.prototype, "isAdmin", null);
__decorate([n$1({ type: String })], CardDevToolResults.prototype, "statusKind", null);
@@ -7474,7 +7774,7 @@
customElements.define("dev-tool-results", CardDevToolResults);
//#endregion
//#region custom_components/hass_datapoints/src/cards/dev-tool/dev-tool-windows/dev-tool-windows.styles.ts
- var styles$64 = i$5`
+ var styles$65 = i$5`
:host {
display: block;
}
@@ -7717,7 +8017,7 @@
`;
}
};
- _defineProperty(CardDevToolWindows, "styles", styles$64);
+ _defineProperty(CardDevToolWindows, "styles", styles$65);
__decorate([n$1({ attribute: false })], CardDevToolWindows.prototype, "windows", null);
__decorate([r$2()], CardDevToolWindows.prototype, "_nextWindowId", null);
customElements.define("dev-tool-windows", CardDevToolWindows);
@@ -8228,7 +8528,7 @@
return document.createElement("hass-datapoints-dev-tool-card-editor");
}
};
- _defineProperty(HassDatapointsDevToolCard, "styles", styles$66);
+ _defineProperty(HassDatapointsDevToolCard, "styles", styles$67);
__decorate([r$2()], HassDatapointsDevToolCard.prototype, "_config", null);
__decorate([r$2()], HassDatapointsDevToolCard.prototype, "_hass", null);
__decorate([r$2()], HassDatapointsDevToolCard.prototype, "_entities", null);
@@ -8395,58 +8695,946 @@
this._resizeObserver = null;
}
}
- static getStubConfig() {
- return { title: "" };
+ static getStubConfig() {
+ return { title: "" };
+ }
+ };
+ //#endregion
+ //#region custom_components/hass_datapoints/src/cards/history/history.styles.ts
+ var styles$64 = i$5`
+ :host {
+ display: block;
+ height: 100%;
+ }
+
+ ha-card {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ }
+
+ .card-header {
+ margin: 0;
+ padding: 12px 16px 0;
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 48px;
+ color: var(--primary-text-color);
+ flex: 0 0 auto;
+ display: flex;
+ justify-content: space-between;
+ gap: 8px;
+ }
+
+ .card-header-title {
+ min-width: 0;
+ font: inherit;
+ line-height: inherit;
+ font-weight: inherit;
+ }
+
+ .card-header-action {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex: 0 0 auto;
+ align-self: center;
+ }
+
+ hass-datapoints-history-chart {
+ flex: 1 1 auto;
+ min-height: 0;
+ }
+`;
+ //#endregion
+ //#region custom_components/hass_datapoints/src/lib/domain/aggregation/calendar.ts
+ var HOUR_MS$2 = 36e5;
+ var MINUTE_MS$2 = 6e4;
+ /** Sub-day intervals of fixed length, aligned to the local wall clock. */
+ var FIXED_INTERVAL_MS = {
+ minute: MINUTE_MS$2,
+ "15min": 15 * MINUTE_MS$2,
+ "30min": 30 * MINUTE_MS$2,
+ hour: HOUR_MS$2
+ };
+ var formatters = /* @__PURE__ */ new Map();
+ function getFormatter(timeZone) {
+ let formatter = formatters.get(timeZone);
+ if (!formatter) {
+ formatter = new Intl.DateTimeFormat("en-US", {
+ timeZone,
+ hourCycle: "h23",
+ year: "numeric",
+ month: "numeric",
+ day: "numeric",
+ hour: "numeric",
+ minute: "numeric",
+ second: "numeric"
+ });
+ formatters.set(timeZone, formatter);
+ }
+ return formatter;
+ }
+ function getLocalParts(ts, timeZone) {
+ const values = {};
+ for (const part of getFormatter(timeZone).formatToParts(ts)) if (part.type !== "literal") values[part.type] = Number(part.value);
+ return {
+ year: values.year,
+ month: values.month,
+ day: values.day,
+ minute: values.minute,
+ second: values.second,
+ weekday: new Date(Date.UTC(values.year, values.month - 1, values.day)).getUTCDay()
+ };
+ }
+ function getOffsetMs(ts, timeZone) {
+ const whole = Math.floor(ts / 1e3) * 1e3;
+ const values = {};
+ for (const part of getFormatter(timeZone).formatToParts(whole)) if (part.type !== "literal") values[part.type] = Number(part.value);
+ return Date.UTC(values.year, values.month - 1, values.day, values.hour, values.minute, values.second) - whole;
+ }
+ /** Local midnight (in `timeZone`) of a possibly un-normalised calendar date. */
+ function localMidnight(year, month, day, timeZone) {
+ const wall = Date.UTC(year, month - 1, day);
+ return wall - getOffsetMs(wall - getOffsetMs(wall, timeZone), timeZone);
+ }
+ function getPeriodStart(ts, interval, timeZone) {
+ const p = getLocalParts(ts, timeZone);
+ const fixedMs = FIXED_INTERVAL_MS[interval];
+ if (fixedMs) {
+ const ms = (ts % 1e3 + 1e3) % 1e3;
+ const offsetMinutes = p.minute % (fixedMs / MINUTE_MS$2);
+ return ts - ms - p.second * 1e3 - offsetMinutes * MINUTE_MS$2;
+ }
+ if (interval === "day") return localMidnight(p.year, p.month, p.day, timeZone);
+ if (interval === "week") return localMidnight(p.year, p.month, p.day - (p.weekday + 6) % 7, timeZone);
+ return localMidnight(p.year, p.month, 1, timeZone);
+ }
+ function getNextPeriodStart(periodStart, interval, timeZone) {
+ const fixedMs = FIXED_INTERVAL_MS[interval];
+ if (fixedMs) return periodStart + fixedMs;
+ const p = getLocalParts(periodStart + HOUR_MS$2 * 2, timeZone);
+ if (interval === "day") return localMidnight(p.year, p.month, p.day + 1, timeZone);
+ if (interval === "week") return localMidnight(p.year, p.month, p.day + 7, timeZone);
+ return localMidnight(p.year, p.month + 1, 1, timeZone);
+ }
+ /**
+ * Calendar periods overlapping `range`, clipped to it. Periods cut by the
+ * range edge are flagged partial; the full period bounds are preserved.
+ */
+ function getPeriods(range, interval, timeZone) {
+ const periods = [];
+ if (!(range.end > range.start)) return periods;
+ let periodStart = getPeriodStart(range.start, interval, timeZone);
+ while (periodStart < range.end) {
+ const periodEnd = getNextPeriodStart(periodStart, interval, timeZone);
+ const start = Math.max(periodStart, range.start);
+ const end = Math.min(periodEnd, range.end);
+ periods.push({
+ periodStart,
+ periodEnd,
+ start,
+ end,
+ partial: start !== periodStart || end !== periodEnd
+ });
+ periodStart = periodEnd;
+ }
+ return periods;
+ }
+ //#endregion
+ //#region custom_components/hass_datapoints/src/lib/domain/aggregation/calculate.ts
+ var MINUTE_MS$1 = 6e4;
+ var UNKNOWN_STATES$1 = /* @__PURE__ */ new Set(["unknown", "unavailable"]);
+ var CUMULATIVE_OPERATIONS$1 = /* @__PURE__ */ new Set([
+ "consumption",
+ "transition_count",
+ "state_duration",
+ "event_count"
+ ]);
+ function isKnownState(state) {
+ return typeof state === "string" && !UNKNOWN_STATES$1.has(state);
+ }
+ function overlap(a, b) {
+ return Math.max(0, Math.min(a.end, b.end) - Math.max(a.start, b.start));
+ }
+ /**
+ * Bucket time-ordered items into periods in one sweep. Minute-level intervals
+ * over long ranges create tens of thousands of periods, so per-period filters
+ * over every item would be quadratic.
+ */
+ function groupByPeriod(items, periods, timeOf) {
+ const sorted = [...items].sort((a, b) => timeOf(a) - timeOf(b));
+ const groups = periods.map(() => []);
+ let cursor = 0;
+ sorted.forEach((item) => {
+ const time = timeOf(item);
+ while (cursor < periods.length && time >= periods[cursor].end) cursor += 1;
+ if (cursor < periods.length && time >= periods[cursor].start) groups[cursor].push(item);
+ });
+ return groups;
+ }
+ function sortSamples(samples) {
+ return [...samples].sort((a, b) => a.time - b.time);
+ }
+ /** Known (covered) time within each period, in one sweep over the coverage. */
+ function knownDurationsOf(periods, coverage) {
+ if (!coverage) return periods.map((period) => period.end - period.start);
+ const sorted = [...coverage].sort((a, b) => a.start - b.start);
+ let first = 0;
+ return periods.map((period) => {
+ while (first < sorted.length && sorted[first].end <= period.start) first += 1;
+ let known = 0;
+ for (let i = first; i < sorted.length && sorted[i].start < period.end; i += 1) known += overlap(period, sorted[i]);
+ return known;
+ });
+ }
+ /** Known-state spans over `range`, seeded by the last sample at/before start. */
+ function buildStateSpans(samples, range, coverage) {
+ const stateful = sortSamples(samples).filter((s) => typeof s.state === "string");
+ const points = [];
+ let seed;
+ for (const s of stateful) if (s.time <= range.start) seed = {
+ time: range.start,
+ state: s.state
+ };
+ else if (s.time < range.end) points.push({
+ time: s.time,
+ state: s.state
+ });
+ if (seed) points.unshift(seed);
+ const spans = [];
+ points.forEach((point, i) => {
+ if (!isKnownState(point.state)) return;
+ const end = i + 1 < points.length ? points[i + 1].time : range.end;
+ const span = {
+ start: point.time,
+ end,
+ state: point.state
+ };
+ if (!coverage) {
+ spans.push(span);
+ return;
+ }
+ for (const c of coverage) {
+ const start = Math.max(span.start, c.start);
+ const clippedEnd = Math.min(span.end, c.end);
+ if (clippedEnd > start) spans.push({
+ start,
+ end: clippedEnd,
+ state: span.state
+ });
+ }
+ });
+ return spans;
+ }
+ function median(values) {
+ const sorted = [...values].sort((a, b) => a - b);
+ const mid = Math.floor(sorted.length / 2);
+ return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
+ }
+ function calculateMeasurement(input, periods) {
+ const grouped = groupByPeriod(sortSamples(input.samples).filter((s) => !s.boundary && typeof s.value === "number" && Number.isFinite(s.value)), periods, (s) => s.time);
+ return periods.map((_period, index) => {
+ const values = grouped[index].map((s) => s.value);
+ if (!values.length) return {
+ value: null,
+ reasons: ["no-data"]
+ };
+ const op = input.definition.operation;
+ let value;
+ if (op === "min") value = Math.min(...values);
+ else if (op === "max") value = Math.max(...values);
+ else if (op === "median") value = median(values);
+ else if (op === "first") value = values[0];
+ else if (op === "last") value = values[values.length - 1];
+ else value = values.reduce((sum, v) => sum + v, 0) / values.length;
+ return {
+ value,
+ reasons: []
+ };
+ });
+ }
+ function calculateTransitions(input, periods) {
+ const selected = input.definition.states ?? [];
+ const stateful = sortSamples(input.samples).filter((s) => typeof s.state === "string");
+ const times = [];
+ stateful.forEach((curr, i) => {
+ const prev = stateful[i - 1];
+ if (!prev || curr.time <= input.range.start || curr.time >= input.range.end || !isKnownState(prev.state) || !isKnownState(curr.state) || prev.state === curr.state || selected.length > 0 && !selected.includes(curr.state)) return;
+ times.push(curr.time);
+ });
+ const grouped = groupByPeriod(times, periods, (t) => t);
+ return periods.map((_period, index) => ({
+ value: grouped[index].length,
+ reasons: []
+ }));
+ }
+ function calculateDurations(input, periods, percentage) {
+ const selected = input.definition.states ?? [];
+ if (!selected.length) return periods.map(() => ({
+ value: null,
+ reasons: ["no-state-selected"]
+ }));
+ const spans = buildStateSpans(input.samples, input.range, input.coverage).sort((a, b) => a.start - b.start);
+ let first = 0;
+ return periods.map((period) => {
+ let known = 0;
+ let matching = 0;
+ while (first < spans.length && spans[first].end <= period.start) first += 1;
+ for (let i = first; i < spans.length && spans[i].start < period.end; i += 1) {
+ const ms = overlap(period, spans[i]);
+ known += ms;
+ if (selected.includes(spans[i].state)) matching += ms;
+ }
+ if (percentage) return known > 0 ? {
+ value: matching / known * 100,
+ reasons: []
+ } : {
+ value: null,
+ reasons: ["no-known-time"]
+ };
+ return known > 0 ? {
+ value: matching / MINUTE_MS$1,
+ reasons: []
+ } : {
+ value: null,
+ reasons: ["no-known-time"]
+ };
+ });
+ }
+ function calculateEvents(input, periods) {
+ const types = input.definition.eventTypes ?? [];
+ const grouped = groupByPeriod(input.samples.filter((s) => !s.boundary && typeof s.eventType === "string" && (types.length === 0 || types.includes(s.eventType))).map((s) => s.eventTime ?? s.time), periods, (t) => t);
+ return periods.map((_period, index) => ({
+ value: grouped[index].length,
+ reasons: []
+ }));
+ }
+ function calculateConsumption(input, periods) {
+ const deltas = [];
+ for (const s of input.samples) if (typeof s.change === "number" && Number.isFinite(s.change) && s.time >= input.range.start && s.time < input.range.end) deltas.push({
+ time: s.time,
+ value: s.change
+ });
+ const readings = sortSamples(input.samples).filter((s) => {
+ const v = s.sum ?? s.value;
+ return s.change === void 0 && typeof v === "number" && Number.isFinite(v);
+ });
+ readings.forEach((curr, i) => {
+ const prev = readings[i - 1];
+ if (!prev || curr.time <= input.range.start || curr.time >= input.range.end) return;
+ const usesSum = typeof curr.sum === "number" && typeof prev.sum === "number";
+ const currValue = usesSum ? curr.sum : curr.value;
+ const delta = currValue - (usesSum ? prev.sum : prev.value);
+ if (usesSum || delta >= 0) deltas.push({
+ time: curr.time,
+ value: delta
+ });
+ else if (input.stateClass === "total_increasing" || curr.lastReset !== void 0 && curr.lastReset !== prev.lastReset) deltas.push({
+ time: curr.time,
+ value: currValue,
+ reason: "reset"
+ });
+ else if (input.stateClass === "total") deltas.push({
+ time: curr.time,
+ value: delta
+ });
+ else deltas.push({
+ time: curr.time,
+ value: 0,
+ reason: "ambiguous-reset"
+ });
+ });
+ const grouped = groupByPeriod(deltas, periods, (d) => d.time);
+ return periods.map((_period, index) => {
+ const inside = grouped[index];
+ if (!inside.length) return {
+ value: null,
+ reasons: ["no-data"]
+ };
+ return {
+ value: inside.reduce((sum, d) => sum + d.value, 0),
+ reasons: [...new Set(inside.flatMap((d) => d.reason ? [d.reason] : []))]
+ };
+ });
+ }
+ function toCumulative(operation, periods, buckets) {
+ const startsAtZeroActivity = operation !== "consumption";
+ let running = 0;
+ let started = false;
+ const keptBuckets = [];
+ const keptPeriods = [];
+ buckets.forEach((bucket, i) => {
+ if (!started) {
+ started = bucket.value !== null && (!startsAtZeroActivity || bucket.value > 0);
+ if (!started) return;
+ }
+ running += bucket.value ?? 0;
+ keptBuckets.push({
+ value: running,
+ reasons: bucket.reasons
+ });
+ keptPeriods.push(periods[i]);
+ });
+ return {
+ buckets: keptBuckets,
+ periods: keptPeriods,
+ started
+ };
+ }
+ function calculateAggregate(input) {
+ const { definition } = input;
+ const cumulative = definition.mode === "cumulative" && CUMULATIVE_OPERATIONS$1.has(definition.operation);
+ if (cumulative && definition.resetOnZoom && input.zoom) {
+ const start = Math.max(input.range.start, input.zoom.start);
+ const end = Math.min(input.range.end, input.zoom.end);
+ if (end > start) return calculateAggregate({
+ ...input,
+ zoom: void 0,
+ range: {
+ start,
+ end
+ }
+ });
+ }
+ let periods = getPeriods(input.range, definition.interval, input.timeZone);
+ const op = definition.operation;
+ let buckets;
+ let unit = input.unit ?? "";
+ if (op === "transition_count") {
+ buckets = calculateTransitions(input, periods);
+ unit = "";
+ } else if (op === "state_duration") {
+ buckets = calculateDurations(input, periods, false);
+ unit = "min";
+ } else if (op === "state_percentage") {
+ buckets = calculateDurations(input, periods, true);
+ unit = "%";
+ } else if (op === "event_count") {
+ buckets = calculateEvents(input, periods);
+ unit = "";
+ } else if (op === "consumption") buckets = calculateConsumption(input, periods);
+ else buckets = calculateMeasurement(input, periods);
+ let started = true;
+ if (cumulative) {
+ const result = toCumulative(op, periods, buckets);
+ ({buckets, periods, started} = result);
+ }
+ const knownDurations = knownDurationsOf(periods, input.coverage);
+ const points = periods.map((period, i) => {
+ const totalDuration = period.end - period.start;
+ const knownDuration = knownDurations[i];
+ const reasons = [...buckets[i].reasons];
+ const incomplete = knownDuration < totalDuration;
+ if (incomplete) reasons.push("incomplete-coverage");
+ return {
+ ...period,
+ time: period.start,
+ value: knownDuration === 0 ? null : buckets[i].value,
+ incomplete: incomplete || reasons.includes("no-data"),
+ knownDuration,
+ totalDuration,
+ reasons
+ };
+ });
+ const isBar = !cumulative && ![
+ "mean",
+ "min",
+ "max",
+ "median",
+ "first",
+ "last"
+ ].includes(op);
+ return {
+ points,
+ unit,
+ presentation: isBar ? "bar" : "line",
+ baseline: cumulative && started && points.length ? 0 : null
+ };
+ }
+ //#endregion
+ //#region custom_components/hass_datapoints/src/lib/domain/aggregation/labels.ts
+ var BINARY_AGGREGATE_KINDS = [
+ {
+ value: "transition_count",
+ label: "Count changes to a state"
+ },
+ {
+ value: "all_changes",
+ label: "Count all state changes"
+ },
+ {
+ value: "state_duration",
+ label: "Time in a state"
+ },
+ {
+ value: "state_percentage",
+ label: "Percentage of time in a state"
+ }
+ ];
+ var AGGREGATE_INTERVAL_OPTIONS = [
+ {
+ value: "minute",
+ label: "Every minute"
+ },
+ {
+ value: "15min",
+ label: "Every 15 minutes"
+ },
+ {
+ value: "30min",
+ label: "Every 30 minutes"
+ },
+ {
+ value: "hour",
+ label: "Hourly"
+ },
+ {
+ value: "day",
+ label: "Daily"
+ },
+ {
+ value: "week",
+ label: "Weekly"
+ },
+ {
+ value: "month",
+ label: "Monthly"
+ }
+ ];
+ var STATE_LABELS = {
+ door: ["Open", "Closed"],
+ garage_door: ["Open", "Closed"],
+ window: ["Open", "Closed"],
+ opening: ["Open", "Closed"],
+ motion: ["Detected", "Clear"],
+ occupancy: ["Occupied", "Clear"],
+ lock: ["Unlocked", "Locked"]
+ };
+ /** On/off options labelled for the entity's device class (e.g. Open/Closed). */
+ function getBinaryStateOptions(deviceClass) {
+ const [on, off] = STATE_LABELS[deviceClass ?? ""] ?? ["On", "Off"];
+ return [{
+ value: "on",
+ label: on
+ }, {
+ value: "off",
+ label: off
+ }];
+ }
+ function getBinaryAggregateKind(definition) {
+ if (definition.operation === "transition_count") return definition.states?.length ? "transition_count" : "all_changes";
+ return definition.operation === "state_percentage" ? "state_percentage" : "state_duration";
+ }
+ /** Apply a user-facing kind to a definition, keeping states consistent. */
+ function applyBinaryAggregateKind(definition, kind, defaultState = "on") {
+ const states = definition.states?.length ? definition.states : [defaultState];
+ if (kind === "all_changes") return {
+ ...definition,
+ operation: "transition_count",
+ states: []
+ };
+ const operation = kind;
+ const mode = operation === "state_percentage" ? "interval" : definition.mode;
+ return {
+ ...definition,
+ operation,
+ mode,
+ states
+ };
+ }
+ function createBinaryAggregate(existingIds, defaultState = "on") {
+ let id;
+ do
+ id = `agg_${Math.random().toString(36).slice(2, 8)}`;
+ while (existingIds.includes(id));
+ return {
+ id,
+ operation: "transition_count",
+ interval: "day",
+ mode: "interval",
+ states: [defaultState],
+ resetOnZoom: false
+ };
+ }
+ var MEASUREMENT_OPERATIONS = [
+ {
+ value: "mean",
+ label: "Mean"
+ },
+ {
+ value: "min",
+ label: "Minimum"
+ },
+ {
+ value: "max",
+ label: "Maximum"
+ },
+ {
+ value: "median",
+ label: "Median"
+ },
+ {
+ value: "first",
+ label: "First reading"
+ },
+ {
+ value: "last",
+ label: "Last reading"
}
+ ];
+ /** Everything offered on a numeric row: measurement summaries plus consumption. */
+ var NUMERIC_AGGREGATE_OPERATIONS = [...MEASUREMENT_OPERATIONS, {
+ value: "consumption",
+ label: "Consumption"
+ }];
+ function isNumericAggregateOperation(operation) {
+ return NUMERIC_AGGREGATE_OPERATIONS.some((o) => o.value === operation);
+ }
+ var MEASUREMENT_VALUES = new Set(MEASUREMENT_OPERATIONS.map((o) => o.value));
+ function isMeasurementOperation(operation) {
+ return MEASUREMENT_VALUES.has(operation);
+ }
+ function createMeasurementAggregate(existingIds, operation = "mean") {
+ return {
+ ...createBinaryAggregate(existingIds),
+ operation,
+ states: void 0
+ };
+ }
+ function createEventAggregate(existingIds, eventTypes = []) {
+ return {
+ ...createBinaryAggregate(existingIds),
+ operation: "event_count",
+ states: void 0,
+ eventTypes
+ };
+ }
+ /**
+ * Dash pattern (px on/off) per aggregation type. Aggregate lines keep the
+ * source series' colour, so the pattern is what tells them apart.
+ */
+ var DASH_PATTERNS = {
+ mean: [10, 4],
+ min: [2, 4],
+ max: [
+ 2,
+ 4,
+ 10,
+ 4
+ ],
+ median: [
+ 6,
+ 3,
+ 2,
+ 3
+ ],
+ first: [1, 5],
+ last: [
+ 1,
+ 3,
+ 1,
+ 3,
+ 8,
+ 3
+ ],
+ consumption: [10, 4],
+ transition_count: [10, 4],
+ state_duration: [2, 4],
+ state_percentage: [
+ 2,
+ 4,
+ 10,
+ 4
+ ],
+ event_count: [
+ 6,
+ 3,
+ 2,
+ 3
+ ]
};
+ function getAggregateDashPattern(operation) {
+ return DASH_PATTERNS[operation] ?? [6, 3];
+ }
//#endregion
- //#region custom_components/hass_datapoints/src/cards/history/history.styles.ts
- var styles$63 = i$5`
- :host {
- display: block;
- height: 100%;
- }
-
- ha-card {
- height: 100%;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- }
-
- .card-header {
- margin: 0;
- padding: 12px 16px 0;
- font-size: 24px;
- font-weight: 400;
- line-height: 48px;
- color: var(--primary-text-color);
- flex: 0 0 auto;
- display: flex;
- justify-content: space-between;
- gap: 8px;
- }
-
- .card-header-title {
- min-width: 0;
- font: inherit;
- line-height: inherit;
- font-weight: inherit;
- }
-
- .card-header-action {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- flex: 0 0 auto;
- align-self: center;
- }
-
- hass-datapoints-history-chart {
- flex: 1 1 auto;
- min-height: 0;
- }
-`;
+ //#region custom_components/hass_datapoints/src/lib/chart/chart-aggregates.ts
+ var UNKNOWN_STATES = /* @__PURE__ */ new Set(["unknown", "unavailable"]);
+ /** Append the cumulative marker so the legend tells totals from per-period bars. */
+ function withCumulativeSuffix(definition, label) {
+ return definition.mode === "cumulative" ? `${label} · ${msg("Cumulative")}` : label;
+ }
+ /** Coverage flags and tooltip notes for each plotted period. */
+ function periodDetails(valued, reasonNotes) {
+ return {
+ periodFlags: valued.map((p) => ({
+ partial: p.partial,
+ incomplete: p.incomplete
+ })),
+ periodNotes: valued.map((p) => [
+ p.partial ? msg("Partial period") : "",
+ p.incomplete ? msg("Incomplete history") : "",
+ reasonNotes ? reasonNotes(p.reasons) : ""
+ ].filter(Boolean).join(", "))
+ };
+ }
+ function getAggregateSeriesId(entityId, definitionId) {
+ return `aggregate:${entityId}:${definitionId}`;
+ }
+ /** Spans where a known (not unknown/unavailable) state was recorded. */
+ function buildCoverage(samples, range) {
+ const sorted = [...samples].sort((a, b) => a.time - b.time);
+ const coverage = [];
+ let seed;
+ const inRange = [];
+ for (const sample of sorted) if (sample.time <= range.start) seed = sample;
+ else if (sample.time < range.end) inRange.push(sample);
+ const points = seed ? [{
+ ...seed,
+ time: range.start
+ }, ...inRange] : inRange;
+ points.forEach((point, i) => {
+ if (!point.state || UNKNOWN_STATES.has(point.state)) return;
+ const end = i + 1 < points.length ? points[i + 1].time : range.end;
+ if (end > point.time) coverage.push({
+ start: point.time,
+ end
+ });
+ });
+ return coverage;
+ }
+ function describe(definition, stateLabel) {
+ const kind = getBinaryAggregateKind(definition);
+ const kindLabel = msg(BINARY_AGGREGATE_KINDS.find((k) => k.value === kind)?.label ?? kind);
+ return `${msg(AGGREGATE_INTERVAL_OPTIONS.find((o) => o.value === definition.interval)?.label ?? definition.interval)} · ${kindLabel}${kind === "all_changes" ? "" : ` (${stateLabel})`}`;
+ }
+ /**
+ * Calculate every configured aggregate for a binary row and shape the results
+ * as chart series. Periods with no known history have no bar (never zero).
+ */
+ function buildBinaryAggregateSeries(args) {
+ const samples = args.stateList.map((entry) => ({
+ time: Math.round(entry.lu * 1e3),
+ state: entry.s
+ }));
+ const coverage = buildCoverage(samples, args.range);
+ const stateOptions = getBinaryStateOptions(args.deviceClass);
+ return args.definitions.filter((definition) => definition.visible !== false).flatMap((definition) => {
+ const result = calculateAggregate({
+ definition,
+ samples,
+ range: args.range,
+ zoom: args.zoom,
+ timeZone: args.timeZone,
+ coverage
+ });
+ const valued = result.points.filter((p) => typeof p.value === "number" && Number.isFinite(p.value));
+ const pts = valued.map((p) => [(p.start + p.end) / 2, p.value]);
+ if (!pts.length) return [];
+ const stateValue = definition.states?.[0] ?? stateOptions[0].value;
+ const stateLabel = msg(stateOptions.find((o) => o.value === stateValue)?.label ?? stateValue);
+ const id = getAggregateSeriesId(args.entityId, definition.id);
+ return [{
+ entityId: id,
+ legendEntityId: id,
+ sourceEntityId: args.entityId,
+ definitionId: definition.id,
+ label: definition.label || withCumulativeSuffix(definition, `${args.entityLabel} · ${describe(definition, stateLabel)}`),
+ unit: result.unit,
+ pts,
+ periods: valued.map((p) => ({
+ start: p.start,
+ end: p.end
+ })),
+ ...periodDetails(valued),
+ color: definition.color || args.color,
+ dashPattern: getAggregateDashPattern(definition.operation),
+ axisKey: `aggregate:${result.unit || "count"}`,
+ bars: result.presentation === "bar",
+ result
+ }];
+ });
+ }
+ /** Aggregate numeric history into per-period measurement summaries (lines). */
+ function buildMeasurementAggregateSeries(args) {
+ const samples = args.stateList.flatMap((entry) => {
+ const value = parseFloat(entry.s);
+ return Number.isNaN(value) ? [] : [{
+ time: Math.round(entry.lu * 1e3),
+ value
+ }];
+ });
+ if (!samples.length) return [];
+ return args.definitions.filter((definition) => definition.visible !== false && isMeasurementOperation(definition.operation)).flatMap((definition) => {
+ const result = calculateAggregate({
+ definition,
+ samples,
+ range: args.range,
+ zoom: args.zoom,
+ timeZone: args.timeZone,
+ unit: args.unit,
+ coverage: [args.range]
+ });
+ const valued = result.points.filter((p) => typeof p.value === "number" && Number.isFinite(p.value));
+ if (!valued.length) return [];
+ const operationLabel = msg(MEASUREMENT_OPERATIONS.find((o) => o.value === definition.operation)?.label ?? definition.operation);
+ const intervalLabel = msg(AGGREGATE_INTERVAL_OPTIONS.find((o) => o.value === definition.interval)?.label ?? definition.interval);
+ const id = getAggregateSeriesId(args.entityId, definition.id);
+ return [{
+ entityId: id,
+ legendEntityId: id,
+ sourceEntityId: args.entityId,
+ definitionId: definition.id,
+ label: definition.label || withCumulativeSuffix(definition, `${args.entityLabel} · ${intervalLabel} · ${operationLabel}`),
+ unit: args.unit,
+ pts: valued.map((p) => [(p.start + p.end) / 2, p.value]),
+ periods: valued.map((p) => ({
+ start: p.start,
+ end: p.end
+ })),
+ ...periodDetails(valued),
+ color: definition.color || args.color,
+ dashPattern: getAggregateDashPattern(definition.operation),
+ axisKey: args.axisKey,
+ bars: result.presentation === "bar",
+ result
+ }];
+ });
+ }
+ /**
+ * Turn an event entity's recorded states into occurrences. The state value is
+ * the event timestamp and `event_type` is an attribute. States at or before the
+ * range start are the initial snapshot, not events that happened in range.
+ */
+ function extractEventSamples(states, range) {
+ return states.flatMap((entry) => {
+ const raw = entry.s ?? entry.state;
+ if (raw === void 0 || UNKNOWN_STATES.has(raw)) return [];
+ const eventType = (entry.a ?? entry.attributes)?.event_type;
+ if (typeof eventType !== "string" || !eventType) return [];
+ const recordedAt = typeof entry.lu === "number" ? entry.lu * 1e3 : Date.parse(entry.last_updated ?? "");
+ const parsed = Date.parse(raw);
+ const eventTime = Number.isFinite(parsed) ? parsed : recordedAt;
+ if (!Number.isFinite(eventTime)) return [];
+ return [{
+ time: Number.isFinite(recordedAt) ? recordedAt : eventTime,
+ eventTime,
+ eventType,
+ boundary: Number.isFinite(recordedAt) && recordedAt <= range.start
+ }];
+ });
+ }
+ /** Count event occurrences per calendar period as bars (or cumulative lines). */
+ function buildEventAggregateSeries(args) {
+ if (!args.states.length) return [];
+ const samples = extractEventSamples(args.states, args.range);
+ return args.definitions.filter((definition) => definition.visible !== false && definition.operation === "event_count").flatMap((definition) => {
+ const result = calculateAggregate({
+ definition,
+ samples,
+ range: args.range,
+ zoom: args.zoom,
+ timeZone: args.timeZone,
+ coverage: [args.range]
+ });
+ const valued = result.points.filter((p) => typeof p.value === "number" && Number.isFinite(p.value));
+ if (!valued.length) return [];
+ const intervalLabel = msg(AGGREGATE_INTERVAL_OPTIONS.find((o) => o.value === definition.interval)?.label ?? definition.interval);
+ const typesLabel = definition.eventTypes?.length ? definition.eventTypes.join(", ") : msg("All event types");
+ const id = getAggregateSeriesId(args.entityId, definition.id);
+ return [{
+ entityId: id,
+ legendEntityId: id,
+ sourceEntityId: args.entityId,
+ definitionId: definition.id,
+ label: definition.label || withCumulativeSuffix(definition, `${args.entityLabel} · ${intervalLabel} · ${typesLabel}`),
+ unit: result.unit,
+ pts: valued.map((p) => [(p.start + p.end) / 2, p.value]),
+ periods: valued.map((p) => ({
+ start: p.start,
+ end: p.end
+ })),
+ ...periodDetails(valued),
+ color: definition.color || args.color,
+ dashPattern: getAggregateDashPattern(definition.operation),
+ axisKey: `aggregate:${result.unit || "count"}`,
+ bars: result.presentation === "bar",
+ result
+ }];
+ });
+ }
+ var STATISTICS_PERIOD_MS = 36e5;
+ function describeConsumptionReasons(reasons) {
+ const notes = [];
+ if (reasons.includes("reset")) notes.push(msg("Meter reset detected"));
+ if (reasons.includes("ambiguous-reset")) notes.push(msg("Ambiguous meter reset, not counted"));
+ return notes.join(", ");
+ }
+ /**
+ * Consumption per period for total / total_increasing meters. Statistics
+ * `change` is used where HA has compiled it; raw readings cover the rest.
+ */
+ function buildConsumptionAggregateSeries(args) {
+ const readings = args.readings.flatMap((entry) => {
+ const value = parseFloat(entry.s);
+ return Number.isNaN(value) ? [] : [{
+ time: Math.round(entry.lu * 1e3),
+ value
+ }];
+ });
+ const changeSamples = args.changes.map((entry) => ({
+ time: entry.time,
+ change: entry.change
+ }));
+ let samples;
+ if (changeSamples.length) {
+ const cutoff = changeSamples[changeSamples.length - 1].time + STATISTICS_PERIOD_MS;
+ const before = readings.filter((r) => r.time < cutoff);
+ const seed = before[before.length - 1];
+ samples = [
+ ...changeSamples,
+ ...seed ? [seed] : [],
+ ...readings.filter((r) => r.time >= cutoff)
+ ];
+ } else samples = readings;
+ if (!samples.length) return [];
+ return args.definitions.filter((definition) => definition.visible !== false && definition.operation === "consumption").flatMap((definition) => {
+ const result = calculateAggregate({
+ definition,
+ samples,
+ range: args.range,
+ zoom: args.zoom,
+ timeZone: args.timeZone,
+ stateClass: args.stateClass,
+ unit: args.unit,
+ coverage: [args.range]
+ });
+ const valued = result.points.filter((p) => typeof p.value === "number" && Number.isFinite(p.value));
+ if (!valued.length) return [];
+ const intervalLabel = msg(AGGREGATE_INTERVAL_OPTIONS.find((o) => o.value === definition.interval)?.label ?? definition.interval);
+ const id = getAggregateSeriesId(args.entityId, definition.id);
+ return [{
+ entityId: id,
+ legendEntityId: id,
+ sourceEntityId: args.entityId,
+ definitionId: definition.id,
+ label: definition.label || withCumulativeSuffix(definition, `${args.entityLabel} · ${intervalLabel} · ${msg("Consumption")}`),
+ unit: args.unit,
+ pts: valued.map((p) => [(p.start + p.end) / 2, p.value]),
+ periods: valued.map((p) => ({
+ start: p.start,
+ end: p.end
+ })),
+ ...periodDetails(valued, describeConsumptionReasons),
+ color: definition.color || args.color,
+ dashPattern: getAggregateDashPattern(definition.operation),
+ axisKey: `aggregate:${args.unit || "count"}`,
+ bars: result.presentation === "bar",
+ result
+ }];
+ });
+ }
//#endregion
//#region custom_components/hass_datapoints/src/cards/history/data/downsampling.ts
/**
@@ -8647,6 +9835,24 @@
merged.sort((a, b) => a.lu - b.lu);
return merged;
}
+ /**
+ * Per-period consumption from HA cumulative statistics (`change`). Entities
+ * without cumulative statistics yield an empty list.
+ */
+ function normalizeStatisticsChanges(entityId, statsData) {
+ const statEntries = statsData && typeof statsData === "object" ? statsData[entityId] ?? [] : [];
+ return (Array.isArray(statEntries) ? statEntries : []).flatMap((entry) => {
+ const change = Number(entry?.change);
+ const raw = entry?.start;
+ let time;
+ if (typeof raw === "number") time = raw > 1e11 ? raw : raw * 1e3;
+ else time = new Date(raw).getTime();
+ return entry?.change == null || !Number.isFinite(change) || !Number.isFinite(time) ? [] : [{
+ time: Math.round(time),
+ change
+ }];
+ }).sort((a, b) => a.time - b.time);
+ }
//#endregion
//#region custom_components/hass_datapoints/src/cards/history/data/axis-extent.ts
/**
@@ -9181,7 +10387,33 @@
const top = Math.min(y, baselineY);
const height = Math.max(1, Math.abs(baselineY - y));
const left = x - barWidth / 2;
+ const style = options.pointStyles?.[i];
+ if (style?.partial) ctx.fillStyle = hexToRgba(color, fillAlpha * .5);
+ else ctx.fillStyle = hexToRgba(color, fillAlpha);
ctx.fillRect(left, top, barWidth, height);
+ if (style?.incomplete) {
+ ctx.save();
+ ctx.beginPath();
+ ctx.rect(left, top, barWidth, height);
+ ctx.clip();
+ ctx.strokeStyle = color;
+ ctx.lineWidth = 1;
+ ctx.beginPath();
+ for (let d = -height; d < barWidth; d += 5) {
+ ctx.moveTo(left + d, top + height);
+ ctx.lineTo(left + d + height, top);
+ }
+ ctx.stroke();
+ ctx.restore();
+ }
+ if (style?.partial || style?.incomplete) {
+ ctx.save();
+ ctx.setLineDash(style.partial ? [4, 3] : [1, 2]);
+ ctx.strokeStyle = color;
+ ctx.lineWidth = 1;
+ ctx.strokeRect(left + .5, top + .5, barWidth - 1, height - 1);
+ ctx.restore();
+ }
}
ctx.restore();
}
@@ -10203,7 +11435,7 @@
if (anomalyTooltip) anomalyTooltip.style.display = "none";
clearAnnotationTooltips(card);
}
- function resolveTooltipSeriesLabel(entry) {
+ function resolveBaseTooltipSeriesLabel(entry) {
const isSubordinate = entry.grouped === true && entry.rawVisible === true;
const isComparisonDerived = entry.comparisonDerived === true && entry.grouped === true;
if (entry.comparison === true) {
@@ -10238,6 +11470,10 @@
}
return String(entry.label || "");
}
+ function resolveTooltipSeriesLabel(entry) {
+ const label = resolveBaseTooltipSeriesLabel(entry);
+ return entry.periodLabel ? `${label} (${entry.periodLabel})` : label;
+ }
function showLineChartTooltip(card, hover, clientX, clientY) {
const root = getRoot(card);
const tooltip = root.getElementById("tooltip");
@@ -10716,18 +11952,33 @@
const rawTimeMs = t0 + ratio * (t1 - t0);
const timeMs = resolveLineChartHoverTime(resolvedSeries, rawTimeMs, options.hoverSnapMode || "follow_series");
const x = renderer.xOf(timeMs, t0, t1);
+ const resolveSeriesValue = (seriesItem) => {
+ if (!seriesItem.periods) return { value: renderer._interpolateValue(seriesItem.pts || [], timeMs, seriesItem.stepped) };
+ const index = seriesItem.periods.findIndex((period) => timeMs >= period.start && timeMs < period.end);
+ if (index === -1) return { value: null };
+ const point = seriesItem.pts?.[index];
+ const { start, end } = (seriesItem.labelPeriods ?? seriesItem.periods)[index];
+ const note = seriesItem.periodNotes?.[index];
+ return {
+ value: point ? point[1] : null,
+ periodLabel: `${formatTooltipDateTimeFromMs(start)} – ${formatTooltipDateTimeFromMs(end)}${note ? ` · ${note}` : ""}`
+ };
+ };
const values = resolvedSeries.map((seriesItem) => {
- const value = renderer._interpolateValue(seriesItem.pts || [], timeMs, seriesItem.stepped);
+ const { value, periodLabel } = resolveSeriesValue(seriesItem);
const axis = resolveHoverAxis(seriesItem);
- return buildHoverValueEntry(seriesItem, value, axis, {}, {
+ return buildHoverValueEntry(seriesItem, value, axis, periodLabel ? { periodLabel } : {}, {
includePosition: value != null,
x
});
});
const comparisonValues = comparisonSeries.map((seriesItem) => {
- const value = renderer._interpolateValue(seriesItem.pts || [], timeMs, seriesItem.stepped);
+ const { value, periodLabel } = resolveSeriesValue(seriesItem);
const axis = resolveHoverAxis(seriesItem);
- return buildHoverValueEntry(seriesItem, value, axis, { comparison: true }, {
+ return buildHoverValueEntry(seriesItem, value, axis, {
+ comparison: true,
+ ...periodLabel ? { periodLabel } : {}
+ }, {
includePosition: value != null,
x
});
@@ -11349,6 +12600,93 @@
no_attributes: options.no_attributes !== false
}));
}
+ /**
+ * Like {@link fetchHistoryDuringPeriod}, but keeps state attributes for HA
+ * `event.*` entities (their `event_type` attribute is what event counting
+ * needs). Every other entity keeps the lean `no_attributes` request.
+ */
+ async function fetchHistoryWithEventAttributes(hass, startTime, endTime, entityIds, options = {}) {
+ const ids = normalizeCacheIdList(entityIds);
+ const eventIds = ids.filter((id) => id.startsWith("event."));
+ if (!eventIds.length) return await fetchHistoryDuringPeriod(hass, startTime, endTime, ids, options);
+ const otherIds = ids.filter((id) => !id.startsWith("event."));
+ const [others, events] = await Promise.all([otherIds.length ? fetchHistoryDuringPeriod(hass, startTime, endTime, otherIds, options) : Promise.resolve({}), fetchHistoryDuringPeriod(hass, startTime, endTime, eventIds, {
+ ...options,
+ no_attributes: false
+ })]);
+ return {
+ ...others,
+ ...events
+ };
+ }
+ //#endregion
+ //#region custom_components/hass_datapoints/src/lib/domain/aggregation/types.ts
+ var AGGREGATE_OPERATIONS = [
+ "mean",
+ "min",
+ "max",
+ "median",
+ "first",
+ "last",
+ "consumption",
+ "transition_count",
+ "state_duration",
+ "state_percentage",
+ "event_count"
+ ];
+ var AGGREGATE_INTERVALS = [
+ "minute",
+ "15min",
+ "30min",
+ "hour",
+ "day",
+ "week",
+ "month"
+ ];
+ //#endregion
+ //#region custom_components/hass_datapoints/src/lib/domain/aggregation/aggregation.ts
+ function normalizeAggregateDefinitions(value) {
+ if (!Array.isArray(value)) return [];
+ const ids = /* @__PURE__ */ new Set();
+ return value.flatMap((item) => {
+ if (!item || typeof item !== "object" || typeof item.id !== "string" || !item.id || ids.has(item.id) || !AGGREGATE_OPERATIONS.includes(item.operation) || !AGGREGATE_INTERVALS.includes(item.interval)) return [];
+ ids.add(item.id);
+ const definition = {
+ id: item.id,
+ operation: item.operation,
+ interval: item.interval,
+ mode: item.mode === "cumulative" && [
+ "consumption",
+ "transition_count",
+ "state_duration",
+ "event_count"
+ ].includes(item.operation) ? "cumulative" : "interval",
+ resetOnZoom: item.resetOnZoom === true
+ };
+ for (const key of ["states", "eventTypes"]) if (Array.isArray(item[key])) definition[key] = [...new Set(item[key].filter((v) => typeof v === "string" && v.length > 0))];
+ for (const key of ["color", "label"]) if (typeof item[key] === "string") definition[key] = item[key];
+ if (typeof item.visible === "boolean") definition.visible = item.visible;
+ return [definition];
+ });
+ }
+ function getAggregateOperations(entity) {
+ if (entity.entity_id.startsWith("event.")) return ["event_count"];
+ if (entity.entity_id.startsWith("binary_sensor.") || entity.state !== void 0 && !["unknown", "unavailable"].includes(entity.state) && !Number.isFinite(Number(entity.state))) return [
+ "transition_count",
+ "state_duration",
+ "state_percentage"
+ ];
+ const operations = [
+ "mean",
+ "min",
+ "max",
+ "median",
+ "first",
+ "last"
+ ];
+ if (["total", "total_increasing"].includes(String(entity.attributes?.state_class))) operations.push("consumption");
+ return operations;
+ }
//#endregion
//#region custom_components/hass_datapoints/src/lib/anomaly-direction.ts
var DEFAULT_ANOMALY_DIRECTION = "both";
@@ -11427,6 +12765,7 @@
function normalizeHistorySeriesAnalysis(analysis) {
const source = analysis && typeof analysis === "object" ? analysis : {};
return {
+ ...Array.isArray(source.aggregates) ? { aggregates: normalizeAggregateDefinitions(source.aggregates) } : {},
expanded: source.expanded === true,
show_trend_lines: source.show_trend_lines === true,
trend_method: [
@@ -12472,7 +13811,7 @@
};
}
function seriesHasActiveAnalysis(analysis, hasSelectedComparisonWindow = false) {
- return !!(analysis.show_trend_lines || analysis.show_summary_stats || analysis.show_rate_of_change || analysis.show_threshold_analysis || analysis.show_anomalies || analysis.show_delta_analysis && hasSelectedComparisonWindow);
+ return !!(analysis.show_trend_lines || analysis.show_summary_stats || analysis.show_rate_of_change || analysis.show_threshold_analysis || analysis.show_anomalies || analysis.show_delta_analysis && hasSelectedComparisonWindow || (analysis.aggregates?.length ?? 0) > 0);
}
function seriesShouldHideSource(analysis, hasSelectedComparisonWindow = false) {
return analysis.hide_source_series === true && seriesHasActiveAnalysis(analysis, hasSelectedComparisonWindow);
@@ -13006,7 +14345,7 @@
}
//#endregion
//#region custom_components/hass_datapoints/src/cards/history/history-chart/history-chart.styles.ts
- var styles$62 = `
+ var styles$63 = `
hass-datapoints-history-chart {
position: relative;
display: flex;
@@ -13420,6 +14759,16 @@
background: color-mix(in srgb, var(--primary-color, #03a9f4) 18%, var(--card-background-color, #fff));
outline: none;
}
+ .aggregate-note {
+ padding: var(--dp-spacing-xs, 4px) var(--dp-spacing-md);
+ padding-left: calc(var(--dp-spacing-md) + var(--dp-chart-axis-left-width, 0px));
+ font-size: 0.74em;
+ color: var(--secondary-text-color);
+ border-top: 1px solid color-mix(in srgb, var(--divider-color, rgba(0, 0, 0, 0.12)) 88%, transparent);
+ }
+ .aggregate-note[hidden] {
+ display: none;
+ }
.legend {
display: flex;
flex-wrap: nowrap;
@@ -13654,6 +15003,18 @@
var HISTORY_CHART_MAX_ZOOM_MULTIPLIER = 365;
var HISTORY_CHART_BINARY_LANE_HEIGHT_PX = 22;
var HISTORY_CHART_BINARY_LANE_GAP_PX = 6;
+ /**
+ * Aggregate lines are drawn in the source colour; their dash pattern (per
+ * aggregation type) tells them apart. Non-aggregate series get no extra style.
+ */
+ function aggregateLineStyle(series) {
+ if (!series.periodFlags || series.bars === true) return {};
+ return {
+ aggregateLine: true,
+ dashed: true,
+ dashPattern: series.dashPattern
+ };
+ }
var HistoryChart$1 = class extends HTMLElement {
constructor(..._args) {
super(..._args);
@@ -13931,7 +15292,7 @@
connectedCallback() {
this.style.cssText = "position:relative;display:flex;flex-direction:column;height:100%;min-height:0;padding:var(--dp-spacing-sm,8px) var(--dp-spacing-md,12px) var(--dp-spacing-md,12px);box-sizing:border-box;overflow:visible;isolation:isolate;z-index:3;";
if (this.querySelector("#chart")) return;
- this.innerHTML = `
+ this.innerHTML = `
@@ -13984,6 +15345,7 @@
+
`;
}
disconnectedCallback() {
@@ -14254,6 +15616,25 @@
_normalizeStatisticsHistory(entityId, statsData) {
return normalizeStatisticsHistory(entityId, statsData);
}
+ /**
+ * Plain-text explanation of the partial/incomplete markers and of the limits
+ * of retained history, so the indicators do not rely on colour or shape alone.
+ */
+ _renderAggregateNote(series) {
+ const noteEl = this.querySelector("#aggregate-note");
+ if (!noteEl) return;
+ const aggregates = series.filter((item) => item.definitionId && item.result);
+ const flagged = aggregates.some((item) => item.bars && item.periodFlags?.some((flag) => flag.partial || flag.incomplete));
+ const hasCounts = aggregates.some((item) => ["transition_count", "event_count"].includes(item.result.points.length ? this._aggregateOperationOf(item) : ""));
+ const lines = [];
+ if (flagged) lines.push(msg("Dashed outlines mark partial periods cut by the selected range; hatched or dotted marks show periods with gaps in recorded history."));
+ if (hasCounts) lines.push(msg("Counts only include activity in retained history; earlier activity is not shown."));
+ D(b`${lines.map((line) => b`${line}
`)}`, noteEl);
+ noteEl.hidden = lines.length === 0;
+ }
+ _aggregateOperationOf(item) {
+ return (this._seriesSettings.find((row) => row.entity_id === item.sourceEntityId)?.analysis?.aggregates?.find((d) => d.id === item.definitionId))?.operation ?? "";
+ }
/** Render legend items for the given series, binary backgrounds and rows. */
_renderLegend(series, binaryBackgrounds, binaryRows = []) {
const legendEl = this.querySelector("#legend");
@@ -14270,7 +15651,7 @@
{
if (!this._hiddenSeries) return;
if (this._hiddenSeries.has(item.entityId)) this._hiddenSeries.delete(item.entityId);
@@ -14317,11 +15698,93 @@
return laneCenters;
}
/** Draw a single series line onto the renderer, with optional data-gap rendering. */
+ /**
+ * Whether a series is hidden. Aggregate series follow their source row: when
+ * the row's visibility switch is off, its aggregates are hidden with it.
+ */
+ _isSeriesHidden(item) {
+ return this._hiddenSeries.has(item.legendEntityId || item.entityId) || item.sourceEntityId !== void 0 && this._hiddenSeries.has(item.sourceEntityId);
+ }
+ /** HA's configured timezone; aggregate calendar periods never use the browser's. */
+ _getTimeZone() {
+ return this._hass?.config?.time_zone || Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
+ }
+ /**
+ * Aggregate series configured on one row, calculated from the given history
+ * and statistics for `range`. Used for the main window and, with a window's
+ * own data and range, for each comparison window.
+ */
+ _buildRowAggregates(args) {
+ const { entityId, definitions, range } = args;
+ if (!definitions.length) return [];
+ const domain = entityId.split(".")[0];
+ const timeZone = this._getTimeZone();
+ const entityIds = this._seriesSettings.map((setting) => setting.entity_id);
+ const attributes = this._hass?.states?.[entityId]?.attributes;
+ if (domain === "event") return buildEventAggregateSeries({
+ entityId,
+ entityLabel: args.label,
+ color: args.color,
+ definitions,
+ states: getHistoryStatesForEntity$1(args.histResult, entityId, entityIds),
+ range,
+ zoom: args.zoom,
+ timeZone
+ });
+ const stateList = args.stateList ?? this._buildEntityStateList(entityId, args.histResult, args.statsResult);
+ if (domain === "binary_sensor") return buildBinaryAggregateSeries({
+ entityId,
+ entityLabel: args.label,
+ color: args.color,
+ deviceClass: String(attributes?.device_class ?? ""),
+ definitions,
+ stateList,
+ range,
+ zoom: args.zoom,
+ timeZone
+ });
+ const unit = args.unit ?? "";
+ return [...buildMeasurementAggregateSeries({
+ entityId,
+ entityLabel: args.label,
+ color: args.color,
+ definitions,
+ stateList,
+ range,
+ zoom: args.zoom,
+ timeZone,
+ unit,
+ axisKey: args.axisKey ?? (unit || "__unitless__")
+ }), ...buildConsumptionAggregateSeries({
+ entityId,
+ entityLabel: args.label,
+ color: args.color,
+ definitions,
+ changes: normalizeStatisticsChanges(entityId, args.statsResult),
+ readings: normalizeNumericHistory(entityId, getHistoryStatesForEntity$1(args.histResult, entityId, entityIds)),
+ stateClass: attributes?.state_class,
+ unit,
+ range,
+ zoom: args.zoom,
+ timeZone
+ })];
+ }
_drawSeriesLine(renderer, pts, color, t0, t1, min, max, opts) {
const r = renderer;
const config = this._config;
const showGaps = config?.show_data_gaps !== false;
const gapThresholdMs = SAMPLE_INTERVAL_MS[config?.data_gap_threshold || "2h"] ?? 72e5;
+ if (opts.bars === true) {
+ renderer.drawBars(pts, color, t0, t1, min, max, {
+ fillAlpha: opts.lineOpacity ?? .78,
+ pointStyles: opts.periodFlags
+ });
+ return;
+ }
+ if (opts.aggregateLine) {
+ r.drawLine(pts, color, t0, t1, min, max, opts);
+ return;
+ }
if (!showGaps || pts.length < 2 || !Number.isFinite(gapThresholdMs)) {
r.drawLine(pts, color, t0, t1, min, max, opts);
return;
@@ -14940,6 +16403,22 @@
const series = [];
const axes = [];
const axisMap = /* @__PURE__ */ new Map();
+ const registerAggregateSeries = (aggregate) => {
+ let aggregateAxis = axisMap.get(aggregate.axisKey);
+ if (!aggregateAxis) {
+ aggregateAxis = {
+ key: aggregate.axisKey,
+ unit: aggregate.unit,
+ color: aggregate.color,
+ side: axisMap.size === 0 ? "left" : "right",
+ values: []
+ };
+ axisMap.set(aggregate.axisKey, aggregateAxis);
+ axes.push(aggregateAxis);
+ }
+ aggregateAxis.values.push(...aggregate.pts.map(([, v]) => v));
+ series.push(aggregate);
+ };
const binaryEntities = [];
const seriesSettings = this._seriesSettings;
const analysisMap = this._getSeriesAnalysisMap();
@@ -14955,13 +16434,32 @@
const entityLabelMap = disambiguateEntityNames(this._hass, seriesSettings.map((s) => s.entity_id));
seriesSettings.forEach((seriesSetting, i) => {
const entityId = seriesSetting.entity_id;
- if (entityId.split(".")[0] === "binary_sensor") {
+ const domain = entityId.split(".")[0];
+ if (domain === "binary_sensor") {
const stateList = this._buildEntityStateList(entityId, histResult, statsResult);
const binaryDisplay = resolveBinaryDisplayMode(seriesSetting.binary_display);
- binaryEntities.push({
+ const binaryAnalysis = analysisMap.get(entityId) || normalizeHistorySeriesAnalysis(null);
+ const binaryColor = seriesSetting.color || COLORS[i % COLORS.length];
+ const binaryLabel = entityLabelMap.get(entityId) || entityId;
+ for (const aggregate of this._buildRowAggregates({
entityId,
- label: entityLabelMap.get(entityId) || entityId,
- color: seriesSetting.color || COLORS[i % COLORS.length],
+ label: binaryLabel,
+ color: binaryColor,
+ index: i,
+ definitions: binaryAnalysis.aggregates ?? [],
+ histResult,
+ statsResult,
+ range: {
+ start: t0,
+ end: t1
+ },
+ zoom: this._zoomRange ?? void 0,
+ stateList
+ })) registerAggregateSeries(aggregate);
+ if (!this._seriesShouldHideSource(binaryAnalysis, hasSelectedComparisonWindow)) binaryEntities.push({
+ entityId,
+ label: binaryLabel,
+ color: binaryColor,
onLabel: this._binaryOnLabel(entityId),
offLabel: this._binaryOffLabel(entityId),
binaryDisplay,
@@ -14969,6 +16467,25 @@
});
return;
}
+ if (domain === "event") {
+ const eventAnalysis = analysisMap.get(entityId) || normalizeHistorySeriesAnalysis(null);
+ const eventColor = seriesSetting.color || COLORS[i % COLORS.length];
+ for (const aggregate of this._buildRowAggregates({
+ entityId,
+ label: entityLabelMap.get(entityId) || entityId,
+ color: eventColor,
+ index: i,
+ definitions: eventAnalysis.aggregates ?? [],
+ histResult,
+ statsResult,
+ range: {
+ start: t0,
+ end: t1
+ },
+ zoom: this._zoomRange ?? void 0
+ })) registerAggregateSeries(aggregate);
+ return;
+ }
const stateList = this._buildEntityStateList(entityId, histResult, statsResult);
const pts = [];
const unit = this._hass?.states?.[entityId]?.attributes?.unit_of_measurement || "";
@@ -14994,6 +16511,23 @@
}
if (pts.length) {
const seriesAnalysis = analysisMap.get(entityId) || normalizeHistorySeriesAnalysis(null);
+ const rowAggregates = this._buildRowAggregates({
+ entityId,
+ label: entityLabelMap.get(entityId) || entityId,
+ color: seriesSetting.color || COLORS[i % COLORS.length],
+ index: i,
+ definitions: seriesAnalysis.aggregates ?? [],
+ histResult,
+ statsResult,
+ range: {
+ start: t0,
+ end: t1
+ },
+ zoom: this._zoomRange ?? void 0,
+ stateList,
+ unit,
+ axisKey
+ });
series.push({
entityId,
legendEntityId: entityId,
@@ -15004,6 +16538,7 @@
axisKey,
stepped: seriesAnalysis.stepped_series === true
});
+ rowAggregates.forEach(registerAggregateSeries);
}
});
const binaryModel = buildBinaryRenderModel(binaryEntities, t0, t1);
@@ -15117,7 +16652,7 @@
t1,
options
];
- const visibleSeries = series.filter((entry) => !this._hiddenSeries.has(entry.legendEntityId || entry.entityId));
+ const visibleSeries = series.filter((entry) => !this._isSeriesHidden(entry));
const visibleRows = binaryRows.filter((row) => !this._hiddenSeries.has(row.entityId));
const binaryLaneBottomMargin = visibleRows.length ? HISTORY_CHART_BINARY_LANE_GAP_PX : 0;
const binaryLaneReserve = reserveLaneSpace(visibleRows.length, HISTORY_CHART_BINARY_LANE_HEIGHT_PX, HISTORY_CHART_BINARY_LANE_GAP_PX) + binaryLaneBottomMargin;
@@ -15228,6 +16763,7 @@
zoomOutButton.onclick = () => this._clearZoomRange();
}
this._renderLegend(series, binaryBackgrounds, binaryRows);
+ this._renderAggregateNote(series);
await this._drawSplitChart({
visibleSeries,
binaryBackgrounds,
@@ -15380,12 +16916,13 @@
if (!extent) return null;
const { min, max } = extent;
const pad = (max - min) * .1 || 1;
+ const barsOnly = series.filter((entry) => entry.axisKey === axis.key).every((entry) => entry.bars === true);
return {
key: axis.key,
unit: axis.unit,
color: axis.color,
side: axis.side === "right" ? "right" : "left",
- min: min - pad,
+ min: barsOnly ? Math.min(0, min) : min - pad,
max: max + pad
};
}).filter((ax) => ax !== null);
@@ -15704,6 +17241,61 @@
comparisonWindowId: win.id
});
}
+ for (const win of drawableComparisonResults) for (let rowIndex = 0; rowIndex < seriesSettings.length; rowIndex += 1) {
+ const seriesSetting = seriesSettings[rowIndex];
+ const entityId = seriesSetting.entity_id;
+ const rowAnalysis = analysisMap.get(entityId) || normalizeHistorySeriesAnalysis(null);
+ const baseColor = seriesSetting.color || COLORS[rowIndex % COLORS.length];
+ const comparisonAggregates = this._buildRowAggregates({
+ entityId,
+ label: entityLabelMap.get(entityId) || entityId,
+ color: baseColor,
+ index: rowIndex,
+ definitions: rowAnalysis.aggregates ?? [],
+ histResult: win.histResult,
+ statsResult: win.statsResult || {},
+ range: {
+ start: t0 + win.time_offset_ms,
+ end: t1 + win.time_offset_ms
+ }
+ });
+ for (const aggregate of comparisonAggregates) {
+ if (this._isSeriesHidden(aggregate)) continue;
+ const aggregateAxis = axisLookup.get(aggregate.axisKey);
+ if (!aggregateAxis) continue;
+ const shift = (time) => time - win.time_offset_ms;
+ const shiftedPts = aggregate.pts.map(([time, value]) => [shift(time), value]);
+ const shiftedPeriods = aggregate.periods.map((period) => ({
+ start: shift(period.start),
+ end: shift(period.end)
+ }));
+ const style = this._getComparisonWindowLineStyle(!!hoveredComparisonWindowId && win.id === hoveredComparisonWindowId, !!selectedComparisonWindowId && win.id === selectedComparisonWindowId, hoveringDifferentComparison);
+ if (aggregate.bars) renderer.drawBars(shiftedPts, aggregate.color, renderT0, renderT1, aggregateAxis.min, aggregateAxis.max, {
+ fillAlpha: style.lineOpacity * .4,
+ widthFactor: .9
+ });
+ else renderer.drawLine(shiftedPts, aggregate.color, renderT0, renderT1, aggregateAxis.min, aggregateAxis.max, {
+ lineOpacity: style.lineOpacity,
+ lineWidth: style.lineWidth,
+ dashed: true,
+ dashPattern: aggregate.dashPattern
+ });
+ comparisonHoverSeries.push({
+ entityId: `${win.id}:${aggregate.entityId}`,
+ relatedEntityId: aggregate.entityId,
+ label: aggregate.label,
+ windowLabel: win.label || "Date window",
+ unit: aggregate.unit,
+ pts: shiftedPts,
+ periods: shiftedPeriods,
+ labelPeriods: aggregate.periods,
+ periodNotes: aggregate.periodNotes,
+ color: aggregate.color,
+ axis: aggregateAxis,
+ hoverOpacity: style.hoverOpacity
+ });
+ }
+ }
this._setAdjustAxisButtonVisibility(comparisonPreviewActive && comparisonOutOfBounds && !this._adjustComparisonAxisScale && !autoAdjustHoveredComparisonAxis);
for (const s of visibleSeries) {
if (hiddenSourceEntityIds.has(s.entityId)) continue;
@@ -15711,7 +17303,10 @@
this._drawSeriesLine(renderer, s.pts, s.color, renderT0, renderT1, sWithAxis.axis.min, sWithAxis.axis.max, {
lineOpacity: mainSeriesHoverOpacity,
lineWidth: this._config?.comparison_hover_active === true ? 1.25 : void 0,
- stepped: (analysisMap.get(s.entityId) || normalizeHistorySeriesAnalysis(null)).stepped_series === true
+ stepped: (analysisMap.get(s.entityId) || normalizeHistorySeriesAnalysis(null)).stepped_series === true,
+ bars: s.bars === true,
+ periodFlags: s.periodFlags,
+ ...aggregateLineStyle(s)
});
}
const trendHoverSeries = trendSeries.map((seriesItem) => {
@@ -15880,6 +17475,7 @@
binaryRowLaneCenters
});
this._renderLegend(series, binaryBackgrounds, binaryRows);
+ this._renderAggregateNote(series);
const eventValueMap = new Map(eventHits.map((hit) => [hit.event.id, hit]));
const enrichedEvents = chartEvents.map((event) => {
const hit = eventValueMap.get(event.id || "");
@@ -16165,7 +17761,7 @@
let axisMax = 1;
if (extent) {
const pad = (extent.max - extent.min) * .1 || 1;
- axisMin = extent.min - pad;
+ axisMin = seriesItem.bars === true ? Math.min(0, extent.min) : extent.min - pad;
axisMax = extent.max + pad;
}
const primaryAxisKey = seriesItem.axisKey || seriesItem.unit || "__unitless__";
@@ -16231,7 +17827,10 @@
if (!rowHideSource) this._drawSeriesLine(renderer, seriesItem.pts, seriesItem.color, renderT0, renderT1, resolvedAxis.min, resolvedAxis.max, {
lineWidth: comparisonPreviewActive ? 1.25 : 1.75,
lineOpacity: mainSeriesOpacity,
- stepped: rowAnalysis.stepped_series === true
+ stepped: rowAnalysis.stepped_series === true,
+ bars: seriesItem.bars === true,
+ periodFlags: seriesItem.periodFlags,
+ ...aggregateLineStyle(seriesItem)
});
for (const win of drawableComparisonResults || []) {
const winPts = await this._resolveComparisonWindowPoints(seriesItem.entityId, win, rowAnalysis, renderT0, renderT1);
@@ -17201,7 +18800,7 @@
});
//#endregion
//#region custom_components/hass_datapoints/src/molecules/annotation-chip-row/annotation-chip-row.styles.ts
- var styles$61 = i$5`
+ var styles$62 = i$5`
:host {
display: block;
}
@@ -17232,7 +18831,7 @@
`;
//#endregion
//#region custom_components/hass_datapoints/src/atoms/interactive/annotation-chip/annotation-chip.styles.ts
- var styles$60 = i$5`
+ var styles$61 = i$5`
:host {
display: inline-flex;
}
@@ -17402,7 +19001,7 @@
`;
}
};
- _defineProperty(AnnotationChip, "styles", styles$60);
+ _defineProperty(AnnotationChip, "styles", styles$61);
__decorate([n$1({ type: String })], AnnotationChip.prototype, "type", null);
__decorate([n$1({
type: String,
@@ -17517,7 +19116,7 @@
`;
}
};
- _defineProperty(AnnotationChipRow, "styles", styles$61);
+ _defineProperty(AnnotationChipRow, "styles", styles$62);
__decorate([n$1({ type: Array })], AnnotationChipRow.prototype, "chips", null);
__decorate([n$1({
type: Object,
@@ -18410,14 +20009,14 @@
const cacheKey = this._getComparisonCacheKey(win, start, end);
const cached = this._comparisonDataCache.get(cacheKey);
if (cached) return cached;
- const historyPromise = fetchHistoryDuringPeriod(hass, start.toISOString(), end.toISOString(), this._entityIds, {
+ const historyPromise = fetchHistoryWithEventAttributes(hass, start.toISOString(), end.toISOString(), this._entityIds, {
include_start_time_state: true,
significant_changes_only: false,
no_attributes: true
}).catch(() => ({}));
const statisticsPromise = this._statisticsEntityIds.length ? fetchStatisticsDuringPeriod(hass, start.toISOString(), end.toISOString(), this._statisticsEntityIds, {
period: "hour",
- types: ["mean"],
+ types: ["mean", "change"],
units: {}
}).catch(() => ({})) : Promise.resolve({});
const [histResult, statsResult] = await Promise.all([historyPromise, statisticsPromise]);
@@ -18641,7 +20240,7 @@
this._setChartLoading(false);
return;
}
- fetchHistoryDuringPeriod(hass, start.toISOString(), end.toISOString(), this._entityIds, {
+ fetchHistoryWithEventAttributes(hass, start.toISOString(), end.toISOString(), this._entityIds, {
include_start_time_state: true,
significant_changes_only: false,
no_attributes: true
@@ -18659,7 +20258,7 @@
});
if (this._statisticsEntityIds.length) fetchStatisticsDuringPeriod(hass, start.toISOString(), end.toISOString(), this._statisticsEntityIds, {
period: "hour",
- types: ["mean"],
+ types: ["mean", "change"],
units: {}
}).then((statsResult) => {
partial.statsResult = statsResult || {};
@@ -18965,11 +20564,11 @@
return document.createElement("hass-datapoints-history-card-editor");
}
};
- _defineProperty(HassDatapointsHistoryCard, "styles", styles$63);
+ _defineProperty(HassDatapointsHistoryCard, "styles", styles$64);
customElements.define("hass-datapoints-history-card", HassDatapointsHistoryCard);
//#endregion
//#region custom_components/hass_datapoints/src/cards/history/editor.styles.ts
- var styles$59 = i$5`
+ var styles$60 = i$5`
.binary-display-row {
display: flex;
align-items: center;
@@ -19134,10 +20733,10 @@
`;
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-sample-group/analysis-sample-group.styles.ts
- var styles$58 = i$5``;
+ var styles$59 = i$5``;
//#endregion
//#region custom_components/hass_datapoints/src/atoms/analysis/analysis-group/analysis-group.styles.ts
- var styles$57 = i$5`
+ var styles$58 = i$5`
:host {
display: block;
--dp-spacing-xs: calc(var(--spacing, 8px) * 0.5);
@@ -19270,7 +20869,7 @@
`;
}
};
- _defineProperty(AnalysisGroup, "styles", styles$57);
+ _defineProperty(AnalysisGroup, "styles", styles$58);
__decorate([n$1({ type: String })], AnalysisGroup.prototype, "label", null);
__decorate([n$1({ type: Boolean })], AnalysisGroup.prototype, "checked", null);
__decorate([n$1({ type: Boolean })], AnalysisGroup.prototype, "disabled", null);
@@ -19281,7 +20880,7 @@
customElements.define("analysis-group", AnalysisGroup);
//#endregion
//#region custom_components/hass_datapoints/src/atoms/form/inline-select/inline-select.styles.ts
- var styles$56 = i$5`
+ var styles$57 = i$5`
:host {
display: inline-block;
}
@@ -19360,7 +20959,7 @@
`;
}
};
- _defineProperty(InlineSelect, "styles", styles$56);
+ _defineProperty(InlineSelect, "styles", styles$57);
__decorate([n$1({ type: String })], InlineSelect.prototype, "value", null);
__decorate([n$1({ type: Array })], InlineSelect.prototype, "options", null);
__decorate([n$1({ type: Boolean })], InlineSelect.prototype, "disabled", null);
@@ -19504,12 +21103,12 @@
`;
}
- }, _defineProperty(_AnalysisSampleGroup, "styles", [sharedStyles, styles$58]), _AnalysisSampleGroup);
+ }, _defineProperty(_AnalysisSampleGroup, "styles", [sharedStyles, styles$59]), _AnalysisSampleGroup);
AnalysisSampleGroup = __decorate([localized()], AnalysisSampleGroup);
customElements.define("analysis-sample-group", AnalysisSampleGroup);
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-trend-group/analysis-trend-group.styles.ts
- var styles$55 = i$5``;
+ var styles$56 = i$5``;
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-trend-group/analysis-trend-group.ts
var _AnalysisTrendGroup;
@@ -19622,12 +21221,12 @@
`;
}
- }, _defineProperty(_AnalysisTrendGroup, "styles", [sharedStyles, styles$55]), _AnalysisTrendGroup);
+ }, _defineProperty(_AnalysisTrendGroup, "styles", [sharedStyles, styles$56]), _AnalysisTrendGroup);
AnalysisTrendGroup = __decorate([localized()], AnalysisTrendGroup);
customElements.define("analysis-trend-group", AnalysisTrendGroup);
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-summary-group/analysis-summary-group.styles.ts
- var styles$54 = i$5``;
+ var styles$55 = i$5``;
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-summary-group/analysis-summary-group.ts
var _AnalysisSummaryGroup = class AnalysisSummaryGroup extends AnalysisGroupMixin(i$2) {
@@ -19654,12 +21253,12 @@
`;
}
};
- var AnalysisSummaryGroup = (_defineProperty(_AnalysisSummaryGroup, "styles", [sharedStyles, styles$54]), _AnalysisSummaryGroup);
+ var AnalysisSummaryGroup = (_defineProperty(_AnalysisSummaryGroup, "styles", [sharedStyles, styles$55]), _AnalysisSummaryGroup);
AnalysisSummaryGroup = __decorate([localized()], AnalysisSummaryGroup);
customElements.define("analysis-summary-group", AnalysisSummaryGroup);
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-rate-group/analysis-rate-group.styles.ts
- var styles$53 = i$5``;
+ var styles$54 = i$5``;
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-rate-group/analysis-rate-group.ts
var _AnalysisRateGroup;
@@ -19728,15 +21327,15 @@
`;
}
- }, _defineProperty(_AnalysisRateGroup, "styles", [sharedStyles, styles$53]), _AnalysisRateGroup);
+ }, _defineProperty(_AnalysisRateGroup, "styles", [sharedStyles, styles$54]), _AnalysisRateGroup);
AnalysisRateGroup = __decorate([localized()], AnalysisRateGroup);
customElements.define("analysis-rate-group", AnalysisRateGroup);
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-threshold-group/analysis-threshold-group.styles.ts
- var styles$52 = i$5``;
+ var styles$53 = i$5``;
//#endregion
//#region custom_components/hass_datapoints/src/atoms/form/number-input/number-input.styles.ts
- var styles$51 = i$5`
+ var styles$52 = i$5`
:host {
display: inline-flex;
align-items: center;
@@ -19818,7 +21417,7 @@
`;
}
};
- _defineProperty(NumberInput, "styles", styles$51);
+ _defineProperty(NumberInput, "styles", styles$52);
__decorate([n$1({ type: String })], NumberInput.prototype, "value", null);
__decorate([n$1({ type: String })], NumberInput.prototype, "placeholder", null);
__decorate([n$1({ type: String })], NumberInput.prototype, "suffix", null);
@@ -19887,13 +21486,13 @@
`;
}
- }, _defineProperty(_AnalysisThresholdGroup, "styles", [sharedStyles, styles$52]), _AnalysisThresholdGroup);
+ }, _defineProperty(_AnalysisThresholdGroup, "styles", [sharedStyles, styles$53]), _AnalysisThresholdGroup);
__decorate([n$1({ type: String })], AnalysisThresholdGroup.prototype, "unit", null);
AnalysisThresholdGroup = __decorate([localized()], AnalysisThresholdGroup);
customElements.define("analysis-threshold-group", AnalysisThresholdGroup);
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-anomaly-group/analysis-anomaly-group.styles.ts
- var styles$50 = i$5`
+ var styles$51 = i$5`
.method-computing-indicator {
display: inline-flex;
align-items: center;
@@ -20034,7 +21633,7 @@
`;
//#endregion
//#region custom_components/hass_datapoints/src/atoms/analysis/analysis-method-subopts/analysis-method-subopts.styles.ts
- var styles$49 = i$5`
+ var styles$50 = i$5`
:host {
display: block;
--dp-spacing-sm: var(--spacing, 8px);
@@ -20056,7 +21655,7 @@
return b`
`;
}
};
- _defineProperty(AnalysisMethodSubopts, "styles", styles$49);
+ _defineProperty(AnalysisMethodSubopts, "styles", styles$50);
customElements.define("analysis-method-subopts", AnalysisMethodSubopts);
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-anomaly-group/analysis-anomaly-group.ts
@@ -20562,7 +22161,7 @@
composed: true
}));
}
- }, _defineProperty(_AnalysisAnomalyGroup, "styles", [sharedStyles, styles$50]), _AnalysisAnomalyGroup);
+ }, _defineProperty(_AnalysisAnomalyGroup, "styles", [sharedStyles, styles$51]), _AnalysisAnomalyGroup);
__decorate([n$1({
type: Array,
attribute: "comparison-windows"
@@ -20591,7 +22190,7 @@
customElements.define("analysis-anomaly-group", AnalysisAnomalyGroup);
//#endregion
//#region custom_components/hass_datapoints/src/molecules/analysis-delta-group/analysis-delta-group.styles.ts
- var styles$48 = i$5`
+ var styles$49 = i$5`
.help-text {
display: inline-block;
color: var(--secondary-text-color);
@@ -20654,7 +22253,7 @@
`;
}
- }, _defineProperty(_AnalysisDeltaGroup, "styles", [sharedStyles, styles$48]), _AnalysisDeltaGroup);
+ }, _defineProperty(_AnalysisDeltaGroup, "styles", [sharedStyles, styles$49]), _AnalysisDeltaGroup);
__decorate([n$1({
type: Boolean,
attribute: "can-show-delta-analysis"
@@ -20663,14 +22262,14 @@
customElements.define("analysis-delta-group", AnalysisDeltaGroup);
//#endregion
//#region custom_components/hass_datapoints/src/molecules/sidebar-options/sections/sidebar-datapoints-section.styles.ts
- var styles$47 = i$5`
+ var styles$48 = i$5`
:host {
display: block;
}
`;
//#endregion
//#region custom_components/hass_datapoints/src/atoms/display/sidebar-options-section/sidebar-options-section.styles.ts
- var styles$46 = i$5`
+ var styles$47 = i$5`
:host {
display: block;
--dp-spacing-xs: calc(var(--spacing, 8px) * 0.5);
@@ -20684,7 +22283,7 @@
`;
//#endregion
//#region custom_components/hass_datapoints/src/atoms/display/sidebar-section-header/sidebar-section-header.styles.ts
- var styles$45 = i$5`
+ var styles$46 = i$5`
:host {
display: block;
}
@@ -20748,7 +22347,7 @@
var _title_accessor_storage$1 = /* @__PURE__ */ new WeakMap();
var _subtitle_accessor_storage$1 = /* @__PURE__ */ new WeakMap();
var _collapsible_accessor_storage$5 = /* @__PURE__ */ new WeakMap();
- var _open_accessor_storage$9 = /* @__PURE__ */ new WeakMap();
+ var _open_accessor_storage$10 = /* @__PURE__ */ new WeakMap();
var _i18n_accessor_storage$6 = /* @__PURE__ */ new WeakMap();
var SidebarSectionHeader = class extends i$2 {
constructor(..._args) {
@@ -20756,7 +22355,7 @@
_classPrivateFieldInitSpec(this, _title_accessor_storage$1, "");
_classPrivateFieldInitSpec(this, _subtitle_accessor_storage$1, "");
_classPrivateFieldInitSpec(this, _collapsible_accessor_storage$5, false);
- _classPrivateFieldInitSpec(this, _open_accessor_storage$9, true);
+ _classPrivateFieldInitSpec(this, _open_accessor_storage$10, true);
_classPrivateFieldInitSpec(this, _i18n_accessor_storage$6, DEFAULT_I18N$5);
}
get title() {
@@ -20778,10 +22377,10 @@
_classPrivateFieldSet2(_collapsible_accessor_storage$5, this, value);
}
get open() {
- return _classPrivateFieldGet2(_open_accessor_storage$9, this);
+ return _classPrivateFieldGet2(_open_accessor_storage$10, this);
}
set open(value) {
- _classPrivateFieldSet2(_open_accessor_storage$9, this, value);
+ _classPrivateFieldSet2(_open_accessor_storage$10, this, value);
}
get i18n() {
return _classPrivateFieldGet2(_i18n_accessor_storage$6, this);
@@ -20841,7 +22440,7 @@
`;
}
};
- _defineProperty(SidebarSectionHeader, "styles", styles$45);
+ _defineProperty(SidebarSectionHeader, "styles", styles$46);
__decorate([n$1({ type: String })], SidebarSectionHeader.prototype, "title", null);
__decorate([n$1({ type: String })], SidebarSectionHeader.prototype, "subtitle", null);
__decorate([n$1({ type: Boolean })], SidebarSectionHeader.prototype, "collapsible", null);
@@ -20853,7 +22452,7 @@
var _title_accessor_storage = /* @__PURE__ */ new WeakMap();
var _subtitle_accessor_storage = /* @__PURE__ */ new WeakMap();
var _collapsible_accessor_storage$4 = /* @__PURE__ */ new WeakMap();
- var _open_accessor_storage$8 = /* @__PURE__ */ new WeakMap();
+ var _open_accessor_storage$9 = /* @__PURE__ */ new WeakMap();
var _i18n_accessor_storage$5 = /* @__PURE__ */ new WeakMap();
var SidebarOptionsSection = class extends i$2 {
constructor(..._args) {
@@ -20861,7 +22460,7 @@
_classPrivateFieldInitSpec(this, _title_accessor_storage, "");
_classPrivateFieldInitSpec(this, _subtitle_accessor_storage, "");
_classPrivateFieldInitSpec(this, _collapsible_accessor_storage$4, false);
- _classPrivateFieldInitSpec(this, _open_accessor_storage$8, true);
+ _classPrivateFieldInitSpec(this, _open_accessor_storage$9, true);
_classPrivateFieldInitSpec(this, _i18n_accessor_storage$5, EMPTY_I18N);
}
get title() {
@@ -20883,10 +22482,10 @@
_classPrivateFieldSet2(_collapsible_accessor_storage$4, this, value);
}
get open() {
- return _classPrivateFieldGet2(_open_accessor_storage$8, this);
+ return _classPrivateFieldGet2(_open_accessor_storage$9, this);
}
set open(value) {
- _classPrivateFieldSet2(_open_accessor_storage$8, this, value);
+ _classPrivateFieldSet2(_open_accessor_storage$9, this, value);
}
get i18n() {
return _classPrivateFieldGet2(_i18n_accessor_storage$5, this);
@@ -20919,7 +22518,7 @@
`;
}
};
- _defineProperty(SidebarOptionsSection, "styles", styles$46);
+ _defineProperty(SidebarOptionsSection, "styles", styles$47);
__decorate([n$1({ type: String })], SidebarOptionsSection.prototype, "title", null);
__decorate([n$1({ type: String })], SidebarOptionsSection.prototype, "subtitle", null);
__decorate([n$1({ type: Boolean })], SidebarOptionsSection.prototype, "collapsible", null);
@@ -20928,7 +22527,7 @@
customElements.define("sidebar-options-section", SidebarOptionsSection);
//#endregion
//#region custom_components/hass_datapoints/src/atoms/form/radio-group/radio-group.styles.ts
- var styles$44 = i$5`
+ var styles$45 = i$5`
:host {
display: block;
}
@@ -21014,7 +22613,7 @@
`;
}
};
- _defineProperty(RadioGroup, "styles", styles$44);
+ _defineProperty(RadioGroup, "styles", styles$45);
__decorate([n$1({ type: String })], RadioGroup.prototype, "name", null);
__decorate([n$1({ type: String })], RadioGroup.prototype, "value", null);
__decorate([n$1({ type: Array })], RadioGroup.prototype, "options", null);
@@ -21024,7 +22623,7 @@
var _SidebarDatapointsSection;
var _datapointScope_accessor_storage$3;
var _collapsible_accessor_storage$3;
- var _open_accessor_storage$7;
+ var _open_accessor_storage$8;
var DATAPOINT_SCOPE_OPTIONS = [
{
value: "linked",
@@ -21039,12 +22638,12 @@
label: "Hide datapoints"
}
];
- var SidebarDatapointsSection = (_datapointScope_accessor_storage$3 = /* @__PURE__ */ new WeakMap(), _collapsible_accessor_storage$3 = /* @__PURE__ */ new WeakMap(), _open_accessor_storage$7 = /* @__PURE__ */ new WeakMap(), _SidebarDatapointsSection = class SidebarDatapointsSection extends i$2 {
+ var SidebarDatapointsSection = (_datapointScope_accessor_storage$3 = /* @__PURE__ */ new WeakMap(), _collapsible_accessor_storage$3 = /* @__PURE__ */ new WeakMap(), _open_accessor_storage$8 = /* @__PURE__ */ new WeakMap(), _SidebarDatapointsSection = class SidebarDatapointsSection extends i$2 {
constructor(..._args) {
super(..._args);
_classPrivateFieldInitSpec(this, _datapointScope_accessor_storage$3, "linked");
_classPrivateFieldInitSpec(this, _collapsible_accessor_storage$3, false);
- _classPrivateFieldInitSpec(this, _open_accessor_storage$7, true);
+ _classPrivateFieldInitSpec(this, _open_accessor_storage$8, true);
}
get datapointScope() {
return _classPrivateFieldGet2(_datapointScope_accessor_storage$3, this);
@@ -21059,10 +22658,10 @@
_classPrivateFieldSet2(_collapsible_accessor_storage$3, this, value);
}
get open() {
- return _classPrivateFieldGet2(_open_accessor_storage$7, this);
+ return _classPrivateFieldGet2(_open_accessor_storage$8, this);
}
set open(value) {
- _classPrivateFieldSet2(_open_accessor_storage$7, this, value);
+ _classPrivateFieldSet2(_open_accessor_storage$8, this, value);
}
_onScopeChange(e) {
this.dispatchEvent(new CustomEvent("dp-scope-change", {
@@ -21094,7 +22693,7 @@
`;
}
- }, _defineProperty(_SidebarDatapointsSection, "styles", styles$47), _SidebarDatapointsSection);
+ }, _defineProperty(_SidebarDatapointsSection, "styles", styles$48), _SidebarDatapointsSection);
__decorate([n$1({
type: String,
attribute: "datapoint-scope"
@@ -21105,14 +22704,14 @@
customElements.define("sidebar-datapoints-section", SidebarDatapointsSection);
//#endregion
//#region custom_components/hass_datapoints/src/molecules/sidebar-options/sections/sidebar-datapoint-display-section.styles.ts
- var styles$43 = i$5`
+ var styles$44 = i$5`
:host {
display: block;
}
`;
//#endregion
//#region custom_components/hass_datapoints/src/atoms/form/checkbox-list/checkbox-list.styles.ts
- var styles$42 = i$5`
+ var styles$43 = i$5`
:host {
display: block;
}
@@ -21177,7 +22776,7 @@
`;
}
};
- _defineProperty(CheckboxList, "styles", styles$42);
+ _defineProperty(CheckboxList, "styles", styles$43);
__decorate([n$1({ type: Array })], CheckboxList.prototype, "items", null);
customElements.define("checkbox-list", CheckboxList);
//#endregion
@@ -21186,14 +22785,14 @@
var _showIcons_accessor_storage$2 = /* @__PURE__ */ new WeakMap();
var _showLines_accessor_storage$2 = /* @__PURE__ */ new WeakMap();
var _collapsible_accessor_storage$2 = /* @__PURE__ */ new WeakMap();
- var _open_accessor_storage$6 = /* @__PURE__ */ new WeakMap();
+ var _open_accessor_storage$7 = /* @__PURE__ */ new WeakMap();
var SidebarDatapointDisplaySection = (_SidebarDatapointDisplaySection = class SidebarDatapointDisplaySection extends i$2 {
constructor(..._args) {
super(..._args);
_classPrivateFieldInitSpec(this, _showIcons_accessor_storage$2, true);
_classPrivateFieldInitSpec(this, _showLines_accessor_storage$2, true);
_classPrivateFieldInitSpec(this, _collapsible_accessor_storage$2, false);
- _classPrivateFieldInitSpec(this, _open_accessor_storage$6, true);
+ _classPrivateFieldInitSpec(this, _open_accessor_storage$7, true);
}
get showIcons() {
return _classPrivateFieldGet2(_showIcons_accessor_storage$2, this);
@@ -21214,10 +22813,10 @@
_classPrivateFieldSet2(_collapsible_accessor_storage$2, this, value);
}
get open() {
- return _classPrivateFieldGet2(_open_accessor_storage$6, this);
+ return _classPrivateFieldGet2(_open_accessor_storage$7, this);
}
set open(value) {
- _classPrivateFieldSet2(_open_accessor_storage$6, this, value);
+ _classPrivateFieldSet2(_open_accessor_storage$7, this, value);
}
_onCheckboxChange(e) {
const { value: name, checked } = e.detail;
@@ -21253,7 +22852,7 @@
`;
}
- }, _defineProperty(_SidebarDatapointDisplaySection, "styles", styles$43), _SidebarDatapointDisplaySection);
+ }, _defineProperty(_SidebarDatapointDisplaySection, "styles", styles$44), _SidebarDatapointDisplaySection);
__decorate([n$1({
type: Boolean,
attribute: "show-icons"
@@ -21268,7 +22867,7 @@
customElements.define("sidebar-datapoint-display-section", SidebarDatapointDisplaySection);
//#endregion
//#region custom_components/hass_datapoints/src/molecules/sidebar-options/sections/sidebar-analysis-section.styles.ts
- var styles$41 = i$5`
+ var styles$42 = i$5`
:host {
display: block;
}
@@ -21301,7 +22900,7 @@
var _showCorrelatedAnomalies_accessor_storage$3;
var _anyAnomaliesEnabled_accessor_storage$2;
var _collapsible_accessor_storage$1;
- var _open_accessor_storage$5;
+ var _open_accessor_storage$6;
var ANALYSIS_ANOMALY_OVERLAP_MODE_OPTIONS$1 = [{
value: "all",
label: "Show all anomalies"
@@ -21309,14 +22908,14 @@
value: "only",
label: "Overlaps only"
}];
- var SidebarAnalysisSection = (_anomalyOverlapMode_accessor_storage$2 = /* @__PURE__ */ new WeakMap(), _showCorrelatedAnomalies_accessor_storage$3 = /* @__PURE__ */ new WeakMap(), _anyAnomaliesEnabled_accessor_storage$2 = /* @__PURE__ */ new WeakMap(), _collapsible_accessor_storage$1 = /* @__PURE__ */ new WeakMap(), _open_accessor_storage$5 = /* @__PURE__ */ new WeakMap(), _SidebarAnalysisSection = class SidebarAnalysisSection extends i$2 {
+ var SidebarAnalysisSection = (_anomalyOverlapMode_accessor_storage$2 = /* @__PURE__ */ new WeakMap(), _showCorrelatedAnomalies_accessor_storage$3 = /* @__PURE__ */ new WeakMap(), _anyAnomaliesEnabled_accessor_storage$2 = /* @__PURE__ */ new WeakMap(), _collapsible_accessor_storage$1 = /* @__PURE__ */ new WeakMap(), _open_accessor_storage$6 = /* @__PURE__ */ new WeakMap(), _SidebarAnalysisSection = class SidebarAnalysisSection extends i$2 {
constructor(..._args) {
super(..._args);
_classPrivateFieldInitSpec(this, _anomalyOverlapMode_accessor_storage$2, "all");
_classPrivateFieldInitSpec(this, _showCorrelatedAnomalies_accessor_storage$3, false);
_classPrivateFieldInitSpec(this, _anyAnomaliesEnabled_accessor_storage$2, false);
_classPrivateFieldInitSpec(this, _collapsible_accessor_storage$1, false);
- _classPrivateFieldInitSpec(this, _open_accessor_storage$5, true);
+ _classPrivateFieldInitSpec(this, _open_accessor_storage$6, true);
}
get anomalyOverlapMode() {
return _classPrivateFieldGet2(_anomalyOverlapMode_accessor_storage$2, this);
@@ -21343,10 +22942,10 @@
_classPrivateFieldSet2(_collapsible_accessor_storage$1, this, value);
}
get open() {
- return _classPrivateFieldGet2(_open_accessor_storage$5, this);
+ return _classPrivateFieldGet2(_open_accessor_storage$6, this);
}
set open(value) {
- _classPrivateFieldSet2(_open_accessor_storage$5, this, value);
+ _classPrivateFieldSet2(_open_accessor_storage$6, this, value);
}
_localizedOptions(options) {
return options.map((opt) => ({
@@ -21409,7 +23008,7 @@
`;
}
- }, _defineProperty(_SidebarAnalysisSection, "styles", styles$41), _SidebarAnalysisSection);
+ }, _defineProperty(_SidebarAnalysisSection, "styles", styles$42), _SidebarAnalysisSection);
__decorate([n$1({
type: String,
attribute: "anomaly-overlap-mode"
@@ -21428,7 +23027,7 @@
customElements.define("sidebar-analysis-section", SidebarAnalysisSection);
//#endregion
//#region custom_components/hass_datapoints/src/molecules/sidebar-options/sections/sidebar-chart-display-section.styles.ts
- var styles$40 = i$5`
+ var styles$41 = i$5`
:host {
display: block;
--dp-spacing-sm: var(--spacing, 8px);
@@ -21480,7 +23079,7 @@
var _yAxisMode_accessor_storage$2;
var _hoverSnapMode_accessor_storage$2;
var _collapsible_accessor_storage;
- var _open_accessor_storage$4;
+ var _open_accessor_storage$5;
var DATA_GAP_THRESHOLD_OPTIONS$1 = [
{
value: "auto",
@@ -21540,7 +23139,7 @@
value: "snap_to_data_points",
label: "Snap to data points"
}];
- var SidebarChartDisplaySection = (_showTooltips_accessor_storage$2 = /* @__PURE__ */ new WeakMap(), _showHoverGuides_accessor_storage$2 = /* @__PURE__ */ new WeakMap(), _showDataGaps_accessor_storage$3 = /* @__PURE__ */ new WeakMap(), _dataGapThreshold_accessor_storage$3 = /* @__PURE__ */ new WeakMap(), _yAxisMode_accessor_storage$2 = /* @__PURE__ */ new WeakMap(), _hoverSnapMode_accessor_storage$2 = /* @__PURE__ */ new WeakMap(), _collapsible_accessor_storage = /* @__PURE__ */ new WeakMap(), _open_accessor_storage$4 = /* @__PURE__ */ new WeakMap(), _SidebarChartDisplaySection = class SidebarChartDisplaySection extends i$2 {
+ var SidebarChartDisplaySection = (_showTooltips_accessor_storage$2 = /* @__PURE__ */ new WeakMap(), _showHoverGuides_accessor_storage$2 = /* @__PURE__ */ new WeakMap(), _showDataGaps_accessor_storage$3 = /* @__PURE__ */ new WeakMap(), _dataGapThreshold_accessor_storage$3 = /* @__PURE__ */ new WeakMap(), _yAxisMode_accessor_storage$2 = /* @__PURE__ */ new WeakMap(), _hoverSnapMode_accessor_storage$2 = /* @__PURE__ */ new WeakMap(), _collapsible_accessor_storage = /* @__PURE__ */ new WeakMap(), _open_accessor_storage$5 = /* @__PURE__ */ new WeakMap(), _SidebarChartDisplaySection = class SidebarChartDisplaySection extends i$2 {
constructor(..._args) {
super(..._args);
_classPrivateFieldInitSpec(this, _showTooltips_accessor_storage$2, true);
@@ -21550,7 +23149,7 @@
_classPrivateFieldInitSpec(this, _yAxisMode_accessor_storage$2, "combined");
_classPrivateFieldInitSpec(this, _hoverSnapMode_accessor_storage$2, "follow_series");
_classPrivateFieldInitSpec(this, _collapsible_accessor_storage, false);
- _classPrivateFieldInitSpec(this, _open_accessor_storage$4, true);
+ _classPrivateFieldInitSpec(this, _open_accessor_storage$5, true);
}
get showTooltips() {
return _classPrivateFieldGet2(_showTooltips_accessor_storage$2, this);
@@ -21595,10 +23194,10 @@
_classPrivateFieldSet2(_collapsible_accessor_storage, this, value);
}
get open() {
- return _classPrivateFieldGet2(_open_accessor_storage$4, this);
+ return _classPrivateFieldGet2(_open_accessor_storage$5, this);
}
set open(value) {
- _classPrivateFieldSet2(_open_accessor_storage$4, this, value);
+ _classPrivateFieldSet2(_open_accessor_storage$5, this, value);
}
_localizedOptions(options) {
return options.map((opt) => ({
@@ -21685,7 +23284,7 @@
`;
}
- }, _defineProperty(_SidebarChartDisplaySection, "styles", styles$40), _SidebarChartDisplaySection);
+ }, _defineProperty(_SidebarChartDisplaySection, "styles", styles$41), _SidebarChartDisplaySection);
__decorate([n$1({
type: Boolean,
attribute: "show-tooltips"
@@ -22091,7 +23690,7 @@
`;
}
};
- _defineProperty(HassDatapointsHistoryCardEditor, "styles", [EditorBase.styles, styles$59]);
+ _defineProperty(HassDatapointsHistoryCardEditor, "styles", [EditorBase.styles, styles$60]);
//#endregion
//#region custom_components/hass_datapoints/src/lib/data/preferences-api.ts
/** HA user-data key for the saved history page. Stored via frontend/set_user_data. */
@@ -22889,7 +24488,7 @@
}
//#endregion
//#region custom_components/hass_datapoints/src/molecules/target-row/target-row.styles.ts
- var styles$39 = i$5`
+ var styles$40 = i$5`
:host {
display: block;
--dp-spacing-xs: calc(var(--spacing, 8px) * 0.5);
@@ -23390,6 +24989,287 @@
border-radius: 4px;
}
`;
+ //#endregion
+ //#region custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/analysis-aggregate-group.styles.ts
+ var styles$39 = i$5`
+ .method-list {
+ display: grid;
+ gap: calc(var(--dp-spacing-sm, 8px) * 0.75);
+ }
+
+ .method-item {
+ display: grid;
+ gap: 6px;
+ }
+`;
+ //#endregion
+ //#region custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/analysis-aggregate-group.ts
+ var _AnalysisAggregateGroup;
+ var _deviceClass_accessor_storage;
+ var _variant_accessor_storage;
+ var _operations_accessor_storage;
+ var _eventTypes_accessor_storage;
+ var _open_accessor_storage$4;
+ var CUMULATIVE_OPERATIONS = [
+ "consumption",
+ "transition_count",
+ "state_duration",
+ "event_count"
+ ];
+ var AnalysisAggregateGroup = (_deviceClass_accessor_storage = /* @__PURE__ */ new WeakMap(), _variant_accessor_storage = /* @__PURE__ */ new WeakMap(), _operations_accessor_storage = /* @__PURE__ */ new WeakMap(), _eventTypes_accessor_storage = /* @__PURE__ */ new WeakMap(), _open_accessor_storage$4 = /* @__PURE__ */ new WeakMap(), _AnalysisAggregateGroup = class AnalysisAggregateGroup extends AnalysisGroupMixin(i$2) {
+ constructor(..._args) {
+ super(..._args);
+ _classPrivateFieldInitSpec(this, _deviceClass_accessor_storage, "");
+ _classPrivateFieldInitSpec(this, _variant_accessor_storage, "binary");
+ _classPrivateFieldInitSpec(this, _operations_accessor_storage, []);
+ _classPrivateFieldInitSpec(this, _eventTypes_accessor_storage, []);
+ _classPrivateFieldInitSpec(this, _open_accessor_storage$4, false);
+ }
+ get deviceClass() {
+ return _classPrivateFieldGet2(_deviceClass_accessor_storage, this);
+ }
+ set deviceClass(value) {
+ _classPrivateFieldSet2(_deviceClass_accessor_storage, this, value);
+ }
+ get variant() {
+ return _classPrivateFieldGet2(_variant_accessor_storage, this);
+ }
+ set variant(value) {
+ _classPrivateFieldSet2(_variant_accessor_storage, this, value);
+ }
+ get operations() {
+ return _classPrivateFieldGet2(_operations_accessor_storage, this);
+ }
+ set operations(value) {
+ _classPrivateFieldSet2(_operations_accessor_storage, this, value);
+ }
+ get eventTypes() {
+ return _classPrivateFieldGet2(_eventTypes_accessor_storage, this);
+ }
+ set eventTypes(value) {
+ _classPrivateFieldSet2(_eventTypes_accessor_storage, this, value);
+ }
+ get _open() {
+ return _classPrivateFieldGet2(_open_accessor_storage$4, this);
+ }
+ set _open(value) {
+ _classPrivateFieldSet2(_open_accessor_storage$4, this, value);
+ }
+ get _definitions() {
+ return this.analysis.aggregates ?? [];
+ }
+ _commit(next) {
+ this._emit("aggregates", next);
+ }
+ _choices() {
+ if (this.variant === "event") return [{
+ key: "event_count",
+ label: msg("Count events")
+ }];
+ if (this.variant === "measurement") return NUMERIC_AGGREGATE_OPERATIONS.filter((o) => this.operations.includes(o.value)).map((o) => ({
+ key: o.value,
+ label: msg(o.label)
+ }));
+ return BINARY_AGGREGATE_KINDS.map((k) => ({
+ key: k.value,
+ label: msg(k.label)
+ }));
+ }
+ _keyOf(definition) {
+ if (this.variant === "binary") return getBinaryAggregateKind(definition);
+ return definition.operation;
+ }
+ _definitionFor(key) {
+ return this._definitions.find((d) => this._keyOf(d) === key);
+ }
+ _defaultState() {
+ return getBinaryStateOptions(this.deviceClass)[0].value;
+ }
+ _create(key) {
+ const ids = this._definitions.map((d) => d.id);
+ let created;
+ if (this.variant === "event") created = createEventAggregate(ids);
+ else if (this.variant === "measurement") created = createMeasurementAggregate(ids, key);
+ else created = applyBinaryAggregateKind(createBinaryAggregate(ids, this._defaultState()), key, this._defaultState());
+ return {
+ ...created,
+ id: ids.includes(key) ? created.id : key
+ };
+ }
+ _onGroupChange(e) {
+ const checked = e.detail.checked;
+ this._open = checked;
+ if (!checked) this._commit([]);
+ }
+ _onToggle(key, checked) {
+ this._commit(checked ? [...this._definitions, this._create(key)] : this._definitions.filter((d) => this._keyOf(d) !== key));
+ }
+ _update(id, patch) {
+ this._commit(this._definitions.map((d) => d.id === id ? patch(d) : d));
+ }
+ _selectValue(e) {
+ return e.detail.value;
+ }
+ _renderIntervalField(definition) {
+ return b`
+
+ ${msg("Interval")}
+ this._update(definition.id, (d) => ({
+ ...d,
+ interval: this._selectValue(e)
+ }))}
+ >
+
+ `;
+ }
+ _renderStateField(definition) {
+ const options = getBinaryStateOptions(this.deviceClass).map((o) => ({
+ value: o.value,
+ label: msg(o.label)
+ }));
+ return b`
+
+ ${msg("State")}
+ this._update(definition.id, (d) => ({
+ ...d,
+ states: [this._selectValue(e)]
+ }))}
+ >
+
+ `;
+ }
+ /** Counts, durations and consumption can accumulate; percentages cannot. */
+ _renderCumulativeFields(definition) {
+ if (!CUMULATIVE_OPERATIONS.includes(definition.operation)) return A;
+ const cumulative = definition.mode === "cumulative";
+ return b`
+
+ this._update(definition.id, (d) => ({
+ ...d,
+ mode: e.target.checked ? "cumulative" : "interval"
+ }))}
+ />
+ ${msg("Cumulative")}
+
+ ${cumulative ? b`
+ this._update(definition.id, (d) => ({
+ ...d,
+ resetOnZoom: e.target.checked
+ }))}
+ />
+ ${msg("Reset total to zoomed range")}
+ ` : A}
+ `;
+ }
+ _onEventTypeToggle(definition, type, checked) {
+ const current = definition.eventTypes ?? [];
+ const next = checked ? [.../* @__PURE__ */ new Set([...current, type])] : current.filter((t) => t !== type);
+ this._update(definition.id, (d) => ({
+ ...d,
+ eventTypes: next
+ }));
+ }
+ _renderEventTypeFields(definition) {
+ const selected = definition.eventTypes ?? [];
+ const allTypes = selected.length === 0;
+ return b`
+
+ this._update(definition.id, (d) => ({
+ ...d,
+ eventTypes: e.target.checked ? [] : this.eventTypes.slice(0, 1)
+ }))}
+ />
+ ${msg("All event types")}
+
+ ${allTypes ? A : this.eventTypes.map((type) => b`
+
+ this._onEventTypeToggle(definition, type, e.target.checked)}
+ />
+ ${type}
+
+ `)}
+ `;
+ }
+ _renderSubopts(key, definition) {
+ return b`
+
+ ${this.variant === "binary" && key !== "all_changes" ? this._renderStateField(definition) : A}
+ ${this._renderIntervalField(definition)}
+ ${this.variant === "event" ? this._renderEventTypeFields(definition) : A}
+ ${this._renderCumulativeFields(definition)}
+
+ `;
+ }
+ render() {
+ const choices = this._choices();
+ return b`
+ 0}
+ @dp-group-change=${this._onGroupChange}
+ >
+
+ ${choices.map((choice) => {
+ const definition = this._definitionFor(choice.key);
+ return b`
+
+
+ this._onToggle(choice.key, e.target.checked)}
+ />
+ ${choice.label}
+
+ ${definition ? this._renderSubopts(choice.key, definition) : A}
+
+ `;
+ })}
+
+
+ `;
+ }
+ }, _defineProperty(_AnalysisAggregateGroup, "styles", [sharedStyles, styles$39]), _AnalysisAggregateGroup);
+ __decorate([n$1({
+ type: String,
+ attribute: "device-class"
+ })], AnalysisAggregateGroup.prototype, "deviceClass", null);
+ __decorate([n$1({ type: String })], AnalysisAggregateGroup.prototype, "variant", null);
+ __decorate([n$1({
+ type: Array,
+ attribute: false
+ })], AnalysisAggregateGroup.prototype, "operations", null);
+ __decorate([n$1({
+ type: Array,
+ attribute: false
+ })], AnalysisAggregateGroup.prototype, "eventTypes", null);
+ __decorate([r$2()], AnalysisAggregateGroup.prototype, "_open", null);
+ AnalysisAggregateGroup = __decorate([localized()], AnalysisAggregateGroup);
+ customElements.define("analysis-aggregate-group", AnalysisAggregateGroup);
//#endregion
//#region custom_components/hass_datapoints/src/molecules/target-row/target-row.ts
var _TargetRow;
@@ -23428,11 +25308,14 @@
].map((c) => Math.max(0, Math.min(255, Math.round(c * (1 - mixStrength) + mixTarget * mixStrength))));
return `rgb(${mixed[0]}, ${mixed[1]}, ${mixed[2]})`;
}
+ function hasAggregates(a) {
+ return (a.aggregates?.length ?? 0) > 0;
+ }
function _hasConfiguredAnalysis(a) {
- return a.show_trend_lines || a.show_summary_stats || a.show_rate_of_change || a.show_threshold_analysis || a.show_anomalies || a.show_delta_analysis || a.stepped_series || a.hide_source_series;
+ return a.show_trend_lines || a.show_summary_stats || a.show_rate_of_change || a.show_threshold_analysis || a.show_anomalies || a.show_delta_analysis || a.stepped_series || a.hide_source_series || hasAggregates(a);
}
function _hasActiveAnalysis(a, hasComparisonWindow) {
- return a.show_trend_lines || a.show_summary_stats || a.show_rate_of_change || a.show_threshold_analysis || a.show_anomalies || a.show_delta_analysis && hasComparisonWindow;
+ return a.show_trend_lines || a.show_summary_stats || a.show_rate_of_change || a.show_threshold_analysis || a.show_anomalies || a.show_delta_analysis && hasComparisonWindow || hasAggregates(a);
}
var TargetRow = (_color_accessor_storage$1 = /* @__PURE__ */ new WeakMap(), _visible_accessor_storage$1 = /* @__PURE__ */ new WeakMap(), _analysis_accessor_storage = /* @__PURE__ */ new WeakMap(), _index_accessor_storage = /* @__PURE__ */ new WeakMap(), _entityId_accessor_storage = /* @__PURE__ */ new WeakMap(), _canShowDeltaAnalysis_accessor_storage$2 = /* @__PURE__ */ new WeakMap(), _stateObj_accessor_storage$1 = /* @__PURE__ */ new WeakMap(), _hass_accessor_storage$10 = /* @__PURE__ */ new WeakMap(), _comparisonWindows_accessor_storage$2 = /* @__PURE__ */ new WeakMap(), _computing_accessor_storage = /* @__PURE__ */ new WeakMap(), _computingProgress_accessor_storage = /* @__PURE__ */ new WeakMap(), _computingMethods_accessor_storage = /* @__PURE__ */ new WeakMap(), _rowCount_accessor_storage = /* @__PURE__ */ new WeakMap(), _allAnalysisSame_accessor_storage = /* @__PURE__ */ new WeakMap(), _hideDragHandle_accessor_storage = /* @__PURE__ */ new WeakMap(), _label_accessor_storage$8 = /* @__PURE__ */ new WeakMap(), _binaryDisplay_accessor_storage = /* @__PURE__ */ new WeakMap(), _showBinaryDisplay_accessor_storage$1 = /* @__PURE__ */ new WeakMap(), _TargetRow = class TargetRow extends i$2 {
constructor(..._args) {
@@ -23460,6 +25343,7 @@
_defineProperty(this, "_unit", "");
_defineProperty(this, "_supportsAnalysis", false);
_defineProperty(this, "_isBinary", false);
+ _defineProperty(this, "_isEvent", false);
}
get color() {
return _classPrivateFieldGet2(_color_accessor_storage$1, this);
@@ -23577,6 +25461,7 @@
this._unit = attrs?.unit_of_measurement ?? "";
this._supportsAnalysis = Boolean(this._entityId) && !this._entityId.startsWith("binary_sensor.");
this._isBinary = isBinarySensorEntity(this._entityId);
+ this._isEvent = this._entityId.startsWith("event.");
}
}
_emit(name, detail) {
@@ -23728,7 +25613,7 @@
- ${this._supportsAnalysis && this.analysis?.expanded ? b`
+ ${this._supportsAnalysis && !this._isEvent && this.analysis?.expanded ? b`
${msg("Stepped series")}
+
` : A}
+ ${this._isEvent && expanded ? b`
+
+ ` : A}
${hasBinaryDisplayOption && expanded ? b`
@@ -23831,13 +25740,32 @@
@dp-change=${this._onBinaryDisplayChange}
>
+
+
+
+ this._onCheckbox("hide_source_series", e)}
+ />
+ ${msg("Hide source series")}
+
+
` : A}
`;
}
- }, _defineProperty(_TargetRow, "styles", styles$39), _TargetRow);
+ }, _defineProperty(_TargetRow, "styles", styles$40), _TargetRow);
__decorate([n$1({ type: String })], TargetRow.prototype, "color", null);
__decorate([n$1({ type: Boolean })], TargetRow.prototype, "visible", null);
__decorate([n$1({ type: Object })], TargetRow.prototype, "analysis", null);
diff --git a/custom_components/hass_datapoints/src/cards/history/data/__tests__/statistics-normalization.spec.ts b/custom_components/hass_datapoints/src/cards/history/data/__tests__/statistics-normalization.spec.ts
index 64c71edb..255dd75a 100644
--- a/custom_components/hass_datapoints/src/cards/history/data/__tests__/statistics-normalization.spec.ts
+++ b/custom_components/hass_datapoints/src/cards/history/data/__tests__/statistics-normalization.spec.ts
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import {
mergeNumericHistoryWithStatistics,
+ normalizeStatisticsChanges,
normalizeStatisticsHistory,
} from "../statistics-normalization";
@@ -51,3 +52,34 @@ describe("data/statistics-normalization", () => {
});
});
});
+
+describe("normalizeStatisticsChanges", () => {
+ describe("GIVEN statistics with change, missing change and an ISO start", () => {
+ it("THEN keeps valid changes in time order with start times in ms", () => {
+ expect.assertions(1);
+ expect(
+ normalizeStatisticsChanges("sensor.energy", {
+ "sensor.energy": [
+ { start: 1_700_003_600_000, change: 2 },
+ { start: 1_700_000_000, change: "1.5" },
+ { start: 1_700_007_200_000, change: null },
+ { start: "2023-11-14T22:13:20.000Z", change: 4 },
+ ],
+ })
+ ).toStrictEqual([
+ { time: 1_700_000_000_000, change: 1.5 },
+ { time: 1_700_000_000_000, change: 4 },
+ { time: 1_700_003_600_000, change: 2 },
+ ]);
+ });
+ });
+
+ describe("GIVEN an entity without cumulative statistics", () => {
+ it("THEN returns an empty list", () => {
+ expect.assertions(1);
+ expect(
+ normalizeStatisticsChanges("sensor.none", { "sensor.other": [] })
+ ).toStrictEqual([]);
+ });
+ });
+});
diff --git a/custom_components/hass_datapoints/src/cards/history/data/statistics-normalization.ts b/custom_components/hass_datapoints/src/cards/history/data/statistics-normalization.ts
index e28fe0d0..78e47fb6 100644
--- a/custom_components/hass_datapoints/src/cards/history/data/statistics-normalization.ts
+++ b/custom_components/hass_datapoints/src/cards/history/data/statistics-normalization.ts
@@ -71,3 +71,41 @@ export function mergeNumericHistoryWithStatistics(
merged.sort((a, b) => a.lu - b.lu);
return merged;
}
+
+export interface StatisticsChangeEntry {
+ /** Start of the statistics period, in ms. */
+ time: number;
+ /** Consumption during the period (HA `change`). */
+ change: number;
+}
+
+/**
+ * Per-period consumption from HA cumulative statistics (`change`). Entities
+ * without cumulative statistics yield an empty list.
+ */
+export function normalizeStatisticsChanges(
+ entityId: string,
+ statsData: unknown
+): StatisticsChangeEntry[] {
+ const statEntries =
+ statsData && typeof statsData === "object"
+ ? ((statsData as RecordWithUnknownValues)[entityId] ?? [])
+ : [];
+ return (Array.isArray(statEntries) ? (statEntries as HaStatEntry[]) : [])
+ .flatMap((entry) => {
+ const change = Number(entry?.change);
+ const raw = entry?.start;
+ let time: number;
+ if (typeof raw === "number") {
+ time = raw > 1e11 ? raw : raw * 1000;
+ } else {
+ time = new Date(raw as string).getTime();
+ }
+ return entry?.change == null ||
+ !Number.isFinite(change) ||
+ !Number.isFinite(time)
+ ? []
+ : [{ time: Math.round(time), change }];
+ })
+ .sort((a, b) => a.time - b.time);
+}
diff --git a/custom_components/hass_datapoints/src/cards/history/data/types.ts b/custom_components/hass_datapoints/src/cards/history/data/types.ts
index 126028c9..0c1e757a 100644
--- a/custom_components/hass_datapoints/src/cards/history/data/types.ts
+++ b/custom_components/hass_datapoints/src/cards/history/data/types.ts
@@ -10,12 +10,17 @@ export interface HaStateEntry {
last_changed?: string;
last_updated?: string;
entity_id?: string;
+ /** State attributes; only present when requested (e.g. event entities). */
+ a?: Record;
+ attributes?: Record;
}
/** Normalised statistics entry returned by the HA statistics API. */
export interface HaStatEntry {
start?: number | string;
mean?: Nullable;
+ sum?: Nullable;
+ change?: Nullable;
}
/** Internal intermediate state record used during normalisation. */
diff --git a/custom_components/hass_datapoints/src/cards/history/history-chart/history-chart.styles.ts b/custom_components/hass_datapoints/src/cards/history/history-chart/history-chart.styles.ts
index ed96ba57..e18ff7a8 100644
--- a/custom_components/hass_datapoints/src/cards/history/history-chart/history-chart.styles.ts
+++ b/custom_components/hass_datapoints/src/cards/history/history-chart/history-chart.styles.ts
@@ -412,6 +412,16 @@ export const styles = `
background: color-mix(in srgb, var(--primary-color, #03a9f4) 18%, var(--card-background-color, #fff));
outline: none;
}
+ .aggregate-note {
+ padding: var(--dp-spacing-xs, 4px) var(--dp-spacing-md);
+ padding-left: calc(var(--dp-spacing-md) + var(--dp-chart-axis-left-width, 0px));
+ font-size: 0.74em;
+ color: var(--secondary-text-color);
+ border-top: 1px solid color-mix(in srgb, var(--divider-color, rgba(0, 0, 0, 0.12)) 88%, transparent);
+ }
+ .aggregate-note[hidden] {
+ display: none;
+ }
.legend {
display: flex;
flex-wrap: nowrap;
diff --git a/custom_components/hass_datapoints/src/cards/history/history-chart/history-chart.ts b/custom_components/hass_datapoints/src/cards/history/history-chart/history-chart.ts
index f28a2593..b2156aaf 100644
--- a/custom_components/hass_datapoints/src/cards/history/history-chart/history-chart.ts
+++ b/custom_components/hass_datapoints/src/cards/history/history-chart/history-chart.ts
@@ -10,11 +10,22 @@
*/
import { html, render } from "lit";
+import {
+ buildBinaryAggregateSeries,
+ buildConsumptionAggregateSeries,
+ buildEventAggregateSeries,
+ buildMeasurementAggregateSeries,
+ type AggregateChartSeries,
+} from "@/lib/chart/chart-aggregates";
+import type { AggregateDefinition } from "@/lib/domain/aggregation/types";
import { msg } from "@/lib/i18n/localize";
import {
binaryOffLabel,
binaryOnLabel,
getAxisValueExtent,
+ getHistoryStatesForEntity,
+ normalizeNumericHistory,
+ normalizeStatisticsChanges,
normalizeStatisticsHistory,
SAMPLE_INTERVAL_MS,
} from "../history-data";
@@ -230,6 +241,25 @@ export interface HistoryChartViewModel {
// ── Component ─────────────────────────────────────────────────────────────────
+/**
+ * Aggregate lines are drawn in the source colour; their dash pattern (per
+ * aggregation type) tells them apart. Non-aggregate series get no extra style.
+ */
+function aggregateLineStyle(series: {
+ periodFlags?: unknown;
+ dashPattern?: number[];
+ bars?: boolean;
+}): RecordWithUnknownValues {
+ if (!series.periodFlags || series.bars === true) {
+ return {};
+ }
+ return {
+ aggregateLine: true,
+ dashed: true,
+ dashPattern: series.dashPattern,
+ };
+}
+
export class HistoryChart extends HTMLElement {
// ── No shadow DOM ───────────────────────────────────────────────────────────
// Renders synchronously into its own children so the canvas and legend are
@@ -300,6 +330,7 @@ export class HistoryChart extends HTMLElement {
+
`;
}
@@ -862,6 +893,62 @@ export class HistoryChart extends HTMLElement {
return normalizeStatisticsHistory(entityId, statsData);
}
+ /**
+ * Plain-text explanation of the partial/incomplete markers and of the limits
+ * of retained history, so the indicators do not rely on colour or shape alone.
+ */
+ _renderAggregateNote(series: unknown[]): void {
+ const noteEl = this.querySelector(
+ "#aggregate-note"
+ ) as Nullable;
+ if (!noteEl) {
+ return;
+ }
+ const aggregates = (series as AggregateChartSeries[]).filter(
+ (item) => item.definitionId && item.result
+ );
+ // Only bars carry the dashed/hatched markers; lines report partial and
+ // incomplete periods in their tooltips.
+ const flagged = aggregates.some(
+ (item) =>
+ item.bars &&
+ item.periodFlags?.some((flag) => flag.partial || flag.incomplete)
+ );
+ const hasCounts = aggregates.some((item) =>
+ ["transition_count", "event_count"].includes(
+ item.result.points.length ? this._aggregateOperationOf(item) : ""
+ )
+ );
+ const lines: string[] = [];
+ if (flagged) {
+ lines.push(
+ msg(
+ "Dashed outlines mark partial periods cut by the selected range; hatched or dotted marks show periods with gaps in recorded history."
+ )
+ );
+ }
+ if (hasCounts) {
+ lines.push(
+ msg(
+ "Counts only include activity in retained history; earlier activity is not shown."
+ )
+ );
+ }
+ render(html`${lines.map((line) => html`${line}
`)}`, noteEl);
+ noteEl.hidden = lines.length === 0;
+ }
+
+ _aggregateOperationOf(item: AggregateChartSeries): string {
+ const rows = this._seriesSettings as Array<{
+ entity_id: string;
+ analysis?: { aggregates?: AggregateDefinition[] };
+ }>;
+ const definition = rows
+ .find((row) => row.entity_id === item.sourceEntityId)
+ ?.analysis?.aggregates?.find((d) => d.id === item.definitionId);
+ return definition?.operation ?? "";
+ }
+
/** Render legend items for the given series, binary backgrounds and rows. */
_renderLegend(
series: unknown[],
@@ -886,7 +973,9 @@ export class HistoryChart extends HTMLElement {
this._updateLegendLayout(legendEl);
render(
html`${allItems.map((item) => {
- const hidden = this._hiddenSeries?.has(item.entityId);
+ const hidden = this._hiddenSeries
+ ? this._isSeriesHidden(item as { entityId: string })
+ : false;
return html`
)
+ ?.config?.time_zone;
+ return zone || Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
+ }
+
+ /**
+ * Aggregate series configured on one row, calculated from the given history
+ * and statistics for `range`. Used for the main window and, with a window's
+ * own data and range, for each comparison window.
+ */
+ _buildRowAggregates(args: {
+ entityId: string;
+ label: string;
+ color: string;
+ index: number;
+ definitions: AggregateDefinition[];
+ histResult: unknown;
+ statsResult: unknown;
+ range: { start: number; end: number };
+ zoom?: { start: number; end: number };
+ stateList?: Array<{ lu: number; s: string }>;
+ unit?: string;
+ axisKey?: string;
+ }): AggregateChartSeries[] {
+ const { entityId, definitions, range } = args;
+ if (!definitions.length) {
+ return [];
+ }
+ const domain = entityId.split(".")[0];
+ const timeZone = this._getTimeZone();
+ const entityIds = this._seriesSettings.map(
+ (setting: { entity_id: string }) => setting.entity_id
+ );
+ const attributes = (
+ this._hass as Nullable<{
+ states?: Record;
+ }>
+ )?.states?.[entityId]?.attributes;
+ if (domain === "event") {
+ return buildEventAggregateSeries({
+ entityId,
+ entityLabel: args.label,
+ color: args.color,
+ definitions,
+ states: getHistoryStatesForEntity(args.histResult, entityId, entityIds),
+ range,
+ zoom: args.zoom,
+ timeZone,
+ });
+ }
+ const stateList =
+ args.stateList ??
+ this._buildEntityStateList(entityId, args.histResult, args.statsResult);
+ if (domain === "binary_sensor") {
+ return buildBinaryAggregateSeries({
+ entityId,
+ entityLabel: args.label,
+ color: args.color,
+ deviceClass: String(attributes?.device_class ?? ""),
+ definitions,
+ stateList,
+ range,
+ zoom: args.zoom,
+ timeZone,
+ });
+ }
+ const unit = args.unit ?? "";
+ return [
+ ...buildMeasurementAggregateSeries({
+ entityId,
+ entityLabel: args.label,
+ color: args.color,
+ definitions,
+ stateList,
+ range,
+ zoom: args.zoom,
+ timeZone,
+ unit,
+ axisKey: args.axisKey ?? (unit || "__unitless__"),
+ }),
+ ...buildConsumptionAggregateSeries({
+ entityId,
+ entityLabel: args.label,
+ color: args.color,
+ definitions,
+ changes: normalizeStatisticsChanges(entityId, args.statsResult),
+ readings: normalizeNumericHistory(
+ entityId,
+ getHistoryStatesForEntity(args.histResult, entityId, entityIds)
+ ),
+ stateClass: attributes?.state_class as string | undefined,
+ unit,
+ range,
+ zoom: args.zoom,
+ timeZone,
+ }),
+ ];
+ }
+
_drawSeriesLine(
renderer: unknown,
pts: [number, number][],
@@ -990,6 +1196,32 @@ export class HistoryChart extends HTMLElement {
const gapThresholdMs =
SAMPLE_INTERVAL_MS[gapThresholdKey] ?? 2 * 60 * 60_000;
+ if (opts.bars === true) {
+ (
+ renderer as {
+ drawBars: (
+ pts: [number, number][],
+ color: string,
+ t0: number,
+ t1: number,
+ min: number,
+ max: number,
+ opts: RecordWithUnknownValues
+ ) => void;
+ }
+ ).drawBars(pts, color, t0, t1, min, max, {
+ fillAlpha: (opts.lineOpacity as number | undefined) ?? 0.78,
+ pointStyles: opts.periodFlags,
+ });
+ return;
+ }
+ if (opts.aggregateLine) {
+ // Aggregate lines have one point per calendar period, so the data-gap
+ // splitter (tuned for raw samples) must not break them up.
+ r.drawLine(pts, color, t0, t1, min, max, opts);
+ return;
+ }
+
if (!showGaps || pts.length < 2 || !Number.isFinite(gapThresholdMs)) {
r.drawLine(pts, color, t0, t1, min, max, opts);
return;
@@ -2402,6 +2634,9 @@ export class HistoryChart extends HTMLElement {
axisKey: string;
axis?: unknown;
stepped?: boolean;
+ bars?: boolean;
+ periods?: { start: number; end: number }[];
+ periodFlags?: { partial: boolean; incomplete: boolean }[];
}> = [];
const axes: Array<{
key: string;
@@ -2411,6 +2646,24 @@ export class HistoryChart extends HTMLElement {
values: number[];
}> = [];
const axisMap = new Map();
+ // Aggregate series join the shared axes: bars/consumption get their own
+ // unit axis, measurement lines reuse the source row's axis.
+ const registerAggregateSeries = (aggregate: AggregateChartSeries) => {
+ let aggregateAxis = axisMap.get(aggregate.axisKey);
+ if (!aggregateAxis) {
+ aggregateAxis = {
+ key: aggregate.axisKey,
+ unit: aggregate.unit,
+ color: aggregate.color,
+ side: axisMap.size === 0 ? "left" : "right",
+ values: [],
+ };
+ axisMap.set(aggregate.axisKey, aggregateAxis);
+ axes.push(aggregateAxis);
+ }
+ aggregateAxis.values.push(...aggregate.pts.map(([, v]) => v));
+ series.push(aggregate);
+ };
// Resolved binary sensors (label/colour/labels/display resolved here against
// live hass state) are collected during the series loop, then classified
// into background bands and stacked row lanes by the pure render model.
@@ -2465,15 +2718,59 @@ export class HistoryChart extends HTMLElement {
const binaryDisplay = resolveBinaryDisplayMode(
seriesSetting.binary_display
);
- binaryEntities.push({
+ const binaryAnalysis = (analysisMap.get(entityId) ||
+ normalizeHistorySeriesAnalysis(null)) as SeriesAnalysis;
+ const binaryColor = seriesSetting.color || COLORS[i % COLORS.length];
+ const binaryLabel = entityLabelMap.get(entityId) || entityId;
+ for (const aggregate of this._buildRowAggregates({
entityId,
- label: entityLabelMap.get(entityId) || entityId,
- color: seriesSetting.color || COLORS[i % COLORS.length],
- onLabel: this._binaryOnLabel(entityId),
- offLabel: this._binaryOffLabel(entityId),
- binaryDisplay,
+ label: binaryLabel,
+ color: binaryColor,
+ index: i,
+ definitions: binaryAnalysis.aggregates ?? [],
+ histResult,
+ statsResult,
+ range: { start: t0, end: t1 },
+ zoom: this._zoomRange ?? undefined,
stateList,
- });
+ })) {
+ registerAggregateSeries(aggregate);
+ }
+ if (
+ !this._seriesShouldHideSource(
+ binaryAnalysis,
+ hasSelectedComparisonWindow
+ )
+ ) {
+ binaryEntities.push({
+ entityId,
+ label: binaryLabel,
+ color: binaryColor,
+ onLabel: this._binaryOnLabel(entityId),
+ offLabel: this._binaryOffLabel(entityId),
+ binaryDisplay,
+ stateList,
+ });
+ }
+ return;
+ }
+ if (domain === "event") {
+ const eventAnalysis = (analysisMap.get(entityId) ||
+ normalizeHistorySeriesAnalysis(null)) as SeriesAnalysis;
+ const eventColor = seriesSetting.color || COLORS[i % COLORS.length];
+ for (const aggregate of this._buildRowAggregates({
+ entityId,
+ label: entityLabelMap.get(entityId) || entityId,
+ color: eventColor,
+ index: i,
+ definitions: eventAnalysis.aggregates ?? [],
+ histResult,
+ statsResult,
+ range: { start: t0, end: t1 },
+ zoom: this._zoomRange ?? undefined,
+ })) {
+ registerAggregateSeries(aggregate);
+ }
return;
}
const stateList = this._buildEntityStateList(
@@ -2513,6 +2810,20 @@ export class HistoryChart extends HTMLElement {
if (pts.length) {
const seriesAnalysis =
analysisMap.get(entityId) || normalizeHistorySeriesAnalysis(null);
+ const rowAggregates = this._buildRowAggregates({
+ entityId,
+ label: entityLabelMap.get(entityId) || entityId,
+ color: seriesSetting.color || COLORS[i % COLORS.length],
+ index: i,
+ definitions: (seriesAnalysis as SeriesAnalysis).aggregates ?? [],
+ histResult,
+ statsResult,
+ range: { start: t0, end: t1 },
+ zoom: this._zoomRange ?? undefined,
+ stateList,
+ unit,
+ axisKey,
+ });
series.push({
entityId,
legendEntityId: entityId,
@@ -2523,6 +2834,7 @@ export class HistoryChart extends HTMLElement {
axisKey,
stepped: (seriesAnalysis as SeriesAnalysis).stepped_series === true,
});
+ rowAggregates.forEach(registerAggregateSeries);
}
});
@@ -2738,7 +3050,7 @@ export class HistoryChart extends HTMLElement {
];
const visibleSeries = series.filter(
- (entry) => !this._hiddenSeries.has(entry.legendEntityId || entry.entityId)
+ (entry) => !this._isSeriesHidden(entry)
);
// Row-mode binary sensors that are not toggled off. Hidden rows are dropped
// here so their lane is reclaimed on the next draw (legend toggle → relayout).
@@ -3030,6 +3342,7 @@ export class HistoryChart extends HTMLElement {
zoomOutButton.onclick = () => this._clearZoomRange();
}
this._renderLegend(series, binaryBackgrounds, binaryRows);
+ this._renderAggregateNote(series);
await this._drawSplitChart({
visibleSeries,
binaryBackgrounds,
@@ -3333,12 +3646,17 @@ export class HistoryChart extends HTMLElement {
}
const { min, max } = extent;
const pad = (max - min) * 0.1 || 1;
+ // Bars grow from zero, so a bar-only axis is anchored at 0 (or the
+ // lowest negative value) instead of being padded around the data.
+ const barsOnly = series
+ .filter((entry) => entry.axisKey === axis.key)
+ .every((entry) => entry.bars === true);
return {
key: axis.key,
unit: axis.unit,
color: axis.color,
side: axis.side === "right" ? "right" : "left",
- min: min - pad,
+ min: barsOnly ? Math.min(0, min) : min - pad,
max: max + pad,
};
})
@@ -3888,6 +4206,95 @@ export class HistoryChart extends HTMLElement {
});
}
}
+ // Aggregates of each comparison window are calculated against that
+ // window's own calendar boundaries, then shifted by the window's offset so
+ // they plot over the main range. Tooltips keep the original period dates.
+ for (const win of drawableComparisonResults) {
+ for (let rowIndex = 0; rowIndex < seriesSettings.length; rowIndex += 1) {
+ const seriesSetting = seriesSettings[rowIndex];
+ const entityId = seriesSetting.entity_id;
+ const rowAnalysis = (analysisMap.get(entityId) ||
+ normalizeHistorySeriesAnalysis(null)) as SeriesAnalysis;
+ const baseColor =
+ seriesSetting.color || COLORS[rowIndex % COLORS.length];
+ const comparisonAggregates = this._buildRowAggregates({
+ entityId,
+ label: entityLabelMap.get(entityId) || entityId,
+ color: baseColor,
+ index: rowIndex,
+ definitions: rowAnalysis.aggregates ?? [],
+ histResult: win.histResult,
+ statsResult: win.statsResult || {},
+ range: {
+ start: t0 + win.time_offset_ms,
+ end: t1 + win.time_offset_ms,
+ },
+ });
+ for (const aggregate of comparisonAggregates) {
+ if (this._isSeriesHidden(aggregate)) {
+ continue;
+ }
+ const aggregateAxis = axisLookup.get(aggregate.axisKey);
+ if (!aggregateAxis) {
+ continue;
+ }
+ const shift = (time: number) => time - win.time_offset_ms;
+ const shiftedPts = aggregate.pts.map(
+ ([time, value]): [number, number] => [shift(time), value]
+ );
+ const shiftedPeriods = aggregate.periods.map((period) => ({
+ start: shift(period.start),
+ end: shift(period.end),
+ }));
+ const style = this._getComparisonWindowLineStyle(
+ !!hoveredComparisonWindowId && win.id === hoveredComparisonWindowId,
+ !!selectedComparisonWindowId &&
+ win.id === selectedComparisonWindowId,
+ hoveringDifferentComparison
+ );
+ if (aggregate.bars) {
+ renderer.drawBars(
+ shiftedPts,
+ aggregate.color,
+ renderT0,
+ renderT1,
+ aggregateAxis.min,
+ aggregateAxis.max,
+ { fillAlpha: style.lineOpacity * 0.4, widthFactor: 0.9 }
+ );
+ } else {
+ renderer.drawLine(
+ shiftedPts,
+ aggregate.color,
+ renderT0,
+ renderT1,
+ aggregateAxis.min,
+ aggregateAxis.max,
+ {
+ lineOpacity: style.lineOpacity,
+ lineWidth: style.lineWidth,
+ dashed: true,
+ dashPattern: aggregate.dashPattern,
+ }
+ );
+ }
+ comparisonHoverSeries.push({
+ entityId: `${win.id}:${aggregate.entityId}`,
+ relatedEntityId: aggregate.entityId,
+ label: aggregate.label,
+ windowLabel: win.label || "Date window",
+ unit: aggregate.unit,
+ pts: shiftedPts,
+ periods: shiftedPeriods,
+ labelPeriods: aggregate.periods,
+ periodNotes: aggregate.periodNotes,
+ color: aggregate.color,
+ axis: aggregateAxis,
+ hoverOpacity: style.hoverOpacity,
+ });
+ }
+ }
+ }
this._setAdjustAxisButtonVisibility(
comparisonPreviewActive &&
comparisonOutOfBounds &&
@@ -3920,6 +4327,9 @@ export class HistoryChart extends HTMLElement {
analysisMap.get(s.entityId) ||
normalizeHistorySeriesAnalysis(null)
).stepped_series === true,
+ bars: s.bars === true,
+ periodFlags: (s as { periodFlags?: unknown }).periodFlags,
+ ...aggregateLineStyle(s),
}
);
}
@@ -4298,6 +4708,7 @@ export class HistoryChart extends HTMLElement {
);
this._renderLegend(series, binaryBackgrounds, binaryRows);
+ this._renderAggregateNote(series);
const eventValueMap = new Map(eventHits.map((hit) => [hit.event.id, hit]));
const enrichedEvents: ChartEventRecord[] = chartEvents.map(
@@ -5069,7 +5480,10 @@ export class HistoryChart extends HTMLElement {
((extent as RecordWithNumericValues).max -
(extent as RecordWithNumericValues).min) *
0.1 || 1;
- axisMin = (extent as RecordWithNumericValues).min - pad;
+ axisMin =
+ seriesItem.bars === true
+ ? Math.min(0, (extent as RecordWithNumericValues).min)
+ : (extent as RecordWithNumericValues).min - pad;
axisMax = (extent as RecordWithNumericValues).max + pad;
}
const primaryAxisKey =
@@ -5178,6 +5592,9 @@ export class HistoryChart extends HTMLElement {
lineWidth: comparisonPreviewActive ? 1.25 : 1.75,
lineOpacity: mainSeriesOpacity,
stepped: rowAnalysis.stepped_series === true,
+ bars: seriesItem.bars === true,
+ periodFlags: seriesItem.periodFlags,
+ ...aggregateLineStyle(seriesItem),
}
);
}
diff --git a/custom_components/hass_datapoints/src/cards/history/history-chart/stories/history-chart.stories.ts b/custom_components/hass_datapoints/src/cards/history/history-chart/stories/history-chart.stories.ts
index 10f7df88..660aac2f 100644
--- a/custom_components/hass_datapoints/src/cards/history/history-chart/stories/history-chart.stories.ts
+++ b/custom_components/hass_datapoints/src/cards/history/history-chart/stories/history-chart.stories.ts
@@ -14,13 +14,19 @@
* HA connection.
*/
import type { Meta, StoryObj } from "@storybook/web-components-vite";
-import { expect, userEvent, waitFor } from "storybook/test";
+import { expect, userEvent, waitFor as baseWaitFor } from "storybook/test";
import { createMockHass } from "@/test-support/mock-hass";
import "../history-chart";
+// The chart draws asynchronously (analysis, downsampling, layout); under a full
+// parallel storybook run the default 1s window is too tight.
+const waitFor = (callback: () => T | Promise) =>
+ baseWaitFor(callback, { timeout: 5000 });
+
type DrawableChart = HTMLElement & {
hass: unknown;
draw(model: Record): void;
+ applyViewState(model: Record): void;
};
type BinaryState = { lu: number; s: string };
@@ -324,3 +330,326 @@ export const SplitViewRow: Story = {
await waitFor(() => expect(laneRegion()).toBeNull());
},
};
+
+// ── (d) Binary aggregates as bars ───────────────────────────────────────────────
+const doorToggles = binaryHistory([
+ { at: 0, s: "off" },
+ { at: 0.2, s: "on" },
+ { at: 0.3, s: "off" },
+ { at: 0.6, s: "on" },
+ { at: 0.7, s: "off" },
+]);
+
+const dailyOpens = {
+ id: "opens",
+ operation: "transition_count",
+ interval: "day",
+ mode: "interval",
+ states: ["on"],
+ resetOnZoom: false,
+};
+
+/**
+ * A door sensor with a daily "changes to open" aggregate. The aggregate appears
+ * as a bar series in the legend alongside the source band.
+ */
+export const BinaryAggregateBars: Story = {
+ render: () => makeChart(),
+ play: async ({ canvasElement }) => {
+ const el = canvasElement.querySelector(
+ "hass-datapoints-history-chart"
+ ) as DrawableChart;
+ el.draw(
+ drawArgs(
+ {
+ series_settings: [
+ {
+ entity_id: "binary_sensor.front_door",
+ analysis: { aggregates: [dailyOpens] },
+ },
+ ],
+ },
+ { "binary_sensor.front_door": doorToggles }
+ )
+ );
+
+ await waitFor(() =>
+ expect(legendLabels(el)).toEqual([
+ "Front Door · Daily · Count changes to a state (Open)",
+ "Front Door",
+ ])
+ );
+ // The 24h window cuts both days short, so the partial-period explanation
+ // and the retained-history limit are shown as text, not only as styling.
+ const note = el.querySelector("#aggregate-note") as HTMLElement;
+ await waitFor(() => {
+ expect(note.hidden).toBe(false);
+ expect(note.textContent).toContain("partial periods");
+ expect(note.textContent).toContain("retained history");
+ });
+ },
+};
+
+/** With Hide source series on, only the aggregate remains in the legend. */
+export const BinaryAggregateHidesSource: Story = {
+ render: () => makeChart(),
+ play: async ({ canvasElement }) => {
+ const el = canvasElement.querySelector(
+ "hass-datapoints-history-chart"
+ ) as DrawableChart;
+ el.draw(
+ drawArgs(
+ {
+ series_settings: [
+ {
+ entity_id: "binary_sensor.front_door",
+ analysis: {
+ aggregates: [dailyOpens],
+ hide_source_series: true,
+ },
+ },
+ ],
+ },
+ { "binary_sensor.front_door": doorToggles }
+ )
+ );
+
+ await waitFor(() =>
+ expect(legendLabels(el)).toEqual([
+ "Front Door · Daily · Count changes to a state (Open)",
+ ])
+ );
+ },
+};
+
+// ── (e) Event counts ─────────────────────────────────────────────────────────────
+/** An event entity's presses are counted per day as bars, by selected type. */
+export const EventCountBars: Story = {
+ render: () => makeChart(),
+ play: async ({ canvasElement }) => {
+ const el = canvasElement.querySelector(
+ "hass-datapoints-history-chart"
+ ) as DrawableChart;
+ const press = (at: number, type: string) => ({
+ lu: SEC(at),
+ s: new Date(SEC(at) * 1000).toISOString(),
+ a: { event_type: type },
+ });
+ el.draw(
+ drawArgs(
+ {
+ series_settings: [
+ {
+ entity_id: "event.button",
+ analysis: {
+ aggregates: [
+ {
+ id: "presses",
+ operation: "event_count",
+ interval: "hour",
+ mode: "interval",
+ eventTypes: ["double_press"],
+ resetOnZoom: false,
+ },
+ ],
+ },
+ },
+ ],
+ },
+ {
+ "event.button": [
+ press(0.2, "single_press"),
+ press(0.4, "double_press"),
+ press(0.6, "double_press"),
+ ],
+ }
+ )
+ );
+
+ await waitFor(() =>
+ expect(legendLabels(el)).toEqual(["event.button · Hourly · double_press"])
+ );
+ },
+};
+
+// ── (f) Comparison window aggregates ────────────────────────────────────────────
+/** A comparison window aggregates its own history and overlays the main bars. */
+export const BinaryAggregateComparison: Story = {
+ render: () => makeChart(),
+ play: async ({ canvasElement }) => {
+ const el = canvasElement.querySelector(
+ "hass-datapoints-history-chart"
+ ) as DrawableChart;
+ const week = 7 * 24 * 3600 * 1000;
+ const args = drawArgs(
+ {
+ comparison_windows: [
+ { id: "last-week", label: "Last week", time_offset_ms: -week },
+ ],
+ selected_comparison_window_id: "last-week",
+ series_settings: [
+ {
+ entity_id: "binary_sensor.front_door",
+ analysis: { aggregates: [dailyOpens] },
+ },
+ ],
+ },
+ { "binary_sensor.front_door": doorToggles }
+ ) as Record;
+ args.comparisonResults = [
+ {
+ id: "last-week",
+ label: "Last week",
+ time_offset_ms: -week,
+ histResult: {
+ "binary_sensor.front_door": doorToggles.map((state) => ({
+ ...state,
+ lu: state.lu - week / 1000,
+ })),
+ },
+ statsResult: {},
+ },
+ ];
+ el.draw(args);
+
+ await waitFor(() =>
+ expect(legendLabels(el)).toEqual([
+ "Front Door · Daily · Count changes to a state (Open)",
+ "Front Door",
+ ])
+ );
+ },
+};
+
+// ── (g) Mixed units ─────────────────────────────────────────────────────────────
+const mixedSettings = () => [
+ {
+ entity_id: "sensor.temperature",
+ analysis: {
+ aggregates: [
+ {
+ id: "mean",
+ operation: "mean",
+ interval: "hour",
+ mode: "interval",
+ resetOnZoom: false,
+ },
+ ],
+ },
+ },
+ {
+ entity_id: "binary_sensor.front_door",
+ analysis: {
+ aggregates: [
+ dailyOpens,
+ {
+ ...dailyOpens,
+ id: "open-time",
+ operation: "state_duration",
+ },
+ ],
+ },
+ },
+];
+
+const mixedHistory = () => ({
+ "sensor.temperature": numericHistory([
+ { at: 0, v: 19 },
+ { at: 0.5, v: 21 },
+ { at: 1, v: 20 },
+ ]),
+ "binary_sensor.front_door": doorToggles,
+});
+
+/**
+ * Counts, minutes and temperature never share a value axis: each unit gets its
+ * own axis, while the mean line reuses the temperature axis.
+ */
+export const MixedUnitAggregatesCombined: Story = {
+ render: () => makeChart(),
+ play: async ({ canvasElement }) => {
+ const el = canvasElement.querySelector(
+ "hass-datapoints-history-chart"
+ ) as DrawableChart;
+ el.draw(drawArgs({ series_settings: mixedSettings() }, mixedHistory()));
+
+ await waitFor(() => expect(legendLabels(el)).toHaveLength(5));
+ const axisUnits = () =>
+ Array.from(el.querySelectorAll(".chart-axis-unit")).map((node) =>
+ node.textContent?.trim()
+ );
+ // Temperature and its mean share one axis; minutes get their own, so the
+ // two units are never drawn against a single scale.
+ await waitFor(() => {
+ expect(axisUnits()).toContain("min");
+ expect(axisUnits().filter((unit) => unit === "min")).toHaveLength(1);
+ expect(axisUnits().length).toBeGreaterThanOrEqual(2);
+ });
+
+ // Legend toggles hide an aggregate like any other series.
+ const toggles = () =>
+ Array.from(
+ el.querySelectorAll("#legend .legend-toggle")
+ );
+ const minutesToggle = toggles().find((button) =>
+ button.textContent?.includes("Time in a state")
+ )!;
+ await userEvent.click(minutesToggle);
+ await waitFor(() =>
+ expect(
+ toggles()
+ .find((button) => button.textContent?.includes("Time in a state"))
+ ?.getAttribute("aria-pressed")
+ ).toBe("false")
+ );
+ },
+};
+
+/** In split view each aggregate series gets its own row with its own axis. */
+export const MixedUnitAggregatesSplit: Story = {
+ render: () => makeChart(),
+ play: async ({ canvasElement }) => {
+ const el = canvasElement.querySelector(
+ "hass-datapoints-history-chart"
+ ) as DrawableChart;
+ el.draw(
+ drawArgs(
+ { split_view: true, series_settings: mixedSettings() },
+ mixedHistory()
+ )
+ );
+
+ await waitFor(() => expect(legendLabels(el)).toHaveLength(5));
+ await waitFor(() =>
+ expect(
+ el.querySelectorAll(".split-series-row").length
+ ).toBeGreaterThanOrEqual(4)
+ );
+ },
+};
+
+/** Switching a row off hides its aggregate series along with the source. */
+export const HiddenRowHidesAggregates: Story = {
+ render: () => makeChart(),
+ play: async ({ canvasElement }) => {
+ const el = canvasElement.querySelector(
+ "hass-datapoints-history-chart"
+ ) as DrawableChart;
+ el.applyViewState({ hiddenSeries: new Set(["sensor.temperature"]) });
+ el.draw(drawArgs({ series_settings: mixedSettings() }, mixedHistory()));
+
+ const pressed = (label: string) =>
+ Array.from(
+ el.querySelectorAll("#legend .legend-toggle")
+ )
+ .find((button) => button.textContent?.includes(label))
+ ?.getAttribute("aria-pressed");
+ await waitFor(() => {
+ // The temperature row and its mean aggregate are both off.
+ expect(pressed("Temperature · Hourly · Mean")).toBe("false");
+ expect(pressed("Temperature")).toBe("false");
+ // The door row is untouched.
+ expect(pressed("Time in a state")).toBe("true");
+ });
+ },
+};
diff --git a/custom_components/hass_datapoints/src/cards/history/history-data.ts b/custom_components/hass_datapoints/src/cards/history/history-data.ts
index f213265d..8312147f 100644
--- a/custom_components/hass_datapoints/src/cards/history/history-data.ts
+++ b/custom_components/hass_datapoints/src/cards/history/history-data.ts
@@ -18,6 +18,7 @@ export {
} from "./data/history-normalization";
export {
mergeNumericHistoryWithStatistics,
+ normalizeStatisticsChanges,
normalizeStatisticsHistory,
} from "./data/statistics-normalization";
export { getAxisValueExtent } from "./data/axis-extent";
diff --git a/custom_components/hass_datapoints/src/cards/history/history.ts b/custom_components/hass_datapoints/src/cards/history/history.ts
index af381f14..1aa44583 100644
--- a/custom_components/hass_datapoints/src/cards/history/history.ts
+++ b/custom_components/hass_datapoints/src/cards/history/history.ts
@@ -17,7 +17,7 @@ import {
resolveEntityIdsFromTarget,
} from "@/lib/domain/target-selection";
import { HistoryAnnotationDialogController } from "@/components/annotation-dialog/annotation-dialog";
-import { fetchHistoryDuringPeriod } from "@/lib/data/history-api";
+import { fetchHistoryWithEventAttributes } from "@/lib/data/history-api";
import { fetchStatisticsDuringPeriod } from "@/lib/data/statistics-api";
import { fetchEvents } from "@/lib/data/events-api";
import { logger } from "@/lib/logger";
@@ -580,7 +580,7 @@ export class HassDatapointsHistoryCard extends ChartCardBase {
return cached;
}
- const historyPromise = fetchHistoryDuringPeriod(
+ const historyPromise = fetchHistoryWithEventAttributes(
hass,
start.toISOString(),
end.toISOString(),
@@ -600,7 +600,7 @@ export class HassDatapointsHistoryCard extends ChartCardBase {
this._statisticsEntityIds,
{
period: "hour",
- types: ["mean"],
+ types: ["mean", "change"],
units: {},
}
).catch(() => ({}) as RecordWithUnknownValues)
@@ -1027,7 +1027,7 @@ export class HassDatapointsHistoryCard extends ChartCardBase {
this._setChartLoading(false);
return;
}
- fetchHistoryDuringPeriod(
+ fetchHistoryWithEventAttributes(
hass,
start.toISOString(),
end.toISOString(),
@@ -1063,7 +1063,7 @@ export class HassDatapointsHistoryCard extends ChartCardBase {
this._statisticsEntityIds,
{
period: "hour",
- types: ["mean"],
+ types: ["mean", "change"],
units: {},
}
)
diff --git a/custom_components/hass_datapoints/src/lib/chart/__tests__/chart-aggregates.spec.ts b/custom_components/hass_datapoints/src/lib/chart/__tests__/chart-aggregates.spec.ts
new file mode 100644
index 00000000..95bcc0b1
--- /dev/null
+++ b/custom_components/hass_datapoints/src/lib/chart/__tests__/chart-aggregates.spec.ts
@@ -0,0 +1,515 @@
+import { describe, expect, it } from "vitest";
+import {
+ buildBinaryAggregateSeries,
+ buildConsumptionAggregateSeries,
+ buildEventAggregateSeries,
+ buildMeasurementAggregateSeries,
+} from "@/lib/chart/chart-aggregates";
+import type { AggregateDefinition } from "@/lib/domain/aggregation/types";
+
+const start = Date.parse("2026-03-01T00:00:00Z");
+const hour = 3600000;
+const day = 24 * hour;
+const sec = (ms: number) => ms / 1000;
+
+const definition = (
+ overrides: Partial = {}
+): AggregateDefinition => ({
+ id: "a1",
+ operation: "transition_count",
+ interval: "day",
+ mode: "interval",
+ states: ["on"],
+ resetOnZoom: false,
+ ...overrides,
+});
+
+const build = (
+ definitions: AggregateDefinition[],
+ stateList: { s: string; lu: number }[]
+) =>
+ buildBinaryAggregateSeries({
+ entityId: "binary_sensor.front_door",
+ entityLabel: "Front Door",
+ color: "#ff9800",
+ deviceClass: "door",
+ definitions,
+ stateList,
+ range: { start, end: start + 2 * day },
+ timeZone: "UTC",
+ });
+
+describe("buildBinaryAggregateSeries", () => {
+ describe("GIVEN a door opened three times on day one and never on day two", () => {
+ const stateList = [
+ { s: "off", lu: sec(start) },
+ { s: "on", lu: sec(start + hour) },
+ { s: "off", lu: sec(start + 2 * hour) },
+ { s: "on", lu: sec(start + 3 * hour) },
+ { s: "off", lu: sec(start + 4 * hour) },
+ { s: "on", lu: sec(start + 5 * hour) },
+ { s: "off", lu: sec(start + 6 * hour) },
+ ];
+ describe("WHEN counting daily changes to open", () => {
+ it("THEN draws bars of 3 and 0 at the period centres with a count axis", () => {
+ expect.assertions(5);
+ const [series] = build([definition()], stateList);
+ expect(series.pts).toStrictEqual([
+ [start + day / 2, 3],
+ [start + day + day / 2, 0],
+ ]);
+ expect(series.bars).toBe(true);
+ expect(series.axisKey).toBe("aggregate:count");
+ expect(series.entityId).toBe("aggregate:binary_sensor.front_door:a1");
+ expect(series.label).toBe(
+ "Front Door · Daily · Count changes to a state (Open)"
+ );
+ });
+ });
+ describe("WHEN showing daily time open", () => {
+ it("THEN reports minutes on a minutes axis", () => {
+ expect.assertions(2);
+ const [series] = build(
+ [definition({ operation: "state_duration" })],
+ stateList
+ );
+ expect(series.pts[0][1]).toBe(180);
+ expect(series.unit).toBe("min");
+ });
+ });
+ describe("WHEN two aggregates are configured", () => {
+ it("THEN each gets an independent series", () => {
+ expect.assertions(1);
+ const out = build(
+ [
+ definition(),
+ definition({ id: "a2", operation: "state_percentage" }),
+ ],
+ stateList
+ );
+ expect(out.map((s) => s.definitionId)).toStrictEqual(["a1", "a2"]);
+ });
+ });
+ });
+
+ describe("GIVEN a sensor with no recorded history", () => {
+ describe("WHEN duration is requested", () => {
+ it("THEN no bars are produced rather than zeros", () => {
+ expect.assertions(1);
+ expect(
+ build([definition({ operation: "state_duration" })], [])
+ ).toStrictEqual([]);
+ });
+ });
+ });
+});
+
+describe("buildMeasurementAggregateSeries", () => {
+ const readings = [2, 3, 10, 4, 6].map((v, i) => ({
+ s: String(v),
+ lu: sec(start + (i < 3 ? i : 24 + i) * hour),
+ }));
+ const run = (operation: AggregateDefinition["operation"]) =>
+ buildMeasurementAggregateSeries({
+ entityId: "sensor.temp",
+ entityLabel: "Temperature",
+ color: "#111111",
+ definitions: [
+ definition({ operation, states: undefined, interval: "day" }),
+ ],
+ stateList: readings,
+ range: { start, end: start + 2 * day },
+ timeZone: "UTC",
+ unit: "°C",
+ axisKey: "°C",
+ });
+
+ describe("GIVEN readings 2, 3, 10 on day one and 4, 6 on day two", () => {
+ describe("WHEN showing the daily mean", () => {
+ it("THEN draws a line of arithmetic means on the source axis", () => {
+ expect.assertions(4);
+ const [series] = run("mean");
+ expect(series.pts.map(([, v]) => v)).toStrictEqual([5, 5]);
+ expect(series.bars).toBe(false);
+ expect(series.axisKey).toBe("°C");
+ expect(series.color).toBe("#111111");
+ });
+ });
+ describe("WHEN showing the daily maximum", () => {
+ it("THEN reports the peak of each period", () => {
+ expect.assertions(1);
+ expect(run("max")[0].pts.map(([, v]) => v)).toStrictEqual([10, 6]);
+ });
+ });
+ describe("WHEN a period has no readings", () => {
+ it("THEN no point is produced for it", () => {
+ expect.assertions(1);
+ const out = buildMeasurementAggregateSeries({
+ entityId: "sensor.temp",
+ entityLabel: "Temperature",
+ color: "#111111",
+ definitions: [definition({ operation: "mean", states: undefined })],
+ stateList: [{ s: "5", lu: sec(start + hour) }],
+ range: { start, end: start + 2 * day },
+ timeZone: "UTC",
+ unit: "°C",
+ axisKey: "°C",
+ });
+ expect(out[0].pts).toHaveLength(1);
+ });
+ });
+ });
+});
+
+describe("buildEventAggregateSeries", () => {
+ const state = (type: string, at: number, recordedAt = at) => ({
+ s: new Date(at).toISOString(),
+ lu: sec(recordedAt),
+ a: { event_type: type },
+ });
+ const states = [
+ // Initial snapshot recorded at the start of the range: not an occurrence.
+ state("single_press", start - 5 * hour, start),
+ state("single_press", start + hour),
+ state("double_press", start + 2 * hour),
+ state("single_press", start + day + hour),
+ { s: "unavailable", lu: sec(start + day + 2 * hour) },
+ ];
+ const run = (eventTypes: string[]) =>
+ buildEventAggregateSeries({
+ entityId: "event.button",
+ entityLabel: "Button",
+ color: "#4caf50",
+ definitions: [
+ definition({
+ operation: "event_count",
+ states: undefined,
+ eventTypes,
+ }),
+ ],
+ states,
+ range: { start, end: start + 2 * day },
+ timeZone: "UTC",
+ });
+
+ describe("GIVEN single and double presses with an initial snapshot", () => {
+ describe("WHEN counting all event types", () => {
+ it("THEN occurrences are counted per day and the snapshot is ignored", () => {
+ expect.assertions(2);
+ const [series] = run([]);
+ expect(series.pts.map(([, v]) => v)).toStrictEqual([2, 1]);
+ expect(series.label).toBe("Button · Daily · All event types");
+ });
+ });
+ describe("WHEN selecting only double_press", () => {
+ it("THEN other types are not counted and zero-activity days stay zero", () => {
+ expect.assertions(1);
+ const [series] = run(["double_press"]);
+ expect(series.pts.map(([, v]) => v)).toStrictEqual([1, 0]);
+ });
+ });
+ describe("WHEN the required attribute is absent", () => {
+ it("THEN those states are not counted as events", () => {
+ expect.assertions(1);
+ const out = buildEventAggregateSeries({
+ entityId: "event.button",
+ entityLabel: "Button",
+ color: "#4caf50",
+ definitions: [
+ definition({ operation: "event_count", states: undefined }),
+ ],
+ states: [
+ { s: new Date(start + hour).toISOString(), lu: sec(start + hour) },
+ ],
+ range: { start, end: start + 2 * day },
+ timeZone: "UTC",
+ });
+ expect(out.flatMap((s) => s.pts.map(([, v]) => v))).toStrictEqual([
+ 0, 0,
+ ]);
+ });
+ });
+ });
+});
+
+describe("buildConsumptionAggregateSeries", () => {
+ const consumption = definition({
+ operation: "consumption",
+ states: undefined,
+ });
+ const run = (args: {
+ changes?: { time: number; change: number }[];
+ readings?: { s: string; lu: number }[];
+ stateClass?: string;
+ }) =>
+ buildConsumptionAggregateSeries({
+ entityId: "sensor.energy",
+ entityLabel: "Energy",
+ color: "#ff9800",
+ definitions: [consumption],
+ changes: args.changes ?? [],
+ readings: args.readings ?? [],
+ stateClass: args.stateClass,
+ unit: "kWh",
+ range: { start, end: start + 2 * day },
+ timeZone: "UTC",
+ });
+
+ describe("GIVEN a meter reading 1000 then 1010 then 1012", () => {
+ const readings = [
+ { s: "1000", lu: sec(start) },
+ { s: "1010", lu: sec(start + hour) },
+ { s: "1012", lu: sec(start + day + hour) },
+ ];
+ describe("WHEN showing daily consumption", () => {
+ it("THEN draws consumption bars on their own kWh axis, not the meter reading", () => {
+ expect.assertions(3);
+ const [series] = run({ readings, stateClass: "total_increasing" });
+ expect(series.pts.map(([, v]) => v)).toStrictEqual([10, 2]);
+ expect(series.axisKey).toBe("aggregate:kWh");
+ expect(series.bars).toBe(true);
+ });
+ });
+ });
+
+ describe("GIVEN a meter that resets mid-day", () => {
+ describe("WHEN showing daily consumption", () => {
+ it("THEN counts post-reset usage and notes the reset", () => {
+ expect.assertions(2);
+ const [series] = run({
+ readings: [
+ { s: "100", lu: sec(start) },
+ { s: "110", lu: sec(start + hour) },
+ { s: "5", lu: sec(start + 2 * hour) },
+ ],
+ stateClass: "total_increasing",
+ });
+ expect(series.pts[0][1]).toBe(15);
+ expect(series.periodNotes?.[0]).toBe("Meter reset detected");
+ });
+ });
+ });
+
+ describe("GIVEN statistics for day one and a raw reading after the last statistic", () => {
+ describe("WHEN showing daily consumption", () => {
+ it("THEN statistics cover compiled hours and readings bridge the rest", () => {
+ expect.assertions(1);
+ const [series] = run({
+ changes: [
+ { time: start, change: 1 },
+ { time: start + hour, change: 2 },
+ ],
+ readings: [
+ { s: "50", lu: sec(start + 2 * hour) },
+ { s: "53", lu: sec(start + 3 * hour) },
+ ],
+ stateClass: "total_increasing",
+ });
+ expect(series.pts[0][1]).toBe(6);
+ });
+ });
+ });
+
+ describe("GIVEN no statistics and no readings", () => {
+ describe("WHEN showing consumption", () => {
+ it("THEN no series is produced", () => {
+ expect.assertions(1);
+ expect(run({})).toStrictEqual([]);
+ });
+ });
+ });
+});
+
+describe("cumulative aggregate series", () => {
+ const states = [
+ { s: "off", lu: sec(start) },
+ { s: "on", lu: sec(start + hour) },
+ { s: "off", lu: sec(start + 2 * hour) },
+ { s: "on", lu: sec(start + day + hour) },
+ ];
+ const run = (resetOnZoom: boolean, zoom?: { start: number; end: number }) =>
+ buildBinaryAggregateSeries({
+ entityId: "binary_sensor.front_door",
+ entityLabel: "Front Door",
+ color: "#ff9800",
+ deviceClass: "door",
+ definitions: [definition({ mode: "cumulative", resetOnZoom })],
+ stateList: states,
+ range: { start, end: start + 2 * day },
+ zoom,
+ timeZone: "UTC",
+ });
+
+ describe("GIVEN a cumulative count of openings across two days", () => {
+ describe("WHEN no zoom is applied", () => {
+ it("THEN draws a running total line labelled cumulative", () => {
+ expect.assertions(3);
+ const [series] = run(false);
+ expect(series.pts.map(([, v]) => v)).toStrictEqual([1, 2]);
+ expect(series.bars).toBe(false);
+ expect(series.label).toContain("Cumulative");
+ });
+ });
+ describe("WHEN zoomed to day two without reset", () => {
+ it("THEN the full-range baseline is kept", () => {
+ expect.assertions(1);
+ const [series] = run(false, {
+ start: start + day,
+ end: start + 2 * day,
+ });
+ expect(series.pts.map(([, v]) => v)).toStrictEqual([1, 2]);
+ });
+ });
+ describe("WHEN zoomed to day two with reset enabled", () => {
+ it("THEN accumulation restarts inside the zoom", () => {
+ expect.assertions(1);
+ const [series] = run(true, {
+ start: start + day,
+ end: start + 2 * day,
+ });
+ expect(series.pts.map(([, v]) => v)).toStrictEqual([1]);
+ });
+ });
+ });
+});
+
+describe("aggregates for a comparison window", () => {
+ // London springs forward on 30 March 2025; the window is the week before.
+ const week = 7 * day;
+ const mainStart = Date.parse("2025-04-05T23:00:00Z");
+ const windowStart = mainStart - week;
+ const toggles = [
+ { s: "off", lu: sec(windowStart) },
+ { s: "on", lu: sec(Date.parse("2025-03-29T23:30:00Z")) },
+ { s: "off", lu: sec(Date.parse("2025-03-30T09:00:00Z")) },
+ ];
+ const run = (range: { start: number; end: number }) =>
+ buildBinaryAggregateSeries({
+ entityId: "binary_sensor.front_door",
+ entityLabel: "Front Door",
+ color: "#ff9800",
+ deviceClass: "door",
+ definitions: [
+ definition({ operation: "state_duration", states: ["on"] }),
+ ],
+ stateList: toggles,
+ range,
+ timeZone: "Europe/London",
+ })[0];
+
+ describe("GIVEN a window range spanning a spring-forward day", () => {
+ describe("WHEN calculated against its own calendar", () => {
+ it("THEN the 23-hour local day is a single period with original dates", () => {
+ expect.assertions(3);
+ const series = run({ start: windowStart, end: windowStart + 2 * day });
+ const spring = series.periods.find(
+ (period) => period.start === Date.parse("2025-03-30T00:00:00Z")
+ );
+ expect(spring).toBeDefined();
+ expect(spring!.end - spring!.start).toBe(23 * hour);
+ expect(series.result.points[1].partial).toBe(false);
+ });
+ });
+ });
+
+ describe("GIVEN a window result shifted onto the main range", () => {
+ describe("WHEN subtracting the window offset", () => {
+ it("THEN positions move while the calculated periods keep their dates", () => {
+ expect.assertions(2);
+ const series = run({ start: windowStart, end: windowStart + 2 * day });
+ const offset = -week;
+ const shifted = series.pts.map(([time]) => time - offset);
+ expect(shifted[0]).toBe(series.pts[0][0] + week);
+ expect(series.periods[0].start).toBeLessThan(mainStart);
+ });
+ });
+ });
+});
+
+describe("coverage flags", () => {
+ const stateList = [
+ { s: "off", lu: sec(start + 6 * hour) },
+ { s: "on", lu: sec(start + 8 * hour) },
+ { s: "unavailable", lu: sec(start + 12 * hour) },
+ { s: "on", lu: sec(start + 16 * hour) },
+ ];
+ const run = (range: { start: number; end: number }) =>
+ buildBinaryAggregateSeries({
+ entityId: "binary_sensor.front_door",
+ entityLabel: "Front Door",
+ color: "#ff9800",
+ deviceClass: "door",
+ definitions: [definition({ operation: "state_duration" })],
+ stateList,
+ range,
+ timeZone: "UTC",
+ })[0];
+
+ describe("GIVEN a range cutting the day short and an unavailable gap", () => {
+ describe("WHEN calculated", () => {
+ it("THEN partial and incomplete are flagged separately with tooltip notes", () => {
+ expect.assertions(4);
+ const series = run({ start, end: start + 20 * hour });
+ const [flags] = series.periodFlags!;
+ expect(flags.partial).toBe(true);
+ expect(flags.incomplete).toBe(true);
+ expect(series.periodNotes![0]).toContain("Partial period");
+ expect(series.periodNotes![0]).toContain("Incomplete history");
+ });
+ });
+ describe("WHEN the range covers the whole day with no gaps", () => {
+ it("THEN the period is neither partial nor incomplete", () => {
+ expect.assertions(2);
+ const series = buildBinaryAggregateSeries({
+ entityId: "binary_sensor.front_door",
+ entityLabel: "Front Door",
+ color: "#ff9800",
+ definitions: [definition({ operation: "state_duration" })],
+ stateList: [{ s: "on", lu: sec(start) }],
+ range: { start, end: start + day },
+ timeZone: "UTC",
+ })[0];
+ expect(series.periodFlags![0]).toStrictEqual({
+ partial: false,
+ incomplete: false,
+ });
+ expect(series.periodNotes![0]).toBe("");
+ });
+ });
+ });
+});
+
+describe("aggregate line styling", () => {
+ const readings = [2, 3].map((v, i) => ({
+ s: String(v),
+ lu: sec(start + i * hour),
+ }));
+ const run = (operation: AggregateDefinition["operation"]) =>
+ buildMeasurementAggregateSeries({
+ entityId: "sensor.temp",
+ entityLabel: "Temperature",
+ color: "#ff9800",
+ definitions: [definition({ operation, states: undefined })],
+ stateList: readings,
+ range: { start, end: start + day },
+ timeZone: "UTC",
+ unit: "°C",
+ axisKey: "°C",
+ })[0];
+
+ describe("GIVEN aggregates of different types on the same row", () => {
+ describe("WHEN built", () => {
+ it("THEN they share the source colour but have distinct dash patterns", () => {
+ expect.assertions(2);
+ const ops = ["mean", "min", "max", "median", "first", "last"] as const;
+ const series = ops.map(run);
+ expect(new Set(series.map((s) => s.color))).toStrictEqual(
+ new Set(["#ff9800"])
+ );
+ expect(
+ new Set(series.map((s) => JSON.stringify(s.dashPattern))).size
+ ).toBe(ops.length);
+ });
+ });
+ });
+});
diff --git a/custom_components/hass_datapoints/src/lib/chart/chart-aggregates.ts b/custom_components/hass_datapoints/src/lib/chart/chart-aggregates.ts
new file mode 100644
index 00000000..a699a282
--- /dev/null
+++ b/custom_components/hass_datapoints/src/lib/chart/chart-aggregates.ts
@@ -0,0 +1,539 @@
+import { msg } from "@/lib/i18n/localize";
+import { calculateAggregate } from "@/lib/domain/aggregation/calculate";
+import {
+ AGGREGATE_INTERVAL_OPTIONS,
+ BINARY_AGGREGATE_KINDS,
+ getBinaryAggregateKind,
+ getAggregateDashPattern,
+ getBinaryStateOptions,
+ MEASUREMENT_OPERATIONS,
+ isMeasurementOperation,
+} from "@/lib/domain/aggregation/labels";
+import type { HaStateEntry } from "@/cards/history/data/types";
+import type { StatisticsChangeEntry } from "@/cards/history/data/statistics-normalization";
+import type {
+ AggregateDefinition,
+ AggregateRange,
+ AggregateResult,
+ AggregateSample,
+} from "@/lib/domain/aggregation/types";
+
+const UNKNOWN_STATES = new Set(["unknown", "unavailable"]);
+
+/** A derived series ready to be drawn by the history chart. */
+export interface AggregateChartSeries {
+ entityId: string;
+ legendEntityId: string;
+ sourceEntityId: string;
+ definitionId: string;
+ label: string;
+ unit: string;
+ pts: [number, number][];
+ /** Calendar period behind each point, aligned with `pts`. */
+ periods: { start: number; end: number }[];
+ /** Localized caveats per point (e.g. a detected meter reset), aligned with `pts`. */
+ periodNotes?: string[];
+ /** Per-point coverage: edge-clipped (partial) vs gaps in history (incomplete). */
+ periodFlags?: { partial: boolean; incomplete: boolean }[];
+ /** Line dash pattern for this aggregation type (ignored for bars). */
+ dashPattern: number[];
+ color: string;
+ axisKey: string;
+ bars: boolean;
+ result: AggregateResult;
+}
+
+export interface BuildBinaryAggregateSeriesArgs {
+ entityId: string;
+ entityLabel: string;
+ color: string;
+ deviceClass?: string;
+ definitions: AggregateDefinition[];
+ /** Recorded states; `lu` is the last-updated time in seconds. */
+ stateList: { s: string; lu: number }[];
+ range: AggregateRange;
+ /** Visible zoom window; only used by cumulative series that reset on zoom. */
+ zoom?: AggregateRange;
+ timeZone: string;
+}
+
+/** Append the cumulative marker so the legend tells totals from per-period bars. */
+function withCumulativeSuffix(
+ definition: AggregateDefinition,
+ label: string
+): string {
+ return definition.mode === "cumulative"
+ ? `${label} · ${msg("Cumulative")}`
+ : label;
+}
+
+interface ValuedPoint {
+ partial: boolean;
+ incomplete: boolean;
+ reasons: string[];
+}
+
+/** Coverage flags and tooltip notes for each plotted period. */
+function periodDetails(
+ valued: ValuedPoint[],
+ reasonNotes?: (reasons: string[]) => string
+) {
+ return {
+ periodFlags: valued.map((p) => ({
+ partial: p.partial,
+ incomplete: p.incomplete,
+ })),
+ periodNotes: valued.map((p) =>
+ [
+ p.partial ? msg("Partial period") : "",
+ p.incomplete ? msg("Incomplete history") : "",
+ reasonNotes ? reasonNotes(p.reasons) : "",
+ ]
+ .filter(Boolean)
+ .join(", ")
+ ),
+ };
+}
+
+export function getAggregateSeriesId(
+ entityId: string,
+ definitionId: string
+): string {
+ return `aggregate:${entityId}:${definitionId}`;
+}
+
+/** Spans where a known (not unknown/unavailable) state was recorded. */
+function buildCoverage(
+ samples: AggregateSample[],
+ range: AggregateRange
+): AggregateRange[] {
+ const sorted = [...samples].sort((a, b) => a.time - b.time);
+ const coverage: AggregateRange[] = [];
+ let seed: AggregateSample | undefined;
+ const inRange: AggregateSample[] = [];
+ for (const sample of sorted) {
+ if (sample.time <= range.start) {
+ seed = sample;
+ } else if (sample.time < range.end) {
+ inRange.push(sample);
+ }
+ }
+ const points = seed ? [{ ...seed, time: range.start }, ...inRange] : inRange;
+ points.forEach((point, i) => {
+ if (!point.state || UNKNOWN_STATES.has(point.state)) {
+ return;
+ }
+ const end = i + 1 < points.length ? points[i + 1].time : range.end;
+ if (end > point.time) {
+ coverage.push({ start: point.time, end });
+ }
+ });
+ return coverage;
+}
+
+function describe(definition: AggregateDefinition, stateLabel: string): string {
+ const kind = getBinaryAggregateKind(definition);
+ const kindLabel = msg(
+ BINARY_AGGREGATE_KINDS.find((k) => k.value === kind)?.label ?? kind
+ );
+ const intervalLabel = msg(
+ AGGREGATE_INTERVAL_OPTIONS.find((o) => o.value === definition.interval)
+ ?.label ?? definition.interval
+ );
+ const suffix = kind === "all_changes" ? "" : ` (${stateLabel})`;
+ return `${intervalLabel} · ${kindLabel}${suffix}`;
+}
+
+/**
+ * Calculate every configured aggregate for a binary row and shape the results
+ * as chart series. Periods with no known history have no bar (never zero).
+ */
+export function buildBinaryAggregateSeries(
+ args: BuildBinaryAggregateSeriesArgs
+): AggregateChartSeries[] {
+ const samples: AggregateSample[] = args.stateList.map((entry) => ({
+ time: Math.round(entry.lu * 1000),
+ state: entry.s,
+ }));
+ const coverage = buildCoverage(samples, args.range);
+ const stateOptions = getBinaryStateOptions(args.deviceClass);
+ return args.definitions
+ .filter((definition) => definition.visible !== false)
+ .flatMap((definition) => {
+ const result = calculateAggregate({
+ definition,
+ samples,
+ range: args.range,
+ zoom: args.zoom,
+ timeZone: args.timeZone,
+ coverage,
+ });
+ const valued = result.points.filter(
+ (p): p is typeof p & { value: number } =>
+ typeof p.value === "number" && Number.isFinite(p.value)
+ );
+ const pts = valued.map((p): [number, number] => [
+ (p.start + p.end) / 2,
+ p.value,
+ ]);
+ if (!pts.length) {
+ return [];
+ }
+ const stateValue = definition.states?.[0] ?? stateOptions[0].value;
+ const stateLabel = msg(
+ stateOptions.find((o) => o.value === stateValue)?.label ?? stateValue
+ );
+ const id = getAggregateSeriesId(args.entityId, definition.id);
+ return [
+ {
+ entityId: id,
+ legendEntityId: id,
+ sourceEntityId: args.entityId,
+ definitionId: definition.id,
+ label:
+ definition.label ||
+ withCumulativeSuffix(
+ definition,
+ `${args.entityLabel} · ${describe(definition, stateLabel)}`
+ ),
+ unit: result.unit,
+ pts,
+ periods: valued.map((p) => ({ start: p.start, end: p.end })),
+ ...periodDetails(valued),
+ color: definition.color || args.color,
+ dashPattern: getAggregateDashPattern(definition.operation),
+ axisKey: `aggregate:${result.unit || "count"}`,
+ bars: result.presentation === "bar",
+ result,
+ },
+ ];
+ });
+}
+
+export interface BuildMeasurementAggregateSeriesArgs {
+ entityId: string;
+ entityLabel: string;
+ color: string;
+ definitions: AggregateDefinition[];
+ stateList: { s: string; lu: number }[];
+ range: AggregateRange;
+ /** Visible zoom window; only used by cumulative series that reset on zoom. */
+ zoom?: AggregateRange;
+ timeZone: string;
+ unit: string;
+ /** Value axis shared with the source row (same unit). */
+ axisKey: string;
+}
+
+/** Aggregate numeric history into per-period measurement summaries (lines). */
+export function buildMeasurementAggregateSeries(
+ args: BuildMeasurementAggregateSeriesArgs
+): AggregateChartSeries[] {
+ const samples: AggregateSample[] = args.stateList.flatMap((entry) => {
+ const value = parseFloat(entry.s);
+ return Number.isNaN(value)
+ ? []
+ : [{ time: Math.round(entry.lu * 1000), value }];
+ });
+ if (!samples.length) {
+ return [];
+ }
+ return args.definitions
+ .filter(
+ (definition) =>
+ definition.visible !== false &&
+ isMeasurementOperation(definition.operation)
+ )
+ .flatMap((definition) => {
+ const result = calculateAggregate({
+ definition,
+ samples,
+ range: args.range,
+ zoom: args.zoom,
+ timeZone: args.timeZone,
+ unit: args.unit,
+ coverage: [args.range],
+ });
+ const valued = result.points.filter(
+ (p): p is typeof p & { value: number } =>
+ typeof p.value === "number" && Number.isFinite(p.value)
+ );
+ if (!valued.length) {
+ return [];
+ }
+ const operationLabel = msg(
+ MEASUREMENT_OPERATIONS.find((o) => o.value === definition.operation)
+ ?.label ?? definition.operation
+ );
+ const intervalLabel = msg(
+ AGGREGATE_INTERVAL_OPTIONS.find((o) => o.value === definition.interval)
+ ?.label ?? definition.interval
+ );
+ const id = getAggregateSeriesId(args.entityId, definition.id);
+ return [
+ {
+ entityId: id,
+ legendEntityId: id,
+ sourceEntityId: args.entityId,
+ definitionId: definition.id,
+ label:
+ definition.label ||
+ withCumulativeSuffix(
+ definition,
+ `${args.entityLabel} · ${intervalLabel} · ${operationLabel}`
+ ),
+ unit: args.unit,
+ pts: valued.map((p): [number, number] => [
+ (p.start + p.end) / 2,
+ p.value,
+ ]),
+ periods: valued.map((p) => ({ start: p.start, end: p.end })),
+ ...periodDetails(valued),
+ color: definition.color || args.color,
+ dashPattern: getAggregateDashPattern(definition.operation),
+ axisKey: args.axisKey,
+ bars: result.presentation === "bar",
+ result,
+ },
+ ];
+ });
+}
+
+export interface BuildEventAggregateSeriesArgs {
+ entityId: string;
+ entityLabel: string;
+ color: string;
+ definitions: AggregateDefinition[];
+ /** Raw history states for the event entity, requested with attributes. */
+ states: HaStateEntry[];
+ range: AggregateRange;
+ /** Visible zoom window; only used by cumulative series that reset on zoom. */
+ zoom?: AggregateRange;
+ timeZone: string;
+}
+
+/**
+ * Turn an event entity's recorded states into occurrences. The state value is
+ * the event timestamp and `event_type` is an attribute. States at or before the
+ * range start are the initial snapshot, not events that happened in range.
+ */
+export function extractEventSamples(
+ states: HaStateEntry[],
+ range: AggregateRange
+): AggregateSample[] {
+ return states.flatMap((entry): AggregateSample[] => {
+ const raw = entry.s ?? entry.state;
+ if (raw === undefined || UNKNOWN_STATES.has(raw)) {
+ return [];
+ }
+ const attributes = entry.a ?? entry.attributes;
+ const eventType = attributes?.event_type;
+ if (typeof eventType !== "string" || !eventType) {
+ return [];
+ }
+ const recordedAt =
+ typeof entry.lu === "number"
+ ? entry.lu * 1000
+ : Date.parse(entry.last_updated ?? "");
+ const parsed = Date.parse(raw);
+ const eventTime = Number.isFinite(parsed) ? parsed : recordedAt;
+ if (!Number.isFinite(eventTime)) {
+ return [];
+ }
+ return [
+ {
+ time: Number.isFinite(recordedAt) ? recordedAt : eventTime,
+ eventTime,
+ eventType,
+ boundary: Number.isFinite(recordedAt) && recordedAt <= range.start,
+ },
+ ];
+ });
+}
+
+/** Count event occurrences per calendar period as bars (or cumulative lines). */
+export function buildEventAggregateSeries(
+ args: BuildEventAggregateSeriesArgs
+): AggregateChartSeries[] {
+ if (!args.states.length) {
+ return [];
+ }
+ const samples = extractEventSamples(args.states, args.range);
+ return args.definitions
+ .filter(
+ (definition) =>
+ definition.visible !== false && definition.operation === "event_count"
+ )
+ .flatMap((definition) => {
+ const result = calculateAggregate({
+ definition,
+ samples,
+ range: args.range,
+ zoom: args.zoom,
+ timeZone: args.timeZone,
+ coverage: [args.range],
+ });
+ const valued = result.points.filter(
+ (p): p is typeof p & { value: number } =>
+ typeof p.value === "number" && Number.isFinite(p.value)
+ );
+ if (!valued.length) {
+ return [];
+ }
+ const intervalLabel = msg(
+ AGGREGATE_INTERVAL_OPTIONS.find((o) => o.value === definition.interval)
+ ?.label ?? definition.interval
+ );
+ const typesLabel = definition.eventTypes?.length
+ ? definition.eventTypes.join(", ")
+ : msg("All event types");
+ const id = getAggregateSeriesId(args.entityId, definition.id);
+ return [
+ {
+ entityId: id,
+ legendEntityId: id,
+ sourceEntityId: args.entityId,
+ definitionId: definition.id,
+ label:
+ definition.label ||
+ withCumulativeSuffix(
+ definition,
+ `${args.entityLabel} · ${intervalLabel} · ${typesLabel}`
+ ),
+ unit: result.unit,
+ pts: valued.map((p): [number, number] => [
+ (p.start + p.end) / 2,
+ p.value,
+ ]),
+ periods: valued.map((p) => ({ start: p.start, end: p.end })),
+ ...periodDetails(valued),
+ color: definition.color || args.color,
+ dashPattern: getAggregateDashPattern(definition.operation),
+ axisKey: `aggregate:${result.unit || "count"}`,
+ bars: result.presentation === "bar",
+ result,
+ },
+ ];
+ });
+}
+
+export interface BuildConsumptionAggregateSeriesArgs {
+ entityId: string;
+ entityLabel: string;
+ color: string;
+ definitions: AggregateDefinition[];
+ /** Per-period consumption from HA cumulative statistics (preferred). */
+ changes: StatisticsChangeEntry[];
+ /** Raw meter readings from history; bridges the period statistics lack. */
+ readings: { s: string; lu: number }[];
+ stateClass?: string;
+ unit: string;
+ range: AggregateRange;
+ /** Visible zoom window; only used by cumulative series that reset on zoom. */
+ zoom?: AggregateRange;
+ timeZone: string;
+}
+
+const STATISTICS_PERIOD_MS = 3600000;
+
+function describeConsumptionReasons(reasons: string[]): string {
+ const notes: string[] = [];
+ if (reasons.includes("reset")) {
+ notes.push(msg("Meter reset detected"));
+ }
+ if (reasons.includes("ambiguous-reset")) {
+ notes.push(msg("Ambiguous meter reset, not counted"));
+ }
+ return notes.join(", ");
+}
+
+/**
+ * Consumption per period for total / total_increasing meters. Statistics
+ * `change` is used where HA has compiled it; raw readings cover the rest.
+ */
+export function buildConsumptionAggregateSeries(
+ args: BuildConsumptionAggregateSeriesArgs
+): AggregateChartSeries[] {
+ const readings: AggregateSample[] = args.readings.flatMap((entry) => {
+ const value = parseFloat(entry.s);
+ return Number.isNaN(value)
+ ? []
+ : [{ time: Math.round(entry.lu * 1000), value }];
+ });
+ const changeSamples: AggregateSample[] = args.changes.map((entry) => ({
+ time: entry.time,
+ change: entry.change,
+ }));
+ let samples: AggregateSample[];
+ if (changeSamples.length) {
+ const cutoff =
+ changeSamples[changeSamples.length - 1].time + STATISTICS_PERIOD_MS;
+ const before = readings.filter((r) => r.time < cutoff);
+ const seed = before[before.length - 1];
+ samples = [
+ ...changeSamples,
+ ...(seed ? [seed] : []),
+ ...readings.filter((r) => r.time >= cutoff),
+ ];
+ } else {
+ samples = readings;
+ }
+ if (!samples.length) {
+ return [];
+ }
+ return args.definitions
+ .filter(
+ (definition) =>
+ definition.visible !== false && definition.operation === "consumption"
+ )
+ .flatMap((definition) => {
+ const result = calculateAggregate({
+ definition,
+ samples,
+ range: args.range,
+ zoom: args.zoom,
+ timeZone: args.timeZone,
+ stateClass: args.stateClass,
+ unit: args.unit,
+ coverage: [args.range],
+ });
+ const valued = result.points.filter(
+ (p): p is typeof p & { value: number } =>
+ typeof p.value === "number" && Number.isFinite(p.value)
+ );
+ if (!valued.length) {
+ return [];
+ }
+ const intervalLabel = msg(
+ AGGREGATE_INTERVAL_OPTIONS.find((o) => o.value === definition.interval)
+ ?.label ?? definition.interval
+ );
+ const id = getAggregateSeriesId(args.entityId, definition.id);
+ return [
+ {
+ entityId: id,
+ legendEntityId: id,
+ sourceEntityId: args.entityId,
+ definitionId: definition.id,
+ label:
+ definition.label ||
+ withCumulativeSuffix(
+ definition,
+ `${args.entityLabel} · ${intervalLabel} · ${msg("Consumption")}`
+ ),
+ unit: args.unit,
+ pts: valued.map((p): [number, number] => [
+ (p.start + p.end) / 2,
+ p.value,
+ ]),
+ periods: valued.map((p) => ({ start: p.start, end: p.end })),
+ ...periodDetails(valued, describeConsumptionReasons),
+ color: definition.color || args.color,
+ dashPattern: getAggregateDashPattern(definition.operation),
+ axisKey: `aggregate:${args.unit || "count"}`,
+ bars: result.presentation === "bar",
+ result,
+ },
+ ];
+ });
+}
diff --git a/custom_components/hass_datapoints/src/lib/chart/chart-analysis.ts b/custom_components/hass_datapoints/src/lib/chart/chart-analysis.ts
index d2f30c9e..040f08a0 100644
--- a/custom_components/hass_datapoints/src/lib/chart/chart-analysis.ts
+++ b/custom_components/hass_datapoints/src/lib/chart/chart-analysis.ts
@@ -208,7 +208,8 @@ export function seriesHasActiveAnalysis(
analysis.show_rate_of_change ||
analysis.show_threshold_analysis ||
analysis.show_anomalies ||
- (analysis.show_delta_analysis && hasSelectedComparisonWindow)
+ (analysis.show_delta_analysis && hasSelectedComparisonWindow) ||
+ (analysis.aggregates?.length ?? 0) > 0
);
}
diff --git a/custom_components/hass_datapoints/src/lib/chart/chart-interaction.ts b/custom_components/hass_datapoints/src/lib/chart/chart-interaction.ts
index 7912b6de..7dbbb0d4 100644
--- a/custom_components/hass_datapoints/src/lib/chart/chart-interaction.ts
+++ b/custom_components/hass_datapoints/src/lib/chart/chart-interaction.ts
@@ -147,6 +147,11 @@ export interface HoverSeriesLike {
onLabel?: string;
offLabel?: string;
stepped?: boolean;
+ /** Aggregate series: values are per calendar period, not interpolated. */
+ periods?: Array<{ start: number; end: number }>;
+ /** Original (unshifted) periods shown in tooltips for comparison windows. */
+ labelPeriods?: Array<{ start: number; end: number }>;
+ periodNotes?: string[];
}
export interface HoverValueEntry {
@@ -161,6 +166,8 @@ export interface HoverValueEntry {
color?: Nullable;
opacity?: number;
hasValue: boolean;
+ /** Formatted calendar period for aggregate bars. */
+ periodLabel?: string;
x?: number;
y?: number;
axisSide?: "left" | "right";
@@ -839,7 +846,7 @@ export function hideTooltip(card: ChartInteractionHost): void {
clearAnnotationTooltips(card);
}
-function resolveTooltipSeriesLabel(entry: HoverValueEntry): string {
+function resolveBaseTooltipSeriesLabel(entry: HoverValueEntry): string {
const isSubordinate = entry.grouped === true && entry.rawVisible === true;
const isComparisonDerived =
entry.comparisonDerived === true && entry.grouped === true;
@@ -888,6 +895,11 @@ function resolveTooltipSeriesLabel(entry: HoverValueEntry): string {
return String(entry.label || "");
}
+function resolveTooltipSeriesLabel(entry: HoverValueEntry): string {
+ const label = resolveBaseTooltipSeriesLabel(entry);
+ return entry.periodLabel ? `${label} (${entry.periodLabel})` : label;
+}
+
export function showLineChartTooltip(
card: ChartInteractionHost,
hover: HoverState,
@@ -1732,18 +1744,45 @@ export function attachLineChartHover(
);
const x = renderer.xOf(timeMs, t0, t1);
- const values = resolvedSeries.map((seriesItem) => {
- const value = renderer._interpolateValue(
- seriesItem.pts || [],
- timeMs,
- seriesItem.stepped
+ // Aggregate series hold one value per calendar period, so they are looked
+ // up by period rather than interpolated between points.
+ const resolveSeriesValue = (
+ seriesItem: HoverSeriesLike
+ ): { value: Nullable; periodLabel?: string } => {
+ if (!seriesItem.periods) {
+ return {
+ value: renderer._interpolateValue(
+ seriesItem.pts || [],
+ timeMs,
+ seriesItem.stepped
+ ),
+ };
+ }
+ const index = seriesItem.periods.findIndex(
+ (period) => timeMs >= period.start && timeMs < period.end
);
+ if (index === -1) {
+ return { value: null };
+ }
+ const point = seriesItem.pts?.[index];
+ const { start, end } = (seriesItem.labelPeriods ?? seriesItem.periods)[
+ index
+ ];
+ const note = seriesItem.periodNotes?.[index];
+ return {
+ value: point ? point[1] : null,
+ periodLabel: `${formatTooltipDateTimeFromMs(start)} – ${formatTooltipDateTimeFromMs(end)}${note ? ` · ${note}` : ""}`,
+ };
+ };
+
+ const values = resolvedSeries.map((seriesItem) => {
+ const { value, periodLabel } = resolveSeriesValue(seriesItem);
const axis = resolveHoverAxis(seriesItem);
return buildHoverValueEntry(
seriesItem,
value,
axis,
- {},
+ periodLabel ? { periodLabel } : {},
{
includePosition: value != null,
x,
@@ -1752,17 +1791,13 @@ export function attachLineChartHover(
});
const comparisonValues = comparisonSeries.map((seriesItem) => {
- const value = renderer._interpolateValue(
- seriesItem.pts || [],
- timeMs,
- seriesItem.stepped
- );
+ const { value, periodLabel } = resolveSeriesValue(seriesItem);
const axis = resolveHoverAxis(seriesItem);
return buildHoverValueEntry(
seriesItem,
value,
axis,
- { comparison: true },
+ { comparison: true, ...(periodLabel ? { periodLabel } : {}) },
{ includePosition: value != null, x }
);
});
diff --git a/custom_components/hass_datapoints/src/lib/chart/chart-renderer.ts b/custom_components/hass_datapoints/src/lib/chart/chart-renderer.ts
index eaa95997..5b56ded5 100644
--- a/custom_components/hass_datapoints/src/lib/chart/chart-renderer.ts
+++ b/custom_components/hass_datapoints/src/lib/chart/chart-renderer.ts
@@ -81,9 +81,14 @@ type DrawLineOptions = {
lineWidth?: number;
};
+/** Coverage of one aggregate period: clipped by the range edge, or with gaps in history. */
+export type PeriodStyle = { partial: boolean; incomplete: boolean };
+
type DrawBarsOptions = {
fillAlpha?: number;
widthFactor?: number;
+ /** Aligned with the points; partial bars are dashed, incomplete bars hatched. */
+ pointStyles?: PeriodStyle[];
};
type DrawAnnotationsOptions = {
@@ -850,7 +855,36 @@ export class ChartRenderer {
const top = Math.min(y, baselineY);
const height = Math.max(1, Math.abs(baselineY - y));
const left = x - barWidth / 2;
+ const style = options.pointStyles?.[i];
+ if (style?.partial) {
+ ctx.fillStyle = hexToRgba(color, fillAlpha * 0.5);
+ } else {
+ ctx.fillStyle = hexToRgba(color, fillAlpha);
+ }
ctx.fillRect(left, top, barWidth, height);
+ if (style?.incomplete) {
+ ctx.save();
+ ctx.beginPath();
+ ctx.rect(left, top, barWidth, height);
+ ctx.clip();
+ ctx.strokeStyle = color;
+ ctx.lineWidth = 1;
+ ctx.beginPath();
+ for (let d = -height; d < barWidth; d += 5) {
+ ctx.moveTo(left + d, top + height);
+ ctx.lineTo(left + d + height, top);
+ }
+ ctx.stroke();
+ ctx.restore();
+ }
+ if (style?.partial || style?.incomplete) {
+ ctx.save();
+ ctx.setLineDash(style.partial ? [4, 3] : [1, 2]);
+ ctx.strokeStyle = color;
+ ctx.lineWidth = 1;
+ ctx.strokeRect(left + 0.5, top + 0.5, barWidth - 1, height - 1);
+ ctx.restore();
+ }
}
ctx.restore();
}
diff --git a/custom_components/hass_datapoints/src/lib/data/__tests__/history-api.spec.ts b/custom_components/hass_datapoints/src/lib/data/__tests__/history-api.spec.ts
index da75fd26..558523ce 100644
--- a/custom_components/hass_datapoints/src/lib/data/__tests__/history-api.spec.ts
+++ b/custom_components/hass_datapoints/src/lib/data/__tests__/history-api.spec.ts
@@ -5,6 +5,7 @@ import {
fetchDownsampledHistory,
fetchDownsampledHistoryProgressive,
fetchHistoryDuringPeriod,
+ fetchHistoryWithEventAttributes,
} from "@/lib/data/history-api";
describe("history-api", () => {
@@ -353,3 +354,71 @@ describe("history-api", () => {
});
});
});
+
+describe("fetchHistoryWithEventAttributes", () => {
+ const makeHass = () => {
+ const sendMessagePromise = vi.fn(
+ async (message: { entity_ids: string[]; no_attributes: boolean }) =>
+ Object.fromEntries(
+ message.entity_ids.map((id) => [
+ id,
+ [
+ {
+ s: "x",
+ a: message.no_attributes ? undefined : { event_type: "t" },
+ },
+ ],
+ ])
+ )
+ );
+ return {
+ sendMessagePromise,
+ hass: { connection: { sendMessagePromise, sendMessage: vi.fn() } },
+ };
+ };
+
+ describe("GIVEN a mix of event and sensor entities", () => {
+ describe("WHEN history is fetched", () => {
+ it("THEN only event entities are requested with attributes and results merge", async () => {
+ expect.assertions(4);
+ const { hass, sendMessagePromise } = makeHass();
+ const result = await fetchHistoryWithEventAttributes(
+ hass,
+ "2026-01-01T00:00:00.000Z",
+ "2026-01-02T00:00:00.000Z",
+ ["event.button", "sensor.temp"],
+ { include_start_time_state: true, no_attributes: true }
+ );
+ const calls = sendMessagePromise.mock.calls.map(([m]) => m);
+ expect(calls).toHaveLength(2);
+ expect(
+ calls.find((m) => m.entity_ids.includes("event.button"))
+ ?.no_attributes
+ ).toBe(false);
+ expect(
+ calls.find((m) => m.entity_ids.includes("sensor.temp"))?.no_attributes
+ ).toBe(true);
+ expect(Object.keys(result).sort()).toStrictEqual([
+ "event.button",
+ "sensor.temp",
+ ]);
+ });
+ });
+ });
+
+ describe("GIVEN no event entities", () => {
+ describe("WHEN history is fetched", () => {
+ it("THEN a single lean request is made", async () => {
+ expect.assertions(1);
+ const { hass, sendMessagePromise } = makeHass();
+ await fetchHistoryWithEventAttributes(
+ hass,
+ "2026-02-01T00:00:00.000Z",
+ "2026-02-02T00:00:00.000Z",
+ ["sensor.only"]
+ );
+ expect(sendMessagePromise).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+});
diff --git a/custom_components/hass_datapoints/src/lib/data/history-api.ts b/custom_components/hass_datapoints/src/lib/data/history-api.ts
index 5266f6b2..2af75afd 100644
--- a/custom_components/hass_datapoints/src/lib/data/history-api.ts
+++ b/custom_components/hass_datapoints/src/lib/data/history-api.ts
@@ -324,3 +324,48 @@ export async function fetchHistoryDuringPeriod(
}) as Promise
);
}
+
+/**
+ * Like {@link fetchHistoryDuringPeriod}, but keeps state attributes for HA
+ * `event.*` entities (their `event_type` attribute is what event counting
+ * needs). Every other entity keeps the lean `no_attributes` request.
+ */
+export async function fetchHistoryWithEventAttributes(
+ hass: Pick,
+ startTime: string,
+ endTime: string,
+ entityIds: unknown,
+ options: HistoryPeriodOptions = {}
+): Promise> {
+ const ids = normalizeCacheIdList(entityIds);
+ const eventIds = ids.filter((id) => id.startsWith("event."));
+ if (!eventIds.length) {
+ return (await fetchHistoryDuringPeriod(
+ hass,
+ startTime,
+ endTime,
+ ids,
+ options
+ )) as Record;
+ }
+ const otherIds = ids.filter((id) => !id.startsWith("event."));
+ const [others, events] = await Promise.all([
+ otherIds.length
+ ? fetchHistoryDuringPeriod>(
+ hass,
+ startTime,
+ endTime,
+ otherIds,
+ options
+ )
+ : Promise.resolve({} as Record),
+ fetchHistoryDuringPeriod>(
+ hass,
+ startTime,
+ endTime,
+ eventIds,
+ { ...options, no_attributes: false }
+ ),
+ ]);
+ return { ...others, ...events };
+}
diff --git a/custom_components/hass_datapoints/src/lib/domain/__tests__/series-rows.spec.ts b/custom_components/hass_datapoints/src/lib/domain/__tests__/series-rows.spec.ts
index e4706b06..0961ea29 100644
--- a/custom_components/hass_datapoints/src/lib/domain/__tests__/series-rows.spec.ts
+++ b/custom_components/hass_datapoints/src/lib/domain/__tests__/series-rows.spec.ts
@@ -430,3 +430,45 @@ describe("series-rows", () => {
});
});
});
+
+describe("series-rows aggregates", () => {
+ const aggregates = [
+ {
+ id: "a1",
+ operation: "transition_count",
+ interval: "day",
+ mode: "interval",
+ states: ["on"],
+ resetOnZoom: false,
+ },
+ ];
+
+ describe("GIVEN a row analysis", () => {
+ describe("WHEN the aggregates key is set", () => {
+ it("THEN the definitions are stored and survive row normalization", () => {
+ expect.assertions(2);
+ const next = computeNextAnalysis(
+ normalizeHistorySeriesAnalysis(null),
+ "aggregates",
+ aggregates
+ );
+ expect(next?.aggregates).toStrictEqual(aggregates);
+ expect(
+ normalizeHistorySeriesAnalysis(JSON.parse(JSON.stringify(next)))
+ .aggregates
+ ).toStrictEqual(aggregates);
+ });
+ });
+ });
+
+ describe("GIVEN a legacy analysis without aggregates", () => {
+ describe("WHEN normalized", () => {
+ it("THEN no aggregates key is introduced", () => {
+ expect.assertions(1);
+ expect(normalizeHistorySeriesAnalysis({})).not.toHaveProperty(
+ "aggregates"
+ );
+ });
+ });
+ });
+});
diff --git a/custom_components/hass_datapoints/src/lib/domain/aggregation/__tests__/aggregation.spec.ts b/custom_components/hass_datapoints/src/lib/domain/aggregation/__tests__/aggregation.spec.ts
new file mode 100644
index 00000000..c40de722
--- /dev/null
+++ b/custom_components/hass_datapoints/src/lib/domain/aggregation/__tests__/aggregation.spec.ts
@@ -0,0 +1,351 @@
+import { describe, expect, it } from "vitest";
+import { calculateAggregate } from "@/lib/domain/aggregation/aggregation";
+import type { AggregateInput } from "@/lib/domain/aggregation/types";
+
+const start = Date.parse("2026-01-01T00:00:00Z");
+const hour = 3600000;
+function input(overrides: Partial = {}): AggregateInput {
+ return {
+ definition: {
+ id: "a",
+ operation: "mean",
+ interval: "hour",
+ mode: "interval",
+ resetOnZoom: false,
+ },
+ samples: [],
+ range: { start, end: start + hour },
+ timeZone: "UTC",
+ coverage: [{ start, end: start + hour }],
+ ...overrides,
+ };
+}
+describe("GIVEN measurement readings 2, 3, and 10", () => {
+ describe("WHEN displaying the hourly average", () => {
+ it("THEN returns their arithmetic mean", () => {
+ expect.assertions(1);
+ expect(
+ calculateAggregate(
+ input({
+ samples: [
+ { time: start, value: 2 },
+ { time: start + 1000, value: 3 },
+ { time: start + 2000, value: 10 },
+ ],
+ })
+ ).points[0].value
+ ).toBe(5);
+ });
+ });
+});
+
+const day = 24 * hour;
+function dayInput(overrides: Partial): AggregateInput {
+ return input({
+ definition: {
+ id: "a",
+ operation: "transition_count",
+ interval: "day",
+ mode: "interval",
+ resetOnZoom: false,
+ states: ["on"],
+ },
+ range: { start, end: start + 2 * day },
+ coverage: [{ start, end: start + 2 * day }],
+ ...overrides,
+ });
+}
+
+describe("GIVEN a door that opens twice with an initial open boundary", () => {
+ describe("WHEN counting transitions into on", () => {
+ it("THEN the boundary state is not counted as a transition", () => {
+ expect.assertions(1);
+ const result = calculateAggregate(
+ dayInput({
+ samples: [
+ { time: start - hour, state: "on", boundary: true },
+ { time: start + hour, state: "off" },
+ { time: start + 2 * hour, state: "on" },
+ { time: start + 3 * hour, state: "off" },
+ { time: start + 4 * hour, state: "on" },
+ ],
+ })
+ );
+ expect(result.points[0].value).toBe(2);
+ });
+ });
+ describe("WHEN counting all state changes", () => {
+ it("THEN both directions are counted", () => {
+ expect.assertions(1);
+ const base = dayInput({});
+ const result = calculateAggregate({
+ ...base,
+ definition: { ...base.definition, states: [] },
+ samples: [
+ { time: start, state: "off" },
+ { time: start + hour, state: "on" },
+ { time: start + 2 * hour, state: "off" },
+ ],
+ });
+ expect(result.points[0].value).toBe(2);
+ });
+ });
+});
+
+describe("GIVEN a sensor that goes unavailable between two on readings", () => {
+ describe("WHEN counting transitions into on", () => {
+ it("THEN recovery from unavailable is not counted", () => {
+ expect.assertions(1);
+ const result = calculateAggregate(
+ dayInput({
+ samples: [
+ { time: start, state: "on" },
+ { time: start + hour, state: "unavailable" },
+ { time: start + 2 * hour, state: "on" },
+ ],
+ })
+ );
+ expect(result.points[0].value).toBe(0);
+ });
+ });
+});
+
+describe("GIVEN a door on for 6 hours then off", () => {
+ const samples = [
+ { time: start, state: "off" },
+ { time: start + 6 * hour, state: "on" },
+ { time: start + 12 * hour, state: "off" },
+ ];
+ const base = dayInput({ samples });
+ describe("WHEN showing duration on", () => {
+ it("THEN returns 360 minutes", () => {
+ expect.assertions(2);
+ const result = calculateAggregate({
+ ...base,
+ definition: { ...base.definition, operation: "state_duration" },
+ });
+ expect(result.points[0].value).toBe(360);
+ expect(result.presentation).toBe("bar");
+ });
+ });
+ describe("WHEN showing percentage on", () => {
+ it("THEN uses known time as the denominator", () => {
+ expect.assertions(1);
+ const result = calculateAggregate({
+ ...base,
+ definition: { ...base.definition, operation: "state_percentage" },
+ coverage: [{ start, end: start + 12 * hour }],
+ });
+ expect(result.points[0].value).toBe(50);
+ });
+ });
+});
+
+describe("GIVEN a day with no recorded history", () => {
+ describe("WHEN counting events", () => {
+ it("THEN the value is null and incomplete, not zero", () => {
+ expect.assertions(2);
+ const base = dayInput({ coverage: [] });
+ const result = calculateAggregate({
+ ...base,
+ definition: { ...base.definition, operation: "event_count" },
+ });
+ expect(result.points[0].value).toBeNull();
+ expect(result.points[0].incomplete).toBe(true);
+ });
+ });
+});
+
+describe("GIVEN a button with single and double presses", () => {
+ const samples = [
+ { time: start + hour, eventType: "single_press" },
+ { time: start + 2 * hour, eventType: "double_press" },
+ { time: start + 3 * hour, eventType: "single_press" },
+ { time: start - hour, eventType: "single_press", boundary: true },
+ ];
+ const base = dayInput({ samples });
+ describe("WHEN selecting single_press", () => {
+ it("THEN only those occurrences are counted", () => {
+ expect.assertions(1);
+ const result = calculateAggregate({
+ ...base,
+ definition: {
+ ...base.definition,
+ operation: "event_count",
+ eventTypes: ["single_press"],
+ },
+ });
+ expect(result.points[0].value).toBe(2);
+ });
+ });
+ describe("WHEN selecting all types cumulatively", () => {
+ it("THEN the running total begins at the first matching event", () => {
+ expect.assertions(2);
+ const result = calculateAggregate({
+ ...base,
+ definition: {
+ ...base.definition,
+ operation: "event_count",
+ eventTypes: [],
+ mode: "cumulative",
+ },
+ samples: [{ time: start + day + hour, eventType: "single_press" }],
+ });
+ expect(result.points.map((p) => p.value)).toEqual([1]);
+ expect(result.presentation).toBe("line");
+ });
+ });
+});
+
+describe("GIVEN an energy meter reading 100, 110, then reset to 5", () => {
+ const samples = [
+ { time: start, value: 100 },
+ { time: start + hour, value: 110 },
+ { time: start + 2 * hour, value: 5 },
+ ];
+ const base = dayInput({ samples, stateClass: "total_increasing" });
+ const definition = { ...base.definition, operation: "consumption" as const };
+ describe("WHEN showing consumption", () => {
+ it("THEN reflects consumption, not summed readings, and flags the reset", () => {
+ expect.assertions(2);
+ const result = calculateAggregate({ ...base, definition });
+ expect(result.points[0].value).toBe(15);
+ expect(result.points[0].reasons).toContain("reset");
+ });
+ });
+ describe("WHEN the state class is unknown", () => {
+ it("THEN a negative change is flagged ambiguous and not guessed", () => {
+ expect.assertions(2);
+ const result = calculateAggregate({
+ ...base,
+ stateClass: undefined,
+ definition,
+ });
+ expect(result.points[0].value).toBe(10);
+ expect(result.points[0].reasons).toContain("ambiguous-reset");
+ });
+ });
+});
+
+describe("GIVEN a bidirectional total meter", () => {
+ describe("WHEN the total decreases", () => {
+ it("THEN the signed change is retained", () => {
+ expect.assertions(1);
+ const base = dayInput({
+ stateClass: "total",
+ samples: [
+ { time: start, value: 50 },
+ { time: start + hour, value: 40 },
+ ],
+ });
+ const result = calculateAggregate({
+ ...base,
+ definition: { ...base.definition, operation: "consumption" },
+ });
+ expect(result.points[0].value).toBe(-10);
+ });
+ });
+});
+
+describe("GIVEN cumulative consumption with a zoomed range", () => {
+ const samples = [
+ { time: start, value: 0 },
+ { time: start + hour, value: 10 },
+ { time: start + day + hour, value: 30 },
+ ];
+ const zoom = { start: start + day, end: start + 2 * day };
+ const base = dayInput({ samples, stateClass: "total_increasing", zoom });
+ describe("WHEN reset on zoom is disabled", () => {
+ it("THEN the full-range baseline is retained", () => {
+ expect.assertions(1);
+ const result = calculateAggregate({
+ ...base,
+ definition: {
+ ...base.definition,
+ operation: "consumption",
+ mode: "cumulative",
+ },
+ });
+ expect(result.points.map((p) => p.value)).toEqual([10, 30]);
+ });
+ });
+ describe("WHEN reset on zoom is enabled", () => {
+ it("THEN accumulation restarts within the zoomed range", () => {
+ expect.assertions(1);
+ const result = calculateAggregate({
+ ...base,
+ definition: {
+ ...base.definition,
+ operation: "consumption",
+ mode: "cumulative",
+ resetOnZoom: true,
+ },
+ });
+ expect(result.points.map((p) => p.value)).toEqual([20]);
+ });
+ });
+});
+
+describe("GIVEN a range ending mid-day", () => {
+ describe("WHEN aggregating by day", () => {
+ it("THEN the edge bucket is partial and ignores later activity", () => {
+ expect.assertions(2);
+ const base = dayInput({
+ range: { start, end: start + 12 * hour },
+ coverage: [{ start, end: start + 12 * hour }],
+ samples: [
+ { time: start + hour, eventType: "press" },
+ { time: start + 13 * hour, eventType: "press" },
+ ],
+ });
+ const result = calculateAggregate({
+ ...base,
+ definition: { ...base.definition, operation: "event_count" },
+ });
+ expect(result.points[0].partial).toBe(true);
+ expect(result.points[0].value).toBe(1);
+ });
+ });
+});
+
+describe("GIVEN readings 1, 2, 3, 9 in one hour", () => {
+ const samples = [1, 2, 3, 9].map((value, i) => ({
+ time: start + i * 1000,
+ value,
+ }));
+ it.each([
+ ["min", 1],
+ ["max", 9],
+ ["median", 2.5],
+ ["first", 1],
+ ["last", 9],
+ ] as const)("THEN %s is %d", (operation, expected) => {
+ expect.assertions(2);
+ const base = input({ samples });
+ const result = calculateAggregate({
+ ...base,
+ definition: { ...base.definition, operation },
+ });
+ expect(result.points[0].value).toBe(expected);
+ expect(result.presentation).toBe("line");
+ });
+});
+
+describe("GIVEN HA statistics reporting hourly consumption", () => {
+ describe("WHEN summing a day", () => {
+ it("THEN uses the statistics change values directly", () => {
+ expect.assertions(1);
+ const base = dayInput({
+ samples: [0.5, 1.5, 2].map((change, i) => ({
+ time: start + i * hour,
+ change,
+ })),
+ });
+ const result = calculateAggregate({
+ ...base,
+ definition: { ...base.definition, operation: "consumption" },
+ });
+ expect(result.points[0].value).toBe(4);
+ });
+ });
+});
diff --git a/custom_components/hass_datapoints/src/lib/domain/aggregation/__tests__/calendar.spec.ts b/custom_components/hass_datapoints/src/lib/domain/aggregation/__tests__/calendar.spec.ts
new file mode 100644
index 00000000..56898cb4
--- /dev/null
+++ b/custom_components/hass_datapoints/src/lib/domain/aggregation/__tests__/calendar.spec.ts
@@ -0,0 +1,137 @@
+import { describe, expect, it } from "vitest";
+import { getPeriods } from "@/lib/domain/aggregation/calendar";
+
+const hours = (n: number) => n * 3600000;
+
+describe("GIVEN a Europe/London spring-forward weekend", () => {
+ describe("WHEN splitting the range into days", () => {
+ it("THEN the 30 March 2025 day is 23 hours long", () => {
+ expect.assertions(2);
+ const periods = getPeriods(
+ {
+ start: Date.parse("2025-03-29T00:00:00Z"),
+ end: Date.parse("2025-03-31T23:00:00Z"),
+ },
+ "day",
+ "Europe/London"
+ );
+ expect(periods).toHaveLength(3);
+ expect(periods[1].periodEnd - periods[1].periodStart).toBe(hours(23));
+ });
+ });
+});
+
+describe("GIVEN a New York autumn fall-back day", () => {
+ describe("WHEN splitting the range into hours", () => {
+ it("THEN the repeated hour yields distinct buckets for a 25-hour day", () => {
+ expect.assertions(2);
+ const start = Date.parse("2025-11-02T04:00:00Z");
+ const periods = getPeriods(
+ { start, end: start + hours(25) },
+ "hour",
+ "America/New_York"
+ );
+ expect(periods).toHaveLength(25);
+ expect(new Set(periods.map((p) => p.start)).size).toBe(25);
+ });
+ });
+});
+
+describe("GIVEN a timezone different from UTC", () => {
+ describe("WHEN splitting into days", () => {
+ it("THEN day boundaries follow the configured timezone", () => {
+ expect.assertions(1);
+ const [first] = getPeriods(
+ {
+ start: Date.parse("2026-01-01T10:00:00Z"),
+ end: Date.parse("2026-01-01T12:00:00Z"),
+ },
+ "day",
+ "Australia/Sydney"
+ );
+ expect(first.periodStart).toBe(Date.parse("2025-12-31T13:00:00Z"));
+ });
+ });
+});
+
+describe("GIVEN a range starting on a Wednesday", () => {
+ describe("WHEN splitting into weeks", () => {
+ it("THEN weeks start on Monday and edge weeks are partial", () => {
+ expect.assertions(3);
+ const periods = getPeriods(
+ {
+ start: Date.parse("2026-01-07T00:00:00Z"),
+ end: Date.parse("2026-01-14T00:00:00Z"),
+ },
+ "week",
+ "UTC"
+ );
+ expect(periods[0].periodStart).toBe(Date.parse("2026-01-05T00:00:00Z"));
+ expect(periods[0].partial).toBe(true);
+ expect(periods).toHaveLength(2);
+ });
+ });
+});
+
+describe("GIVEN a range crossing February", () => {
+ describe("WHEN splitting into months", () => {
+ it("THEN February has 28 days in 2026", () => {
+ expect.assertions(1);
+ const periods = getPeriods(
+ {
+ start: Date.parse("2026-01-31T00:00:00Z"),
+ end: Date.parse("2026-03-02T00:00:00Z"),
+ },
+ "month",
+ "UTC"
+ );
+ expect(periods[1].periodEnd - periods[1].periodStart).toBe(
+ 28 * hours(24)
+ );
+ });
+ });
+});
+
+describe("GIVEN sub-hour intervals", () => {
+ const range = {
+ start: Date.parse("2026-01-01T10:07:30Z"),
+ end: Date.parse("2026-01-01T11:00:00Z"),
+ };
+
+ describe("WHEN splitting into 15 minute periods", () => {
+ it("THEN periods align to :00/:15/:30/:45 and the first is partial", () => {
+ expect.assertions(3);
+ const periods = getPeriods(range, "15min", "UTC");
+ expect(
+ periods.map((p) => new Date(p.periodStart).getUTCMinutes())
+ ).toStrictEqual([0, 15, 30, 45]);
+ expect(periods[0].partial).toBe(true);
+ expect(periods[1].partial).toBe(false);
+ });
+ });
+
+ describe("WHEN splitting into minutes", () => {
+ it("THEN there is one period per started minute", () => {
+ expect.assertions(1);
+ expect(getPeriods(range, "minute", "UTC")).toHaveLength(53);
+ });
+ });
+
+ describe("WHEN splitting into 30 minute periods in a half-hour offset zone", () => {
+ it("THEN boundaries follow local :00/:30", () => {
+ expect.assertions(1);
+ // Kolkata is UTC+5:30, so local :00/:30 are UTC :30/:00.
+ const [first] = getPeriods(
+ {
+ start: Date.parse("2026-01-01T10:10:00Z"),
+ end: Date.parse("2026-01-01T11:00:00Z"),
+ },
+ "30min",
+ "Asia/Kolkata"
+ );
+ expect(new Date(first.periodStart).toISOString()).toBe(
+ "2026-01-01T10:00:00.000Z"
+ );
+ });
+ });
+});
diff --git a/custom_components/hass_datapoints/src/lib/domain/aggregation/aggregation.ts b/custom_components/hass_datapoints/src/lib/domain/aggregation/aggregation.ts
new file mode 100644
index 00000000..73340d3f
--- /dev/null
+++ b/custom_components/hass_datapoints/src/lib/domain/aggregation/aggregation.ts
@@ -0,0 +1,99 @@
+import {
+ AGGREGATE_INTERVALS,
+ AGGREGATE_OPERATIONS,
+ type AggregateDefinition,
+ type AggregateOperation,
+} from "@/lib/domain/aggregation/types";
+
+export function normalizeAggregateDefinitions(
+ value: unknown
+): AggregateDefinition[] {
+ if (!Array.isArray(value)) {
+ return [];
+ }
+ const ids = new Set();
+ return value.flatMap((item) => {
+ if (
+ !item ||
+ typeof item !== "object" ||
+ typeof item.id !== "string" ||
+ !item.id ||
+ ids.has(item.id) ||
+ !AGGREGATE_OPERATIONS.includes(item.operation) ||
+ !AGGREGATE_INTERVALS.includes(item.interval)
+ ) {
+ return [];
+ }
+ ids.add(item.id);
+ const definition: AggregateDefinition = {
+ id: item.id,
+ operation: item.operation,
+ interval: item.interval,
+ mode:
+ item.mode === "cumulative" &&
+ [
+ "consumption",
+ "transition_count",
+ "state_duration",
+ "event_count",
+ ].includes(item.operation)
+ ? "cumulative"
+ : "interval",
+ resetOnZoom: item.resetOnZoom === true,
+ };
+ for (const key of ["states", "eventTypes"] as const) {
+ if (Array.isArray(item[key])) {
+ definition[key] = [
+ ...new Set(
+ item[key].filter(
+ (v: unknown): v is string => typeof v === "string" && v.length > 0
+ )
+ ),
+ ];
+ }
+ }
+ for (const key of ["color", "label"] as const) {
+ if (typeof item[key] === "string") {
+ definition[key] = item[key];
+ }
+ }
+ if (typeof item.visible === "boolean") {
+ definition.visible = item.visible;
+ }
+ return [definition];
+ });
+}
+export function getAggregateOperations(entity: {
+ entity_id: string;
+ state?: string;
+ attributes?: Record;
+}): AggregateOperation[] {
+ if (entity.entity_id.startsWith("event.")) {
+ return ["event_count"];
+ }
+ if (
+ entity.entity_id.startsWith("binary_sensor.") ||
+ (entity.state !== undefined &&
+ !["unknown", "unavailable"].includes(entity.state) &&
+ !Number.isFinite(Number(entity.state)))
+ ) {
+ return ["transition_count", "state_duration", "state_percentage"];
+ }
+ const operations: AggregateOperation[] = [
+ "mean",
+ "min",
+ "max",
+ "median",
+ "first",
+ "last",
+ ];
+ if (
+ ["total", "total_increasing"].includes(
+ String(entity.attributes?.state_class)
+ )
+ ) {
+ operations.push("consumption");
+ }
+ return operations;
+}
+export { calculateAggregate } from "@/lib/domain/aggregation/calculate";
diff --git a/custom_components/hass_datapoints/src/lib/domain/aggregation/calculate.ts b/custom_components/hass_datapoints/src/lib/domain/aggregation/calculate.ts
new file mode 100644
index 00000000..81afb4c9
--- /dev/null
+++ b/custom_components/hass_datapoints/src/lib/domain/aggregation/calculate.ts
@@ -0,0 +1,433 @@
+import { getPeriods } from "@/lib/domain/aggregation/calendar";
+import type {
+ AggregateInput,
+ AggregatePeriod,
+ AggregatePoint,
+ AggregateRange,
+ AggregateResult,
+ AggregateSample,
+} from "@/lib/domain/aggregation/types";
+
+const MINUTE_MS = 60000;
+const UNKNOWN_STATES = new Set(["unknown", "unavailable"]);
+const CUMULATIVE_OPERATIONS = new Set([
+ "consumption",
+ "transition_count",
+ "state_duration",
+ "event_count",
+]);
+
+interface StateSpan extends AggregateRange {
+ state: string;
+}
+
+interface Bucket {
+ value: number | null;
+ reasons: string[];
+}
+
+function isKnownState(state: string | undefined): state is string {
+ return typeof state === "string" && !UNKNOWN_STATES.has(state);
+}
+
+function overlap(a: AggregateRange, b: AggregateRange): number {
+ return Math.max(0, Math.min(a.end, b.end) - Math.max(a.start, b.start));
+}
+
+/**
+ * Bucket time-ordered items into periods in one sweep. Minute-level intervals
+ * over long ranges create tens of thousands of periods, so per-period filters
+ * over every item would be quadratic.
+ */
+function groupByPeriod(
+ items: T[],
+ periods: AggregatePeriod[],
+ timeOf: (item: T) => number
+): T[][] {
+ const sorted = [...items].sort((a, b) => timeOf(a) - timeOf(b));
+ const groups: T[][] = periods.map(() => []);
+ let cursor = 0;
+ sorted.forEach((item) => {
+ const time = timeOf(item);
+ while (cursor < periods.length && time >= periods[cursor].end) {
+ cursor += 1;
+ }
+ if (cursor < periods.length && time >= periods[cursor].start) {
+ groups[cursor].push(item);
+ }
+ });
+ return groups;
+}
+
+function sortSamples(samples: AggregateSample[]): AggregateSample[] {
+ return [...samples].sort((a, b) => a.time - b.time);
+}
+
+/** Known (covered) time within each period, in one sweep over the coverage. */
+function knownDurationsOf(
+ periods: AggregatePeriod[],
+ coverage: AggregateRange[] | undefined
+): number[] {
+ if (!coverage) {
+ return periods.map((period) => period.end - period.start);
+ }
+ const sorted = [...coverage].sort((a, b) => a.start - b.start);
+ let first = 0;
+ return periods.map((period) => {
+ while (first < sorted.length && sorted[first].end <= period.start) {
+ first += 1;
+ }
+ let known = 0;
+ for (
+ let i = first;
+ i < sorted.length && sorted[i].start < period.end;
+ i += 1
+ ) {
+ known += overlap(period, sorted[i]);
+ }
+ return known;
+ });
+}
+
+/** Known-state spans over `range`, seeded by the last sample at/before start. */
+function buildStateSpans(
+ samples: AggregateSample[],
+ range: AggregateRange,
+ coverage: AggregateRange[] | undefined
+): StateSpan[] {
+ const stateful = sortSamples(samples).filter(
+ (s) => typeof s.state === "string"
+ );
+ const points: { time: number; state: string }[] = [];
+ let seed: { time: number; state: string } | undefined;
+ for (const s of stateful) {
+ if (s.time <= range.start) {
+ seed = { time: range.start, state: s.state as string };
+ } else if (s.time < range.end) {
+ points.push({ time: s.time, state: s.state as string });
+ }
+ }
+ if (seed) {
+ points.unshift(seed);
+ }
+ const spans: StateSpan[] = [];
+ points.forEach((point, i) => {
+ if (!isKnownState(point.state)) {
+ return;
+ }
+ const end = i + 1 < points.length ? points[i + 1].time : range.end;
+ const span = { start: point.time, end, state: point.state };
+ if (!coverage) {
+ spans.push(span);
+ return;
+ }
+ for (const c of coverage) {
+ const start = Math.max(span.start, c.start);
+ const clippedEnd = Math.min(span.end, c.end);
+ if (clippedEnd > start) {
+ spans.push({ start, end: clippedEnd, state: span.state });
+ }
+ }
+ });
+ return spans;
+}
+
+function median(values: number[]): number {
+ const sorted = [...values].sort((a, b) => a - b);
+ const mid = Math.floor(sorted.length / 2);
+ return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
+}
+
+function calculateMeasurement(
+ input: AggregateInput,
+ periods: AggregatePeriod[]
+): Bucket[] {
+ const readings = sortSamples(input.samples).filter(
+ (s) =>
+ !s.boundary && typeof s.value === "number" && Number.isFinite(s.value)
+ );
+ const grouped = groupByPeriod(readings, periods, (s) => s.time);
+ return periods.map((_period, index) => {
+ const values = grouped[index].map((s) => s.value as number);
+ if (!values.length) {
+ return { value: null, reasons: ["no-data"] };
+ }
+ const op = input.definition.operation;
+ let value: number;
+ if (op === "min") {
+ value = Math.min(...values);
+ } else if (op === "max") {
+ value = Math.max(...values);
+ } else if (op === "median") {
+ value = median(values);
+ } else if (op === "first") {
+ value = values[0];
+ } else if (op === "last") {
+ value = values[values.length - 1];
+ } else {
+ value = values.reduce((sum, v) => sum + v, 0) / values.length;
+ }
+ return { value, reasons: [] };
+ });
+}
+
+function calculateTransitions(
+ input: AggregateInput,
+ periods: AggregatePeriod[]
+): Bucket[] {
+ const selected = input.definition.states ?? [];
+ const stateful = sortSamples(input.samples).filter(
+ (s) => typeof s.state === "string"
+ );
+ const times: number[] = [];
+ stateful.forEach((curr, i) => {
+ const prev = stateful[i - 1];
+ if (
+ !prev ||
+ curr.time <= input.range.start ||
+ curr.time >= input.range.end ||
+ !isKnownState(prev.state) ||
+ !isKnownState(curr.state) ||
+ prev.state === curr.state ||
+ (selected.length > 0 && !selected.includes(curr.state))
+ ) {
+ return;
+ }
+ times.push(curr.time);
+ });
+ const grouped = groupByPeriod(times, periods, (t) => t);
+ return periods.map((_period, index) => ({
+ value: grouped[index].length,
+ reasons: [],
+ }));
+}
+
+function calculateDurations(
+ input: AggregateInput,
+ periods: AggregatePeriod[],
+ percentage: boolean
+): Bucket[] {
+ const selected = input.definition.states ?? [];
+ if (!selected.length) {
+ return periods.map(() => ({ value: null, reasons: ["no-state-selected"] }));
+ }
+ const spans = buildStateSpans(
+ input.samples,
+ input.range,
+ input.coverage
+ ).sort((a, b) => a.start - b.start);
+ let first = 0;
+ return periods.map((period) => {
+ let known = 0;
+ let matching = 0;
+ // Spans are ordered and non-overlapping, so skip those already behind us.
+ while (first < spans.length && spans[first].end <= period.start) {
+ first += 1;
+ }
+ for (
+ let i = first;
+ i < spans.length && spans[i].start < period.end;
+ i += 1
+ ) {
+ const ms = overlap(period, spans[i]);
+ known += ms;
+ if (selected.includes(spans[i].state)) {
+ matching += ms;
+ }
+ }
+ if (percentage) {
+ return known > 0
+ ? { value: (matching / known) * 100, reasons: [] }
+ : { value: null, reasons: ["no-known-time"] };
+ }
+ return known > 0
+ ? { value: matching / MINUTE_MS, reasons: [] }
+ : { value: null, reasons: ["no-known-time"] };
+ });
+}
+
+function calculateEvents(
+ input: AggregateInput,
+ periods: AggregatePeriod[]
+): Bucket[] {
+ const types = input.definition.eventTypes ?? [];
+ const times = input.samples
+ .filter(
+ (s) =>
+ !s.boundary &&
+ typeof s.eventType === "string" &&
+ (types.length === 0 || types.includes(s.eventType))
+ )
+ .map((s) => s.eventTime ?? s.time);
+ const grouped = groupByPeriod(times, periods, (t) => t);
+ return periods.map((_period, index) => ({
+ value: grouped[index].length,
+ reasons: [],
+ }));
+}
+
+function calculateConsumption(
+ input: AggregateInput,
+ periods: AggregatePeriod[]
+): Bucket[] {
+ const deltas: { time: number; value: number; reason?: string }[] = [];
+ // HA statistics supply per-period consumption directly (reset-aware).
+ for (const s of input.samples) {
+ if (
+ typeof s.change === "number" &&
+ Number.isFinite(s.change) &&
+ s.time >= input.range.start &&
+ s.time < input.range.end
+ ) {
+ deltas.push({ time: s.time, value: s.change });
+ }
+ }
+ const readings = sortSamples(input.samples).filter((s) => {
+ const v = s.sum ?? s.value;
+ return (
+ s.change === undefined && typeof v === "number" && Number.isFinite(v)
+ );
+ });
+ readings.forEach((curr, i) => {
+ const prev = readings[i - 1];
+ if (
+ !prev ||
+ curr.time <= input.range.start ||
+ curr.time >= input.range.end
+ ) {
+ return;
+ }
+ const usesSum =
+ typeof curr.sum === "number" && typeof prev.sum === "number";
+ const currValue = (usesSum ? curr.sum : curr.value) as number;
+ const prevValue = (usesSum ? prev.sum : prev.value) as number;
+ const delta = currValue - prevValue;
+ if (usesSum || delta >= 0) {
+ deltas.push({ time: curr.time, value: delta });
+ } else if (
+ input.stateClass === "total_increasing" ||
+ (curr.lastReset !== undefined && curr.lastReset !== prev.lastReset)
+ ) {
+ deltas.push({ time: curr.time, value: currValue, reason: "reset" });
+ } else if (input.stateClass === "total") {
+ deltas.push({ time: curr.time, value: delta });
+ } else {
+ deltas.push({ time: curr.time, value: 0, reason: "ambiguous-reset" });
+ }
+ });
+ const grouped = groupByPeriod(deltas, periods, (d) => d.time);
+ return periods.map((_period, index) => {
+ const inside = grouped[index];
+ if (!inside.length) {
+ return { value: null, reasons: ["no-data"] };
+ }
+ return {
+ value: inside.reduce((sum, d) => sum + d.value, 0),
+ reasons: [
+ ...new Set(inside.flatMap((d) => (d.reason ? [d.reason] : []))),
+ ],
+ };
+ });
+}
+
+function toCumulative(
+ operation: string,
+ periods: AggregatePeriod[],
+ buckets: Bucket[]
+): { buckets: Bucket[]; periods: AggregatePeriod[]; started: boolean } {
+ const startsAtZeroActivity = operation !== "consumption";
+ let running = 0;
+ let started = false;
+ const keptBuckets: Bucket[] = [];
+ const keptPeriods: AggregatePeriod[] = [];
+ buckets.forEach((bucket, i) => {
+ if (!started) {
+ started =
+ bucket.value !== null && (!startsAtZeroActivity || bucket.value > 0);
+ if (!started) {
+ return;
+ }
+ }
+ running += bucket.value ?? 0;
+ keptBuckets.push({ value: running, reasons: bucket.reasons });
+ keptPeriods.push(periods[i]);
+ });
+ return { buckets: keptBuckets, periods: keptPeriods, started };
+}
+
+export function calculateAggregate(input: AggregateInput): AggregateResult {
+ const { definition } = input;
+ const cumulative =
+ definition.mode === "cumulative" &&
+ CUMULATIVE_OPERATIONS.has(definition.operation);
+ if (cumulative && definition.resetOnZoom && input.zoom) {
+ const start = Math.max(input.range.start, input.zoom.start);
+ const end = Math.min(input.range.end, input.zoom.end);
+ if (end > start) {
+ return calculateAggregate({
+ ...input,
+ zoom: undefined,
+ range: { start, end },
+ });
+ }
+ }
+
+ let periods = getPeriods(input.range, definition.interval, input.timeZone);
+ const op = definition.operation;
+ let buckets: Bucket[];
+ let unit = input.unit ?? "";
+ if (op === "transition_count") {
+ buckets = calculateTransitions(input, periods);
+ unit = "";
+ } else if (op === "state_duration") {
+ buckets = calculateDurations(input, periods, false);
+ unit = "min";
+ } else if (op === "state_percentage") {
+ buckets = calculateDurations(input, periods, true);
+ unit = "%";
+ } else if (op === "event_count") {
+ buckets = calculateEvents(input, periods);
+ unit = "";
+ } else if (op === "consumption") {
+ buckets = calculateConsumption(input, periods);
+ } else {
+ buckets = calculateMeasurement(input, periods);
+ }
+
+ let started = true;
+ if (cumulative) {
+ const result = toCumulative(op, periods, buckets);
+ ({ buckets, periods, started } = result);
+ }
+
+ const knownDurations = knownDurationsOf(periods, input.coverage);
+ const points: AggregatePoint[] = periods.map((period, i) => {
+ const totalDuration = period.end - period.start;
+ const knownDuration = knownDurations[i];
+ const reasons = [...buckets[i].reasons];
+ const incomplete = knownDuration < totalDuration;
+ if (incomplete) {
+ reasons.push("incomplete-coverage");
+ }
+ return {
+ ...period,
+ time: period.start,
+ // Missing history is never reported as zero activity.
+ value: knownDuration === 0 ? null : buckets[i].value,
+ incomplete: incomplete || reasons.includes("no-data"),
+ knownDuration,
+ totalDuration,
+ reasons,
+ };
+ });
+
+ const isBar =
+ !cumulative &&
+ !["mean", "min", "max", "median", "first", "last"].includes(op);
+ return {
+ points,
+ unit,
+ presentation: isBar ? "bar" : "line",
+ baseline: cumulative && started && points.length ? 0 : null,
+ };
+}
diff --git a/custom_components/hass_datapoints/src/lib/domain/aggregation/calendar.ts b/custom_components/hass_datapoints/src/lib/domain/aggregation/calendar.ts
new file mode 100644
index 00000000..1af15e2c
--- /dev/null
+++ b/custom_components/hass_datapoints/src/lib/domain/aggregation/calendar.ts
@@ -0,0 +1,171 @@
+import type {
+ AggregateInterval,
+ AggregatePeriod,
+ AggregateRange,
+} from "@/lib/domain/aggregation/types";
+
+const HOUR_MS = 3600000;
+const MINUTE_MS = 60000;
+
+/** Sub-day intervals of fixed length, aligned to the local wall clock. */
+const FIXED_INTERVAL_MS: Partial> = {
+ minute: MINUTE_MS,
+ "15min": 15 * MINUTE_MS,
+ "30min": 30 * MINUTE_MS,
+ hour: HOUR_MS,
+};
+const formatters = new Map();
+
+function getFormatter(timeZone: string): Intl.DateTimeFormat {
+ let formatter = formatters.get(timeZone);
+ if (!formatter) {
+ formatter = new Intl.DateTimeFormat("en-US", {
+ timeZone,
+ hourCycle: "h23",
+ year: "numeric",
+ month: "numeric",
+ day: "numeric",
+ hour: "numeric",
+ minute: "numeric",
+ second: "numeric",
+ });
+ formatters.set(timeZone, formatter);
+ }
+ return formatter;
+}
+
+interface LocalParts {
+ year: number;
+ month: number;
+ day: number;
+ minute: number;
+ second: number;
+ weekday: number;
+}
+
+function getLocalParts(ts: number, timeZone: string): LocalParts {
+ const values: Record = {};
+ for (const part of getFormatter(timeZone).formatToParts(ts)) {
+ if (part.type !== "literal") {
+ values[part.type] = Number(part.value);
+ }
+ }
+ return {
+ year: values.year,
+ month: values.month,
+ day: values.day,
+ minute: values.minute,
+ second: values.second,
+ weekday: new Date(
+ Date.UTC(values.year, values.month - 1, values.day)
+ ).getUTCDay(),
+ };
+}
+
+function getOffsetMs(ts: number, timeZone: string): number {
+ const whole = Math.floor(ts / 1000) * 1000;
+ const values: Record = {};
+ for (const part of getFormatter(timeZone).formatToParts(whole)) {
+ if (part.type !== "literal") {
+ values[part.type] = Number(part.value);
+ }
+ }
+ return (
+ Date.UTC(
+ values.year,
+ values.month - 1,
+ values.day,
+ values.hour,
+ values.minute,
+ values.second
+ ) - whole
+ );
+}
+
+/** Local midnight (in `timeZone`) of a possibly un-normalised calendar date. */
+function localMidnight(
+ year: number,
+ month: number,
+ day: number,
+ timeZone: string
+): number {
+ const wall = Date.UTC(year, month - 1, day);
+ const guess = wall - getOffsetMs(wall, timeZone);
+ return wall - getOffsetMs(guess, timeZone);
+}
+
+export function getPeriodStart(
+ ts: number,
+ interval: AggregateInterval,
+ timeZone: string
+): number {
+ const p = getLocalParts(ts, timeZone);
+ const fixedMs = FIXED_INTERVAL_MS[interval];
+ if (fixedMs) {
+ const ms = ((ts % 1000) + 1000) % 1000;
+ // Align on local minutes so half-hour offset zones still land on :00/:15/:30.
+ const offsetMinutes = p.minute % (fixedMs / MINUTE_MS);
+ return ts - ms - p.second * 1000 - offsetMinutes * MINUTE_MS;
+ }
+ if (interval === "day") {
+ return localMidnight(p.year, p.month, p.day, timeZone);
+ }
+ if (interval === "week") {
+ return localMidnight(
+ p.year,
+ p.month,
+ p.day - ((p.weekday + 6) % 7),
+ timeZone
+ );
+ }
+ return localMidnight(p.year, p.month, 1, timeZone);
+}
+
+export function getNextPeriodStart(
+ periodStart: number,
+ interval: AggregateInterval,
+ timeZone: string
+): number {
+ const fixedMs = FIXED_INTERVAL_MS[interval];
+ if (fixedMs) {
+ return periodStart + fixedMs;
+ }
+ const p = getLocalParts(periodStart + HOUR_MS * 2, timeZone);
+ if (interval === "day") {
+ return localMidnight(p.year, p.month, p.day + 1, timeZone);
+ }
+ if (interval === "week") {
+ return localMidnight(p.year, p.month, p.day + 7, timeZone);
+ }
+ return localMidnight(p.year, p.month + 1, 1, timeZone);
+}
+
+/**
+ * Calendar periods overlapping `range`, clipped to it. Periods cut by the
+ * range edge are flagged partial; the full period bounds are preserved.
+ */
+export function getPeriods(
+ range: AggregateRange,
+ interval: AggregateInterval,
+ timeZone: string
+): AggregatePeriod[] {
+ const periods: AggregatePeriod[] = [];
+ if (!(range.end > range.start)) {
+ return periods;
+ }
+ let periodStart = getPeriodStart(range.start, interval, timeZone);
+ while (periodStart < range.end) {
+ const periodEnd = getNextPeriodStart(periodStart, interval, timeZone);
+ const start = Math.max(periodStart, range.start);
+ const end = Math.min(periodEnd, range.end);
+ periods.push({
+ periodStart,
+ periodEnd,
+ start,
+ end,
+ partial: start !== periodStart || end !== periodEnd,
+ });
+ periodStart = periodEnd;
+ }
+ return periods;
+}
diff --git a/custom_components/hass_datapoints/src/lib/domain/aggregation/labels.ts b/custom_components/hass_datapoints/src/lib/domain/aggregation/labels.ts
new file mode 100644
index 00000000..d55dada5
--- /dev/null
+++ b/custom_components/hass_datapoints/src/lib/domain/aggregation/labels.ts
@@ -0,0 +1,186 @@
+import type {
+ AggregateDefinition,
+ AggregateInterval,
+ AggregateOperation,
+} from "@/lib/domain/aggregation/types";
+
+export interface AggregateStateOption {
+ /** Canonical HA state value, stored in the definition. */
+ value: string;
+ /** English label; localize with `msg()` at render time. */
+ label: string;
+}
+
+/** Choices a user makes for a binary row; maps onto an operation + states. */
+export type BinaryAggregateKind =
+ | "transition_count"
+ | "all_changes"
+ | "state_duration"
+ | "state_percentage";
+
+export const BINARY_AGGREGATE_KINDS: {
+ value: BinaryAggregateKind;
+ label: string;
+}[] = [
+ { value: "transition_count", label: "Count changes to a state" },
+ { value: "all_changes", label: "Count all state changes" },
+ { value: "state_duration", label: "Time in a state" },
+ { value: "state_percentage", label: "Percentage of time in a state" },
+];
+
+export const AGGREGATE_INTERVAL_OPTIONS: {
+ value: AggregateInterval;
+ label: string;
+}[] = [
+ { value: "minute", label: "Every minute" },
+ { value: "15min", label: "Every 15 minutes" },
+ { value: "30min", label: "Every 30 minutes" },
+ { value: "hour", label: "Hourly" },
+ { value: "day", label: "Daily" },
+ { value: "week", label: "Weekly" },
+ { value: "month", label: "Monthly" },
+];
+
+const STATE_LABELS: Record = {
+ door: ["Open", "Closed"],
+ garage_door: ["Open", "Closed"],
+ window: ["Open", "Closed"],
+ opening: ["Open", "Closed"],
+ motion: ["Detected", "Clear"],
+ occupancy: ["Occupied", "Clear"],
+ lock: ["Unlocked", "Locked"],
+};
+
+/** On/off options labelled for the entity's device class (e.g. Open/Closed). */
+export function getBinaryStateOptions(
+ deviceClass?: string
+): AggregateStateOption[] {
+ const [on, off] = STATE_LABELS[deviceClass ?? ""] ?? ["On", "Off"];
+ return [
+ { value: "on", label: on },
+ { value: "off", label: off },
+ ];
+}
+
+export function getBinaryAggregateKind(
+ definition: AggregateDefinition
+): BinaryAggregateKind {
+ if (definition.operation === "transition_count") {
+ return definition.states?.length ? "transition_count" : "all_changes";
+ }
+ return definition.operation === "state_percentage"
+ ? "state_percentage"
+ : "state_duration";
+}
+
+/** Apply a user-facing kind to a definition, keeping states consistent. */
+export function applyBinaryAggregateKind(
+ definition: AggregateDefinition,
+ kind: BinaryAggregateKind,
+ defaultState = "on"
+): AggregateDefinition {
+ const states = definition.states?.length ? definition.states : [defaultState];
+ if (kind === "all_changes") {
+ return { ...definition, operation: "transition_count", states: [] };
+ }
+ const operation = kind;
+ // Cumulative is only valid for counts and durations.
+ const mode = operation === "state_percentage" ? "interval" : definition.mode;
+ return { ...definition, operation, mode, states };
+}
+
+export function createBinaryAggregate(
+ existingIds: string[],
+ defaultState = "on"
+): AggregateDefinition {
+ let id: string;
+ do {
+ id = `agg_${Math.random().toString(36).slice(2, 8)}`;
+ } while (existingIds.includes(id));
+ return {
+ id,
+ operation: "transition_count",
+ interval: "day",
+ mode: "interval",
+ states: [defaultState],
+ resetOnZoom: false,
+ };
+}
+
+export const MEASUREMENT_OPERATIONS: {
+ value: AggregateOperation;
+ label: string;
+}[] = [
+ { value: "mean", label: "Mean" },
+ { value: "min", label: "Minimum" },
+ { value: "max", label: "Maximum" },
+ { value: "median", label: "Median" },
+ { value: "first", label: "First reading" },
+ { value: "last", label: "Last reading" },
+];
+
+/** Everything offered on a numeric row: measurement summaries plus consumption. */
+export const NUMERIC_AGGREGATE_OPERATIONS: {
+ value: AggregateOperation;
+ label: string;
+}[] = [
+ ...MEASUREMENT_OPERATIONS,
+ { value: "consumption", label: "Consumption" },
+];
+
+export function isNumericAggregateOperation(operation: string): boolean {
+ return NUMERIC_AGGREGATE_OPERATIONS.some((o) => o.value === operation);
+}
+
+const MEASUREMENT_VALUES = new Set(
+ MEASUREMENT_OPERATIONS.map((o) => o.value)
+);
+
+export function isMeasurementOperation(operation: string): boolean {
+ return MEASUREMENT_VALUES.has(operation);
+}
+
+export function createMeasurementAggregate(
+ existingIds: string[],
+ operation: AggregateOperation = "mean"
+): AggregateDefinition {
+ const base = createBinaryAggregate(existingIds);
+ return { ...base, operation, states: undefined };
+}
+
+export function createEventAggregate(
+ existingIds: string[],
+ eventTypes: string[] = []
+): AggregateDefinition {
+ const base = createBinaryAggregate(existingIds);
+ return {
+ ...base,
+ operation: "event_count",
+ states: undefined,
+ eventTypes,
+ };
+}
+
+/**
+ * Dash pattern (px on/off) per aggregation type. Aggregate lines keep the
+ * source series' colour, so the pattern is what tells them apart.
+ */
+const DASH_PATTERNS: Partial> = {
+ mean: [10, 4],
+ min: [2, 4],
+ max: [2, 4, 10, 4],
+ median: [6, 3, 2, 3],
+ first: [1, 5],
+ last: [1, 3, 1, 3, 8, 3],
+ consumption: [10, 4],
+ transition_count: [10, 4],
+ state_duration: [2, 4],
+ state_percentage: [2, 4, 10, 4],
+ event_count: [6, 3, 2, 3],
+};
+
+export function getAggregateDashPattern(
+ operation: AggregateOperation
+): number[] {
+ return DASH_PATTERNS[operation] ?? [6, 3];
+}
diff --git a/custom_components/hass_datapoints/src/lib/domain/aggregation/types.ts b/custom_components/hass_datapoints/src/lib/domain/aggregation/types.ts
new file mode 100644
index 00000000..1b9503f4
--- /dev/null
+++ b/custom_components/hass_datapoints/src/lib/domain/aggregation/types.ts
@@ -0,0 +1,81 @@
+export const AGGREGATE_OPERATIONS = [
+ "mean",
+ "min",
+ "max",
+ "median",
+ "first",
+ "last",
+ "consumption",
+ "transition_count",
+ "state_duration",
+ "state_percentage",
+ "event_count",
+] as const;
+export type AggregateOperation = (typeof AGGREGATE_OPERATIONS)[number];
+export const AGGREGATE_INTERVALS = [
+ "minute",
+ "15min",
+ "30min",
+ "hour",
+ "day",
+ "week",
+ "month",
+] as const;
+export type AggregateInterval = (typeof AGGREGATE_INTERVALS)[number];
+export interface AggregateDefinition {
+ id: string;
+ operation: AggregateOperation;
+ interval: AggregateInterval;
+ mode: "interval" | "cumulative";
+ states?: string[];
+ eventTypes?: string[];
+ resetOnZoom: boolean;
+ color?: string;
+ visible?: boolean;
+ label?: string;
+}
+export interface AggregateSample {
+ time: number;
+ state?: string;
+ value?: number | null;
+ eventType?: string;
+ eventTime?: number;
+ sum?: number | null;
+ /** Consumption already computed by HA statistics for the period starting at `time`. */
+ change?: number | null;
+ lastReset?: number;
+ boundary?: boolean;
+}
+export interface AggregateRange {
+ start: number;
+ end: number;
+}
+export interface AggregateInput {
+ definition: AggregateDefinition;
+ samples: AggregateSample[];
+ range: AggregateRange;
+ zoom?: AggregateRange;
+ timeZone: string;
+ stateClass?: string;
+ unit?: string;
+ coverage?: AggregateRange[];
+}
+export interface AggregatePeriod extends AggregateRange {
+ periodStart: number;
+ periodEnd: number;
+ partial: boolean;
+}
+export interface AggregatePoint extends AggregatePeriod {
+ time: number;
+ value: number | null;
+ incomplete: boolean;
+ knownDuration: number;
+ totalDuration: number;
+ reasons: string[];
+}
+export interface AggregateResult {
+ points: AggregatePoint[];
+ unit: string;
+ presentation: "bar" | "line";
+ baseline: number | null;
+}
diff --git a/custom_components/hass_datapoints/src/lib/domain/history-series.ts b/custom_components/hass_datapoints/src/lib/domain/history-series.ts
index e3170afb..b9d00d5b 100644
--- a/custom_components/hass_datapoints/src/lib/domain/history-series.ts
+++ b/custom_components/hass_datapoints/src/lib/domain/history-series.ts
@@ -1,3 +1,5 @@
+import { normalizeAggregateDefinitions } from "@/lib/domain/aggregation/aggregation";
+import type { AggregateDefinition } from "@/lib/domain/aggregation/types";
import { COLORS } from "@/constants";
import {
type AnomalyDirection,
@@ -10,6 +12,7 @@ import { normalizeEntityIds } from "@/lib/domain/target-selection";
*/
export interface HistorySeriesAnalysis {
+ aggregates?: AggregateDefinition[];
expanded: boolean;
show_trend_lines: boolean;
trend_method:
@@ -164,6 +167,9 @@ export function normalizeHistorySeriesAnalysis(
): HistorySeriesAnalysis {
const source = analysis && typeof analysis === "object" ? analysis : {};
return {
+ ...(Array.isArray(source.aggregates)
+ ? { aggregates: normalizeAggregateDefinitions(source.aggregates) }
+ : {}),
expanded: source.expanded === true,
show_trend_lines: source.show_trend_lines === true,
trend_method: (
@@ -304,7 +310,8 @@ export function historySeriesRowHasConfiguredAnalysis(
analysis.show_anomalies ||
analysis.show_delta_analysis ||
analysis.stepped_series ||
- analysis.hide_source_series
+ analysis.hide_source_series ||
+ (analysis.aggregates?.length ?? 0) > 0
);
}
diff --git a/custom_components/hass_datapoints/src/lib/i18n/__tests__/translations-coverage.spec.ts b/custom_components/hass_datapoints/src/lib/i18n/__tests__/translations-coverage.spec.ts
index 533e72ee..ef90c5e9 100644
--- a/custom_components/hass_datapoints/src/lib/i18n/__tests__/translations-coverage.spec.ts
+++ b/custom_components/hass_datapoints/src/lib/i18n/__tests__/translations-coverage.spec.ts
@@ -52,6 +52,8 @@ const UNTRANSLATED_WHITELIST = new Set([
"Max",
"Min",
"Median",
+ "Minimum", // German / French "Minimum"
+ "Maximum", // German / French "Maximum"
// Technical terms borrowed without translation
"Auto",
diff --git a/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/__tests__/analysis-aggregate-group.spec.ts b/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/__tests__/analysis-aggregate-group.spec.ts
new file mode 100644
index 00000000..656059d5
--- /dev/null
+++ b/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/__tests__/analysis-aggregate-group.spec.ts
@@ -0,0 +1,377 @@
+import { afterEach, describe, expect, it, vi } from "vitest";
+import "../analysis-aggregate-group";
+import type {
+ AggregateDefinition,
+ AggregateOperation,
+} from "@/lib/domain/aggregation/types";
+import type { NormalizedAnalysis } from "../../target-row/types";
+
+type GroupElement = HTMLElement & {
+ analysis: NormalizedAnalysis;
+ entityId: string;
+ deviceClass: string;
+ variant: string;
+ operations: AggregateOperation[];
+ eventTypes: string[];
+ updateComplete: Promise;
+};
+
+const definition = (
+ overrides: Partial = {}
+): AggregateDefinition => ({
+ id: "transition_count",
+ operation: "transition_count",
+ interval: "day",
+ mode: "interval",
+ states: ["on"],
+ resetOnZoom: false,
+ ...overrides,
+});
+
+async function mount(
+ aggregates: AggregateDefinition[],
+ props: Partial = {}
+) {
+ const el = document.createElement("analysis-aggregate-group") as GroupElement;
+ Object.assign(el, {
+ analysis: { aggregates } as NormalizedAnalysis,
+ entityId: "binary_sensor.front_door",
+ deviceClass: "door",
+ ...props,
+ });
+ document.body.appendChild(el);
+ await el.updateComplete;
+ return el;
+}
+
+const choice = (el: GroupElement, key: string) =>
+ el.shadowRoot!.querySelector(
+ `.aggregate-choice[data-key="${key}"]`
+ ) as HTMLInputElement;
+
+const check = (input: HTMLInputElement, checked: boolean) => {
+ input.checked = checked;
+ input.dispatchEvent(new Event("change"));
+};
+
+const selects = (el: GroupElement) =>
+ [...el.shadowRoot!.querySelectorAll("inline-select")] as (HTMLElement & {
+ value: string;
+ options: { value: string; label: string }[];
+ })[];
+
+function choose(select: HTMLElement, value: string) {
+ select.dispatchEvent(
+ new CustomEvent("dp-change", {
+ detail: { type: "select", value },
+ bubbles: true,
+ composed: true,
+ })
+ );
+}
+
+const lastValue = (handler: ReturnType) =>
+ handler.mock.calls.at(-1)![0].detail.value as AggregateDefinition[];
+
+describe("analysis-aggregate-group", () => {
+ let el: GroupElement;
+ afterEach(() => el?.remove());
+
+ describe("GIVEN a door sensor with no aggregates", () => {
+ describe("WHEN the group is rendered", () => {
+ it("THEN the group is closed", async () => {
+ expect.assertions(1);
+ el = await mount([]);
+ const group = el.shadowRoot!.querySelector(
+ "analysis-group"
+ ) as HTMLElement & { checked: boolean };
+ expect(group.checked).toBe(false);
+ });
+ });
+
+ describe("WHEN the group is switched on", () => {
+ it("THEN the four binary choices appear unchecked", async () => {
+ expect.assertions(2);
+ el = await mount([]);
+ el.shadowRoot!.querySelector("analysis-group")!.dispatchEvent(
+ new CustomEvent("dp-group-change", {
+ detail: { checked: true },
+ bubbles: true,
+ composed: true,
+ })
+ );
+ await el.updateComplete;
+ const boxes = [
+ ...el.shadowRoot!.querySelectorAll(".aggregate-choice"),
+ ] as HTMLInputElement[];
+ expect(boxes.map((b) => b.dataset.key)).toStrictEqual([
+ "transition_count",
+ "all_changes",
+ "state_duration",
+ "state_percentage",
+ ]);
+ expect(boxes.every((b) => !b.checked)).toBe(true);
+ });
+ });
+ });
+
+ describe("GIVEN an enabled group on a door sensor", () => {
+ describe("WHEN Count changes to a state is checked", () => {
+ it("THEN a daily aggregate for the open state is added", async () => {
+ expect.assertions(2);
+ el = await mount([
+ definition({ id: "state_duration", operation: "state_duration" }),
+ ]);
+ const handler = vi.fn();
+ el.addEventListener("dp-group-analysis-change", handler);
+ check(choice(el, "transition_count"), true);
+ const added = lastValue(handler).find(
+ (d) => d.id === "transition_count"
+ );
+ expect(added).toMatchObject({
+ operation: "transition_count",
+ interval: "day",
+ states: ["on"],
+ });
+ expect(lastValue(handler)).toHaveLength(2);
+ });
+ });
+
+ describe("WHEN a checked aggregate is unchecked", () => {
+ it("THEN only that aggregate is removed", async () => {
+ expect.assertions(1);
+ el = await mount([
+ definition(),
+ definition({ id: "state_duration", operation: "state_duration" }),
+ ]);
+ const handler = vi.fn();
+ el.addEventListener("dp-group-analysis-change", handler);
+ check(choice(el, "transition_count"), false);
+ expect(lastValue(handler).map((d) => d.id)).toStrictEqual([
+ "state_duration",
+ ]);
+ });
+ });
+
+ describe("WHEN the master checkbox is switched off", () => {
+ it("THEN every aggregate is cleared", async () => {
+ expect.assertions(1);
+ el = await mount([definition()]);
+ const handler = vi.fn();
+ el.addEventListener("dp-group-analysis-change", handler);
+ el.shadowRoot!.querySelector("analysis-group")!.dispatchEvent(
+ new CustomEvent("dp-group-change", {
+ detail: { checked: false },
+ bubbles: true,
+ composed: true,
+ })
+ );
+ expect(lastValue(handler)).toStrictEqual([]);
+ });
+ });
+ });
+
+ describe("GIVEN a checked changes-to-state aggregate", () => {
+ describe("WHEN its settings are shown", () => {
+ it("THEN state choices use device labels but store canonical values", async () => {
+ expect.assertions(2);
+ el = await mount([definition()]);
+ const [stateSelect] = selects(el);
+ expect(stateSelect.options.map((o) => o.label)).toStrictEqual([
+ "Open",
+ "Closed",
+ ]);
+ expect(stateSelect.options.map((o) => o.value)).toStrictEqual([
+ "on",
+ "off",
+ ]);
+ });
+ });
+
+ describe("WHEN the state is changed to Closed", () => {
+ it("THEN emits the stored value off", async () => {
+ expect.assertions(1);
+ el = await mount([definition()]);
+ const handler = vi.fn();
+ el.addEventListener("dp-group-analysis-change", handler);
+ choose(selects(el)[0], "off");
+ expect(lastValue(handler)[0].states).toStrictEqual(["off"]);
+ });
+ });
+
+ describe("WHEN the interval is changed", () => {
+ it("THEN only that aggregate changes", async () => {
+ expect.assertions(2);
+ el = await mount([
+ definition(),
+ definition({ id: "state_duration", operation: "state_duration" }),
+ ]);
+ const handler = vi.fn();
+ el.addEventListener("dp-group-analysis-change", handler);
+ choose(selects(el)[1], "month");
+ const next = lastValue(handler);
+ expect(next[0].interval).toBe("month");
+ expect(next[1].interval).toBe("day");
+ });
+ });
+ });
+
+ describe("GIVEN a checked all-state-changes aggregate", () => {
+ it("THEN no state selector is shown, only the interval", async () => {
+ expect.assertions(2);
+ el = await mount([definition({ id: "all_changes", states: [] })]);
+ expect(selects(el)).toHaveLength(1);
+ expect(selects(el)[0].value).toBe("day");
+ });
+ });
+
+ describe("GIVEN a percentage-of-time aggregate", () => {
+ it("THEN no cumulative option is offered", async () => {
+ expect.assertions(1);
+ el = await mount([
+ definition({ id: "state_percentage", operation: "state_percentage" }),
+ ]);
+ expect(el.shadowRoot!.querySelector(".cumulative")).toBeNull();
+ });
+ });
+
+ describe("GIVEN a checked count aggregate", () => {
+ describe("WHEN Cumulative is checked", () => {
+ it("THEN the mode becomes cumulative and the zoom reset option appears", async () => {
+ expect.assertions(3);
+ el = await mount([definition()]);
+ expect(el.shadowRoot!.querySelector(".reset-on-zoom")).toBeNull();
+ const handler = vi.fn();
+ el.addEventListener("dp-group-analysis-change", handler);
+ check(el.shadowRoot!.querySelector(".cumulative")!, true);
+ expect(lastValue(handler)[0].mode).toBe("cumulative");
+ el.analysis = { aggregates: lastValue(handler) } as NormalizedAnalysis;
+ await el.updateComplete;
+ check(el.shadowRoot!.querySelector(".reset-on-zoom")!, true);
+ expect(lastValue(handler)[0].resetOnZoom).toBe(true);
+ });
+ });
+ });
+});
+
+describe("analysis-aggregate-group measurement variant", () => {
+ let el: GroupElement;
+ afterEach(() => el?.remove());
+
+ const mountMeasurement = (aggregates: AggregateDefinition[]) =>
+ mount(aggregates, {
+ variant: "measurement",
+ operations: ["mean", "min", "consumption"],
+ entityId: "sensor.energy",
+ });
+
+ describe("GIVEN a meter suggesting mean, min and consumption", () => {
+ describe("WHEN an aggregate is checked", () => {
+ it("THEN only suggested operations are offered and the definition has no states", async () => {
+ expect.assertions(3);
+ el = await mountMeasurement([
+ definition({ id: "mean", operation: "mean", states: undefined }),
+ ]);
+ const keys = [
+ ...el.shadowRoot!.querySelectorAll(".aggregate-choice"),
+ ].map((b) => (b as HTMLInputElement).dataset.key);
+ expect(keys).toStrictEqual(["mean", "min", "consumption"]);
+ const handler = vi.fn();
+ el.addEventListener("dp-group-analysis-change", handler);
+ check(choice(el, "min"), true);
+ const added = lastValue(handler).find((d) => d.id === "min");
+ expect(added).toMatchObject({ operation: "min", interval: "day" });
+ expect(added!.states).toBeUndefined();
+ });
+ });
+
+ describe("WHEN consumption is checked", () => {
+ it("THEN its settings offer a cumulative option", async () => {
+ expect.assertions(1);
+ el = await mountMeasurement([
+ definition({
+ id: "consumption",
+ operation: "consumption",
+ states: undefined,
+ }),
+ ]);
+ expect(el.shadowRoot!.querySelector(".cumulative")).not.toBeNull();
+ });
+ });
+
+ describe("WHEN a mean aggregate is checked", () => {
+ it("THEN no cumulative option is offered", async () => {
+ expect.assertions(1);
+ el = await mountMeasurement([
+ definition({ id: "mean", operation: "mean", states: undefined }),
+ ]);
+ expect(el.shadowRoot!.querySelector(".cumulative")).toBeNull();
+ });
+ });
+ });
+});
+
+describe("analysis-aggregate-group event variant", () => {
+ let el: GroupElement;
+ afterEach(() => el?.remove());
+
+ const mountEvent = (aggregates: AggregateDefinition[]) =>
+ mount(aggregates, {
+ variant: "event",
+ eventTypes: ["single_press", "double_press"],
+ entityId: "event.button",
+ });
+ const eventDefinition = (eventTypes: string[]) =>
+ definition({
+ id: "event_count",
+ operation: "event_count",
+ states: undefined,
+ eventTypes,
+ });
+
+ describe("GIVEN a button reporting single and double presses", () => {
+ describe("WHEN Count events is checked", () => {
+ it("THEN it counts all event types by default", async () => {
+ expect.assertions(1);
+ el = await mountEvent([eventDefinition([])]);
+ check(choice(el, "event_count"), false);
+ const handler = vi.fn();
+ el.addEventListener("dp-group-analysis-change", handler);
+ check(choice(el, "event_count"), true);
+ expect(lastValue(handler)[0]).toMatchObject({
+ operation: "event_count",
+ eventTypes: [],
+ });
+ });
+ });
+
+ describe("WHEN All event types is unchecked", () => {
+ it("THEN the first type is selected", async () => {
+ expect.assertions(1);
+ el = await mountEvent([eventDefinition([])]);
+ const handler = vi.fn();
+ el.addEventListener("dp-group-analysis-change", handler);
+ check(el.shadowRoot!.querySelector(".all-types")!, false);
+ expect(lastValue(handler)[0].eventTypes).toStrictEqual([
+ "single_press",
+ ]);
+ });
+ });
+
+ describe("WHEN specific types are selected", () => {
+ it("THEN each type is individually toggleable", async () => {
+ expect.assertions(2);
+ el = await mountEvent([eventDefinition(["single_press"])]);
+ const boxes = el.shadowRoot!.querySelectorAll(".event-type");
+ expect(boxes).toHaveLength(2);
+ const handler = vi.fn();
+ el.addEventListener("dp-group-analysis-change", handler);
+ check(boxes[1] as HTMLInputElement, true);
+ expect(lastValue(handler)[0].eventTypes).toStrictEqual([
+ "single_press",
+ "double_press",
+ ]);
+ });
+ });
+ });
+});
diff --git a/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/analysis-aggregate-group.styles.ts b/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/analysis-aggregate-group.styles.ts
new file mode 100644
index 00000000..8eb73fff
--- /dev/null
+++ b/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/analysis-aggregate-group.styles.ts
@@ -0,0 +1,13 @@
+import { css } from "lit";
+
+export const styles = css`
+ .method-list {
+ display: grid;
+ gap: calc(var(--dp-spacing-sm, 8px) * 0.75);
+ }
+
+ .method-item {
+ display: grid;
+ gap: 6px;
+ }
+`;
diff --git a/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/analysis-aggregate-group.ts b/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/analysis-aggregate-group.ts
new file mode 100644
index 00000000..5f4a832b
--- /dev/null
+++ b/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/analysis-aggregate-group.ts
@@ -0,0 +1,342 @@
+import { LitElement, html, nothing, type TemplateResult } from "lit";
+import { property, state } from "lit/decorators.js";
+import { localized, msg } from "@/lib/i18n/localize";
+
+import { AnalysisGroupMixin } from "../analysis-group-shared/analysis-group.mixin";
+import { sharedStyles } from "../analysis-group-shared/analysis-group-shared.styles";
+import { styles } from "./analysis-aggregate-group.styles";
+import {
+ AGGREGATE_INTERVAL_OPTIONS,
+ BINARY_AGGREGATE_KINDS,
+ NUMERIC_AGGREGATE_OPERATIONS,
+ applyBinaryAggregateKind,
+ createBinaryAggregate,
+ createEventAggregate,
+ createMeasurementAggregate,
+ getBinaryAggregateKind,
+ getBinaryStateOptions,
+} from "@/lib/domain/aggregation/labels";
+import type {
+ AggregateDefinition,
+ AggregateInterval,
+ AggregateOperation,
+} from "@/lib/domain/aggregation/types";
+import "@/atoms/analysis/analysis-group/analysis-group";
+import "@/atoms/analysis/analysis-method-subopts/analysis-method-subopts";
+import "@/atoms/form/inline-select/inline-select";
+
+interface AggregateChoice {
+ /** Stable key; also the id of the definition this choice creates. */
+ key: string;
+ label: string;
+}
+
+const CUMULATIVE_OPERATIONS: AggregateOperation[] = [
+ "consumption",
+ "transition_count",
+ "state_duration",
+ "event_count",
+];
+
+/**
+ * Aggregate series for a row. Like the anomaly group, each aggregate is a
+ * checkbox and its settings (state, interval, cumulative, ...) nest beneath it
+ * while it is checked. One definition exists per checked choice.
+ * @fires dp-group-analysis-change - `{ entityId, key: "aggregates", value: AggregateDefinition[] }` — the full next list of definitions
+ */
+@localized()
+export class AnalysisAggregateGroup extends AnalysisGroupMixin(LitElement) {
+ /** HA device class of the source entity; selects Open/Closed style labels. */
+ @property({ type: String, attribute: "device-class" })
+ accessor deviceClass: string = "";
+
+ /** `binary` offers state operations; `measurement` numeric ones; `event` event counts. */
+ @property({ type: String })
+ accessor variant: "binary" | "measurement" | "event" = "binary";
+
+ /** Numeric operations suggested for the entity; the user still chooses. */
+ @property({ type: Array, attribute: false })
+ accessor operations: AggregateOperation[] = [];
+
+ /** Event types the entity can emit (HA `event_types` attribute). */
+ @property({ type: Array, attribute: false })
+ accessor eventTypes: string[] = [];
+
+ /** Keeps the group open right after it is switched on, before any choice is checked. */
+ @state() private accessor _open = false;
+
+ static styles = [sharedStyles, styles];
+
+ private get _definitions(): AggregateDefinition[] {
+ return this.analysis.aggregates ?? [];
+ }
+
+ private _commit(next: AggregateDefinition[]) {
+ this._emit("aggregates", next);
+ }
+
+ private _choices(): AggregateChoice[] {
+ if (this.variant === "event") {
+ return [{ key: "event_count", label: msg("Count events") }];
+ }
+ if (this.variant === "measurement") {
+ return NUMERIC_AGGREGATE_OPERATIONS.filter((o) =>
+ this.operations.includes(o.value)
+ ).map((o) => ({ key: o.value, label: msg(o.label) }));
+ }
+ return BINARY_AGGREGATE_KINDS.map((k) => ({
+ key: k.value,
+ label: msg(k.label),
+ }));
+ }
+
+ private _keyOf(definition: AggregateDefinition): string {
+ if (this.variant === "binary") {
+ return getBinaryAggregateKind(definition);
+ }
+ return definition.operation;
+ }
+
+ private _definitionFor(key: string): AggregateDefinition | undefined {
+ return this._definitions.find((d) => this._keyOf(d) === key);
+ }
+
+ private _defaultState(): string {
+ return getBinaryStateOptions(this.deviceClass)[0].value;
+ }
+
+ private _create(key: string): AggregateDefinition {
+ const ids = this._definitions.map((d) => d.id);
+ let created: AggregateDefinition;
+ if (this.variant === "event") {
+ created = createEventAggregate(ids);
+ } else if (this.variant === "measurement") {
+ created = createMeasurementAggregate(ids, key as AggregateOperation);
+ } else {
+ created = applyBinaryAggregateKind(
+ createBinaryAggregate(ids, this._defaultState()),
+ key as ReturnType,
+ this._defaultState()
+ );
+ }
+ return { ...created, id: ids.includes(key) ? created.id : key };
+ }
+
+ private _onGroupChange(e: CustomEvent) {
+ const checked = (e.detail as { checked: boolean }).checked;
+ this._open = checked;
+ if (!checked) {
+ this._commit([]);
+ }
+ }
+
+ private _onToggle(key: string, checked: boolean) {
+ this._commit(
+ checked
+ ? [...this._definitions, this._create(key)]
+ : this._definitions.filter((d) => this._keyOf(d) !== key)
+ );
+ }
+
+ private _update(
+ id: string,
+ patch: (d: AggregateDefinition) => AggregateDefinition
+ ) {
+ this._commit(this._definitions.map((d) => (d.id === id ? patch(d) : d)));
+ }
+
+ private _selectValue(e: Event): string {
+ return (e as CustomEvent<{ type: "select"; value: string }>).detail.value;
+ }
+
+ private _renderIntervalField(definition: AggregateDefinition) {
+ return html`
+
+ ${msg("Interval")}
+
+ this._update(definition.id, (d) => ({
+ ...d,
+ interval: this._selectValue(e) as AggregateInterval,
+ }))}
+ >
+
+ `;
+ }
+
+ private _renderStateField(definition: AggregateDefinition) {
+ const options = getBinaryStateOptions(this.deviceClass).map((o) => ({
+ value: o.value,
+ label: msg(o.label),
+ }));
+ return html`
+
+ ${msg("State")}
+
+ this._update(definition.id, (d) => ({
+ ...d,
+ states: [this._selectValue(e)],
+ }))}
+ >
+
+ `;
+ }
+
+ /** Counts, durations and consumption can accumulate; percentages cannot. */
+ private _renderCumulativeFields(definition: AggregateDefinition) {
+ if (!CUMULATIVE_OPERATIONS.includes(definition.operation)) {
+ return nothing;
+ }
+ const cumulative = definition.mode === "cumulative";
+ return html`
+
+
+ this._update(definition.id, (d) => ({
+ ...d,
+ mode: (e.target as HTMLInputElement).checked
+ ? "cumulative"
+ : "interval",
+ }))}
+ />
+ ${msg("Cumulative")}
+
+ ${cumulative
+ ? html`
+
+ this._update(definition.id, (d) => ({
+ ...d,
+ resetOnZoom: (e.target as HTMLInputElement).checked,
+ }))}
+ />
+ ${msg("Reset total to zoomed range")}
+ `
+ : nothing}
+ `;
+ }
+
+ private _onEventTypeToggle(
+ definition: AggregateDefinition,
+ type: string,
+ checked: boolean
+ ) {
+ const current = definition.eventTypes ?? [];
+ const next = checked
+ ? [...new Set([...current, type])]
+ : current.filter((t) => t !== type);
+ // Deselecting the last specific type falls back to All event types.
+ this._update(definition.id, (d) => ({ ...d, eventTypes: next }));
+ }
+
+ private _renderEventTypeFields(definition: AggregateDefinition) {
+ const selected = definition.eventTypes ?? [];
+ const allTypes = selected.length === 0;
+ return html`
+
+
+ this._update(definition.id, (d) => ({
+ ...d,
+ eventTypes: (e.target as HTMLInputElement).checked
+ ? []
+ : this.eventTypes.slice(0, 1),
+ }))}
+ />
+ ${msg("All event types")}
+
+ ${allTypes
+ ? nothing
+ : this.eventTypes.map(
+ (type) => html`
+
+
+ this._onEventTypeToggle(
+ definition,
+ type,
+ (e.target as HTMLInputElement).checked
+ )}
+ />
+ ${type}
+
+ `
+ )}
+ `;
+ }
+
+ private _renderSubopts(
+ key: string,
+ definition: AggregateDefinition
+ ): TemplateResult {
+ return html`
+
+ ${this.variant === "binary" && key !== "all_changes"
+ ? this._renderStateField(definition)
+ : nothing}
+ ${this._renderIntervalField(definition)}
+ ${this.variant === "event"
+ ? this._renderEventTypeFields(definition)
+ : nothing}
+ ${this._renderCumulativeFields(definition)}
+
+ `;
+ }
+
+ render() {
+ const choices = this._choices();
+ return html`
+ 0}
+ @dp-group-change=${this._onGroupChange}
+ >
+
+ ${choices.map((choice) => {
+ const definition = this._definitionFor(choice.key);
+ return html`
+
+
+
+ this._onToggle(
+ choice.key,
+ (e.target as HTMLInputElement).checked
+ )}
+ />
+ ${choice.label}
+
+ ${definition
+ ? this._renderSubopts(choice.key, definition)
+ : nothing}
+
+ `;
+ })}
+
+
+ `;
+ }
+}
+
+customElements.define("analysis-aggregate-group", AnalysisAggregateGroup);
diff --git a/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/de.ts b/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/de.ts
new file mode 100644
index 00000000..543f57c4
--- /dev/null
+++ b/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/de.ts
@@ -0,0 +1,47 @@
+import type { ComponentTranslations } from "@/lib/i18n/types";
+
+export const translations: ComponentTranslations = {
+ "Aggregate series": "Aggregierte Serien",
+ "Count changes to a state": "Wechsel in einen Zustand zählen",
+ "Count all state changes": "Alle Zustandswechsel zählen",
+ "Time in a state": "Zeit in einem Zustand",
+ "Percentage of time in a state": "Zeitanteil in einem Zustand in Prozent",
+ Hourly: "Stündlich",
+ Daily: "Täglich",
+ Weekly: "Wöchentlich",
+ Monthly: "Monatlich",
+ On: "An",
+ Off: "Aus",
+ Open: "Offen",
+ Closed: "Geschlossen",
+ Detected: "Erkannt",
+ Clear: "Frei",
+ Occupied: "Belegt",
+ Unlocked: "Entsperrt",
+ Locked: "Gesperrt",
+ Mean: "Mittelwert",
+ Minimum: "Minimum",
+ Maximum: "Maximum",
+ Median: "Median",
+ "First reading": "Erster Messwert",
+ "Last reading": "Letzter Messwert",
+ "All event types": "Alle Ereignistypen",
+ Consumption: "Verbrauch",
+ "Meter reset detected": "Zählerrücksetzung erkannt",
+ "Ambiguous meter reset, not counted":
+ "Unklare Zählerrücksetzung, nicht gezählt",
+ Cumulative: "Kumulativ",
+ "Reset total to zoomed range": "Summe auf den gezoomten Bereich zurücksetzen",
+ "Partial period": "Teilperiode",
+ "Incomplete history": "Unvollständiger Verlauf",
+ "Dashed outlines mark partial periods cut by the selected range; hatched or dotted marks show periods with gaps in recorded history.":
+ "Gestrichelte Umrandungen kennzeichnen Teilperioden am Rand des gewählten Bereichs; schraffierte oder gepunktete Markierungen zeigen Zeiträume mit Lücken im aufgezeichneten Verlauf.",
+ "Counts only include activity in retained history; earlier activity is not shown.":
+ "Zählungen enthalten nur Aktivität im gespeicherten Verlauf; frühere Aktivität wird nicht angezeigt.",
+ "Count events": "Ereignisse zählen",
+ Interval: "Intervall",
+ State: "Zustand",
+ "Every minute": "Jede Minute",
+ "Every 15 minutes": "Alle 15 Minuten",
+ "Every 30 minutes": "Alle 30 Minuten",
+};
diff --git a/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/es.ts b/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/es.ts
new file mode 100644
index 00000000..bbb71af6
--- /dev/null
+++ b/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/es.ts
@@ -0,0 +1,47 @@
+import type { ComponentTranslations } from "@/lib/i18n/types";
+
+export const translations: ComponentTranslations = {
+ "Aggregate series": "Series agregadas",
+ "Count changes to a state": "Contar cambios a un estado",
+ "Count all state changes": "Contar todos los cambios de estado",
+ "Time in a state": "Tiempo en un estado",
+ "Percentage of time in a state": "Porcentaje de tiempo en un estado",
+ Hourly: "Por hora",
+ Daily: "Diario",
+ Weekly: "Semanal",
+ Monthly: "Mensual",
+ On: "Encendido",
+ Off: "Apagado",
+ Open: "Abierto",
+ Closed: "Cerrado",
+ Detected: "Detectado",
+ Clear: "Libre",
+ Occupied: "Ocupado",
+ Unlocked: "Desbloqueado",
+ Locked: "Bloqueado",
+ Mean: "Media",
+ Minimum: "Mínimo",
+ Maximum: "Máximo",
+ Median: "Mediana",
+ "First reading": "Primera lectura",
+ "Last reading": "Última lectura",
+ "All event types": "Todos los tipos de evento",
+ Consumption: "Consumo",
+ "Meter reset detected": "Reinicio del contador detectado",
+ "Ambiguous meter reset, not counted":
+ "Reinicio del contador ambiguo, no contabilizado",
+ Cumulative: "Acumulado",
+ "Reset total to zoomed range": "Reiniciar el total al rango ampliado",
+ "Partial period": "Periodo parcial",
+ "Incomplete history": "Historial incompleto",
+ "Dashed outlines mark partial periods cut by the selected range; hatched or dotted marks show periods with gaps in recorded history.":
+ "Los contornos discontinuos marcan los periodos parciales cortados por el rango seleccionado; las marcas rayadas o punteadas indican periodos con huecos en el historial registrado.",
+ "Counts only include activity in retained history; earlier activity is not shown.":
+ "Los recuentos solo incluyen la actividad del historial conservado; la actividad anterior no se muestra.",
+ "Count events": "Contar eventos",
+ Interval: "Intervalo",
+ State: "Estado",
+ "Every minute": "Cada minuto",
+ "Every 15 minutes": "Cada 15 minutos",
+ "Every 30 minutes": "Cada 30 minutos",
+};
diff --git a/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/fi.ts b/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/fi.ts
new file mode 100644
index 00000000..2deabf46
--- /dev/null
+++ b/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/fi.ts
@@ -0,0 +1,47 @@
+import type { ComponentTranslations } from "@/lib/i18n/types";
+
+export const translations: ComponentTranslations = {
+ "Aggregate series": "Koosteet",
+ "Count changes to a state": "Laske muutokset tilaan",
+ "Count all state changes": "Laske kaikki tilamuutokset",
+ "Time in a state": "Aika tilassa",
+ "Percentage of time in a state": "Tilassa vietetyn ajan osuus prosentteina",
+ Hourly: "Tunneittain",
+ Daily: "Päivittäin",
+ Weekly: "Viikoittain",
+ Monthly: "Kuukausittain",
+ On: "Päällä",
+ Off: "Pois",
+ Open: "Auki",
+ Closed: "Kiinni",
+ Detected: "Havaittu",
+ Clear: "Vapaa",
+ Occupied: "Varattu",
+ Unlocked: "Avattu",
+ Locked: "Lukittu",
+ Mean: "Keskiarvo",
+ Minimum: "Pienin",
+ Maximum: "Suurin",
+ Median: "Mediaani",
+ "First reading": "Ensimmäinen lukema",
+ "Last reading": "Viimeisin lukema",
+ "All event types": "Kaikki tapahtumatyypit",
+ Consumption: "Kulutus",
+ "Meter reset detected": "Mittarin nollaus havaittu",
+ "Ambiguous meter reset, not counted":
+ "Epäselvä mittarin nollaus, ei laskettu",
+ Cumulative: "Kumulatiivinen",
+ "Reset total to zoomed range": "Nollaa kokonaismäärä zoomattuun alueeseen",
+ "Partial period": "Osittainen jakso",
+ "Incomplete history": "Puutteellinen historia",
+ "Dashed outlines mark partial periods cut by the selected range; hatched or dotted marks show periods with gaps in recorded history.":
+ "Katkoviivareunat osoittavat valitun aikavälin reunoilla katkenneet osittaiset jaksot; viivoitetut tai pisteviivamerkinnät osoittavat jaksot, joiden tallennetussa historiassa on aukkoja.",
+ "Counts only include activity in retained history; earlier activity is not shown.":
+ "Laskennat sisältävät vain säilytetyn historian toiminnan; aiempaa toimintaa ei näytetä.",
+ "Count events": "Laske tapahtumat",
+ Interval: "Jakso",
+ State: "Tila",
+ "Every minute": "Joka minuutti",
+ "Every 15 minutes": "Joka 15. minuutti",
+ "Every 30 minutes": "Joka 30. minuutti",
+};
diff --git a/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/fr.ts b/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/fr.ts
new file mode 100644
index 00000000..6374a5bd
--- /dev/null
+++ b/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/fr.ts
@@ -0,0 +1,47 @@
+import type { ComponentTranslations } from "@/lib/i18n/types";
+
+export const translations: ComponentTranslations = {
+ "Aggregate series": "Séries agrégées",
+ "Count changes to a state": "Compter les passages à un état",
+ "Count all state changes": "Compter tous les changements d'état",
+ "Time in a state": "Durée dans un état",
+ "Percentage of time in a state": "Pourcentage du temps dans un état",
+ Hourly: "Par heure",
+ Daily: "Par jour",
+ Weekly: "Par semaine",
+ Monthly: "Par mois",
+ On: "Allumé",
+ Off: "Éteint",
+ Open: "Ouvert",
+ Closed: "Fermé",
+ Detected: "Détecté",
+ Clear: "Libre",
+ Occupied: "Occupé",
+ Unlocked: "Déverrouillé",
+ Locked: "Verrouillé",
+ Mean: "Moyenne",
+ Minimum: "Minimum",
+ Maximum: "Maximum",
+ Median: "Médiane",
+ "First reading": "Première mesure",
+ "Last reading": "Dernière mesure",
+ "All event types": "Tous les types d'événement",
+ Consumption: "Consommation",
+ "Meter reset detected": "Réinitialisation du compteur détectée",
+ "Ambiguous meter reset, not counted":
+ "Réinitialisation du compteur ambiguë, non comptée",
+ Cumulative: "Cumulé",
+ "Reset total to zoomed range": "Réinitialiser le total sur la plage zoomée",
+ "Partial period": "Période partielle",
+ "Incomplete history": "Historique incomplet",
+ "Dashed outlines mark partial periods cut by the selected range; hatched or dotted marks show periods with gaps in recorded history.":
+ "Les contours en pointillés signalent les périodes partielles coupées par la plage sélectionnée ; les marques hachurées ou pointillées indiquent des périodes avec des lacunes dans l'historique enregistré.",
+ "Counts only include activity in retained history; earlier activity is not shown.":
+ "Les décomptes n'incluent que l'activité de l'historique conservé ; l'activité antérieure n'est pas affichée.",
+ "Count events": "Compter les événements",
+ Interval: "Intervalle",
+ State: "État",
+ "Every minute": "Chaque minute",
+ "Every 15 minutes": "Toutes les 15 minutes",
+ "Every 30 minutes": "Toutes les 30 minutes",
+};
diff --git a/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/pt.ts b/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/pt.ts
new file mode 100644
index 00000000..a859231f
--- /dev/null
+++ b/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/pt.ts
@@ -0,0 +1,47 @@
+import type { ComponentTranslations } from "@/lib/i18n/types";
+
+export const translations: ComponentTranslations = {
+ "Aggregate series": "Séries agregadas",
+ "Count changes to a state": "Contar mudanças para um estado",
+ "Count all state changes": "Contar todas as mudanças de estado",
+ "Time in a state": "Tempo num estado",
+ "Percentage of time in a state": "Percentagem de tempo num estado",
+ Hourly: "Por hora",
+ Daily: "Diário",
+ Weekly: "Semanal",
+ Monthly: "Mensal",
+ On: "Ligado",
+ Off: "Desligado",
+ Open: "Aberto",
+ Closed: "Fechado",
+ Detected: "Detetado",
+ Clear: "Livre",
+ Occupied: "Ocupado",
+ Unlocked: "Desbloqueado",
+ Locked: "Bloqueado",
+ Mean: "Média",
+ Minimum: "Mínimo",
+ Maximum: "Máximo",
+ Median: "Mediana",
+ "First reading": "Primeira leitura",
+ "Last reading": "Última leitura",
+ "All event types": "Todos os tipos de evento",
+ Consumption: "Consumo",
+ "Meter reset detected": "Reposição do contador detetada",
+ "Ambiguous meter reset, not counted":
+ "Reposição do contador ambígua, não contabilizada",
+ Cumulative: "Acumulado",
+ "Reset total to zoomed range": "Repor o total no intervalo ampliado",
+ "Partial period": "Período parcial",
+ "Incomplete history": "Histórico incompleto",
+ "Dashed outlines mark partial periods cut by the selected range; hatched or dotted marks show periods with gaps in recorded history.":
+ "Os contornos tracejados indicam períodos parciais cortados pelo intervalo selecionado; as marcas tracejadas finas ou pontilhadas indicam períodos com lacunas no histórico registado.",
+ "Counts only include activity in retained history; earlier activity is not shown.":
+ "As contagens só incluem a atividade do histórico retido; a atividade anterior não é mostrada.",
+ "Count events": "Contar eventos",
+ Interval: "Intervalo",
+ State: "Estado",
+ "Every minute": "A cada minuto",
+ "Every 15 minutes": "A cada 15 minutos",
+ "Every 30 minutes": "A cada 30 minutos",
+};
diff --git a/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/zh-hans.ts b/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/zh-hans.ts
new file mode 100644
index 00000000..4a399505
--- /dev/null
+++ b/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/i18n/zh-hans.ts
@@ -0,0 +1,46 @@
+import type { ComponentTranslations } from "@/lib/i18n/types";
+
+export const translations: ComponentTranslations = {
+ "Aggregate series": "聚合序列",
+ "Count changes to a state": "统计变为某状态的次数",
+ "Count all state changes": "统计所有状态变化次数",
+ "Time in a state": "处于某状态的时长",
+ "Percentage of time in a state": "处于某状态的时间百分比",
+ Hourly: "每小时",
+ Daily: "每天",
+ Weekly: "每周",
+ Monthly: "每月",
+ On: "开",
+ Off: "关",
+ Open: "打开",
+ Closed: "关闭",
+ Detected: "已检测到",
+ Clear: "无人",
+ Occupied: "有人",
+ Unlocked: "已解锁",
+ Locked: "已锁定",
+ Mean: "平均值",
+ Minimum: "最小值",
+ Maximum: "最大值",
+ Median: "中位数",
+ "First reading": "首次读数",
+ "Last reading": "最后读数",
+ "All event types": "所有事件类型",
+ Consumption: "用量",
+ "Meter reset detected": "检测到电表重置",
+ "Ambiguous meter reset, not counted": "重置情况不明确,未计入",
+ Cumulative: "累计",
+ "Reset total to zoomed range": "将累计值重置为缩放范围",
+ "Partial period": "部分时段",
+ "Incomplete history": "历史记录不完整",
+ "Dashed outlines mark partial periods cut by the selected range; hatched or dotted marks show periods with gaps in recorded history.":
+ "虚线轮廓表示被所选范围截断的部分时段;阴影或点状标记表示已记录历史中存在缺口的时段。",
+ "Counts only include activity in retained history; earlier activity is not shown.":
+ "计数仅包含保留历史中的活动;更早的活动不会显示。",
+ "Count events": "统计事件数",
+ Interval: "间隔",
+ State: "状态",
+ "Every minute": "每分钟",
+ "Every 15 minutes": "每 15 分钟",
+ "Every 30 minutes": "每 30 分钟",
+};
diff --git a/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/stories/analysis-aggregate-group.stories.ts b/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/stories/analysis-aggregate-group.stories.ts
new file mode 100644
index 00000000..80a8c3b9
--- /dev/null
+++ b/custom_components/hass_datapoints/src/molecules/analysis-aggregate-group/stories/analysis-aggregate-group.stories.ts
@@ -0,0 +1,88 @@
+import { html } from "lit";
+import { expect, userEvent } from "@storybook/test";
+import "../analysis-aggregate-group";
+import type { AggregateDefinition } from "@/lib/domain/aggregation/types";
+
+export default {
+ title: "Molecules/Analysis/Aggregate Group",
+ component: "analysis-aggregate-group",
+ parameters: {
+ actions: { handles: ["dp-group-analysis-change"] },
+ },
+};
+
+const opens: AggregateDefinition = {
+ id: "transition_count",
+ operation: "transition_count",
+ interval: "day",
+ mode: "interval",
+ states: ["on"],
+ resetOnZoom: false,
+};
+
+export const Off = {
+ render: () => html`
+
+ `,
+};
+
+/** Checked aggregates show their settings nested beneath them. */
+export const DoorSensorWithAggregates = {
+ render: () => html`
+
+ `,
+ play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
+ const group = canvasElement.querySelector(
+ "analysis-aggregate-group"
+ ) as HTMLElement;
+ const events: CustomEvent[] = [];
+ group.addEventListener("dp-group-analysis-change", (e) =>
+ events.push(e as CustomEvent)
+ );
+ // Keyboard: focus an unchecked choice and press space to enable it.
+ const percentage = group.shadowRoot!.querySelector(
+ '.aggregate-choice[data-key="state_percentage"]'
+ ) as HTMLInputElement;
+ percentage.focus();
+ await userEvent.keyboard(" ");
+ await expect(events.at(-1)!.detail.value).toHaveLength(3);
+ },
+};
+
+export const NumericMeter = {
+ render: () => html`
+
+ `,
+};
diff --git a/custom_components/hass_datapoints/src/molecules/target-row/__tests__/target-row-aggregates.spec.ts b/custom_components/hass_datapoints/src/molecules/target-row/__tests__/target-row-aggregates.spec.ts
new file mode 100644
index 00000000..0a98d8b2
--- /dev/null
+++ b/custom_components/hass_datapoints/src/molecules/target-row/__tests__/target-row-aggregates.spec.ts
@@ -0,0 +1,67 @@
+import { afterEach, describe, expect, it } from "vitest";
+import { _hasActiveAnalysis, _hasConfiguredAnalysis } from "../target-row";
+import { normalizeHistorySeriesAnalysis } from "@/lib/domain/history-series";
+import type { NormalizedAnalysis } from "../types";
+
+const withAggregate = (hide = false) =>
+ normalizeHistorySeriesAnalysis({
+ aggregates: [
+ {
+ id: "a1",
+ operation: "state_duration",
+ interval: "day",
+ states: ["on"],
+ },
+ ],
+ hide_source_series: hide,
+ }) as unknown as NormalizedAnalysis;
+
+describe("target-row aggregates", () => {
+ let el: HTMLElement | undefined;
+ afterEach(() => el?.remove());
+
+ describe("GIVEN a row with an aggregate and no other analysis", () => {
+ describe("WHEN checking analysis state", () => {
+ it("THEN it is configured and active so Hide source series is enabled", () => {
+ expect.assertions(2);
+ expect(_hasConfiguredAnalysis(withAggregate())).toBe(true);
+ expect(_hasActiveAnalysis(withAggregate(), false)).toBe(true);
+ });
+ });
+ });
+
+ describe("GIVEN a row whose last aggregate was removed", () => {
+ describe("WHEN checking analysis state", () => {
+ it("THEN it is inactive but keeps the stored hide preference", () => {
+ expect.assertions(2);
+ const analysis = normalizeHistorySeriesAnalysis({
+ aggregates: [],
+ hide_source_series: true,
+ }) as unknown as NormalizedAnalysis;
+ expect(_hasActiveAnalysis(analysis, false)).toBe(false);
+ expect(analysis.hide_source_series).toBe(true);
+ });
+ });
+ });
+
+ describe("GIVEN an expanded binary row with display options enabled", () => {
+ describe("WHEN rendered", () => {
+ it("THEN the aggregate group and Hide source series are offered", async () => {
+ expect.assertions(2);
+ el = document.createElement("target-row");
+ Object.assign(el, {
+ entityId: "binary_sensor.front_door",
+ showBinaryDisplay: true,
+ analysis: { ...withAggregate(), expanded: true },
+ });
+ document.body.appendChild(el);
+ await (el as HTMLElement & { updateComplete: Promise })
+ .updateComplete;
+ expect(
+ el.shadowRoot!.querySelector("analysis-aggregate-group")
+ ).not.toBeNull();
+ expect(el.shadowRoot!.textContent).toContain("Hide source series");
+ });
+ });
+ });
+});
diff --git a/custom_components/hass_datapoints/src/molecules/target-row/target-row.ts b/custom_components/hass_datapoints/src/molecules/target-row/target-row.ts
index 2b1e8f50..283312a3 100644
--- a/custom_components/hass_datapoints/src/molecules/target-row/target-row.ts
+++ b/custom_components/hass_datapoints/src/molecules/target-row/target-row.ts
@@ -4,6 +4,8 @@ import { localized, msg } from "@/lib/i18n/localize";
import { styles } from "./target-row.styles";
import type { HassLike, HassState } from "@/lib/types";
+import { getAggregateOperations } from "@/lib/domain/aggregation/aggregation";
+import { isNumericAggregateOperation } from "@/lib/domain/aggregation/labels";
import {
type BinaryDisplayMode,
isBinarySensorEntity,
@@ -28,6 +30,7 @@ import "@/molecules/analysis-rate-group/analysis-rate-group";
import "@/molecules/analysis-threshold-group/analysis-threshold-group";
import "@/molecules/analysis-anomaly-group/analysis-anomaly-group";
import "@/molecules/analysis-delta-group/analysis-delta-group";
+import "@/molecules/analysis-aggregate-group/analysis-aggregate-group";
export type { NormalizedAnalysis, ComparisonWindow, HassEntityState };
@@ -60,6 +63,10 @@ export function deriveSwatchIconColor(color: string): string {
return `rgb(${mixed[0]}, ${mixed[1]}, ${mixed[2]})`;
}
+function hasAggregates(a: NormalizedAnalysis): boolean {
+ return (a.aggregates?.length ?? 0) > 0;
+}
+
export function _hasConfiguredAnalysis(a: NormalizedAnalysis): boolean {
return (
a.show_trend_lines ||
@@ -69,7 +76,8 @@ export function _hasConfiguredAnalysis(a: NormalizedAnalysis): boolean {
a.show_anomalies ||
a.show_delta_analysis ||
a.stepped_series ||
- a.hide_source_series
+ a.hide_source_series ||
+ hasAggregates(a)
);
}
@@ -83,7 +91,8 @@ export function _hasActiveAnalysis(
a.show_rate_of_change ||
a.show_threshold_analysis ||
a.show_anomalies ||
- (a.show_delta_analysis && hasComparisonWindow)
+ (a.show_delta_analysis && hasComparisonWindow) ||
+ hasAggregates(a)
);
}
@@ -187,6 +196,8 @@ export class TargetRow extends LitElement {
private _isBinary = false;
+ private _isEvent = false;
+
override willUpdate(changed: PropertyValues) {
if (
changed.has("stateObj") ||
@@ -204,6 +215,7 @@ export class TargetRow extends LitElement {
this._supportsAnalysis =
Boolean(this._entityId) && !this._entityId.startsWith("binary_sensor.");
this._isBinary = isBinarySensorEntity(this._entityId);
+ this._isEvent = this._entityId.startsWith("event.");
}
}
@@ -391,7 +403,7 @@ export class TargetRow extends LitElement {
- ${this._supportsAnalysis && this.analysis?.expanded
+ ${this._supportsAnalysis && !this._isEvent && this.analysis?.expanded
? html`
@@ -409,6 +421,17 @@ export class TargetRow extends LitElement {
/>
${msg("Stepped series")}
+
`
: nothing}
+ ${this._isEvent && expanded
+ ? html`
+
+ `
+ : nothing}
${hasBinaryDisplayOption && expanded
? html`
@@ -506,6 +548,30 @@ export class TargetRow extends LitElement {
@dp-change=${this._onBinaryDisplayChange}
>
+
+
+
+
+ this._onCheckbox("hide_source_series", e)}
+ />
+ ${msg("Hide source series")}
+
+
`
diff --git a/custom_components/hass_datapoints/src/molecules/target-row/types.ts b/custom_components/hass_datapoints/src/molecules/target-row/types.ts
index f67628cf..b870b91b 100644
--- a/custom_components/hass_datapoints/src/molecules/target-row/types.ts
+++ b/custom_components/hass_datapoints/src/molecules/target-row/types.ts
@@ -1,8 +1,10 @@
+import type { AggregateDefinition } from "@/lib/domain/aggregation/types";
// ---------------------------------------------------------------------------
// Shared types for target-row and related components
// ---------------------------------------------------------------------------
export interface NormalizedAnalysis {
+ aggregates?: AggregateDefinition[];
expanded: boolean;
show_trend_lines: boolean;
trend_method: string;
diff --git a/custom_components/hass_datapoints/src/panels/datapoints/__tests__/datapoints-url-sync.spec.ts b/custom_components/hass_datapoints/src/panels/datapoints/__tests__/datapoints-url-sync.spec.ts
index 716be21a..5b2f829b 100644
--- a/custom_components/hass_datapoints/src/panels/datapoints/__tests__/datapoints-url-sync.spec.ts
+++ b/custom_components/hass_datapoints/src/panels/datapoints/__tests__/datapoints-url-sync.spec.ts
@@ -108,3 +108,69 @@ describe("HassDatapointsHistoryPanel URL sync", () => {
});
});
});
+
+describe("datapoints panel aggregate series", () => {
+ const agg = (id: string, operation: string) => ({
+ id,
+ operation,
+ interval: "day",
+ mode: "interval",
+ states: ["on"],
+ resetOnZoom: false,
+ });
+ const makePanel = (analysis: unknown) => ({
+ _seriesRows: [
+ {
+ entity_id: "binary_sensor.door",
+ color: "#03a9f4",
+ visible: true,
+ analysis: normalizeHistorySeriesAnalysis(analysis),
+ },
+ ],
+ _saveSessionState: vi.fn(),
+ _updateUrl: vi.fn(),
+ _renderTargetRows: vi.fn(),
+ _renderSidebarOptions: vi.fn(),
+ _renderContent: vi.fn(),
+ });
+ const setAggregates = (panel: ReturnType
, value: unknown) =>
+ HassDatapointsHistoryPanel.prototype._setSeriesAnalysisOption.call(
+ panel,
+ "binary_sensor.door",
+ "aggregates",
+ value
+ );
+
+ describe("GIVEN a binary row with no aggregates", () => {
+ describe("WHEN two independent aggregates are added", () => {
+ it("THEN both are stored, the session saved and the chart re-rendered", () => {
+ expect.assertions(3);
+ const panel = makePanel(null);
+ setAggregates(panel, [
+ agg("a1", "transition_count"),
+ agg("a2", "state_duration"),
+ ]);
+ expect(
+ panel._seriesRows[0].analysis.aggregates?.map((d) => d.operation)
+ ).toStrictEqual(["transition_count", "state_duration"]);
+ expect(panel._saveSessionState).toHaveBeenCalledTimes(1);
+ expect(panel._renderContent).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+
+ describe("GIVEN a row hiding its source behind an aggregate", () => {
+ describe("WHEN the last aggregate is removed", () => {
+ it("THEN the stored hide preference is retained", () => {
+ expect.assertions(2);
+ const panel = makePanel({
+ aggregates: [agg("a1", "transition_count")],
+ hide_source_series: true,
+ });
+ setAggregates(panel, []);
+ expect(panel._seriesRows[0].analysis.aggregates).toStrictEqual([]);
+ expect(panel._seriesRows[0].analysis.hide_source_series).toBe(true);
+ });
+ });
+ });
+});
diff --git a/package.json b/package.json
index 0df06627..6f80332b 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,8 @@
"lint": "pnpm run lint:eslint && pnpm run lint:package-json && pnpm run lint:prettier && pnpm run lint:types",
"format": "pnpm run format:eslint && pnpm run format:prettier",
"sb": "storybook dev -p 6006",
- "sb:build": "storybook build"
+ "sb:build": "storybook build",
+ "prepare": "husky"
},
"devDependencies": {
"@chromatic-com/storybook": "5.1.1",
@@ -61,6 +62,8 @@
"eslint-plugin-wc": "3.1.0",
"globals": "17.4.0",
"happy-dom": "20.8.9",
+ "husky": "9.1.7",
+ "lint-staged": "17.6.0",
"mockdate": "3.0.5",
"npm-package-json-lint": "10.4.0",
"playwright": "1.58.2",
@@ -87,5 +90,14 @@
"@lit/context": "1.1.6",
"@lit/localize": "0.12.2",
"lit": "3.3.2"
+ },
+ "lint-staged": {
+ "custom_components/hass_datapoints/src/**/*.{js,ts}": [
+ "eslint --fix --max-warnings=0",
+ "prettier --write"
+ ],
+ "*.{json,md,yaml,yml,css}": [
+ "prettier --write"
+ ]
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index bf0cba91..dac564b3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -46,7 +46,7 @@ importers:
version: 10.3.3(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))
"@storybook/addon-docs":
specifier: 10.3.3
- version: 10.3.3(@types/react@19.2.14)(esbuild@0.28.2)(rollup@4.60.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))
+ version: 10.3.3(@types/react@19.2.14)(esbuild@0.28.2)(rollup@4.60.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))
"@storybook/addon-mcp":
specifier: 0.5.0
version: 0.5.0(@storybook/addon-vitest@10.3.3(@vitest/browser-playwright@4.1.11)(@vitest/browser@4.1.11)(@vitest/runner@4.1.11)(react@19.3.0)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))(vitest@4.1.11))(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))(typescript@5.9.3)
@@ -58,13 +58,13 @@ importers:
version: 8.6.15(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))
"@storybook/web-components-vite":
specifier: 10.3.3
- version: 10.3.3(esbuild@0.28.2)(lit@3.3.2)(rollup@4.60.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))
+ version: 10.3.3(esbuild@0.28.2)(lit@3.3.2)(rollup@4.60.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))
"@vitest/browser":
specifier: 4.1.11
- version: 4.1.11(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))(vitest@4.1.11)
+ version: 4.1.11(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))(vitest@4.1.11)
"@vitest/browser-playwright":
specifier: 4.1.11
- version: 4.1.11(playwright@1.58.2)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))(vitest@4.1.11)
+ version: 4.1.11(playwright@1.58.2)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))(vitest@4.1.11)
"@vitest/coverage-v8":
specifier: 4.1.11
version: 4.1.11(@vitest/browser@4.1.11)(vitest@4.1.11)
@@ -101,6 +101,12 @@ importers:
happy-dom:
specifier: 20.8.9
version: 20.8.9
+ husky:
+ specifier: 9.1.7
+ version: 9.1.7
+ lint-staged:
+ specifier: 17.6.0
+ version: 17.6.0
mockdate:
specifier: 3.0.5
version: 3.0.5
@@ -124,10 +130,10 @@ importers:
version: 8.58.1(eslint@10.4.0)(typescript@5.9.3)
vite:
specifier: 8.3.3
- version: 8.3.3(@types/node@26.6.4)(esbuild@0.28.2)
+ version: 8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1)
vitest:
specifier: 4.1.11
- version: 4.1.11(@types/node@26.6.4)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.8.9)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))
+ version: 4.1.11(@types/node@26.6.4)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.8.9)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))
packages:
"@adobe/css-tools@4.5.0":
@@ -3313,6 +3319,14 @@ packages:
integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==,
}
+ husky@9.1.7:
+ resolution:
+ {
+ integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==,
+ }
+ engines: { node: ">=18" }
+ hasBin: true
+
ignore@5.3.2:
resolution:
{
@@ -3877,6 +3891,14 @@ packages:
integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==,
}
+ lint-staged@17.6.0:
+ resolution:
+ {
+ integrity: sha512-nOhBfYkZEwkFtNzD5pK9s9LkcWoxJ+whLzEnhqi9nl7zTB5n7tg8+9iYgMGx2Wm3VlLu8cbNj0RACzY+nWi5iQ==,
+ }
+ engines: { node: ">=22.22.1" }
+ hasBin: true
+
lit-element@4.2.2:
resolution:
{
@@ -4050,18 +4072,18 @@ packages:
integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==,
}
- mrmime@2.0.1:
+ mockdate@3.0.5:
resolution:
{
- integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==,
+ integrity: sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==,
}
- engines: { node: ">=10" }
- mockdate@3.0.5:
+ mrmime@2.0.1:
resolution:
{
- integrity: sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==,
+ integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==,
}
+ engines: { node: ">=10" }
ms@2.1.3:
resolution:
@@ -4790,6 +4812,13 @@ packages:
prettier:
optional: true
+ string-argv@0.3.2:
+ resolution:
+ {
+ integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==,
+ }
+ engines: { node: ">=0.6.19" }
+
string.prototype.includes@2.0.1:
resolution:
{
@@ -5366,6 +5395,14 @@ packages:
integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==,
}
+ yaml@2.9.1:
+ resolution:
+ {
+ integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==,
+ }
+ engines: { node: ">= 14.6" }
+ hasBin: true
+
yargs-parser@20.2.9:
resolution:
{
@@ -5878,10 +5915,10 @@ snapshots:
axe-core: 4.14.0
storybook: 10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0)
- "@storybook/addon-docs@10.3.3(@types/react@19.2.14)(esbuild@0.28.2)(rollup@4.60.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))":
+ "@storybook/addon-docs@10.3.3(@types/react@19.2.14)(esbuild@0.28.2)(rollup@4.60.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))":
dependencies:
"@mdx-js/react": 3.1.1(@types/react@19.2.14)(react@19.3.0)
- "@storybook/csf-plugin": 10.3.3(esbuild@0.28.2)(rollup@4.60.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))
+ "@storybook/csf-plugin": 10.3.3(esbuild@0.28.2)(rollup@4.60.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))
"@storybook/icons": 2.1.0(react@19.3.0)
"@storybook/react-dom-shim": 10.3.3(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))
react: 19.3.0
@@ -5916,32 +5953,32 @@ snapshots:
"@storybook/icons": 2.1.0(react@19.3.0)
storybook: 10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0)
optionalDependencies:
- "@vitest/browser": 4.1.11(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))(vitest@4.1.11)
- "@vitest/browser-playwright": 4.1.11(playwright@1.58.2)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))(vitest@4.1.11)
+ "@vitest/browser": 4.1.11(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))(vitest@4.1.11)
+ "@vitest/browser-playwright": 4.1.11(playwright@1.58.2)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))(vitest@4.1.11)
"@vitest/runner": 4.1.11
- vitest: 4.1.11(@types/node@26.6.4)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.8.9)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))
+ vitest: 4.1.11(@types/node@26.6.4)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.8.9)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))
transitivePeerDependencies:
- react
- "@storybook/builder-vite@10.3.3(esbuild@0.28.2)(rollup@4.60.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))":
+ "@storybook/builder-vite@10.3.3(esbuild@0.28.2)(rollup@4.60.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))":
dependencies:
- "@storybook/csf-plugin": 10.3.3(esbuild@0.28.2)(rollup@4.60.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))
+ "@storybook/csf-plugin": 10.3.3(esbuild@0.28.2)(rollup@4.60.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))
storybook: 10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0)
ts-dedent: 2.3.0
- vite: 8.3.3(@types/node@26.6.4)(esbuild@0.28.2)
+ vite: 8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1)
transitivePeerDependencies:
- esbuild
- rollup
- webpack
- "@storybook/csf-plugin@10.3.3(esbuild@0.28.2)(rollup@4.60.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))":
+ "@storybook/csf-plugin@10.3.3(esbuild@0.28.2)(rollup@4.60.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))":
dependencies:
storybook: 10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0)
unplugin: 2.3.11
optionalDependencies:
esbuild: 0.28.2
rollup: 4.60.1
- vite: 8.3.3(@types/node@26.6.4)(esbuild@0.28.2)
+ vite: 8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1)
"@storybook/global@5.0.0": {}
@@ -5982,9 +6019,9 @@ snapshots:
"@vitest/spy": 2.0.5
storybook: 10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0)
- "@storybook/web-components-vite@10.3.3(esbuild@0.28.2)(lit@3.3.2)(rollup@4.60.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))":
+ "@storybook/web-components-vite@10.3.3(esbuild@0.28.2)(lit@3.3.2)(rollup@4.60.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))":
dependencies:
- "@storybook/builder-vite": 10.3.3(esbuild@0.28.2)(rollup@4.60.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))
+ "@storybook/builder-vite": 10.3.3(esbuild@0.28.2)(rollup@4.60.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))
"@storybook/web-components": 10.3.3(lit@3.3.2)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0))
storybook: 10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react@19.3.0)
transitivePeerDependencies:
@@ -6289,29 +6326,29 @@ snapshots:
dependencies:
valibot: 1.5.0(typescript@5.9.3)
- "@vitest/browser-playwright@4.1.11(playwright@1.58.2)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))(vitest@4.1.11)":
+ "@vitest/browser-playwright@4.1.11(playwright@1.58.2)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))(vitest@4.1.11)":
dependencies:
- "@vitest/browser": 4.1.11(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))(vitest@4.1.11)
- "@vitest/mocker": 4.1.11(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))
+ "@vitest/browser": 4.1.11(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))(vitest@4.1.11)
+ "@vitest/mocker": 4.1.11(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))
playwright: 1.58.2
tinyrainbow: 3.2.0
- vitest: 4.1.11(@types/node@26.6.4)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.8.9)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))
+ vitest: 4.1.11(@types/node@26.6.4)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.8.9)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))
transitivePeerDependencies:
- bufferutil
- msw
- utf-8-validate
- vite
- "@vitest/browser@4.1.11(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))(vitest@4.1.11)":
+ "@vitest/browser@4.1.11(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))(vitest@4.1.11)":
dependencies:
"@blazediff/core": 1.9.1
- "@vitest/mocker": 4.1.11(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))
+ "@vitest/mocker": 4.1.11(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))
"@vitest/utils": 4.1.11
magic-string: 0.30.21
pngjs: 7.0.0
sirv: 3.0.2
tinyrainbow: 3.2.0
- vitest: 4.1.11(@types/node@26.6.4)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.8.9)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))
+ vitest: 4.1.11(@types/node@26.6.4)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.8.9)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))
ws: 8.22.0
transitivePeerDependencies:
- bufferutil
@@ -6331,9 +6368,9 @@ snapshots:
obug: 2.2.1
std-env: 4.3.0
tinyrainbow: 3.2.0
- vitest: 4.1.11(@types/node@26.6.4)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.8.9)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))
+ vitest: 4.1.11(@types/node@26.6.4)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.8.9)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))
optionalDependencies:
- "@vitest/browser": 4.1.11(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))(vitest@4.1.11)
+ "@vitest/browser": 4.1.11(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))(vitest@4.1.11)
"@vitest/expect@2.0.5":
dependencies:
@@ -6359,13 +6396,13 @@ snapshots:
chai: 6.3.0
tinyrainbow: 3.2.0
- "@vitest/mocker@4.1.11(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))":
+ "@vitest/mocker@4.1.11(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))":
dependencies:
"@vitest/spy": 4.1.11
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
- vite: 8.3.3(@types/node@26.6.4)(esbuild@0.28.2)
+ vite: 8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1)
"@vitest/pretty-format@2.0.5":
dependencies:
@@ -6414,7 +6451,7 @@ snapshots:
sirv: 3.0.2
tinyglobby: 0.2.17
tinyrainbow: 3.2.0
- vitest: 4.1.11(@types/node@26.6.4)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.8.9)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))
+ vitest: 4.1.11(@types/node@26.6.4)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.8.9)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))
"@vitest/utils@2.0.5":
dependencies:
@@ -7453,6 +7490,8 @@ snapshots:
html-escaper@2.0.2: {}
+ husky@9.1.7: {}
+
ignore@5.3.2: {}
ignore@7.0.12: {}
@@ -7745,6 +7784,14 @@ snapshots:
lines-and-columns@1.2.4: {}
+ lint-staged@17.6.0:
+ dependencies:
+ picomatch: 4.0.7
+ string-argv: 0.3.2
+ tinyexec: 1.3.1
+ optionalDependencies:
+ yaml: 2.9.1
+
lit-element@4.2.2:
dependencies:
"@lit-labs/ssr-dom-shim": 1.6.0
@@ -7851,10 +7898,10 @@ snapshots:
minimist@1.2.8: {}
- mrmime@2.0.1: {}
-
mockdate@3.0.5: {}
+ mrmime@2.0.1: {}
+
ms@2.1.3: {}
nanoid@3.3.20: {}
@@ -8360,6 +8407,8 @@ snapshots:
- react
- utf-8-validate
+ string-argv@0.3.2: {}
+
string.prototype.includes@2.0.1:
dependencies:
call-bind: 1.0.9
@@ -8623,7 +8672,7 @@ snapshots:
validate-npm-package-name@6.0.0: {}
- vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2):
+ vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1):
dependencies:
lightningcss: 1.33.0
picomatch: 4.0.7
@@ -8634,11 +8683,12 @@ snapshots:
"@types/node": 26.6.4
esbuild: 0.28.2
fsevents: 2.3.3
+ yaml: 2.9.1
- vitest@4.1.11(@types/node@26.6.4)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.8.9)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)):
+ vitest@4.1.11(@types/node@26.6.4)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.8.9)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1)):
dependencies:
"@vitest/expect": 4.1.11
- "@vitest/mocker": 4.1.11(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))
+ "@vitest/mocker": 4.1.11(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))
"@vitest/pretty-format": 4.1.11
"@vitest/runner": 4.1.11
"@vitest/snapshot": 4.1.11
@@ -8655,11 +8705,11 @@ snapshots:
tinyexec: 1.3.1
tinyglobby: 0.2.17
tinyrainbow: 3.2.0
- vite: 8.3.3(@types/node@26.6.4)(esbuild@0.28.2)
+ vite: 8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1)
why-is-node-running: 2.3.0
optionalDependencies:
"@types/node": 26.6.4
- "@vitest/browser-playwright": 4.1.11(playwright@1.58.2)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2))(vitest@4.1.11)
+ "@vitest/browser-playwright": 4.1.11(playwright@1.58.2)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(yaml@2.9.1))(vitest@4.1.11)
"@vitest/coverage-v8": 4.1.11(@vitest/browser@4.1.11)(vitest@4.1.11)
"@vitest/ui": 4.1.11(vitest@4.1.11)
happy-dom: 20.8.9
@@ -8732,6 +8782,9 @@ snapshots:
yallist@4.0.0: {}
+ yaml@2.9.1:
+ optional: true
+
yargs-parser@20.2.9: {}
yocto-queue@0.1.0: {}