Skip to content

Commit c5bde8f

Browse files
authored
Merge branch 'dev' into add-solar-power-badge
2 parents fd0f889 + 3bbce56 commit c5bde8f

231 files changed

Lines changed: 9803 additions & 5336 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/copilot-instructions.md

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
You are an assistant helping with development of the Home Assistant frontend. The frontend is built using Lit-based Web Components and TypeScript, providing a responsive and performant interface for home automation control.
44

5-
**Note**: This file contains high-level guidelines and references to implementation patterns. For detailed component documentation, API references, and usage examples, refer to the `gallery/` directory.
5+
**Note**: This file contains high-level guidelines and references to implementation patterns. For gallery-specific documentation, demos, page structure, and usage examples, see [`gallery/AGENTS.md`](gallery/AGENTS.md).
66

77
## Table of Contents
88

99
- [Quick Reference](#quick-reference)
1010
- [Core Architecture](#core-architecture)
11+
- [State Access: Contexts Instead of `hass`](#state-access-contexts-instead-of-hass)
1112
- [Development Standards](#development-standards)
1213
- [Component Library](#component-library)
1314
- [Common Patterns](#common-patterns)
@@ -52,6 +53,57 @@ The Home Assistant frontend is a modern web application that:
5253
- Communicates with the backend via WebSocket API
5354
- Provides comprehensive theming and internationalization
5455

56+
## State Access: Contexts Instead of `hass`
57+
58+
Every component used to take the whole `hass: HomeAssistant` object — a god-object that re-renders on any unrelated `hass` change, forces tests to mock everything, and hides what a component actually reads. We're moving leaf components to **fine-grained [Lit context](https://lit.dev/docs/data/context/)**: consume only the slice you need and re-render only when it changes.
59+
60+
For new code, consume the matching context instead of adding a `hass` property. `hass` stays for container components that own it and feed the providers; the canonical migration is [`hui-button-card.ts`](src/panels/lovelace/cards/hui-button-card.ts). Infrastructure: contexts in [`src/data/context/index.ts`](src/data/context/index.ts), the `consume…` helpers in [`src/common/decorators/consume-context-entry.ts`](src/common/decorators/consume-context-entry.ts), and `@transform` in [`src/common/decorators/transform.ts`](src/common/decorators/transform.ts). Providers are wired automatically by `contextMixin` on `HassBaseEl` — you only consume.
61+
62+
### Contexts
63+
64+
Consume the narrowest context that covers your reads:
65+
66+
| Context | Replaces |
67+
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
68+
| `statesContext` | `hass.states` |
69+
| `entitiesContext` / `devicesContext` / `areasContext` / `floorsContext` | `hass.entities` / `.devices` / `.areas` / `.floors` (or `registriesContext` for all four) |
70+
| `servicesContext` | `hass.services` |
71+
| `internationalizationContext` | `hass.localize`, `hass.locale`, `hass.language` |
72+
| `formattersContext` | `hass.formatEntityName`, `hass.formatEntityState`, `hass.formatEntityAttributeName`, … |
73+
| `configContext` | `hass.config`, `hass.user`, `hass.auth`, `hass.userData` |
74+
| `connectionContext` | `hass.connection`, `hass.connected`, `hass.hassUrl` |
75+
| `apiContext` | `hass.callService`, `hass.callApi`, `hass.callWS`, `hass.sendWS`, `hass.fetchWithAuth` |
76+
| `uiContext` | `hass.themes`, `hass.selectedTheme`, `hass.panels`, `hass.dockedSidebar`, … |
77+
| `narrowViewportContext` | narrow-layout boolean |
78+
79+
Lazy contexts (subscribe on first consumer, tear down after the last): `labelsContext`, `fullEntitiesContext`, `configEntriesContext`, `manifestsContext`. The single-field contexts (`localizeContext`, `themesContext`, `userContext`, …) are **deprecated** — use the grouped ones above.
80+
81+
### Consuming
82+
83+
Use the `consume…` helpers for entity-scoped and `localize` reads. `entityIdPath` is resolved against `this`, so these watch `this._config.entity`:
84+
85+
```ts
86+
@state() @consumeEntityState({ entityIdPath: ["_config", "entity"] })
87+
private _stateObj?: HassEntity; // consumeEntityStates(...) for a record of several
88+
89+
@state() @consumeEntityRegistryEntry({ entityIdPath: ["_config", "entity"] })
90+
private _entity?: EntityRegistryDisplayEntry;
91+
92+
@state() @consumeLocalize()
93+
private _localize!: LocalizeFunc;
94+
```
95+
96+
For any other single field, pair `@consume` with `@transform`:
97+
98+
```ts
99+
@state()
100+
@consume({ context: uiContext, subscribe: true })
101+
@transform<HomeAssistantUI, Themes>({ transformer: ({ themes }) => themes })
102+
private _themes!: Themes;
103+
```
104+
105+
`@transform`'s `watch` option re-runs the transformer when a host prop changes — needed when an entity id is computed, since `consumeEntityState` only watches the first path segment. To consume a whole group untransformed, drop `@transform` and type it `ContextType<typeof statesContext>`.
106+
55107
## Development Standards
56108

57109
### Code Quality Requirements
@@ -136,6 +188,7 @@ export class HaMyComponent extends LitElement {
136188
### Data Management
137189

138190
- **Use WebSocket API**: All backend communication via home-assistant-js-websocket
191+
- **Prefer contexts over `hass`**: For state reads, consume the relevant Lit context instead of taking the whole `hass` object — see [State Access: Contexts Instead of `hass`](#state-access-contexts-instead-of-hass)
139192
- **Cache appropriately**: Use collections and caching for frequently accessed data
140193
- **Handle errors gracefully**: All API calls should have error handling
141194
- **Update real-time**: Subscribe to state changes for live updates
@@ -285,11 +338,6 @@ Common patterns:
285338
- **Destructive actions**: `variant="danger"` for delete/remove operations (the generic confirmation dialog uses `variant="danger"` for its confirm button — see `src/dialogs/generic/dialog-box.ts`)
286339
- Always place primary action in `slot="primaryAction"` and secondary in `slot="secondaryAction"` within `ha-dialog-footer`
287340

288-
**Gallery Documentation:**
289-
290-
- `gallery/src/pages/components/ha-dialog.markdown`
291-
- `gallery/src/pages/components/ha-dialogs.markdown`
292-
293341
### Form Component (ha-form)
294342

295343
- Schema-driven using `HaFormSchema[]`
@@ -308,10 +356,6 @@ Common patterns:
308356
></ha-form>
309357
```
310358

311-
**Gallery Documentation:**
312-
313-
- `gallery/src/pages/components/ha-form.markdown`
314-
315359
### Alert Component (ha-alert)
316360

317361
- Types: `error`, `warning`, `info`, `success`
@@ -325,10 +369,6 @@ Common patterns:
325369
<ha-alert alert-type="success" dismissable>Success message</ha-alert>
326370
```
327371

328-
**Gallery Documentation:**
329-
330-
- `gallery/src/pages/components/ha-alert.markdown`
331-
332372
### Keyboard Shortcuts (ShortcutManager)
333373

334374
The `ShortcutManager` class provides a unified way to register keyboard shortcuts with automatic input field protection.
@@ -352,7 +392,6 @@ The `ha-tooltip` component wraps Web Awesome tooltip with Home Assistant theming
352392

353393
- **Component definition**: `src/components/ha-tooltip.ts`
354394
- **Usage example**: `src/components/ha-label.ts`
355-
- **Gallery documentation**: `gallery/src/pages/components/ha-tooltip.markdown`
356395

357396
## Common Patterns
358397

@@ -382,7 +421,7 @@ export class HaPanelMyFeature extends SubscribeMixin(LitElement) {
382421

383422
#### Creating a Lovelace Card
384423

385-
**Purpose**: Cards allow users to tell different stories about their house (based on gallery)
424+
**Purpose**: Cards allow users to tell different stories about their house.
386425

387426
```typescript
388427
@customElement("hui-my-card")
@@ -455,6 +494,10 @@ this.hass.localize("ui.panel.config.updates.update_available", {
455494
4. **Test**: `yarn test` - Add and run tests
456495
5. **Build**: `script/build_frontend` - Test production build
457496

497+
### Gallery
498+
499+
For Gallery-specific structure, page/demo naming, sidebar behavior, content standards, and commands, see [`gallery/AGENTS.md`](gallery/AGENTS.md).
500+
458501
### Common Pitfalls to Avoid
459502

460503
- Don't manually query the DOM with `querySelector` - use the `@query`/`@queryAll` decorators or component properties
@@ -485,7 +528,7 @@ When creating a pull request, you **must** use the PR template located at `.gith
485528

486529
#### Terminology Standards
487530

488-
**Delete vs Remove** (Based on gallery/src/pages/Text/remove-delete-add-create.markdown)
531+
**Delete vs Remove**
489532

490533
- **Use "Remove"** for actions that can be restored or reapplied:
491534
- Removing a user's permission
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Blocking labels
2+
3+
on:
4+
pull_request:
5+
types:
6+
- opened
7+
- synchronize
8+
- reopened
9+
- labeled
10+
- unlabeled
11+
branches:
12+
- dev
13+
- master
14+
15+
permissions:
16+
contents: read
17+
18+
jobs:
19+
check:
20+
name: Check for labels which block the Pull Request from being merged
21+
runs-on: ubuntu-latest
22+
steps:
23+
- name: Check for blocking labels
24+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
25+
with:
26+
script: |
27+
const blockingLabels = [
28+
"wait for backend",
29+
"Needs UX",
30+
"Do Not Review",
31+
"Blocked",
32+
"has-parent",
33+
];
34+
const prLabels = context.payload.pull_request.labels.map(
35+
(l) => l.name
36+
);
37+
const found = blockingLabels.filter((bl) => prLabels.includes(bl));
38+
if (found.length > 0) {
39+
const message = `This Pull Request is blocked by label${found.length > 1 ? "s" : ""}: ${found.join(", ")}`;
40+
await core.summary
41+
.addHeading(":no_entry_sign: Pull Request is blocked", 2)
42+
.addRaw(message)
43+
.write();
44+
core.setFailed(message);
45+
} else {
46+
await core.summary
47+
.addHeading(":white_check_mark: Pull Request is clear to merge after review", 2)
48+
.addRaw("This Pull Request is not blocked by any labels which prevent it from being merged.")
49+
.write();
50+
}

.github/workflows/release-drafter.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ jobs:
1818
pull-requests: read
1919
runs-on: ubuntu-latest
2020
steps:
21-
- uses: release-drafter/release-drafter@c2e2804cc59f45f57076a99af580d0fedb697927 # v7.3.0
21+
- uses: release-drafter/release-drafter@693d20e7c1ce1a81d3a41962f85914253b518449 # v7.3.1
2222
env:
2323
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
diff --git a/dist/tinykeys.cjs b/dist/tinykeys.cjs
2+
index 08c98b6eff3b8fb4b727fe8e6b096951d6ef6347..9c44f14862f582766ea1733b6dc0e97f962800d8 100644
3+
--- a/dist/tinykeys.cjs
4+
+++ b/dist/tinykeys.cjs
5+
@@ -61,6 +61,18 @@ function defaultKeybindingsHandlerIgnore(event) {
6+
function getModifierState(event, mod) {
7+
return typeof event.getModifierState === "function" ? event.getModifierState(mod) || ALT_GRAPH_ALIASES.includes(mod) && event.getModifierState("AltGraph") : false;
8+
}
9+
+function splitKeybindingPress(press) {
10+
+ let parts = [];
11+
+ let start = 0;
12+
+ for (let index = 0; index < press.length; index++) {
13+
+ if (press[index] === "+" && /[\w\]]/.test(press[index - 1] || "")) {
14+
+ parts.push(press.slice(start, index));
15+
+ start = index + 1;
16+
+ }
17+
+ }
18+
+ parts.push(press.slice(start));
19+
+ return parts;
20+
+}
21+
/**
22+
* Parses a keybinding string into its parts.
23+
*
24+
@@ -76,10 +88,10 @@ function getModifierState(event, mod) {
25+
*/
26+
function parseKeybinding(str) {
27+
return str.trim().split(" ").map((press) => {
28+
- let parts = press.split(/(?<=\w|\])\+/);
29+
+ let parts = splitKeybindingPress(press);
30+
let last = parts.pop();
31+
let regex = last.match(/^\((.+)\)$/);
32+
- let key = regex ? new RegExp(`^(?:${regex[1]})$`, "iv") : last;
33+
+ let key = regex ? new RegExp(`^(?:${regex[1]})$`, "i") : last;
34+
let requiredModifiers = [];
35+
let optionalModifiers = [];
36+
for (const part of parts) {
37+
@@ -201,5 +213,3 @@ exports.defaultKeybindingsHandlerIgnore = defaultKeybindingsHandlerIgnore;
38+
exports.matchKeybindingPress = matchKeybindingPress;
39+
exports.parseKeybinding = parseKeybinding;
40+
exports.tinykeys = tinykeys;
41+
-
42+
-//# sourceMappingURL=tinykeys.cjs.map
43+
\ No newline at end of file
44+
diff --git a/dist/tinykeys.mjs b/dist/tinykeys.mjs
45+
index c289972d2728e03d9b272268c38fd3392e8845bf..e22897b00aae6cdb0dbbb971445227c07be52918 100644
46+
--- a/dist/tinykeys.mjs
47+
+++ b/dist/tinykeys.mjs
48+
@@ -60,6 +60,18 @@ function defaultKeybindingsHandlerIgnore(event) {
49+
function getModifierState(event, mod) {
50+
return typeof event.getModifierState === "function" ? event.getModifierState(mod) || ALT_GRAPH_ALIASES.includes(mod) && event.getModifierState("AltGraph") : false;
51+
}
52+
+function splitKeybindingPress(press) {
53+
+ let parts = [];
54+
+ let start = 0;
55+
+ for (let index = 0; index < press.length; index++) {
56+
+ if (press[index] === "+" && /[\w\]]/.test(press[index - 1] || "")) {
57+
+ parts.push(press.slice(start, index));
58+
+ start = index + 1;
59+
+ }
60+
+ }
61+
+ parts.push(press.slice(start));
62+
+ return parts;
63+
+}
64+
/**
65+
* Parses a keybinding string into its parts.
66+
*
67+
@@ -75,10 +87,10 @@ function getModifierState(event, mod) {
68+
*/
69+
function parseKeybinding(str) {
70+
return str.trim().split(" ").map((press) => {
71+
- let parts = press.split(/(?<=\w|\])\+/);
72+
+ let parts = splitKeybindingPress(press);
73+
let last = parts.pop();
74+
let regex = last.match(/^\((.+)\)$/);
75+
- let key = regex ? new RegExp(`^(?:${regex[1]})$`, "iv") : last;
76+
+ let key = regex ? new RegExp(`^(?:${regex[1]})$`, "i") : last;
77+
let requiredModifiers = [];
78+
let optionalModifiers = [];
79+
for (const part of parts) {
80+
@@ -196,5 +208,3 @@ function tinykeys(target, keybindingMap, options = {}) {
81+
}
82+
//#endregion
83+
export { createKeybindingsHandler, defaultKeybindingsHandlerIgnore, matchKeybindingPress, parseKeybinding, tinykeys };
84+
-
85+
-//# sourceMappingURL=tinykeys.mjs.map
86+
\ No newline at end of file

0 commit comments

Comments
 (0)