Compare commits

..
Author SHA1 Message Date
Petar Petrov 747f160a02 Parse formatjs locale data instead of evaluating it
Match the JSON.parse string that intl-datetimeformat 7.8.0 emits rather
than running the files in node:vm.

Re-seed the bundle budget: the formatjs polyfills add about 3 KiB to
each entry chunk. Most of the budget increase is restored 3% headroom.
2026-09-28 10:47:21 +03:00
Petar Petrov baabdaf3fa Evaluate formatjs locale data instead of regex parsing
intl-datetimeformat 7.8.0 wraps its locale data in an IIFE with
JSON.parse, which the regex no longer matched.
2026-09-28 10:22:23 +03:00
renovate[bot] d6b85ea47e Update formatjs 2026-09-28 06:02:09 +00:00
renovate[bot] 66321c57d2 Update dependency home-assistant-js-websocket to v9.7.0 (#54412)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-28 08:59:51 +03:00
renovate[bot] f990c85b32 Update dependency eslint to v10.11.0 (#54411)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-28 08:59:28 +03:00
nokside 97336c4009 Add default support to object selector fields (#53739)
* Add default support to object selector fields

* Clone object selector field defaults

* Use unknown for object selector defaults

* Use deepClone for object selector defaults
2026-09-28 08:42:28 +03:00
Dominik c3e84e4bb4 Remove backdrop from text-only markdown (#54390) 2026-09-28 08:41:56 +03:00
renovate[bot] 90f8e2f8d6 Update dependency browserslist to v4.29.1 (#54410)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-28 08:39:38 +03:00
renovate[bot] daaa673113 Update dependency @lokalise/node-api to v16.6.0 (#54409)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-28 08:39:12 +03:00
Paulus SchoutsenandClaude 113d645dad Hide the host in the MCP server URLs (#54375)
The MCP card showed the full URLs, which reveal the host of the
Home Assistant instance in screenshots. The card now shows only the
path. The copy button still copies the full URL.


Claude-Session: https://claude.ai/code/session_01GXiaNSBbB8C33fBL9yg5EP

Co-authored-by: Claude <[email protected]>
2026-09-28 05:57:05 +02:00
renovate[bot] 2d05e8f90f Update Yarn to v4.18.1 (#54407)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-28 05:53:15 +02:00
renovate[bot] c376786885 Update dependency @html-eslint/eslint-plugin to v0.66.1 (#54408)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-28 03:41:36 +00:00
renovate[bot] 61e15a6a4a Update dependency typescript-eslint to v8.70.1 (#54401)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-28 03:38:50 +00:00
Franck NijhofandClaude b4d65e3b83 Use a toggle for unlimited login attempts in HTTP config (#54399)
Use an unlimited toggle for the HTTP login attempts threshold

Core uses -1 to mean unlimited login attempts. Instead of asking users to
type -1, show an "Unlimited login attempts" toggle and only show the
number field when it is off. The -1 mapping stays internal to the form.

Co-authored-by: Claude <[email protected]>
2026-09-28 05:31:20 +02:00
renovate[bot] 728bb8055c Update dependency @lezer/highlight to v1.2.4 (#54387)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-28 05:29:27 +02:00
renovate[bot] 70e8cd376b Update dependency @rspack/core to v2.2.7 (#54388)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-28 05:29:09 +02:00
renovate[bot] 9fc555ced6 Update dependency fs-extra to v11.4.1 (#54394)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-28 05:28:40 +02:00
renovate[bot] 72b3a52a2e Update dependency js-yaml to v5.4.2 (#54395)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-28 05:28:21 +02:00
renovate[bot] dbbaa3e626 Update dependency marked to v18.0.14 (#54396)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-28 05:27:25 +02:00
renovate[bot] 11401b2a79 Update dependency vite to v8.3.1 (#54402)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-28 05:26:34 +02:00
renovate[bot] 01124ffbb2 Update vitest monorepo to v5.0.1 (#54405)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-28 05:26:01 +02:00
Allen Porter fd19a0e4e3 Forward PKCE code_challenge parameters in login flow (#54389)
* Forward PKCE code_challenge parameters in login flow

* Move auth data test to test/data/auth.test.ts
2026-09-27 17:59:30 -04:00
Marcel van der Veldt 40270f5a86 Send the right route to apps opened from their sidebar entry (#54404) 2026-09-27 17:58:26 -04:00
Franck Nijhof e7d76ce6c9 Move GitHub Actions updates from Dependabot to Renovate (#54382) 2026-09-27 19:34:43 +02:00
renovate[bot] 6c53fd6c4f Update CodeMirror (#54385)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-27 15:24:41 +02:00
renovate[bot] d54b818d83 Update babel monorepo to v8.0.6 (#54384)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-27 15:24:24 +02:00
Franck Nijhof c4f1a1bea2 Fix Renovate keeping the Playwright container update pending forever (#54380) 2026-09-27 15:24:10 +02:00
Felix Schneider 0cb50303c3 Add a user picker to an assist pipeline (#54276)
Let an assist pipeline act as a user

Needs home-assistant/core#182750.
2026-09-27 08:01:51 +00:00
Paulus SchoutsenandClaude Opus 5.5 d16e04a2b4 Enable the MCP server in one click (#54349)
* Enable MCP in one click and show discovered MCP servers

Enable now creates the Model Context Protocol Server entry without a
dialog and then opens the options flow. If the config flow still asks
for input, the config flow dialog opens instead.

The MCP card lists MCP servers that apps offer through Supervisor
discovery, with actions to add or ignore each one. The section is
hidden when nothing is discovered.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GXiaNSBbB8C33fBL9yg5EP

* Move discovered MCP servers to a separate pull request

This pull request only makes Enable create the Model Context Protocol
Server entry without a dialog.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GXiaNSBbB8C33fBL9yg5EP

* Keep the options closed after enabling the MCP server

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GXiaNSBbB8C33fBL9yg5EP

* Keep the Enable button loading until the MCP card reloads

The button became clickable again while the card was still loading
the new entry.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GXiaNSBbB8C33fBL9yg5EP

---------

Co-authored-by: Claude <[email protected]>
2026-09-27 07:42:38 +00:00
Paulus SchoutsenandClaude 21957ed41f Correct registry and auth API types in the frontend (#54288)
Compare the registry, auth and tag data modules with the handlers that
produce them, and fix the mismatches.

Updating a floor returned the floor entry, not an area. Looking up
extended entity entries returns null for an id the registry does not
know, so the callers now drop those before use.

Correct the refresh token payload, which was missing the provider type
and typed several always-present nullable fields as optional, and drop
a field the auth provider list does not return.


Claude-Session: https://claude.ai/code/session_01YDqKNfAAvMV7zJJNodsHfK

Co-authored-by: Claude <[email protected]>
2026-09-27 09:37:36 +02:00
Paulus SchoutsenandClaude Opus 5 8b5302ac06 Correct device protocol API types in the frontend (#54287)
* Correct device protocol API types in the frontend

Compare the Z-Wave, Zigbee, Matter, Thread, MQTT, Bluetooth and
ESPHome data modules with the handlers that produce them, and fix the
mismatches.

Widen the unions the backends have outgrown. The Zigbee radio type
gained a member the union did not list, and the Matter topology node
kind is documented as an open set.

Correct several declared results that never matched the wire,
including the rebuild and abort commands, the smart start calls, and
the log level setter. Mark the many controller and node fields the
Z-Wave server can omit as nullable, and split the MQTT debug message
type from the subscription message, which carries no timestamp.

Correct the Zigbee signal strength, which the frontend typed as text
and the library reports as a number.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YDqKNfAAvMV7zJJNodsHfK

* Separate the Z-Wave set config parameter result from its display state

The command reports a numeric SetValueStatus, but the type declared a
string status and an error field the backend never sends. The node
config view reused that type for its own row state, so it fed the
number straight into a translation key and the result line rendered
nothing.

Type the command result as sent, give the view its own state, and map
one to the other.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YDqKNfAAvMV7zJJNodsHfK

---------

Co-authored-by: Claude <[email protected]>
2026-09-27 09:36:11 +02:00
Paulus SchoutsenandClaude 4db73f6a3d Correct backup, onboarding and cloud API types in the frontend (#54286)
Compare the backup, onboarding and cloud data modules with the
handlers that produce them, and fix the mismatches.

Several backup calls declared the wrong result. Deleting a backup
returns agent errors, generating one returns a job id rather than a
backup id, and restoring one returns nothing. The backup list carries
extended content, not the base type.

Add the blocked manager state, which is the state the manager starts
in and replays to new subscribers, along with the event reason fields
and the core_restart restore state.

Correct the onboarding installation type, which listed a value the
backend never sends and omitted one it does, and narrow the cloud
onboarding status to the single field that endpoint returns.

Remove the cloud strict connection preference and its translations.
No such preference exists in core, so the warning it gated could never
render.


Claude-Session: https://claude.ai/code/session_01YDqKNfAAvMV7zJJNodsHfK

Co-authored-by: Claude <[email protected]>
2026-09-27 09:35:47 +02:00
Paulus SchoutsenandClaude 7c5a7c81e5 Hide MCP options for a disabled MCP server entry (#54369)
A disabled Model Context Protocol Server entry showed the URLs and the
options flow, although the server does not run. The card now shows
Enable instead, which enables the existing entry.


Claude-Session: https://claude.ai/code/session_01GXiaNSBbB8C33fBL9yg5EP

Co-authored-by: Claude <[email protected]>
2026-09-27 09:34:30 +02:00
karwosts 8da3713ec4 Standardize translation function for 'Ctrl' key. (#54368) 2026-09-27 09:33:47 +02:00
karwosts e7f60b8583 Fix multiple invocations of image cropper (#54376) 2026-09-27 09:32:09 +02:00
dependabot[bot] 6d7ecb90a1 Bump the codeql-action group across 1 directory with 2 updates (#54377)
Bumps the codeql-action group with 2 updates in the / directory: [github/codeql-action/init](https://github.com/github/codeql-action) and [github/codeql-action/analyze](https://github.com/github/codeql-action).


Updates `github/codeql-action/init` from 4.38.0 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

Updates `github/codeql-action/analyze` from 4.38.0 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-action
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.38.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-action
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-27 09:32:06 +02:00
Tom Carpenter 8fcf822025 Fix energy period selector icon resizing (#54372)
On narrow screens, the energy period selector resizes its date picker icon to ensure there is enough space to render the buttons. However the CSS used to target this icon is not specific enough meaning the resize leaks into elements within the date picker itself.

To fix this, add a part specifier to the date range inputs on the ha-date-range-picker, and target the CSS at that. This prevents targetted CSS from leaking into the picker itself.
2026-09-27 08:43:45 +02:00
Paulus SchoutsenandClaude 6878828996 Fix type error for notifications without a title (#54374)
A persistent notification can have a null title, but estimateDirection
takes a string. The function already treats a falsy value as an empty
string, so the behavior does not change.


Claude-Session: https://claude.ai/code/session_01GXiaNSBbB8C33fBL9yg5EP

Co-authored-by: Claude <[email protected]>
2026-09-26 20:36:14 -07:00
Paulus SchoutsenandClaude Opus 5 5c187a4e13 Correct Assist pipeline and speech API types in the frontend (#54285)
* Correct Assist pipeline and speech API types in the frontend

Compare the pipeline, conversation, speech and Wyoming data modules
with the handlers that produce them, and fix the mismatches.

The pipeline run events were the furthest adrift. Correct the wake
word, speech-to-text, intent and text-to-speech payloads, add the
stt-vad-start and stt-vad-end events, and drop keys the backend never
sends.

Two of these were live bugs. The chat log subscription nests its
payload under chat_log, so the log was read one level too high. The
debug view read the wake word result as ww_id, which is wake_word_id,
so that field always rendered undefined.

Rewrite the Wyoming info types against the wyoming library, which the
previous types modelled as empty tuples.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YDqKNfAAvMV7zJJNodsHfK

* Leave the chat log tool result to its own change

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YDqKNfAAvMV7zJJNodsHfK

---------

Co-authored-by: Claude <[email protected]>
2026-09-26 13:13:08 +02:00
56f695f4ed Clarify that IP bans do not expire (#54365)
* Clarify that IP bans do not expire

* Apply suggestion from @silamon

Co-authored-by: Simon Lamon <[email protected]>

---------

Co-authored-by: Petar Petrov <[email protected]>
Co-authored-by: Simon Lamon <[email protected]>
2026-09-26 11:52:21 +02:00
João Pedro Hickmann e0b7b929dd Skip drawing hidden line chart symbols to speed up tooltips (#54355)
* Don't draw the invisible symbols of line charts

The chart theme sets a 1px symbol for line series, so symbols are not
visible, but echarts still draws one per data point. Every tooltip or
axis pointer move repaints the whole layer, including those hundreds of
symbols: on a power sources graph with 200 points per series, one drag
step repainted about 1000 elements.

Set showSymbol: false in the theme. Echarts still draws the symbol of the
hovered point. On a slow tablet this cut the repaint per drag step from
about 50ms to 8ms and roughly halved the frame time while dragging.

* Fix ECharts capitalization
2026-09-26 12:38:51 +03:00
Paulus SchoutsenandClaude Opus 5 f0c63f8840 Correct config flow and integration API types in the frontend (#54289)
* Correct config flow and integration API types in the frontend

Compare the config entry, flow and manifest data modules with the
handlers that produce them, and fix the mismatches.

Split the in-progress flow message into a discriminated union, because
the removed event carries no flow. Mark the flow step fields the
backend sends as null, and correct the manifest, where the discovery
entries and several always-optional keys did not match the schema.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YDqKNfAAvMV7zJJNodsHfK

* Request a single config flow handler type

The endpoint indexes one key, and only "helper" and "integration"
exist. Asking for a list joined them into one unknown key, so looking
up a non-helper integration raised on the backend.

Add the config entry unloading state and the virtual integration type,
both of which the backend reports, with the strings they render.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YDqKNfAAvMV7zJJNodsHfK

---------

Co-authored-by: Claude <[email protected]>
2026-09-26 10:44:43 +02:00
Paulus SchoutsenandClaude 1ac04b0a65 Correct energy, history and recorder API types in the frontend (#54292)
Compare the energy, history and recorder data modules with the
handlers that produce them, and fix the mismatches.

History states omit attributes when the caller asks for a minimal
response, which both subscriptions do, so the field is optional and
the readers now guard it. Statistics omit the values that were not
requested, and the validation issues carry nullable placeholders.


Claude-Session: https://claude.ai/code/session_01YDqKNfAAvMV7zJJNodsHfK

Co-authored-by: Claude <[email protected]>
2026-09-26 10:41:17 +02:00
Paulus SchoutsenandClaude a7eb100132 Correct entity domain and helper API types in the frontend (#54293)
Compare the entity domain and helper data modules with the domains
that produce the attributes, and fix the mismatches.

Add supported feature flags the domains define and the frontend did
not list, and add attributes the domains report. Remove attributes
that are service fields rather than state, and the light colour
temperature keys core replaced with the kelvin forms.

Correct the weather forecast, where the event carries the whole list
and the wind speed is a number, and correct the image update command,
which sent the wrong id.


Claude-Session: https://claude.ai/code/session_01YDqKNfAAvMV7zJJNodsHfK

Co-authored-by: Claude <[email protected]>
2026-09-26 10:40:32 +02:00
237bf119e7 Keep zone occupants in one map bubble (#54307)
* Keep zone occupants in one map bubble

Several bubbles piled up on a zone marker. A person inside a zone whose
state did not name it (stale or router-based tracking) had no zone key
and showed as a loose marker under the bubble, or formed a second
bubble on the same spot. Occupants spread beyond the group radius fell
back to proximity clusters, and every cluster of same-zone members was
pinned above the zone marker, stacking bubbles. Zoomed out, each zone
kept its own bubble even when the zone markers overlapped.

Zone membership now also follows position: an entity inside a drawn
zone's circle joins that zone's bubble. The engine hands the builder
the group key, so only the zone's own group is pinned to its marker and
proximity clusters show where their members are. Proximity is measured
against every member of a group rather than only its first. Bubbles
whose rectangles would overlap on screen merge into one plain bubble,
keeping the zone's tail only when a single zone is involved.

The Leaflet fallback ignores group keys, so it no longer pins bubbles
to zones there.

* Fix map cluster test lint types

Co-authored-by: maartenla <[email protected]>

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2026-09-26 09:56:02 +02:00
Balloob BotandPaulus Schoutsen 4a6765d886 Skip upstream-only scheduled workflows on forks (#54366)
Co-authored-by: Paulus Schoutsen <[email protected]>
2026-09-26 07:50:09 +00:00
Jan-Philipp BeneckeandClaude Opus 5 8c5c35ca06 Describe trace graph nodes for screen readers (#54358)
* Describe trace graph nodes for screen readers

The trace graph is drawn entirely in SVG. Its nodes and branches are
keyboard reachable, but carried no accessible name and no role, so a
screen reader announced nothing while tabbing through a run. Everything
the graph says about the outcome of a step — the error badge, the
strike-through, the dashed outline, the repeat count — was visual only.

Each focusable stop now carries the same step description the automation
editor shows, followed by what the run did with that step. The ARIA is
written at the call sites in hat-script-graph, which is the only place
that knows whether an element is a step or just layout: the branch heads,
the then/else arrows, the failed-condition cross and the spacers are
marked aria-hidden, while the branch that merely groups the triggers is
left alone so the triggers inside it stay in the accessibility tree.

The names themselves are built by buildTraceLabels, a pure function that
walks the trace tree and returns one string per node path, reusing the
same describers the editor uses. The trace panels call it because they
already own hass, and memoize it on the trace, so the graph takes a plain
record and stays a leaf: it subscribes to nothing, and entity state
changes no longer rerender it.

Co-Authored-By: Claude Opus 5 <[email protected]>

* Process code review comments

Fix the memoization of the trace labels: `hass` is replaced on every
state update, so comparing it rebuilt every label on every state event,
which was worse than the rerendering the labels were hoisted out of the
graph to avoid. Compare everything but `hass`, and let an absent entity
registry stay undefined rather than allocating a new array each render.

Report whether a condition passed or failed instead of only that it ran.
That outcome is otherwise shown by the tracked path and by a cross on a
node that is aria-hidden, so it never reached a screen reader.

Drop `role="button"` from the nodes. Selection follows focus and nothing
handles clicks or key presses, so the role promised an activation that
does not exist. The branches keep `role="group"`, which makes no such
promise.

Pass the integration manifests through from both trace panels, so the
names match the ones the automation editor shows.

Co-Authored-By: Claude Opus 5 <[email protected]>

* Process code review comments

Give the leaf nodes `role="img"`. Dropping `role="button"` left them with
the implicit generic role, on which ARIA prohibits an author-provided
name, so the labels risked being ignored and the nodes going back to
being unnamed focus stops. A node draws one step and slots only
decoration, so `img` names it without claiming it is a control. The
branches keep `role="group"`, since `img` would make their focusable
children presentational. `aria-disabled` goes with the button role: it is
unsupported on `img`, and the state is already part of the name.

Assert the nested steps in the label coverage test. It only checked the
building blocks and the choose options, so a regression in the recursion
through branch children would have gone unnoticed.

Co-Authored-By: Claude Opus 5 <[email protected]>

---------

Co-authored-by: Claude Opus 5 <[email protected]>
2026-09-25 20:25:04 +02:00
Yosi LevyandCopilot Autofix powered by AI a3cb6d8202 Automatic direction for alerts and notifications (#54342)
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-09-25 16:52:45 +01:00
Dr.Blank f60a40de72 Show security rating scale in app rating dialog (#54345) 2026-09-25 14:53:07 +01:00
187 changed files with 2882 additions and 1930 deletions
-24
View File
@@ -1,24 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
# Dependabot only scans .github/workflows by default; composite actions
# under .github/actions must be listed explicitly to stay updated.
# https://github.com/dependabot/dependabot-core/issues/6704
directories:
- "/"
- "/.github/actions/setup"
- "/.github/actions/build"
- "/.github/actions/netlify-deploy"
schedule:
interval: weekly
time: "06:00"
cooldown:
default-days: 7
open-pull-requests-limit: 10
groups:
codeql-action:
patterns:
- "github/codeql-action/*"
labels:
- Dependencies
- GitHub Actions
+2 -2
View File
@@ -18,7 +18,7 @@ jobs:
deploy_dev:
runs-on: ubuntu-latest
name: Deploy Development
if: github.event_name != 'push'
if: github.repository == 'home-assistant/frontend' && github.event_name != 'push'
environment:
name: Cast Development
url: ${{ steps.deploy.outputs.netlify_url }}
@@ -50,7 +50,7 @@ jobs:
deploy_master:
runs-on: ubuntu-latest
name: Deploy Production
if: github.event_name == 'push'
if: github.repository == 'home-assistant/frontend' && github.event_name == 'push'
environment:
name: Cast Production
url: ${{ steps.deploy.outputs.netlify_url }}
+2 -2
View File
@@ -32,12 +32,12 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
with:
languages: javascript-typescript
build-mode: none
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
with:
category: "/language:javascript-typescript"
+2 -2
View File
@@ -19,7 +19,7 @@ jobs:
deploy_dev:
runs-on: ubuntu-latest
name: Demo Development
if: github.event_name != 'push' || github.ref_name != 'master'
if: github.repository == 'home-assistant/frontend' && (github.event_name != 'push' || github.ref_name != 'master')
environment:
name: Demo Development
url: ${{ steps.deploy.outputs.unique_deploy_url }}
@@ -50,7 +50,7 @@ jobs:
deploy_master:
runs-on: ubuntu-latest
name: Demo Production
if: github.event_name == 'push' && github.ref_name == 'master'
if: github.repository == 'home-assistant/frontend' && github.event_name == 'push' && github.ref_name == 'master'
environment:
name: Demo Production
url: ${{ steps.deploy.outputs.netlify_url }}
+1
View File
@@ -13,6 +13,7 @@ env:
jobs:
deploy:
if: github.repository == 'home-assistant/frontend'
runs-on: ubuntu-latest
environment:
name: Design
+1
View File
@@ -11,6 +11,7 @@ permissions:
jobs:
lock:
if: github.repository == 'home-assistant/frontend'
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c # v6.0.2
+2
View File
@@ -14,6 +14,7 @@ permissions:
jobs:
nightly:
if: github.repository == 'home-assistant/frontend'
name: Nightly
runs-on: ubuntu-latest
permissions:
@@ -113,6 +114,7 @@ jobs:
# needed for the uploaded artifact, not the wheel, so they are downloaded in
# parallel here instead of blocking the build above.
translations:
if: github.repository == 'home-assistant/frontend'
name: Translations
runs-on: ubuntu-latest
steps:
+1
View File
@@ -12,6 +12,7 @@ permissions:
jobs:
stale:
if: github.repository == 'home-assistant/frontend'
runs-on: ubuntu-latest
steps:
- name: 90 days stale policy
@@ -14,6 +14,7 @@ permissions:
jobs:
sync:
if: github.repository == 'home-assistant/frontend'
name: Sync
runs-on: ubuntu-latest
permissions:
@@ -15,6 +15,7 @@ permissions:
jobs:
sync:
if: github.repository == 'home-assistant/frontend'
name: Sync
runs-on: ubuntu-latest
permissions:
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -13,4 +13,4 @@ nodeLinker: node-modules
npmMinimalAgeGate: 3d
yarnPath: .yarn/releases/yarn-4.18.0.cjs
yarnPath: .yarn/releases/yarn-4.18.1.cjs
+8 -8
View File
@@ -1,15 +1,15 @@
{
"_comment": "Initial JS budget (raw/uncompressed bytes) for the cold-load critical entrypoints. Enforced by build-scripts/check-bundle-size.cjs in CI. Re-seed after an intentional change with `--update --headroom=<percent>`.",
"frontend-modern": {
"app": 576583,
"core": 54790,
"authorize": 543547,
"onboarding": 655556
"app": 593517,
"core": 54884,
"authorize": 557490,
"onboarding": 676131
},
"frontend-legacy": {
"app": 717124,
"core": 181583,
"authorize": 699175,
"onboarding": 816133
"app": 738388,
"core": 188119,
"authorize": 716748,
"onboarding": 840680
}
}
+10 -3
View File
@@ -47,16 +47,23 @@ const convertToJSON = async (
`Intl\\.${obj}\\.${addFunc}\\((?<data>.*)\\)`,
"s"
);
const encodedData = localeData.match(
/\(JSON\.parse\((?<data>"(?:[^"\\]|\\.)*")\)\);?\s*$/s
)?.groups?.data;
localeData = firstBlock.match(dataRegex)?.groups?.data;
if (!localeData) {
if (!localeData && !encodedData) {
throw Error(`Failed to extract data for language ${lang} from ${pkg}`);
}
// Parse to validate JSON, then stringify to minify
try {
localeData = JSON.stringify(JSON.parse(localeData));
localeData = JSON.stringify(
JSON.parse(localeData || JSON.parse(encodedData))
);
await writeFile(join(outDir, `${pkg}/${lang}.json`), localeData);
} catch (e) {
throw Error(`Failed to parse JSON for language ${lang} from ${pkg}: ${e}`);
throw Error(`Failed to parse JSON for language ${lang} from ${pkg}: ${e}`, {
cause: e,
});
}
};
+3
View File
@@ -15,6 +15,7 @@ const pipelines: AssistPipeline[] = [
tts_voice: "JennyNeural",
wake_word_entity: null,
wake_word_id: null,
prefer_local_intents: false,
},
{
id: "01local",
@@ -29,6 +30,7 @@ const pipelines: AssistPipeline[] = [
tts_voice: null,
wake_word_entity: null,
wake_word_id: null,
prefer_local_intents: false,
},
{
id: "01chatgpt",
@@ -43,6 +45,7 @@ const pipelines: AssistPipeline[] = [
tts_voice: "ElviraNeural",
wake_word_entity: null,
wake_word_id: null,
prefer_local_intents: false,
},
];
+10 -2
View File
@@ -1,7 +1,6 @@
import type {
BackupAgentsInfo,
BackupConfig,
BackupContent,
BackupInfo,
} from "../../../src/data/backup";
import { BackupScheduleRecurrence } from "../../../src/data/backup";
@@ -28,7 +27,16 @@ const backupInfo: BackupInfo = {
"backup.local": { size: 1024 * 1024 * 512, protected: true },
"cloud.cloud": { size: 1024 * 1024 * 512, protected: true },
},
} as BackupContent,
failed_agent_ids: [],
failed_addons: [],
failed_folders: [],
extra_metadata: {},
addons: [],
database_included: true,
folders: [],
homeassistant_included: true,
homeassistant_version: "2026.9.0",
},
],
agent_errors: {},
last_attempted_automatic_backup: recent,
+7 -3
View File
@@ -20,12 +20,14 @@ const demoWebhooks: Webhook[] = [
domain: "automation",
name: "Front door motion",
local_only: false,
allowed_methods: ["POST", "PUT"],
},
{
webhook_id: "demo_companion_app",
domain: "mobile_app",
name: "Companion app",
local_only: false,
allowed_methods: ["POST", "PUT"],
},
];
@@ -49,12 +51,13 @@ const demoCloudhooks = Object.fromEntries(
// object directly and reset on reload.
const cloudStatus: CloudStatusLoggedIn = {
logged_in: true,
auto_login: null,
cloud: "connected",
cloud_last_disconnect_reason: null,
email: "[email protected]",
google_registered: false,
google_local_connected: false,
google_entities: emptyFilter(),
google_domains: ["light", "switch", "climate", "cover"],
alexa_registered: true,
alexa_entities: emptyFilter(),
remote_domain: "demo-instance.ui.nabu.casa",
@@ -75,10 +78,11 @@ const cloudStatus: CloudStatusLoggedIn = {
alexa_enabled: true,
remote_enabled: true,
remote_allow_remote_enable: true,
strict_connection: "disabled",
google_secure_devices_pin: undefined,
google_secure_devices_pin: null,
cloudhooks: demoCloudhooks,
alexa_default_expose: null,
alexa_report_state: true,
google_default_expose: null,
google_report_state: true,
tts_default_voice: ["en-US", "JennyNeural"],
cloud_ice_servers_enabled: false,
+2 -2
View File
@@ -192,9 +192,9 @@ const buildNodeDiagnostics = (
node_type: node.is_bridge
? NodeType.BRIDGE
: (NODE_TYPE_BY_ROLE[node.role ?? ""] ?? NodeType.END_DEVICE),
network_name: node.network_name ?? node.ssid ?? undefined,
network_name: node.network_name ?? node.ssid ?? null,
ip_adresses: [nodeIpAddress(node)],
mac_address: node.ext_address?.match(/.{2}/g)?.join(":"),
mac_address: node.ext_address?.match(/.{2}/g)?.join(":") ?? null,
available: node.available !== false,
active_fabrics: FABRICS,
active_fabric_index: 1,
+5 -4
View File
@@ -1,5 +1,6 @@
import type {
MQTTDeviceDebugInfo,
MQTTLoggedMessage,
MQTTMessage,
} from "../../../../../src/data/mqtt";
import type { MockHomeAssistant } from "../../../../../src/fake_data/provide_hass";
@@ -35,11 +36,11 @@ const resolveFilter = (filter: string): string =>
})
.join("/") || "homeassistant/status";
const buildMessage = (topic: string, qos: number): MQTTMessage => ({
const buildMessage = (topic: string, qos: number): MQTTLoggedMessage => ({
topic,
payload: (PAYLOADS[topic] ?? PAYLOADS.default)(),
qos,
retain: 0,
retain: false,
time: new Date().toISOString(),
});
@@ -167,11 +168,11 @@ export const mockMqtt = (hass: MockHomeAssistant) => {
?.filter((action) => action.action === "mqtt.publish")
.forEach((action) => {
const topic = String(action.data?.topic ?? "");
const message: MQTTMessage = {
const message: MQTTLoggedMessage = {
topic,
payload: String(action.data?.payload ?? ""),
qos: Number(action.data?.qos ?? 0),
retain: action.data?.retain ? 1 : 0,
retain: Boolean(action.data?.retain),
time: new Date().toISOString(),
};
subscriptions.forEach((subscription) => {
+8 -8
View File
@@ -45,7 +45,7 @@ const DEVICES: ZHADevice[] = [
ieee: COORDINATOR_IEEE,
nwk: 0x0000,
lqi: 255,
rssi: "0",
rssi: 0,
last_seen: minutesAgo(0),
manufacturer: "Nabu Casa",
model: "Connect ZBT-1",
@@ -72,7 +72,7 @@ const DEVICES: ZHADevice[] = [
ieee: PORCH_IEEE,
nwk: 0x1a2b,
lqi: 224,
rssi: "-58",
rssi: -58,
last_seen: minutesAgo(2),
manufacturer: "IKEA of Sweden",
model: "TRADFRI bulb E27 CWS 806lm",
@@ -99,7 +99,7 @@ const DEVICES: ZHADevice[] = [
ieee: MOTION_IEEE,
nwk: 0x5e6f,
lqi: 142,
rssi: "-77",
rssi: -77,
last_seen: minutesAgo(9),
manufacturer: "IKEA of Sweden",
model: "TRADFRI motion sensor",
@@ -122,7 +122,7 @@ const DEVICES: ZHADevice[] = [
ieee: PLUG_IEEE,
nwk: 0x3c4d,
lqi: 198,
rssi: "-64",
rssi: -64,
last_seen: minutesAgo(240),
manufacturer: "Innr",
model: "SP 220",
@@ -145,7 +145,7 @@ const DEVICES: ZHADevice[] = [
ieee: KITCHEN_IEEE,
nwk: 0x9c0d,
lqi: 186,
rssi: "-69",
rssi: -69,
last_seen: minutesAgo(4),
manufacturer: "IKEA of Sweden",
model: "TRADFRI on/off switch",
@@ -168,7 +168,7 @@ const DEVICES: ZHADevice[] = [
ieee: LANDING_IEEE,
nwk: 0xab12,
lqi: 164,
rssi: "-74",
rssi: -74,
last_seen: minutesAgo(6),
manufacturer: "Aqara",
model: "WSDCGQ11LM",
@@ -191,7 +191,7 @@ const DEVICES: ZHADevice[] = [
ieee: GARAGE_IEEE,
nwk: 0xcd34,
lqi: 118,
rssi: "-83",
rssi: -83,
last_seen: minutesAgo(21),
manufacturer: "Aqara",
model: "MCCGQ11LM",
@@ -214,7 +214,7 @@ const DEVICES: ZHADevice[] = [
ieee: OFFICE_IEEE,
nwk: 0x7a8b,
lqi: 211,
rssi: "-61",
rssi: -61,
last_seen: minutesAgo(1),
manufacturer: "Innr",
model: "SP 240",
@@ -160,7 +160,6 @@ const buildNodeStatistics = (
return {
event: "statistics updated",
source: "node",
nodeId,
node_id: nodeId,
commands_tx: 1200 + nodeId * 7,
commands_rx: 980 + nodeId * 5,
@@ -11,7 +11,6 @@ export const mockPersistentNotification = (hass: MockHomeAssistant) => {
message: "There was motion detected in the backyard.",
notification_id: "demo-1",
title: "Motion Detected!",
status: "unread",
},
},
} as PersistentNotificationMessage);
+7 -3
View File
@@ -121,12 +121,14 @@ const demoWebhooks: Webhook[] = [
domain: "automation",
name: "Front door motion",
local_only: false,
allowed_methods: ["POST", "PUT"],
},
{
webhook_id: "demo_companion_app",
domain: "mobile_app",
name: "Companion app",
local_only: false,
allowed_methods: ["POST", "PUT"],
},
];
@@ -155,12 +157,13 @@ const buildCloudStatus = (scenario: CloudDemoScenario): CloudStatusLoggedIn => {
: {};
return {
logged_in: true,
auto_login: null,
cloud: "connected",
cloud_last_disconnect_reason: null,
email: "[email protected]",
google_registered: scenario.google,
google_local_connected: false,
google_entities: emptyFilter(),
google_domains: ["light", "switch", "climate", "cover"],
alexa_registered: scenario.alexa,
alexa_entities: emptyFilter(),
remote_domain: "demo-instance.ui.nabu.casa",
@@ -181,10 +184,11 @@ const buildCloudStatus = (scenario: CloudDemoScenario): CloudStatusLoggedIn => {
alexa_enabled: scenario.alexa,
remote_enabled: scenario.remote,
remote_allow_remote_enable: true,
strict_connection: "disabled",
google_secure_devices_pin: undefined,
google_secure_devices_pin: null,
cloudhooks,
alexa_default_expose: null,
alexa_report_state: true,
google_default_expose: null,
google_report_state: true,
tts_default_voice: ["en-US", "JennyNeural"],
cloud_ice_servers_enabled: scenario.webrtc,
+29 -29
View File
@@ -42,25 +42,25 @@
"@babel/runtime": "8.0.5",
"@braintree/sanitize-url": "7.1.2",
"@codemirror/autocomplete": "6.20.3",
"@codemirror/commands": "6.11.0",
"@codemirror/commands": "6.11.1",
"@codemirror/lang-jinja": "6.0.1",
"@codemirror/lang-yaml": "6.1.3",
"@codemirror/language": "6.12.4",
"@codemirror/lint": "6.9.7",
"@codemirror/search": "6.7.2",
"@codemirror/state": "6.7.4",
"@codemirror/view": "6.43.11",
"@codemirror/state": "6.7.6",
"@codemirror/view": "6.43.13",
"@date-fns/tz": "1.5.0",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "7.6.2",
"@formatjs/intl-displaynames": "7.3.14",
"@formatjs/intl-durationformat": "0.11.0",
"@formatjs/intl-datetimeformat": "7.8.0",
"@formatjs/intl-displaynames": "7.3.15",
"@formatjs/intl-durationformat": "0.11.1",
"@formatjs/intl-getcanonicallocales": "3.2.12",
"@formatjs/intl-listformat": "8.3.14",
"@formatjs/intl-locale": "5.3.11",
"@formatjs/intl-numberformat": "9.4.1",
"@formatjs/intl-pluralrules": "6.3.14",
"@formatjs/intl-relativetimeformat": "12.3.14",
"@formatjs/intl-listformat": "8.3.15",
"@formatjs/intl-locale": "5.3.12",
"@formatjs/intl-numberformat": "9.4.3",
"@formatjs/intl-pluralrules": "6.3.15",
"@formatjs/intl-relativetimeformat": "12.3.15",
"@fullcalendar/core": "6.1.21",
"@fullcalendar/daygrid": "6.1.21",
"@fullcalendar/interaction": "6.1.21",
@@ -68,7 +68,7 @@
"@fullcalendar/luxon3": "6.1.21",
"@fullcalendar/timegrid": "6.1.21",
"@home-assistant/webawesome": "3.7.0-ha.1",
"@lezer/highlight": "1.2.3",
"@lezer/highlight": "1.2.4",
"@lit-labs/motion": "1.1.0",
"@lit-labs/observers": "2.1.0",
"@lit-labs/virtualizer": "2.1.1",
@@ -104,17 +104,17 @@
"element-internals-polyfill": "3.0.2",
"fuse.js": "7.5.0",
"hls.js": "1.7.3",
"home-assistant-js-websocket": "9.6.0",
"home-assistant-js-websocket": "9.7.0",
"idb-keyval": "6.3.0",
"intl-messageformat": "11.2.15",
"js-yaml": "5.4.1",
"js-yaml": "5.4.2",
"leaflet": "1.9.4",
"leaflet.markercluster": "1.5.3",
"lit": "3.3.3",
"lit-html": "3.3.3",
"luxon": "3.7.2",
"maplibre-gl": "5.24.0",
"marked": "18.0.12",
"marked": "18.0.14",
"memoize-one": "6.0.0",
"nanoid": "6.0.1",
"node-vibrant": "4.0.4",
@@ -139,21 +139,21 @@
},
"devDependencies": {
"@ampproject/remapping": "2.3.0",
"@babel/core": "8.0.5",
"@babel/core": "8.0.6",
"@babel/helper-define-polyfill-provider": "1.0.0",
"@babel/plugin-transform-runtime": "8.0.1",
"@babel/preset-env": "8.0.5",
"@babel/plugin-transform-runtime": "8.0.6",
"@babel/preset-env": "8.0.6",
"@bundle-stats/plugin-webpack-filter": "4.22.3",
"@eslint/js": "10.0.1",
"@gfx/zopfli": "1.0.15",
"@html-eslint/eslint-plugin": "0.65.0",
"@lokalise/node-api": "16.4.1",
"@html-eslint/eslint-plugin": "0.66.1",
"@lokalise/node-api": "16.6.0",
"@octokit/auth-oauth-device": "8.0.5",
"@octokit/plugin-retry": "8.1.1",
"@octokit/rest": "22.0.1",
"@playwright/test": "1.62.1",
"@rsdoctor/rspack-plugin": "1.6.4",
"@rspack/core": "2.2.3",
"@rspack/core": "2.2.7",
"@rspack/dev-server": "2.2.1",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.26",
@@ -170,13 +170,13 @@
"@types/tar": "7.0.87",
"@typescript/native": "npm:[email protected]",
"@versatiles/style": "5.13.1",
"@vitest/coverage-v8": "5.0.0",
"@vitest/coverage-v8": "5.0.1",
"babel-loader": "10.1.1",
"babel-plugin-polyfill-corejs3": "1.0.0",
"browserslist": "4.28.9",
"browserslist": "4.29.1",
"browserslist-useragent-regexp": "4.1.4",
"del": "8.0.1",
"eslint": "10.10.0",
"eslint": "10.11.0",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-webpack": "0.13.11",
"eslint-plugin-import-x": "4.17.1",
@@ -185,7 +185,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.4.1",
"generate-license-file": "4.2.5",
"glob": "13.0.6",
"globals": "17.12.0",
@@ -213,10 +213,10 @@
"terser-webpack-plugin": "5.6.1",
"ts-lit-plugin": "2.0.2",
"typescript": "6.0.3",
"typescript-eslint": "8.70.0",
"vite": "8.3.0",
"typescript-eslint": "8.70.1",
"vite": "8.3.1",
"vite-tsconfig-paths": "6.1.1",
"vitest": "5.0.0",
"vitest": "5.0.1",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.4.1#~/.yarn/patches/workbox-build-npm-7.4.1-c84561662c.patch"
@@ -230,7 +230,7 @@
"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"
},
"packageManager": "[email protected].0",
"packageManager": "[email protected].1",
"volta": {
"node": "24.21.0"
}
+37 -1
View File
@@ -10,7 +10,7 @@
"group:monorepos",
"group:recommended"
],
"enabledManagers": ["npm", "nvm", "custom.regex"],
"enabledManagers": ["npm", "nvm", "custom.regex", "github-actions"],
"minimumReleaseAge": "3 days",
"postUpdateOptions": ["yarnDedupeHighest"],
"lockFileMaintenance": {
@@ -108,6 +108,42 @@
"groupName": "Playwright",
"matchPackageNames": ["@playwright/test", "mcr.microsoft.com/playwright"],
"minimumGroupSize": 5
},
{
"description": "MCR returns no release timestamps, so the minimum release age would keep the Playwright container pending forever. The npm package still gates the group.",
"matchPackageNames": ["mcr.microsoft.com/playwright"],
"minimumReleaseAge": null
},
{
"description": "GitHub Actions: weekly, with a 7 day cooldown, like Dependabot did",
"matchManagers": ["github-actions"],
"addLabels": ["GitHub Actions"],
"schedule": ["before 8am on monday"],
"minimumReleaseAge": "7 days"
},
{
"description": "Leave runner labels and action version inputs alone, Dependabot never updated those",
"matchManagers": ["github-actions"],
"matchDepTypes": ["github-runner", "uses-with"],
"enabled": false
},
{
"description": "The Playwright container is already managed by the custom regex manager. Extracting it twice would break the minimumGroupSize count of the Playwright group.",
"matchManagers": ["github-actions"],
"matchPackageNames": ["mcr.microsoft.com/playwright"],
"enabled": false
},
{
"description": "Group the github/codeql-action subactions into one PR (they share a release)",
"matchPackageNames": ["/^github\\/codeql-action(?:\\/|$)/"],
"groupName": "github/codeql-action",
"groupSlug": "codeql-action"
},
{
"description": "Group the actions/cache subactions into one PR (they share a release)",
"matchPackageNames": ["/^actions\\/cache(?:\\/|$)/"],
"groupName": "actions/cache",
"groupSlug": "actions-cache"
}
]
}
+12 -5
View File
@@ -37,6 +37,10 @@ export class HaAuthFlow extends LitElement {
@property({ attribute: false }) public oauth2State?: string;
@property({ attribute: false }) public codeChallenge?: string;
@property({ attribute: false }) public codeChallengeMethod?: string;
@property({ attribute: false }) public step?: DataEntryFlowStep;
@property({ attribute: false }) public initStoreToken = false;
@@ -241,7 +245,7 @@ export class HaAuthFlow extends LitElement {
html`<ha-auth-form
.data=${this._stepData!}
.schema=${autocompleteLoginFields(step.data_schema)}
.error=${step.errors}
.error=${step.errors ?? undefined}
.disabled=${this._submitting}
.computeLabel=${this._computeLabelCallback(step)}
.computeError=${this._computeErrorCallback(step)}
@@ -298,10 +302,13 @@ export class HaAuthFlow extends LitElement {
}
try {
const response = await createLoginFlow(this.clientId, this.redirectUri, [
newProvider.type,
newProvider.id,
]);
const response = await createLoginFlow(
this.clientId,
this.redirectUri,
[newProvider.type, newProvider.id],
this.codeChallenge,
this.codeChallengeMethod
);
const data = await response.json();
+12
View File
@@ -33,6 +33,10 @@ export class HaAuthorize extends provideLiteI18nMixin(
@property({ attribute: false }) public oauth2State?: string;
@property({ attribute: false }) public codeChallenge?: string;
@property({ attribute: false }) public codeChallengeMethod?: string;
@property({ attribute: false }) public translationFragment = "page-authorize";
@state() private _authProvider?: AuthProvider;
@@ -57,6 +61,12 @@ export class HaAuthorize extends provideLiteI18nMixin(
if (query.state) {
this.oauth2State = query.state;
}
if (query.code_challenge) {
this.codeChallenge = query.code_challenge;
}
if (query.code_challenge_method) {
this.codeChallengeMethod = query.code_challenge_method;
}
}
protected render() {
@@ -185,6 +195,8 @@ export class HaAuthorize extends provideLiteI18nMixin(
.clientId=${this.clientId}
.redirectUri=${this.redirectUri}
.oauth2State=${this.oauth2State}
.codeChallenge=${this.codeChallenge}
.codeChallengeMethod=${this.codeChallengeMethod}
.authProvider=${this._authProvider}
.initStoreToken=${this._preselectStoreToken}
></ha-auth-flow>
+15
View File
@@ -0,0 +1,15 @@
import { mdiAppleKeyboardCommand } from "@mdi/js";
import { html, type TemplateResult } from "lit";
import "../../components/ha-svg-icon";
import { isMac } from "../../util/is_mac";
import type { LocalizeFunc } from "../translations/localize";
export const ctrlOrCmdLabel = (localize: LocalizeFunc): string =>
isMac ? "⌘" : localize("ui.dialogs.shortcuts.keys.ctrl");
export const renderCtrlOrCmd = (
localize: LocalizeFunc
): TemplateResult | string =>
isMac
? html`<ha-svg-icon .path=${mdiAppleKeyboardCommand}></ha-svg-icon>`
: localize("ui.dialogs.shortcuts.keys.ctrl");
+94 -29
View File
@@ -31,6 +31,7 @@ import {
import { isTouch } from "../../../util/is_touch";
import type {
MapCircleOptions,
MapClusterIcon,
MapClusterOptions,
MapControlPosition,
MapDraggableMarkerOptions,
@@ -132,12 +133,18 @@ interface ClusterGroup {
/** Members are shown in a bubble at their spot instead of an icon */
open?: boolean;
/** Grouped by key (a zone), so it bubbles even with a single member */
keyed?: boolean;
key?: string;
members: ManagedMarker[];
center: MapLatLng;
icon?: MapClusterIcon;
iconMarker?: MapLibreMarker;
}
const centerOf = (members: ManagedMarker[]): MapLatLng => [
members.reduce((sum, m) => sum + m.location[0], 0) / members.length,
members.reduce((sum, m) => sum + m.location[1], 0) / members.length,
];
/**
* The MapLibre GL engine: native vector rendering, requires WebGL2. The host
* falls back to Leaflet without it or after a fatal context loss.
@@ -1236,9 +1243,9 @@ export class MapLibreMapEngine implements MapEngine {
if (regroup || !this._clusterGroups.length) {
const { radius, groupKey, groupRadius } = this._clusterOptions;
const groups: {
seed: { x: number; y: number };
points: { x: number; y: number }[];
members: ManagedMarker[];
keyed?: boolean;
key?: string;
}[] = [];
// Keyed groups first; one spread too wide falls through to proximity
@@ -1253,7 +1260,7 @@ export class MapLibreMapEngine implements MapEngine {
(byKey[key] ??= []).push(managed);
}
}
for (const members of Object.values(byKey)) {
for (const [key, members] of Object.entries(byKey)) {
const points = members.map((m) => this._project(m.location));
const xs = points.map((p) => p.x);
const ys = points.map((p) => p.y);
@@ -1264,7 +1271,7 @@ export class MapLibreMapEngine implements MapEngine {
// A keyed group (a zone) bubbles even with a single member, so a lone
// person or device in a zone still shows in a bubble pinned to it.
if (members.length === 1 || spread <= (groupRadius ?? radius)) {
groups.push({ seed: points[0], members, keyed: true });
groups.push({ points, members, key });
} else {
ungrouped.push(...members);
}
@@ -1275,28 +1282,23 @@ export class MapLibreMapEngine implements MapEngine {
for (const managed of ungrouped) {
const point = this._project(managed.location);
const group = groups.find(
(candidate) =>
Math.hypot(
candidate.seed.x - point.x,
candidate.seed.y - point.y
) <= radius
const group = groups.find((candidate) =>
candidate.points.some(
(member) =>
Math.hypot(member.x - point.x, member.y - point.y) <= radius
)
);
if (group) {
group.members.push(managed);
group.points.push(point);
} else {
groups.push({ seed: point, members: [managed] });
groups.push({ points: [point], members: [managed] });
}
}
this._clusterGroups = groups.map((group) => ({
members: group.members,
keyed: group.keyed,
center: [
group.members.reduce((sum, m) => sum + m.location[0], 0) /
group.members.length,
group.members.reduce((sum, m) => sum + m.location[1], 0) /
group.members.length,
] as MapLatLng,
key: group.key,
center: centerOf(group.members),
}));
}
@@ -1304,20 +1306,27 @@ export class MapLibreMapEngine implements MapEngine {
group.iconMarker = undefined;
// A lone non-keyed marker shows plainly; a lone zone occupant falls
// through to the bubble path so it renders in a bubble at its zone.
if (group.members.length === 1 && !group.keyed) {
this._showMarker(group.members[0]);
continue;
}
if (group.open) {
this._openGroup(group);
group.icon =
(group.members.length > 1 || group.key !== undefined) && !group.open
? this._buildIcon(group)
: undefined;
}
if (regroup) {
this._mergeOverlappingBubbles();
}
for (const group of this._clusterGroups) {
if (!group.icon) {
if (group.open) {
this._openGroup(group);
} else {
this._showMarker(group.members[0]);
}
continue;
}
group.members.forEach((managed) => this._hideMarker(managed));
const icon = this._clusterOptions.iconBuilder(
group.members.map((managed) => managed.handle),
group.center
);
const { icon } = group;
icon.element.style.width = `${icon.size[0]}px`;
icon.element.style.height = `${icon.size[1]}px`;
// Clicking a bubble zooms in on its members; when zooming cannot
@@ -1380,4 +1389,60 @@ export class MapLibreMapEngine implements MapEngine {
}
}
}
private _buildIcon(group: ClusterGroup): MapClusterIcon {
return this._clusterOptions!.iconBuilder(
group.members.map((managed) => managed.handle),
group.center,
group.key
);
}
private _iconRect(group: ClusterGroup) {
const { size, anchor, location } = group.icon!;
const point = this._project(location ?? group.center);
const [left, top] = anchor
? [point.x - anchor[0], point.y - anchor[1]]
: [point.x - size[0] / 2, point.y - size[1] / 2];
return { left, top, right: left + size[0], bottom: top + size[1] };
}
private _mergeOverlappingBubbles(): void {
for (;;) {
const bubbles = this._clusterGroups.filter((group) => group.icon);
const rects = bubbles.map((group) => this._iconRect(group));
let pair: [ClusterGroup, ClusterGroup] | undefined;
for (let i = 0; i < bubbles.length && !pair; i++) {
for (let j = i + 1; j < bubbles.length && !pair; j++) {
const a = rects[i];
const b = rects[j];
if (
a.left < b.right &&
b.left < a.right &&
a.top < b.bottom &&
b.top < a.bottom
) {
pair = [bubbles[i], bubbles[j]];
}
}
}
if (!pair) {
return;
}
const members = [...pair[0].members, ...pair[1].members];
// Two zones cannot share a pinned bubble
const keys = [pair[0].key, pair[1].key].filter(
(key) => key !== undefined
);
const merged: ClusterGroup = {
members,
key: keys.length === 1 ? keys[0] : undefined,
center: centerOf(members),
};
merged.icon = this._buildIcon(merged);
this._clusterGroups = this._clusterGroups
.filter((group) => !pair!.includes(group))
.concat(merged);
}
}
}
+5 -1
View File
@@ -179,7 +179,11 @@ export interface MapClusterOptions {
groupKey?(marker: MapMarkerHandle): string | undefined;
groupRadius?: number;
/** Builds a cluster's element; called when its members change and on refreshClusters() */
iconBuilder(members: MapMarkerHandle[], location: MapLatLng): MapClusterIcon;
iconBuilder(
members: MapMarkerHandle[],
location: MapLatLng,
key?: string
): MapClusterIcon;
}
export interface MapEngine {
+23
View File
@@ -0,0 +1,23 @@
// Character ranges for Hebrew and Arabic
const rtlRange = "\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC";
// Character ranges for ASCII and Latin characters, explicit LTR marks, etc.
const ltrRange =
"A-Za-z\u00C0-\u00D6\u00D8-\u00F6" +
"\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF\u200E\u2C00-\uFB1C" +
"\uFE00-\uFE6F\uFEFD-\uFFFF";
/* eslint-disable no-misleading-character-class */
const rtl = new RegExp(`^[^${ltrRange}]*[${rtlRange}]`);
const ltr = new RegExp(`^[^${rtlRange}]*[${ltrRange}]`);
/* eslint-enable no-misleading-character-class */
/**
* Detect the direction of text: left-to-right, right-to-left, or neutral
*
* @param {string} value
* @returns {'rtl'|'ltr'|'neutral'}
*/
export function estimateDirection(value: string) {
const source = String(value || "");
return rtl.test(source) ? "rtl" : ltr.test(source) ? "ltr" : "neutral";
}
+4
View File
@@ -1126,6 +1126,10 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
},
line: {
lineStyle: { width: 1.5 },
// At this size the symbols are invisible, but drawing one per data point
// makes every tooltip move repaint hundreds of them. ECharts still draws
// the symbol of the hovered point.
showSymbol: false,
symbolSize: 1,
symbol: "circle",
smooth: false,
+33 -101
View File
@@ -32,11 +32,8 @@ import { internationalizationContext } from "../../data/context";
import type { FrontendLocaleData } from "../../data/translation";
import { haStyleScrollbar } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer";
import "../animation/ha-fade-in";
import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox";
import "../skeleton/ha-skeleton-icon";
import "../skeleton/ha-skeleton-text";
import "../ha-svg-icon";
import "../input/ha-input-search";
import { filterData, sortData } from "./sort-filter";
@@ -114,25 +111,6 @@ export type SortableColumnContainer = Record<string, ClonedDataTableColumnData>;
const UNDEFINED_GROUP_KEY = "zzzzz_undefined";
const AUTO_FOCUS_ALLOWED_ACTIVE_TAGS = ["BODY", "HTML", "HOME-ASSISTANT"];
// Default row height, used to fill the viewport with skeleton rows.
const ROW_HEIGHT = 52;
const cellClasses = (column: DataTableColumnData) => ({
"mdc-data-table__cell--flex": column.type === "flex",
"mdc-data-table__cell--numeric": column.type === "numeric",
"mdc-data-table__cell--icon": column.type === "icon",
"mdc-data-table__cell--icon-button": column.type === "icon-button",
"mdc-data-table__cell--overflow-menu": column.type === "overflow-menu",
"mdc-data-table__cell--overflow": column.type === "overflow",
forceLTR: Boolean(column.forceLTR),
});
const cellStyles = (column: DataTableColumnData) => ({
minWidth: column.minWidth,
maxWidth: column.maxWidth,
flex: column.flex || 1,
});
@customElement("ha-data-table")
export class HaDataTable extends LitElement {
@state()
@@ -542,79 +520,20 @@ export class HaDataTable extends LitElement {
</div>
${
!this._filteredData?.length
? this.loading ||
!this._filteredData ||
(this.data.length && !this._filteredDataSourceLength)
? html`
<div class="mdc-data-table__content" role="row">
<ha-fade-in .duration=${300} easing="ease-in">
<div role="cell">
<div
role="progressbar"
aria-label=${
this._i18n?.localize?.("ui.common.loading") ||
? html`
<div class="mdc-data-table__content">
<div class="mdc-data-table__row" role="row">
<div
class="mdc-data-table__cell grows center"
role="cell"
>
${
this.loading ||
!this._filteredData ||
(this.data.length && !this._filteredDataSourceLength)
? this._i18n?.localize?.("ui.common.loading") ||
"Loading"
}
>
${Array.from(
{
length: this.autoHeight
? 1
: Math.ceil(window.innerHeight / ROW_HEIGHT),
},
() => html`
<div class="mdc-data-table__row">
${
this.selectable
? html`<div
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
></div>`
: nothing
}
${Object.entries(columns).map(
([key, column]) =>
(this.narrow &&
!column.main &&
!column.showNarrow) ||
!this._isColumnVisible(key, column)
? nothing
: html`
<div
class="mdc-data-table__cell ${classMap(
cellClasses(column)
)}"
style=${styleMap(cellStyles(column))}
>
${
column.type === "icon"
? html`<ha-skeleton-icon></ha-skeleton-icon>`
: column.type ===
"icon-button" ||
column.type ===
"overflow-menu"
? nothing
: html`<ha-skeleton-text></ha-skeleton-text>`
}
</div>
`
)}
</div>
`
)}
</div>
</div>
</ha-fade-in>
</div>
`
: html`
<div class="mdc-data-table__content">
<div class="mdc-data-table__row" role="row">
<div
class="mdc-data-table__cell grows center"
role="cell"
>
${
this.data.length
: this.data.length
? this._i18n?.localize?.(
"ui.components.data-table.no_match_filter"
) || "No rows matching current filters"
@@ -623,11 +542,11 @@ export class HaDataTable extends LitElement {
"ui.components.data-table.no-data"
) ||
"No data"
}
</div>
}
</div>
</div>
`
</div>
`
: html`
<lit-virtualizer
scroller
@@ -721,8 +640,22 @@ export class HaDataTable extends LitElement {
@mouseover=${this._setTitle}
@focus=${this._setTitle}
role=${column.main ? "rowheader" : "cell"}
class="mdc-data-table__cell ${classMap(cellClasses(column))}"
style=${styleMap(cellStyles(column))}
class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--flex": column.type === "flex",
"mdc-data-table__cell--numeric": column.type === "numeric",
"mdc-data-table__cell--icon": column.type === "icon",
"mdc-data-table__cell--icon-button":
column.type === "icon-button",
"mdc-data-table__cell--overflow-menu":
column.type === "overflow-menu",
"mdc-data-table__cell--overflow": column.type === "overflow",
forceLTR: Boolean(column.forceLTR),
})}"
style=${styleMap({
minWidth: column.minWidth,
maxWidth: column.maxWidth,
flex: column.flex || 1,
})}
>
${
column.template
@@ -1397,8 +1330,7 @@ export class HaDataTable extends LitElement {
.mdc-data-table__cell--icon:first-child ha-svg-icon,
.mdc-data-table__cell--icon:first-child ha-state-icon,
.mdc-data-table__cell--icon:first-child ha-domain-icon,
.mdc-data-table__cell--icon:first-child ha-service-icon,
.mdc-data-table__cell--icon:first-child ha-skeleton-icon {
.mdc-data-table__cell--icon:first-child ha-service-icon {
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
@@ -188,7 +188,9 @@ export class HaDateRangePicker extends LitElement {
protected render(): TemplateResult {
return html`
<div class="container">
<div class="date-range-inputs">${this._renderField()}</div>
<div class="date-range-inputs" part="range-input">
${this._renderField()}
</div>
${
this._pickerWrapperOpen || this._opened
? this._openedNarrow
+5
View File
@@ -8,6 +8,7 @@ import {
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { estimateDirection } from "../common/util/estimate-direction";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../common/translations/localize";
import { fireEvent } from "../common/dom/fire_event";
@@ -48,6 +49,7 @@ class HaAlert extends LitElement {
<div
class="issue-type ${classMap({
[this.alertType]: true,
ltr: estimateDirection(this.title) !== "rtl",
})}"
role="alert"
>
@@ -175,6 +177,9 @@ class HaAlert extends LitElement {
margin: 0;
padding-inline-start: 20px;
}
.ltr {
direction: ltr;
}
`;
}
+1 -1
View File
@@ -844,7 +844,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre>
progress.continueConversation =
event.data.intent_output.continue_conversation;
const response =
event.data.intent_output.response.speech?.plain.speech;
event.data.intent_output.response.speech.plain?.speech;
if (!response) {
return;
}
@@ -4,6 +4,7 @@ import {
mdiDragHorizontalVariant,
mdiPencil,
} from "@mdi/js";
import deepClone from "deep-clone-simple";
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -217,6 +218,9 @@ export class HaObjectSelector extends LitElement {
name: key,
selector: field.selector,
required: field.required ?? false,
...("default" in field
? { default: field.default as HaFormSchema["default"] }
: {}),
}));
});
@@ -237,13 +241,21 @@ export class HaObjectSelector extends LitElement {
ev.stopPropagation();
const schema = this._schema(this.selector);
const data = {
...computeInitialHaFormData(schema, {
skipUnsupportedSelectors: true,
}),
...Object.fromEntries(
schema
.filter((field) => "default" in field)
.map((field) => [field.name, deepClone(field.default)])
),
};
const newItem = await showFormDialog(this, {
title: this.hass.localize("ui.common.add"),
schema,
data: computeInitialHaFormData(schema, {
skipUnsupportedSelectors: true,
}),
data,
computeLabel: this._computeLabel,
computeHelper: this._computeHelper,
submitText: this.hass.localize("ui.common.add"),
+14 -16
View File
@@ -59,22 +59,20 @@ export class HaSTTPicker extends LitElement {
value = NONE;
}
const options: HaSelectOption[] = this._engines
.filter((engine) => !engine.deprecated || engine.engine_id !== value)
.map((engine) => {
let label: string;
if (engine.engine_id.includes(".")) {
const stateObj = this.hass.states[engine.engine_id];
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else {
label = engine.name || engine.engine_id;
}
return {
value: engine.engine_id,
label,
disabled: engine.supported_languages?.length === 0,
};
});
const options: HaSelectOption[] = this._engines.map((engine) => {
let label: string;
if (engine.engine_id.includes(".")) {
const stateObj = this.hass.states[engine.engine_id];
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else {
label = engine.name || engine.engine_id;
}
return {
value: engine.engine_id,
label,
disabled: engine.supported_languages?.length === 0,
};
});
if (this.required || value === NONE) {
options.unshift({
+30 -8
View File
@@ -31,7 +31,10 @@ import type {
MapEditableMarkerHandle,
MapEditingSupport,
} from "../../common/map/map-engine";
import { circleBoundsPoints } from "../../common/map/map-engine";
import {
circleBoundsPoints,
distanceMeters,
} from "../../common/map/map-engine";
import { editableCircleStyles } from "../../common/map/editable-circle";
import { entityMapColor, zoneColor } from "../../common/map/entity-map-colors";
import {
@@ -311,6 +314,8 @@ export class HaMap extends ReactiveElement {
private _zonePositions: Record<string, MapLatLng> = {};
private _zoneRadii: Record<string, number> = {};
@property({ attribute: "theme-mode", type: String })
public themeMode: ThemeMode = "auto";
@@ -1133,6 +1138,7 @@ export class HaMap extends ReactiveElement {
// A person's state is "home" for the home zone, the zone name otherwise
const zoneByState: Record<string, string> = {};
this._zonePositions = {};
this._zoneRadii = {};
for (const entity of this.entities) {
const stateObj = states[getEntityId(entity)];
// A zone that is not drawn cannot anchor a bubble either
@@ -1154,6 +1160,9 @@ export class HaMap extends ReactiveElement {
stateObj.attributes.latitude,
stateObj.attributes.longitude,
];
if (typeof stateObj.attributes.radius === "number") {
this._zoneRadii[stateObj.entity_id] = stateObj.attributes.radius;
}
}
}
}
@@ -1301,7 +1310,7 @@ export class HaMap extends ReactiveElement {
zoneId: ["person", "device_tracker"].includes(
computeStateDomain(stateObj)
)
? zoneByState[stateObj.state]
? (zoneByState[stateObj.state] ?? this._zoneContaining(position))
: undefined,
};
@@ -1339,9 +1348,26 @@ export class HaMap extends ReactiveElement {
);
}
private _zoneContaining(position: MapLatLng): string | undefined {
let found: string | undefined;
let foundRadius = Infinity;
for (const [zoneId, radius] of Object.entries(this._zoneRadii)) {
if (
radius < foundRadius &&
distanceMeters(position, this._zonePositions[zoneId]) <= radius
) {
found = zoneId;
foundRadius = radius;
}
}
return found;
}
// Renders a marker cluster as a bubble of its members' avatars
private _createClusterBubble = (
members: MapMarkerHandle[]
members: MapMarkerHandle[],
_location: MapLatLng,
zoneId?: string
): MapClusterIcon => {
const data = members.map((member) => member.clusterData as ClusterData);
const shown = data.slice(0, CLUSTER_MAX_AVATARS);
@@ -1387,12 +1413,8 @@ export class HaMap extends ReactiveElement {
}
// A cluster of one zone's occupants attaches to that zone's marker
const zoneId = data[0]?.zoneId;
const zonePosition = zoneId ? this._zonePositions[zoneId] : undefined;
const atZone =
!!zoneId &&
!!zonePosition &&
data.every((member) => member?.zoneId === zoneId);
const atZone = !!zonePosition;
let height = CLUSTER_AVATAR_SIZE + 2 * CLUSTER_BUBBLE_PADDING;
let root: HTMLElement = bubble;
+57 -1
View File
@@ -16,6 +16,7 @@ import {
} from "@mdi/js";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import type { PropertyValues } from "lit";
import memoizeOne from "memoize-one";
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
@@ -74,6 +75,9 @@ export class HatScriptGraph extends LitElement {
@property({ attribute: false }) public selected?: string;
/** Accessible name per node path, from `buildTraceLabels`. */
@property({ attribute: false }) public labels?: Record<string, string>;
@query("hat-graph-node[active], hat-graph-branch[active]")
private _activeNode?: HTMLElement;
@@ -106,6 +110,9 @@ export class HatScriptGraph extends LitElement {
?active=${this.selected === path}
.notEnabled=${node.disabled}
.error=${node.error}
role="img"
aria-label=${ifDefined(this.labels?.[path])}
aria-current=${ifDefined(this.selected === path || undefined)}
tabindex=${hasTrace ? "0" : "-1"}
>
<ha-trigger-icon
@@ -153,6 +160,10 @@ export class HatScriptGraph extends LitElement {
?track=${track}
?active=${this.selected === path}
.notEnabled=${node.disabled}
role="group"
aria-label=${ifDefined(this.labels?.[path])}
aria-current=${ifDefined(this.selected === path || undefined)}
aria-disabled=${ifDefined(node.disabled || undefined)}
>
<hat-graph-node
.graphStart=${graphStart}
@@ -164,6 +175,7 @@ export class HatScriptGraph extends LitElement {
.error=${node.error}
slot="head"
nofocus
aria-hidden="true"
></hat-graph-node>
${node.branches.slice(0, -1).map(
@@ -187,6 +199,9 @@ export class HatScriptGraph extends LitElement {
?track=${branch.hasTrace}
?active=${this.selected === branch.path}
.notEnabled=${branch.disabled}
role="img"
aria-label=${ifDefined(this.labels?.[branch.path])}
aria-current=${ifDefined(this.selected === branch.path || undefined)}
></hat-graph-node>
${branch.children.map((action) => this._renderActionNode(action))}
</div>
@@ -196,7 +211,10 @@ export class HatScriptGraph extends LitElement {
?track=${defaultBranch.hasTrace}
?unfinished=${defaultBranch.unfinished}
>
<hat-graph-spacer ?track=${defaultBranch.hasTrace}></hat-graph-spacer>
<hat-graph-spacer
aria-hidden="true"
?track=${defaultBranch.hasTrace}
></hat-graph-spacer>
${defaultBranch.children.map((action) =>
this._renderActionNode(action)
)}
@@ -215,6 +233,10 @@ export class HatScriptGraph extends LitElement {
?track=${track}
?active=${this.selected === path}
.notEnabled=${node.disabled}
role="group"
aria-label=${ifDefined(this.labels?.[path])}
aria-current=${ifDefined(this.selected === path || undefined)}
aria-disabled=${ifDefined(node.disabled || undefined)}
>
<hat-graph-node
.graphStart=${graphStart}
@@ -226,6 +248,7 @@ export class HatScriptGraph extends LitElement {
.error=${node.error}
slot="head"
nofocus
aria-hidden="true"
></hat-graph-node>
${
config.else
@@ -240,12 +263,14 @@ export class HatScriptGraph extends LitElement {
?active=${this.selected === path}
.notEnabled=${elseBranch.disabled}
nofocus
aria-hidden="true"
></hat-graph-node
>${elseBranch.children.map((action) =>
this._renderActionNode(action)
)}
</div>`
: html`<hat-graph-spacer
aria-hidden="true"
?track=${elseBranch.hasTrace}
></hat-graph-spacer>`
}
@@ -260,6 +285,7 @@ export class HatScriptGraph extends LitElement {
?active=${this.selected === path}
.notEnabled=${thenBranch.disabled}
nofocus
aria-hidden="true"
></hat-graph-node>
${thenBranch.children.map((action) => this._renderActionNode(action))}
</div>
@@ -281,6 +307,10 @@ export class HatScriptGraph extends LitElement {
?track=${track}
?active=${this.selected === path}
.notEnabled=${model.disabled}
role="group"
aria-label=${ifDefined(this.labels?.[path])}
aria-current=${ifDefined(this.selected === path || undefined)}
aria-disabled=${ifDefined(model.disabled || undefined)}
tabindex=${hasTrace ? "0" : "-1"}
short
>
@@ -293,6 +323,7 @@ export class HatScriptGraph extends LitElement {
.error=${model.error}
?building-block=${CONDITION_BUILDING_BLOCKS.includes(condition)}
nofocus
aria-hidden="true"
>
<ha-condition-icon
slot="icon"
@@ -308,6 +339,7 @@ export class HatScriptGraph extends LitElement {
<hat-graph-node
.iconPath=${mdiClose}
nofocus
aria-hidden="true"
?track=${failed}
?active=${this.selected === path}
.notEnabled=${model.disabled}
@@ -329,6 +361,10 @@ export class HatScriptGraph extends LitElement {
?track=${track}
?active=${this.selected === path}
.notEnabled=${model.disabled}
role="group"
aria-label=${ifDefined(this.labels?.[path])}
aria-current=${ifDefined(this.selected === path || undefined)}
aria-disabled=${ifDefined(model.disabled || undefined)}
>
<hat-graph-node
.graphStart=${graphStart}
@@ -341,6 +377,7 @@ export class HatScriptGraph extends LitElement {
.badge=${model.badge}
slot="head"
nofocus
aria-hidden="true"
></hat-graph-node>
<div
class="repeat-sequence"
@@ -372,6 +409,9 @@ export class HatScriptGraph extends LitElement {
?active=${this.selected === path}
.notEnabled=${model.disabled}
.error=${model.error}
role="img"
aria-label=${ifDefined(this.labels?.[path])}
aria-current=${ifDefined(this.selected === path || undefined)}
tabindex=${model.hasTrace ? "0" : "-1"}
>
${
@@ -405,6 +445,9 @@ export class HatScriptGraph extends LitElement {
?active=${this.selected === path}
.notEnabled=${model.disabled}
.error=${model.error}
role="img"
aria-label=${ifDefined(this.labels?.[path])}
aria-current=${ifDefined(this.selected === path || undefined)}
tabindex=${model.hasTrace ? "0" : "-1"}
></hat-graph-node>
`;
@@ -423,6 +466,10 @@ export class HatScriptGraph extends LitElement {
?track=${track}
?active=${this.selected === path}
.notEnabled=${model.disabled}
role="group"
aria-label=${ifDefined(this.labels?.[path])}
aria-current=${ifDefined(this.selected === path || undefined)}
aria-disabled=${ifDefined(model.disabled || undefined)}
>
<div
class="graph-container"
@@ -439,6 +486,7 @@ export class HatScriptGraph extends LitElement {
.error=${model.error}
slot="head"
nofocus
aria-hidden="true"
></hat-graph-node>
${branch.children.map((action) => this._renderActionNode(action))}
</div>
@@ -458,6 +506,10 @@ export class HatScriptGraph extends LitElement {
?track=${track}
?active=${this.selected === path}
.notEnabled=${model.disabled}
role="group"
aria-label=${ifDefined(this.labels?.[path])}
aria-current=${ifDefined(this.selected === path || undefined)}
aria-disabled=${ifDefined(model.disabled || undefined)}
>
<hat-graph-node
.graphStart=${graphStart}
@@ -469,6 +521,7 @@ export class HatScriptGraph extends LitElement {
.error=${model.error}
slot="head"
nofocus
aria-hidden="true"
></hat-graph-node>
${model.branches.map(
(branch) =>
@@ -496,6 +549,9 @@ export class HatScriptGraph extends LitElement {
?active=${this.selected === path}
.error=${model.error}
.notEnabled=${model.disabled}
role="img"
aria-label=${ifDefined(this.labels?.[path])}
aria-current=${ifDefined(this.selected === path || undefined)}
></hat-graph-node>
`;
}
+5 -1
View File
@@ -119,7 +119,11 @@ class HaUserPicker extends LitElement {
private _getItems = () => this._getUsers(this.users);
protected render(): TemplateResult {
protected render(): TemplateResult | typeof nothing {
if (!this.users) {
return nothing;
}
const placeholder =
this.placeholder ?? this.hass.localize("ui.components.user-picker.user");
+2 -2
View File
@@ -2,7 +2,7 @@ import type { HomeAssistant } from "../types";
export interface AlexaEntity {
entity_id: string;
display_categories: string[];
display_categories: string[] | null;
interfaces: string[];
}
@@ -10,7 +10,7 @@ export const fetchCloudAlexaEntities = (hass: HomeAssistant) =>
hass.callWS<AlexaEntity[]>({ type: "cloud/alexa/entities" });
export const fetchCloudAlexaEntity = (hass: HomeAssistant, entity_id: string) =>
hass.callWS<AlexaEntity>({
hass.callWS<undefined>({
type: "cloud/alexa/entities/get",
entity_id,
});
+4 -2
View File
@@ -1,10 +1,11 @@
import type { HomeAssistant } from "../types";
export interface ApplicationCredentialsDomainConfig {
description_placeholders: Record<string, string>;
description_placeholders?: Record<string, string>;
}
export interface ApplicationCredentialsConfig {
domains: string[];
integrations: Record<string, ApplicationCredentialsDomainConfig>;
}
@@ -17,7 +18,8 @@ export interface ApplicationCredential {
domain: string;
client_id: string;
client_secret: string;
name: string;
name?: string;
auth_domain?: string;
}
export const fetchApplicationCredentialsConfig = async (hass: HomeAssistant) =>
+43 -10
View File
@@ -9,7 +9,7 @@ export interface AssistPipeline {
language: string;
conversation_engine: string;
conversation_language: string | null;
prefer_local_intents?: boolean;
prefer_local_intents: boolean;
stt_engine: string | null;
stt_language: string | null;
tts_engine: string | null;
@@ -17,11 +17,12 @@ export interface AssistPipeline {
tts_voice: string | null;
wake_word_entity: string | null;
wake_word_id: string | null;
user_id?: string | null;
}
export interface AssistDevice {
device_id: string;
pipeline_entity: string;
pipeline_entity: string | null;
}
export interface AssistPipelineMutableParams {
@@ -37,6 +38,7 @@ export interface AssistPipelineMutableParams {
tts_voice: string | null;
wake_word_entity: string | null;
wake_word_id: string | null;
user_id?: string | null;
}
export interface AssistRunListing {
@@ -54,6 +56,7 @@ interface PipelineRunStartEvent extends PipelineEventBase {
pipeline: string;
language: string;
conversation_id: string;
satellite_id?: string;
runner_data: {
stt_binary_handler_id: number | null;
timeout: number;
@@ -62,12 +65,13 @@ interface PipelineRunStartEvent extends PipelineEventBase {
token: string;
url: string;
mime_type: string;
stream_response: boolean;
};
};
}
interface PipelineRunEndEvent extends PipelineEventBase {
type: "run-end";
data: Record<string, never>;
data: null;
}
interface PipelineErrorEvent extends PipelineEventBase {
@@ -81,14 +85,21 @@ interface PipelineErrorEvent extends PipelineEventBase {
interface PipelineWakeWordStartEvent extends PipelineEventBase {
type: "wake_word-start";
data: {
engine: string;
metadata: SpeechMetadata;
entity_id: string;
metadata: Omit<SpeechMetadata, "language">;
timeout: number;
};
}
interface PipelineWakeWordEndEvent extends PipelineEventBase {
type: "wake_word-end";
data: { wake_word_output: { ww_id: string; timestamp: number } };
data: {
wake_word_output: {
wake_word_id?: string;
wake_word_phrase?: string;
timestamp?: number | null;
};
};
}
interface PipelineSTTStartEvent extends PipelineEventBase {
@@ -96,8 +107,23 @@ interface PipelineSTTStartEvent extends PipelineEventBase {
data: {
engine: string;
metadata: SpeechMetadata;
audio_processing: {
requires_external_vad: boolean;
prefers_auto_gain_enabled: boolean;
prefers_noise_reduction_enabled: boolean;
};
};
}
interface PipelineSTTVADStartEvent extends PipelineEventBase {
type: "stt-vad-start";
data: { timestamp: number };
}
interface PipelineSTTVADEndEvent extends PipelineEventBase {
type: "stt-vad-end";
data: { timestamp: number };
}
interface PipelineSTTEndEvent extends PipelineEventBase {
type: "stt-end";
data: {
@@ -110,8 +136,11 @@ interface PipelineIntentStartEvent extends PipelineEventBase {
data: {
engine: string;
language: string;
prefer_local_intents: boolean;
intent_input: string;
conversation_id: string;
device_id: string | null;
satellite_id: string | null;
prefer_local_intents: boolean;
};
}
@@ -123,6 +152,7 @@ export interface ConversationChatLogAssistantDelta {
id: string;
tool_name: string;
tool_args: Record<string, unknown>;
external: boolean;
}[];
}
@@ -157,9 +187,10 @@ interface PipelineTTSStartEvent extends PipelineEventBase {
type: "tts-start";
data: {
engine: string;
language: string;
voice: string;
language: string | null;
voice: string | null;
tts_input: string;
acknowledge_override: boolean;
};
}
interface PipelineTTSEndEvent extends PipelineEventBase {
@@ -181,6 +212,8 @@ export type PipelineRunEvent =
| PipelineWakeWordStartEvent
| PipelineWakeWordEndEvent
| PipelineSTTStartEvent
| PipelineSTTVADStartEvent
| PipelineSTTVADEndEvent
| PipelineSTTEndEvent
| PipelineIntentStartEvent
| PipelineIntentProgressEvent
@@ -426,7 +459,7 @@ export const deleteAssistPipeline = (hass: HomeAssistant, pipelineId: string) =>
});
export const fetchAssistPipelineLanguages = (hass: HomeAssistant) =>
hass.callWS<{ languages: string[] }>({
hass.callWS<{ languages: string[] | null }>({
type: "assist_pipeline/language/list",
});
+3 -3
View File
@@ -8,7 +8,7 @@ export enum AssistSatelliteEntityFeature {
}
export interface WakeWordInterceptMessage {
wake_word_phrase: string;
wake_word_phrase: string | null;
}
export interface WakeWordOption {
@@ -21,8 +21,8 @@ export interface AssistSatelliteConfiguration {
active_wake_words: string[];
available_wake_words: WakeWordOption[];
max_active_wake_words: number;
pipeline_entity_id: string;
vad_entity_id: string;
pipeline_entity_id: string | null;
vad_entity_id: string | null;
}
export const interceptWakeWord = (
+7 -2
View File
@@ -6,13 +6,14 @@ export interface AuthUrlSearchParams {
client_id?: string;
redirect_uri?: string;
state?: string;
code_challenge?: string;
code_challenge_method?: string;
}
export interface AuthProvider {
name: string;
id: string;
type: string;
users?: Record<string, string>;
}
export interface Credential {
@@ -53,7 +54,9 @@ export const fetchAuthProviders = () =>
export const createLoginFlow = (
client_id: string | undefined,
redirect_uri: string | undefined,
handler: (string | null)[]
handler: (string | null)[],
code_challenge?: string,
code_challenge_method?: string
) =>
fetch("/auth/login_flow", {
method: "POST",
@@ -62,6 +65,8 @@ export const createLoginFlow = (
client_id,
handler,
redirect_uri,
...(code_challenge && { code_challenge }),
...(code_challenge_method && { code_challenge_method }),
}),
});
+20 -15
View File
@@ -47,7 +47,7 @@ export interface BackupConfig {
last_attempted_automatic_backup: string | null;
last_completed_automatic_backup: string | null;
next_automatic_backup: string | null;
next_automatic_backup_additional?: boolean;
next_automatic_backup_additional: boolean;
create_backup: {
agent_ids: string[];
include_addons: string[] | null;
@@ -60,7 +60,7 @@ export interface BackupConfig {
retention: Retention;
schedule: {
recurrence: BackupScheduleRecurrence;
time?: string | null;
time: string | null;
days: BackupDay[];
};
agents: BackupAgentsConfig;
@@ -79,9 +79,9 @@ export interface BackupMutableConfig {
};
retention?: Retention;
schedule?: {
recurrence: BackupScheduleRecurrence;
recurrence?: BackupScheduleRecurrence;
time?: string | null;
days?: BackupDay[] | null;
days?: BackupDay[];
};
agents?: BackupAgentsConfig;
}
@@ -114,21 +114,22 @@ export interface BackupContent {
date: string;
name: string;
agents: Record<string, BackupContentAgent>;
failed_agent_ids?: string[];
failed_addons?: AddonInfo[];
failed_folders?: string[];
extra_metadata?: {
failed_agent_ids: string[];
failed_addons: AddonInfo[];
failed_folders: string[];
extra_metadata: {
"supervisor.addon_update"?: string;
"supervisor.app_update"?: string;
[key: string]: string | boolean | undefined;
};
with_automatic_settings: boolean;
with_automatic_settings: boolean | null;
}
export interface BackupData {
addons: BackupAddon[];
database_included: boolean;
folders: string[];
homeassistant_version: string;
homeassistant_version: string | null;
homeassistant_included: boolean;
}
@@ -141,7 +142,7 @@ export interface BackupAddon {
export interface BackupContentExtended extends BackupContent, BackupData {}
export interface BackupInfo {
backups: BackupContent[];
backups: BackupContentExtended[];
agent_errors: Record<string, string>;
last_attempted_automatic_backup: string | null;
last_completed_automatic_backup: string | null;
@@ -152,6 +153,7 @@ export interface BackupInfo {
}
export interface BackupDetails {
agent_errors: Record<string, string>;
backup: BackupContentExtended;
}
@@ -232,7 +234,10 @@ export const fetchBackupAgentsInfo = (
type: "backup/agents/info",
});
export const deleteBackup = (hass: HomeAssistant, id: string): Promise<void> =>
export const deleteBackup = (
hass: HomeAssistant,
id: string
): Promise<{ agent_errors: Record<string, string> }> =>
hass.callWS({
type: "backup/delete",
backup_id: id,
@@ -241,7 +246,7 @@ export const deleteBackup = (hass: HomeAssistant, id: string): Promise<void> =>
export const generateBackup = (
hass: HomeAssistant,
params: GenerateBackupParams
): Promise<{ backup_id: string }> =>
): Promise<{ backup_job_id: string }> =>
hass.callWS({
type: "backup/generate",
...params,
@@ -249,7 +254,7 @@ export const generateBackup = (
export const generateBackupWithAutomaticSettings = (
hass: HomeAssistant
): Promise<void> =>
): Promise<{ backup_job_id: string }> =>
hass.callWS({
type: "backup/generate_with_automatic_settings",
});
@@ -257,7 +262,7 @@ export const generateBackupWithAutomaticSettings = (
export const restoreBackup = (
hass: HomeAssistant,
params: RestoreBackupParams
): Promise<{ backup_id: string }> =>
): Promise<void> =>
hass.callWS({
type: "backup/restore",
...params,
+16 -4
View File
@@ -1,7 +1,7 @@
import type { HomeAssistant } from "../types";
export type BackupManagerState =
"idle" | "create_backup" | "receive_backup" | "restore_backup";
"idle" | "create_backup" | "receive_backup" | "restore_backup" | "blocked";
export type CreateBackupStage =
| "addon_repositories"
@@ -39,7 +39,8 @@ export type RestoreBackupStage =
| "remove_delta_addons"
| "remove_delta_apps";
export type RestoreBackupState = "completed" | "failed" | "in_progress";
export type RestoreBackupState =
"completed" | "core_restart" | "failed" | "in_progress";
interface IdleEvent {
manager_state: "idle";
@@ -47,22 +48,29 @@ interface IdleEvent {
interface CreateBackupEvent {
manager_state: "create_backup";
reason: string | null;
stage: CreateBackupStage | null;
state: CreateBackupState;
}
interface ReceiveBackupEvent {
manager_state: "receive_backup";
reason: string | null;
stage: ReceiveBackupStage | null;
state: ReceiveBackupState;
}
interface RestoreBackupEvent {
manager_state: "restore_backup";
reason: string | null;
stage: RestoreBackupStage | null;
state: RestoreBackupState;
}
interface BlockedEvent {
manager_state: "blocked";
}
export interface UploadBackupEvent {
manager_state: BackupManagerState;
agent_id: string;
@@ -71,10 +79,14 @@ export interface UploadBackupEvent {
}
export type ManagerState =
"idle" | "create_backup" | "receive_backup" | "restore_backup";
"idle" | "create_backup" | "receive_backup" | "restore_backup" | "blocked";
export type ManagerStateEvent =
IdleEvent | CreateBackupEvent | ReceiveBackupEvent | RestoreBackupEvent;
| IdleEvent
| CreateBackupEvent
| ReceiveBackupEvent
| RestoreBackupEvent
| BlockedEvent;
export type BackupSubscriptionEvent = ManagerStateEvent | UploadBackupEvent;
+1 -1
View File
@@ -8,7 +8,7 @@ import type {
export interface BackupOnboardingInfo {
state: BackupManagerState;
last_action_event?: {
last_action_event: {
manager_state: BackupManagerState;
stage: RestoreBackupStage | null;
state: RestoreBackupState;
+2 -2
View File
@@ -9,14 +9,14 @@ import type { DataTableRowData } from "../components/data-table/ha-data-table";
export interface BluetoothDeviceData extends DataTableRowData {
address: string;
connectable: boolean;
manufacturer_data: Record<number, string>;
manufacturer_data: Record<string, string>;
name: string;
rssi: number;
service_data: Record<string, string>;
service_uuids: string[];
source: string;
time: number;
tx_power: number;
tx_power: number | null;
raw: string | null;
}
+2
View File
@@ -217,6 +217,8 @@ export interface CalendarEventApiData {
uid?: string | null;
recurrence_id?: string | null;
rrule?: string | null;
status?: "confirmed" | "tentative" | null;
all_day?: boolean;
}
export interface CalendarEventSubscription {
+3 -4
View File
@@ -16,11 +16,10 @@ export type StreamType = typeof STREAM_TYPE_HLS | typeof STREAM_TYPE_WEB_RTC;
export { CameraEntityFeature } from "./feature/camera_entity_feature";
interface CameraEntityAttributes extends HassEntityAttributeBase {
model_name: string;
model_name?: string;
access_token?: string;
brand: string;
motion_detection: boolean;
frontend_stream_type: string;
brand?: string;
motion_detection?: boolean;
}
export interface CameraEntity extends HassEntityBase {
+2 -1
View File
@@ -4,8 +4,9 @@ import type { Store } from "home-assistant-js-websocket/dist/store";
import { stringCompare } from "../common/string/compare";
import type { HomeAssistant } from "../types";
import { debounce } from "../common/util/debounce";
import type { RegistryEntry } from "./registry";
export interface CategoryRegistryEntry {
export interface CategoryRegistryEntry extends RegistryEntry {
category_id: string;
name: string;
icon: string | null;
+3 -3
View File
@@ -138,7 +138,7 @@ interface ChatLogIndexInitialStateEvent {
interface ChatLogCreatedEvent {
conversation_id: string;
event_type: ChatLogEventType.CREATED;
data: ChatLogWire;
data: { chat_log: ChatLogWire };
}
interface ChatLogUpdatedEvent {
@@ -150,7 +150,7 @@ interface ChatLogUpdatedEvent {
interface ChatLogDeletedEvent {
conversation_id: string;
event_type: ChatLogEventType.DELETED;
data: ChatLogWire;
data: Record<string, never>;
}
interface ChatLogContentAddedEvent {
@@ -215,7 +215,7 @@ export const subscribeChatLogIndex = (
chatLogs = event.data.map(processChatLog);
callback(chatLogs);
} else if (event.event_type === ChatLogEventType.CREATED) {
chatLogs = [...chatLogs, processChatLog(event.data)];
chatLogs = [...chatLogs, processChatLog(event.data.chat_log)];
callback(chatLogs);
} else if (event.event_type === ChatLogEventType.DELETED) {
chatLogs = chatLogs.filter(
+2 -3
View File
@@ -39,13 +39,12 @@ export type HvacAction =
export type ClimateEntity = HassEntityBase & {
attributes: HassEntityAttributeBase & {
hvac_mode: HvacMode;
hvac_modes: HvacMode[];
hvac_action?: HvacAction;
current_temperature: number;
current_temperature?: number;
min_temp: number;
max_temp: number;
temperature: number;
temperature?: number;
target_temp_step?: number;
target_temp_high?: number;
target_temp_low?: number;
+8 -9
View File
@@ -1,8 +1,6 @@
import type { EntityDomainFilter } from "../common/entity/entity_domain_filter";
import type { HomeAssistant } from "../types";
type StrictConnectionMode = "disabled" | "guard_page" | "drop_connection";
export interface CloudAutoLogin {
email: string;
failed: string | null;
@@ -27,10 +25,11 @@ export interface CloudPreferences {
alexa_enabled: boolean;
remote_enabled: boolean;
remote_allow_remote_enable: boolean;
strict_connection: StrictConnectionMode;
google_secure_devices_pin: string | undefined;
google_secure_devices_pin: string | null;
cloudhooks: Record<string, CloudWebhook>;
alexa_default_expose: string[] | null;
alexa_report_state: boolean;
google_default_expose: string[] | null;
google_report_state: boolean;
tts_default_voice: [string, string];
cloud_ice_servers_enabled: boolean;
@@ -43,18 +42,19 @@ export type RemoteCertificateStatus =
export interface CloudStatusLoggedIn {
logged_in: true;
auto_login: null;
cloud: "disconnected" | "connecting" | "connected";
cloud_last_disconnect_reason: { clean: boolean; reason: string } | null;
email: string;
google_registered: boolean;
google_local_connected: boolean;
google_entities: EntityDomainFilter;
google_domains: string[];
alexa_registered: boolean;
alexa_entities: EntityDomainFilter;
prefs: CloudPreferences;
remote_domain: string | undefined;
remote_domain: string | null;
remote_connected: boolean;
remote_certificate: undefined | CertificateInformation;
remote_certificate: CertificateInformation | null;
remote_certificate_status: RemoteCertificateStatus | null;
http_use_ssl: boolean;
active_subscription: boolean;
@@ -115,7 +115,7 @@ export interface CloudLoginMFA extends CloudLoginBase {
export type CloudEvent =
| { type: "login" | "logout" | "auto_login_cancelled" }
| { type: "auto_login_failed"; translation_key: string };
| { type: "auto_login_failed"; translation_key: string | null };
export const cloudLogin = ({
hass,
@@ -196,7 +196,6 @@ export const updateCloudPref = (
google_secure_devices_pin?: CloudPreferences["google_secure_devices_pin"];
tts_default_voice?: CloudPreferences["tts_default_voice"];
remote_allow_remote_enable?: CloudPreferences["remote_allow_remote_enable"];
strict_connection?: CloudPreferences["strict_connection"];
cloud_ice_servers_enabled?: CloudPreferences["cloud_ice_servers_enabled"];
}
) =>
+3 -3
View File
@@ -14,7 +14,8 @@ export interface ConfigEntry {
| "setup_retry"
| "not_loaded"
| "failed_unload"
| "setup_in_progress";
| "setup_in_progress"
| "unload_in_progress";
supports_options: boolean;
supports_remove_device: boolean;
supports_unload: boolean;
@@ -34,7 +35,7 @@ export interface SubEntry {
subentry_id: string;
subentry_type: string;
title: string;
unique_id: string;
unique_id: string | null;
}
export const getSubEntries = (hass: HomeAssistant, entry_id: string) =>
@@ -102,7 +103,6 @@ export const subscribeConfigEntries = (
callbackFunction: (message: ConfigEntryUpdate[]) => void,
filters?: {
type?: IntegrationType[];
domain?: string;
}
): Promise<UnsubscribeFunc> => {
const params: any = {
+12 -7
View File
@@ -5,7 +5,6 @@ import type {
DataEntryFlowProgress,
DataEntryFlowStep,
} from "./data_entry_flow";
import type { IntegrationType } from "./integration";
import { domainToName } from "./integration";
export const DISCOVERY_SOURCES = [
@@ -75,9 +74,10 @@ export const ignoreConfigFlow = (
export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) =>
hass.callApi("DELETE", `config/config_entries/flow/${flowId}`);
// The backend indexes a single key, and only these two exist.
export const getConfigFlowHandlers = (
hass: HomeAssistant,
type?: IntegrationType[]
type?: "helper" | "integration"
) =>
hass.callApi<string[]>(
"GET",
@@ -91,11 +91,16 @@ export const fetchConfigFlowInProgress = (
type: "config_entries/flow/progress",
});
export interface ConfigFlowInProgressMessage {
type: null | "added" | "removed";
flow_id: string;
flow: DataEntryFlowProgress;
}
export type ConfigFlowInProgressMessage =
| {
type: "removed";
flow_id: string;
}
| {
type: null | "added";
flow_id: string;
flow: DataEntryFlowProgress;
};
export const subscribeConfigFlowInProgress = (
hass: HomeAssistant,
+23 -9
View File
@@ -6,20 +6,28 @@ export enum ConversationEntityFeature {
}
interface IntentTarget {
type: "area" | "device" | "entity" | "domain" | "device_class" | "custom";
type:
| "area"
| "floor"
| "device"
| "entity"
| "domain"
| "device_class"
| "custom";
name: string;
id: string | null;
}
interface IntentResultBase {
language: string;
speech: Record<"plain" | "ssml", { extra_data: any; speech: string }> | null;
speech: Partial<
Record<"plain" | "ssml", { extra_data: any; speech: string }>
>;
}
interface IntentResultActionDone extends IntentResultBase {
response_type: "action_done";
data: {
targets: IntentTarget[];
success: IntentTarget[];
failed: IntentTarget[];
};
@@ -28,7 +36,6 @@ interface IntentResultActionDone extends IntentResultBase {
interface IntentResultQueryAnswer extends IntentResultBase {
response_type: "query_answer";
data: {
targets: IntentTarget[];
success: IntentTarget[];
failed: IntentTarget[];
};
@@ -56,17 +63,24 @@ export interface Agent {
}
export interface AssistDebugResult {
intent: {
match: boolean;
sentence_template: string;
source?: "trigger" | "custom" | "builtin";
file?: string | null;
intent?: {
name: string;
};
entities: Record<
slots?: Record<string, unknown>;
details?: Record<
string,
{
name: string;
value: string;
text: string;
value: unknown;
text: string | null;
}
>;
targets?: Record<string, { matched: boolean }>;
unmatched_slots?: Record<string, string | number>;
}
export interface AssistDebugResponse {
@@ -132,7 +146,7 @@ export const getLanguageScores = (
hass: HomeAssistant,
language?: string,
country?: string
): Promise<{ languages: LanguageScores; preferred_language: string | null }> =>
): Promise<{ languages: LanguageScores; preferred_language: string }> =>
hass.callWS({
type: "conversation/agent/homeassistant/language_scores",
language,
+2 -2
View File
@@ -6,8 +6,8 @@ export interface Counter {
icon?: string;
initial?: number;
restore?: boolean;
minimum?: number;
maximum?: number;
minimum?: number | null;
maximum?: number | null;
step?: number;
}
+1
View File
@@ -106,6 +106,7 @@ export function canStopTilt(stateObj: CoverEntity): boolean {
interface CoverEntityAttributes extends HassEntityAttributeBase {
current_position?: number;
current_tilt_position?: number;
supported_speeds?: string[];
}
export interface CoverEntity extends HassEntityBase {
+7 -7
View File
@@ -29,7 +29,7 @@ export interface DataEntryFlowProgress {
handler: string;
step_id: string;
context: {
title_placeholders: Record<string, string>;
title_placeholders?: Record<string, string>;
[key: string]: any;
};
}
@@ -40,7 +40,7 @@ export interface DataEntryFlowStepForm {
handler: string;
step_id: string;
data_schema: HaFormSchema[];
errors: Record<string, string>;
errors: Record<string, string> | null;
description_placeholders?: Record<string, string>;
last_step: boolean | null;
preview?: string;
@@ -61,13 +61,13 @@ export interface DataEntryFlowStepCreateEntry<
TResult extends ConfigEntry | RepairsIssue = ConfigEntry,
> {
type: "create_entry";
version: number;
version?: number;
flow_id: string;
next_flow?: [FlowType, string]; // [flow_type, flow_id]
handler: string;
title: string;
result?: TResult;
description: string;
title?: string;
result?: TResult | null;
description: string | null;
description_placeholders?: Record<string, string>;
translation_domain?: string;
}
@@ -81,7 +81,7 @@ export interface DataEntryFlowStepAbort<
reason: string;
description_placeholders?: Record<string, string>;
translation_domain?: string;
result?: TResult;
result?: TResult | null;
next_flow?: [FlowType, string]; // [flow_type, flow_id]
}
+2 -2
View File
@@ -242,8 +242,8 @@ export interface EnergyInfo {
export interface EnergyValidationIssue {
type: string;
affected_entities: [string, unknown][];
translation_placeholders: Record<string, string>;
affected_entities: [string, string | number | null][];
translation_placeholders: Record<string, string> | null;
}
export interface EnergyPreferencesValidation {
+5 -5
View File
@@ -37,7 +37,7 @@ export interface EntityRegistryDisplayEntryResponse {
ec?: number;
en?: string;
ic?: string;
pl?: string;
pl: string;
tk?: string;
hb?: boolean;
dp?: number;
@@ -58,7 +58,7 @@ export interface EntityRegistryEntry extends RegistryEntry {
area_id: string | null;
labels: string[];
disabled_by: "user" | "device" | "integration" | "config_entry" | null;
hidden_by: Exclude<EntityRegistryEntry["disabled_by"], "config_entry">;
hidden_by: "integration" | "user" | null;
entity_category: EntityCategory | null;
has_entity_name: boolean;
original_name?: string;
@@ -69,7 +69,7 @@ export interface EntityRegistryEntry extends RegistryEntry {
}
export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
capabilities: Record<string, unknown>;
capabilities: Record<string, unknown> | null;
original_icon?: string;
device_class?: string;
original_device_class?: string;
@@ -196,7 +196,7 @@ export interface EntityRegistryEntryUpdateParams {
device_class?: string | null;
area_id?: string | null;
disabled_by?: string | null;
hidden_by: string | null;
hidden_by?: string | null;
new_entity_id?: string;
options_domain?: string;
options?:
@@ -278,7 +278,7 @@ export const getExtendedEntityRegistryEntry = (
export const getExtendedEntityRegistryEntries = (
hass: HomeAssistant,
entityIds: string[]
): Promise<Record<string, ExtEntityRegistryEntry>> =>
): Promise<Record<string, ExtEntityRegistryEntry | null>> =>
hass.callWS({
type: "config/entity_registry/get_entries",
entity_ids: entityIds,
+1 -1
View File
@@ -1,7 +1,7 @@
import type { HomeAssistant } from "../types";
export interface ESPHomeEncryptionKey {
encryption_key: string;
encryption_key: string | null;
}
export const fetchESPHomeEncryptionKey = (
+1
View File
@@ -7,4 +7,5 @@ export enum CoverEntityFeature {
CLOSE_TILT = 32,
STOP_TILT = 64,
SET_TILT_POSITION = 128,
SPEED = 256,
}
@@ -19,4 +19,7 @@ export enum MediaPlayerEntityFeature {
BROWSE_MEDIA = 131072,
REPEAT_SET = 262144,
GROUPING = 524288,
MEDIA_ANNOUNCE = 1048576,
MEDIA_ENQUEUE = 2097152,
SEARCH_MEDIA = 4194304,
}
+1 -1
View File
@@ -35,7 +35,7 @@ export const updateFloorRegistryEntry = (
floorId: string,
updates: Partial<FloorRegistryEntryMutableParams>
) =>
hass.callWS<AreaRegistryEntry>({
hass.callWS<FloorRegistryEntry>({
type: "config/floor_registry/update",
floor_id: floorId,
...updates,
+1 -1
View File
@@ -4,7 +4,7 @@ export interface GoogleEntity {
entity_id: string;
traits: string[];
might_2fa: boolean;
disable_2fa?: boolean;
disable_2fa?: boolean | null;
}
export const fetchCloudGoogleEntities = (hass: HomeAssistant) =>
+5 -5
View File
@@ -80,7 +80,7 @@ export interface EntityHistoryState {
/** state */
s: string;
/** attributes */
a: Record<string, any>;
a?: Record<string, any>;
/** last_changed; if set, also applies to lu */
lc?: number;
/** last_updated */
@@ -384,7 +384,7 @@ const processTimelineEntity = (
? " ◂ "
: " ▸ ",
}) || current_state.entity_id
: computeStateNameFromEntityAttributes(entityId, first.a),
: computeStateNameFromEntityAttributes(entityId, first.a ?? {}),
entity_id: entityId,
data,
};
@@ -423,7 +423,7 @@ const processLineChartEntities = (
};
for (const attr of LINE_ATTRIBUTES_TO_KEEP) {
if (attr in state.a) {
if (state.a && attr in state.a) {
processedState.attributes![attr] = state.a[attr];
}
}
@@ -456,7 +456,7 @@ const processLineChartEntities = (
}) || entityId
: computeStateNameFromEntityAttributes(
entityId,
"friendly_name" in first.a ? first.a : {}
first.a && "friendly_name" in first.a ? first.a : {}
);
data.push({
@@ -603,7 +603,7 @@ export const computeHistory = (
if (isNumeric) {
unit =
currentState?.attributes.unit_of_measurement ||
numericStateFromHistory?.a.unit_of_measurement ||
numericStateFromHistory?.a?.unit_of_measurement ||
BLANK_UNIT;
} else {
unit = {
+1 -1
View File
@@ -75,7 +75,7 @@ export const updateImage = (
) =>
hass.callWS<Image>({
type: "image/update",
media_id: id,
image_id: id,
...updates,
});
+1
View File
@@ -9,6 +9,7 @@ export interface InputText {
max?: number;
pattern?: string;
mode?: "text" | "password";
unit_of_measurement?: string;
}
export interface InputTextMutableParams {
+23 -5
View File
@@ -37,7 +37,14 @@ export const getConfigPanelPath = (
?.url_path || integrationsWithPanel[domain];
export type IntegrationType =
"device" | "helper" | "hub" | "service" | "hardware" | "entity" | "system";
| "device"
| "helper"
| "hub"
| "service"
| "hardware"
| "entity"
| "system"
| "virtual";
export type DomainManifestLookup = Record<string, IntegrationManifest>;
@@ -46,7 +53,7 @@ export interface IntegrationManifest {
overwrites_built_in?: boolean;
domain: string;
name: string;
config_flow: boolean;
config_flow?: boolean;
documentation?: string;
issue_tracker?: string;
dependencies?: string[];
@@ -54,8 +61,18 @@ export interface IntegrationManifest {
codeowners?: string[];
requirements?: string[];
ssdp?: { manufacturer?: string; modelName?: string; st?: string }[];
zeroconf?: string[];
homekit?: { models: string[] };
zeroconf?: (
| string
| {
type: string;
macaddress?: string;
manufacturer?: string;
model?: string;
name?: string;
properties?: Record<string, string>;
}
)[];
homekit?: { models?: string[] };
integration_type?: IntegrationType;
loggers?: string[];
quality_scale?:
@@ -67,8 +84,9 @@ export interface IntegrationManifest {
| "internal"
| "legacy"
| "custom";
iot_class:
iot_class?:
| "assumed_state"
| "calculated"
| "cloud_polling"
| "cloud_push"
| "local_polling"
-3
View File
@@ -69,12 +69,9 @@ export const getLightCurrentModeRgbColor = (
interface LightEntityAttributes extends HassEntityAttributeBase {
min_color_temp_kelvin?: number;
max_color_temp_kelvin?: number;
min_mireds?: number;
max_mireds?: number;
brightness?: number;
xy_color?: [number, number];
hs_color?: [number, number];
color_temp?: number;
color_temp_kelvin?: number;
rgb_color?: [number, number, number];
rgbw_color?: [number, number, number, number];
+5 -5
View File
@@ -42,9 +42,9 @@ export interface MatterLockUser {
user_index: number | null;
user_name: string | null;
user_unique_id: number | null;
user_status: MatterLockUserStatus;
user_status: MatterLockUserStatus | "unknown";
user_type: MatterLockUserType;
credential_rule: MatterLockCredentialRule;
credential_rule: MatterLockCredentialRule | "unknown";
credentials: MatterLockCredentialRef[];
next_user_index: number | null;
}
@@ -64,9 +64,9 @@ export interface SetMatterLockUserParams {
export interface SetMatterLockCredentialParams {
credential_type: MatterLockCredentialType;
credential_data: string;
credential_index?: number | null;
user_index?: number | null;
user_status?: MatterLockUserStatus;
credential_index?: number;
user_index?: number;
user_status?: "occupied_enabled" | "occupied_disabled";
user_type?: MatterLockUserType;
}
+5 -8
View File
@@ -27,17 +27,17 @@ export interface MatterFabricData {
fabric_id: number;
vendor_id: number;
fabric_index: number;
fabric_label?: string;
vendor_name?: string;
fabric_label: string | null;
vendor_name: string | null;
}
export interface MatterNodeDiagnostics {
node_id: number;
network_type: NetworkType;
node_type: NodeType;
network_name?: string;
network_name: string | null;
ip_adresses: string[];
mac_address?: string;
mac_address: string | null;
available: boolean;
active_fabrics: MatterFabricData[];
active_fabric_index: number;
@@ -45,9 +45,6 @@ export interface MatterNodeDiagnostics {
export type MatterPingResult = Record<string, boolean>;
export type MatterTopologyNodeKind =
"matter" | "border_router" | "thread_unknown" | "wifi_ap";
export type MatterTopologyStrength =
"strong" | "medium" | "weak" | "none" | "unknown";
@@ -59,7 +56,7 @@ export interface MatterTopologyDirectionInfo {
export interface MatterNetworkTopologyNode {
id: string;
kind: MatterTopologyNodeKind;
kind: string;
network_type: string;
node_id?: number | null;
ha_device_id?: string | null;
+4
View File
@@ -48,10 +48,14 @@ interface MediaPlayerEntityAttributes extends HassEntityAttributeBase {
media_content_id?: string;
media_content_type?: string;
media_artist?: string;
media_album_name?: string;
media_album_artist?: string;
media_track?: number;
media_playlist?: string;
media_series_title?: string;
media_season?: any;
media_episode?: any;
app_id?: string;
app_name?: string;
media_position_updated_at?: string | number | Date;
media_duration?: number;
+7 -3
View File
@@ -4,13 +4,17 @@ export interface MQTTMessage {
topic: string;
payload: string;
qos: number;
retain: number;
retain: boolean;
}
// Debug info replays stored messages, so it also carries when each was logged.
export interface MQTTLoggedMessage extends MQTTMessage {
time: string;
}
export interface MQTTTopicDebugInfo {
topic: string;
messages: MQTTMessage[];
messages: MQTTLoggedMessage[];
}
export interface MQTTDiscoveryDebugInfo {
@@ -29,7 +33,7 @@ export interface MQTTEntityDebugInfo {
}
export interface MQTTTriggerDebugInfo {
discovery_data: MQTTDiscoveryDebugInfo;
discovery_data: MQTTDiscoveryDebugInfo | null;
}
export interface MQTTDeviceDebugInfo {
+7 -3
View File
@@ -1,11 +1,11 @@
import type { HomeAssistant } from "../types";
import { handleFetchPromise } from "../util/hass-call-api";
import type { CloudStatus } from "./cloud";
export interface InstallationType {
installation_type:
| "Home Assistant Operating System"
| "Home Assistant OS"
| "Home Assistant Container"
| "Unsupported Third Party Container"
| "Home Assistant Supervised"
| "Home Assistant Core"
| "Unknown";
@@ -49,6 +49,10 @@ export interface CloudLoginMFA extends CloudLoginBase {
code: string;
}
export interface OnboardingCloudStatus {
logged_in: boolean;
}
export const fetchOnboardingOverview = () =>
fetch(`${__HASS_URL__}/api/onboarding`, { credentials: "same-origin" });
@@ -115,7 +119,7 @@ export const loginHaCloud = async (
})
);
export const fetchHaCloudStatus = async (): Promise<CloudStatus> =>
export const fetchHaCloudStatus = async (): Promise<OnboardingCloudStatus> =>
handleFetchPromise(fetch("/api/onboarding/cloud/status"));
export const signOutHaCloud = async () =>
+3 -3
View File
@@ -1,11 +1,11 @@
import type { HomeAssistant } from "../types";
export interface OTBRInfo {
active_dataset_tlvs: string;
active_dataset_tlvs: string | null;
border_agent_id: string;
channel: number;
channel: number | null;
extended_address: string;
extended_pan_id: string;
extended_pan_id: string | null;
url: string;
}
+1 -2
View File
@@ -15,8 +15,7 @@ export interface PersistentNotification {
created_at: string;
message: string;
notification_id: string;
title: string;
status: "read" | "unread";
title: string | null;
}
export type PersistentNotifications = Record<string, PersistentNotification>;
+1 -2
View File
@@ -14,8 +14,7 @@ export const subscribePreviewGeneric = (
hass: HomeAssistant,
domain: string,
flow_id: string,
flow_type:
"config_flow" | "options_flow" | "config_subentries_flow" | "repair_flow",
flow_type: "config_flow" | "options_flow",
user_input: Record<string, any>,
callback: (preview: GenericPreview) => void
): Promise<UnsubscribeFunc> =>
+9 -9
View File
@@ -5,7 +5,7 @@ import type { HomeAssistant } from "../types";
import { firstWeekday } from "../common/datetime/first_weekday";
export interface RecorderInfo {
backlog: number | null;
backlog: number;
db_in_default_location: boolean;
max_backlog: number;
migration_in_progress: boolean;
@@ -33,10 +33,10 @@ export interface StatisticValue {
}
export interface Statistic {
max: number | null;
mean: number | null;
min: number | null;
change: number | null;
max?: number | null;
mean?: number | null;
min?: number | null;
change?: number | null;
}
export enum StatisticMeanType {
@@ -96,11 +96,11 @@ export interface StatisticsValidationResultUnitsChanged {
type: "units_changed";
data: {
statistic_id: string;
state_unit: string;
state_unit: string | null;
state_unit_class: string | null;
metadata_unit: string;
metadata_unit: string | null;
metadata_unit_class: string | null;
supported_unit: string;
supported_unit: string | null;
};
}
@@ -194,7 +194,7 @@ export const fetchStatistic = (
period: {
fixed_period?: { start: string | Date; end: string | Date };
calendar?: { period: string; offset?: number };
rolling_window?: { duration: HaDurationData; offset: HaDurationData };
rolling_window?: { duration: HaDurationData; offset?: HaDurationData };
},
units?: StatisticsUnitConfiguration
) =>
+8 -7
View File
@@ -4,17 +4,18 @@ declare global {
}
}
export type RefreshTokenType = "normal" | "long_lived_access_token";
export type RefreshTokenType = "normal" | "system" | "long_lived_access_token";
export interface RefreshToken {
client_icon?: string;
client_id: string;
client_name?: string;
auth_provider_type: string | null;
client_icon: string | null;
client_id: string | null;
client_name: string | null;
created_at: string;
expire_at?: string;
expire_at: string | null;
id: string;
is_current: boolean;
last_used_at?: string;
last_used_ip?: string;
last_used_at: string | null;
last_used_ip: string | null;
type: RefreshTokenType;
}
+2 -2
View File
@@ -11,8 +11,8 @@ export enum RemoteEntityFeature {
export type RemoteEntity = HassEntityBase & {
attributes: HassEntityAttributeBase & {
current_activity: string | null;
activity_list: string[] | null;
current_activity?: string | null;
activity_list?: string[] | null;
[key: string]: any;
};
};
+1
View File
@@ -13,6 +13,7 @@ export const weekdays = [
export interface ScheduleDay {
from: string;
to: string;
data?: Record<string, boolean | string | number>;
}
type ScheduleDays = Partial<Record<(typeof weekdays)[number], ScheduleDay[]>>;
+1
View File
@@ -438,6 +438,7 @@ interface ObjectSelectorField {
label?: string;
description?: string;
required?: boolean;
default?: unknown;
}
export interface ObjectSelector {
+7 -7
View File
@@ -7,15 +7,15 @@ import type { Store } from "home-assistant-js-websocket/dist/store";
import type { DataTableRowData } from "../components/data-table/ha-data-table";
export interface SSDPDiscoveryData extends DataTableRowData {
name: string | undefined;
name: string | null;
ssdp_usn: string;
ssdp_st: string;
upnp: Record<string, unknown>;
ssdp_location: string | undefined;
ssdp_nt: string | undefined;
ssdp_udn: string | undefined;
ssdp_ext: string | undefined;
ssdp_server: string | undefined;
ssdp_location: string | null;
ssdp_nt: string | null;
ssdp_udn: string | null;
ssdp_ext: string | null;
ssdp_server: string | null;
ssdp_headers: Record<string, unknown>;
ssdp_all_locations: string[];
x_homeassistant_matching_domains: string[];
@@ -23,7 +23,7 @@ export interface SSDPDiscoveryData extends DataTableRowData {
interface SSDPRemoveDiscoveryData {
ssdp_st: string;
ssdp_location: string | undefined;
ssdp_location: string | null;
}
interface SSDPSubscriptionMessage {
+1 -2
View File
@@ -12,9 +12,8 @@ export interface SpeechMetadata {
export interface STTEngine {
engine_id: string;
supported_languages?: string[];
supported_languages: string[];
name?: string;
deprecated: boolean;
}
export const listSTTEngines = (
+1
View File
@@ -16,6 +16,7 @@ export interface Tag {
name?: string;
description?: string;
last_scanned?: string;
device_id?: string;
}
export interface UpdateTagParams {
+12 -7
View File
@@ -2,7 +2,7 @@ import type { HomeAssistant } from "../types";
export interface ThreadRouter {
instance_name: string;
addresses: [string];
addresses: string[];
border_agent_id: string | null;
brand: string | null;
extended_address: string;
@@ -28,11 +28,16 @@ export interface ThreadDataSet {
source: string;
}
export interface ThreadRouterDiscoveryEvent {
key: string;
type: "router_discovered" | "router_removed";
data: ThreadRouter;
}
export type ThreadRouterDiscoveryEvent =
| {
key: string;
type: "router_discovered";
data: ThreadRouter;
}
| {
key: string;
type: "router_removed";
};
class DiscoveryStream {
routers: Record<string, ThreadRouter>;
@@ -81,7 +86,7 @@ export const addThreadDataSet = (
hass: HomeAssistant,
source: string,
tlv: string
): Promise<void> =>
): Promise<{ result: "stored" | "discarded" }> =>
hass.callWS({
type: "thread/add_dataset_tlv",
source,
+1 -1
View File
@@ -79,7 +79,7 @@ export const subscribeItems = (
entity_id: string,
callback: (update: TodoItems) => void
) =>
hass.connection.subscribeMessage<any>(callback, {
hass.connection.subscribeMessage<TodoItems>(callback, {
type: "todo/item/subscribe",
entity_id,
});
+173
View File
@@ -0,0 +1,173 @@
import { ensureArray } from "../common/array/ensure-array";
import type { HomeAssistant } from "../types";
import type { Condition, Trigger } from "./automation";
import { describeCondition, describeTrigger } from "./automation_i18n";
import type { EntityRegistryEntry } from "./entity/entity_registry";
import type { DomainManifestLookup } from "./integration";
import { describeAction } from "./script_i18n";
import type { TraceExtended } from "./trace";
import { TraceTree } from "./trace-tree";
import type { TraceActionNode, TraceBranch, TraceNode } from "./trace-tree";
const STATE_KEY = "ui.panel.config.automation.trace.graph.state";
/** Shared so an absent registry does not break the callers' memoization. */
const NO_ENTITY_REGISTRY: EntityRegistryEntry[] = [];
interface LabelContext {
hass: HomeAssistant;
entityRegistry: EntityRegistryEntry[];
manifests?: DomainManifestLookup;
}
type StatefulNode = Pick<TraceNode, "disabled" | "error" | "track"> &
Partial<Pick<TraceNode, "notTriggered" | "condition">>;
/**
* Whether a condition passed is drawn as the tracked path, and failing as a
* cross on a node that is aria-hidden, so the outcome needs saying. A repeated
* condition can have done both across its evaluations.
*/
const conditionOutcome = (
condition: NonNullable<TraceNode["condition"]>
): "passed" | "failed" | "passed_and_failed" | undefined => {
if (condition.passed && condition.failed) {
return "passed_and_failed";
}
if (condition.passed) {
return "passed";
}
if (condition.failed) {
return "failed";
}
return undefined;
};
/** The graph shows the run outcome only visually, so the label says it. */
const withState = (
{ hass }: LabelContext,
description: string,
node: StatefulNode,
badge?: number
): string => {
const outcome = node.condition && conditionOutcome(node.condition);
let state: string;
if (node.disabled) {
state = hass.localize(`${STATE_KEY}.disabled`);
} else if (node.error) {
state = hass.localize(`${STATE_KEY}.error`);
} else if (node.notTriggered) {
state = hass.localize(`${STATE_KEY}.not_triggered`);
} else if (outcome) {
state = hass.localize(`${STATE_KEY}.${outcome}`);
} else if (node.track && badge) {
state = hass.localize(`${STATE_KEY}.repeated`, { count: badge });
} else if (node.track) {
state = hass.localize(`${STATE_KEY}.executed`);
} else {
state = hass.localize(`${STATE_KEY}.not_executed`);
}
return hass.localize("ui.panel.config.automation.trace.graph.node_label", {
description,
state,
});
};
/** Named by its first condition, as `ha-automation-option-row` does. */
const optionLabel = (ctx: LabelContext, branch: TraceBranch): string => {
const conditions = branch.option?.conditions
? ensureArray<Condition | string>(branch.option.conditions)
: undefined;
let description: string;
if (!conditions || conditions.length === 0) {
description = ctx.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.no_conditions"
);
} else if (typeof conditions[0] === "string") {
description = conditions[0];
} else {
description = describeCondition(
conditions[0],
ctx.hass,
ctx.entityRegistry
);
}
if (conditions && conditions.length > 1) {
description += ctx.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.option_description_additional",
{ numberOfAdditionalConditions: conditions.length - 1 }
);
}
return withState(ctx, description, {
disabled: branch.disabled,
error: false,
track: branch.hasTrace,
});
};
const addAction = (
ctx: LabelContext,
labels: Record<string, string>,
node: TraceActionNode
): void => {
labels[node.path] = withState(
ctx,
// The tree's `actionType` is a wider union than describeAction accepts;
// it derives the type from the config anyway.
describeAction(
ctx.hass,
ctx.entityRegistry,
node.config,
undefined,
undefined,
ctx.manifests
),
node,
node.badge
);
for (const branch of node.branches) {
// Only `choose` branches carry an option, which gets a node of its own.
if (branch.option) {
labels[branch.path] = optionLabel(ctx, branch);
}
for (const child of branch.children) {
addAction(ctx, labels, child);
}
}
};
/**
* Accessible names for every step of a trace, keyed by node path. Called from
* the trace panels, which own `hass`; memoize on everything but `hass`, which
* is replaced on every state update.
*/
export const buildTraceLabels = (
trace: TraceExtended,
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[] | undefined,
manifests?: DomainManifestLookup
): Record<string, string> => {
const entities = entityRegistry ?? NO_ENTITY_REGISTRY;
const ctx: LabelContext = { hass, entityRegistry: entities, manifests };
const tree = new TraceTree(trace);
const labels: Record<string, string> = {};
tree.triggers?.forEach((node: TraceNode<Trigger>) => {
labels[node.path] = withState(
ctx,
describeTrigger(node.config, hass, entities),
node
);
});
tree.conditions.forEach((node: TraceNode<Condition>) => {
labels[node.path] = withState(
ctx,
describeCondition(node.config, hass, entities),
node
);
});
tree.actions.forEach((node) => addAction(ctx, labels, node));
tree.sequence.forEach((node) => addAction(ctx, labels, node));
return labels;
};
+2 -2
View File
@@ -2,9 +2,9 @@ import type { HomeAssistant } from "../types";
export interface TTSEngine {
engine_id: string;
supported_languages?: string[];
supported_languages: string[];
name?: string;
deprecated: boolean;
deprecated?: boolean;
}
export interface TTSVoice {
+1 -1
View File
@@ -67,7 +67,7 @@ export function canReturnHome(stateObj: VacuumEntity): boolean {
export interface Segment {
id: string;
name: string;
group?: string;
group: string | null;
}
export const getVacuumSegments = (
+1 -2
View File
@@ -68,8 +68,7 @@ export function canStop(stateObj: ValveEntity): boolean {
}
interface ValveEntityAttributes extends HassEntityAttributeBase {
current_position?: number;
position?: number;
current_position?: number | null;
}
export interface ValveEntity extends HassEntityBase {
+1
View File
@@ -3,6 +3,7 @@ import type { HomeAssistant } from "../types";
export interface WakeWord {
id: string;
name: string;
phrase: string | null;
}
export const fetchWakeWordInfo = (hass: HomeAssistant, entity_id: string) =>
+5 -2
View File
@@ -7,6 +7,7 @@ export enum WaterHeaterEntityFeature {
TARGET_TEMPERATURE = 1,
OPERATION_MODE = 2,
AWAY_MODE = 4,
ON_OFF = 8,
}
export const OPERATION_MODES = [
@@ -28,8 +29,10 @@ export type WaterHeaterEntity = HassEntityBase & {
max_temp: number;
current_temperature?: number;
temperature?: number;
operation_mode: OperationMode;
operation_list: OperationMode[];
target_temp_high?: number;
target_temp_low?: number;
operation_mode?: OperationMode;
operation_list?: OperationMode[];
away_mode?: "on" | "off";
};
};
+14 -3
View File
@@ -56,13 +56,19 @@ export interface ForecastAttribute {
temperature: number;
datetime: string;
templow?: number;
apparent_temperature?: number;
dew_point?: number;
precipitation?: number;
precipitation_probability?: number;
humidity?: number;
cloud_coverage?: number;
uv_index?: number;
condition?: string;
is_daytime?: boolean;
pressure?: number;
wind_speed?: string;
wind_bearing?: number | string;
wind_gust_speed?: number;
wind_speed?: number;
}
export type ForecastPrecipitationType = "amount" | "probability";
@@ -77,13 +83,18 @@ export const getForecastPrecipitation = (
interface WeatherEntityAttributes extends HassEntityAttributeBase {
attribution?: string;
apparent_temperature?: number;
cloud_coverage?: number;
dew_point?: number;
humidity?: number;
forecast?: ForecastAttribute[];
is_daytime?: boolean;
ozone?: number;
pressure?: number;
temperature?: number;
uv_index?: number;
visibility?: number;
wind_bearing?: number | string;
wind_gust_speed?: number;
wind_speed?: number;
precipitation_unit: string;
pressure_unit: string;
@@ -94,7 +105,7 @@ interface WeatherEntityAttributes extends HassEntityAttributeBase {
export interface ForecastEvent {
type: "hourly" | "daily" | "twice_daily";
forecast: [ForecastAttribute] | null;
forecast: ForecastAttribute[] | null;
}
export interface WeatherEntity extends HassEntityBase {
+1
View File
@@ -5,6 +5,7 @@ export interface Webhook {
domain: string;
name: string;
local_only: boolean;
allowed_methods: string[];
}
export interface WebhookError {
code: number;

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