Compare commits

..

1 Commits

Author SHA1 Message Date
Aidan Timson 23a82918f4 Reduce extra lines on agents file 2026-07-21 08:45:27 +01:00
241 changed files with 4108 additions and 8434 deletions
+9 -14
View File
@@ -23,10 +23,11 @@ Dialog implementation requirements:
- Use `ha-dialog`.
- Use `DialogMixin`, which implements `HassDialogNext<T>`, for new dialogs. See `src/dialogs/dialog-mixin.ts`.
- Read dialog parameters from the mixin's `params` property and render the dialog open. Return `nothing` while required parameters are absent.
- Call the mixin's `closeDialog()` to close a new dialog. The mixin handles the `closed` event, fires `dialog-closed`, and removes the host element.
- Existing dialogs may implement the legacy `HassDialog<T>` interface from `src/dialogs/make-dialog-manager.ts`.
- Preserve the existing `showDialog()`, open-state, and close-event lifecycle when maintaining a legacy dialog; do not copy that lifecycle into a `DialogMixin` dialog.
- Use `@state() private _open = false` to control visibility.
- Set `_open = true` in `showDialog()` and `_open = false` in `closeDialog()`.
- Return `nothing` while required params are absent.
- Fire `dialog-closed` in the close handler.
- Use `header-title` and `header-subtitle` for simple header text.
- Use slots when standard header attributes are not enough.
- Use `ha-dialog-footer` with `primaryAction` and `secondaryAction` slots.
@@ -63,7 +64,7 @@ Use `computeLabel`, `computeError`, and `computeHelper` for translated labels, v
.data=${this._data}
.schema=${this._schema}
.error=${this._errors}
.computeLabel=${(schema) => this._localize(`ui.panel.${schema.name}`)}
.computeLabel=${(schema) => this.hass.localize(`ui.panel.${schema.name}`)}
@value-changed=${this._valueChanged}
></ha-form>
```
@@ -77,16 +78,10 @@ Use `ha-alert` for user-visible status messaging.
- Slots: `icon` for custom leading icon, `action` for custom action content.
- Content is announced by screen readers when dynamically displayed.
```ts
html`
<ha-alert alert-type="error">${this._localize("ui.example.error")}</ha-alert>
<ha-alert alert-type="warning" .title=${this._localize("ui.example.warning")}>
${this._localize("ui.example.description")}
</ha-alert>
<ha-alert alert-type="success" dismissable>
${this._localize("ui.example.success")}
</ha-alert>
`;
```html
<ha-alert alert-type="error">Error message</ha-alert>
<ha-alert alert-type="warning" title="Warning">Description</ha-alert>
<ha-alert alert-type="success" dismissable>Success message</ha-alert>
```
## Shortcuts And Tooltips
+16 -23
View File
@@ -23,27 +23,27 @@ Container components may keep `hass` when they own it and feed providers. Leaf c
## Context Selection
| Context | Replaces |
| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `statesContext` | `hass.states` |
| `entitiesContext`, `devicesContext`, `areasContext`, `floorsContext` | `hass.entities`, `hass.devices`, `hass.areas`, `hass.floors` |
| `registriesContext` | all four registries together |
| `servicesContext` | `hass.services` |
| `internationalizationContext` | `hass.localize`, `hass.locale`, `hass.language` |
| `formattersContext` | entity and attribute formatters |
| `configContext` | `hass.config`, `hass.user`, `hass.auth`, `hass.userData` |
| `connectionContext` | `hass.connection`, `hass.connected`, `hass.debugConnection`, `hass.hassUrl` |
| `apiContext` | `hass.callService`, `hass.callApi`, `hass.callApiRaw`, `hass.callWS`, `hass.sendWS`, `hass.fetchWithAuth` |
| `uiContext` | themes, selected theme, panels, sidebar, and UI state |
| `narrowViewportContext` | narrow-layout boolean |
| Context | Replaces |
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `statesContext` | `hass.states` |
| `entitiesContext`, `devicesContext`, `areasContext`, `floorsContext` | `hass.entities`, `hass.devices`, `hass.areas`, `hass.floors` |
| `registriesContext` | all four registries together |
| `servicesContext` | `hass.services` |
| `internationalizationContext` | `hass.localize`, `hass.locale`, `hass.language` |
| `formattersContext` | entity and attribute formatters |
| `configContext` | `hass.config`, `hass.user`, `hass.auth`, `hass.userData` |
| `connectionContext` | `hass.connection`, `hass.connected`, `hass.hassUrl` |
| `apiContext` | `hass.callService`, `hass.callApi`, `hass.callWS`, `hass.sendWS`, `hass.fetchWithAuth` |
| `uiContext` | themes, selected theme, panels, sidebar, and UI state |
| `narrowViewportContext` | narrow-layout boolean |
Lazy contexts subscribe on first consumer and tear down after the last consumer: `labelsContext`, `fullEntitiesContext`, `configEntriesContext`, `manifestsContext`, `triggerDescriptionsContext`, and `conditionDescriptionsContext`.
Lazy contexts subscribe on first consumer and tear down after the last consumer: `labelsContext`, `fullEntitiesContext`, `configEntriesContext`, and `manifestsContext`.
The single-field contexts such as `localizeContext`, `themesContext`, and `userContext` are deprecated. Use grouped contexts instead.
## Consumption Patterns
Use entity-scoped helpers when the component watches an entity ID held on the host:
Use entity-scoped helpers when the component watches an entity id held on the host:
```ts
@state() @consumeEntityState({ entityIdPath: ["_config", "entity"] })
@@ -56,13 +56,6 @@ private _entity?: EntityRegistryDisplayEntry;
private _localize!: LocalizeFunc;
```
Use `consumeEntityStates` when the host property contains one or more entity IDs. It filters missing entities and preserves the previous record when none of the selected entities changed.
```ts
@state() @consumeEntityStates({ entityIdPath: ["_config", "entities"] })
private _stateObjs?: Record<string, HassEntity>;
```
For a single field from a grouped context, pair `@consume` with `@transform`:
```ts
@@ -72,7 +65,7 @@ For a single field from a grouped context, pair `@consume` with `@transform`:
private _themes!: Themes;
```
Use `@transform` with `watch` when the transformer depends on a host property, such as a computed entity ID. `consumeEntityState` and `consumeEntityStates` only watch the first path segment.
Use `@transform` with `watch` when the transformer depends on a host property, such as a computed entity id. `consumeEntityState` only watches the first path segment.
To consume a whole group untransformed, omit `@transform` and type the field as `ContextType<typeof statesContext>` or the matching context type.
+10 -18
View File
@@ -1,18 +1,12 @@
---
name: ha-frontend-testing
description: Home Assistant frontend testing and validation workflow. Use when adding or updating tests, running lint, TypeScript checks, Vitest, Playwright e2e suites, dev servers, or chart-data benchmarks.
description: Home Assistant frontend validation workflow. Use when running lint, TypeScript checks, Vitest, Playwright e2e suites, dev servers, or chart-data benchmarks.
---
# HA Frontend Testing
Use this skill when choosing or running validation for frontend changes.
## Test Helpers
- Before adding or changing tests, inspect the relevant suite's existing helpers and fixtures. Reuse them instead of duplicating setup, test data, navigation, interactions, waits, or assertions.
- When the same test flow appears more than once, move it into the closest suite-local helper with a focused interface.
- Keep one-off test behaviour in the test unless a helper makes the intent materially clearer. Do not hide the behaviour under test behind broad, configurable abstractions.
## Core Commands
```bash
@@ -41,7 +35,7 @@ For focused type feedback on one file, use editor diagnostics instead of a file-
`yarn dev:serve` also serves locally and supports `-c` for the core URL and `-p` for the port. The default is 8124, or 8123 in a devcontainer.
Dev server commands support `--background`, `--status`, `--stop`, and `--logs [--follow]`. Prefer managed background mode while iterating so the watcher stays available across test runs without occupying the terminal.
Dev server commands support `--background`, `--status`, `--stop`, and `--logs [--follow]`.
## Playwright E2E
@@ -49,25 +43,23 @@ Each suite has its own dev server port. Playwright reuses an existing server loc
Start the relevant suite server, then run that suite:
| Suite | Background server | Test command |
| ------- | -------------------------------------------- | ----------------------- |
| App | `yarn test:e2e:app:dev --background` on 8095 | `yarn test:e2e:app` |
| Demo | `yarn dev:demo --background` on 8090 | `yarn test:e2e:demo` |
| Gallery | `yarn dev:gallery --background` on 8100 | `yarn test:e2e:gallery` |
| Suite | Server | Test command |
| ------- | ------------------------------- | ----------------------- |
| App | `yarn test:e2e:app:dev` on 8095 | `yarn test:e2e:app` |
| Demo | `yarn dev:demo` on 8090 | `yarn test:e2e:demo` |
| Gallery | `yarn dev:gallery` on 8100 | `yarn test:e2e:gallery` |
The custom development wrappers use `/__ha_dev_status` to identify and manage their own suites. Playwright server reuse checks the configured URL instead. Wrapper start and stop operations are idempotent for a matching suite and reject an unrelated process occupying the port.
Local runs against a watched development server do not always match CI's clean build artifacts, environment, sharding, or worker configuration. Use background servers for the fast iteration loop, but confirm the relevant CI jobs complete successfully before considering E2E changes verified.
Use `-g "<title>" --project=chromium` to narrow a run. `yarn test:e2e` runs all three suites in parallel when every managed server is available, otherwise it runs them sequentially to prevent cold builds racing over shared generated assets. Run suites directly; piping through output truncation hides progress and failures.
Use `-g "<title>" --project=chromium` to narrow a run. `yarn test:e2e` runs all three suites. Run suites directly; piping through output truncation hides progress and failures.
The app suite uses a stripped-down harness for e2e. Demo and gallery use their normal dev servers.
## Benchmarks
For chart data transforms such as history, statistics, energy, and downsampling, read and follow the complete workflow in `test/benchmarks/README.md` before making benchmark or optimization changes.
For chart data transforms such as history, statistics, energy, and downsampling, follow `test/benchmarks/README.md`.
That workflow owns the baseline, noise analysis, guardrails, acceptance thresholds, and reporting requirements. In particular, optimizations must keep output bit-identical; never update snapshots or modify fixtures to make an optimization pass.
Use seeded fixtures, characterization snapshot tests, and `yarn test:bench` before and after optimization. Optimizations must keep output bit-identical.
## Verification Selection
@@ -16,7 +16,7 @@ Use this skill for all user-facing text, translations, labels, buttons, dialog c
- Give translators enough context through key naming and placeholders.
```ts
this._localize("ui.panel.config.updates.updates_refreshed", {
this.hass.localize("ui.panel.config.updates.updates_refreshed", {
count: 5,
});
```
+18 -8
View File
@@ -1,26 +1,36 @@
[modern]
# Modern builds target recent browsers supporting the latest features to minimize transpilation, polyfills, etc.
# It is served to browsers meeting the following requirements:
# - released in the last 3 years + current alpha/beta versions
# - released in the last year + current alpha/beta versions
# - Firefox extended support release (ESR)
# - with global utilization at or above 0.5%
# - exclude dead browsers (no security maintenance for 2+ years)
# - exclude KaiOS, QQ, and UC browsers due to lack of sufficient feature support data
unreleased versions
last 2 years
last 1 year
Firefox ESR
>= 0.5%
not dead
not KaiOS > 0
not QQAndroid > 0
not UCAndroid > 0
[legacy]
# Legacy builds are served when modern requirements are not met and support browsers:
# - released in the last 7 years with global utilization at or above 0.001%
# - released in the last 7 years + current alpha/beta versionss
# - with global utilization at or above 0.05%
# - exclude dead browsers (no security maintenance for 2+ years)
# - exclude Opera Mini which does not support web sockets
last 7 years and >= 0.001%
unreleased versions
last 7 years
>= 0.05%
not dead
not op_mini all
[legacy-sw]
# Same as legacy, restricted to browsers that support service workers
# (currently resolves to the same set; guards the service worker build if the legacy floor ever drops below them)
last 7 years and >= 0.001% and supports serviceworkers
not dead
# Same as legacy plus supports service workers
unreleased versions
last 7 years
>= 0.05% and supports serviceworkers
not dead
not op_mini all
@@ -1,41 +0,0 @@
name: Prepare dependencies
description: Install and cache the complete dependency tree
inputs:
node-modules-cache-key:
description: Prefix for the shared node_modules cache key
default: node-modules-v1
runs:
using: composite
steps:
- name: Check for complete dependency tree
id: dependencies
continue-on-error: true
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
node_modules
.yarn/install-state.gz
key: >-
${{ inputs.node-modules-cache-key }}-${{ runner.os }}-${{ runner.arch }}-${{
hashFiles('.nvmrc', 'package.json', 'yarn.lock', '.yarnrc.yml', '.yarn/releases/**', '.yarn/patches/**') }}
lookup-only: true
- name: Setup Node and install
if: steps.dependencies.outputs.cache-hit != 'true'
uses: ./.github/actions/setup
with:
cache: false
- name: Save complete dependency tree
if: steps.dependencies.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
node_modules
.yarn/install-state.gz
key: >-
${{ inputs.node-modules-cache-key }}-${{ runner.os }}-${{ runner.arch }}-${{
hashFiles('.nvmrc', 'package.json', 'yarn.lock', '.yarnrc.yml', '.yarn/releases/**', '.yarn/patches/**') }}
+1 -25
View File
@@ -8,40 +8,16 @@ inputs:
cache:
description: Enable the yarn cache in setup-node
default: "true"
node-modules-cache:
description: Restore the exact shared node_modules cache before installing
default: "false"
node-modules-cache-key:
description: Prefix for the shared node_modules cache key
default: node-modules-v1
runs:
using: composite
steps:
- name: Restore complete dependency tree
id: dependency-cache
if: inputs.node-modules-cache == 'true'
continue-on-error: true
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
node_modules
.yarn/install-state.gz
key: >-
${{ inputs.node-modules-cache-key }}-${{ runner.os }}-${{ runner.arch }}-${{
hashFiles('.nvmrc', 'package.json', 'yarn.lock', '.yarnrc.yml', '.yarn/releases/**', '.yarn/patches/**') }}
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: ".nvmrc"
cache: ${{ inputs.cache == 'true' && (inputs.node-modules-cache != 'true' || steps.dependency-cache.outputs.cache-hit != 'true') && 'yarn' || '' }}
- name: Enable Corepack
shell: bash
run: corepack enable
cache: ${{ inputs.cache == 'true' && 'yarn' || '' }}
- name: Install dependencies
if: inputs.node-modules-cache != 'true' || steps.dependency-cache.outputs.cache-hit != 'true'
shell: bash
run: yarn install ${{ inputs.immutable == 'true' && '--immutable' || '' }}
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
ACTIONLINT_VERSION: 1.7.12
steps:
- name: Check out files from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Run actionlint
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out workflow scripts
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
sparse-checkout: .github/scripts
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
url: ${{ steps.deploy.outputs.netlify_url }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: dev
persist-credentials: false
@@ -56,7 +56,7 @@ jobs:
url: ${{ steps.deploy.outputs.netlify_url }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: master
persist-credentials: false
+6 -27
View File
@@ -1,7 +1,6 @@
name: CI
on:
workflow_dispatch:
push:
branches:
- dev
@@ -22,30 +21,16 @@ permissions:
contents: read
jobs:
prepare-dependencies:
name: Prepare dependencies
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Prepare dependencies
uses: ./.github/actions/prepare-dependencies
lint:
name: Lint and check format
needs: prepare-dependencies
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Node with shared dependencies
- name: Setup Node and install
uses: ./.github/actions/setup
with:
node-modules-cache: true
- name: Check for duplicate dependencies
run: yarn dedupe --check
- name: Build resources
@@ -78,17 +63,14 @@ jobs:
run: yarn run lint:licenses
test:
name: Run tests
needs: prepare-dependencies
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Node with shared dependencies
- name: Setup Node and install
uses: ./.github/actions/setup
with:
node-modules-cache: true
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data
env:
@@ -98,19 +80,16 @@ jobs:
build:
name: Build frontend
needs:
- prepare-dependencies
- lint
- test
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Node with shared dependencies
- name: Setup Node and install
uses: ./.github/actions/setup
with:
node-modules-cache: true
- name: Build Application
uses: ./.github/actions/build
with:
+3 -3
View File
@@ -27,17 +27,17 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
with:
languages: javascript-typescript
build-mode: none
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
with:
category: "/language:javascript-typescript"
-34
View File
@@ -1,34 +0,0 @@
name: Copilot Setup Steps
on:
workflow_dispatch:
push:
paths:
- .github/workflows/copilot-setup-steps.yml
pull_request:
paths:
- .github/workflows/copilot-setup-steps.yml
env:
NODE_OPTIONS: --max_old_space_size=6144
jobs:
copilot-setup-steps:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Check out files from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Node and install
uses: ./.github/actions/setup
with:
node-modules-cache: true
- name: Skip fetching nightly translations
run: echo "SKIP_FETCH_NIGHTLY_TRANSLATIONS=1" >> "$GITHUB_ENV"
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
+2 -2
View File
@@ -25,7 +25,7 @@ jobs:
url: ${{ steps.deploy.outputs.unique_deploy_url }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: dev
persist-credentials: false
@@ -56,7 +56,7 @@ jobs:
url: ${{ steps.deploy.outputs.netlify_url }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: master
persist-credentials: false
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
url: ${{ steps.deploy.outputs.netlify_url }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps:
- name: Check out files from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
+49 -225
View File
@@ -22,60 +22,24 @@ permissions:
contents: read
jobs:
prepare-dependencies:
name: Prepare dependencies
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Prepare dependencies
uses: ./.github/actions/prepare-dependencies
prepare-container-dependencies:
name: Prepare container dependencies
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.61.1-noble
options: --user 1001
defaults:
run:
shell: bash
steps:
- name: Check out files from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Prepare dependencies
uses: ./.github/actions/prepare-dependencies
with:
node-modules-cache-key: node-modules-container-v1
# ── Build the demo once and share it across test jobs via artifact ──────────
build-demo:
name: Build demo
needs: prepare-dependencies
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Node with shared dependencies
- name: Setup Node and install
uses: ./.github/actions/setup
with:
node-modules-cache: true
- name: Build demo
uses: ./.github/actions/build
with:
target: build-demo-e2e
target: build-demo
github-token: ${{ secrets.GITHUB_TOKEN }}
is-test: true
- name: Upload demo build
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
@@ -88,25 +52,21 @@ jobs:
# ── Build the e2e test app and share it via artifact ────────────────────────
build-e2e-test-app:
name: Build e2e test app
needs: prepare-dependencies
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Node with shared dependencies
- name: Setup Node and install
uses: ./.github/actions/setup
with:
node-modules-cache: true
- name: Build e2e test app
uses: ./.github/actions/build
with:
target: build-e2e-test-app-e2e
target: build-e2e-test-app
github-token: ${{ secrets.GITHUB_TOKEN }}
is-test: true
- name: Upload e2e test app build
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
@@ -119,18 +79,15 @@ jobs:
# ── Build the gallery and share it via artifact ─────────────────────────────
build-gallery:
name: Build gallery
needs: prepare-dependencies
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Node with shared dependencies
- name: Setup Node and install
uses: ./.github/actions/setup
with:
node-modules-cache: true
- name: Build gallery
uses: ./.github/actions/build
@@ -146,39 +103,41 @@ jobs:
if-no-files-found: error
retention-days: 3
# ── Run Playwright tests against Chromium ──────────────────────────────────
e2e-demo:
name: E2E demo (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
needs:
- build-demo
- prepare-container-dependencies
# ── Run Playwright tests locally against Chromium ──────────────────────────
e2e-local:
name: E2E (local Chromium)
needs: [build-demo, build-e2e-test-app, build-gallery]
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.61.1-noble
options: --user 1001 --ipc=host
defaults:
run:
shell: bash
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
shardIndex:
- 1
- 2
shardTotal:
- 2
# Fail fast if anything hangs. The whole suite should take < 15 minutes on
# Chromium; anything longer is almost certainly an install or webServer
# hang.
timeout-minutes: 30
steps:
- name: Check out files from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Node with shared dependencies
- name: Setup Node and install
uses: ./.github/actions/setup
# Resolve the installed Playwright version so the browser cache tracks
# Playwright itself, not every unrelated dependency bump.
- name: Resolve Playwright version
id: playwright-version
run: echo "version=$(node -p 'require("@playwright/test/package.json").version')" >> "$GITHUB_OUTPUT"
# Cache the downloaded browser build keyed on the installed Playwright
# version, so re-runs skip the ~170 MB download unless Playwright changes.
- name: Cache Playwright browsers
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
node-modules-cache: true
node-modules-cache-key: node-modules-container-v1
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}
- name: Install Playwright browsers
run: yarn playwright install --with-deps chromium
timeout-minutes: 10
- name: Download demo build
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
@@ -186,169 +145,54 @@ jobs:
name: demo-dist
path: demo/dist/
- name: Run Playwright demo tests
run: yarn test:e2e:demo --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
timeout-minutes: 15
- name: Upload demo blob report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: blob-report-demo-${{ matrix.shardIndex }}
path: test/e2e/reports/demo/
if-no-files-found: warn
retention-days: 3
e2e-app:
name: E2E app (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
needs:
- build-e2e-test-app
- prepare-container-dependencies
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.61.1-noble
options: --user 1001 --ipc=host
defaults:
run:
shell: bash
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
shardIndex:
- 1
- 2
- 3
- 4
shardTotal:
- 4
steps:
- name: Check out files from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Node with shared dependencies
uses: ./.github/actions/setup
with:
node-modules-cache: true
node-modules-cache-key: node-modules-container-v1
- name: Download e2e test app build
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: e2e-test-app-dist
path: test/e2e/app/dist/
- name: Run Playwright app tests
run: yarn test:e2e:app --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
timeout-minutes: 15
- name: Upload app blob report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: blob-report-app-${{ matrix.shardIndex }}
path: test/e2e/reports/app/
if-no-files-found: warn
retention-days: 3
e2e-gallery:
name: E2E gallery (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
needs:
- build-gallery
- prepare-container-dependencies
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.61.1-noble
options: --user 1001 --ipc=host
defaults:
run:
shell: bash
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
shardIndex:
- 1
- 2
- 3
- 4
shardTotal:
- 4
steps:
- name: Check out files from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Node with shared dependencies
uses: ./.github/actions/setup
with:
node-modules-cache: true
node-modules-cache-key: node-modules-container-v1
- name: Download gallery build
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: gallery-dist
path: gallery/dist/
- name: Run Playwright gallery tests
run: yarn test:e2e:gallery --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
- name: Run Playwright tests (local)
run: yarn test:e2e
timeout-minutes: 15
- name: Upload gallery blob report
- name: Upload blob report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: blob-report-gallery-${{ matrix.shardIndex }}
path: test/e2e/reports/gallery/
if-no-files-found: warn
name: blob-report-local
path: test/e2e/reports/
retention-days: 3
# ── Merge local blob reports and post PR comment ───────────────────────────
report:
name: Report
needs:
- e2e-demo
- e2e-app
- e2e-gallery
needs: [e2e-local]
runs-on: ubuntu-latest
if: ${{ always() }}
if: ${{ !cancelled() }}
permissions:
contents: read
pull-requests: write
steps:
- name: Check out files from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Node with shared dependencies
- name: Setup Node and install
uses: ./.github/actions/setup
with:
node-modules-cache: true
- name: Download demo blob reports
- name: Download blob report (local)
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
continue-on-error: true
with:
pattern: blob-report-demo-*
path: test/e2e/reports/demo/
- name: Download app blob reports
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
continue-on-error: true
with:
pattern: blob-report-app-*
path: test/e2e/reports/app/
- name: Download gallery blob reports
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
continue-on-error: true
with:
pattern: blob-report-gallery-*
path: test/e2e/reports/gallery/
name: blob-report-local
path: test/e2e/reports/
- name: Stage blobs for merge
run: node test/e2e/collect-blob-reports.mjs
@@ -365,11 +209,7 @@ jobs:
retention-days: 14
- name: Post report to PR
if: >-
github.event_name == 'pull_request' &&
(needs.e2e-demo.result == 'failure' ||
needs.e2e-app.result == 'failure' ||
needs.e2e-gallery.result == 'failure')
if: github.event_name == 'pull_request' && needs.e2e-local.result == 'failure'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
@@ -377,19 +217,3 @@ jobs:
`${process.env.GITHUB_WORKSPACE}/test/e2e/post-report-comment.mjs`
);
await postReportComment({ github, context, core });
- name: Check suite results
run: |
failed=0
for suite in \
"demo:${{ needs.e2e-demo.result }}" \
"app:${{ needs.e2e-app.result }}" \
"gallery:${{ needs.e2e-gallery.result }}"; do
name="${suite%%:*}"
result="${suite#*:}"
echo "E2E ${name}: ${result}"
if [ "$result" != "success" ]; then
failed=1
fi
done
exit "$failed"
+1 -1
View File
@@ -10,6 +10,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Apply labels
uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0
uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0
with:
sync-labels: true
+2 -2
View File
@@ -20,12 +20,12 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -24,7 +24,7 @@ jobs:
pull-requests: write # To label and comment on pull requests
steps:
- name: Check out workflow scripts
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
sparse-checkout: .github/scripts
+1 -1
View File
@@ -18,6 +18,6 @@ jobs:
pull-requests: read
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@eada3c96a64734dd381cfbda23511034e328ddb0 # v7.6.0
- uses: release-drafter/release-drafter@4d75298e00d9e34c483e5ff8c68d0ea1c1940c1e # v7.5.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+5 -5
View File
@@ -26,17 +26,17 @@ jobs:
if: github.repository_owner == 'home-assistant'
steps:
- name: Checkout the repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@e3fb68ebda13d88a0d695082f471ba2c83d025fb # master
uses: home-assistant/actions/helpers/verify-version@f4ca6f671bd429efb108c0f2fa0ae8af0215986c # master
- name: Setup Node and install
uses: ./.github/actions/setup
@@ -56,7 +56,7 @@ jobs:
script/release
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
with:
skip-existing: true
@@ -111,7 +111,7 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Node and install
@@ -42,7 +42,7 @@ jobs:
if: github.event.issue.type.name == 'Task'
steps:
- name: Check out workflow scripts
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
sparse-checkout: .github/scripts
@@ -21,7 +21,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout the repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
+2 -6
View File
@@ -2,7 +2,7 @@
You are helping develop the Home Assistant frontend. This repository is a TypeScript application built from Lit-based Web Components for the Home Assistant web UI.
For the standalone demo — including how to open a specific demo configuration or page via URL — read `demo/AGENTS.md`.
For gallery-specific documentation, demos, page structure, and examples, read `gallery/AGENTS.md` when working under `gallery/`.
## Essential Commands
@@ -46,7 +46,6 @@ Detailed guidance lives in project skills under `.agents/skills/`. Load the matc
- `ha-frontend-testing`: lint, typecheck, Vitest, Playwright e2e dev servers, and benchmarks.
- `ha-frontend-user-facing-text`: localization, terminology, sentence case, and Home Assistant text style.
- `ha-frontend-review`: PR template use, review checklist, and recurring review issues.
- `ha-frontend-gallery`: gallery pages, demos, sidebar structure, content, and verification.
## Pull Requests
@@ -55,7 +54,4 @@ When creating a pull request, use `.github/PULL_REQUEST_TEMPLATE.md` as the PR b
## AI policy
This project follows the [Open Home Foundation AI Policy](AI_POLICY.md).
Autonomous contributions are not accepted: a human must review, understand,
and be able to explain every change before it is submitted. Do not open
issues or pull requests autonomously, and do not post comments on behalf of
a user without their review.
Autonomous contributions are not accepted: a human must review, understand, and be able to explain every change before it is submitted. Do not open issues or pull requests autonomously, and do not post comments on behalf of a user without their review.
+2 -4
View File
@@ -232,7 +232,7 @@ module.exports.config = {
};
},
demo({ isProdBuild, latestBuild, isStatsBuild, isTestBuild }) {
demo({ isProdBuild, latestBuild, isStatsBuild }) {
return {
name: "demo" + nameSuffix(latestBuild),
entry: {
@@ -247,7 +247,6 @@ module.exports.config = {
isProdBuild,
latestBuild,
isStatsBuild,
isTestBuild,
};
},
@@ -307,7 +306,7 @@ module.exports.config = {
};
},
e2eTestApp({ isProdBuild, latestBuild, isStatsBuild, isTestBuild }) {
e2eTestApp({ isProdBuild, latestBuild, isStatsBuild }) {
return {
name: "e2e-test-app" + nameSuffix(latestBuild),
entry: {
@@ -322,7 +321,6 @@ module.exports.config = {
isProdBuild,
latestBuild,
isStatsBuild,
isTestBuild,
};
},
};
-16
View File
@@ -42,22 +42,6 @@ gulp.task(
)
);
gulp.task(
"build-demo-e2e",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
"clean-demo",
// Cast needs to be backwards compatible and older HA has no translations
"translations-enable-merge-backend",
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-demo",
"rspack-prod-demo-e2e",
"gen-pages-demo-prod-e2e"
)
);
gulp.task(
"analyze-demo",
gulp.series(
-15
View File
@@ -39,18 +39,3 @@ gulp.task(
"gen-pages-e2e-test-app-prod"
)
);
gulp.task(
"build-e2e-test-app-e2e",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
"clean-e2e-test-app",
"translations-enable-merge-backend",
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-e2e-test-app",
"rspack-prod-e2e-test-app-e2e",
"gen-pages-e2e-test-app-prod"
)
);
-10
View File
@@ -225,16 +225,6 @@ gulp.task(
)
);
gulp.task(
"gen-pages-demo-prod-e2e",
genPagesProdTask(
DEMO_PAGE_ENTRIES,
paths.demo_dir,
paths.demo_output_root,
paths.demo_output_latest
)
);
const GALLERY_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
gulp.task(
-24
View File
@@ -177,18 +177,6 @@ gulp.task("rspack-prod-demo", () =>
bothBuilds(createDemoConfig, {
isProdBuild: true,
isStatsBuild: env.isStatsBuild(),
isTestBuild: env.isTestBuild(),
})
)
);
gulp.task("rspack-prod-demo-e2e", () =>
prodBuild(
createDemoConfig({
isProdBuild: true,
latestBuild: true,
isStatsBuild: env.isStatsBuild(),
isTestBuild: env.isTestBuild(),
})
)
);
@@ -281,18 +269,6 @@ gulp.task("rspack-prod-e2e-test-app", () =>
bothBuilds(createE2eTestAppConfig, {
isProdBuild: true,
isStatsBuild: env.isStatsBuild(),
isTestBuild: env.isTestBuild(),
})
)
);
gulp.task("rspack-prod-e2e-test-app-e2e", () =>
prodBuild(
createE2eTestAppConfig({
isProdBuild: true,
latestBuild: true,
isStatsBuild: env.isStatsBuild(),
isTestBuild: env.isTestBuild(),
})
)
);
+4 -19
View File
@@ -387,14 +387,9 @@ const createAppConfig = ({
bundle.config.app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild })
);
const createDemoConfig = ({
isProdBuild,
latestBuild,
isStatsBuild,
isTestBuild,
}) =>
const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) =>
createRspackConfig(
bundle.config.demo({ isProdBuild, latestBuild, isStatsBuild, isTestBuild })
bundle.config.demo({ isProdBuild, latestBuild, isStatsBuild })
);
const createCastConfig = ({ isProdBuild, latestBuild }) =>
@@ -406,19 +401,9 @@ const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
const createLandingPageConfig = ({ isProdBuild, latestBuild }) =>
createRspackConfig(bundle.config.landingPage({ isProdBuild, latestBuild }));
const createE2eTestAppConfig = ({
isProdBuild,
latestBuild,
isStatsBuild,
isTestBuild,
}) =>
const createE2eTestAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) =>
createRspackConfig(
bundle.config.e2eTestApp({
isProdBuild,
latestBuild,
isStatsBuild,
isTestBuild,
})
bundle.config.e2eTestApp({ isProdBuild, latestBuild, isStatsBuild })
);
module.exports = {
-46
View File
@@ -1,46 +0,0 @@
# Demo Agent Instructions
This file applies to all files under `demo/`. Follow the root `AGENTS.md` for repository-wide standards.
The demo is the full Home Assistant frontend running against a mocked backend (published at https://demo.home-assistant.io). It needs no Home Assistant server, which makes it the easiest way to load the real UI in a browser, for example to take screenshots.
## Running the demo
Run from the repository root:
```bash
yarn dev:demo # dev server on http://localhost:8090
yarn dev:demo --background # detached; also supports --status/--stop/--logs
```
## Opening a specific demo
The demo contains multiple demo configurations. Select one directly with the `demo` query parameter, e.g. `http://localhost:8090/?demo=<slug>`. The valid slugs are defined in `demoConfigs` in `src/configs/demo-configs.ts`, so "the second demo" means the slug of the second entry in that list. An unknown slug falls back to the default (the first entry).
## Opening a specific page
The demo build uses hash-based routing: the frontend path goes in the URL hash, and the `demo` query parameter goes before the `#`. The URL format is:
```
http://localhost:8090/?demo=<slug>#/<path>
```
Useful paths:
- `/lovelace/0` — the selected demo's dashboard (also the default when no hash is given)
- `/energy` — energy dashboard. Its tabs are views: `/energy/overview` (Summary), `/energy/electricity`, `/energy/gas`, `/energy/water`, `/energy/now`
- `/map`, `/history`, `/todo`, `/config` — other sidebar panels
Example — the water tab of the energy dashboard of the second demo:
```
http://localhost:8090/?demo=<second slug>#/energy/water
```
## Structure
- `src/ha-demo.ts`: Root element; sets up all backend mocks.
- `src/configs/<slug>/`: One directory per demo configuration (entities, dashboard, theme).
- `src/configs/demo-configs.ts`: Registry of demo configurations and URL slug handling.
- `src/stubs/`: Mocked WebSocket/REST APIs.
- `script/develop_demo`, `script/build_demo`: Dev server and static build wrappers.
-1
View File
@@ -1 +0,0 @@
AGENTS.md
+13 -36
View File
@@ -1,9 +1,6 @@
import { navigate } from "../../../src/common/navigate";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import type { Lovelace } from "../../../src/panels/lovelace/types";
import { setDemoAreas } from "../stubs/area_registry";
import { energyEntities } from "../stubs/entities";
import { setDemoFloors } from "../stubs/floor_registry";
import { getDemoTheme } from "../stubs/frontend";
import type { DemoConfig, DemoTheme } from "./types";
@@ -15,53 +12,33 @@ export const applyDemoTheme = (hass: MockHomeAssistant, theme: DemoTheme) => {
hass.mockTheme(null, getDemoTheme(theme));
};
export const demoConfigs: Record<string, () => Promise<DemoConfig>> = {
sections: () => import("./sections").then((mod) => mod.demoSections),
home: () => import("./home").then((mod) => mod.demoHome),
arsaboo: () => import("./arsaboo").then((mod) => mod.demoArsaboo),
teachingbirds: () =>
import("./teachingbirds").then((mod) => mod.demoTeachingbirds),
kernehed: () => import("./kernehed").then((mod) => mod.demoKernehed),
jimpower: () => import("./jimpower").then((mod) => mod.demoJimpower),
};
export const demos = Object.keys(demoConfigs);
const initialDemo = () => {
const slug = new URLSearchParams(window.location.search).get("demo");
return slug && demos.includes(slug) ? slug : demos[0];
};
export const demoConfigs: (() => Promise<DemoConfig>)[] = [
() => import("./sections").then((mod) => mod.demoSections),
() => import("./arsaboo").then((mod) => mod.demoArsaboo),
() => import("./teachingbirds").then((mod) => mod.demoTeachingbirds),
() => import("./kernehed").then((mod) => mod.demoKernehed),
() => import("./jimpower").then((mod) => mod.demoJimpower),
];
// eslint-disable-next-line import-x/no-mutable-exports
export let selectedDemo = initialDemo();
export let selectedDemoConfigIndex = 0;
// eslint-disable-next-line import-x/no-mutable-exports
export let selectedDemoConfig: Promise<DemoConfig> =
demoConfigs[selectedDemo]();
demoConfigs[selectedDemoConfigIndex]();
export const setDemoConfig = async (
hass: MockHomeAssistant,
lovelace: Lovelace,
demo: string
index: number
) => {
const confProm = demoConfigs[demo]();
const confProm = demoConfigs[index]();
const config = await confProm;
selectedDemo = demo;
selectedDemoConfigIndex = index;
selectedDemoConfig = confProm;
setDemoFloors(hass, config.floors);
setDemoAreas(hass, config.areas);
hass.addEntities(config.entities(hass.localize), true);
hass.addEntities(energyEntities());
// Let the new registries and entities reach the dashboard before saving the
// config, so dashboard strategies generate against them
await new Promise((resolve) => {
setTimeout(resolve, 0);
});
await lovelace.saveConfig(config.lovelace(hass.localize));
// The view of the previous demo might not exist in the new one
navigate(`/${hass.panelUrl}?demo=${demo}`, { replace: true });
lovelace.saveConfig(config.lovelace(hass.localize));
applyDemoTheme(hass, config.theme);
};
-63
View File
@@ -1,63 +0,0 @@
import type { DemoArea } from "../../stubs/area_registry";
import type { DemoFloor } from "../../stubs/floor_registry";
export const demoFloorsHome: DemoFloor[] = [
{
floor_id: "ground",
name: "Ground floor",
level: 0,
},
{
floor_id: "upstairs",
name: "Upstairs",
level: 1,
},
];
export const demoAreasHome: DemoArea[] = [
{
area_id: "living_room",
name: "Living room",
floor_id: "ground",
icon: "mdi:sofa",
temperature_entity_id: "sensor.living_room_temperature",
humidity_entity_id: "sensor.living_room_humidity",
},
{
area_id: "kitchen",
name: "Kitchen",
floor_id: "ground",
icon: "mdi:fridge",
temperature_entity_id: "sensor.kitchen_temperature",
humidity_entity_id: "sensor.kitchen_humidity",
},
{
area_id: "entrance",
name: "Entrance",
floor_id: "ground",
icon: "mdi:door",
},
{
area_id: "bedroom",
name: "Bedroom",
floor_id: "upstairs",
icon: "mdi:bed",
temperature_entity_id: "sensor.bedroom_temperature",
humidity_entity_id: "sensor.bedroom_humidity",
},
{
area_id: "office",
name: "Office",
floor_id: "upstairs",
icon: "mdi:desk",
temperature_entity_id: "sensor.office_temperature",
humidity_entity_id: "sensor.office_humidity",
},
{
area_id: "garden",
name: "Garden",
icon: "mdi:tree",
temperature_entity_id: "sensor.garden_temperature",
humidity_entity_id: "sensor.garden_humidity",
},
];
-465
View File
@@ -1,465 +0,0 @@
import type { DemoConfig } from "../types";
export const demoEntitiesHome: DemoConfig["entities"] = () => [
// The devices tile on the overview uses zone.home, which always exists in
// a real installation.
{
entity_id: "zone.home",
state: "1",
attributes: {
latitude: 52.3731339,
longitude: 4.8903147,
radius: 100,
friendly_name: "Home",
icon: "mdi:home",
},
},
{
entity_id: "light.living_room_floor_lamp",
state: "on",
area_id: "living_room",
attributes: {
min_color_temp_kelvin: 2000,
max_color_temp_kelvin: 6535,
supported_color_modes: ["color_temp", "xy"],
color_mode: "color_temp",
brightness: 178,
color_temp_kelvin: 2583,
icon: "mdi:floor-lamp",
friendly_name: "Floor lamp",
supported_features: 44,
},
},
{
entity_id: "light.living_room_spotlights",
state: "on",
area_id: "living_room",
attributes: {
supported_color_modes: ["brightness"],
color_mode: "brightness",
brightness: 126,
icon: "mdi:ceiling-light-multiple",
friendly_name: "Spotlights",
supported_features: 32,
},
},
{
entity_id: "climate.living_room",
state: "heat",
area_id: "living_room",
attributes: {
hvac_modes: ["auto", "heat", "off"],
min_temp: 7,
max_temp: 35,
current_temperature: 21.5,
temperature: 21,
friendly_name: "Thermostat",
supported_features: 385,
},
},
{
entity_id: "cover.living_room_blinds",
state: "open",
area_id: "living_room",
attributes: {
current_position: 100,
device_class: "blind",
friendly_name: "Blinds",
supported_features: 15,
},
},
{
entity_id: "binary_sensor.living_room_window",
state: "off",
area_id: "living_room",
attributes: {
device_class: "window",
friendly_name: "Window",
},
},
{
entity_id: "media_player.living_room_speaker",
state: "playing",
area_id: "living_room",
attributes: {
device_class: "speaker",
volume_level: 0.28,
is_volume_muted: false,
media_content_type: "music",
media_duration: 300,
media_position: 0,
media_position_updated_at: new Date(
new Date().getTime() - 23000
).toISOString(),
media_title: "I Wasn't Born To Follow",
media_artist: "The Byrds",
media_album_name: "The Notorious Byrd Brothers",
shuffle: false,
friendly_name: "Living room speaker",
supported_features: 64063,
},
},
{
entity_id: "sensor.living_room_temperature",
state: "21.5",
area_id: "living_room",
attributes: {
state_class: "measurement",
unit_of_measurement: "°C",
device_class: "temperature",
friendly_name: "Temperature",
},
},
{
entity_id: "sensor.living_room_humidity",
state: "45",
area_id: "living_room",
attributes: {
state_class: "measurement",
unit_of_measurement: "%",
device_class: "humidity",
friendly_name: "Humidity",
},
},
{
entity_id: "light.kitchen_spotlights",
state: "on",
area_id: "kitchen",
attributes: {
supported_color_modes: ["brightness"],
color_mode: "brightness",
brightness: 255,
icon: "mdi:ceiling-light-multiple",
friendly_name: "Spotlights",
supported_features: 32,
},
},
{
entity_id: "light.kitchen_worktop",
state: "off",
area_id: "kitchen",
attributes: {
supported_color_modes: ["brightness"],
color_mode: null,
brightness: null,
icon: "mdi:light-flood-down",
friendly_name: "Worktop",
supported_features: 32,
},
},
{
entity_id: "switch.coffee_machine",
state: "on",
area_id: "kitchen",
attributes: {
icon: "mdi:coffee-maker",
friendly_name: "Coffee machine",
},
},
{
entity_id: "binary_sensor.kitchen_motion",
state: "off",
area_id: "kitchen",
attributes: {
device_class: "motion",
friendly_name: "Motion",
},
},
{
entity_id: "media_player.kitchen_speaker",
state: "idle",
area_id: "kitchen",
attributes: {
device_class: "speaker",
volume_level: 0.18,
is_volume_muted: false,
friendly_name: "Kitchen speaker",
supported_features: 64063,
},
},
{
entity_id: "sensor.kitchen_temperature",
state: "22.1",
area_id: "kitchen",
attributes: {
state_class: "measurement",
unit_of_measurement: "°C",
device_class: "temperature",
friendly_name: "Temperature",
},
},
{
entity_id: "sensor.kitchen_humidity",
state: "48",
area_id: "kitchen",
attributes: {
state_class: "measurement",
unit_of_measurement: "%",
device_class: "humidity",
friendly_name: "Humidity",
},
},
{
entity_id: "lock.front_door",
state: "locked",
area_id: "entrance",
attributes: {
friendly_name: "Front door",
supported_features: 1,
},
},
{
entity_id: "binary_sensor.front_door",
state: "off",
area_id: "entrance",
attributes: {
device_class: "door",
friendly_name: "Front door",
},
},
{
entity_id: "alarm_control_panel.home_alarm",
state: "disarmed",
area_id: "entrance",
attributes: {
changed_by: null,
code_arm_required: false,
friendly_name: "Home alarm",
supported_features: 3,
},
},
{
entity_id: "light.entrance_ceiling",
state: "off",
area_id: "entrance",
attributes: {
supported_color_modes: ["brightness"],
color_mode: null,
brightness: null,
friendly_name: "Ceiling light",
supported_features: 32,
},
},
{
entity_id: "sensor.front_door_lock_battery",
state: "88",
area_id: "entrance",
attributes: {
state_class: "measurement",
unit_of_measurement: "%",
device_class: "battery",
friendly_name: "Front door lock battery",
},
},
{
entity_id: "light.bedroom_ceiling",
state: "off",
area_id: "bedroom",
attributes: {
supported_color_modes: ["color_temp"],
color_mode: null,
brightness: null,
friendly_name: "Ceiling light",
supported_features: 44,
},
},
{
entity_id: "light.bedside_lamp",
state: "off",
area_id: "bedroom",
attributes: {
supported_color_modes: ["brightness"],
color_mode: null,
brightness: null,
icon: "mdi:lamp",
friendly_name: "Bedside lamp",
supported_features: 32,
},
},
{
entity_id: "cover.bedroom_shutter",
state: "open",
area_id: "bedroom",
attributes: {
current_position: 100,
device_class: "shutter",
friendly_name: "Shutter",
supported_features: 15,
},
},
{
entity_id: "media_player.bedroom_speaker",
state: "off",
area_id: "bedroom",
attributes: {
device_class: "speaker",
friendly_name: "Bedroom speaker",
supported_features: 64063,
},
},
{
entity_id: "binary_sensor.bedroom_motion",
state: "off",
area_id: "bedroom",
attributes: {
device_class: "motion",
friendly_name: "Motion",
},
},
{
entity_id: "sensor.bedroom_motion_battery",
state: "15",
area_id: "bedroom",
attributes: {
state_class: "measurement",
unit_of_measurement: "%",
device_class: "battery",
friendly_name: "Motion sensor battery",
},
},
{
entity_id: "sensor.bedroom_temperature",
state: "19.6",
area_id: "bedroom",
attributes: {
state_class: "measurement",
unit_of_measurement: "°C",
device_class: "temperature",
friendly_name: "Temperature",
},
},
{
entity_id: "sensor.bedroom_humidity",
state: "52",
area_id: "bedroom",
attributes: {
state_class: "measurement",
unit_of_measurement: "%",
device_class: "humidity",
friendly_name: "Humidity",
},
},
{
entity_id: "light.office_desk_lamp",
state: "on",
area_id: "office",
attributes: {
supported_color_modes: ["brightness"],
color_mode: "brightness",
brightness: 200,
icon: "mdi:desk-lamp",
friendly_name: "Desk lamp",
supported_features: 32,
},
},
{
entity_id: "fan.office_ceiling_fan",
state: "off",
area_id: "office",
attributes: {
percentage: 0,
percentage_step: 25,
friendly_name: "Ceiling fan",
supported_features: 1,
},
},
{
entity_id: "binary_sensor.office_window",
state: "off",
area_id: "office",
attributes: {
device_class: "window",
friendly_name: "Window",
},
},
{
entity_id: "sensor.office_temperature",
state: "21.8",
area_id: "office",
attributes: {
state_class: "measurement",
unit_of_measurement: "°C",
device_class: "temperature",
friendly_name: "Temperature",
},
},
{
entity_id: "sensor.office_humidity",
state: "50",
area_id: "office",
attributes: {
state_class: "measurement",
unit_of_measurement: "%",
device_class: "humidity",
friendly_name: "Humidity",
},
},
{
entity_id: "switch.garden_sprinkler",
state: "off",
area_id: "garden",
attributes: {
icon: "mdi:sprinkler-variant",
friendly_name: "Sprinkler",
},
},
{
entity_id: "light.garden_path",
state: "off",
area_id: "garden",
attributes: {
supported_color_modes: ["brightness"],
color_mode: null,
brightness: null,
icon: "mdi:outdoor-lamp",
friendly_name: "Path lights",
supported_features: 32,
},
},
{
entity_id: "binary_sensor.garden_gate",
state: "off",
area_id: "garden",
attributes: {
device_class: "opening",
friendly_name: "Gate",
},
},
{
entity_id: "sensor.garden_temperature",
state: "14.2",
area_id: "garden",
attributes: {
state_class: "measurement",
unit_of_measurement: "°C",
device_class: "temperature",
friendly_name: "Temperature",
},
},
{
entity_id: "sensor.garden_humidity",
state: "68",
area_id: "garden",
attributes: {
state_class: "measurement",
unit_of_measurement: "%",
device_class: "humidity",
friendly_name: "Humidity",
},
},
{
entity_id: "weather.home",
state: "partlycloudy",
area_id: "garden",
attributes: {
temperature: 14.2,
temperature_unit: "°C",
humidity: 68,
pressure: 1012,
pressure_unit: "hPa",
wind_speed: 11.2,
wind_speed_unit: "km/h",
friendly_name: "Home",
},
},
];
-17
View File
@@ -1,17 +0,0 @@
import type { DemoConfig } from "../types";
import { demoAreasHome, demoFloorsHome } from "./areas";
import { demoEntitiesHome } from "./entities";
import { demoLovelaceHome } from "./lovelace";
export const demoHome: DemoConfig = {
authorName: "Home Assistant",
authorUrl: "https://www.home-assistant.io",
name: "Home page",
description:
"The page you land on when you open Home Assistant, automatically built from the areas in your home and the devices in them.",
lovelace: demoLovelaceHome,
entities: demoEntitiesHome,
floors: demoFloorsHome,
areas: demoAreasHome,
theme: { theme: "default", dark: false },
};
-9
View File
@@ -1,9 +0,0 @@
import "./strategies";
import type { DemoConfig } from "../types";
export const demoLovelaceHome: DemoConfig["lovelace"] = () => ({
strategy: {
type: "custom:demo-home",
favorite_entities: ["lock.front_door", "switch.garden_sprinkler"],
},
});
-109
View File
@@ -1,109 +0,0 @@
import { ReactiveElement } from "lit";
import { isStrategySection } from "../../../../src/data/lovelace/config/section";
import type { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
import type { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view";
import { isStrategyView } from "../../../../src/data/lovelace/config/view";
import type { HomeDashboardStrategyConfig } from "../../../../src/panels/lovelace/strategies/home/home-dashboard-strategy";
import { HomeDashboardStrategy } from "../../../../src/panels/lovelace/strategies/home/home-dashboard-strategy";
import type { HomeOverviewViewStrategyConfig } from "../../../../src/panels/lovelace/strategies/home/home-overview-view-strategy";
import { HomeOverviewViewStrategy } from "../../../../src/panels/lovelace/strategies/home/home-overview-view-strategy";
import { generateLovelaceSectionStrategy } from "../../../../src/panels/lovelace/strategies/get-strategy";
import type { HomeAssistant } from "../../../../src/types";
export interface DemoHomeDashboardStrategyConfig extends Omit<
HomeDashboardStrategyConfig,
"type"
> {
type: "custom:demo-home";
}
interface DemoHomeOverviewViewStrategyConfig extends Omit<
HomeOverviewViewStrategyConfig,
"type"
> {
type: "custom:demo-home-overview";
}
class DemoHomeDashboardStrategy extends ReactiveElement {
static registryDependencies = HomeDashboardStrategy.registryDependencies;
static async generate(
config: DemoHomeDashboardStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceConfig> {
const generated = await HomeDashboardStrategy.generate(
{ ...config, type: "home" },
hass
);
// Swap the overview view for the demo version, which adds the demo card.
return {
...generated,
views: generated.views.map((view) =>
isStrategyView(view) && view.strategy.type === "home-overview"
? {
...view,
strategy: { ...view.strategy, type: "custom:demo-home-overview" },
}
: view
),
};
}
}
class DemoHomeOverviewViewStrategy extends ReactiveElement {
static registryDependencies = HomeOverviewViewStrategy.registryDependencies;
static async generate(
config: DemoHomeOverviewViewStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const view = await HomeOverviewViewStrategy.generate(
{ ...config, type: "home-overview" },
hass
);
// Expand the favorites section so the demo card can be added to it
const sections = await Promise.all(
(view.sections || []).map(async (section) => {
if (
!isStrategySection(section) ||
section.strategy.type !== "common-controls"
) {
return section;
}
// The demo card takes up the space of two tiles
const limit = (section.strategy.limit as number | undefined) ?? 8;
const favorites = await generateLovelaceSectionStrategy(
{ ...section, strategy: { ...section.strategy, limit: limit - 2 } },
hass
);
const [heading, ...cards] = favorites.cards || [];
return {
...favorites,
// Place the demo card first so the tiles fill the rows next to it
cards: [heading, { type: "custom:ha-demo-next-card" }, ...cards],
};
})
);
return {
...view,
sections: sections,
};
}
}
customElements.define(
"ll-strategy-dashboard-demo-home",
DemoHomeDashboardStrategy
);
customElements.define(
"ll-strategy-view-demo-home-overview",
DemoHomeOverviewViewStrategy
);
declare global {
interface HTMLElementTagNameMap {
"ll-strategy-dashboard-demo-home": DemoHomeDashboardStrategy;
"ll-strategy-view-demo-home-overview": DemoHomeOverviewViewStrategy;
}
}
+2 -6
View File
@@ -1,10 +1,8 @@
import type { TemplateResult } from "lit";
import type { LocalizeFunc } from "../../../src/common/translations/localize";
import type { LovelaceRawConfig } from "../../../src/data/lovelace/config/types";
import type { LovelaceConfig } from "../../../src/data/lovelace/config/types";
import type { EntityInput } from "../../../src/fake_data/entities/types";
import type { ThemeSettings } from "../../../src/types";
import type { DemoArea } from "../stubs/area_registry";
import type { DemoFloor } from "../stubs/floor_registry";
export type DemoTheme = ThemeSettings | (() => Record<string, string> | null);
@@ -15,9 +13,7 @@ export interface DemoConfig {
authorUrl: string;
description?:
string | ((localize: LocalizeFunc) => string | TemplateResult<1>);
lovelace: (localize: LocalizeFunc) => LovelaceRawConfig;
lovelace: (localize: LocalizeFunc) => LovelaceConfig;
entities: (localize: LocalizeFunc) => EntityInput[];
floors?: DemoFloor[];
areas?: DemoArea[];
theme: DemoTheme;
}
+18 -23
View File
@@ -41,30 +41,25 @@ class CastDemoRow extends LitElement implements LovelaceRow {
protected firstUpdated(changedProps: PropertyValues<this>) {
super.firstUpdated(changedProps);
import("../../../src/cast/cast_manager").then(({ getCastManager }) =>
getCastManager().then(
(mgr) => {
this._castManager = mgr;
mgr.addEventListener("state-changed", () => {
this.requestUpdate();
});
mgr.castContext.addEventListener(
cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
(ev) => {
// On Android, opening a new session always results in SESSION_RESUMED.
// So treat both as the same.
if (
ev.sessionState === "SESSION_STARTED" ||
ev.sessionState === "SESSION_RESUMED"
) {
castSendShowDemo(mgr);
}
getCastManager().then((mgr) => {
this._castManager = mgr;
mgr.addEventListener("state-changed", () => {
this.requestUpdate();
});
mgr.castContext.addEventListener(
cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
(ev) => {
// On Android, opening a new session always results in SESSION_RESUMED.
// So treat both as the same.
if (
ev.sessionState === "SESSION_STARTED" ||
ev.sessionState === "SESSION_RESUMED"
) {
castSendShowDemo(mgr);
}
);
},
() => {
this._castManager = null;
}
)
}
);
})
);
}
+9 -5
View File
@@ -13,9 +13,9 @@ import type {
LovelaceCard,
} from "../../../src/panels/lovelace/types";
import {
demos,
selectedDemo,
demoConfigs,
selectedDemoConfig,
selectedDemoConfigIndex,
} from "../configs/demo-configs";
@customElement("ha-demo-card")
@@ -112,12 +112,16 @@ export class HADemoCard extends LitElement implements LovelaceCard {
}
private _nextConfig() {
this._updateConfig(demos[(demos.indexOf(selectedDemo) + 1) % demos.length]);
this._updateConfig(
selectedDemoConfigIndex < demoConfigs.length - 1
? selectedDemoConfigIndex + 1
: 0
);
}
private async _updateConfig(demo: string) {
private async _updateConfig(index: number) {
this._switching = true;
fireEvent(this, "set-demo-config" as any, { demo });
fireEvent(this, "set-demo-config" as any, { index });
}
static get styles(): CSSResultGroup {
-103
View File
@@ -1,103 +0,0 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-card";
import "../../../src/components/ha-control-button";
import "../../../src/components/ha-control-button-group";
import "../../../src/components/tile/ha-tile-container";
import "../../../src/components/tile/ha-tile-icon";
import "../../../src/components/tile/ha-tile-info";
import type { LovelaceCardConfig } from "../../../src/data/lovelace/config/card";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import { tileCardStyle } from "../../../src/panels/lovelace/cards/tile/tile-card-style";
import type {
LovelaceCard,
LovelaceGridOptions,
} from "../../../src/panels/lovelace/types";
import {
demos,
selectedDemo,
selectedDemoConfig,
} from "../configs/demo-configs";
@customElement("ha-demo-next-card")
export class HADemoNextCard extends LitElement implements LovelaceCard {
@property({ attribute: false }) public hass!: MockHomeAssistant;
@state() private _switching = false;
public getCardSize() {
return 2;
}
public getGridOptions(): LovelaceGridOptions {
return {
columns: 6,
rows: 2,
min_columns: 6,
min_rows: 2,
};
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
public setConfig(_config: LovelaceCardConfig) {}
protected render() {
return html`
<ha-card>
<ha-tile-container>
<ha-tile-icon slot="icon" icon="mdi:home-assistant"></ha-tile-icon>
<ha-tile-info slot="info">
<span slot="primary">
${until(
selectedDemoConfig.then((conf) => conf.name),
nothing
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.page-demo.cards.demo.interactive_demo"
)}
</span>
</ha-tile-info>
<ha-control-button-group slot="features">
<ha-control-button
.disabled=${this._switching}
@click=${this._nextConfig}
>
${this.hass.localize("ui.panel.page-demo.cards.demo.next_demo")}
</ha-control-button>
</ha-control-button-group>
</ha-tile-container>
</ha-card>
`;
}
private _nextConfig() {
this._switching = true;
fireEvent(this, "set-demo-config" as any, {
demo: demos[(demos.indexOf(selectedDemo) + 1) % demos.length],
});
}
static styles = [
tileCardStyle,
css`
:host {
--tile-color: var(--primary-color);
}
ha-control-button-group {
--control-button-group-spacing: 0;
--control-button-group-thickness: var(--feature-height, 42px);
padding: 0 var(--ha-space-3) var(--ha-space-3);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-demo-next-card": HADemoNextCard;
}
}
+5 -16
View File
@@ -6,7 +6,7 @@ import { provideHass } from "../../src/fake_data/provide_hass";
import { HomeAssistantAppEl } from "../../src/layouts/home-assistant";
import type { HomeAssistant } from "../../src/types";
import { applyDemoTheme, selectedDemoConfig } from "./configs/demo-configs";
import { mockAreaRegistry, setDemoAreas } from "./stubs/area_registry";
import { mockAreaRegistry } from "./stubs/area_registry";
import { mockAuth } from "./stubs/auth";
import { demoDevices } from "./stubs/devices";
import { mockDeviceRegistry } from "./stubs/device_registry";
@@ -14,7 +14,7 @@ import { mockEnergy } from "./stubs/energy";
import { energyEntities } from "./stubs/entities";
import { mockEntityRegistry } from "./stubs/entity_registry";
import { mockEvents } from "./stubs/events";
import { mockFloorRegistry, setDemoFloors } from "./stubs/floor_registry";
import { mockFloorRegistry } from "./stubs/floor_registry";
import { mockFrontend } from "./stubs/frontend";
import { mockIntegration } from "./stubs/integration";
import { mockLabelRegistry } from "./stubs/label_registry";
@@ -29,7 +29,6 @@ import { mockSystemLog } from "./stubs/system_log";
import { mockTemplate } from "./stubs/template";
import { mockTodo } from "./stubs/todo";
import { mockTranslations } from "./stubs/translations";
import { mockUsagePrediction } from "./stubs/usage_prediction";
import "./cloud/cloud-demo-controls";
// WS command / REST path prefixes whose mocks live in the lazily imported
@@ -75,18 +74,11 @@ export class HaDemo extends HomeAssistantAppEl {
// The cloud account page only fetches backup config and the webhook count
// when those integrations are loaded. Enable them here (demo only) so the
// mocked backup/config/info and webhook/list are queried. usage_prediction
// is needed for common-controls sections in strategy dashboards.
// mocked backup/config/info and webhook/list are queried.
hass.updateHass({
config: {
...hass.config,
components: [
...(hass.config?.components ?? []),
"backup",
"webhook",
"usage_prediction",
"assist_pipeline",
],
components: [...(hass.config?.components ?? []), "backup", "webhook"],
},
});
@@ -130,7 +122,6 @@ export class HaDemo extends HomeAssistantAppEl {
mockDeviceRegistry(hass, demoDevices);
mockFloorRegistry(hass);
mockLabelRegistry(hass);
mockUsagePrediction(hass);
mockEntityRegistry(hass, [
{
config_entry_id: "co2signal",
@@ -178,11 +169,9 @@ export class HaDemo extends HomeAssistantAppEl {
hass.addEntities(energyEntities());
// Once config is loaded AND localize, set registries, entities and theme.
// Once config is loaded AND localize, set entities and apply theme.
Promise.all([selectedDemoConfig, localizePromise]).then(
([conf, localize]) => {
setDemoFloors(hass, conf.floors);
setDemoAreas(hass, conf.areas);
hass.addEntities(conf.entities(localize));
applyDemoTheme(hass, conf.theme);
}
+6 -38
View File
@@ -1,46 +1,14 @@
import type { AreaRegistryEntry } from "../../../src/data/area/area_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export interface DemoArea {
area_id: string;
name: string;
floor_id?: string;
icon?: string;
temperature_entity_id?: string;
humidity_entity_id?: string;
}
let areas: AreaRegistryEntry[] = [];
export const mockAreaRegistry = (
hass: MockHomeAssistant,
data: DemoArea[] = []
data: AreaRegistryEntry[] = []
) => {
hass.mockWS("config/area_registry/list", () => areas);
setDemoAreas(hass, data);
};
/** Set the areas of the currently loaded demo config. */
export const setDemoAreas = (
hass: MockHomeAssistant,
demoAreas: DemoArea[] = []
) => {
areas = demoAreas.map((area) => ({
area_id: area.area_id,
name: area.name,
floor_id: area.floor_id ?? null,
icon: area.icon ?? null,
temperature_entity_id: area.temperature_entity_id ?? null,
humidity_entity_id: area.humidity_entity_id ?? null,
aliases: [],
labels: [],
picture: null,
created_at: 0,
modified_at: 0,
}));
const areasById: Record<string, AreaRegistryEntry> = {};
areas.forEach((area) => {
areasById[area.area_id] = area;
hass.mockWS("config/area_registry/list", () => data);
const areas = {};
data.forEach((area) => {
areas[area.area_id] = area;
});
hass.updateHass({ areas: areasById });
hass.updateHass({ areas });
};
+4 -18
View File
@@ -19,31 +19,17 @@ const pipelines: AssistPipeline[] = [
{
id: "01local",
name: "Local",
language: "nl",
language: "en",
conversation_engine: "conversation.home_assistant",
conversation_language: "nl",
conversation_language: "en",
stt_engine: "stt.faster_whisper",
stt_language: "nl",
stt_language: "en",
tts_engine: "tts.piper",
tts_language: "nl",
tts_language: "en",
tts_voice: null,
wake_word_entity: null,
wake_word_id: null,
},
{
id: "01chatgpt",
name: "ChatGPT",
language: "es",
conversation_engine: "conversation.chatgpt",
conversation_language: "es",
stt_engine: "cloud",
stt_language: "es-ES",
tts_engine: "cloud",
tts_language: "es-ES",
tts_voice: "ElviraNeural",
wake_word_entity: null,
wake_word_id: null,
},
];
export const mockAssist = (hass: MockHomeAssistant) => {
+1 -1
View File
@@ -85,7 +85,7 @@ export const energyEntities = () =>
},
},
"sensor.energy_consumption_tarif_1": {
entity_id: "sensor.energy_consumption_tarif_1",
entity_id: "sensor.energy_consumption_tarif_1 ",
state: "88.6",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
+2 -35
View File
@@ -1,40 +1,7 @@
import type { FloorRegistryEntry } from "../../../src/data/floor_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export interface DemoFloor {
floor_id: string;
name: string;
level?: number;
icon?: string;
}
let floors: FloorRegistryEntry[] = [];
export const mockFloorRegistry = (
hass: MockHomeAssistant,
data: DemoFloor[] = []
) => {
hass.mockWS("config/floor_registry/list", () => floors);
setDemoFloors(hass, data);
};
/** Set the floors of the currently loaded demo config. */
export const setDemoFloors = (
hass: MockHomeAssistant,
demoFloors: DemoFloor[] = []
) => {
floors = demoFloors.map((floor) => ({
floor_id: floor.floor_id,
name: floor.name,
level: floor.level ?? null,
icon: floor.icon ?? null,
aliases: [],
created_at: 0,
modified_at: 0,
}));
const floorsById: Record<string, FloorRegistryEntry> = {};
floors.forEach((floor) => {
floorsById[floor.floor_id] = floor;
});
hass.updateHass({ floors: floorsById });
};
data: FloorRegistryEntry[] = []
) => hass.mockWS("config/floor_registry/list", () => data);
+4 -5
View File
@@ -2,13 +2,12 @@ import type { LocalizeFunc } from "../../../src/common/translations/localize";
import type { LovelaceInfo } from "../../../src/data/lovelace/resource";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import {
selectedDemo,
selectedDemoConfig,
selectedDemoConfigIndex,
setDemoConfig,
} from "../configs/demo-configs";
import "../custom-cards/cast-demo-row";
import "../custom-cards/ha-demo-card";
import "../custom-cards/ha-demo-next-card";
import { mapEntities } from "./entities";
export const mockLovelace = (
@@ -46,11 +45,11 @@ customElements.whenDefined("hui-root").then(() => {
HUIRoot.prototype.firstUpdated = function (changedProperties) {
oldFirstUpdated.call(this, changedProperties);
this.addEventListener("set-demo-config", async (ev) => {
const demo = (ev as CustomEvent).detail.demo;
const index = (ev as CustomEvent).detail.index;
try {
await setDemoConfig(this.hass, this.lovelace!, demo);
await setDemoConfig(this.hass, this.lovelace!, index);
} catch (_err: any) {
setDemoConfig(this.hass, this.lovelace!, selectedDemo);
setDemoConfig(this.hass, this.lovelace!, selectedDemoConfigIndex);
alert("Failed to switch config :-(");
}
});
-19
View File
@@ -1,19 +0,0 @@
import type { CommonControlsResult } from "../../../src/data/usage_prediction";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockUsagePrediction = (hass: MockHomeAssistant) => {
// Entities that don't exist in the currently loaded demo config are
// filtered out by the common-controls section strategy.
hass.mockWS("usage_prediction/common_control", (): CommonControlsResult => ({
entities: [
"light.living_room_floor_lamp",
"climate.living_room",
"cover.living_room_blinds",
"media_player.living_room_speaker",
"light.kitchen_spotlights",
"switch.coffee_machine",
"light.bedside_lamp",
"alarm_control_panel.home_alarm",
],
}));
};
@@ -1,11 +1,6 @@
---
name: ha-frontend-gallery
description: Home Assistant frontend gallery structure, pages, demos, content, and verification. Use when changing files under gallery/, including gallery markdown, TypeScript demos, sidebar entries, mock data, page generation, or gallery builds.
---
# Gallery Agent Instructions
# HA Frontend Gallery
Use this skill for all work under `gallery/`. Follow the persistent repository guidance in `AGENTS.md` and load the matching specialist skills alongside this gallery-specific guidance.
This file applies to all files under `gallery/`. Follow the root `AGENTS.md` for repository-wide Home Assistant frontend, TypeScript, Lit, accessibility, and copy standards. This file adds gallery-specific structure, page, demo, and verification guidance.
## Quick Reference
@@ -18,7 +13,7 @@ yarn lint # ESLint, Prettier, TypeScript, and Lit checks
yarn lint:types # TypeScript compiler, without file arguments
```
Never run `yarn lint:types` or `tsc` with file arguments. File arguments make `tsc` ignore `tsconfig.json` and can emit `.js` files into `src/`.
Never run `yarn lint:types` or `tsc` with file arguments. See the root `AGENTS.md` for the generated `.js` file risk.
## Purpose
@@ -31,36 +26,36 @@ The gallery is a developer and designer reference for Home Assistant frontend UI
## Structure
- `gallery/sidebar.js`: Defines gallery sections, headers, and explicit page ordering.
- `gallery/script/develop_gallery`: Wrapper for the `develop-gallery` gulp task.
- `gallery/script/build_gallery`: Wrapper for the `build-gallery` gulp task.
- `gallery/src/entrypoint.js`: Creates the `<ha-gallery>` shell.
- `gallery/src/ha-gallery.ts`: Renders the drawer, page routing, markdown descriptions, demos, edit links, and RTL toggle.
- `gallery/src/html/index.html.template`: HTML template used by the gallery build.
- `gallery/src/pages/<category>/<page>.markdown`: Optional page description and frontmatter.
- `gallery/src/pages/<category>/<page>.ts`: Optional live demo module for the same page ID.
- `gallery/src/components/`: Gallery-only demo wrappers like `demo-card`, `demo-cards`, `demo-more-info`, and `page-description`.
- `gallery/src/data/`: Fake `hass`, demo states, mock traces, and reusable sample data.
- `gallery/public/`: Static assets copied into the gallery output.
- `sidebar.js`: Defines gallery sections, headers, and explicit page ordering.
- `script/develop_gallery`: Wrapper for the `develop-gallery` gulp task.
- `script/build_gallery`: Wrapper for the `build-gallery` gulp task.
- `src/entrypoint.js`: Creates the `<ha-gallery>` shell.
- `src/ha-gallery.ts`: Renders the drawer, page routing, markdown descriptions, demos, edit links, and RTL toggle.
- `src/html/index.html.template`: HTML template used by the gallery build.
- `src/pages/<category>/<page>.markdown`: Optional page description and frontmatter.
- `src/pages/<category>/<page>.ts`: Optional live demo module for the same page id.
- `src/components/`: Gallery-only demo wrappers like `demo-card`, `demo-cards`, `demo-more-info`, and `page-description`.
- `src/data/`: Fake `hass`, demo states, mock traces, and reusable sample data.
- `public/`: Static assets copied into the gallery output.
## Page Model
Gallery pages are generated by `gather-gallery-pages` in `build-scripts/gulp/gallery.js`.
- A page ID is the path under `gallery/src/pages/` without the extension, like `components/ha-button`.
- A `.markdown` file and a `.ts` file with the same page ID become one gallery page.
- A page id is the path under `src/pages/` without the extension, like `components/ha-button`.
- A `.markdown` file and a `.ts` file with the same page id become one gallery page.
- A page may have only markdown, only a TypeScript demo, or both.
- Markdown can contain YAML frontmatter with `title` and optional `subtitle`.
- Markdown that contains only frontmatter contributes metadata without rendering a description block.
- TypeScript demo modules are dynamically imported for side effects when the page is opened.
- A demo module must define a custom element named `demo-${category}-${page}` with slashes replaced by hyphens, like `demo-components-ha-button` for `components/ha-button`.
- `gallery/src/ha-gallery.ts` renders that element with `dynamicElement()` based on the current page ID.
- `ha-gallery.ts` renders that element with `dynamicElement()` based on the current page id.
## Sidebar
Use `gallery/sidebar.js` when a page needs a visible section, section header, or deterministic ordering.
Use `sidebar.js` when a page needs a visible section, section header, or deterministic ordering.
- `category` must match the first directory name under `gallery/src/pages/`.
- `category` must match the first directory name under `src/pages/`.
- `header` is the section label shown in the drawer.
- `pages` is optional. When present, listed pages keep that exact order.
- Pages in a category that are not listed are appended alphabetically after the listed pages.
@@ -88,20 +83,20 @@ Use markdown pages for explanations, design guidance, API notes, and copy standa
- Use fenced code blocks with a language tag for copyable examples.
- Keep examples short and focused on the behavior being documented.
- Prefer real component names and attributes over prose-only descriptions.
- Use Home Assistant terminology from `ha-frontend-user-facing-text`.
- For remove/delete and add/create wording, follow `gallery/src/pages/misc/remove-delete-add-create.markdown`.
- Use Home Assistant terminology from the root `AGENTS.md`.
- For remove/delete and add/create wording, follow `src/pages/misc/remove-delete-add-create.markdown`.
Gallery markdown is documentation content and is not localized with `localize`. If demo code creates production UI strings, follow the localization and copy guidance in `ha-frontend-user-facing-text`.
Gallery markdown is documentation content and is not localized with `localize`. If demo code creates production UI strings, keep those strings aligned with the root localization and copy guidance.
## Demo Components
Use TypeScript demo pages for interactive or stateful examples.
- Import production components from `src/` using the correct relative path from the demo file.
- Import production components from `../../../src/...` or the correct relative path from the demo file.
- Import reusable gallery helpers from `gallery/src/components/` when they already model the pattern.
- Use `demo-card` and `demo-cards` for Lovelace card examples that render YAML card configs.
- Use `demo-more-info` and `demo-more-infos` for more-info dialog examples.
- Use shared mock data from `gallery/src/data/` instead of repeating large fake state objects inline.
- Use shared mock data from `src/data/` instead of repeating large fake state objects inline.
- Show meaningful states, such as loading, unavailable, empty, error, active, inactive, and disabled when relevant.
- Check responsive behavior and the gallery RTL toggle when layout or direction-sensitive UI changes.
- Keep unavoidable casts or loose demo parsing local to the demo helper or demo page.
@@ -110,7 +105,7 @@ The gallery ESLint config allows `console` for gallery diagnostics. Do not copy
## Content Standards
Follow the detailed copy standards in `ha-frontend-user-facing-text`: use American English, sentence case, active voice, inclusive language, direct user-focused wording, and consistent Home Assistant terminology.
The root copy standards still apply: use American English, sentence case, active voice, inclusive language, direct user-focused wording, and consistent Home Assistant terminology.
- Use `Home Assistant` in full, not `HA` or `HASS`.
- Use `integration` instead of `component` for product concepts.
+28 -5
View File
@@ -2,10 +2,7 @@
import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
import {
mockAreaRegistry,
type DemoArea,
} from "../../../../demo/src/stubs/area_registry";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockConfigEntries } from "../../../../demo/src/stubs/config_entries";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
@@ -13,6 +10,7 @@ import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervis
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../../src/types";
@@ -138,20 +136,45 @@ const DEVICES: DeviceRegistryEntry[] = [
},
];
const AREAS: DemoArea[] = [
const AREAS: AreaRegistryEntry[] = [
{
area_id: "backyard",
floor_id: null,
name: "Backyard",
icon: null,
picture: null,
aliases: [],
labels: [],
temperature_entity_id: null,
humidity_entity_id: null,
created_at: 0,
modified_at: 0,
},
{
area_id: "bedroom",
floor_id: null,
name: "Bedroom",
icon: "mdi:bed",
picture: null,
aliases: [],
labels: [],
temperature_entity_id: null,
humidity_entity_id: null,
created_at: 0,
modified_at: 0,
},
{
area_id: "livingroom",
floor_id: null,
name: "Livingroom",
icon: "mdi:sofa",
picture: null,
aliases: [],
labels: [],
temperature_entity_id: null,
humidity_entity_id: null,
created_at: 0,
modified_at: 0,
},
];
+38 -10
View File
@@ -1,25 +1,21 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import {
mockAreaRegistry,
type DemoArea,
} from "../../../../demo/src/stubs/area_registry";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockConfigEntries } from "../../../../demo/src/stubs/config_entries";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import {
mockFloorRegistry,
type DemoFloor,
} from "../../../../demo/src/stubs/floor_registry";
import { mockFloorRegistry } from "../../../../demo/src/stubs/floor_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
import type { HASSDomEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-selector/ha-selector";
import "../../../../src/components/ha-settings-row";
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
import type { BlueprintInput } from "../../../../src/data/blueprint";
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
import type { LabelRegistryEntry } from "../../../../src/data/label/label_registry";
import {
showDialog,
@@ -151,43 +147,75 @@ const DEVICES: DeviceRegistryEntry[] = [
},
];
const AREAS: DemoArea[] = [
const AREAS: AreaRegistryEntry[] = [
{
area_id: "backyard",
floor_id: "ground",
name: "Backyard",
icon: null,
picture: null,
aliases: [],
labels: [],
temperature_entity_id: null,
humidity_entity_id: null,
created_at: 0,
modified_at: 0,
},
{
area_id: "bedroom",
floor_id: "first",
name: "Bedroom",
icon: "mdi:bed",
picture: null,
aliases: [],
labels: [],
temperature_entity_id: null,
humidity_entity_id: null,
created_at: 0,
modified_at: 0,
},
{
area_id: "livingroom",
floor_id: "ground",
name: "Livingroom",
icon: "mdi:sofa",
picture: null,
aliases: [],
labels: [],
temperature_entity_id: null,
humidity_entity_id: null,
created_at: 0,
modified_at: 0,
},
];
const FLOORS: DemoFloor[] = [
const FLOORS: FloorRegistryEntry[] = [
{
floor_id: "ground",
name: "Ground floor",
level: 0,
icon: null,
aliases: [],
created_at: 0,
modified_at: 0,
},
{
floor_id: "first",
name: "First floor",
level: 1,
icon: "mdi:numeric-1",
aliases: [],
created_at: 0,
modified_at: 0,
},
{
floor_id: "second",
name: "Second floor",
level: 2,
icon: "mdi:numeric-2",
aliases: [],
created_at: 0,
modified_at: 0,
},
];
+14 -13
View File
@@ -77,7 +77,7 @@
"@lit/task": "1.0.3",
"@material/mwc-formfield": "patch:@material/mwc-formfield@npm%3A0.27.0#~/.yarn/patches/@material-mwc-formfield-npm-0.27.0-9528cb60f6.patch",
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
"@material/web": "2.5.0",
"@material/web": "2.4.1",
"@mdi/js": "7.4.47",
"@mdi/svg": "7.4.47",
"@replit/codemirror-indentation-markers": "6.5.3",
@@ -91,7 +91,7 @@
"@webcomponents/webcomponentsjs": "2.8.0",
"barcode-detector": "3.2.1",
"cally": "0.9.2",
"color-name": "2.1.1",
"color-name": "2.1.0",
"comlink": "4.4.2",
"core-js": "3.49.0",
"cropperjs": "1.6.2",
@@ -108,14 +108,14 @@
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.3.0",
"intl-messageformat": "11.2.12",
"js-yaml": "5.2.2",
"js-yaml": "5.2.1",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
"leaflet.markercluster": "1.5.3",
"lit": "3.3.3",
"lit-html": "3.3.3",
"luxon": "3.7.2",
"marked": "18.0.7",
"marked": "18.0.6",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.4",
"object-hash": "3.0.0",
@@ -151,8 +151,8 @@
"@octokit/plugin-retry": "8.1.0",
"@octokit/rest": "22.0.1",
"@playwright/test": "1.61.1",
"@rsdoctor/rspack-plugin": "1.6.1",
"@rspack/core": "2.1.5",
"@rsdoctor/rspack-plugin": "1.6.0",
"@rspack/core": "2.1.4",
"@rspack/dev-server": "2.1.0",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.26",
@@ -183,7 +183,7 @@
"eslint-plugin-unused-imports": "4.4.1",
"eslint-plugin-wc": "3.1.0",
"fancy-log": "2.0.0",
"fs-extra": "11.4.0",
"fs-extra": "11.3.6",
"generate-license-file": "4.2.1",
"glob": "13.0.6",
"globals": "17.7.0",
@@ -196,22 +196,22 @@
"jsdom": "29.1.1",
"jszip": "3.10.1",
"license-checker-rseidelsohn": "5.0.1",
"lint-staged": "17.2.0",
"lint-staged": "17.0.8",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.18.1",
"map-stream": "0.0.7",
"minify-literals": "2.1.0",
"pinst": "3.0.0",
"prettier": "3.9.6",
"prettier": "3.9.5",
"rspack-manifest-plugin": "5.2.2",
"serve": "14.2.6",
"sinon": "22.1.0",
"tar": "7.5.21",
"sinon": "22.0.0",
"tar": "7.5.20",
"terser-webpack-plugin": "5.6.1",
"ts-lit-plugin": "2.0.2",
"typescript": "6.0.3",
"typescript-eslint": "8.65.0",
"typescript-eslint": "8.64.0",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.1.10",
"webpack-stats-plugin": "1.1.3",
@@ -226,7 +226,8 @@
"@fullcalendar/daygrid": "6.1.21",
"globals": "17.7.0",
"tslib": "2.8.1",
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
"glob@^10.2.2": "^10.5.0"
},
"packageManager": "yarn@4.17.1",
"volta": {
-16
View File
@@ -58,17 +58,6 @@
"depNameTemplate": "rhysd/actionlint",
"datasourceTemplate": "github-releases",
"extractVersionTemplate": "^v(?<version>.+)$"
},
{
"description": "Keep Playwright CI container image up to date",
"customType": "regex",
"managerFilePatterns": ["/^\\.github/workflows/e2e\\.yaml$/"],
"matchStrings": [
"mcr\\.microsoft\\.com/playwright:(?<currentValue>v\\d+\\.\\d+\\.\\d+-noble)"
],
"depNameTemplate": "mcr.microsoft.com/playwright",
"datasourceTemplate": "docker",
"versioningTemplate": "regex:^v(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)-(?<compatibility>noble)$"
}
],
"packageRules": [
@@ -102,11 +91,6 @@
"description": "Group formatjs monorepo package",
"groupName": "formatjs",
"matchPackageNames": ["@formatjs/**"]
},
{
"description": "Group Playwright package and CI container updates",
"groupName": "Playwright",
"matchPackageNames": ["@playwright/test", "mcr.microsoft.com/playwright"]
}
]
}
-113
View File
@@ -1,113 +0,0 @@
# Build size comparison: old vs new `.browserslistrc`
Both builds were produced on the `pass-browser-list` branch (which passes the browserslist
config through to rspack/SWC and Lightning CSS), so the **only difference between the two
builds is the `.browserslistrc` content**:
- **Old** = `.browserslistrc` as committed at HEAD (`aa02a1117`)
- **New** = the staged `.browserslistrc` on this branch
Both full production builds (`yarn build``gulp build-app`) took ~18 min.
## Resolved browser targets
| Env | Old | New |
| --------- | ------------ | ------------ |
| modern | 75 browsers | 124 browsers |
| legacy | 424 browsers | 181 browsers |
| legacy-sw | 422 browsers | 181 browsers |
Key minimum versions per env:
| Browser | modern old | modern new | legacy old | legacy new |
| ---------------- | ---------- | ---------- | ---------- | ---------- |
| Chrome | 109 | 128 | 39 | 79 |
| Edge | 139 | 128 | 79 | 92 |
| Firefox | 140 (ESR) | 129 | 69 | 78 |
| Safari | 26.0 | 17.6 | 13 | 13.1 |
| iOS Safari | 18.5 | 17.6 | 11.0 | 13.2 |
| Samsung Internet | 29 | 27 | 10.1 | 24 |
The new modern list (`last 2 years`) _lowers_ the Safari/Firefox floor but _raises_ the
Chrome floor (the old `>= 0.5%` rule kept Chrome 109 alive). The new legacy list
(`last 7 years and >= 0.001%`, intersection instead of union) raises the floor across the
board — that is where the size win comes from.
## Total JS output size
All `.js` files in the output directory; gzip is `gzip -9` recomputed per file, brotli is
the `.js.br` files the build itself emits (what is actually served).
### `frontend_latest` (modern build)
| Metric | Old | New | Δ |
| ------------- | --------- | --------- | ------------------ |
| Raw JS | 31.97 MiB | 31.89 MiB | 86.7 KiB (0.3%) |
| Gzip | 9.50 MiB | 9.46 MiB | 39.5 KiB (0.4%) |
| Brotli | 8.16 MiB | 8.13 MiB | 33.6 KiB (0.4%) |
| Sourcemaps | 28.18 MiB | 28.06 MiB | 126.8 KiB (0.4%) |
| JS file count | 1099 | 1096 | 3 |
### `frontend_es5` (legacy build)
| Metric | Old | New | Δ |
| ------------- | --------- | --------- | ---------------------- |
| Raw JS | 35.31 MiB | 31.74 MiB | **3.57 MiB (10.1%)** |
| Gzip | 9.62 MiB | 9.38 MiB | 249.0 KiB (2.5%) |
| Brotli | 8.13 MiB | 8.03 MiB | 100.0 KiB (1.2%) |
| Sourcemaps | 32.05 MiB | 28.16 MiB | 3.89 MiB (12.1%) |
| JS file count | 1151 | 1138 | 13 |
Total `hass_frontend/` on disk: 260.9 MiB → 252.7 MiB (8.2 MiB).
## Cold-load critical entrypoints (raw initial JS)
These are the budget-tracked entrypoints from `build-scripts/check-bundle-size.cjs`
(downloaded before anything interactive happens).
### Modern
| Entrypoint | Old | New | Δ |
| ------------ | --------- | --------- | ---------------- |
| app | 542.0 KiB | 537.3 KiB | 4.8 KiB (0.9%) |
| core | 52.4 KiB | 50.2 KiB | 2.2 KiB (4.2%) |
| authorize | 520.7 KiB | 514.7 KiB | 6.0 KiB (1.2%) |
| onboarding | 620.0 KiB | 613.0 KiB | 7.0 KiB (1.1%) |
| custom-panel | 63.3 KiB | 61.1 KiB | 2.2 KiB (3.5%) |
### Legacy
| Entrypoint | Old | New | Δ |
| ------------ | --------- | --------- | ----------------------- |
| app | 822.5 KiB | 632.6 KiB | **189.8 KiB (23.1%)** |
| core | 229.0 KiB | 158.2 KiB | **70.8 KiB (30.9%)** |
| authorize | 795.9 KiB | 617.1 KiB | 178.9 KiB (22.5%) |
| onboarding | 939.3 KiB | 722.7 KiB | 216.6 KiB (23.1%) |
| custom-panel | 174.1 KiB | 106.9 KiB | **67.2 KiB (38.6%)** |
## Takeaways
- **The modern build barely changes** (≈−0.4% across the board). Trading the old
usage-based floor for a clean `last 2 years` rule is roughly size-neutral: the lower
Safari floor costs a little, the higher Chrome floor saves a little more.
- **The legacy build shrinks substantially**: 10% raw JS and 23% to 39% on the
cold-load entrypoints, because raising the floor (Chrome 39→79, Samsung 10→24,
iOS 11→13.2) drops a large amount of transpilation output and polyfills.
- Over the wire the legacy win is smaller (2.5% gzip / 1.2% brotli) since transpilation
boilerplate compresses very well — but parse/execute cost on the (typically slow)
devices that receive the legacy build scales with the raw size, so the 10% still
matters where it hurts most.
- The legacy entrypoints land well under the current `build-scripts/bundle-budget.json`
budgets; those could be re-seeded (`node build-scripts/check-bundle-size.cjs --update
--headroom=3`) once this browserslist change is final.
## Method
1. `yarn build` with the staged (new) `.browserslistrc`; recorded per-file raw and
`gzip -9` sizes of `hass_frontend/frontend_latest` + `frontend_es5` and the rspack
stats (`build/stats/frontend-{modern,legacy}.json`).
2. Restored the HEAD (old) `.browserslistrc`, rebuilt, recorded the same.
3. Restored the staged file. Entrypoint sizes are the initial entry-chunk JS as computed
by the same logic as `check-bundle-size.cjs`. Translations/static assets are identical
between builds and excluded from the JS numbers. The root service-worker files
(`sw-modern.js`, `sw-legacy.js`) were not compared separately.
+10 -11
View File
@@ -9,17 +9,16 @@ export const castApiAvailable = () => {
loadedPromise = new Promise((resolve) => {
(window as any).__onGCastApiAvailable = resolve;
// Any element with a specific ID will get set as a JS variable on window
// This will override the cast SDK if the iconset is loaded afterwards.
// Conflicting IDs will no longer mess with window, so we'll just append one.
const el = document.createElement("div");
el.id = "cast";
document.body.append(el);
loadJS(
"https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"
).catch(() => resolve(false));
});
// Any element with a specific ID will get set as a JS variable on window
// This will override the cast SDK if the iconset is loaded afterwards.
// Conflicting IDs will no longer mess with window, so we'll just append one.
const el = document.createElement("div");
el.id = "cast";
document.body.append(el);
loadJS(
"https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"
);
return loadedPromise;
};
+1 -8
View File
@@ -91,14 +91,7 @@ export const STATES_OFF = ["closed", "locked", "off"];
export const BINARY_STATE_ON = "on";
export const BINARY_STATE_OFF = "off";
/** Domains where we allow toggle in Lovelace.
* This is not strictly a list of what is possible to toggle, but the list of
* domains where toggle is considered the primary default behavior.
* Entities card uses this to determine which entities are controlled by the
* header toggle.
* Some cards use this to decide the default tap action.
* Use canToggleDomain/canToggleState for determining if toggling is possible.
*/
/** Domains where we allow toggle in Lovelace. */
export const DOMAINS_TOGGLE = new Set([
"fan",
"input_boolean",
+8 -4
View File
@@ -1,12 +1,16 @@
import type { HomeAssistant } from "../../types";
import { getToggleAction } from "./get_toggle_action";
export const canToggleDomain = (hass: HomeAssistant, domain: string) => {
const services = hass.services[domain];
if (!services) {
return false;
}
const actionOn = getToggleAction(domain, true);
const actionOff = getToggleAction(domain, false);
return actionOn in services && actionOff in services;
if (domain === "lock") {
return "lock" in services;
}
if (domain === "cover") {
return "open_cover" in services;
}
return "turn_on" in services;
};
+2 -8
View File
@@ -3,7 +3,6 @@ import type { HomeAssistant } from "../../types";
import { canToggleDomain } from "./can_toggle_domain";
import { computeStateDomain } from "./compute_state_domain";
import { supportsFeature } from "./supports-feature";
import { SPECIAL_TOGGLE_ACTIONS } from "./get_toggle_action";
export const canToggleState = (hass: HomeAssistant, stateObj: HassEntity) => {
const domain = computeStateDomain(stateObj);
@@ -26,13 +25,8 @@ export const canToggleState = (hass: HomeAssistant, stateObj: HassEntity) => {
return false;
}
if (
domain in SPECIAL_TOGGLE_ACTIONS &&
SPECIAL_TOGGLE_ACTIONS[domain].feature
) {
return SPECIAL_TOGGLE_ACTIONS[domain].feature.every((f) =>
supportsFeature(stateObj, f)
);
if (domain === "climate") {
return supportsFeature(stateObj, 4096);
}
return canToggleDomain(hass, domain);
+5 -5
View File
@@ -2,19 +2,19 @@ import { AITaskEntityFeature } from "../../data/ai_task";
import { AlarmControlPanelEntityFeature } from "../../data/alarm_control_panel";
import { AssistSatelliteEntityFeature } from "../../data/assist_satellite";
import { CalendarEntityFeature } from "../../data/calendar";
import { CameraEntityFeature } from "../../data/feature/camera_entity_feature";
import { ClimateEntityFeature } from "../../data/feature/climate_entity_feature";
import { CameraEntityFeature } from "../../data/camera";
import { ClimateEntityFeature } from "../../data/climate";
import { ConversationEntityFeature } from "../../data/conversation";
import { CoverEntityFeature } from "../../data/feature/cover_entity_feature";
import { CoverEntityFeature } from "../../data/cover";
import { FanEntityFeature } from "../../data/fan";
import { HumidifierEntityFeature } from "../../data/humidifier";
import { LawnMowerEntityFeature } from "../../data/lawn_mower";
import { LightEntityFeature } from "../../data/light";
import { LockEntityFeature } from "../../data/lock";
import { MediaPlayerEntityFeature } from "../../data/feature/media-player_entity_feature";
import { MediaPlayerEntityFeature } from "../../data/media-player";
import { NotifyEntityFeature } from "../../data/notify";
import { RemoteEntityFeature } from "../../data/remote";
import { SirenEntityFeature } from "../../data/feature/siren_entity_feature";
import { SirenEntityFeature } from "../../data/siren";
import { TodoListEntityFeature } from "../../data/todo";
import { UpdateEntityFeature } from "../../data/update";
import { VacuumEntityFeature } from "../../data/vacuum";
+2 -7
View File
@@ -89,7 +89,7 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
device_class: [
"battery",
"battery_charging",
"carbon_monoxide",
"co",
"cold",
"connectivity",
"door",
@@ -227,12 +227,7 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
"voltage",
"volume_flow_rate",
],
state_class: [
"measurement",
"measurement_angle",
"total",
"total_increasing",
],
state_class: ["measurement", "total", "total_increasing"],
},
switch: {
device_class: ["outlet", "switch"],
-72
View File
@@ -1,72 +0,0 @@
import { CameraEntityFeature } from "../../data/feature/camera_entity_feature";
import { ClimateEntityFeature } from "../../data/feature/climate_entity_feature";
import { CoverEntityFeature } from "../../data/feature/cover_entity_feature";
import { MediaPlayerEntityFeature } from "../../data/feature/media-player_entity_feature";
import { SirenEntityFeature } from "../../data/feature/siren_entity_feature";
// These are domains which have nonstandard 'toggle' behavior.
// Otherwise, any domain with a turn_on and turn_off service may be toggled.
// If features are provided, all features must be supported.
interface SpecialToggleAction {
on: string;
off?: string;
feature?: number[];
}
export const SPECIAL_TOGGLE_ACTIONS: Record<string, SpecialToggleAction> = {
button: {
on: "press",
},
camera: {
on: "turn_on",
off: "turn_off",
feature: [CameraEntityFeature.ON_OFF],
},
climate: {
on: "turn_on",
off: "turn_off",
feature: [ClimateEntityFeature.TURN_ON, ClimateEntityFeature.TURN_OFF],
},
cover: {
on: "open_cover",
off: "close_cover",
feature: [CoverEntityFeature.OPEN, CoverEntityFeature.CLOSE],
},
input_button: {
on: "press",
},
lock: {
on: "unlock",
off: "lock",
},
media_player: {
on: "turn_on",
off: "turn_off",
feature: [
MediaPlayerEntityFeature.TURN_ON,
MediaPlayerEntityFeature.TURN_OFF,
],
},
scene: {
on: "turn_on",
},
siren: {
on: "turn_on",
off: "turn_off",
feature: [SirenEntityFeature.TURN_ON, SirenEntityFeature.TURN_OFF],
},
valve: {
on: "open_valve",
off: "close_valve",
},
};
// This function assumes that the passed domain can toggle, it may otherwise
// return a service that does not exist.
export const getToggleAction = (domain: string, onOff: boolean): string => {
return (
SPECIAL_TOGGLE_ACTIONS[domain]?.[onOff ? "on" : "off"] ||
SPECIAL_TOGGLE_ACTIONS[domain]?.["on"] ||
(onOff ? "turn_on" : "turn_off")
);
};
+4 -2
View File
@@ -2,7 +2,6 @@ import type { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
import type { HomeAssistant } from "../../types";
import { computeStateDomain } from "./compute_state_domain";
import { getToggleAction } from "./get_toggle_action";
export const computeGroupEntitiesState = (states: HassEntity[]): string => {
if (!states.length) {
@@ -58,11 +57,14 @@ export const toggleGroupEntities = (
const isOn = state === "on" || state === "open";
let service = getToggleAction(domain, !isOn);
let service = isOn ? "turn_off" : "turn_on";
if (domain === "cover") {
if (state === "opening" || state === "closing") {
// If the cover is opening or closing, we toggle it to stop it
service = "stop_cover";
} else {
// For covers, we use the open/close service
service = isOn ? "close_cover" : "open_cover";
}
}
+18 -11
View File
@@ -64,15 +64,22 @@ export const navigate = async (path: string, options?: NavigateOptions) => {
const replace = options?.replace || false;
if (__DEMO__) {
if (!path.includes("#")) {
// The demo routes with the hash instead of the pathname. Resolve the
// path like the browser would do for pushState, and keep the query
// parameters in the URL query instead of inside the hash.
const url = new URL(
path,
`${mainWindow.location.origin}${mainWindow.location.hash.substring(1)}`
);
path = `${mainWindow.location.pathname}${url.search}#${url.pathname}`;
if (path.includes("#")) {
if (replace) {
mainWindow.history.replaceState(
mainWindow.history.state?.root
? { root: true }
: (options?.data ?? null),
"",
path
);
} else {
mainWindow.history.pushState(options?.data ?? null, "", path);
}
fireEvent(mainWindow, "location-changed", {
replace,
});
return true;
}
if (replace) {
mainWindow.history.replaceState(
@@ -80,10 +87,10 @@ export const navigate = async (path: string, options?: NavigateOptions) => {
? { root: true }
: (options?.data ?? null),
"",
path
`${mainWindow.location.pathname}#${path}`
);
} else {
mainWindow.history.pushState(options?.data ?? null, "", path);
mainWindow.location.hash = path;
}
} else if (replace) {
mainWindow.history.replaceState(
-4
View File
@@ -36,10 +36,6 @@ export function downSampleLineData<
const min = minX ?? getPointData(data[0]!)[0];
const max = maxX ?? getPointData(data[data.length - 1]!)[0];
const step = Math.ceil((max - min) / Math.floor(maxDetails));
if (!Number.isFinite(step) || step <= 0) {
// a degenerate frame size would put every point in a single frame
return data;
}
if (useMean) {
// Group points into frames, accumulating sums in insertion order.
+1 -4
View File
@@ -51,7 +51,6 @@ export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
const LEGEND_OVERFLOW_LIMIT = 10;
const LEGEND_OVERFLOW_LIMIT_MOBILE = 6;
const DOUBLE_TAP_TIME = 300;
const DEFAULT_CHART_WIDTH = 500;
type RawSeriesOption = Exclude<
NonNullable<ECOption["series"]>,
@@ -1049,9 +1048,7 @@ export class HaChartBase extends LitElement {
sampling: undefined,
data: downSampleLineData(
data as LineSeriesOption["data"],
// 0 while inside a hidden container, e.g. a section with a visibility condition
(this.clientWidth || DEFAULT_CHART_WIDTH) *
window.devicePixelRatio,
this.clientWidth * window.devicePixelRatio,
minX,
maxX
),
+12 -23
View File
@@ -1,27 +1,12 @@
// A range smaller than this fraction of the axis magnitude is floating-point
// noise (e.g. from summed statistics), not real precision.
const NEGLIGIBLE_RANGE_RATIO = 1e-10;
// Derive the number of decimal digits to use for Y-axis labels from the
// observed data range. We mirror how ECharts sizes its ticks: it splits the
// range into ~5 intervals (its default `splitNumber`) and rounds that raw
// interval to a "nice" 1/2/3/5×10ⁿ value, then reports the decimals that nice
// interval needs. This matches the precision ECharts actually renders, so
// labels are neither truncated to identical values nor padded with extra zeros.
export function computeYAxisFractionDigits(
min: number,
max: number,
// Bar axes render from 0, so union the extent with 0 to match.
includeZero = false
): number {
const lo = includeZero ? Math.min(min, 0) : min;
const hi = includeZero ? Math.max(max, 0) : max;
const range = hi - lo;
export function computeYAxisFractionDigits(min: number, max: number): number {
const range = max - min;
if (!Number.isFinite(range) || range <= 0) return 1;
// A near-zero range is fp noise; deriving digits from it would pad the labels
// with a tail of zeros (e.g. "0.20000000000000"), so treat it as flat.
const magnitude = Math.max(Math.abs(lo), Math.abs(hi));
if (range <= magnitude * NEGLIGIBLE_RANGE_RATIO) return 1;
const rawInterval = range / 5;
const exponent = Math.floor(Math.log10(rawInterval));
const mantissa = rawInterval / 10 ** exponent; // in [1, 10)
@@ -53,7 +38,9 @@ const resolveYAxisBound = (
export function createYAxisPrecisionBounds(options: {
min?: YAxisBound;
max?: YAxisBound;
// Set for bar axes anchored at 0, so precision reflects the 0-based range.
// Axes without `scale: true` (e.g. bar charts) stay anchored at 0, so the
// rendered ticks span from 0 even when the data does not. Union the extent
// with 0 to match the labels ECharts actually draws.
includeZero?: boolean;
onFractionDigits: (digits: number) => void;
}): {
@@ -65,11 +52,13 @@ export function createYAxisPrecisionBounds(options: {
min: (values) => {
const resolvedMin = resolveYAxisBound(min, values);
const resolvedMax = resolveYAxisBound(max, values);
const extentMin = resolvedMin ?? values.min;
const extentMax = resolvedMax ?? values.max;
onFractionDigits(
computeYAxisFractionDigits(extentMin, extentMax, includeZero)
);
let extentMin = resolvedMin ?? values.min;
let extentMax = resolvedMax ?? values.max;
if (includeZero) {
extentMin = Math.min(extentMin, 0);
extentMax = Math.max(extentMax, 0);
}
onFractionDigits(computeYAxisFractionDigits(extentMin, extentMax));
return resolvedMin;
},
max: (values) => resolveYAxisBound(max, values),
+3 -3
View File
@@ -1,8 +1,8 @@
import { AssistChip } from "@material/web/chips/internal/assist-chip";
import { styles } from "@material/web/chips/internal/assist-styles.cssresult.js";
import { styles } from "@material/web/chips/internal/assist-styles";
import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles.cssresult.js";
import { styles as elevatedStyles } from "@material/web/chips/internal/elevated-styles.cssresult.js";
import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles";
import { styles as elevatedStyles } from "@material/web/chips/internal/elevated-styles";
import { css, html } from "lit";
import { customElement, property } from "lit/decorators";
+5 -5
View File
@@ -1,9 +1,9 @@
import { styles as elevatedStyles } from "@material/web/chips/internal/elevated-styles.cssresult.js";
import { styles as elevatedStyles } from "@material/web/chips/internal/elevated-styles";
import { FilterChip } from "@material/web/chips/internal/filter-chip";
import { styles } from "@material/web/chips/internal/filter-styles.cssresult.js";
import { styles as selectableStyles } from "@material/web/chips/internal/selectable-styles.cssresult.js";
import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles.cssresult.js";
import { styles as trailingIconStyles } from "@material/web/chips/internal/trailing-icon-styles.cssresult.js";
import { styles } from "@material/web/chips/internal/filter-styles";
import { styles as selectableStyles } from "@material/web/chips/internal/selectable-styles";
import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles";
import { styles as trailingIconStyles } from "@material/web/chips/internal/trailing-icon-styles";
import { css, html } from "lit";
import { customElement, property } from "lit/decorators";
+4 -4
View File
@@ -1,8 +1,8 @@
import { InputChip } from "@material/web/chips/internal/input-chip";
import { styles } from "@material/web/chips/internal/input-styles.cssresult.js";
import { styles as selectableStyles } from "@material/web/chips/internal/selectable-styles.cssresult.js";
import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles.cssresult.js";
import { styles as trailingIconStyles } from "@material/web/chips/internal/trailing-icon-styles.cssresult.js";
import { styles } from "@material/web/chips/internal/input-styles";
import { styles as selectableStyles } from "@material/web/chips/internal/selectable-styles";
import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles";
import { styles as trailingIconStyles } from "@material/web/chips/internal/trailing-icon-styles";
import { css } from "lit";
import { customElement } from "lit/decorators";
@@ -33,6 +33,7 @@ const HIDDEN_ATTRIBUTES = [
"battery_level",
"code_arm_required",
"code_format",
"color_modes",
"device_class",
"editable",
"effect_list",
+18 -4
View File
@@ -13,7 +13,6 @@ import { forwardHaptic } from "../../data/haptics";
import "../ha-formfield";
import "../ha-icon-button";
import "../ha-switch";
import { getToggleAction } from "../../common/entity/get_toggle_action";
const isOn = (stateObj?: HassEntity) =>
stateObj !== undefined &&
@@ -125,10 +124,25 @@ export class HaEntityToggle extends LitElement {
}
forwardHaptic(this, "light");
const stateDomain = computeStateDomain(this.stateObj);
let serviceDomain;
let service;
const serviceDomain =
stateDomain === "group" ? "homeassistant" : stateDomain;
const service = getToggleAction(stateDomain, turnOn);
if (stateDomain === "lock") {
serviceDomain = "lock";
service = turnOn ? "unlock" : "lock";
} else if (stateDomain === "cover") {
serviceDomain = "cover";
service = turnOn ? "open_cover" : "close_cover";
} else if (stateDomain === "valve") {
serviceDomain = "valve";
service = turnOn ? "open_valve" : "close_valve";
} else if (stateDomain === "group") {
serviceDomain = "homeassistant";
service = turnOn ? "turn_on" : "turn_off";
} else {
serviceDomain = stateDomain;
service = turnOn ? "turn_on" : "turn_off";
}
const currentState = this.stateObj;
+11 -24
View File
@@ -6,10 +6,8 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { computeCssColor } from "../../common/color/compute-color";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { stateActive } from "../../common/entity/state_active";
import {
stateColorBrightness,
stateColorCss,
@@ -29,15 +27,10 @@ export class StateBadge extends LitElement {
@property({ attribute: false }) public overrideImage?: string;
/**
* Cannot be a boolean attribute because undefined is treated different than
* false. When it is undefined, state is still colored for light entities.
* @deprecated use `color` instead
*/
// Cannot be a boolean attribute because undefined is treated different than
// false. When it is undefined, state is still colored for light entities.
@property({ attribute: false }) public stateColor?: boolean;
// "state", "none" or a color (theme color name or CSS color). Custom colors
// apply when the entity is active. Takes precedence over stateColor.
@property() public color?: string;
// @todo Consider reworking to eliminate need for attribute since it is manipulated internally
@@ -74,17 +67,11 @@ export class StateBadge extends LitElement {
}
}
private get _color(): string | undefined {
if (this.color) {
return this.color;
}
if (this.stateColor !== undefined) {
return this.stateColor ? "state" : "none";
}
private get _stateColor() {
const domain = this.stateObj
? computeStateDomain(this.stateObj)
: undefined;
return domain === "light" ? "state" : undefined;
return this.stateColor ?? domain === "light";
}
protected render() {
@@ -144,7 +131,6 @@ export class StateBadge extends LitElement {
if (stateObj) {
const domain = computeDomain(stateObj.entity_id);
const color = this._color;
if (this.overrideImage === undefined) {
// hide icon if we have entity picture
if (
@@ -161,10 +147,13 @@ export class StateBadge extends LitElement {
}
backgroundImage = `url(${imageUrl})`;
this.icon = false;
} else if (color === "state") {
const stateColor = stateColorCss(stateObj);
if (stateColor) {
iconStyle.color = stateColor;
} else if (this.color) {
// Externally provided overriding color wins over state color
iconStyle.color = this.color;
} else if (this._stateColor) {
const color = stateColorCss(stateObj);
if (color) {
iconStyle.color = color;
}
if (stateObj.attributes.rgb_color) {
iconStyle.color = `rgb(${stateObj.attributes.rgb_color.join(",")})`;
@@ -191,8 +180,6 @@ export class StateBadge extends LitElement {
delete iconStyle.color;
}
}
} else if (color && color !== "none" && stateActive(stateObj)) {
iconStyle.color = computeCssColor(color);
}
} else if (this.overrideImage) {
backgroundImage = `url(${this._resolveImageUrl(this.overrideImage)})`;
+2 -4
View File
@@ -1,7 +1,6 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import type { HomeAssistant } from "../../types";
import "../ha-relative-time";
import "../ha-tooltip";
@@ -24,11 +23,10 @@ class StateInfo extends LitElement {
const name = this.hass.formatEntityName(this.stateObj, { type: "entity" });
// Inline style because the state-badge color API only colors active entities
return html`<state-badge
.stateObj=${this.stateObj}
.stateColor=${!this.color}
style=${styleMap({ color: this.color })}
.stateColor=${true}
.color=${this.color}
></state-badge>
<div class="info">
<div class="name ${this.inDialog ? "in-dialog" : ""}" .title=${name}>
+1 -37
View File
@@ -57,16 +57,6 @@ interface AssistMessage {
error?: boolean;
}
export const initialPromptToSubmit = (
prompt: string | undefined,
submit: boolean
): string | undefined => (submit ? prompt?.trim() || undefined : undefined);
export const assistPipelineChanged = (
previous: AssistPipeline | undefined,
current: AssistPipeline | undefined
): boolean => previous?.id !== current?.id;
@customElement("ha-assist-chat")
export class HaAssistChat extends LitElement {
@property({ attribute: false }) public pipeline?: AssistPipeline;
@@ -77,12 +67,6 @@ export class HaAssistChat extends LitElement {
@property({ attribute: false })
public startListening?: boolean;
@property({ attribute: false })
public initialPrompt?: string;
@property({ attribute: false })
public submitInitialPrompt = false;
@query("#message-input") private _messageInput!: HaInput;
@query(".message:last-child")
@@ -115,8 +99,6 @@ export class HaAssistChat extends LitElement {
private _conversationId: string | null = null;
private _initialPromptSubmitted = false;
private _audioRecorder?: AudioRecorder;
private _audioBuffer?: Int16Array[];
@@ -126,11 +108,7 @@ export class HaAssistChat extends LitElement {
private _stt_binary_handler_id?: number | null;
protected willUpdate(changedProperties: PropertyValues<this>): void {
if (
!this.hasUpdated ||
(changedProperties.has("pipeline") &&
assistPipelineChanged(changedProperties.get("pipeline"), this.pipeline))
) {
if (!this.hasUpdated || changedProperties.has("pipeline")) {
this._conversation = [
{
who: "hass",
@@ -160,20 +138,6 @@ export class HaAssistChat extends LitElement {
if (changedProps.has("_conversation")) {
this._scrollMessagesBottom();
}
if (
!this._initialPromptSubmitted &&
(changedProps.has("initialPrompt") ||
changedProps.has("submitInitialPrompt"))
) {
const prompt = initialPromptToSubmit(
this.initialPrompt,
this.submitInitialPrompt
);
if (prompt) {
this._initialPromptSubmitted = true;
this._processText(prompt);
}
}
}
public disconnectedCallback() {
+7 -30
View File
@@ -57,40 +57,17 @@ export const evaluateCondition = (
return matchFieldCondition(condition, data);
};
export const isFieldVisible = (
export const isFieldHidden = (
schema: HaFormSchema,
data: HaFormDataContainer | undefined
): boolean => {
const { visible } = schema as HaFormBaseSchema;
if (visible === undefined || visible === true) {
return true;
}
if (visible === false) {
const { hidden } = schema as HaFormBaseSchema;
if (!hidden) {
return false;
}
const conditions = Array.isArray(visible) ? visible : [visible];
if (hidden === true) {
return true;
}
const conditions = Array.isArray(hidden) ? hidden : [hidden];
return conditions.every((condition) => evaluateCondition(condition, data));
};
// Hiding a field drops its value, which can flip another field's condition, so
// resolve the set to a fixpoint.
export const getHiddenFields = (
schema: readonly HaFormSchema[],
data: HaFormDataContainer | undefined
): Set<string> => {
const hidden = new Set<string>();
const evalData: HaFormDataContainer = { ...(data ?? {}) };
let changed = true;
while (changed) {
changed = false;
for (const field of schema) {
if (hidden.has(field.name) || isFieldVisible(field, evalData)) {
continue;
}
hidden.add(field.name);
delete evalData[field.name];
changed = true;
}
}
return hidden;
};
+2 -4
View File
@@ -2,7 +2,7 @@ import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, queryAll } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import { getHiddenFields } from "./conditions";
import { isFieldHidden } from "./conditions";
import "./ha-form";
import type { HaForm } from "./ha-form";
import type {
@@ -68,11 +68,9 @@ export class HaFormGrid extends LitElement implements HaFormElement {
}
protected render(): TemplateResult {
const hiddenFields = getHiddenFields(this.schema.schema, this.data);
return html`
${this.schema.schema
.filter((item) => !hiddenFields.has(item.name))
.filter((item) => !isFieldHidden(item, this.data))
.map(
(item) => html`
<ha-form
+3 -6
View File
@@ -6,7 +6,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import type { HomeAssistant } from "../../types";
import "../ha-alert";
import "../ha-selector/ha-selector";
import { getHiddenFields } from "./conditions";
import { isFieldHidden } from "./conditions";
import type { HaFormDataContainer, HaFormElement, HaFormSchema } from "./types";
const LOAD_ELEMENTS = {
@@ -99,9 +99,8 @@ export class HaForm extends LitElement implements HaFormElement {
let isValid = true;
let firstInvalidElement: HTMLElement | undefined;
const hiddenFields = getHiddenFields(this.schema, this.data);
const visibleSchema = this.schema.filter(
(item) => !hiddenFields.has(item.name)
(item) => !isFieldHidden(item, this.data)
);
visibleSchema.forEach((item, index) => {
@@ -158,8 +157,6 @@ export class HaForm extends LitElement implements HaFormElement {
}
protected render(): TemplateResult {
const renderHiddenFields = getHiddenFields(this.schema, this.data);
return html`
<div class="root" part="root">
${
@@ -172,7 +169,7 @@ export class HaForm extends LitElement implements HaFormElement {
: ""
}
${this.schema.map((item) => {
if (renderHiddenFields.has(item.name)) {
if (isFieldHidden(item, this.data)) {
return nothing;
}
+3 -3
View File
@@ -22,9 +22,9 @@ export interface HaFormBaseSchema {
default?: HaFormData;
required?: boolean;
disabled?: boolean;
// Field is visible while the condition holds (visible by default).
// Serializable so it can be shared with the backend and other renderers.
visible?: boolean | HaFormCondition | HaFormCondition[];
// Field is hidden while the condition holds. Serializable so it can be
// shared with the backend and other renderers.
hidden?: boolean | HaFormCondition | HaFormCondition[];
description?: {
suffix?: string;
// This value will be set initially when form is loaded
+1 -1
View File
@@ -1,5 +1,5 @@
import { ListItemEl } from "@material/web/list/internal/listitem/list-item";
import { styles } from "@material/web/list/internal/listitem/list-item-styles.cssresult.js";
import { styles } from "@material/web/list/internal/listitem/list-item-styles";
import { css, html, nothing, type TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import "./ha-ripple";
+1 -1
View File
@@ -1,5 +1,5 @@
import { List } from "@material/web/list/internal/list";
import { styles } from "@material/web/list/internal/list-styles.cssresult.js";
import { styles } from "@material/web/list/internal/list-styles";
import { css } from "lit";
import { customElement } from "lit/decorators";
+2 -2
View File
@@ -1,6 +1,6 @@
import { OutlinedButton } from "@material/web/button/internal/outlined-button";
import { styles as sharedStyles } from "@material/web/button/internal/shared-styles.cssresult.js";
import { styles } from "@material/web/button/internal/outlined-styles.cssresult.js";
import { styles as sharedStyles } from "@material/web/button/internal/shared-styles";
import { styles } from "@material/web/button/internal/outlined-styles";
import { css } from "lit";
import { customElement } from "lit/decorators";
+2 -2
View File
@@ -1,6 +1,6 @@
import { OutlinedField } from "@material/web/field/internal/outlined-field";
import { styles } from "@material/web/field/internal/outlined-styles.cssresult.js";
import { styles as sharedStyles } from "@material/web/field/internal/shared-styles.cssresult.js";
import { styles } from "@material/web/field/internal/outlined-styles";
import { styles as sharedStyles } from "@material/web/field/internal/shared-styles";
import { css } from "lit";
import { customElement } from "lit/decorators";
import { literal } from "lit/static-html";
+2 -2
View File
@@ -1,6 +1,6 @@
import { IconButton } from "@material/web/iconbutton/internal/icon-button";
import { styles } from "@material/web/iconbutton/internal/outlined-styles.cssresult.js";
import { styles as sharedStyles } from "@material/web/iconbutton/internal/shared-styles.cssresult.js";
import { styles } from "@material/web/iconbutton/internal/outlined-styles";
import { styles as sharedStyles } from "@material/web/iconbutton/internal/shared-styles";
import { css } from "lit";
import { customElement } from "lit/decorators";
+18 -35
View File
@@ -4,23 +4,6 @@ import { customElement, property, query, state } from "lit/decorators";
import QRCode from "qrcode";
import "./ha-alert";
import { rgb2hex } from "../common/color/convert-color";
import {
getContrastedColorHex,
getRGBContrastRatio,
} from "../common/color/rgb";
// Minimum contrast ratio (WCAG non-text minimum) below which we substitute a
// legible foreground so the QR code never renders unscannable.
const MIN_CONTRAST_RATIO = 3;
const parseRgbVariable = (
value: string
): [number, number, number] | undefined => {
const parts = value.split(",").map((part) => parseInt(part, 10));
return parts.length === 3 && parts.every((part) => !Number.isNaN(part))
? (parts as [number, number, number])
: undefined;
};
@customElement("ha-qr-code")
export class HaQrCode extends LitElement {
@@ -77,26 +60,26 @@ export class HaQrCode extends LitElement {
changedProperties.has("centerImage"))
) {
const computedStyles = getComputedStyle(this);
const textRgb = parseRgbVariable(
computedStyles.getPropertyValue("--rgb-primary-text-color")
const textRgb = computedStyles.getPropertyValue(
"--rgb-primary-text-color"
);
const backgroundRgb = parseRgbVariable(
computedStyles.getPropertyValue("--rgb-card-background-color")
const backgroundRgb = computedStyles.getPropertyValue(
"--rgb-card-background-color"
);
const textHex = rgb2hex(
textRgb.split(",").map((a) => parseInt(a, 10)) as [
number,
number,
number,
]
);
const backgroundHex = rgb2hex(
backgroundRgb.split(",").map((a) => parseInt(a, 10)) as [
number,
number,
number,
]
);
const backgroundHex = backgroundRgb ? rgb2hex(backgroundRgb) : "#ffffff";
let textHex = textRgb ? rgb2hex(textRgb) : "#000000";
// If the theme colors are missing/invalid or don't contrast enough, the
// QR code would be unscannable (e.g. white-on-white). Fall back to a
// foreground guaranteed to contrast the resolved background.
if (
!textRgb ||
!backgroundRgb ||
getRGBContrastRatio(textRgb, backgroundRgb) < MIN_CONTRAST_RATIO
) {
textHex = getContrastedColorHex(backgroundHex);
}
QRCode.toCanvas(canvas, this.data, {
errorCorrectionLevel:
+1 -1
View File
@@ -1,6 +1,6 @@
import { AttachableController } from "@material/web/internal/controller/attachable-controller";
import { Ripple } from "@material/web/ripple/internal/ripple";
import { styles } from "@material/web/ripple/internal/ripple-styles.cssresult.js";
import { styles } from "@material/web/ripple/internal/ripple-styles";
import { css } from "lit";
import { customElement } from "lit/decorators";
@@ -141,7 +141,7 @@ export class HaChooseSelector extends LitElement {
}
private _value(choice?: string): any {
if (this.value === null || this.value === undefined) {
if (!this.value) {
return undefined;
}
return typeof this.value === "object"
+14 -40
View File
@@ -1,23 +1,19 @@
import { consume, type ContextType } from "@lit/context";
import { initialState } from "@lit/task";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import type { HassEntity } from "home-assistant-js-websocket";
import { AsyncValueTask } from "../../common/controllers/async-value-task";
import { consumeEntityState } from "../../common/decorators/consume-context-entry";
import { fireEvent } from "../../common/dom/fire_event";
import {
configContext,
connectionContext,
entitiesContext,
} from "../../data/context";
import { entityIcon } from "../../data/icons";
import type { IconSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-icon-picker";
import "../ha-state-icon";
@customElement("ha-selector-icon")
export class HaIconSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: IconSelector;
@property() public value?: string;
@@ -34,21 +30,10 @@ export class HaIconSelector extends LitElement {
icon_entity?: string;
};
@state()
@consumeEntityState({ entityIdPath: ["context", "icon_entity"] })
private _stateObj?: HassEntity;
@state()
@consume({ context: entitiesContext, subscribe: true })
private _entities?: ContextType<typeof entitiesContext>;
@state()
@consume({ context: configContext, subscribe: true })
private _config?: ContextType<typeof configContext>;
@state()
@consume({ context: connectionContext, subscribe: true })
private _connection?: ContextType<typeof connectionContext>;
private get _stateObj(): HassEntity | undefined {
const iconEntity = this.context?.icon_entity;
return iconEntity ? this.hass.states[iconEntity] : undefined;
}
private _placeholderTask = new AsyncValueTask(this, {
task: ([
@@ -59,31 +44,19 @@ export class HaIconSelector extends LitElement {
connection,
stateObj,
]) => {
if (
placeholder ||
attributeIcon ||
!entities ||
!config ||
!connection ||
!stateObj
) {
if (placeholder || attributeIcon || !stateObj) {
return initialState;
}
return entityIcon(
entities,
config.config,
connection.connection,
stateObj
);
return entityIcon(entities, config, connection, stateObj);
},
args: () => {
const stateObj = this._stateObj;
return [
this.selector.icon?.placeholder,
stateObj?.attributes.icon,
this._entities,
this._config,
this._connection,
this.hass.entities,
this.hass.config,
this.hass.connection,
stateObj,
] as const;
},
@@ -99,6 +72,7 @@ export class HaIconSelector extends LitElement {
return html`
<ha-icon-picker
.hass=${this.hass}
.label=${this.label}
.value=${this.value}
.required=${this.required}
@@ -10,7 +10,6 @@ import type {
} from "../../common/translations/localize";
import type { HomeAssistant } from "../../types";
import "../ha-form/ha-form";
import { unitOfMeasurementOptions } from "../../data/number";
const SELECTOR_DEFAULTS = {
number: {
@@ -113,16 +112,6 @@ const SELECTOR_SCHEMAS = {
name: "step",
selector: { number: { mode: "box", step: "any" } },
},
{
name: "unit_of_measurement",
selector: {
select: {
custom_value: true,
sort: true,
options: unitOfMeasurementOptions,
},
},
},
] as const,
object: [] as const,
color_rgb: [] as const,
+1 -1
View File
@@ -1,5 +1,5 @@
import { SubMenu } from "@material/web/menu/internal/submenu/sub-menu";
import { styles } from "@material/web/menu/internal/submenu/sub-menu-styles.cssresult.js";
import { styles } from "@material/web/menu/internal/submenu/sub-menu-styles";
import { css } from "lit";
import { customElement } from "lit/decorators";
+2 -22
View File
@@ -24,8 +24,6 @@ export interface ToastClosedEventDetail {
export class HaToast extends LitElement {
@property({ attribute: "label-text" }) public labelText = "";
@property({ attribute: "announce-text" }) public announceText?: string;
@property({ type: Number, attribute: "timeout-ms" }) public timeoutMs = 4000;
@property({ type: Number, attribute: "bottom-offset" }) public bottomOffset =
@@ -192,6 +190,8 @@ export class HaToast extends LitElement {
style=${styleMap({
"--ha-toast-bottom-offset": `${this.bottomOffset}px`,
})}
role="status"
aria-live="polite"
popover=${ifDefined(popoverSupported ? "manual" : undefined)}
>
<span class="message">${this.labelText}</span>
@@ -200,14 +200,6 @@ export class HaToast extends LitElement {
<slot name="dismiss"></slot>
</div>
</div>
<span
class="assistive-message"
role="status"
aria-live=${this._active ? "polite" : "off"}
aria-atomic="true"
>
${this.announceText ?? this.labelText}
</span>
`;
}
@@ -262,18 +254,6 @@ export class HaToast extends LitElement {
min-width: 0;
}
.assistive-message {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.actions {
display: flex;
align-items: center;
+2 -1
View File
@@ -1,3 +1,4 @@
import { memoize } from "@fullcalendar/core/internal";
import { setHours, setMinutes } from "date-fns";
import type { HassConfig } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
@@ -410,7 +411,7 @@ export type BackupType = "automatic" | "manual" | "app_update";
const BACKUP_TYPE_ORDER: BackupType[] = ["automatic", "app_update", "manual"];
export const getBackupTypes = memoizeOne((isHassio: boolean) =>
export const getBackupTypes = memoize((isHassio: boolean) =>
isHassio
? BACKUP_TYPE_ORDER
: BACKUP_TYPE_ORDER.filter((type) => type !== "app_update")
+4 -1
View File
@@ -13,7 +13,10 @@ export const STREAM_TYPE_WEB_RTC = "web_rtc";
export type StreamType = typeof STREAM_TYPE_HLS | typeof STREAM_TYPE_WEB_RTC;
export { CameraEntityFeature } from "./feature/camera_entity_feature";
export enum CameraEntityFeature {
ON_OFF = 1,
STREAM = 2,
}
interface CameraEntityAttributes extends HassEntityAttributeBase {
model_name: string;
+12 -1
View File
@@ -68,7 +68,18 @@ export type ClimateEntity = HassEntityBase & {
};
};
export { ClimateEntityFeature } from "./feature/climate_entity_feature";
export enum ClimateEntityFeature {
TARGET_TEMPERATURE = 1,
TARGET_TEMPERATURE_RANGE = 2,
TARGET_HUMIDITY = 4,
FAN_MODE = 8,
PRESET_MODE = 16,
SWING_MODE = 32,
AUX_HEAT = 64,
TURN_OFF = 128,
TURN_ON = 256,
SWING_HORIZONTAL_MODE = 512,
}
const hvacModeOrdering = HVAC_MODES.reduce(
(order, mode, index) => {
-10
View File
@@ -36,13 +36,3 @@ export interface DirtyStateContext<
* boundary.
*/
export const dirtyStateContext = createContext<DirtyStateContext>("dirtyState");
declare global {
interface Window {
isDirtyState?: boolean;
}
interface HASSDomEvents {
"dirty-state-changed": { isDirty: boolean };
}
}
+10 -2
View File
@@ -6,9 +6,17 @@ import { stateActive } from "../common/entity/state_active";
import { supportsFeature } from "../common/entity/supports-feature";
import type { HomeAssistantFormatters } from "../types";
import { UNAVAILABLE } from "./entity/entity";
import { CoverEntityFeature } from "./feature/cover_entity_feature";
export { CoverEntityFeature };
export enum CoverEntityFeature {
OPEN = 1,
CLOSE = 2,
SET_POSITION = 4,
STOP = 8,
OPEN_TILT = 16,
CLOSE_TILT = 32,
STOP_TILT = 64,
SET_TILT_POSITION = 128,
}
export const DEFAULT_COVER_FAVORITE_POSITIONS = [0, 25, 75, 100];
-1
View File
@@ -173,7 +173,6 @@ export interface BatterySourceTypeEnergyPreference {
stat_rate?: string; // always available if power_config is set
power_config?: PowerConfig;
stat_soc?: string;
capacity?: number; // usable capacity in kWh, used to weight the combined SOC
name?: string;
}
export interface GasSourceTypeEnergyPreference {
+2 -2
View File
@@ -127,6 +127,7 @@ export const NON_NUMERIC_ATTRIBUTES = [
"away_mode",
"changed_by",
"code_format",
"color_modes",
"current_activity",
"device_class",
"editable",
@@ -176,7 +177,6 @@ export const NON_NUMERIC_ATTRIBUTES = [
"source_type",
"source",
"state_class",
"supported_color_modes",
"supported_features",
"swing_mode",
"swing_mode",
@@ -190,6 +190,7 @@ export const NON_NUMERIC_ATTRIBUTES = [
export const STATE_CONDITION_HIDDEN_ATTRIBUTES = [
"access_token",
"available_modes",
"color_modes",
"editable",
"effect_list",
"entity_picture",
@@ -206,7 +207,6 @@ export const STATE_CONDITION_HIDDEN_ATTRIBUTES = [
"sound_mode_list",
"source_list",
"state_class",
"supported_color_modes",
"swing_modes",
"token",
];

Some files were not shown because too many files have changed in this diff Show More