@kipk/ha-better-history 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +405 -0
- package/dist/controllers/data-controller.d.ts +38 -0
- package/dist/controllers/tooltip-controller.d.ts +56 -0
- package/dist/data/attribute-units.d.ts +5 -0
- package/dist/data/format.d.ts +5 -0
- package/dist/data/history-queue.d.ts +24 -0
- package/dist/data/history.d.ts +63 -0
- package/dist/data/resolve-config.d.ts +28 -0
- package/dist/data/value-type.d.ts +2 -0
- package/dist/define.d.ts +6 -0
- package/dist/define.js +4 -0
- package/dist/ha-better-history-BY4eqV1H.js +4410 -0
- package/dist/ha-better-history.d.ts +143 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +2 -0
- package/dist/load-ha-components.d.ts +2 -0
- package/dist/localize/localize.d.ts +2 -0
- package/dist/render/chart.d.ts +109 -0
- package/dist/render/climate-overlay.d.ts +10 -0
- package/dist/render/colors.d.ts +5 -0
- package/dist/render/downsample.d.ts +8 -0
- package/dist/render/scales.d.ts +40 -0
- package/dist/styles/chart.css.d.ts +1 -0
- package/dist/types/config.d.ts +73 -0
- package/dist/types/ha.d.ts +18 -0
- package/dist/ui/date-picker.d.ts +5 -0
- package/dist/ui/entity-picker.d.ts +35 -0
- package/dist/utils/performance.d.ts +3 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# ha-better-history
|
|
2
|
+
|
|
3
|
+
Author: @KipK
|
|
4
|
+
|
|
5
|
+
Standalone web component for Home Assistant history charts. Built with **Lit 3** and **TypeScript**. Renders SVG charts with no external charting dependencies.
|
|
6
|
+
|
|
7
|
+
**Status: WIP** — API not yet stable.
|
|
8
|
+
|
|
9
|
+
## Why ha-better-history?
|
|
10
|
+
|
|
11
|
+
`ha-better-history` keeps the familiar Home Assistant history-chart experience, but exposes it as a reusable web component with extra controls for dashboards, dialogs, and custom cards.
|
|
12
|
+
|
|
13
|
+
- **Familiar HA-style history UX, with more control** — time-series charts, legend toggles, tooltips, date ranges, and Home Assistant theming are kept close to the native experience while adding configuration and runtime tools.
|
|
14
|
+
- **Entity attributes as first-class series** — chart `entity.state` or any supported attribute path, including nested attributes. The picker can browse and search attributes, then add them directly to the chart.
|
|
15
|
+
- **Use only the chart or the full explorer** — render a minimal graph, enable just the date picker, or expose the complete viewer with tools, zoom, export, entity picker, and attribute picker.
|
|
16
|
+
- **No-refetch view tools** — zoom and pan inside the already loaded range without asking Home Assistant for the same history again.
|
|
17
|
+
- **Flexible rendering modes** — numeric series can be displayed as stair steps, straight lines, or columns, globally or per series.
|
|
18
|
+
- **Smarter multi-series layout** — automatic graph grouping by unit or explicit `scaleGroup`, optional manual Y ranges, dual-axis handling, and stable colors for readable comparison.
|
|
19
|
+
- **Climate-aware overlays** — when climate temperature and `hvac_action` are present, heating periods can be rendered as a contextual area overlay.
|
|
20
|
+
- **Runtime series editing** — users can add, remove, reorder, and hide non-default series from the UI without rebuilding the host card.
|
|
21
|
+
- **Portable export format** — visible data can be exported as compact `ha-better-history-series-v1` JSON for debugging, sharing, future analysis tools, or re-importing into the component.
|
|
22
|
+
- **Standalone integration surface** — the component can be embedded in Lovelace cards, dialogs, more-info style views, or any Home Assistant frontend context that can provide `hass`.
|
|
23
|
+
|
|
24
|
+
### Screenshots
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
```html
|
|
33
|
+
<ha-better-history></ha-better-history>
|
|
34
|
+
|
|
35
|
+
<script type="module" src="dist/define.js"></script>
|
|
36
|
+
<script>
|
|
37
|
+
const chart = document.querySelector("ha-better-history");
|
|
38
|
+
chart.hass = hass; // required: HomeAssistant instance
|
|
39
|
+
chart.entities = ["sensor.temperature", "sensor.humidity"];
|
|
40
|
+
</script>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Properties
|
|
44
|
+
|
|
45
|
+
All properties are camelCase in JS and kebab-case as HTML attributes (for boolean/string/number props). Object/complex props are JS-only (no attribute).
|
|
46
|
+
|
|
47
|
+
### Top-level attributes (HTML)
|
|
48
|
+
|
|
49
|
+
| Attribute | Type | Default | Description |
|
|
50
|
+
| -------------------- | --------- | --------- | ------------------------------------------------- |
|
|
51
|
+
| `hours` | `number` | `24` | Time range in hours before `endDate` |
|
|
52
|
+
| `show-date-picker` | `boolean` | `false` | Show `ha-date-range-picker` above the chart |
|
|
53
|
+
| `show-entity-picker` | `boolean` | `false` | Show entity picker + attribute browser |
|
|
54
|
+
| `show-import-button` | `boolean` | `false` | Show a JSON import button in the tools panel |
|
|
55
|
+
| `show-legend` | `boolean` | `true` | Legend below the chart |
|
|
56
|
+
| `show-tooltip` | `boolean` | `true` | Multi-series tooltip on hover |
|
|
57
|
+
| `width` | `string` | `"100%"` | CSS width of the component wrapper |
|
|
58
|
+
| `height` | `string` | — | CSS height; if omitted, computed from graph count |
|
|
59
|
+
| `line-mode` | `string` | `"stair"` | Global numeric display mode: `"stair"`, `"line"`, or `"column"` |
|
|
60
|
+
| `line-width` | `string` | `"2.5"` | Global SVG stroke width for numeric lines |
|
|
61
|
+
| `background-color` | `string` | transparent | CSS background color for the component wrapper |
|
|
62
|
+
| `graph-title` | `string` | — | Optional title above the chart |
|
|
63
|
+
| `title-font-family` | `string` | HA theme | Optional title font-family override |
|
|
64
|
+
| `title-font-size` | `string` | HA theme | Optional title font-size override |
|
|
65
|
+
| `title-color` | `string` | HA theme | Optional title color override |
|
|
66
|
+
| `language` | `string` | HA locale | Language code for labels (`"en"`, `"fr"`, …) |
|
|
67
|
+
| `tools-open` | `boolean` | `false` | Open/close the viewer tools panel from outside |
|
|
68
|
+
|
|
69
|
+
### JS-only properties
|
|
70
|
+
|
|
71
|
+
| Property | Type | Default | Description |
|
|
72
|
+
| ---------------- | --------------------- | ----------- | -------------------------------------------------- |
|
|
73
|
+
| `hass` | `HomeAssistant` | — | **Required.** The Home Assistant object |
|
|
74
|
+
| `config` | `BetterHistoryConfig` | `undefined` | Full declarative configuration |
|
|
75
|
+
| `entities` | `string[]` | `undefined` | Shortcut: entity IDs to plot their `state` |
|
|
76
|
+
| `startDate` | `Date` | `undefined` | Lower bound (overrides `hours`) |
|
|
77
|
+
| `endDate` | `Date` | `undefined` | Upper bound (default: now) |
|
|
78
|
+
| `attributeUnits` | `AttributeUnitMap` | `undefined` | Map from attribute dot-paths to display units |
|
|
79
|
+
|
|
80
|
+
If `endDate` is in the future, the component fetches and renders only up to the current time. The visible time axis then advances live until the requested end is reached, using current `hass.states` updates for entity and attribute points instead of refetching Home Assistant history for every update.
|
|
81
|
+
|
|
82
|
+
## `BetterHistoryConfig`
|
|
83
|
+
|
|
84
|
+
The `config` property accepts a `BetterHistoryConfig` object. Every field is optional — the component does something reasonable when nothing is provided.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
interface BetterHistoryConfig {
|
|
88
|
+
// Time window
|
|
89
|
+
hours?: number; // default: 24
|
|
90
|
+
startDate?: Date;
|
|
91
|
+
endDate?: Date;
|
|
92
|
+
|
|
93
|
+
// UI chrome
|
|
94
|
+
showDatePicker?: boolean; // default: false
|
|
95
|
+
showEntityPicker?: boolean; // default: false
|
|
96
|
+
showImportButton?: boolean; // default: false
|
|
97
|
+
showLegend?: boolean; // default: true
|
|
98
|
+
showTooltip?: boolean; // default: true
|
|
99
|
+
width?: string; // default: "100%"
|
|
100
|
+
height?: string;
|
|
101
|
+
lineMode?: "stair" | "line" | "column"; // default: "stair"
|
|
102
|
+
lineWidth?: number | string; // default: "2.5"
|
|
103
|
+
backgroundColor?: string; // default: transparent
|
|
104
|
+
title?: string; // omitted/empty = no title
|
|
105
|
+
titleFontFamily?: string; // default: HA/theme font
|
|
106
|
+
titleFontSize?: string; // default: HA/theme title size
|
|
107
|
+
titleColor?: string; // default: HA/theme text color
|
|
108
|
+
|
|
109
|
+
// Data
|
|
110
|
+
series?: SeriesConfig[]; // explicit series list
|
|
111
|
+
defaultEntities?: string[]; // shown in entity picker when enabled
|
|
112
|
+
disableClimateOverlay?: boolean; // default: false
|
|
113
|
+
|
|
114
|
+
// Attribute units
|
|
115
|
+
attributeUnits?: AttributeUnitMap; // map attribute dot-paths to display units
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### `SeriesConfig`
|
|
120
|
+
|
|
121
|
+
Each series describes what to plot and how it should be displayed.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
interface SeriesConfig {
|
|
125
|
+
entity: string; // Required: entity_id (e.g. "climate.living")
|
|
126
|
+
attribute?: string | string[]; // Dotted path or array; omit = entity.state
|
|
127
|
+
label?: string; // Legend label; default = friendly_name or attribute path
|
|
128
|
+
color?: string; // CSS color; default = automatic palette
|
|
129
|
+
unit?: string; // Override unit (for axis grouping and label)
|
|
130
|
+
|
|
131
|
+
scaleGroup?: string; // Series with same scaleGroup share a Y axis
|
|
132
|
+
scaleMode?: "auto" | "manual"; // default: "auto"
|
|
133
|
+
scaleMin?: number; // only when scaleMode = "manual"
|
|
134
|
+
scaleMax?: number; // only when scaleMode = "manual"
|
|
135
|
+
lineMode?: "stair" | "line" | "column"; // overrides global lineMode
|
|
136
|
+
lineWidth?: number | string; // overrides global lineWidth
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Attribute units
|
|
141
|
+
|
|
142
|
+
HA attributes have no native unit in history responses. Use `attributeUnits` to map attribute dot-paths to display units. This drives both axis grouping and label display.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
history.attributeUnits = {
|
|
146
|
+
"specific_states.ema_temperature": "temperature",
|
|
147
|
+
"power_percent": "%"
|
|
148
|
+
};
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Keys are dot-separated paths from `entity.attributes` (e.g. `"specific_states.ema_temperature"`). Matching is exact — no wildcards, no entity-id prefix. Values are the unit string to display.
|
|
152
|
+
|
|
153
|
+
Use the special value `"temperature"` for attributes that should use the active temperature unit. When a temperature graph exists, the component resolves it to the configured temperature unit such as `°C` or `°F`, so the attribute shares the same graph without hard-coding Celsius/Fahrenheit.
|
|
154
|
+
|
|
155
|
+
Unit resolution priority for a series:
|
|
156
|
+
1. `SeriesConfig.unit` (explicit, including empty string to suppress the unit)
|
|
157
|
+
2. `.attributeUnits` property
|
|
158
|
+
3. `config.attributeUnits`
|
|
159
|
+
4. `unit_of_measurement` for entity-state series
|
|
160
|
+
5. No unit
|
|
161
|
+
|
|
162
|
+
A numeric attribute with a temperature unit (`°C`, `°F`, `K`) is automatically placed in the same graph as other temperature series when a `group:temperature` group already exists. Likewise, attributes added via the entity picker receive their unit from the map before scale grouping is applied.
|
|
163
|
+
|
|
164
|
+
## Scale grouping rules
|
|
165
|
+
|
|
166
|
+
1. **Automatic (default)**: numeric series with the **same unit** share a graph and Y axis. Series with different units (or no unit) each get their own stacked graph. Non-numeric series (string/boolean) render as **colored segment ribbons** below the numeric graphs.
|
|
167
|
+
|
|
168
|
+
2. **Explicit `scaleGroup`**: series sharing a `scaleGroup` value share the same graph and Y axis regardless of unit. Un-grouped series continue to use rule 1 among themselves.
|
|
169
|
+
|
|
170
|
+
3. **`scaleMode: "manual"`**: locks the Y axis to `[scaleMin, scaleMax]`. If the series is in a shared scale group, the manual range takes priority: the axis is extended (never contracted) to accommodate the manual range.
|
|
171
|
+
|
|
172
|
+
## Colors
|
|
173
|
+
|
|
174
|
+
If `color` is not set, the built-in palette cycles through: `#ff9800`, `#42a5f5`, `#66bb6a`, `#ec407a`, `#ab47bc`, `#26a69a`.
|
|
175
|
+
|
|
176
|
+
## Line and title styling
|
|
177
|
+
|
|
178
|
+
Numeric series render as stair-step lines by default to match Home Assistant state history. Set `lineMode: "line"` globally, or per `SeriesConfig`, to connect points with straight segments. Set `lineMode: "column"` to render numeric values as time-span columns. `lineWidth` accepts an SVG stroke width such as `1.5`, `"2px"`, or `"0.18rem"` for line-based modes.
|
|
179
|
+
|
|
180
|
+
Use top-level HTML attributes for simple global styling:
|
|
181
|
+
|
|
182
|
+
```html
|
|
183
|
+
<ha-better-history
|
|
184
|
+
graph-title="Living room"
|
|
185
|
+
line-mode="line"
|
|
186
|
+
line-width="2"
|
|
187
|
+
background-color="transparent"
|
|
188
|
+
></ha-better-history>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Use `config` for per-series overrides:
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
chart.config = {
|
|
195
|
+
title: "Living room",
|
|
196
|
+
titleFontSize: "18px",
|
|
197
|
+
titleColor: "var(--primary-text-color)",
|
|
198
|
+
lineMode: "stair",
|
|
199
|
+
lineWidth: 2.5,
|
|
200
|
+
series: [
|
|
201
|
+
{ entity: "climate.living", attribute: "current_temperature", lineMode: "line", lineWidth: 2 },
|
|
202
|
+
{ entity: "climate.living", attribute: "temperature", lineWidth: 3 }
|
|
203
|
+
]
|
|
204
|
+
};
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Events
|
|
208
|
+
|
|
209
|
+
All events bubble and are composed.
|
|
210
|
+
|
|
211
|
+
| Event | Detail | When |
|
|
212
|
+
| ----------------- | -------------------------------------------------- | ----------------------------------------------------------- |
|
|
213
|
+
| `range-changed` | `{ startDate: Date, endDate: Date }` | Date picker changes |
|
|
214
|
+
| `view-range-changed` | `{ start: Date, end: Date }` | Tools range zoom changes without refetching history |
|
|
215
|
+
| `series-toggled` | `{ id: string, hidden: boolean }` | Legend item clicked |
|
|
216
|
+
| `series-added` | `{ source: HistorySource }` | User adds a series via entity picker |
|
|
217
|
+
| `series-removed` | `{ sourceId: string }` | User removes a non-default series |
|
|
218
|
+
| `series-reordered` | `{ sourceIds: string[] }` | User drags selected source chips into a new order |
|
|
219
|
+
| `data-imported` | `{ start: Date, end: Date, seriesCount: number }` | A `ha-better-history-series-v1` JSON file is imported |
|
|
220
|
+
| `tooltip-changed` | `{ time: number, values: TooltipValue[] } \| null` | Pointer moves over chart (useful for syncing multiple charts) |
|
|
221
|
+
| `picker-overlay-changed` | `{ open: boolean }` | Entity picker or attribute browser overlay opens/closes |
|
|
222
|
+
|
|
223
|
+
Legend toggles only keep visible series in the automatic numeric Y scale. Hidden numeric series remain available in the legend, but no longer stretch the scale for the displayed curves.
|
|
224
|
+
|
|
225
|
+
On touch screens, the tooltip is anchored away from the active finger position so values stay readable while scrubbing the chart.
|
|
226
|
+
|
|
227
|
+
## Default behaviour (no config)
|
|
228
|
+
|
|
229
|
+
When both `config` and `entities` are undefined:
|
|
230
|
+
|
|
231
|
+
- If the element has an `entity` attribute/prop, its `state` is plotted.
|
|
232
|
+
- Otherwise, renders an empty slot.
|
|
233
|
+
|
|
234
|
+
When `entities` is a non-empty array:
|
|
235
|
+
|
|
236
|
+
- Plots `entity.state` for each entity ID.
|
|
237
|
+
- Range = 24 hours.
|
|
238
|
+
- Date/entity pickers OFF, legend ON, tooltip ON.
|
|
239
|
+
|
|
240
|
+
## Recipes
|
|
241
|
+
|
|
242
|
+
### Two sensors with shared temperature scale
|
|
243
|
+
|
|
244
|
+
```js
|
|
245
|
+
chart.config = {
|
|
246
|
+
series: [
|
|
247
|
+
{ entity: "climate.living", attribute: "current_temperature", label: "Indoor", scaleGroup: "temp" },
|
|
248
|
+
{ entity: "sensor.outdoor_temp", label: "Outdoor", scaleGroup: "temp" },
|
|
249
|
+
]
|
|
250
|
+
};
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Climate entity with heating area overlay
|
|
254
|
+
|
|
255
|
+
When a chart includes **both** `current_temperature` and `hvac_action` attributes from the **same** climate entity, the component automatically draws a semi-transparent area under the temperature line during `"heating"` periods.
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
chart.config = {
|
|
259
|
+
series: [
|
|
260
|
+
{ entity: "climate.living", attribute: "current_temperature", label: "Temperature", color: "#42a5f5" },
|
|
261
|
+
{ entity: "climate.living", attribute: "hvac_action", label: "State" },
|
|
262
|
+
]
|
|
263
|
+
};
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Disable with `disableClimateOverlay: true`.
|
|
267
|
+
|
|
268
|
+
### Manual Y axis range
|
|
269
|
+
|
|
270
|
+
```js
|
|
271
|
+
chart.config = {
|
|
272
|
+
series: [
|
|
273
|
+
{ entity: "sensor.pressure", label: "Pressure", scaleMode: "manual", scaleMin: 960, scaleMax: 1040 }
|
|
274
|
+
]
|
|
275
|
+
};
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### With date picker enabled
|
|
279
|
+
|
|
280
|
+
```html
|
|
281
|
+
<ha-better-history show-date-picker></ha-better-history>
|
|
282
|
+
<script>
|
|
283
|
+
chart.addEventListener("range-changed", (e) => {
|
|
284
|
+
console.log(e.detail.startDate, e.detail.endDate);
|
|
285
|
+
});
|
|
286
|
+
</script>
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### With entity picker enabled
|
|
290
|
+
|
|
291
|
+
```html
|
|
292
|
+
<ha-better-history show-entity-picker></ha-better-history>
|
|
293
|
+
<script>
|
|
294
|
+
chart.config = {
|
|
295
|
+
defaultEntities: ["climate.living", "sensor.outdoor_temp"],
|
|
296
|
+
series: [
|
|
297
|
+
{ entity: "climate.living", attribute: "current_temperature", label: "Indoor" }
|
|
298
|
+
]
|
|
299
|
+
};
|
|
300
|
+
</script>
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
The entity picker lets users browse entity attributes and add/remove series at runtime. The attribute browser includes a local search field that finds top-level attributes, nested dotted paths, and primitive values inside attribute dictionaries. Non-default series are removable via chip buttons. Selected source chips can be dragged to reorder user-added graphs without refetching history; the chip order previews while dragging and is restored if the drag is cancelled.
|
|
304
|
+
|
|
305
|
+
### Viewer tools
|
|
306
|
+
|
|
307
|
+
The viewer toolbar appears above the graph when `tools-open` is `true`. It includes:
|
|
308
|
+
|
|
309
|
+
- a time range selector that zooms inside the already loaded history range without refetching data;
|
|
310
|
+
- a display mode switch for stair, line, and column rendering;
|
|
311
|
+
- a JSON export button;
|
|
312
|
+
- an optional JSON import button when `show-import-button` or `config.showImportButton` is `true`.
|
|
313
|
+
|
|
314
|
+
Drag the highlighted range selection to pan the zoomed graph through the loaded period while keeping the same visible duration. A minimum drag target stays available even when the visible range is tiny, and the minimum zoom span adapts to the loaded range.
|
|
315
|
+
|
|
316
|
+
The panel has no built-in toggle button — visibility is fully controlled by the parent via the `tools-open` attribute (or `.toolsOpen` property). A typical integration adds a `mdi:tools` icon button in its own header and binds its state:
|
|
317
|
+
|
|
318
|
+
```html
|
|
319
|
+
<button @click=${() => this._toolsOpen = !this._toolsOpen}>tools</button>
|
|
320
|
+
<ha-better-history .toolsOpen=${this._toolsOpen}></ha-better-history>
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Exports use the compact `ha-better-history-series-v1` format:
|
|
324
|
+
|
|
325
|
+
```json
|
|
326
|
+
{
|
|
327
|
+
"format": "ha-better-history-series-v1",
|
|
328
|
+
"exportedAt": "2026-05-07T13:24:00.000Z",
|
|
329
|
+
"loadedRange": { "start": "2026-05-07T00:00:00.000Z", "end": "2026-05-07T12:00:00.000Z" },
|
|
330
|
+
"viewRange": { "start": "2026-05-07T06:00:00.000Z", "end": "2026-05-07T09:00:00.000Z" },
|
|
331
|
+
"series": [
|
|
332
|
+
{
|
|
333
|
+
"id": "attr:climate.living:current_temperature",
|
|
334
|
+
"entityId": "climate.living",
|
|
335
|
+
"attribute": "current_temperature",
|
|
336
|
+
"label": "current_temperature",
|
|
337
|
+
"unit": "°C",
|
|
338
|
+
"valueType": "number",
|
|
339
|
+
"lineMode": "stair",
|
|
340
|
+
"color": "#42a5f5",
|
|
341
|
+
"points": [{ "timestamp": "2026-05-07T06:00:00.000Z", "value": 19.5 }]
|
|
342
|
+
}
|
|
343
|
+
]
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
The optional import button accepts the same `ha-better-history-series-v1` JSON. Imported files replace the current displayed series, apply the exported loaded range and view range, and render locally without querying Home Assistant history.
|
|
348
|
+
|
|
349
|
+
## CSS custom properties
|
|
350
|
+
|
|
351
|
+
Override these on the host element to customize appearance.
|
|
352
|
+
|
|
353
|
+
| Property | Fallback |
|
|
354
|
+
| ------------------------------- | ------------------------- |
|
|
355
|
+
| `--better-history-bg` | `--card-background-color` |
|
|
356
|
+
| `--better-history-text-color` | `--primary-text-color` |
|
|
357
|
+
| `--better-history-muted-color` | `--secondary-text-color` |
|
|
358
|
+
| `--better-history-border-color` | `--divider-color` |
|
|
359
|
+
| `--better-history-accent-color` | `--accent-color` |
|
|
360
|
+
| `--better-history-radius` | `8px` |
|
|
361
|
+
| `--better-history-font-family` | `inherit` |
|
|
362
|
+
| `--better-history-title-color` | `--primary-text-color` |
|
|
363
|
+
| `--better-history-title-font-family` | `inherit` |
|
|
364
|
+
| `--better-history-title-font-size` | `--ha-font-size-xl, 20px` |
|
|
365
|
+
|
|
366
|
+
## Loading / setup
|
|
367
|
+
|
|
368
|
+
**Bundled** (recommended for production):
|
|
369
|
+
|
|
370
|
+
```js
|
|
371
|
+
// Auto-registers <ha-better-history>
|
|
372
|
+
import "ha-better-history/define";
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**Manual register** (no side-effect import):
|
|
376
|
+
|
|
377
|
+
```js
|
|
378
|
+
import { HaBetterHistory } from "ha-better-history";
|
|
379
|
+
customElements.define("ha-better-history", HaBetterHistory);
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**Date picker / entity picker** load their required HA components lazily via `@kipk/load-ha-components`. If `show-date-picker` or `show-entity-picker` is set, the component calls `ensureDateRangePicker()` / `ensureHaComponents()` on `connectedCallback`. These must run inside a Home Assistant frontend context (where `partial-panel-resolver` is available). In a standalone dev page, loading will fail gracefully after a 10-second timeout.
|
|
383
|
+
|
|
384
|
+
## Dev page
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
npm install
|
|
388
|
+
npm run dev
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Opens a local Vite dev server with synthetic data. No HA instance needed. See `dev/index.html`.
|
|
392
|
+
|
|
393
|
+
## Release
|
|
394
|
+
|
|
395
|
+
Releases are created by pushing a version tag that matches `package.json` exactly:
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
npm version 1.0.0 --no-git-tag-version
|
|
399
|
+
git tag 1.0.0
|
|
400
|
+
git push origin main --tags
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
Accepted tag formats are `1.0.0`, `1.0.0-rc1`, and `1.0.0-beta1`. Stable tags publish to the npm `latest` dist-tag; release candidates publish to `rc`; beta releases publish to `beta`.
|
|
404
|
+
|
|
405
|
+
The GitHub Action builds the package, uploads the npm tarball and browser `dist` archive to the GitHub Release, and publishes to npm through Trusted Publishing. Configure the package on npm with GitHub Actions as trusted publisher for `KipK/ha-better-history` and workflow filename `release.yml`; no `NPM_TOKEN` secret is required.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ReactiveController, ReactiveControllerHost } from "lit";
|
|
2
|
+
import { type HistorySeries, type HistorySource } from "../data/history.js";
|
|
3
|
+
import type { HomeAssistant } from "../types/ha.js";
|
|
4
|
+
export declare class DataController implements ReactiveController {
|
|
5
|
+
readonly host: ReactiveControllerHost;
|
|
6
|
+
series: HistorySeries[];
|
|
7
|
+
loading: boolean;
|
|
8
|
+
error: string;
|
|
9
|
+
debugPerformance: boolean;
|
|
10
|
+
private _prevKey;
|
|
11
|
+
private _nextSessionId;
|
|
12
|
+
private _session?;
|
|
13
|
+
private _progressUpdateScheduled;
|
|
14
|
+
private _lastProgressUpdateMs;
|
|
15
|
+
constructor(host: ReactiveControllerHost);
|
|
16
|
+
hostConnected(): void;
|
|
17
|
+
hostDisconnected(): void;
|
|
18
|
+
private _createSession;
|
|
19
|
+
private _cancelSession;
|
|
20
|
+
private _activeSession;
|
|
21
|
+
private _isCurrentSession;
|
|
22
|
+
private _addSessionSources;
|
|
23
|
+
private _hasActiveEntityLoad;
|
|
24
|
+
private _beginLoad;
|
|
25
|
+
private _completeLoad;
|
|
26
|
+
private _sessionSources;
|
|
27
|
+
private _hasAccumulatorSeries;
|
|
28
|
+
private _availableSessionSeries;
|
|
29
|
+
private _requestProgressUpdate;
|
|
30
|
+
fetch(hass: HomeAssistant | undefined, sources: HistorySource[], start: Date, end: Date): void;
|
|
31
|
+
setImportedSeries(series: HistorySeries[], start: Date, end: Date): void;
|
|
32
|
+
setError(error: string): void;
|
|
33
|
+
addSources(hass: HomeAssistant | undefined, newSources: HistorySource[], start: Date, end: Date): void;
|
|
34
|
+
updateLivePoints(hass: HomeAssistant | undefined, sources: HistorySource[], start: Date, end: Date): void;
|
|
35
|
+
private _mergeSeries;
|
|
36
|
+
private _mergePartial;
|
|
37
|
+
removeSources(sourceIds: string[]): void;
|
|
38
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { nothing, type TemplateResult } from "lit";
|
|
2
|
+
import type { ReactiveController, ReactiveControllerHost } from "lit";
|
|
3
|
+
import type { HistorySeries } from "../data/history.js";
|
|
4
|
+
export interface SyncedSeries {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
color: string;
|
|
8
|
+
}
|
|
9
|
+
export interface TooltipValue {
|
|
10
|
+
label: string;
|
|
11
|
+
color: string;
|
|
12
|
+
value: string;
|
|
13
|
+
}
|
|
14
|
+
export interface TooltipState {
|
|
15
|
+
x: number;
|
|
16
|
+
tooltipX: number;
|
|
17
|
+
y: number;
|
|
18
|
+
placement: "above" | "below";
|
|
19
|
+
activeTop: number;
|
|
20
|
+
activeHeight: number;
|
|
21
|
+
activeKey: string;
|
|
22
|
+
time: number;
|
|
23
|
+
values: TooltipValue[];
|
|
24
|
+
}
|
|
25
|
+
export declare class TooltipController implements ReactiveController {
|
|
26
|
+
private readonly _host;
|
|
27
|
+
tooltip: TooltipState | undefined;
|
|
28
|
+
private _series;
|
|
29
|
+
private _fetchedRef?;
|
|
30
|
+
private _cacheKey;
|
|
31
|
+
private _frame?;
|
|
32
|
+
private _pendingPoint?;
|
|
33
|
+
private _timeBounds;
|
|
34
|
+
constructor(host: ReactiveControllerHost & EventTarget);
|
|
35
|
+
hostConnected(): void;
|
|
36
|
+
hostDisconnected(): void;
|
|
37
|
+
/** Call each render cycle to keep chart dimensions + series data up to date. */
|
|
38
|
+
sync(series: SyncedSeries[], fetched: HistorySeries[], hiddenIds: string[], _chartHeight: number, timeBounds: {
|
|
39
|
+
start: number;
|
|
40
|
+
end: number;
|
|
41
|
+
}): void;
|
|
42
|
+
private _rebuildCache;
|
|
43
|
+
handlePointerMove(event: PointerEvent): void;
|
|
44
|
+
handlePointerLeave(): void;
|
|
45
|
+
private _clear;
|
|
46
|
+
private _apply;
|
|
47
|
+
private _placement;
|
|
48
|
+
private _tooltipY;
|
|
49
|
+
private _emit;
|
|
50
|
+
private _nearest;
|
|
51
|
+
private _nearestPoint;
|
|
52
|
+
private _pointAtOrBefore;
|
|
53
|
+
private _timeAt;
|
|
54
|
+
private _svgPoint;
|
|
55
|
+
renderTooltip(): TemplateResult | typeof nothing;
|
|
56
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AttributeUnitMap } from "../types/config.js";
|
|
2
|
+
export declare const ATTRIBUTE_UNIT_TEMPERATURE = "temperature";
|
|
3
|
+
export declare function attributePathKey(path: string[]): string;
|
|
4
|
+
export declare function isAttributeTemperatureUnit(unit: string | undefined): boolean;
|
|
5
|
+
export declare function unitForAttributePath(path: string[] | undefined, units?: AttributeUnitMap): string | undefined;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function isUnavailableState(value: unknown): boolean;
|
|
2
|
+
export declare function asString(value: unknown): string | undefined;
|
|
3
|
+
export declare function asNumber(value: unknown): number | undefined;
|
|
4
|
+
export declare function asStringArray(value: unknown): string[];
|
|
5
|
+
export declare function firstDefined<T>(...values: Array<T | undefined>): T | undefined;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface HistoryQueueTask<T> {
|
|
2
|
+
id: string;
|
|
3
|
+
run(): Promise<T>;
|
|
4
|
+
}
|
|
5
|
+
export interface HistoryQueueEvent {
|
|
6
|
+
event: "queue.start" | "queue.task_start" | "queue.task_complete" | "queue.cancelled";
|
|
7
|
+
queuedCount: number;
|
|
8
|
+
activeCount: number;
|
|
9
|
+
completedCount: number;
|
|
10
|
+
taskId?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface HistoryQueueOptions {
|
|
13
|
+
concurrency?: number;
|
|
14
|
+
isCancelled?: () => boolean;
|
|
15
|
+
onEvent?: (event: HistoryQueueEvent) => void;
|
|
16
|
+
}
|
|
17
|
+
export interface HistoryQueueResult<T, TTask extends HistoryQueueTask<T> = HistoryQueueTask<T>> {
|
|
18
|
+
task: TTask;
|
|
19
|
+
value: T;
|
|
20
|
+
durationMs: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function runHistoryQueue<T, TTask extends HistoryQueueTask<T> = HistoryQueueTask<T>>(tasks: TTask[], options?: HistoryQueueOptions & {
|
|
23
|
+
onResult?: (result: HistoryQueueResult<T, TTask>) => void | Promise<void>;
|
|
24
|
+
}): Promise<HistoryQueueResult<T, TTask>[]>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { HassEntity, HomeAssistant } from "../types/ha.js";
|
|
2
|
+
import type { HistorySourceKind, HistoryValueType } from "./value-type.js";
|
|
3
|
+
import { type PerformanceDetails } from "../utils/performance.js";
|
|
4
|
+
export type { HistoryValueType, HistorySourceKind };
|
|
5
|
+
export interface HistorySource {
|
|
6
|
+
id: string;
|
|
7
|
+
kind: HistorySourceKind;
|
|
8
|
+
entityId: string;
|
|
9
|
+
label: string;
|
|
10
|
+
path?: string[];
|
|
11
|
+
valueType: HistoryValueType;
|
|
12
|
+
unit?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface HistoryPoint {
|
|
15
|
+
time: number;
|
|
16
|
+
value: number | string | boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface HistorySeries {
|
|
19
|
+
source: HistorySource;
|
|
20
|
+
points: HistoryPoint[];
|
|
21
|
+
}
|
|
22
|
+
export interface HistoryPerformanceEvent {
|
|
23
|
+
event: string;
|
|
24
|
+
details: PerformanceDetails;
|
|
25
|
+
}
|
|
26
|
+
export type HistoryPerformanceCallback = (event: HistoryPerformanceEvent) => void;
|
|
27
|
+
export interface HistoryFetchOptions {
|
|
28
|
+
concurrency?: number;
|
|
29
|
+
isCancelled?: () => boolean;
|
|
30
|
+
chunkTimeoutMs?: number;
|
|
31
|
+
maxChunkAttempts?: number;
|
|
32
|
+
chunkRetryBaseDelayMs?: number;
|
|
33
|
+
accumulator?: HistoryDataAccumulator;
|
|
34
|
+
}
|
|
35
|
+
export interface HistoryState {
|
|
36
|
+
entity_id?: string;
|
|
37
|
+
state?: string;
|
|
38
|
+
s?: string;
|
|
39
|
+
last_changed?: string;
|
|
40
|
+
last_updated?: string;
|
|
41
|
+
lu?: number;
|
|
42
|
+
attributes?: Record<string, unknown>;
|
|
43
|
+
a?: Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
export declare function valueType(value: unknown): HistoryValueType | undefined;
|
|
46
|
+
export declare function entityStateSource(entity: HassEntity): HistorySource | undefined;
|
|
47
|
+
export declare function attributeSource(entity: HassEntity, path: string[], label?: string): HistorySource | undefined;
|
|
48
|
+
export declare function currentSourcePoint(hass: HomeAssistant, source: HistorySource, fallbackTime?: number): HistoryPoint | undefined;
|
|
49
|
+
type HistoryCoverageKind = "state" | "full";
|
|
50
|
+
export declare class HistoryDataAccumulator {
|
|
51
|
+
private readonly _entities;
|
|
52
|
+
hasStates(entityId: string): boolean;
|
|
53
|
+
hasFullStates(entityId: string): boolean;
|
|
54
|
+
hasCoverage(entityId: string, start: Date, end: Date, kind: HistoryCoverageKind): boolean;
|
|
55
|
+
missingIntervals(entityId: string, start: Date, end: Date, kind: HistoryCoverageKind): Array<{
|
|
56
|
+
start: Date;
|
|
57
|
+
end: Date;
|
|
58
|
+
}>;
|
|
59
|
+
integrate(entityId: string, states: HistoryState[], start: Date, end: Date, kind: HistoryCoverageKind): void;
|
|
60
|
+
buildSeries(source: HistorySource, hass: HomeAssistant, start: Date, end: Date): HistorySeries;
|
|
61
|
+
private coverageEnd;
|
|
62
|
+
}
|
|
63
|
+
export declare function fetchHistory(hass: HomeAssistant, sources: HistorySource[], start: Date, end: Date, onProgress?: (series: HistorySeries[]) => void, onPerformance?: HistoryPerformanceCallback, options?: HistoryFetchOptions): Promise<HistorySeries[]>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type HistorySource } from "./history.js";
|
|
2
|
+
import type { AttributeUnitMap, BetterHistoryConfig, BetterHistoryLineMode, ResolvedConfig, ResolvedSeries } from "../types/config.js";
|
|
3
|
+
import type { HomeAssistant } from "../types/ha.js";
|
|
4
|
+
export declare function resolvedSeriesToSource(s: ResolvedSeries): HistorySource;
|
|
5
|
+
export interface ResolveConfigOpts {
|
|
6
|
+
config?: BetterHistoryConfig;
|
|
7
|
+
entities?: string[];
|
|
8
|
+
hours?: number;
|
|
9
|
+
startDate?: Date;
|
|
10
|
+
endDate?: Date;
|
|
11
|
+
showDatePicker?: boolean;
|
|
12
|
+
showEntityPicker?: boolean;
|
|
13
|
+
showLegend?: boolean;
|
|
14
|
+
showTooltip?: boolean;
|
|
15
|
+
width?: string;
|
|
16
|
+
height?: string;
|
|
17
|
+
lineMode?: BetterHistoryLineMode;
|
|
18
|
+
lineWidth?: number | string;
|
|
19
|
+
backgroundColor?: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
titleFontFamily?: string;
|
|
22
|
+
titleFontSize?: string;
|
|
23
|
+
titleColor?: string;
|
|
24
|
+
language?: string;
|
|
25
|
+
hass?: HomeAssistant;
|
|
26
|
+
attributeUnits?: AttributeUnitMap;
|
|
27
|
+
}
|
|
28
|
+
export declare function resolveConfig(opts: ResolveConfigOpts): ResolvedConfig;
|
package/dist/define.d.ts
ADDED
package/dist/define.js
ADDED