Compare commits

...

50 Commits

Author SHA1 Message Date
Bram Kragten
010e25b49e Add support for helper text in form boolean 2024-11-07 09:56:10 +01:00
renovate[bot]
a08c7a319f Update formatjs monorepo (#22681)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-07 08:52:53 +02:00
Petar Petrov
5e8868e4b1 Fix import type linter issues (#22702) 2024-11-07 06:39:30 +00:00
Bram Kragten
64285d5155 Add zwave expert UI / Installer settings (#21897)
* Add zwave expert UI / Installer settings

* Fix zwave invoceCC api function name

* Fix function calls of invokeZWaveCCApi

* Add zwave node-installer translations and endpoint separation

* Add zwave capability-control error handling, translations and thermostat setback

* Fix zwave capability thermostat setback

---------

Co-authored-by: Wendelin <w@pe8.at>
2024-11-07 08:00:51 +02:00
Bram Kragten
5247b74fd4 Bumped version to 20241106.0 2024-11-06 13:43:57 +01:00
Wendelin
26e914290d Fix hassio logs translations (#22693)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-11-06 12:33:45 +00:00
Wendelin
ed3096157c Fix logs overflow when there are 0 boots (#22694) 2024-11-06 13:19:17 +01:00
Simon Lamon
04a45a4361 Fix action descriptions switch to English when using search (#22689)
* var mistake

* Update src/panels/config/automation/add-automation-element-dialog.ts

* prettier
2024-11-06 11:17:36 +01:00
Paul Bottein
5430040b96 Fix update more info margin (#22691) 2024-11-06 11:17:14 +01:00
Wendelin
4bd70167ad Add overflow menu to error-log-card (#22684)
* Add overflow menu to error-log-card

* Add toggle line wrap icon-button in error-log-card
2024-11-06 09:59:01 +00:00
Wendelin
e908fbb48e Check for empty logs (#22675)
* Fix download logs default lines + translations + iOS, add live logs indicator

* Fix rtl in error-log-card

* Fix downloadFileSupported
2024-11-06 09:42:46 +00:00
Paul Bottein
38da01abfa Fix icon click in edit card overflow (#22686) 2024-11-06 09:42:38 +00:00
Paul Bottein
c3b7ce8dc4 Revert "More flexible translation keys for logbook binary sensors" (#22687)
Revert "More flexible translation keys for logbook binary sensors (#22257)"

This reverts commit df3e4576db.
2024-11-06 09:30:19 +00:00
Simon Lamon
0488d199ac Localize automation logbook text (#22685)
* localize automation text

* localize domain

* fixup

* split up

* fixup

* prettier
2024-11-06 09:03:41 +00:00
karwosts
df3e4576db More flexible translation keys for logbook binary sensors (#22257) 2024-11-06 10:55:17 +02:00
Paul Bottein
6bd7788815 Use grid options instead of layout options for all cards. (#22676)
* Use grid options for all cards

* Improve min columns for some cards

* Fix button and area card
2024-11-05 18:58:19 +01:00
Paul Bottein
9cdae4fea7 Bumped version to 20241105.0 2024-11-05 18:46:16 +01:00
Bram Kragten
7adf9f8526 fix height of header table rows (#22678) 2024-11-05 16:13:52 +00:00
Bram Kragten
35dcb46703 Group stream picking logic (#22674)
* Group stream picking logic

* MJPEG too

* handle errors when 1 stream type

* correct import

* change to array

* Update ha-camera-stream.ts

* Update ha-camera-stream.ts

* Update ha-camera-stream.ts

* rename
2024-11-05 14:33:34 +00:00
Bram Kragten
17db85ebad Migrate select in md-dialog to md-select (#22670)
* Migrate select in md-dialog to md-select

* Fix md-select in es5 md-dialogs

---------

Co-authored-by: Wendelin <w@pe8.at>
2024-11-05 15:24:09 +01:00
Petar Petrov
fa39595c37 Fix scanning of small QR codes with JS (#22651)
* Fix scanning of small QR codes with JS

* fix filename

* fix qr error and add device button

* fix yarn.lock
2024-11-05 16:00:05 +02:00
Bram Kragten
4db908171f Restrict webrtc logging to dev (#22671)
restrict webrtc logging to dev
2024-11-05 12:33:36 +01:00
Paulus Schoutsen
7306b8c102 Remove sections blog post link (#22663) 2024-11-05 06:07:27 +01:00
Bram Kragten
928bf3465e Bumped version to 20241104.0 2024-11-04 19:03:40 +01:00
Wendelin
0b38143765 Fix load older logs at boot 0 in error-log-card (#22657)
* Fix load older logs at boot 0 in error-log-card

* Refactor fetch boot logs in error-log-card

* Refactor download url boot logs in error-log-card
2024-11-04 16:36:25 +00:00
Bram Kragten
2f974078e0 Only show webrtc if it has video (#22659) 2024-11-04 16:34:08 +00:00
Bram Kragten
efe90fcc55 Show error when using wrong username format during onboarding (#22658) 2024-11-04 16:23:32 +00:00
renovate[bot]
01e33f5412 Update dependency webpack to v5.96.1 (#22655)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-04 14:35:51 +01:00
Bram Kragten
51fdc484c3 Update voice wizard animations (#22656) 2024-11-04 13:11:35 +01:00
Paul Bottein
3d9fa462a6 Improve imported card container style (#22653) 2024-11-04 12:31:22 +01:00
Paul Bottein
32b5d67806 Fix create backup toggle ignored (#22652) 2024-11-04 11:27:04 +01:00
Paulus Schoutsen
20d3681da3 Delay loading IndexedDB to when first icon is requested (#22637) 2024-11-04 11:26:45 +01:00
Paulus Schoutsen
9b97274bf6 CSS Fixes for hui-energy-date-selection-card (#22640)
* CSS Fixes for hui-energy-date-selection-card

* Remove unused class

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2024-11-04 09:46:21 +00:00
Paulus Schoutsen
ede0dff030 CSS Fixes for ha-toast (#22639) 2024-11-04 10:29:58 +01:00
Simon Lamon
4cd4635fa5 Collection of localization issues (#22615)
* Fix wrong use of 'zero' in ICU formatted string for condition headlines

* Matter: Use setup code consistently

* Matter: Share from Google Home dialog

* Remove question format for settings toggles

* Add translation for current add-on version:" in add-on details

* Missing space

* Localize integration name not localized in single_config_entry alert

* Reword start into restart to indicate that the addon restarts when it crashes

* Rephrase rename description

* localize migrate script / automation

* Fixup script translation
2024-11-04 08:00:27 +01:00
renovate[bot]
7832219749 Update dependency @codemirror/search to v6.5.7 (#22647)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-04 08:51:06 +02:00
dependabot[bot]
a8d4726caf Bump relative-ci/agent-action from 2.1.12 to 2.1.13 (#22648) 2024-11-04 07:26:16 +01:00
dependabot[bot]
4b3e20c6ca Bump softprops/action-gh-release from 2.0.8 to 2.0.9 (#22649) 2024-11-04 07:25:44 +01:00
karwosts
f9a53743ce Make duration input clearable (#22614) 2024-11-04 08:25:09 +02:00
G Johansson
89250c0c01 Render preview based of entity domain (#21926)
* Render preview based of entity domain

* Add some more

* More

* return string

* Final

* Add image

* Sort

* Missing format
2024-11-04 08:07:12 +02:00
karwosts
4ef944ea08 Fix map zone focus issues (#22623) 2024-11-04 07:58:13 +02:00
renovate[bot]
5f58c183f4 Update dependency webpack to v5.96.0 (#22645)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-04 07:44:41 +02:00
renovate[bot]
f71feff916 Update dependency core-js to v3.39.0 (#22636)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-03 07:12:27 +01:00
renovate[bot]
50fb3b314b Update dependency mocha to v10.8.2 (#22633)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-02 21:07:53 +01:00
renovate[bot]
06298562cd Update dependency @codemirror/autocomplete to v6.18.2 (#22632)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-02 21:07:25 +01:00
renovate[bot]
89e74f3f07 Update dependency mocha to v10.8.1 (#22629)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-01 22:12:25 +01:00
renovate[bot]
da96c27893 Update dependency mocha to v10.8.0 (#22627)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-01 21:41:09 +01:00
renovate[bot]
3321dd4ca7 Update workbox monorepo to v7.3.0 (#22626)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-01 20:21:23 +01:00
Josh McCarty
7106d56b33 Fix NFS proper name (#22620) 2024-11-01 08:53:00 +01:00
ildar170975
25cd8a9d9f Revert height=100% in horizontal-stack-card (#22617)
Update hui-horizontal-stack-card.ts
2024-10-31 20:12:32 +00:00
100 changed files with 2316 additions and 723 deletions

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Send bundle stats and build information to RelativeCI - name: Send bundle stats and build information to RelativeCI
uses: relative-ci/agent-action@v2.1.12 uses: relative-ci/agent-action@v2.1.13
with: with:
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }} key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
token: ${{ github.token }} token: ${{ github.token }}

View File

@@ -55,7 +55,7 @@ jobs:
script/release script/release
- name: Upload release assets - name: Upload release assets
uses: softprops/action-gh-release@v2.0.8 uses: softprops/action-gh-release@v2.0.9
with: with:
files: | files: |
dist/*.whl dist/*.whl

View File

@@ -106,6 +106,14 @@ function copyMapPanel(staticDir) {
); );
} }
function copyZXingWasm(staticDir) {
const staticPath = genStaticPath(staticDir);
copyFileDir(
npmPath("zxing-wasm/dist/reader/zxing_reader.wasm"),
staticPath("js")
);
}
gulp.task("copy-locale-data", async () => { gulp.task("copy-locale-data", async () => {
const staticDir = paths.app_output_static; const staticDir = paths.app_output_static;
copyLocaleData(staticDir); copyLocaleData(staticDir);
@@ -143,6 +151,7 @@ gulp.task("copy-static-app", async () => {
copyMapPanel(staticDir); copyMapPanel(staticDir);
// Qr Scanner assets // Qr Scanner assets
copyZXingWasm(staticDir);
copyQrScannerWorker(staticDir); copyQrScannerWorker(staticDir);
}); });

View File

@@ -1,16 +0,0 @@
import { html } from "lit";
import type { DemoConfig } from "../types";
export const demoLovelaceDescription: DemoConfig["description"] = (
localize
) => html`
<p>
${localize("ui.panel.page-demo.config.sections.description", {
blog_post: html`<a
href="https://www.home-assistant.io/blog/2024/03/04/dashboard-chapter-1/"
target="_blank"
>${localize("ui.panel.page-demo.config.sections.description_blog_post")}
</a>`,
})}
</p>
`;

View File

@@ -1,5 +1,4 @@
import type { DemoConfig } from "../types"; import type { DemoConfig } from "../types";
import { demoLovelaceDescription } from "./description";
import { demoEntitiesSections } from "./entities"; import { demoEntitiesSections } from "./entities";
import { demoLovelaceSections } from "./lovelace"; import { demoLovelaceSections } from "./lovelace";
@@ -7,7 +6,6 @@ export const demoSections: DemoConfig = {
authorName: "Home Assistant", authorName: "Home Assistant",
authorUrl: "https://github.com/home-assistant/frontend/", authorUrl: "https://github.com/home-assistant/frontend/",
name: "Home Demo", name: "Home Demo",
description: demoLovelaceDescription,
lovelace: demoLovelaceSections, lovelace: demoLovelaceSections,
entities: demoEntitiesSections, entities: demoEntitiesSections,
theme: () => ({}), theme: () => ({}),

View File

@@ -510,6 +510,7 @@ class DemoHaForm extends LitElement {
.computeError=${(error) => translations[error] || error} .computeError=${(error) => translations[error] || error}
.computeLabel=${(schema) => .computeLabel=${(schema) =>
translations[schema.name] || schema.name} translations[schema.name] || schema.name}
.computeHelper=${() => "Helper text"}
@value-changed=${(e) => { @value-changed=${(e) => {
this.data[idx] = e.detail.value; this.data[idx] = e.detail.value;
this.requestUpdate(); this.requestUpdate();

View File

@@ -223,7 +223,10 @@ class HassioAddonInfo extends LitElement {
<div class="description light-color"> <div class="description light-color">
${this.addon.version ${this.addon.version
? html` ? html`
Current version: ${this.addon.version} ${this.supervisor.localize(
"addon.dashboard.current_version",
{ version: this.addon.version }
)}
<div class="changelog" @click=${this._openChangelog}> <div class="changelog" @click=${this._openChangelog}>
(<span class="changelog-link" (<span class="changelog-link"
>${this.supervisor.localize( >${this.supervisor.localize(

View File

@@ -38,12 +38,13 @@ class HassioAddonLogDashboard extends LitElement {
@value-changed=${this._filterChanged} @value-changed=${this._filterChanged}
.hass=${this.hass} .hass=${this.hass}
.filter=${this._filter} .filter=${this._filter}
.label=${this.hass.localize("ui.panel.config.logs.search")} .label=${this.supervisor.localize("ui.panel.config.logs.search")}
></search-input> ></search-input>
</div> </div>
<div class="content"> <div class="content">
<error-log-card <error-log-card
.hass=${this.hass} .hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.header=${this.addon.name} .header=${this.addon.name}
.provider=${this.addon.slug} .provider=${this.addon.slug}
show show

View File

@@ -27,22 +27,22 @@
"dependencies": { "dependencies": {
"@babel/runtime": "7.26.0", "@babel/runtime": "7.26.0",
"@braintree/sanitize-url": "7.1.0", "@braintree/sanitize-url": "7.1.0",
"@codemirror/autocomplete": "6.18.1", "@codemirror/autocomplete": "6.18.2",
"@codemirror/commands": "6.7.1", "@codemirror/commands": "6.7.1",
"@codemirror/language": "6.10.3", "@codemirror/language": "6.10.3",
"@codemirror/legacy-modes": "6.4.1", "@codemirror/legacy-modes": "6.4.1",
"@codemirror/search": "6.5.6", "@codemirror/search": "6.5.7",
"@codemirror/state": "6.4.1", "@codemirror/state": "6.4.1",
"@codemirror/view": "6.34.1", "@codemirror/view": "6.34.1",
"@egjs/hammerjs": "2.0.17", "@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.16.1", "@formatjs/intl-datetimeformat": "6.16.3",
"@formatjs/intl-displaynames": "6.8.1", "@formatjs/intl-displaynames": "6.8.3",
"@formatjs/intl-getcanonicallocales": "2.5.1", "@formatjs/intl-getcanonicallocales": "2.5.2",
"@formatjs/intl-listformat": "7.7.1", "@formatjs/intl-listformat": "7.7.3",
"@formatjs/intl-locale": "4.2.1", "@formatjs/intl-locale": "4.2.3",
"@formatjs/intl-numberformat": "8.14.1", "@formatjs/intl-numberformat": "8.14.3",
"@formatjs/intl-pluralrules": "5.3.1", "@formatjs/intl-pluralrules": "5.3.3",
"@formatjs/intl-relativetimeformat": "11.4.1", "@formatjs/intl-relativetimeformat": "11.4.3",
"@fullcalendar/core": "6.1.15", "@fullcalendar/core": "6.1.15",
"@fullcalendar/daygrid": "6.1.15", "@fullcalendar/daygrid": "6.1.15",
"@fullcalendar/interaction": "6.1.15", "@fullcalendar/interaction": "6.1.15",
@@ -98,10 +98,11 @@
"@webcomponents/scoped-custom-element-registry": "0.0.9", "@webcomponents/scoped-custom-element-registry": "0.0.9",
"@webcomponents/webcomponentsjs": "2.8.0", "@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1", "app-datepicker": "5.1.1",
"barcode-detector": "2.2.11",
"chart.js": "4.4.6", "chart.js": "4.4.6",
"color-name": "2.0.0", "color-name": "2.0.0",
"comlink": "4.4.1", "comlink": "4.4.1",
"core-js": "3.38.1", "core-js": "3.39.0",
"cropperjs": "1.6.2", "cropperjs": "1.6.2",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"date-fns-tz": "3.2.0", "date-fns-tz": "3.2.0",
@@ -114,7 +115,7 @@
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch", "hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.4.0", "home-assistant-js-websocket": "9.4.0",
"idb-keyval": "6.2.1", "idb-keyval": "6.2.1",
"intl-messageformat": "10.7.3", "intl-messageformat": "10.7.5",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"leaflet": "1.9.4", "leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch", "leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
@@ -142,12 +143,12 @@
"vue": "2.7.16", "vue": "2.7.16",
"vue2-daterange-picker": "0.6.8", "vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0", "weekstart": "2.0.0",
"workbox-cacheable-response": "7.1.0", "workbox-cacheable-response": "7.3.0",
"workbox-core": "7.1.0", "workbox-core": "7.3.0",
"workbox-expiration": "7.1.0", "workbox-expiration": "7.3.0",
"workbox-precaching": "7.1.0", "workbox-precaching": "7.3.0",
"workbox-routing": "7.1.0", "workbox-routing": "7.3.0",
"workbox-strategies": "7.1.0", "workbox-strategies": "7.3.0",
"xss": "1.0.15" "xss": "1.0.15"
}, },
"devDependencies": { "devDependencies": {
@@ -224,7 +225,7 @@
"lodash.template": "4.5.0", "lodash.template": "4.5.0",
"magic-string": "0.30.12", "magic-string": "0.30.12",
"map-stream": "0.0.7", "map-stream": "0.0.7",
"mocha": "10.7.3", "mocha": "10.8.2",
"object-hash": "3.0.0", "object-hash": "3.0.0",
"open": "10.1.0", "open": "10.1.0",
"pinst": "3.0.0", "pinst": "3.0.0",
@@ -241,7 +242,7 @@
"transform-async-modules-webpack-plugin": "1.1.1", "transform-async-modules-webpack-plugin": "1.1.1",
"ts-lit-plugin": "2.0.2", "ts-lit-plugin": "2.0.2",
"typescript": "5.6.3", "typescript": "5.6.3",
"webpack": "5.95.0", "webpack": "5.96.1",
"webpack-cli": "5.1.4", "webpack-cli": "5.1.4",
"webpack-dev-server": "5.1.0", "webpack-dev-server": "5.1.0",
"webpack-manifest-plugin": "5.0.0", "webpack-manifest-plugin": "5.0.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20241031.0" version = "20241106.0"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "The Home Assistant frontend" description = "The Home Assistant frontend"
readme = "README.md" readme = "README.md"

View File

@@ -1185,6 +1185,7 @@ export class HaDataTable extends LitElement {
.group-header { .group-header {
padding-top: 12px; padding-top: 12px;
height: var(--data-table-row-height, 52px);
padding-left: 12px; padding-left: 12px;
padding-inline-start: 12px; padding-inline-start: 12px;
padding-inline-end: initial; padding-inline-end: initial;

View File

@@ -12,6 +12,7 @@ import {
query, query,
state as litState, state as litState,
} from "lit/decorators"; } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
interface State { interface State {
bold: boolean; bold: boolean;
@@ -26,12 +27,15 @@ interface State {
export class HaAnsiToHtml extends LitElement { export class HaAnsiToHtml extends LitElement {
@property() public content!: string; @property() public content!: string;
@property({ type: Boolean, attribute: "wrap-disabled" }) public wrapDisabled =
false;
@query("pre") private _pre?: HTMLPreElement; @query("pre") private _pre?: HTMLPreElement;
@litState() private _filter = ""; @litState() private _filter = "";
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
return html`<pre></pre>`; return html`<pre class=${classMap({ wrap: !this.wrapDisabled })}></pre>`;
} }
protected firstUpdated(_changedProperties: PropertyValues): void { protected firstUpdated(_changedProperties: PropertyValues): void {
@@ -47,9 +51,11 @@ export class HaAnsiToHtml extends LitElement {
return css` return css`
pre { pre {
overflow-x: auto; overflow-x: auto;
margin: 0;
}
pre.wrap {
white-space: pre-wrap; white-space: pre-wrap;
overflow-wrap: break-word; overflow-wrap: break-word;
margin: 0;
} }
.bold { .bold {
font-weight: bold; font-weight: bold;

View File

@@ -7,6 +7,8 @@ import {
type PropertyValues, type PropertyValues,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { computeStateName } from "../common/entity/compute_state_name"; import { computeStateName } from "../common/entity/compute_state_name";
import { supportsFeature } from "../common/entity/supports-feature"; import { supportsFeature } from "../common/entity/supports-feature";
import { import {
@@ -24,6 +26,13 @@ import type { HomeAssistant } from "../types";
import "./ha-hls-player"; import "./ha-hls-player";
import "./ha-web-rtc-player"; import "./ha-web-rtc-player";
const MJPEG_STREAM = "mjpeg";
type Stream = {
type: StreamType | typeof MJPEG_STREAM;
visible: boolean;
};
@customElement("ha-camera-stream") @customElement("ha-camera-stream")
export class HaCameraStream extends LitElement { export class HaCameraStream extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@@ -46,8 +55,6 @@ export class HaCameraStream extends LitElement {
@state() private _capabilities?: CameraCapabilities; @state() private _capabilities?: CameraCapabilities;
@state() private _streamType?: StreamType;
@state() private _hlsStreams?: { hasAudio: boolean; hasVideo: boolean }; @state() private _hlsStreams?: { hasAudio: boolean; hasVideo: boolean };
@state() private _webRtcStreams?: { hasAudio: boolean; hasVideo: boolean }; @state() private _webRtcStreams?: { hasAudio: boolean; hasVideo: boolean };
@@ -55,7 +62,6 @@ export class HaCameraStream extends LitElement {
public willUpdate(changedProps: PropertyValues): void { public willUpdate(changedProps: PropertyValues): void {
if ( if (
changedProps.has("stateObj") && changedProps.has("stateObj") &&
!this._shouldRenderMJPEG &&
this.stateObj && this.stateObj &&
(changedProps.get("stateObj") as CameraEntity | undefined)?.entity_id !== (changedProps.get("stateObj") as CameraEntity | undefined)?.entity_id !==
this.stateObj.entity_id this.stateObj.entity_id
@@ -79,20 +85,35 @@ export class HaCameraStream extends LitElement {
if (!this.stateObj) { if (!this.stateObj) {
return nothing; return nothing;
} }
if (__DEMO__ || this._shouldRenderMJPEG) { const streams = this._streams(
this._capabilities?.frontend_stream_types,
this._hlsStreams,
this._webRtcStreams
);
return html`${repeat(
streams,
(stream) => stream.type + this.stateObj!.entity_id,
(stream) => this._renderStream(stream)
)}`;
}
private _renderStream(stream: Stream) {
if (!this.stateObj) {
return nothing;
}
if (stream.type === MJPEG_STREAM) {
return html`<img return html`<img
.src=${__DEMO__ .src=${__DEMO__
? this.stateObj.attributes.entity_picture! ? this.stateObj.attributes.entity_picture!
: this._connected : this._connected
? computeMJPEGStreamUrl(this.stateObj) ? computeMJPEGStreamUrl(this.stateObj)
: ""} : this._posterUrl || ""}
alt=${`Preview of the ${computeStateName(this.stateObj)} camera.`} alt=${`Preview of the ${computeStateName(this.stateObj)} camera.`}
/>`; />`;
} }
return html`${this._streamType === STREAM_TYPE_HLS ||
(!this._streamType && if (stream.type === STREAM_TYPE_HLS) {
this._capabilities?.frontend_stream_types.includes(STREAM_TYPE_HLS)) return html`<ha-hls-player
? html`<ha-hls-player
autoplay autoplay
playsinline playsinline
.allowExoPlayer=${this.allowExoPlayer} .allowExoPlayer=${this.allowExoPlayer}
@@ -102,13 +123,12 @@ export class HaCameraStream extends LitElement {
.entityid=${this.stateObj.entity_id} .entityid=${this.stateObj.entity_id}
.posterUrl=${this._posterUrl} .posterUrl=${this._posterUrl}
@streams=${this._handleHlsStreams} @streams=${this._handleHlsStreams}
class=${!this._streamType && this._webRtcStreams ? "hidden" : ""} class=${stream.visible ? "" : "hidden"}
></ha-hls-player>` ></ha-hls-player>`;
: nothing} }
${this._streamType === STREAM_TYPE_WEB_RTC ||
(!this._streamType && if (stream.type === STREAM_TYPE_WEB_RTC) {
this._capabilities?.frontend_stream_types.includes(STREAM_TYPE_WEB_RTC)) return html`<ha-web-rtc-player
? html`<ha-web-rtc-player
autoplay autoplay
playsinline playsinline
.muted=${this.muted} .muted=${this.muted}
@@ -117,12 +137,11 @@ export class HaCameraStream extends LitElement {
.entityid=${this.stateObj.entity_id} .entityid=${this.stateObj.entity_id}
.posterUrl=${this._posterUrl} .posterUrl=${this._posterUrl}
@streams=${this._handleWebRtcStreams} @streams=${this._handleWebRtcStreams}
class=${this._streamType !== STREAM_TYPE_WEB_RTC && class=${stream.visible ? "" : "hidden"}
!this._webRtcStreams ></ha-web-rtc-player>`;
? "hidden" }
: ""}
></ha-web-rtc-player>` return nothing;
: nothing}`;
} }
private async _getCapabilities() { private async _getCapabilities() {
@@ -130,35 +149,13 @@ export class HaCameraStream extends LitElement {
this._hlsStreams = undefined; this._hlsStreams = undefined;
this._webRtcStreams = undefined; this._webRtcStreams = undefined;
if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) { if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) {
this._capabilities = { frontend_stream_types: [] };
return; return;
} }
this._capabilities = await fetchCameraCapabilities( this._capabilities = await fetchCameraCapabilities(
this.hass!, this.hass!,
this.stateObj!.entity_id this.stateObj!.entity_id
); );
if (this._capabilities.frontend_stream_types.length === 1) {
this._streamType = this._capabilities.frontend_stream_types[0];
}
}
private get _shouldRenderMJPEG() {
if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) {
// Steaming is not supported by the camera so fallback to MJPEG stream
return true;
}
if (
this._capabilities &&
(!this._capabilities.frontend_stream_types.includes(STREAM_TYPE_HLS) ||
this._hlsStreams?.hasVideo === false) &&
(!this._capabilities.frontend_stream_types.includes(
STREAM_TYPE_WEB_RTC
) ||
this._webRtcStreams?.hasVideo === false)
) {
// No video in HLS stream and no video in WebRTC stream
return true;
}
return false;
} }
private async _getPosterUrl(): Promise<void> { private async _getPosterUrl(): Promise<void> {
@@ -177,29 +174,88 @@ export class HaCameraStream extends LitElement {
private _handleHlsStreams(ev: CustomEvent) { private _handleHlsStreams(ev: CustomEvent) {
this._hlsStreams = ev.detail; this._hlsStreams = ev.detail;
this._pickStreamType();
} }
private _handleWebRtcStreams(ev: CustomEvent) { private _handleWebRtcStreams(ev: CustomEvent) {
this._webRtcStreams = ev.detail; this._webRtcStreams = ev.detail;
this._pickStreamType();
} }
private _pickStreamType() { private _streams = memoizeOne(
if (!this._hlsStreams || !this._webRtcStreams) { (
return; supportedTypes?: StreamType[],
hlsStreams?: { hasAudio: boolean; hasVideo: boolean },
webRtcStreams?: { hasAudio: boolean; hasVideo: boolean }
): Stream[] => {
if (__DEMO__) {
return [{ type: MJPEG_STREAM, visible: true }];
} }
if (!supportedTypes) {
return [];
}
if (supportedTypes.length === 0) {
// doesn't support any stream type, fallback to mjpeg
return [{ type: MJPEG_STREAM, visible: true }];
}
if (supportedTypes.length === 1) {
// only 1 stream type, no need to choose
if ( if (
this._hlsStreams.hasVideo && (supportedTypes[0] === STREAM_TYPE_HLS &&
this._hlsStreams.hasAudio && hlsStreams?.hasVideo === false) ||
!this._webRtcStreams.hasAudio (supportedTypes[0] === STREAM_TYPE_WEB_RTC &&
webRtcStreams?.hasVideo === false)
) { ) {
this._streamType = STREAM_TYPE_HLS; // stream failed to load, fallback to mjpeg
} else if (this._webRtcStreams.hasVideo) { return [{ type: MJPEG_STREAM, visible: true }];
this._streamType = STREAM_TYPE_WEB_RTC; }
return [{ type: supportedTypes[0], visible: true }];
}
if (hlsStreams && webRtcStreams) {
// fully loaded
if (
hlsStreams.hasVideo &&
hlsStreams.hasAudio &&
!webRtcStreams.hasAudio
) {
// webRTC stream is missing audio, use HLS
return [{ type: STREAM_TYPE_HLS, visible: true }];
}
if (webRtcStreams.hasVideo) {
return [{ type: STREAM_TYPE_WEB_RTC, visible: true }];
}
// both streams failed to load, fallback to mjpeg
return [{ type: MJPEG_STREAM, visible: true }];
}
if (hlsStreams?.hasVideo !== webRtcStreams?.hasVideo) {
// one of the two streams is loaded, or errored
// choose the one that has video or is still loading
if (hlsStreams?.hasVideo) {
return [
{ type: STREAM_TYPE_HLS, visible: true },
{ type: STREAM_TYPE_WEB_RTC, visible: false },
];
}
if (hlsStreams?.hasVideo === false) {
return [{ type: STREAM_TYPE_WEB_RTC, visible: true }];
}
if (webRtcStreams?.hasVideo) {
return [
{ type: STREAM_TYPE_WEB_RTC, visible: true },
{ type: STREAM_TYPE_HLS, visible: false },
];
}
if (webRtcStreams?.hasVideo === false) {
return [{ type: STREAM_TYPE_HLS, visible: true }];
} }
} }
return [
{ type: STREAM_TYPE_HLS, visible: true },
{ type: STREAM_TYPE_WEB_RTC, visible: false },
];
}
);
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host, :host,

View File

@@ -43,6 +43,7 @@ class HaDurationInput extends LitElement {
.label=${this.label} .label=${this.label}
.helper=${this.helper} .helper=${this.helper}
.required=${this.required} .required=${this.required}
.clearable=${!this.required && this.data !== undefined}
.autoValidate=${this.required} .autoValidate=${this.required}
.disabled=${this.disabled} .disabled=${this.disabled}
errorMessage="Required" errorMessage="Required"
@@ -67,28 +68,56 @@ class HaDurationInput extends LitElement {
} }
private get _days() { private get _days() {
return this.data?.days ? Number(this.data.days) : 0; return this.data?.days
? Number(this.data.days)
: this.required || this.data
? 0
: NaN;
} }
private get _hours() { private get _hours() {
return this.data?.hours ? Number(this.data.hours) : 0; return this.data?.hours
? Number(this.data.hours)
: this.required || this.data
? 0
: NaN;
} }
private get _minutes() { private get _minutes() {
return this.data?.minutes ? Number(this.data.minutes) : 0; return this.data?.minutes
? Number(this.data.minutes)
: this.required || this.data
? 0
: NaN;
} }
private get _seconds() { private get _seconds() {
return this.data?.seconds ? Number(this.data.seconds) : 0; return this.data?.seconds
? Number(this.data.seconds)
: this.required || this.data
? 0
: NaN;
} }
private get _milliseconds() { private get _milliseconds() {
return this.data?.milliseconds ? Number(this.data.milliseconds) : 0; return this.data?.milliseconds
? Number(this.data.milliseconds)
: this.required || this.data
? 0
: NaN;
} }
private _durationChanged(ev: CustomEvent<{ value: TimeChangedEvent }>) { private _durationChanged(ev: CustomEvent<{ value?: TimeChangedEvent }>) {
ev.stopPropagation(); ev.stopPropagation();
const value = { ...ev.detail.value }; const value = ev.detail.value ? { ...ev.detail.value } : undefined;
if (value) {
value.hours ||= 0;
value.minutes ||= 0;
value.seconds ||= 0;
if ("days" in value) value.days ||= 0;
if ("milliseconds" in value) value.milliseconds ||= 0;
if (!this.enableMillisecond && !value.milliseconds) { if (!this.enableMillisecond && !value.milliseconds) {
// @ts-ignore // @ts-ignore
@@ -112,6 +141,7 @@ class HaDurationInput extends LitElement {
value.days = (value.days ?? 0) + Math.floor(value.hours / 24); value.days = (value.days ?? 0) + Math.floor(value.hours / 24);
value.hours %= 24; value.hours %= 24;
} }
}
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value, value,

View File

@@ -1,6 +1,6 @@
import "@material/mwc-formfield"; import "@material/mwc-formfield";
import type { TemplateResult } from "lit"; import type { CSSResultGroup, TemplateResult } from "lit";
import { html, LitElement } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import type { import type {
@@ -19,6 +19,8 @@ export class HaFormBoolean extends LitElement implements HaFormElement {
@property() public label!: string; @property() public label!: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@query("ha-checkbox", true) private _input?: HTMLElement; @query("ha-checkbox", true) private _input?: HTMLElement;
@@ -37,6 +39,12 @@ export class HaFormBoolean extends LitElement implements HaFormElement {
.disabled=${this.disabled} .disabled=${this.disabled}
@change=${this._valueChanged} @change=${this._valueChanged}
></ha-checkbox> ></ha-checkbox>
<span slot="label">
<p class="primary">${this.label}</p>
${this.helper
? html`<p class="secondary">${this.helper}</p>`
: nothing}
</span>
</mwc-formfield> </mwc-formfield>
`; `;
} }
@@ -46,6 +54,28 @@ export class HaFormBoolean extends LitElement implements HaFormElement {
value: (ev.target as HaCheckbox).checked, value: (ev.target as HaCheckbox).checked,
}); });
} }
static get styles(): CSSResultGroup {
return css`
ha-formfield {
display: flex;
min-height: 56px;
align-items: center;
--mdc-typography-body2-font-size: 1em;
}
p {
margin: 0;
}
.secondary {
direction: var(--direction);
padding-top: 4px;
box-sizing: border-box;
color: var(--secondary-text-color);
font-size: 0.875rem;
font-weight: var(--mdc-typography-body2-font-weight, 400);
}
`;
}
} }
declare global { declare global {

View File

@@ -8,7 +8,6 @@ import { customIcons } from "../data/custom_icons";
import type { Chunks, Icons } from "../data/iconsets"; import type { Chunks, Icons } from "../data/iconsets";
import { import {
MDI_PREFIXES, MDI_PREFIXES,
checkCacheVersion,
findIconChunk, findIconChunk,
getIcon, getIcon,
writeCache, writeCache,
@@ -26,11 +25,6 @@ const mdiDeprecatedIcons: DeprecatedIcon = {};
const chunks: Chunks = {}; const chunks: Chunks = {};
// Supervisor doesn't use icons, and should not update/downgrade the icon DB.
if (!__SUPERVISOR__) {
checkCacheVersion();
}
const debouncedWriteCache = debounce(() => writeCache(chunks), 2000); const debouncedWriteCache = debounce(() => writeCache(chunks), 2000);
const cachedIcons: Record<string, string> = {}; const cachedIcons: Record<string, string> = {};

View File

@@ -182,6 +182,10 @@ export class HaMdDialog extends MdDialog {
display: contents; display: contents;
} }
.scroller {
overflow: var(--dialog-content-overflow, auto);
}
slot[name="content"]::slotted(*) { slot[name="content"]::slotted(*) {
padding: var(--dialog-content-padding, 24px); padding: var(--dialog-content-padding, 24px);
} }

View File

@@ -0,0 +1,26 @@
import { MdSelectOption } from "@material/web/select/select-option";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-md-select-option")
export class HaMdSelectOption extends MdSelectOption {
static override styles = [
...super.styles,
css`
:host {
--ha-icon-display: block;
--md-sys-color-primary: var(--primary-text-color);
--md-sys-color-secondary: var(--secondary-text-color);
--md-sys-color-surface: var(--card-background-color);
--md-sys-color-on-surface: var(--primary-text-color);
--md-sys-color-on-surface-variant: var(--secondary-text-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-md-select-option": HaMdSelectOption;
}
}

View File

@@ -0,0 +1,32 @@
import { MdFilledSelect } from "@material/web/select/filled-select";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-md-select")
export class HaMdSelect extends MdFilledSelect {
static override styles = [
...super.styles,
css`
:host {
--ha-icon-display: block;
--md-sys-color-primary: var(--primary-text-color);
--md-sys-color-secondary: var(--secondary-text-color);
--md-sys-color-surface: var(--card-background-color);
--md-sys-color-on-surface-variant: var(--secondary-text-color);
--md-sys-color-surface-container-highest: var(--input-fill-color);
--md-sys-color-on-surface: var(--input-ink-color);
--md-sys-color-surface-container: var(--input-fill-color);
--md-sys-color-secondary-container: var(--input-fill-color);
--md-menu-container-color: var(--card-background-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-md-select": HaMdSelect;
}
}

View File

@@ -4,6 +4,11 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
// The BarcodeDetector Web API is not yet supported in all browsers,
// and "qr-scanner" defaults to a suboptimal implementation if it is not available.
// The following import makes a better implementation available that is based on a
// WebAssembly port of ZXing:
import { setZXingModuleOverrides } from "barcode-detector";
import type QrScanner from "qr-scanner"; import type QrScanner from "qr-scanner";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation"; import { stopPropagation } from "../common/dom/stop_propagation";
@@ -16,6 +21,15 @@ import "./ha-list-item";
import "./ha-textfield"; import "./ha-textfield";
import type { HaTextField } from "./ha-textfield"; import type { HaTextField } from "./ha-textfield";
setZXingModuleOverrides({
locateFile: (path: string, prefix: string) => {
if (path.endsWith(".wasm")) {
return "/static/js/zxing_reader.wasm";
}
return prefix + path;
},
});
@customElement("ha-qr-scanner") @customElement("ha-qr-scanner")
class HaQrScanner extends LitElement { class HaQrScanner extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -174,7 +188,7 @@ class HaQrScanner extends LitElement {
} }
private _qrCodeError = (err: any) => { private _qrCodeError = (err: any) => {
if (err === "No QR code found") { if (err.endsWith("No QR code found")) {
this._qrNotFoundCount++; this._qrNotFoundCount++;
if (this._qrNotFoundCount === 250) { if (this._qrNotFoundCount === 250) {
this._reportError(err); this._reportError(err);

View File

@@ -24,7 +24,7 @@ export class HaToast extends Snackbar {
max-width: 650px; max-width: 650px;
} }
// Revert the default styles set by mwc-snackbar /* Revert the default styles set by mwc-snackbar */
@media (max-width: 480px), (max-width: 344px) { @media (max-width: 480px), (max-width: 344px) {
.mdc-snackbar__surface { .mdc-snackbar__surface {
min-width: inherit; min-width: inherit;

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-console */
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
@@ -108,18 +107,18 @@ class HaWebRtcPlayer extends LitElement {
return; return;
} }
console.time("WebRTC");
this._error = undefined; this._error = undefined;
console.timeLog("WebRTC", "start clientConfig"); this._startTimer();
this._logEvent("start clientConfig");
this._clientConfig = await fetchWebRtcClientConfiguration( this._clientConfig = await fetchWebRtcClientConfiguration(
this.hass, this.hass,
this.entityid this.entityid
); );
console.timeLog("WebRTC", "end clientConfig", this._clientConfig); this._logEvent("end clientConfig", this._clientConfig);
this._peerConnection = new RTCPeerConnection( this._peerConnection = new RTCPeerConnection(
this._clientConfig.configuration this._clientConfig.configuration
@@ -141,11 +140,10 @@ class HaWebRtcPlayer extends LitElement {
this._peerConnection.onsignalingstatechange = (ev) => { this._peerConnection.onsignalingstatechange = (ev) => {
switch ((ev.target as RTCPeerConnection).signalingState) { switch ((ev.target as RTCPeerConnection).signalingState) {
case "stable": case "stable":
console.timeLog("WebRTC", "ICE negotiation complete"); this._logEvent("ICE negotiation complete");
break; break;
default: default:
console.timeLog( this._logEvent(
"WebRTC",
"Signaling state changed", "Signaling state changed",
(ev.target as RTCPeerConnection).signalingState (ev.target as RTCPeerConnection).signalingState
); );
@@ -170,7 +168,7 @@ class HaWebRtcPlayer extends LitElement {
offerToReceiveVideo: true, offerToReceiveVideo: true,
}; };
console.timeLog("WebRTC", "start createOffer", offerOptions); this._logEvent("start createOffer", offerOptions);
const offer: RTCSessionDescriptionInit = const offer: RTCSessionDescriptionInit =
await this._peerConnection.createOffer(offerOptions); await this._peerConnection.createOffer(offerOptions);
@@ -179,9 +177,9 @@ class HaWebRtcPlayer extends LitElement {
return; return;
} }
console.timeLog("WebRTC", "end createOffer", offer); this._logEvent("end createOffer", offer);
console.timeLog("WebRTC", "start setLocalDescription"); this._logEvent("start setLocalDescription");
await this._peerConnection.setLocalDescription(offer); await this._peerConnection.setLocalDescription(offer);
@@ -189,7 +187,7 @@ class HaWebRtcPlayer extends LitElement {
return; return;
} }
console.timeLog("WebRTC", "end setLocalDescription"); this._logEvent("end setLocalDescription");
let candidates = ""; let candidates = "";
@@ -203,11 +201,7 @@ class HaWebRtcPlayer extends LitElement {
resolve(); resolve();
} }
console.timeLog( this._logEvent("Ice gathering state changed", iceGatheringState);
"WebRTC",
"Ice gathering state changed",
iceGatheringState
);
}; };
}); });
@@ -225,7 +219,7 @@ class HaWebRtcPlayer extends LitElement {
const offer_sdp = offer.sdp! + candidates; const offer_sdp = offer.sdp! + candidates;
console.timeLog("WebRTC", "start webRtcOffer", offer_sdp); this._logEvent("start webRtcOffer", offer_sdp);
try { try {
this._unsub = webRtcOffer(this.hass, this.entityid, offer_sdp, (event) => this._unsub = webRtcOffer(this.hass, this.entityid, offer_sdp, (event) =>
@@ -238,8 +232,7 @@ class HaWebRtcPlayer extends LitElement {
}; };
private _iceConnectionStateChanged = () => { private _iceConnectionStateChanged = () => {
console.timeLog( this._logEvent(
"WebRTC",
"ice connection state change", "ice connection state change",
this._peerConnection?.iceConnectionState this._peerConnection?.iceConnectionState
); );
@@ -265,18 +258,19 @@ class HaWebRtcPlayer extends LitElement {
this._candidatesList = []; this._candidatesList = [];
} }
if (event.type === "answer") { if (event.type === "answer") {
console.timeLog("WebRTC", "answer", event.answer); this._logEvent("answer", event.answer);
this._handleAnswer(event); this._handleAnswer(event);
} }
if (event.type === "candidate") { if (event.type === "candidate") {
console.timeLog("WebRTC", "remote ice candidate", event.candidate); this._logEvent("remote ice candidate", event.candidate);
try { try {
await this._peerConnection?.addIceCandidate( await this._peerConnection?.addIceCandidate(
new RTCIceCandidate({ candidate: event.candidate, sdpMid: "0" }) new RTCIceCandidate({ candidate: event.candidate, sdpMid: "0" })
); );
} catch (err: any) { } catch (err: any) {
// eslint-disable-next-line no-console
console.error(err); console.error(err);
} }
} }
@@ -291,11 +285,7 @@ class HaWebRtcPlayer extends LitElement {
return; return;
} }
console.timeLog( this._logEvent("local ice candidate", event.candidate?.candidate);
"WebRTC",
"local ice candidate",
event.candidate?.candidate
);
if (this._sessionId) { if (this._sessionId) {
addWebRtcCandidate( addWebRtcCandidate(
@@ -334,19 +324,16 @@ class HaWebRtcPlayer extends LitElement {
sdp: event.answer, sdp: event.answer,
}); });
try { try {
console.timeLog("WebRTC", "start setRemoteDescription", remoteDesc); this._logEvent("start setRemoteDescription", remoteDesc);
await this._peerConnection.setRemoteDescription(remoteDesc); await this._peerConnection.setRemoteDescription(remoteDesc);
} catch (err: any) { } catch (err: any) {
this._error = "Failed to connect WebRTC stream: " + err.message; this._error = "Failed to connect WebRTC stream: " + err.message;
this._cleanUp(); this._cleanUp();
} }
console.timeLog("WebRTC", "end setRemoteDescription"); this._logEvent("end setRemoteDescription");
} }
private _cleanUp() { private _cleanUp() {
console.timeLog("WebRTC", "stopped");
console.timeEnd("WebRTC");
if (this._remoteStream) { if (this._remoteStream) {
this._remoteStream.getTracks().forEach((track) => { this._remoteStream.getTracks().forEach((track) => {
track.stop(); track.stop();
@@ -372,6 +359,9 @@ class HaWebRtcPlayer extends LitElement {
this._peerConnection.onsignalingstatechange = null; this._peerConnection.onsignalingstatechange = null;
this._peerConnection = undefined; this._peerConnection = undefined;
this._logEvent("stopped");
this._stopTimer();
} }
this._unsub?.then((unsub) => unsub()); this._unsub?.then((unsub) => unsub());
this._unsub = undefined; this._unsub = undefined;
@@ -380,17 +370,43 @@ class HaWebRtcPlayer extends LitElement {
} }
private _loadedData() { private _loadedData() {
console.timeLog("WebRTC", "loadedData");
console.timeEnd("WebRTC");
const video = this._videoEl; const video = this._videoEl;
const stream = video.srcObject as MediaStream; const stream = video.srcObject as MediaStream;
fireEvent(this, "load"); const data = {
fireEvent(this, "streams", {
hasAudio: Boolean(stream?.getAudioTracks().length), hasAudio: Boolean(stream?.getAudioTracks().length),
hasVideo: Boolean(stream?.getVideoTracks().length), hasVideo: Boolean(stream?.getVideoTracks().length),
}); };
fireEvent(this, "load");
fireEvent(this, "streams", data);
this._logEvent("loadedData", data);
this._stopTimer();
}
private _startTimer() {
if (!__DEV__) {
return;
}
// eslint-disable-next-line no-console
console.time("WebRTC");
}
private _stopTimer() {
if (!__DEV__) {
return;
}
// eslint-disable-next-line no-console
console.timeEnd("WebRTC");
}
private _logEvent(msg: string, ...args: unknown[]) {
if (!__DEV__) {
return;
}
// eslint-disable-next-line no-console
console.timeLog("WebRTC", msg, ...args);
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@@ -86,6 +86,8 @@ export class HaMap extends ReactiveElement {
private _mapZones: Array<Marker | Circle> = []; private _mapZones: Array<Marker | Circle> = [];
private _mapFocusZones: Array<Marker | Circle> = [];
private _mapPaths: Array<Polyline | CircleMarker> = []; private _mapPaths: Array<Polyline | CircleMarker> = [];
public connectedCallback(): void { public connectedCallback(): void {
@@ -201,7 +203,11 @@ export class HaMap extends ReactiveElement {
return; return;
} }
if (!this._mapFocusItems.length && !this.layers?.length) { if (
!this._mapFocusItems.length &&
!this._mapFocusZones.length &&
!this.layers?.length
) {
this.leafletMap.setView( this.leafletMap.setView(
new this.Leaflet.LatLng( new this.Leaflet.LatLng(
this.hass.config.latitude, this.hass.config.latitude,
@@ -218,13 +224,9 @@ export class HaMap extends ReactiveElement {
: [] : []
); );
if (this.fitZones) { this._mapFocusZones?.forEach((zone) => {
this._mapZones?.forEach((zone) => { bounds.extend("getBounds" in zone ? zone.getBounds() : zone.getLatLng());
bounds.extend(
"getBounds" in zone ? zone.getBounds() : zone.getLatLng()
);
}); });
}
this.layers?.forEach((layer: any) => { this.layers?.forEach((layer: any) => {
bounds.extend( bounds.extend(
@@ -395,6 +397,7 @@ export class HaMap extends ReactiveElement {
if (this._mapZones.length) { if (this._mapZones.length) {
this._mapZones.forEach((marker) => marker.remove()); this._mapZones.forEach((marker) => marker.remove());
this._mapZones = []; this._mapZones = [];
this._mapFocusZones = [];
} }
if (!this.entities) { if (!this.entities) {
@@ -466,13 +469,18 @@ export class HaMap extends ReactiveElement {
); );
// create circle around it // create circle around it
this._mapZones.push( const circle = Leaflet.circle([latitude, longitude], {
Leaflet.circle([latitude, longitude], {
interactive: false, interactive: false,
color: passive ? passiveZoneColor : zoneColor, color: passive ? passiveZoneColor : zoneColor,
radius, radius,
}) });
); this._mapZones.push(circle);
if (
this.fitZones &&
(typeof entity === "string" || entity.focus !== false)
) {
this._mapFocusZones.push(circle);
}
continue; continue;
} }

View File

@@ -26,7 +26,10 @@ export class HaTraceLogbook extends LitElement {
.entries=${this.logbookEntries} .entries=${this.logbookEntries}
.narrow=${this.narrow} .narrow=${this.narrow}
></ha-logbook-renderer> ></ha-logbook-renderer>
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note> <hat-logbook-note
.hass=${this.hass}
.domain=${this.trace.domain}
></hat-logbook-note>
` `
: html`<div class="padded-box"> : html`<div class="padded-box">
No Logbook entries found for this step. No Logbook entries found for this step.

View File

@@ -291,7 +291,10 @@ export class HaTracePathDetails extends LitElement {
.entries=${entries} .entries=${entries}
.narrow=${this.narrow} .narrow=${this.narrow}
></ha-logbook-renderer> ></ha-logbook-renderer>
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note> <hat-logbook-note
.hass=${this.hass}
.domain=${this.trace.domain}
></hat-logbook-note>
` `
: html`<div class="padded-box"> : html`<div class="padded-box">
${this.hass!.localize( ${this.hass!.localize(

View File

@@ -28,7 +28,10 @@ export class HaTraceTimeline extends LitElement {
allowPick allowPick
> >
</hat-trace-timeline> </hat-trace-timeline>
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note> <hat-logbook-note
.hass=${this.hass}
.domain=${this.trace.domain}
></hat-logbook-note>
`; `;
} }

View File

@@ -1,14 +1,22 @@
import { css, html, LitElement } from "lit"; import { css, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import type { HomeAssistant } from "../../types";
@customElement("hat-logbook-note") @customElement("hat-logbook-note")
class HatLogbookNote extends LitElement { class HatLogbookNote extends LitElement {
@property() public domain = "automation"; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public domain: "automation" | "script" = "automation";
render() { render() {
return html` if (this.domain === "script") {
Not all shown logbook entries might be related to this ${this.domain}. return this.hass.localize(
`; "ui.panel.config.automation.trace.messages.not_all_entries_are_related_script_note"
);
}
return this.hass.localize(
"ui.panel.config.automation.trace.messages.not_all_entries_are_related_automation_note"
);
} }
static styles = css` static styles = css`

View File

@@ -193,7 +193,7 @@ export const fetchHassioLogs = async (
) => ) =>
hass.callApiRaw( hass.callApiRaw(
"GET", "GET",
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs/boots/${boot}`, `hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs${boot !== 0 ? `/boots/${boot}` : ""}`,
undefined, undefined,
range range
? { ? {
@@ -203,20 +203,6 @@ export const fetchHassioLogs = async (
); );
export const fetchHassioLogsFollow = async ( export const fetchHassioLogsFollow = async (
hass: HomeAssistant,
provider: string,
signal: AbortSignal,
lines = 100
) =>
hass.callApiRaw(
"GET",
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs/follow?lines=${lines}`,
undefined,
undefined,
signal
);
export const fetchHassioLogsBootFollow = async (
hass: HomeAssistant, hass: HomeAssistant,
provider: string, provider: string,
signal: AbortSignal, signal: AbortSignal,
@@ -225,7 +211,7 @@ export const fetchHassioLogsBootFollow = async (
) => ) =>
hass.callApiRaw( hass.callApiRaw(
"GET", "GET",
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs/boots/${boot}/follow?lines=${lines}`, `hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs${boot !== 0 ? `/boots/${boot}` : ""}/follow?lines=${lines}`,
undefined, undefined,
undefined, undefined,
signal signal
@@ -236,19 +222,14 @@ export const getHassioLogDownloadUrl = (provider: string) =>
provider.includes("_") ? `addons/${provider}` : provider provider.includes("_") ? `addons/${provider}` : provider
}/logs`; }/logs`;
export const getHassioLogDownloadLinesUrl = (provider: string, lines: number) => export const getHassioLogDownloadLinesUrl = (
`/api/hassio/${
provider.includes("_") ? `addons/${provider}` : provider
}/logs?lines=${lines}`;
export const getHassioLogBootDownloadLinesUrl = (
provider: string, provider: string,
lines: number, lines: number,
boot = 0 boot = 0
) => ) =>
`/api/hassio/${ `/api/hassio/${
provider.includes("_") ? `addons/${provider}` : provider provider.includes("_") ? `addons/${provider}` : provider
}/logs/boots/${boot}?lines=${lines}`; }/logs${boot !== 0 ? `/boots/${boot}` : ""}?lines=${lines}`;
export const setSupervisorOption = async ( export const setSupervisorOption = async (
hass: HomeAssistant, hass: HomeAssistant,

View File

@@ -1,4 +1,5 @@
import { clear, get, set, createStore, promisifyRequest } from "idb-keyval"; import { clear, get, set, createStore, promisifyRequest } from "idb-keyval";
import memoizeOne from "memoize-one";
import { promiseTimeout } from "../common/util/promise-timeout"; import { promiseTimeout } from "../common/util/promise-timeout";
import { iconMetadata } from "../resources/icon-metadata"; import { iconMetadata } from "../resources/icon-metadata";
import type { IconMeta } from "../types"; import type { IconMeta } from "../types";
@@ -11,7 +12,23 @@ export interface Chunks {
[key: string]: Promise<Icons>; [key: string]: Promise<Icons>;
} }
export const iconStore = createStore("hass-icon-db", "mdi-icon-store"); const getStore = memoizeOne(async () => {
const iconStore = createStore("hass-icon-db", "mdi-icon-store");
// Supervisor doesn't use icons, and should not update/downgrade the icon DB.
if (!__SUPERVISOR__) {
const version = await get("_version", iconStore);
if (!version) {
set("_version", iconMetadata.version, iconStore);
} else if (version !== iconMetadata.version) {
await clear(iconStore);
set("_version", iconMetadata.version, iconStore);
}
}
return iconStore;
});
export const MDI_PREFIXES = ["mdi", "hass", "hassio", "hademo"]; export const MDI_PREFIXES = ["mdi", "hass", "hassio", "hademo"];
@@ -28,7 +45,10 @@ export const getIcon = (iconName: string) =>
return; return;
} }
const readIcons = () => // Start initializing the store, so it's ready when we need it
const iconStoreProm = getStore();
const readIcons = async () => {
const iconStore = await iconStoreProm;
iconStore("readonly", (store) => { iconStore("readonly", (store) => {
for (const [iconName_, resolve_, reject_] of toRead) { for (const [iconName_, resolve_, reject_] of toRead) {
promisifyRequest<string | undefined>(store.get(iconName_)) promisifyRequest<string | undefined>(store.get(iconName_))
@@ -37,6 +57,7 @@ export const getIcon = (iconName: string) =>
} }
toRead = []; toRead = [];
}); });
};
promiseTimeout(1000, readIcons()).catch((e) => { promiseTimeout(1000, readIcons()).catch((e) => {
// Firefox in private mode doesn't support IDB // Firefox in private mode doesn't support IDB
@@ -62,6 +83,7 @@ export const findIconChunk = (icon: string): string => {
export const writeCache = async (chunks: Chunks) => { export const writeCache = async (chunks: Chunks) => {
const keys = Object.keys(chunks); const keys = Object.keys(chunks);
const iconsSets: Icons[] = await Promise.all(Object.values(chunks)); const iconsSets: Icons[] = await Promise.all(Object.values(chunks));
const iconStore = await getStore();
// We do a batch opening the store just once, for (considerable) performance // We do a batch opening the store just once, for (considerable) performance
iconStore("readwrite", (store) => { iconStore("readwrite", (store) => {
iconsSets.forEach((icons, idx) => { iconsSets.forEach((icons, idx) => {
@@ -72,14 +94,3 @@ export const writeCache = async (chunks: Chunks) => {
}); });
}); });
}; };
export const checkCacheVersion = async () => {
const version = await get("_version", iconStore);
if (!version) {
set("_version", iconMetadata.version, iconStore);
} else if (version !== iconMetadata.version) {
await clear(iconStore);
set("_version", iconMetadata.version, iconStore);
}
};

View File

@@ -209,6 +209,17 @@ export interface ZWaveJSNodeStatus {
has_firmware_update_cc: boolean; has_firmware_update_cc: boolean;
} }
export type ZWaveJSNodeCapabilities = {
[endpoint: number]: ZWaveJSEndpointCapability[];
};
export interface ZWaveJSEndpointCapability {
id: number;
name: string;
version: number;
is_secure: boolean;
}
export interface ZwaveJSNodeMetadata { export interface ZwaveJSNodeMetadata {
node_id: number; node_id: number;
exclusion: string; exclusion: string;
@@ -404,6 +415,25 @@ export interface RequestedGrant {
clientSideAuth: boolean; clientSideAuth: boolean;
} }
export const invokeZWaveCCApi = (
hass: HomeAssistant,
device_id: string,
command_class: number,
endpoint: number | undefined,
method_name: string,
parameters: any[],
wait_for_result?: boolean
): Promise<unknown> =>
hass.callWS({
type: "zwave_js/invoke_cc_api",
device_id,
command_class,
endpoint,
method_name,
parameters,
wait_for_result,
});
export const fetchZwaveNetworkStatus = ( export const fetchZwaveNetworkStatus = (
hass: HomeAssistant, hass: HomeAssistant,
device_or_entry_id: { device_or_entry_id: {
@@ -579,6 +609,15 @@ export const fetchZwaveNodeStatus = (
device_id, device_id,
}); });
export const fetchZwaveNodeCapabilities = (
hass: HomeAssistant,
device_id: string
): Promise<ZWaveJSNodeCapabilities> =>
hass.callWS({
type: "zwave_js/node_capabilities",
device_id,
});
export const subscribeZwaveNodeStatus = ( export const subscribeZwaveNodeStatus = (
hass: HomeAssistant, hass: HomeAssistant,
device_id: string, device_id: string,

View File

@@ -1,10 +1,24 @@
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { format } from "date-fns";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/ha-climate-state";
import "../../../components/ha-cover-controls";
import "../../../components/ha-cover-tilt-controls";
import "../../../components/ha-date-input";
import "../../../components/ha-humidifier-state";
import "../../../components/ha-select";
import "../../../components/ha-slider";
import "../../../components/ha-time-input";
import "../../../components/entity/ha-entity-toggle";
import "../../../components/entity/state-badge"; import "../../../components/entity/state-badge";
import { isTiltOnly } from "../../../data/cover";
import { isUnavailableState } from "../../../data/entity"; import { isUnavailableState } from "../../../data/entity";
import type { ImageEntity } from "../../../data/image";
import { computeImageUrl } from "../../../data/image";
import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor"; import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor";
import "../../../panels/lovelace/components/hui-timestamp-display"; import "../../../panels/lovelace/components/hui-timestamp-display";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
@@ -28,18 +42,7 @@ class EntityPreviewRow extends LitElement {
<div class="name" .title=${computeStateName(stateObj)}> <div class="name" .title=${computeStateName(stateObj)}>
${computeStateName(stateObj)} ${computeStateName(stateObj)}
</div> </div>
<div class="value"> <div class="value">${this.renderEntityState(stateObj)}</div>`;
${stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP &&
!isUnavailableState(stateObj.state)
? html`
<hui-timestamp-display
.hass=${this.hass}
.ts=${new Date(stateObj.state)}
capitalize
></hui-timestamp-display>
`
: this.hass.formatEntityState(stateObj)}
</div>`;
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
@@ -59,8 +62,308 @@ class EntityPreviewRow extends LitElement {
.value { .value {
direction: ltr; direction: ltr;
} }
.numberflex {
display: flex;
align-items: center;
justify-content: flex-end;
flex-grow: 2;
}
.numberstate {
min-width: 45px;
text-align: end;
}
ha-textfield {
text-align: end;
direction: ltr !important;
}
ha-slider {
width: 100%;
max-width: 200px;
}
ha-time-input {
margin-left: 4px;
margin-inline-start: 4px;
margin-inline-end: initial;
direction: var(--direction);
}
.datetimeflex {
display: flex;
justify-content: flex-end;
width: 100%;
}
mwc-button {
margin-right: -0.57em;
margin-inline-end: -0.57em;
margin-inline-start: initial;
}
img {
display: block;
width: 100%;
}
`; `;
} }
private renderEntityState(stateObj: HassEntity): TemplateResult | string {
const domain = stateObj.entity_id.split(".", 1)[0];
if (domain === "button") {
return html`
<mwc-button .disabled=${isUnavailableState(stateObj.state)}>
${this.hass.localize("ui.card.button.press")}
</mwc-button>
`;
}
const climateDomains = ["climate", "water_heater"];
if (climateDomains.includes(domain)) {
return html`
<ha-climate-state .hass=${this.hass} .stateObj=${stateObj}>
</ha-climate-state>
`;
}
if (domain === "cover") {
return html`
${isTiltOnly(stateObj)
? html`
<ha-cover-tilt-controls
.hass=${this.hass}
.stateObj=${stateObj}
></ha-cover-tilt-controls>
`
: html`
<ha-cover-controls
.hass=${this.hass}
.stateObj=${stateObj}
></ha-cover-controls>
`}
`;
}
if (domain === "date") {
return html`
<ha-date-input
.locale=${this.hass.locale}
.disabled=${isUnavailableState(stateObj.state)}
.value=${isUnavailableState(stateObj.state)
? undefined
: stateObj.state}
>
</ha-date-input>
`;
}
if (domain === "datetime") {
const dateObj = isUnavailableState(stateObj.state)
? undefined
: new Date(stateObj.state);
const time = dateObj ? format(dateObj, "HH:mm:ss") : undefined;
const date = dateObj ? format(dateObj, "yyyy-MM-dd") : undefined;
return html`
<div class="datetimeflex">
<ha-date-input
.label=${computeStateName(stateObj)}
.locale=${this.hass.locale}
.value=${date}
.disabled=${isUnavailableState(stateObj.state)}
>
</ha-date-input>
<ha-time-input
.value=${time}
.disabled=${isUnavailableState(stateObj.state)}
.locale=${this.hass.locale}
></ha-time-input>
</div>
`;
}
if (domain === "event") {
return html`
<div class="when">
${isUnavailableState(stateObj.state)
? this.hass.formatEntityState(stateObj)
: html`<hui-timestamp-display
.hass=${this.hass}
.ts=${new Date(stateObj.state)}
capitalize
></hui-timestamp-display>`}
</div>
<div class="what">
${isUnavailableState(stateObj.state)
? nothing
: this.hass.formatEntityAttributeValue(stateObj, "event_type")}
</div>
`;
}
const toggleDomains = ["fan", "light", "remote", "siren", "switch"];
if (toggleDomains.includes(domain)) {
const showToggle =
stateObj.state === "on" ||
stateObj.state === "off" ||
isUnavailableState(stateObj.state);
return html`
${showToggle
? html`
<ha-entity-toggle
.hass=${this.hass}
.stateObj=${stateObj}
></ha-entity-toggle>
`
: this.hass.formatEntityState(stateObj)}
`;
}
if (domain === "humidifier") {
return html`
<ha-humidifier-state .hass=${this.hass} .stateObj=${stateObj}>
</ha-humidifier-state>
`;
}
if (domain === "image") {
const image: string = computeImageUrl(stateObj as ImageEntity);
return html`
<img
alt=${ifDefined(stateObj?.attributes.friendly_name)}
src=${this.hass.hassUrl(image)}
/>
`;
}
if (domain === "lock") {
return html`
<mwc-button
.disabled=${isUnavailableState(stateObj.state)}
class="text-content"
>
${stateObj.state === "locked"
? this.hass!.localize("ui.card.lock.unlock")
: this.hass!.localize("ui.card.lock.lock")}
</mwc-button>
`;
}
if (domain === "number") {
const showNumberSlider =
stateObj.attributes.mode === "slider" ||
(stateObj.attributes.mode === "auto" &&
(Number(stateObj.attributes.max) - Number(stateObj.attributes.min)) /
Number(stateObj.attributes.step) <=
256);
return html`
${showNumberSlider
? html`
<div class="numberflex">
<ha-slider
labeled
.disabled=${isUnavailableState(stateObj.state)}
.step=${Number(stateObj.attributes.step)}
.min=${Number(stateObj.attributes.min)}
.max=${Number(stateObj.attributes.max)}
.value=${Number(stateObj.state)}
></ha-slider>
<span class="state">
${this.hass.formatEntityState(stateObj)}
</span>
</div>
`
: html` <div class="numberflex numberstate">
<ha-textfield
autoValidate
.disabled=${isUnavailableState(stateObj.state)}
pattern="[0-9]+([\\.][0-9]+)?"
.step=${Number(stateObj.attributes.step)}
.min=${Number(stateObj.attributes.min)}
.max=${Number(stateObj.attributes.max)}
.value=${stateObj.state}
.suffix=${stateObj.attributes.unit_of_measurement}
type="number"
></ha-textfield>
</div>`}
`;
}
if (domain === "select") {
return html`
<ha-select
.label=${computeStateName(stateObj)}
.value=${stateObj.state}
.disabled=${isUnavailableState(stateObj.state)}
naturalMenuWidth
>
${stateObj.attributes.options
? stateObj.attributes.options.map(
(option) => html`
<mwc-list-item .value=${option}>
${this.hass!.formatEntityState(stateObj, option)}
</mwc-list-item>
`
)
: ""}
</ha-select>
`;
}
if (domain === "sensor") {
const showSensor =
stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP &&
!isUnavailableState(stateObj.state);
return html`
${showSensor
? html`
<hui-timestamp-display
.hass=${this.hass}
.ts=${new Date(stateObj.state)}
capitalize
></hui-timestamp-display>
`
: this.hass.formatEntityState(stateObj)}
`;
}
if (domain === "text") {
return html`
<ha-textfield
.label=${computeStateName(stateObj)}
.disabled=${isUnavailableState(stateObj.state)}
.value=${stateObj.state}
.minlength=${stateObj.attributes.min}
.maxlength=${stateObj.attributes.max}
.autoValidate=${stateObj.attributes.pattern}
.pattern=${stateObj.attributes.pattern}
.type=${stateObj.attributes.mode}
placeholder=${this.hass!.localize("ui.card.text.emtpy_value")}
></ha-textfield>
`;
}
if (domain === "time") {
return html`
<ha-time-input
.value=${isUnavailableState(stateObj.state)
? undefined
: stateObj.state}
.locale=${this.hass.locale}
.disabled=${isUnavailableState(stateObj.state)}
></ha-time-input>
`;
}
if (domain === "weather") {
return html`
<div>
${isUnavailableState(stateObj.state) ||
stateObj.attributes.temperature === undefined ||
stateObj.attributes.temperature === null
? this.hass.formatEntityState(stateObj)
: this.hass.formatEntityAttributeValue(stateObj, "temperature")}
</div>
`;
}
return this.hass.formatEntityState(stateObj);
}
} }
declare global { declare global {

View File

@@ -52,6 +52,7 @@ class MoreInfoUpdate extends LitElement {
return html` return html`
<div class="content"> <div class="content">
<div class="summary">
${this.stateObj.attributes.in_progress ${this.stateObj.attributes.in_progress
? supportsFeature(this.stateObj, UpdateEntityFeature.PROGRESS) && ? supportsFeature(this.stateObj, UpdateEntityFeature.PROGRESS) &&
this.stateObj.attributes.update_percentage !== null this.stateObj.attributes.update_percentage !== null
@@ -105,6 +106,7 @@ class MoreInfoUpdate extends LitElement {
</div> </div>
</div>` </div>`
: nothing} : nothing}
</div>
${supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES) && ${supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES) &&
!this._error !this._error
? this._releaseNotes === undefined ? this._releaseNotes === undefined
@@ -143,7 +145,7 @@ class MoreInfoUpdate extends LitElement {
)} )}
</span> </span>
<ha-switch <ha-switch
id="create_backup" id="create-backup"
checked checked
.disabled=${updateIsInstalling(this.stateObj)} .disabled=${updateIsInstalling(this.stateObj)}
></ha-switch> ></ha-switch>
@@ -293,6 +295,11 @@ class MoreInfoUpdate extends LitElement {
ha-expansion-panel { ha-expansion-panel {
margin: 16px 0; margin: 16px 0;
} }
.summary {
margin-bottom: 16px;
}
.row { .row {
margin: 0; margin: 0;
display: flex; display: flex;
@@ -308,7 +315,9 @@ class MoreInfoUpdate extends LitElement {
); );
position: sticky; position: sticky;
bottom: 0; bottom: 0;
margin: 0 -24px -24px -24px; margin: 0 -24px 0 -24px;
margin-bottom: calc(-1 * max(env(safe-area-inset-bottom), 24px));
padding-bottom: env(safe-area-inset-bottom);
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -66,6 +66,8 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
private _dialogClosed() { private _dialogClosed() {
this._params = undefined; this._params = undefined;
this._assistConfiguration = undefined; this._assistConfiguration = undefined;
this._previousSteps = [];
this._nextStep = undefined;
this._step = STEP.INIT; this._step = STEP.INIT;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }

View File

@@ -17,7 +17,7 @@ export class HaVoiceAssistantSetupStepArea extends LitElement {
const device = this.hass.devices[this.deviceId]; const device = this.hass.devices[this.deviceId];
return html`<div class="content"> return html`<div class="content">
<img src="/static/images/voice-assistant/area.gif" /> <img src="/static/images/voice-assistant/area.png" />
<h1>Select area</h1> <h1>Select area</h1>
<p class="secondary"> <p class="secondary">
When you voice assistant knows where it is, it can better control the When you voice assistant knows where it is, it can better control the

View File

@@ -21,7 +21,7 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
protected override render() { protected override render() {
return html`<div class="padding content"> return html`<div class="padding content">
<img src="/static/images/voice-assistant/change-wake-word.gif" /> <img src="/static/images/voice-assistant/change-wake-word.png" />
<h1>Change wake word</h1> <h1>Change wake word</h1>
<p class="secondary"> <p class="secondary">
Some wake words are better for Some wake words are better for

View File

@@ -6,6 +6,7 @@ import "../../components/ha-circular-progress";
import { testAssistSatelliteConnection } from "../../data/assist_satellite"; import { testAssistSatelliteConnection } from "../../data/assist_satellite";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { AssistantSetupStyles } from "./styles"; import { AssistantSetupStyles } from "./styles";
import { documentationUrl } from "../../util/documentation-url";
@customElement("ha-voice-assistant-setup-step-check") @customElement("ha-voice-assistant-setup-step-check")
export class HaVoiceAssistantSetupStepCheck extends LitElement { export class HaVoiceAssistantSetupStepCheck extends LitElement {
@@ -35,7 +36,7 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
protected override render() { protected override render() {
return html`<div class="content"> return html`<div class="content">
${this._status === "timeout" ${this._status === "timeout"
? html`<img src="/static/images/voice-assistant/error.gif" /> ? html`<img src="/static/images/voice-assistant/error.png" />
<h1>The voice assistant is unable to connect to Home Assistant</h1> <h1>The voice assistant is unable to connect to Home Assistant</h1>
<p class="secondary"> <p class="secondary">
To play audio, the voice assistant device has to connect to Home To play audio, the voice assistant device has to connect to Home
@@ -44,12 +45,15 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
</p> </p>
<div class="footer"> <div class="footer">
<a <a
href="https://www.home-assistant.io/docs/configuration/remote/#adding-a-remote-url-to-home-assistant" href=${documentationUrl(
this.hass,
"/voice_control/troubleshooting/#i-dont-get-a-voice-response"
)}
><ha-button>Help me</ha-button></a ><ha-button>Help me</ha-button></a
> >
<ha-button @click=${this._testConnection}>Retry</ha-button> <ha-button @click=${this._testConnection}>Retry</ha-button>
</div>` </div>`
: html`<img src="/static/images/voice-assistant/hi.gif" /> : html`<img src="/static/images/voice-assistant/hi.png" />
<h1>Hi</h1> <h1>Hi</h1>
<p class="secondary"> <p class="secondary">
Over the next couple steps we're going to personalize your voice Over the next couple steps we're going to personalize your voice

View File

@@ -67,7 +67,7 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
: undefined; : undefined;
return html`<div class="content"> return html`<div class="content">
<img src="/static/images/voice-assistant/heart.gif" /> <img src="/static/images/voice-assistant/heart.png" />
<h1>Ready to Assist!</h1> <h1>Ready to Assist!</h1>
<p class="secondary"> <p class="secondary">
Make any final customizations here. You can always change these in the Make any final customizations here. You can always change these in the

View File

@@ -65,7 +65,7 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
const progressIsNumeric = stateObj && updateUsesProgress(stateObj); const progressIsNumeric = stateObj && updateUsesProgress(stateObj);
return html`<div class="content"> return html`<div class="content">
<img src="/static/images/voice-assistant/update.gif" /> <img src="/static/images/voice-assistant/update.png" />
<h1> <h1>
${stateObj && ${stateObj &&
(stateObj.state === "unavailable" || updateIsInstalling(stateObj)) (stateObj.state === "unavailable" || updateIsInstalling(stateObj))

View File

@@ -64,14 +64,14 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
return html`<div class="content"> return html`<div class="content">
${!this._detected ${!this._detected
? html` ? html`
<img src="/static/images/voice-assistant/sleep.gif" /> <img src="/static/images/voice-assistant/sleep.png" />
<h1> <h1>
Say “${this._activeWakeWord(this.assistConfiguration)}” to wake the Say “${this._activeWakeWord(this.assistConfiguration)}” to wake the
device up device up
</h1> </h1>
<p class="secondary">Setup will continue once the device is awake.</p> <p class="secondary">Setup will continue once the device is awake.</p>
</div>` </div>`
: html`<img src="/static/images/voice-assistant/ok-nabu.gif" /> : html`<img src="/static/images/voice-assistant/ok-nabu.png" />
<h1> <h1>
Say “${this._activeWakeWord(this.assistConfiguration)}” again Say “${this._activeWakeWord(this.assistConfiguration)}” again
</h1> </h1>

View File

@@ -264,6 +264,7 @@ export interface ExternalConfig {
hasAssist: boolean; hasAssist: boolean;
hasBarCodeScanner: number; hasBarCodeScanner: number;
canSetupImprov: boolean; canSetupImprov: boolean;
downloadFileSupported: boolean;
} }
export class ExternalMessaging { export class ExternalMessaging {

View File

@@ -16,6 +16,8 @@ import type { ValueChangedEvent } from "../types";
import { onBoardingStyles } from "./styles"; import { onBoardingStyles } from "./styles";
import { debounce } from "../common/util/debounce"; import { debounce } from "../common/util/debounce";
const CHECK_USERNAME_REGEX = /\s|[A-Z]/;
const CREATE_USER_SCHEMA: HaFormSchema[] = [ const CREATE_USER_SCHEMA: HaFormSchema[] = [
{ {
name: "name", name: "name",
@@ -121,6 +123,7 @@ class OnboardingCreateUser extends LitElement {
ev: ValueChangedEvent<HaFormDataContainer> ev: ValueChangedEvent<HaFormDataContainer>
): void { ): void {
const nameChanged = ev.detail.value.name !== this._newUser.name; const nameChanged = ev.detail.value.name !== this._newUser.name;
const usernameChanged = ev.detail.value.username !== this._newUser.username;
const passwordChanged = const passwordChanged =
ev.detail.value.password !== this._newUser.password || ev.detail.value.password !== this._newUser.password ||
ev.detail.value.password_confirm !== this._newUser.password_confirm; ev.detail.value.password_confirm !== this._newUser.password_confirm;
@@ -135,6 +138,9 @@ class OnboardingCreateUser extends LitElement {
this._debouncedCheckPasswordMatch(); this._debouncedCheckPasswordMatch();
} }
} }
if (usernameChanged) {
this._checkUsername();
}
} }
private _debouncedCheckPasswordMatch = debounce( private _debouncedCheckPasswordMatch = debounce(
@@ -164,6 +170,21 @@ class OnboardingCreateUser extends LitElement {
const parts = String(this._newUser.name).split(" "); const parts = String(this._newUser.name).split(" ");
if (parts.length) { if (parts.length) {
this._newUser.username = parts[0].toLowerCase(); this._newUser.username = parts[0].toLowerCase();
this._checkUsername();
}
}
private _checkUsername(): void {
const old = this._formError.username;
if (CHECK_USERNAME_REGEX.test(this._newUser.username as string)) {
this._formError.username = this.localize(
"ui.panel.page-onboarding.user.error.username_not_normalized"
);
} else {
this._formError.username = "";
}
if (old !== this._formError.username) {
this.requestUpdate("_formError");
} }
} }

View File

@@ -49,6 +49,7 @@ export class HaDelayAction extends LitElement implements ActionElement {
.disabled=${this.disabled} .disabled=${this.disabled}
.data=${this._timeData} .data=${this._timeData}
enableMillisecond enableMillisecond
required
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-duration-input>`; ></ha-duration-input>`;
} }

View File

@@ -67,9 +67,6 @@ export class HaWaitForTriggerAction
private _timeoutChanged(ev: CustomEvent<{ value: TimeChangedEvent }>): void { private _timeoutChanged(ev: CustomEvent<{ value: TimeChangedEvent }>): void {
ev.stopPropagation(); ev.stopPropagation();
const value = ev.detail.value; const value = ev.detail.value;
if (!value) {
return;
}
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { ...this.action, timeout: value }, value: { ...this.action, timeout: value },
}); });

View File

@@ -370,7 +370,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
}`, }`,
description: description:
this.hass.localize( this.hass.localize(
`component.${domain}.services.${service}.description` `component.${dmn}.services.${service}.description`
) || services[dmn][service]?.description, ) || services[dmn][service]?.description,
}); });
} }

View File

@@ -712,8 +712,12 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
private async _duplicate() { private async _duplicate() {
const result = this._readOnly const result = this._readOnly
? await showConfirmationDialog(this, { ? await showConfirmationDialog(this, {
title: "Migrate automation?", title: this.hass.localize(
text: "You can migrate this automation, so it can be edited from the UI. After it is migrated and you have saved it, you will have to manually delete your old automation from your configuration. Do you want to migrate this automation?", "ui.panel.config.automation.picker.migrate_automation"
),
text: this.hass.localize(
"ui.panel.config.automation.picker.migrate_automation_description"
),
}) })
: await this.confirmUnsavedChanged(); : await this.confirmUnsavedChanged();
if (result) { if (result) {

View File

@@ -46,7 +46,7 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement {
], ],
], ],
}, },
{ name: "offset", selector: { duration: {} } }, { name: "offset", required: true, selector: { duration: {} } },
{ {
name: "offset_type", name: "offset_type",
type: "select", type: "select",

View File

@@ -5,6 +5,7 @@ import {
mdiHospitalBox, mdiHospitalBox,
mdiInformation, mdiInformation,
mdiUpload, mdiUpload,
mdiWrench,
} from "@mdi/js"; } from "@mdi/js";
import { getConfigEntries } from "../../../../../../data/config_entries"; import { getConfigEntries } from "../../../../../../data/config_entries";
import type { DeviceRegistryEntry } from "../../../../../../data/device_registry"; import type { DeviceRegistryEntry } from "../../../../../../data/device_registry";
@@ -98,6 +99,13 @@ export const getZwaveDeviceActions = async (
showZWaveJSNodeStatisticsDialog(el, { showZWaveJSNodeStatisticsDialog(el, {
device, device,
}), }),
},
{
label: hass.localize(
"ui.panel.config.zwave_js.device_info.installer_settings"
),
icon: mdiWrench,
href: `/config/zwave_js/node_installer/${device.id}?config_entry=${entryId}`,
} }
); );
} }

View File

@@ -584,6 +584,10 @@ class AddIntegrationDialog extends LitElement {
}); });
if (configEntries.length > 0) { if (configEntries.length > 0) {
this.closeDialog(); this.closeDialog();
const localize = await this.hass.loadBackendTranslation(
"title",
integration.name
);
showAlertDialog(this, { showAlertDialog(this, {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.integrations.config_flow.single_config_entry_title" "ui.panel.config.integrations.config_flow.single_config_entry_title"
@@ -591,7 +595,7 @@ class AddIntegrationDialog extends LitElement {
text: this.hass.localize( text: this.hass.localize(
"ui.panel.config.integrations.config_flow.single_config_entry", "ui.panel.config.integrations.config_flow.single_config_entry",
{ {
integration_name: integration.name, integration_name: domainToName(localize, integration.name),
} }
), ),
}); });

View File

@@ -1387,6 +1387,10 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
this._extraConfigEntries || this.configEntries this._extraConfigEntries || this.configEntries
); );
if (entries.length > 0) { if (entries.length > 0) {
const localize = await this.hass.loadBackendTranslation(
"title",
this._manifest.name
);
await showAlertDialog(this, { await showAlertDialog(this, {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.integrations.config_flow.single_config_entry_title" "ui.panel.config.integrations.config_flow.single_config_entry_title"
@@ -1394,7 +1398,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
text: this.hass.localize( text: this.hass.localize(
"ui.panel.config.integrations.config_flow.single_config_entry", "ui.panel.config.integrations.config_flow.single_config_entry",
{ {
integration_name: this._manifest.name, integration_name: domainToName(localize, this._manifest.name),
} }
), ),
}); });

View File

@@ -744,6 +744,10 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) {
if (integration.single_config_entry) { if (integration.single_config_entry) {
const configEntries = await getConfigEntries(this.hass, { domain }); const configEntries = await getConfigEntries(this.hass, { domain });
if (configEntries.length > 0) { if (configEntries.length > 0) {
const localize = await this.hass.loadBackendTranslation(
"title",
integration.name
);
showAlertDialog(this, { showAlertDialog(this, {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.integrations.config_flow.single_config_entry_title" "ui.panel.config.integrations.config_flow.single_config_entry_title"
@@ -751,7 +755,7 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) {
text: this.hass.localize( text: this.hass.localize(
"ui.panel.config.integrations.config_flow.single_config_entry", "ui.panel.config.integrations.config_flow.single_config_entry",
{ {
integration_name: integration.name, integration_name: domainToName(localize, integration.name!),
} }
), ),
}); });

View File

@@ -44,7 +44,6 @@ class MatterAddDeviceGoogleHome extends LitElement {
home_assistant: html`<b>Home Assistant</b>`, home_assistant: html`<b>Home Assistant</b>`,
} }
)} )}
<br />
<span <span
class="link" class="link"
type="button" type="button"
@@ -57,13 +56,13 @@ class MatterAddDeviceGoogleHome extends LitElement {
)} )}
</span> </span>
</li> </li>
</ol> <li>
<br />
<p>
${this.hass.localize( ${this.hass.localize(
`ui.dialogs.matter-add-device.google_home.redirect` `ui.dialogs.matter-add-device.google_home.redirect`
)} )}
</p> </li>
</ol>
<br />
</div> </div>
`; `;
} }

View File

@@ -0,0 +1,152 @@
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../../components/buttons/ha-progress-button";
import type { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import type { HomeAssistant } from "../../../../../../types";
import { invokeZWaveCCApi } from "../../../../../../data/zwave_js";
import "../../../../../../components/ha-textfield";
import "../../../../../../components/ha-select";
import "../../../../../../components/ha-list-item";
import "../../../../../../components/ha-alert";
import "../../../../../../components/ha-formfield";
import "../../../../../../components/ha-switch";
import type { HaProgressButton } from "../../../../../../components/buttons/ha-progress-button";
import type { HaSelect } from "../../../../../../components/ha-select";
import type { HaTextField } from "../../../../../../components/ha-textfield";
import type { HaSwitch } from "../../../../../../components/ha-switch";
import { extractApiErrorMessage } from "../../../../../../data/hassio/common";
@customElement("zwave_js-capability-control-multilevel_switch")
class ZWaveJSCapabilityMultiLevelSwitch extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public device!: DeviceRegistryEntry;
@property({ type: Number }) public endpoint!: number;
@property({ type: Number }) public command_class!: number;
@property({ type: Number }) public version!: number;
@state() private _error?: string;
protected render() {
return html`
<h3>
${this.hass.localize(
"ui.panel.config.zwave_js.node_installer.capability_controls.multilevel_switch.title"
)}
</h3>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<ha-select
.label=${this.hass.localize(
"ui.panel.config.zwave_js.node_installer.capability_controls.multilevel_switch.direction"
)}
id="direction"
>
<ha-list-item .value=${"up"} selected
>${this.hass.localize(
"ui.panel.config.zwave_js.node_installer.capability_controls.multilevel_switch.up"
)}</ha-list-item
>
<ha-list-item .value=${"down"}
>${this.hass.localize(
"ui.panel.config.zwave_js.node_installer.capability_controls.multilevel_switch.down"
)}</ha-list-item
>
</ha-select>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.zwave_js.node_installer.capability_controls.multilevel_switch.ignore_start_level"
)}
>
<ha-switch id="ignore_start_level"></ha-switch>
</ha-formfield>
<ha-textfield
type="number"
id="start_level"
value="0"
.label=${this.hass.localize(
"ui.panel.config.zwave_js.node_installer.capability_controls.multilevel_switch.start_level"
)}
></ha-textfield>
<div class="actions">
<ha-progress-button
.control=${"startLevelChange"}
@click=${this._controlTransition}
>
${this.hass.localize(
"ui.panel.config.zwave_js.node_installer.capability_controls.multilevel_switch.start_transition"
)}
</ha-progress-button>
<ha-progress-button
.control=${"stopLevelChange"}
@click=${this._controlTransition}
>
${this.hass.localize(
"ui.panel.config.zwave_js.node_installer.capability_controls.multilevel_switch.stop_transition"
)}
</ha-progress-button>
</div>
`;
}
private async _controlTransition(ev: any) {
const control = ev.currentTarget!.control;
const button = ev.currentTarget as HaProgressButton;
button.progress = true;
const direction = (this.shadowRoot!.getElementById("direction") as HaSelect)
.value;
const ignoreStartLevel = (
this.shadowRoot!.getElementById("ignore_start_level") as HaSwitch
).checked;
const startLevel = Number(
(this.shadowRoot!.getElementById("start_level") as HaTextField).value
);
try {
button.actionSuccess();
await invokeZWaveCCApi(
this.hass,
this.device.id,
this.command_class,
this.endpoint,
control,
[{ direction, ignoreStartLevel, startLevel }],
true
);
} catch (err) {
button.actionError();
this._error = this.hass.localize(
"ui.panel.config.zwave_js.node_installer.capability_controls.multilevel_switch.control_failed",
{ error: extractApiErrorMessage(err) }
);
}
button.progress = false;
}
static styles = css`
ha-select,
ha-formfield,
ha-textfield {
display: block;
margin-bottom: 8px;
}
.actions {
display: flex;
justify-content: flex-end;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"zwave_js-capability-control-multilevel_switch": ZWaveJSCapabilityMultiLevelSwitch;
}
}

View File

@@ -0,0 +1,241 @@
import { LitElement, css, html } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import type { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import type { HomeAssistant } from "../../../../../../types";
import { invokeZWaveCCApi } from "../../../../../../data/zwave_js";
import "../../../../../../components/ha-button";
import "../../../../../../components/buttons/ha-progress-button";
import "../../../../../../components/ha-textfield";
import "../../../../../../components/ha-select";
import "../../../../../../components/ha-list-item";
import "../../../../../../components/ha-alert";
import type { HaSelect } from "../../../../../../components/ha-select";
import type { HaTextField } from "../../../../../../components/ha-textfield";
import { extractApiErrorMessage } from "../../../../../../data/hassio/common";
import type { HaProgressButton } from "../../../../../../components/buttons/ha-progress-button";
// enum with special states
enum SpecialState {
frost_protection = "Frost Protection",
energy_saving = "Energy Saving",
unused = "Unused",
}
const SETBACK_TYPE_OPTIONS = ["none", "temporary", "permanent"];
@customElement("zwave_js-capability-control-thermostat_setback")
class ZWaveJSCapabilityThermostatSetback extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public device!: DeviceRegistryEntry;
@property({ type: Number }) public endpoint!: number;
@property({ type: Number }) public command_class!: number;
@property({ type: Number }) public version!: number;
@state() private _disableSetbackState = false;
@query("#setback_type") private _setbackTypeInput!: HaSelect;
@query("#setback_state") private _setbackStateInput!: HaTextField;
@query("#setback_special_state")
private _setbackSpecialStateSelect!: HaSelect;
@state() private _error?: string;
@state() private _loading = true;
protected render() {
return html`
<h3>
${this.hass.localize(
`ui.panel.config.zwave_js.node_installer.capability_controls.thermostat_setback.title`
)}
</h3>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<ha-select
.label=${this.hass.localize(
`ui.panel.config.zwave_js.node_installer.capability_controls.thermostat_setback.setback_type.label`
)}
id="setback_type"
.value=${"0"}
.disabled=${this._loading}
>
${SETBACK_TYPE_OPTIONS.map(
(translationKey, index) =>
html`<ha-list-item .value=${String(index)}>
${this.hass.localize(
`ui.panel.config.zwave_js.node_installer.capability_controls.thermostat_setback.setback_type.${translationKey}`
)}
</ha-list-item>`
)}
</ha-select>
<div class="setback-state">
<ha-textfield
type="number"
id="setback_state"
value="0"
.label=${this.hass.localize(
`ui.panel.config.zwave_js.node_installer.capability_controls.thermostat_setback.setback_state_label`
)}
min="-12.8"
max="12.0"
step=".1"
.helper=${this.hass.localize(
`ui.panel.config.zwave_js.node_installer.capability_controls.thermostat_setback.setback_state_helper`
)}
.disabled=${this._disableSetbackState || this._loading}
></ha-textfield>
<ha-select
.label=${this.hass.localize(
`ui.panel.config.zwave_js.node_installer.capability_controls.thermostat_setback.setback_special_state.label`
)}
id="setback_special_state"
@change=${this._changeSpecialState}
.disabled=${this._loading}
>
<ha-list-item selected> </ha-list-item>
${Object.entries(SpecialState).map(
([translationKey, value]) =>
html`<ha-list-item .value=${value}>
${this.hass.localize(
`ui.panel.config.zwave_js.node_installer.capability_controls.thermostat_setback.setback_special_state.${translationKey}`
)}
</ha-list-item>`
)}
</ha-select>
</div>
<div class="actions">
<ha-button
class="clear-button"
@click=${this._clear}
.disabled=${this._loading}
>${this.hass.localize("ui.common.clear")}</ha-button
>
<ha-progress-button
@click=${this._saveSetback}
.disabled=${this._loading}
>
${this.hass.localize("ui.common.save")}
</ha-progress-button>
</div>
`;
}
protected firstUpdated() {
this._loadSetback();
}
private async _loadSetback() {
this._loading = true;
try {
const { setbackType, setbackState } = (await invokeZWaveCCApi(
this.hass,
this.device.id,
this.command_class,
this.endpoint,
"get",
[],
true
)) as { setbackType: number; setbackState: number | SpecialState };
this._setbackTypeInput.value = String(setbackType);
if (typeof setbackState === "number") {
this._setbackStateInput.value = String(setbackState);
this._setbackSpecialStateSelect.value = "";
} else {
this._setbackSpecialStateSelect.value = setbackState;
}
} catch (err) {
this._error = this.hass.localize(
"ui.panel.config.zwave_js.node_installer.capability_controls.thermostat_setback.get_setback_failed",
{ error: extractApiErrorMessage(err) }
);
}
this._loading = false;
}
private _changeSpecialState() {
this._disableSetbackState = !!this._setbackSpecialStateSelect.value;
}
private async _saveSetback(ev: CustomEvent) {
const button = ev.currentTarget as HaProgressButton;
button.progress = true;
this._error = undefined;
const setbackType = this._setbackTypeInput.value;
let setbackState: number | string = Number(this._setbackStateInput.value);
if (this._setbackSpecialStateSelect.value) {
setbackState = this._setbackSpecialStateSelect.value;
}
try {
await invokeZWaveCCApi(
this.hass,
this.device.id,
this.command_class,
this.endpoint,
"set",
[Number(setbackType), setbackState],
true
);
button.actionSuccess();
} catch (err) {
button.actionError();
this._error = this.hass.localize(
"ui.panel.config.zwave_js.node_installer.capability_controls.thermostat_setback.save_setback_failed",
{ error: extractApiErrorMessage(err) }
);
}
button.progress = false;
}
private _clear() {
this._loadSetback();
}
static styles = css`
:host {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
:host > ha-select {
width: 100%;
}
.actions {
width: 100%;
display: flex;
justify-content: flex-end;
}
.actions .clear-button {
--mdc-theme-primary: var(--red-color);
}
.setback-state {
width: 100%;
display: flex;
gap: 16px;
}
.setback-state ha-select,
ha-textfield {
flex: 1;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"zwave_js-capability-control-thermostat_setback": ZWaveJSCapabilityThermostatSetback;
}
}

View File

@@ -690,9 +690,6 @@ class DialogZWaveJSAddNode extends LitElement {
provisioningInfo provisioningInfo
); );
this._status = "provisioned"; this._status = "provisioned";
if (this._params?.addedCallback) {
this._params.addedCallback();
}
} catch (err: any) { } catch (err: any) {
this._error = err.message; this._error = err.message;
this._status = "failed"; this._status = "failed";
@@ -831,9 +828,6 @@ class DialogZWaveJSAddNode extends LitElement {
if (message.event === "interview completed") { if (message.event === "interview completed") {
this._unsubscribe(); this._unsubscribe();
this._status = "finished"; this._status = "finished";
if (this._params?.addedCallback) {
this._params.addedCallback();
}
} }
if (message.event === "interview stage completed") { if (message.event === "interview stage completed") {
@@ -874,6 +868,9 @@ class DialogZWaveJSAddNode extends LitElement {
} }
if (this._entryId) { if (this._entryId) {
stopZwaveInclusion(this.hass, this._entryId); stopZwaveInclusion(this.hass, this._entryId);
if (this._params?.onStop) {
this._params.onStop();
}
} }
this._requestedGrant = undefined; this._requestedGrant = undefined;
this._dsk = undefined; this._dsk = undefined;

View File

@@ -2,7 +2,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
export interface ZWaveJSAddNodeDialogParams { export interface ZWaveJSAddNodeDialogParams {
entry_id: string; entry_id: string;
addedCallback?: () => void; onStop?: () => void;
} }
export const loadAddNodeDialog = () => import("./dialog-zwave_js-add-node"); export const loadAddNodeDialog = () => import("./dialog-zwave_js-add-node");

View File

@@ -564,7 +564,8 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
private async _addNodeClicked() { private async _addNodeClicked() {
showZWaveJSAddNodeDialog(this, { showZWaveJSAddNodeDialog(this, {
entry_id: this.configEntryId!, entry_id: this.configEntryId!,
addedCallback: () => this._fetchData(), // refresh the data after the dialog is closed. add a small delay for the inclusion state to update
onStop: () => setTimeout(() => this._fetchData(), 100),
}); });
} }

View File

@@ -48,6 +48,10 @@ class ZWaveJSConfigRouter extends HassRouterPage {
tag: "zwave_js-node-config", tag: "zwave_js-node-config",
load: () => import("./zwave_js-node-config"), load: () => import("./zwave_js-node-config"),
}, },
node_installer: {
tag: "zwave_js-node-installer",
load: () => import("./zwave_js-node-installer"),
},
logs: { logs: {
tag: "zwave_js-logs", tag: "zwave_js-logs",
load: () => import("./zwave_js-logs"), load: () => import("./zwave_js-logs"),

View File

@@ -0,0 +1,210 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list-item";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive";
import "../../../../../components/ha-card";
import { computeDeviceName } from "../../../../../data/device_registry";
import type {
ZWaveJSNodeCapabilities,
ZwaveJSNodeMetadata,
} from "../../../../../data/zwave_js";
import {
fetchZwaveNodeCapabilities,
fetchZwaveNodeMetadata,
} from "../../../../../data/zwave_js";
import "../../../../../layouts/hass-error-screen";
import "../../../../../layouts/hass-loading-screen";
import "../../../../../layouts/hass-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import "../../../ha-config-section";
import "./capability-controls/zwave_js-capability-control-multilevel-switch";
import "./capability-controls/zwave_js-capability-control-thermostat-setback";
const CAPABILITY_CONTROLS = {
38: "multilevel_switch",
71: "thermostat_setback",
};
@customElement("zwave_js-node-installer")
class ZWaveJSNodeInstaller extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean }) public isWide = false;
@property() public configEntryId?: string;
@property() public deviceId!: string;
@state() private _nodeMetadata?: ZwaveJSNodeMetadata;
@state() private _capabilities?: ZWaveJSNodeCapabilities;
@state() private _error?: string;
public connectedCallback(): void {
super.connectedCallback();
this.deviceId = this.route.path.substr(1);
}
protected updated(changedProps: PropertyValues): void {
if (!this._capabilities || changedProps.has("deviceId")) {
this._fetchData();
}
}
protected render(): TemplateResult {
if (this._error) {
return html`<hass-error-screen
.hass=${this.hass}
.error=${this.hass.localize(
`ui.panel.config.zwave_js.node_config.error_${this._error}`
)}
></hass-error-screen>`;
}
if (!this._capabilities || !this._nodeMetadata) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
const device = this.hass.devices[this.deviceId];
const endpoints = Object.entries(this._capabilities).filter(
([_endpoint, capabilities]) => {
const filteredCapabilities = capabilities.filter(
(capability) => capability.id in CAPABILITY_CONTROLS
);
return filteredCapabilities.length > 0;
}
);
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
>
<ha-config-section
.narrow=${this.narrow}
.isWide=${this.isWide}
vertical
>
<div slot="header">
${this.hass.localize(
"ui.panel.config.zwave_js.node_installer.header"
)}
</div>
<div slot="introduction">
${device
? html`
<div class="device-info">
<h2>${computeDeviceName(device, this.hass)}</h2>
<p>${device.manufacturer} ${device.model}</p>
</div>
`
: ``}
${this.hass.localize(
"ui.panel.config.zwave_js.node_installer.introduction"
)}
</div>
${endpoints.length
? endpoints.map(
([endpoint, capabilities]) => html`
<h3>
${this.hass.localize(
"ui.panel.config.zwave_js.node_installer.endpoint"
)}:
${endpoint}
</h3>
<ha-card>
${capabilities.map(
(capability) => html`
${capability.id in CAPABILITY_CONTROLS
? html` <div class="capability">
<h4>
${this.hass.localize(
"ui.panel.config.zwave_js.node_installer.command_class"
)}:
${capability.name}
</h4>
${dynamicElement(
`zwave_js-capability-control-${CAPABILITY_CONTROLS[capability.id]}`,
{
hass: this.hass,
device: device,
endpoint: endpoint,
command_class: capability.id,
version: capability.version,
is_secure: capability.is_secure,
}
)}
</div>`
: nothing}
`
)}
</ha-card>
`
)
: html`<ha-card class="empty"
>${this.hass.localize(
"ui.panel.config.zwave_js.node_installer.no_settings"
)}</ha-card
>`}
</ha-config-section>
</hass-subpage>
`;
}
private async _fetchData() {
if (!this.configEntryId) {
return;
}
const device = this.hass.devices[this.deviceId];
if (!device) {
this._error = "device_not_found";
return;
}
[this._nodeMetadata, this._capabilities] = await Promise.all([
fetchZwaveNodeMetadata(this.hass, device.id),
fetchZwaveNodeCapabilities(this.hass, device.id),
]);
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
ha-card {
margin-bottom: 40px;
margin-top: 0;
}
.capability {
border-bottom: 1px solid var(--divider-color);
padding: 4px 16px;
}
.capability:last-child {
border-bottom: none;
}
.empty {
margin-top: 32px;
padding: 24px 16px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zwave_js-node-installer": ZWaveJSNodeInstaller;
}
}

View File

@@ -2,24 +2,22 @@ import { mdiClose } from "@mdi/js";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import "../../../components/ha-md-dialog"; import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-button"; import "../../../components/ha-button";
import "../../../components/ha-dialog-header"; import "../../../components/ha-dialog-header";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../components/ha-md-dialog"; import type { HaMdDialog } from "../../../components/ha-md-dialog";
import type { HomeAssistant } from "../../../types"; import "../../../components/ha-md-select";
import { haStyle, haStyleDialog } from "../../../resources/styles"; import "../../../components/ha-md-select-option";
import { fireEvent } from "../../../common/dom/fire_event";
import type { DownloadLogsDialogParams } from "./show-dialog-download-logs";
import "../../../components/ha-select";
import "../../../components/ha-list-item";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import {
getHassioLogDownloadLinesUrl,
getHassioLogBootDownloadLinesUrl,
} from "../../../data/hassio/supervisor";
import { getSignedPath } from "../../../data/auth"; import { getSignedPath } from "../../../data/auth";
import { getHassioLogDownloadLinesUrl } from "../../../data/hassio/supervisor";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { fileDownload } from "../../../util/file_download"; import { fileDownload } from "../../../util/file_download";
import type { DownloadLogsDialogParams } from "./show-dialog-download-logs";
const DEFAULT_LINE_COUNT = 500;
@customElement("dialog-download-logs") @customElement("dialog-download-logs")
class DownloadLogsDialog extends LitElement { class DownloadLogsDialog extends LitElement {
@@ -27,13 +25,13 @@ class DownloadLogsDialog extends LitElement {
@state() private _dialogParams?: DownloadLogsDialogParams; @state() private _dialogParams?: DownloadLogsDialogParams;
@state() private _lineCount = 100; @state() private _lineCount = DEFAULT_LINE_COUNT;
@query("ha-md-dialog") private _dialogElement!: HaMdDialog; @query("ha-md-dialog") private _dialogElement!: HaMdDialog;
public showDialog(dialogParams: DownloadLogsDialogParams) { public showDialog(dialogParams: DownloadLogsDialogParams) {
this._dialogParams = dialogParams; this._dialogParams = dialogParams;
this._lineCount = this._dialogParams?.defaultLineCount ?? 100; this._lineCount = this._dialogParams?.defaultLineCount || 500;
} }
public closeDialog() { public closeDialog() {
@@ -42,7 +40,7 @@ class DownloadLogsDialog extends LitElement {
private _dialogClosed() { private _dialogClosed() {
this._dialogParams = undefined; this._dialogParams = undefined;
this._lineCount = 100; this._lineCount = DEFAULT_LINE_COUNT;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
@@ -52,7 +50,7 @@ class DownloadLogsDialog extends LitElement {
} }
const numberOfLinesOptions = [100, 500, 1000, 5000, 10000]; const numberOfLinesOptions = [100, 500, 1000, 5000, 10000];
if (!numberOfLinesOptions.includes(this._lineCount)) { if (!numberOfLinesOptions.includes(this._lineCount) && this._lineCount) {
numberOfLinesOptions.push(this._lineCount); numberOfLinesOptions.push(this._lineCount);
numberOfLinesOptions.sort((a, b) => a - b); numberOfLinesOptions.sort((a, b) => a - b);
} }
@@ -67,7 +65,7 @@ class DownloadLogsDialog extends LitElement {
.path=${mdiClose} .path=${mdiClose}
></ha-icon-button> ></ha-icon-button>
<span slot="title" id="dialog-light-color-favorite-title"> <span slot="title" id="dialog-light-color-favorite-title">
${this.hass.localize("ui.panel.config.logs.download_full_log")} ${this.hass.localize("ui.panel.config.logs.download_logs")}
</span> </span>
<span slot="subtitle"> <span slot="subtitle">
${this._dialogParams.header}${this._dialogParams.boot === 0 ${this._dialogParams.header}${this._dialogParams.boot === 0
@@ -81,28 +79,25 @@ class DownloadLogsDialog extends LitElement {
"ui.panel.config.logs.select_number_of_lines" "ui.panel.config.logs.select_number_of_lines"
)}: )}:
</div> </div>
<ha-select <ha-md-select
.label=${this.hass.localize("ui.panel.config.logs.lines")} .label=${this.hass.localize("ui.panel.config.logs.lines")}
@selected=${this._setNumberOfLogs} @change=${this._setNumberOfLogs}
fixedMenuPosition
naturalMenuWidth
@closed=${stopPropagation}
.value=${String(this._lineCount)} .value=${String(this._lineCount)}
> >
${numberOfLinesOptions.map( ${numberOfLinesOptions.map(
(option) => html` (option) => html`
<ha-list-item .value=${String(option)}> <ha-md-select-option .value=${String(option)}>
${option} ${option}
</ha-list-item> </ha-md-select-option>
` `
)} )}
</ha-select> </ha-md-select>
</div> </div>
<div slot="actions"> <div slot="actions">
<ha-button @click=${this.closeDialog}> <ha-button @click=${this.closeDialog}>
${this.hass.localize("ui.common.cancel")} ${this.hass.localize("ui.common.cancel")}
</ha-button> </ha-button>
<ha-button @click=${this._dowloadLogs}> <ha-button @click=${this._downloadLogs}>
${this.hass.localize("ui.common.download")} ${this.hass.localize("ui.common.download")}
</ha-button> </ha-button>
</div> </div>
@@ -110,12 +105,12 @@ class DownloadLogsDialog extends LitElement {
`; `;
} }
private async _dowloadLogs() { private async _downloadLogs() {
const provider = this._dialogParams!.provider; const provider = this._dialogParams!.provider;
const boot = this._dialogParams!.boot; const boot = this._dialogParams!.boot;
const timeString = new Date().toISOString().replace(/:/g, "-"); const timeString = new Date().toISOString().replace(/:/g, "-");
const downloadUrl = this._getDownloadUrlFunction()( const downloadUrl = getHassioLogDownloadLinesUrl(
provider, provider,
this._lineCount, this._lineCount,
boot boot
@@ -129,13 +124,6 @@ class DownloadLogsDialog extends LitElement {
this.closeDialog(); this.closeDialog();
} }
private _getDownloadUrlFunction() {
if (this._dialogParams!.boot === 0) {
return getHassioLogDownloadLinesUrl;
}
return getHassioLogBootDownloadLinesUrl;
}
private _setNumberOfLogs(ev) { private _setNumberOfLogs(ev) {
this._lineCount = Number(ev.target.value); this._lineCount = Number(ev.target.value);
} }
@@ -147,6 +135,7 @@ class DownloadLogsDialog extends LitElement {
css` css`
:host { :host {
direction: var(--direction); direction: var(--direction);
--dialog-content-overflow: visible;
} }
.content { .content {
display: flex; display: flex;

View File

@@ -1,9 +1,16 @@
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import type { ActionDetail } from "@material/mwc-list";
import { import {
mdiArrowCollapseDown, mdiArrowCollapseDown,
mdiDotsVertical,
mdiCircle,
mdiDownload, mdiDownload,
mdiFormatListNumbered,
mdiMenuDown, mdiMenuDown,
mdiRefresh, mdiRefresh,
mdiWrap,
mdiWrapDisabled,
} from "@mdi/js"; } from "@mdi/js";
import { import {
css, css,
@@ -31,6 +38,8 @@ import "../../../components/chips/ha-assist-chip";
import "../../../components/ha-menu"; import "../../../components/ha-menu";
import "../../../components/ha-md-menu-item"; import "../../../components/ha-md-menu-item";
import "../../../components/ha-md-divider"; import "../../../components/ha-md-divider";
import "../../../components/ha-button-menu";
import "../../../components/ha-list-item";
import { getSignedPath } from "../../../data/auth"; import { getSignedPath } from "../../../data/auth";
@@ -39,12 +48,15 @@ import { extractApiErrorMessage } from "../../../data/hassio/common";
import { import {
fetchHassioBoots, fetchHassioBoots,
fetchHassioLogs, fetchHassioLogs,
fetchHassioLogsBootFollow,
fetchHassioLogsFollow, fetchHassioLogsFollow,
getHassioLogDownloadLinesUrl,
getHassioLogDownloadUrl, getHassioLogDownloadUrl,
} from "../../../data/hassio/supervisor"; } from "../../../data/hassio/supervisor";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { fileDownload } from "../../../util/file_download"; import {
downloadFileSupported,
fileDownload,
} from "../../../util/file_download";
import type { HASSDomEvent } from "../../../common/dom/fire_event"; import type { HASSDomEvent } from "../../../common/dom/fire_event";
import type { ConnectionStatus } from "../../../data/connection-status"; import type { ConnectionStatus } from "../../../data/connection-status";
import { atLeastVersion } from "../../../common/config/version"; import { atLeastVersion } from "../../../common/config/version";
@@ -52,6 +64,7 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { debounce } from "../../../common/util/debounce"; import { debounce } from "../../../common/util/debounce";
import { showDownloadLogsDialog } from "./show-dialog-download-logs"; import { showDownloadLogsDialog } from "./show-dialog-download-logs";
import type { HaMenu } from "../../../components/ha-menu"; import type { HaMenu } from "../../../components/ha-menu";
import type { LocalizeFunc } from "../../../common/translations/localize";
const NUMBER_OF_LINES = 100; const NUMBER_OF_LINES = 100;
@@ -59,6 +72,8 @@ const NUMBER_OF_LINES = 100;
class ErrorLogCard extends LitElement { class ErrorLogCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public localizeFunc?: LocalizeFunc<any>;
@property() public filter = ""; @property() public filter = "";
@property() public header?: string; @property() public header?: string;
@@ -110,30 +125,42 @@ class ErrorLogCard extends LitElement {
@state() private _boots?: number[]; @state() private _boots?: number[];
@state() private _showBootsSelect = false;
@state() private _wrapLines = true;
@state() private _downloadSupported;
@state() private _logsFileLink;
protected render(): TemplateResult { protected render(): TemplateResult {
const localize = this.localizeFunc || this.hass.localize;
return html` return html`
<div class="error-log-intro"> <div class="error-log-intro">
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""} : nothing}
<ha-card outlined class=${classMap({ hidden: this.show === false })}> <ha-card outlined class=${classMap({ hidden: this.show === false })}>
<div class="header"> <div class="header">
<h1 class="card-header"> <h1 class="card-header">
${this.header || ${this.header || localize("ui.panel.config.logs.show_full_logs")}
this.hass.localize("ui.panel.config.logs.show_full_logs")}
</h1> </h1>
<div class="action-buttons"> <div class="action-buttons">
${this._streamSupported && Array.isArray(this._boots) ${this._streamSupported &&
Array.isArray(this._boots) &&
this._showBootsSelect
? html` ? html`
<ha-assist-chip <ha-assist-chip
.label=${this._boot === 0 .title=${localize(
? this.hass.localize("ui.panel.config.logs.current") "ui.panel.config.logs.haos_boots_title"
: this._boot === -1
? this.hass.localize("ui.panel.config.logs.previous")
: this.hass.localize(
"ui.panel.config.logs.startups_ago",
{ boot: this._boot * -1 }
)} )}
.label=${this._boot === 0
? localize("ui.panel.config.logs.current")
: this._boot === -1
? localize("ui.panel.config.logs.previous")
: localize("ui.panel.config.logs.startups_ago", {
boot: this._boot * -1,
})}
id="boots-anchor" id="boots-anchor"
@click=${this._toggleBootsMenu} @click=${this._toggleBootsMenu}
> >
@@ -155,14 +182,10 @@ class ErrorLogCard extends LitElement {
.selected=${boot === this._boot} .selected=${boot === this._boot}
> >
${boot === 0 ${boot === 0
? this.hass.localize( ? localize("ui.panel.config.logs.current")
"ui.panel.config.logs.current"
)
: boot === -1 : boot === -1
? this.hass.localize( ? localize("ui.panel.config.logs.previous")
"ui.panel.config.logs.previous" : localize(
)
: this.hass.localize(
"ui.panel.config.logs.startups_ago", "ui.panel.config.logs.startups_ago",
{ boot: boot * -1 } { boot: boot * -1 }
)} )}
@@ -177,20 +200,61 @@ class ErrorLogCard extends LitElement {
</ha-menu> </ha-menu>
` `
: nothing} : nothing}
${this._downloadSupported
? html`
<ha-icon-button <ha-icon-button
.path=${mdiDownload} .path=${mdiDownload}
@click=${this._downloadFullLog} @click=${this._downloadLogs}
.label=${this.hass.localize( .label=${localize("ui.panel.config.logs.download_logs")}
"ui.panel.config.logs.download_full_log" ></ha-icon-button>
`
: this._logsFileLink
? html`
<a
href=${this._logsFileLink}
target="_blank"
class="download-link"
>
<ha-icon-button
.path=${mdiDownload}
.label=${localize(
"ui.panel.config.logs.download_logs"
)}
></ha-icon-button>
</a>
`
: nothing}
<ha-icon-button
.path=${this._wrapLines ? mdiWrapDisabled : mdiWrap}
@click=${this._toggleLineWrap}
.label=${localize(
`ui.panel.config.logs.${this._wrapLines ? "full_width" : "wrap_lines"}`
)} )}
></ha-icon-button> ></ha-icon-button>
${!this._streamSupported || this._error ${!this._streamSupported || this._error
? html`<ha-icon-button ? html`<ha-icon-button
.path=${mdiRefresh} .path=${mdiRefresh}
@click=${this._loadLogs} @click=${this._loadLogs}
.label=${this.hass.localize("ui.common.refresh")} .label=${localize("ui.common.refresh")}
></ha-icon-button>` ></ha-icon-button>`
: nothing} : nothing}
${this._streamSupported && Array.isArray(this._boots)
? html`
<ha-button-menu @action=${this._handleOverflowAction}>
<ha-icon-button slot="trigger" .path=${mdiDotsVertical}>
</ha-icon-button>
<ha-list-item graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${mdiFormatListNumbered}
></ha-svg-icon>
${localize(
`ui.panel.config.logs.${this._showBootsSelect ? "hide" : "show"}_haos_boots`
)}
</ha-list-item>
</ha-button-menu>
`
: nothing}
</div> </div>
</div> </div>
<div class="card-content error-log"> <div class="card-content error-log">
@@ -203,25 +267,22 @@ class ErrorLogCard extends LitElement {
</div>` </div>`
: nothing} : nothing}
${this._loadingState === "loading" ${this._loadingState === "loading"
? html`<div> ? html`<div>${localize("ui.panel.config.logs.loading_log")}</div>`
${this.hass.localize("ui.panel.config.logs.loading_log")}
</div>`
: this._loadingState === "empty" : this._loadingState === "empty"
? html`<div> ? html`<div>${localize("ui.panel.config.logs.no_errors")}</div>`
${this.hass.localize("ui.panel.config.logs.no_errors")}
</div>`
: nothing} : nothing}
${this._loadingState === "loaded" && ${this._loadingState === "loaded" &&
this.filter && this.filter &&
this._noSearchResults this._noSearchResults
? html`<div> ? html`<div>
${this.hass.localize( ${localize("ui.panel.config.logs.no_issues_search", {
"ui.panel.config.logs.no_issues_search", term: this.filter,
{ term: this.filter } })}
)}
</div>` </div>`
: nothing} : nothing}
<ha-ansi-to-html></ha-ansi-to-html> <ha-ansi-to-html
?wrap-disabled=${!this._wrapLines}
></ha-ansi-to-html>
<div id="scroll-bottom-marker"></div> <div id="scroll-bottom-marker"></div>
</div> </div>
<ha-button <ha-button
@@ -237,24 +298,36 @@ class ErrorLogCard extends LitElement {
.path=${mdiArrowCollapseDown} .path=${mdiArrowCollapseDown}
slot="icon" slot="icon"
></ha-svg-icon> ></ha-svg-icon>
${this.hass.localize("ui.panel.config.logs.scroll_down_button")} ${localize("ui.panel.config.logs.scroll_down_button")}
<ha-svg-icon <ha-svg-icon
.path=${mdiArrowCollapseDown} .path=${mdiArrowCollapseDown}
slot="trailingIcon" slot="trailingIcon"
></ha-svg-icon> ></ha-svg-icon>
</ha-button> </ha-button>
${this._streamSupported &&
this._loadingState !== "loading" &&
!this._error
? html`<div class="live-indicator">
<ha-svg-icon path=${mdiCircle}></ha-svg-icon>
Live
</div>`
: nothing}
</ha-card> </ha-card>
${this.show === false ${this.show === false
? html` ? html`
<ha-button outlined @click=${this._downloadFullLog}> ${this._downloadSupported
? html`
<ha-button outlined @click=${this._downloadLogs}>
<ha-svg-icon .path=${mdiDownload}></ha-svg-icon> <ha-svg-icon .path=${mdiDownload}></ha-svg-icon>
${this.hass.localize("ui.panel.config.logs.download_full_log")} ${localize("ui.panel.config.logs.download_logs")}
</ha-button> </ha-button>
`
: nothing}
<mwc-button raised @click=${this._showLogs}> <mwc-button raised @click=${this._showLogs}>
${this.hass.localize("ui.panel.config.logs.load_logs")} ${localize("ui.panel.config.logs.load_logs")}
</mwc-button> </mwc-button>
` `
: ""} : nothing}
</div> </div>
`; `;
} }
@@ -269,6 +342,9 @@ class ErrorLogCard extends LitElement {
11 11
); );
} }
if (this._downloadSupported === undefined && this.hass) {
this._downloadSupported = downloadFileSupported(this.hass);
}
} }
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
@@ -332,7 +408,7 @@ class ErrorLogCard extends LitElement {
); );
} }
private async _downloadFullLog(): Promise<void> { private async _downloadLogs(): Promise<void> {
if (this._streamSupported) { if (this._streamSupported) {
showDownloadLogsDialog(this, { showDownloadLogsDialog(this, {
header: this.header, header: this.header,
@@ -379,7 +455,19 @@ class ErrorLogCard extends LitElement {
isComponentLoaded(this.hass, "hassio") && isComponentLoaded(this.hass, "hassio") &&
this.provider this.provider
) { ) {
const response = await this._fetchLogsFunction()( // check if there are any logs at all
const testResponse = await fetchHassioLogs(
this.hass,
this.provider,
`entries=:-1:`,
this._boot
);
const testLogs = await testResponse.text();
if (!testLogs.trim()) {
this._loadingState = "empty";
}
const response = await fetchHassioLogsFollow(
this.hass, this.hass,
this.provider, this.provider,
this._logStreamAborter.signal, this._logStreamAborter.signal,
@@ -439,6 +527,17 @@ class ErrorLogCard extends LitElement {
} else { } else {
this._newLogsIndicator = true; this._newLogsIndicator = true;
} }
if (!this._downloadSupported) {
const downloadUrl = getHassioLogDownloadLinesUrl(
this.provider,
this._numberOfLines,
this._boot
);
getSignedPath(this.hass, downloadUrl).then((signedUrl) => {
this._logsFileLink = signedUrl.path;
});
}
} }
} }
} else { } else {
@@ -462,20 +561,16 @@ class ErrorLogCard extends LitElement {
if (err.name === "AbortError") { if (err.name === "AbortError") {
return; return;
} }
this._error = this.hass.localize("ui.panel.config.logs.failed_get_logs", { this._error = (this.localizeFunc || this.hass.localize)(
"ui.panel.config.logs.failed_get_logs",
{
provider: this.provider, provider: this.provider,
error: extractApiErrorMessage(err), error: extractApiErrorMessage(err),
}); }
);
} }
} }
private _fetchLogsFunction = () => {
if (this._boot === 0) {
return fetchHassioLogsFollow;
}
return fetchHassioLogsBootFollow;
};
private _debounceSearch = debounce(() => { private _debounceSearch = debounce(() => {
this._noSearchResults = !this._ansiToHtmlElement?.filterLines(this.filter); this._noSearchResults = !this._ansiToHtmlElement?.filterLines(this.filter);
@@ -593,6 +688,18 @@ class ErrorLogCard extends LitElement {
} }
} }
private _toggleLineWrap() {
this._wrapLines = !this._wrapLines;
}
private _handleOverflowAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._showBootsSelect = !this._showBootsSelect;
break;
}
}
private _toggleBootsMenu() { private _toggleBootsMenu() {
if (this._bootsMenu) { if (this._bootsMenu) {
this._bootsMenu.open = !this._bootsMenu.open; this._bootsMenu.open = !this._bootsMenu.open;
@@ -605,6 +712,9 @@ class ErrorLogCard extends LitElement {
} }
static styles: CSSResultGroup = css` static styles: CSSResultGroup = css`
:host {
direction: var(--direction);
}
.error-log-intro { .error-log-intro {
text-align: center; text-align: center;
margin: 16px; margin: 16px;
@@ -654,7 +764,7 @@ class ErrorLogCard extends LitElement {
position: relative; position: relative;
font-family: var(--code-font-family, monospace); font-family: var(--code-font-family, monospace);
clear: both; clear: both;
text-align: left; text-align: start;
padding-top: 12px; padding-top: 12px;
padding-bottom: 12px; padding-bottom: 12px;
overflow-y: scroll; overflow-y: scroll;
@@ -721,6 +831,36 @@ class ErrorLogCard extends LitElement {
--ha-assist-chip-container-shape: 10px; --ha-assist-chip-container-shape: 10px;
--md-assist-chip-trailing-space: 8px; --md-assist-chip-trailing-space: 8px;
} }
@keyframes breathe {
from {
opacity: 0.8;
}
to {
opacity: 0;
}
}
.live-indicator {
position: absolute;
bottom: 0;
inset-inline-end: 16px;
border-top-right-radius: 8px;
border-top-left-radius: 8px;
background-color: var(--primary-color);
color: var(--text-primary-color);
padding: 4px 8px;
opacity: 0.8;
}
.live-indicator ha-svg-icon {
animation: breathe 1s cubic-bezier(0.5, 0, 1, 1) infinite alternate;
height: 14px;
width: 14px;
}
.download-link {
color: var(--text-color);
}
`; `;
} }

View File

@@ -681,8 +681,12 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
private async _duplicate() { private async _duplicate() {
const result = this._readOnly const result = this._readOnly
? await showConfirmationDialog(this, { ? await showConfirmationDialog(this, {
title: "Migrate script?", title: this.hass.localize(
text: "You can migrate this script, so it can be edited from the UI. After it is migrated and you have saved it, you will have to manually delete your old script from your configuration. Do you want to migrate this script?", "ui.panel.config.script.picker.migrate_script"
),
text: this.hass.localize(
"ui.panel.config.script.picker.migrate_script_description"
),
}) })
: await this.confirmUnsavedChanged(); : await this.confirmUnsavedChanged();
if (result) { if (result) {

View File

@@ -1,11 +1,11 @@
import type { CSSResultGroup, PropertyValues } from "lit"; import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "../../../../components/ha-card";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { hasConfigChanged } from "../../common/has-changed"; import { hasConfigChanged } from "../../common/has-changed";
import "../../components/hui-energy-period-selector"; import "../../components/hui-energy-period-selector";
import "../../../../components/ha-card"; import type { LovelaceCard, LovelaceGridOptions } from "../../types";
import type { LovelaceCard, LovelaceLayoutOptions } from "../../types";
import type { EnergyCardBaseConfig } from "../types"; import type { EnergyCardBaseConfig } from "../types";
@customElement("hui-energy-date-selection-card") @customElement("hui-energy-date-selection-card")
@@ -21,10 +21,10 @@ export class HuiEnergyDateSelectionCard
return 1; return 1;
} }
public getLayoutOptions(): LovelaceLayoutOptions { public getGridOptions(): LovelaceGridOptions {
return { return {
grid_rows: 1, rows: 1,
grid_columns: 4, columns: 12,
}; };
} }
@@ -59,18 +59,12 @@ export class HuiEnergyDateSelectionCard
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host {
ha-card { ha-card {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
} }
.padded {
padding-left: 16px !important;
padding-inline-start: 16px !important;
padding-inline-end: initial !important;
}
`; `;
} }
} }

View File

@@ -45,7 +45,7 @@ import "../components/hui-warning";
import type { import type {
LovelaceCard, LovelaceCard,
LovelaceCardEditor, LovelaceCardEditor,
LovelaceLayoutOptions, LovelaceGridOptions,
} from "../types"; } from "../types";
import type { AreaCardConfig } from "./types"; import type { AreaCardConfig } from "./types";
@@ -534,10 +534,11 @@ export class HuiAreaCard
forwardHaptic("light"); forwardHaptic("light");
} }
getLayoutOptions(): LovelaceLayoutOptions { getGridOptions(): LovelaceGridOptions {
return { return {
grid_columns: 4, columns: 12,
grid_rows: 3, rows: 3,
min_columns: 3,
}; };
} }

View File

@@ -46,7 +46,7 @@ import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { import type {
LovelaceCard, LovelaceCard,
LovelaceCardEditor, LovelaceCardEditor,
LovelaceLayoutOptions, LovelaceGridOptions,
} from "../types"; } from "../types";
import type { ButtonCardConfig } from "./types"; import type { ButtonCardConfig } from "./types";
@@ -134,20 +134,23 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
); );
} }
public getLayoutOptions(): LovelaceLayoutOptions { public getGridOptions(): LovelaceGridOptions {
if ( if (
this._config?.show_icon && this._config?.show_icon &&
(this._config?.show_name || this._config?.show_state) (this._config?.show_name || this._config?.show_state)
) { ) {
return { return {
grid_rows: 2, rows: 2,
grid_columns: 2, columns: 6,
grid_min_rows: 2, min_columns: 2,
min_rows: 2,
}; };
} }
return { return {
grid_rows: 1, rows: 1,
grid_columns: 1, columns: 3,
min_columns: 2,
min_rows: 1,
}; };
} }

View File

@@ -33,8 +33,8 @@ import { createEntityNotFoundWarning } from "../components/hui-warning";
import { createHeaderFooterElement } from "../create-element/create-header-footer-element"; import { createHeaderFooterElement } from "../create-element/create-header-footer-element";
import type { import type {
LovelaceCard, LovelaceCard,
LovelaceGridOptions,
LovelaceHeaderFooter, LovelaceHeaderFooter,
LovelaceLayoutOptions,
} from "../types"; } from "../types";
import type { HuiErrorCard } from "./hui-error-card"; import type { HuiErrorCard } from "./hui-error-card";
import type { EntityCardConfig } from "./types"; import type { EntityCardConfig } from "./types";
@@ -249,12 +249,12 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
fireEvent(this, "hass-more-info", { entityId: this._config!.entity }); fireEvent(this, "hass-more-info", { entityId: this._config!.entity });
} }
public getLayoutOptions(): LovelaceLayoutOptions { public getGridOptions(): LovelaceGridOptions {
return { return {
grid_columns: 2, columns: 6,
grid_rows: 2, rows: 2,
grid_min_columns: 2, min_columns: 6,
grid_min_rows: 2, min_rows: 2,
}; };
} }

View File

@@ -16,7 +16,7 @@ import "../heading-badges/hui-heading-badge";
import type { import type {
LovelaceCard, LovelaceCard,
LovelaceCardEditor, LovelaceCardEditor,
LovelaceLayoutOptions, LovelaceGridOptions,
} from "../types"; } from "../types";
import type { HeadingCardConfig } from "./types"; import type { HeadingCardConfig } from "./types";
@@ -65,10 +65,11 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
return 1; return 1;
} }
public getLayoutOptions(): LovelaceLayoutOptions { public getGridOptions(): LovelaceGridOptions {
return { return {
grid_columns: "full", columns: "full",
grid_rows: this._config?.heading_style === "subtitle" ? "auto" : 1, rows: this._config?.heading_style === "subtitle" ? "auto" : 1,
min_columns: 3,
}; };
} }

View File

@@ -28,6 +28,7 @@ export class HuiHorizontalStackCard extends HuiStackCard {
css` css`
#root { #root {
display: flex; display: flex;
height: 100%;
gap: var(--horizontal-stack-card-gap, var(--stack-card-gap, 8px)); gap: var(--horizontal-stack-card-gap, var(--stack-card-gap, 8px));
} }
#root > hui-card { #root > hui-card {

View File

@@ -19,7 +19,7 @@ import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { import type {
LovelaceCard, LovelaceCard,
LovelaceCardEditor, LovelaceCardEditor,
LovelaceLayoutOptions, LovelaceGridOptions,
} from "../types"; } from "../types";
import type { HumidifierCardConfig } from "./types"; import type { HumidifierCardConfig } from "./types";
@@ -171,21 +171,21 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
`; `;
} }
public getLayoutOptions(): LovelaceLayoutOptions { public getGridOptions(): LovelaceGridOptions {
const grid_columns = 4; const columns = 12;
let grid_rows = 5; let rows = 5;
let grid_min_rows = 2; let min_rows = 2;
const grid_min_columns = 2; const min_columns = 6;
if (this._config?.features?.length) { if (this._config?.features?.length) {
const featureHeight = Math.ceil((this._config.features.length * 2) / 3); const featureHeight = Math.ceil((this._config.features.length * 2) / 3);
grid_rows += featureHeight; rows += featureHeight;
grid_min_rows += featureHeight; min_rows += featureHeight;
} }
return { return {
grid_columns, columns,
grid_rows, rows,
grid_min_rows, min_columns,
grid_min_columns, min_rows,
}; };
} }

View File

@@ -11,7 +11,7 @@ import { IFRAME_SANDBOX } from "../../../util/iframe";
import type { import type {
LovelaceCard, LovelaceCard,
LovelaceCardEditor, LovelaceCardEditor,
LovelaceLayoutOptions, LovelaceGridOptions,
} from "../types"; } from "../types";
import type { IframeCardConfig } from "./types"; import type { IframeCardConfig } from "./types";
@@ -113,11 +113,12 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
`; `;
} }
public getLayoutOptions(): LovelaceLayoutOptions { public getGridOptions(): LovelaceGridOptions {
return { return {
grid_columns: "full", columns: "full",
grid_rows: 4, rows: 4,
grid_min_rows: 2, min_columns: 3,
min_rows: 2,
}; };
} }

View File

@@ -11,8 +11,8 @@ import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import { deepEqual } from "../../../common/util/deep-equal"; import { deepEqual } from "../../../common/util/deep-equal";
import parseAspectRatio from "../../../common/util/parse-aspect-ratio"; import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
import "../../../components/ha-card";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-card";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/map/ha-map"; import "../../../components/map/ha-map";
import type { import type {
@@ -23,15 +23,15 @@ import type {
} from "../../../components/map/ha-map"; } from "../../../components/map/ha-map";
import type { HistoryStates } from "../../../data/history"; import type { HistoryStates } from "../../../data/history";
import { subscribeHistoryStatesTimeWindow } from "../../../data/history"; import { subscribeHistoryStatesTimeWindow } from "../../../data/history";
import type { HomeAssistant } from "../../../types";
import { findEntities } from "../common/find-entities";
import { import {
hasConfigChanged, hasConfigChanged,
hasConfigOrEntitiesChanged, hasConfigOrEntitiesChanged,
} from "../common/has-changed"; } from "../common/has-changed";
import type { HomeAssistant } from "../../../types";
import { findEntities } from "../common/find-entities";
import { processConfigEntities } from "../common/process-config-entities"; import { processConfigEntities } from "../common/process-config-entities";
import type { EntityConfig } from "../entity-rows/types"; import type { EntityConfig } from "../entity-rows/types";
import type { LovelaceCard, LovelaceLayoutOptions } from "../types"; import type { LovelaceCard, LovelaceGridOptions } from "../types";
import type { MapCardConfig } from "./types"; import type { MapCardConfig } from "./types";
export const DEFAULT_HOURS_TO_SHOW = 0; export const DEFAULT_HOURS_TO_SHOW = 0;
@@ -431,12 +431,12 @@ class HuiMapCard extends LitElement implements LovelaceCard {
} }
); );
public getLayoutOptions(): LovelaceLayoutOptions { public getGridOptions(): LovelaceGridOptions {
return { return {
grid_columns: "full", columns: "full",
grid_rows: 4, rows: 4,
grid_min_columns: 2, min_columns: 6,
grid_min_rows: 2, min_rows: 2,
}; };
} }

View File

@@ -6,7 +6,7 @@ import { computeDomain } from "../../../common/entity/compute_domain";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { findEntities } from "../common/find-entities"; import { findEntities } from "../common/find-entities";
import type { GraphHeaderFooterConfig } from "../header-footer/types"; import type { GraphHeaderFooterConfig } from "../header-footer/types";
import type { LovelaceCardEditor, LovelaceLayoutOptions } from "../types"; import type { LovelaceCardEditor, LovelaceGridOptions } from "../types";
import { HuiEntityCard } from "./hui-entity-card"; import { HuiEntityCard } from "./hui-entity-card";
import type { EntityCardConfig, SensorCardConfig } from "./types"; import type { EntityCardConfig, SensorCardConfig } from "./types";
@@ -73,12 +73,12 @@ class HuiSensorCard extends HuiEntityCard {
super.setConfig(entityCardConfig); super.setConfig(entityCardConfig);
} }
public getLayoutOptions(): LovelaceLayoutOptions { public getGridOptions(): LovelaceGridOptions {
return { return {
grid_columns: 2, columns: 6,
grid_rows: 2, rows: 2,
grid_min_columns: 2, min_columns: 6,
grid_min_rows: 2, min_rows: 2,
}; };
} }

View File

@@ -26,7 +26,7 @@ import type {
LovelaceCard, LovelaceCard,
LovelaceCardEditor, LovelaceCardEditor,
LovelaceHeaderFooter, LovelaceHeaderFooter,
LovelaceLayoutOptions, LovelaceGridOptions,
} from "../types"; } from "../types";
import type { HuiErrorCard } from "./hui-error-card"; import type { HuiErrorCard } from "./hui-error-card";
import type { EntityCardConfig, StatisticCardConfig } from "./types"; import type { EntityCardConfig, StatisticCardConfig } from "./types";
@@ -249,12 +249,12 @@ export class HuiStatisticCard extends LitElement implements LovelaceCard {
fireEvent(this, "hass-more-info", { entityId: this._config!.entity }); fireEvent(this, "hass-more-info", { entityId: this._config!.entity });
} }
public getLayoutOptions(): LovelaceLayoutOptions { public getGridOptions(): LovelaceGridOptions {
return { return {
grid_columns: 2, columns: 6,
grid_rows: 2, rows: 2,
grid_min_columns: 2, min_columns: 6,
grid_min_rows: 2, min_rows: 2,
}; };
} }

View File

@@ -19,7 +19,7 @@ import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { import type {
LovelaceCard, LovelaceCard,
LovelaceCardEditor, LovelaceCardEditor,
LovelaceLayoutOptions, LovelaceGridOptions,
} from "../types"; } from "../types";
import type { ThermostatCardConfig } from "./types"; import type { ThermostatCardConfig } from "./types";
@@ -163,21 +163,21 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
`; `;
} }
public getLayoutOptions(): LovelaceLayoutOptions { public getGridOptions(): LovelaceGridOptions {
const grid_columns = 4; const columns = 12;
let grid_rows = 5; let rows = 5;
let grid_min_rows = 2; let min_rows = 2;
const grid_min_columns = 2; const min_columns = 6;
if (this._config?.features?.length) { if (this._config?.features?.length) {
const featureHeight = Math.ceil((this._config.features.length * 2) / 3); const featureHeight = Math.ceil((this._config.features.length * 2) / 3);
grid_rows += featureHeight; rows += featureHeight;
grid_min_rows += featureHeight; min_rows += featureHeight;
} }
return { return {
grid_columns, columns,
grid_rows, rows,
grid_min_rows, min_columns,
grid_min_columns, min_rows,
}; };
} }

View File

@@ -34,7 +34,7 @@ import { hasAction } from "../common/has-action";
import type { import type {
LovelaceCard, LovelaceCard,
LovelaceCardEditor, LovelaceCardEditor,
LovelaceLayoutOptions, LovelaceGridOptions,
} from "../types"; } from "../types";
import { renderTileBadge } from "./tile/badges/tile-badge"; import { renderTileBadge } from "./tile/badges/tile-badge";
import type { ThermostatCardConfig, TileCardConfig } from "./types"; import type { ThermostatCardConfig, TileCardConfig } from "./types";
@@ -109,22 +109,22 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
); );
} }
public getLayoutOptions(): LovelaceLayoutOptions { public getGridOptions(): LovelaceGridOptions {
const grid_columns = 2; const columns = 6;
let grid_min_columns = 2; let min_columns = 6;
let grid_rows = 1; let rows = 1;
if (this._config?.features?.length) { if (this._config?.features?.length) {
grid_rows += this._config.features.length; rows += this._config.features.length;
} }
if (this._config?.vertical) { if (this._config?.vertical) {
grid_rows++; rows++;
grid_min_columns = 1; min_columns = 3;
} }
return { return {
grid_columns, columns,
grid_rows, rows,
grid_min_rows: grid_rows, min_columns,
grid_min_columns, min_rows: rows,
}; };
} }

View File

@@ -34,7 +34,7 @@ import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { import type {
LovelaceCard, LovelaceCard,
LovelaceCardEditor, LovelaceCardEditor,
LovelaceLayoutOptions, LovelaceGridOptions,
} from "../types"; } from "../types";
import type { WeatherForecastCardConfig } from "./types"; import type { WeatherForecastCardConfig } from "./types";
@@ -418,31 +418,31 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
return typeof item !== "undefined" && item !== null; return typeof item !== "undefined" && item !== null;
} }
public getLayoutOptions(): LovelaceLayoutOptions { public getGridOptions(): LovelaceGridOptions {
if ( if (
this._config?.show_current !== false && this._config?.show_current !== false &&
this._config?.show_forecast !== false this._config?.show_forecast !== false
) { ) {
return { return {
grid_columns: 4, columns: 12,
grid_min_columns: 2, rows: 4,
grid_rows: 4, min_columns: 6,
grid_min_rows: 4, min_rows: 4,
}; };
} }
if (this._config?.show_forecast !== false) { if (this._config?.show_forecast !== false) {
return { return {
grid_columns: 4, columns: 12,
grid_min_columns: 2, rows: 3,
grid_rows: 3, min_columns: 6,
grid_min_rows: 3, min_rows: 3,
}; };
} }
return { return {
grid_columns: 4, columns: 12,
grid_min_columns: 2, rows: 2,
grid_rows: 2, min_columns: 6,
grid_min_rows: 2, min_rows: 2,
}; };
} }

View File

@@ -233,7 +233,7 @@ export class HuiCardEditMode extends LitElement {
} }
private _handleAction(ev) { private _handleAction(ev) {
switch (ev.target.action) { switch (ev.currentTarget.action) {
case "edit": case "edit":
this._editCard(); this._editCard();
break; break;

View File

@@ -64,6 +64,7 @@ const cardConfigStruct = assign(
hours_to_show: optional(number()), hours_to_show: optional(number()),
geo_location_sources: optional(array(geoSourcesConfigStruct)), geo_location_sources: optional(array(geoSourcesConfigStruct)),
auto_fit: optional(boolean()), auto_fit: optional(boolean()),
fit_zones: optional(boolean()),
theme_mode: optional(string()), theme_mode: optional(string()),
}) })
); );

View File

@@ -242,8 +242,9 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
min-height: var(--row-height); min-height: var(--row-height);
} }
.container.edit-mode:not(.import-only) { .container.import-only {
border-start-end-radius: 0px; border: none;
padding: 0 !important;
} }
.card { .card {

View File

@@ -1,5 +1,11 @@
import { ResizeController } from "@lit-labs/observers/resize-controller"; import { ResizeController } from "@lit-labs/observers/resize-controller";
import { mdiDelete, mdiDrag, mdiPencil, mdiViewGridPlus } from "@mdi/js"; import {
mdiDelete,
mdiDrag,
mdiEyeOff,
mdiPencil,
mdiViewGridPlus,
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit"; import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@@ -245,6 +251,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
<div class="section imported-cards"> <div class="section imported-cards">
<div class="imported-card-header"> <div class="imported-card-header">
<p class="title"> <p class="title">
<ha-svg-icon .path=${mdiEyeOff}></ha-svg-icon>
${this.hass.localize( ${this.hass.localize(
"ui.panel.lovelace.editor.section.imported_cards_title" "ui.panel.lovelace.editor.section.imported_cards_title"
)} )}
@@ -480,9 +487,9 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
} }
.imported-card-header { .imported-card-header {
margin-top: 24px; margin-top: 36px;
padding: 16px 8px; padding: 32px 0 16px 0;
border-top: 2px dashed var(--divider-color); border-top: 4px dotted var(--divider-color);
} }
.imported-card-header .title { .imported-card-header .title {
@@ -491,6 +498,11 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;
line-height: 24px; line-height: 24px;
--mdc-icon-size: 18px;
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
} }
.imported-card-header .subtitle { .imported-card-header .subtitle {
margin: 0; margin: 0;

View File

@@ -1745,8 +1745,8 @@
"answer_generic": "Other controllers" "answer_generic": "Other controllers"
}, },
"google_home": { "google_home": {
"header": "Link Matter app", "header": "Share from Google Home",
"step_1": "Find your device in Google Home. Tap the gear icon to open the device settings.", "step_1": "Find your device in the Google Home app. Tap the gear icon to open the device settings.",
"step_2": "Tap {linked_matter_apps_services}.", "step_2": "Tap {linked_matter_apps_services}.",
"step_3": "Tap {link_apps_services} and choose {home_assistant} from the list.", "step_3": "Tap {link_apps_services} and choose {home_assistant} from the list.",
"linked_matter_apps_services": "Linked Matter apps and services", "linked_matter_apps_services": "Linked Matter apps and services",
@@ -1776,8 +1776,8 @@
"code_instructions": "Paste the code you just received from the other controller." "code_instructions": "Paste the code you just received from the other controller."
}, },
"generic": { "generic": {
"header": "Copy setup code", "header": "Enter setup code",
"code_instructions": "Search for the sharing mode in the app of your controller, and activate it. You will get a sharing code, enter that below.", "code_instructions": "Search for the sharing mode in the app of your controller, and activate it. You will get a setup code, enter that below.",
"setup_code": "Setup code" "setup_code": "Setup code"
} }
} }
@@ -2492,10 +2492,15 @@
"show_full_logs": "Show full logs", "show_full_logs": "Show full logs",
"select_number_of_lines": "Select number of lines to download", "select_number_of_lines": "Select number of lines to download",
"lines": "Lines", "lines": "Lines",
"download_full_log": "Download full log", "download_logs": "Download logs",
"scroll_down_button": "New logs - Click to scroll", "scroll_down_button": "New logs - Click to scroll",
"provider_not_found": "Log provider not found", "provider_not_found": "Log provider not found",
"provider_not_available": "Logs for ''{provider}'' are not available on your system.", "provider_not_available": "Logs for ''{provider}'' are not available on your system.",
"haos_boots_title": "Logs of HAOS startup",
"show_haos_boots": "Show HAOS startups",
"hide_haos_boots": "Hide HAOS startups",
"full_width": "Full width",
"wrap_lines": "Wrap lines",
"current": "Current", "current": "Current",
"previous": "Previous", "previous": "Previous",
"startups_ago": "{boot} startups ago", "startups_ago": "{boot} startups ago",
@@ -2792,7 +2797,9 @@
}, },
"empty_header": "Start automating", "empty_header": "Start automating",
"empty_text_1": "Automations make Home Assistant automatically respond to things happening in and around your home.", "empty_text_1": "Automations make Home Assistant automatically respond to things happening in and around your home.",
"empty_text_2": "Automations connect triggers to actions in a ''when trigger then action'' fashion with optional conditions. For example: ''When the sun sets and if {user} is home, then turn on the lights''." "empty_text_2": "Automations connect triggers to actions in a ''when trigger then action'' fashion with optional conditions. For example: ''When the sun sets and if {user} is home, then turn on the lights''.",
"migrate_automation": "Migrate automation?",
"migrate_automation_description": "You can migrate this automation, so it can be edited from the UI. After it is migrated and you have saved it, you will have to manually delete your old automation from your configuration. Do you want to migrate this automation?"
}, },
"dialog_new": { "dialog_new": {
"header": "Create automation", "header": "Create automation",
@@ -3215,7 +3222,7 @@
"description": { "description": {
"picker": "If an entity (or attribute) is in a specific state.", "picker": "If an entity (or attribute) is in a specific state.",
"no_entity": "Confirm state", "no_entity": "Confirm state",
"full": "If{hasAttribute, select, \n true { {attribute} of}\n other {}\n} {numberOfEntities, plural,\n zero {an entity is}\n one {{entities} is}\n other {{entities} are}\n} {numberOfStates, plural,\n zero {a state}\n other {{states}}\n}{hasDuration, select, \n true { for {duration}} \n other {}\n }" "full": "If{hasAttribute, select, \n true { {attribute} of}\n other {}\n} {numberOfEntities, plural,\n =0 {an entity is}\n one {{entities} is}\n other {{entities} are}\n} {numberOfStates, plural,\n =0 {a state}\n other {{states}}\n}{hasDuration, select, \n true { for {duration}} \n other {}\n }"
} }
}, },
"sun": { "sun": {
@@ -3579,7 +3586,9 @@
"stopped_unknown_reason": "Stopped because of unknown reason {reason} at {time} (runtime: {executiontime} seconds)", "stopped_unknown_reason": "Stopped because of unknown reason {reason} at {time} (runtime: {executiontime} seconds)",
"disabled": "(disabled)", "disabled": "(disabled)",
"triggered_by": "{triggeredBy, select, \n alias {{alias} triggered}\n other {Triggered} \n} {triggeredPath, select, \n trigger {by the {trigger}}\n other {manually} \n} at {time}", "triggered_by": "{triggeredBy, select, \n alias {{alias} triggered}\n other {Triggered} \n} {triggeredPath, select, \n trigger {by the {trigger}}\n other {manually} \n} at {time}",
"path_error": "Unable to extract path {path}. Download trace and report as bug." "path_error": "Unable to extract path {path}. Download trace and report as bug.",
"not_all_entries_are_related_automation_note": "Not all shown logbook entries might be related to this automation.",
"not_all_entries_are_related_script_note": "Not all shown logbook entries might be related to this script."
} }
} }
}, },
@@ -3676,7 +3685,9 @@
"duplicate": "[%key:ui::common::duplicate%]", "duplicate": "[%key:ui::common::duplicate%]",
"empty_header": "Create your first script", "empty_header": "Create your first script",
"empty_text": "A script is a sequence of actions that can be run from a dashboard, an automation, or be triggered by voice. For example, a ''Wake-up routine''' script that gradually turns on the light in the bedroom and opens the blinds after a delay.", "empty_text": "A script is a sequence of actions that can be run from a dashboard, an automation, or be triggered by voice. For example, a ''Wake-up routine''' script that gradually turns on the light in the bedroom and opens the blinds after a delay.",
"search": "Search {number} scripts" "search": "Search {number} scripts",
"migrate_script": "Migrate script?",
"migrate_script_description": "You can migrate this script, so it can be edited from the UI. After it is migrated and you have saved it, you will have to manually delete your old script from your configuration. Do you want to migrate this script?"
}, },
"dialog_new": { "dialog_new": {
"header": "Create script", "header": "Create script",
@@ -4115,7 +4126,7 @@
"hidden": "Hidden" "hidden": "Hidden"
}, },
"confirm_rename_entity_ids": "Do you also want to rename the entity IDs of your entities?", "confirm_rename_entity_ids": "Do you also want to rename the entity IDs of your entities?",
"confirm_rename_entity_ids_warning": "This will not change any configuration (like automations, scripts, scenes, dashboards) that is currently using these entities! You will have to update them yourself to use the new entity IDs!", "confirm_rename_entity_ids_warning": "This will not change any configuration (like automations, scripts, scenes, dashboards) that is currently using these entities! You will have to manually edit them yourself to use the new entity IDs!",
"confirm_rename_entity_will_rename": "{count} {count, plural,\n one {entity ID}\n other {entity IDs}\n} will be renamed", "confirm_rename_entity_will_rename": "{count} {count, plural,\n one {entity ID}\n other {entity IDs}\n} will be renamed",
"confirm_rename_new": "New", "confirm_rename_new": "New",
"confirm_rename_old": "Old", "confirm_rename_old": "Old",
@@ -4830,6 +4841,7 @@
"node_id": "ID", "node_id": "ID",
"node_ready": "Ready", "node_ready": "Ready",
"device_config": "Configure", "device_config": "Configure",
"installer_settings": "Installer settings",
"reinterview_device": "Re-interview", "reinterview_device": "Re-interview",
"rebuild_routes": "Rebuild routes", "rebuild_routes": "Rebuild routes",
"remove_failed": "Remove failed", "remove_failed": "Remove failed",
@@ -5121,6 +5133,45 @@
"subscribed_to_logs": "Subscribed to Z-Wave JS log messages…", "subscribed_to_logs": "Subscribed to Z-Wave JS log messages…",
"log_level_changed": "Log Level changed to: {level}", "log_level_changed": "Log Level changed to: {level}",
"download_logs": "Download logs" "download_logs": "Download logs"
},
"node_installer": {
"header": "Installer Settings",
"introduction": "Configure your device installer settings.",
"endpoint": "Endpoint",
"no_settings": "This device does not have any installer settings.",
"command_class": "Command Class",
"capability_controls": {
"thermostat_setback": {
"title": "Thermostat Setback",
"setback_state_label": "Setback in 1/10 degrees (Kelvin)",
"setback_state_helper": "Min: -12.8, max: 12.0",
"setback_special_state": {
"label": "Setback special state",
"frost_protection": "Frost protection",
"energy_saving": "Energy saving",
"unused": "Unused"
},
"setback_type": {
"label": "Setback Type",
"none": "None",
"temporary": "Temporary",
"permanent": "Permanent"
},
"get_setback_failed": "Failed to get setback state. {error}",
"save_setback_failed": "Failed to save setback state. {error}"
},
"multilevel_switch": {
"title": "Transition",
"direction": "Direction",
"up": "Up",
"down": "Down",
"ignore_start_level": "Ignore start level",
"start_level": "Start level",
"start_transition": "Start transition",
"stop_transition": "Stop transition",
"control_failed": "Failed to control transition. {error}"
}
}
} }
}, },
"matter": { "matter": {
@@ -5306,7 +5357,7 @@
"share": "Share" "share": "Share"
}, },
"mount_type": { "mount_type": {
"nfs": "Network file share (NFS)", "nfs": "Network File System (NFS)",
"cifs": "Samba/Windows (CIFS)" "cifs": "Samba/Windows (CIFS)"
}, },
"cifs_versions": { "cifs_versions": {
@@ -5884,7 +5935,7 @@
}, },
"entities": { "entities": {
"name": "Entities", "name": "Entities",
"show_header_toggle": "Show header toggle?", "show_header_toggle": "Show header toggle",
"toggle": "Toggle entities.", "toggle": "Toggle entities.",
"description": "The Entities card is the most common type of card. It groups items together into lists.", "description": "The Entities card is the most common type of card. It groups items together into lists.",
"special_row": "special row", "special_row": "special row",
@@ -5931,9 +5982,9 @@
}, },
"gauge": { "gauge": {
"name": "Gauge", "name": "Gauge",
"needle_gauge": "Display as needle gauge?", "needle_gauge": "Display as needle gauge",
"severity": { "severity": {
"define": "Define severity?", "define": "Define severity",
"green": "Green", "green": "Green",
"red": "Red", "red": "Red",
"yellow": "Yellow" "yellow": "Yellow"
@@ -6067,7 +6118,7 @@
"state": "State", "state": "State",
"secondary_info_attribute": "Secondary info attribute", "secondary_info_attribute": "Secondary info attribute",
"search": "Search", "search": "Search",
"state_color": "Color icons based on state?", "state_color": "Show state color",
"suggested_cards": "Suggested cards", "suggested_cards": "Suggested cards",
"other_cards": "Other cards", "other_cards": "Other cards",
"custom_cards": "Custom cards", "custom_cards": "Custom cards",
@@ -6104,7 +6155,6 @@
"name": "Map", "name": "Map",
"geo_location_sources": "Geolocation sources", "geo_location_sources": "Geolocation sources",
"no_geo_location_sources": "No geolocation sources available", "no_geo_location_sources": "No geolocation sources available",
"dark_mode": "Dark mode?",
"appearance": "Appearance", "appearance": "Appearance",
"theme_mode": "Theme Mode", "theme_mode": "Theme Mode",
"theme_modes": { "theme_modes": {
@@ -6884,8 +6934,6 @@
} }
}, },
"sections": { "sections": {
"description": "This dashboard is using the sections view released in Home Assistant 2024.3. Learn more about it in this {blog_post}.",
"description_blog_post": "blog post",
"titles": { "titles": {
"welcome": "Welcome", "welcome": "Welcome",
"living_room": "Living room", "living_room": "Living room",
@@ -7203,6 +7251,7 @@
}, },
"create_account": "Create account", "create_account": "Create account",
"error": { "error": {
"username_not_normalized": "Username can only contain lowercase letters, and can not contain whitespace.",
"password_not_match": "Passwords don't match" "password_not_match": "Passwords don't match"
} }
}, },
@@ -7372,6 +7421,7 @@
}, },
"dashboard": { "dashboard": {
"changelog": "Changelog", "changelog": "Changelog",
"current_version": "Current version: {version}",
"cpu_usage": "Add-on CPU usage", "cpu_usage": "Add-on CPU usage",
"ram_usage": "Add-on RAM usage", "ram_usage": "Add-on RAM usage",
"hostname": "Hostname", "hostname": "Hostname",
@@ -7472,7 +7522,7 @@
}, },
"watchdog": { "watchdog": {
"title": "Watchdog", "title": "Watchdog",
"description": "This will start the add-on if it crashes" "description": "This will restart the add-on if it crashes"
}, },
"auto_update": { "auto_update": {
"title": "Auto update", "title": "Auto update",
@@ -7845,6 +7895,62 @@
"restore": "[%key:ui::components::data-table::settings::restore%]" "restore": "[%key:ui::components::data-table::settings::restore%]"
} }
} }
},
"panel": {
"config": {
"logs": {
"caption": "[%key:ui::panel::config::logs::caption%]",
"description": "[%key:ui::panel::config::logs::description%]",
"details": "[%key:ui::panel::config::logs::details%]",
"search": "[%key:ui::panel::config::logs::search%]",
"failed_get_logs": "[%key:ui::panel::config::logs::failed_get_logs%]",
"no_issues_search": "[%key:ui::panel::config::logs::no_issues_search%]",
"load_logs": "[%key:ui::panel::config::logs::load_logs%]",
"nr_of_lines": "[%key:ui::panel::config::logs::nr_of_lines%]",
"loading_log": "[%key:ui::panel::config::logs::loading_log%]",
"no_errors": "[%key:ui::panel::config::logs::no_errors%]",
"no_issues": "[%key:ui::panel::config::logs::no_issues%]",
"clear": "[%key:ui::panel::config::logs::clear%]",
"refresh": "[%key:ui::panel::config::logs::refresh%]",
"copy": "[%key:ui::panel::config::logs::copy%]",
"log_provider": "[%key:ui::panel::config::logs::log_provider%]",
"multiple_messages": "[%key:ui::panel::config::logs::multiple_messages%]",
"level": {
"critical": "[%key:ui::panel::config::logs::level::critical%]",
"error": "[%key:ui::panel::config::logs::level::error%]",
"warning": "[%key:ui::panel::config::logs::level::warning%]",
"info": "[%key:ui::panel::config::logs::level::info%]",
"debug": "[%key:ui::panel::config::logs::level::debug%]"
},
"custom_integration": "[%key:ui::panel::config::logs::custom_integration%]",
"error_from_custom_integration": "[%key:ui::panel::config::logs::error_from_custom_integration%]",
"show_full_logs": "[%key:ui::panel::config::logs::show_full_logs%]",
"select_number_of_lines": "[%key:ui::panel::config::logs::select_number_of_lines%]",
"lines": "[%key:ui::panel::config::logs::lines%]",
"download_logs": "[%key:ui::panel::config::logs::download_logs%]",
"scroll_down_button": "[%key:ui::panel::config::logs::scroll_down_button%]",
"provider_not_found": "[%key:ui::panel::config::logs::provider_not_found%]",
"provider_not_available": "[%key:ui::panel::config::logs::provider_not_available%]",
"haos_boots_title": "[%key:ui::panel::config::logs::haos_boots_title%]",
"show_haos_boots": "[%key:ui::panel::config::logs::show_haos_boots%]",
"hide_haos_boots": "[%key:ui::panel::config::logs::hide_haos_boots%]",
"full_width": "[%key:ui::panel::config::logs::full_width%]",
"wrap_lines": "[%key:ui::panel::config::logs::wrap_lines%]",
"current": "[%key:ui::panel::config::logs::current%]",
"previous": "[%key:ui::panel::config::logs::previous%]",
"startups_ago": "[%key:ui::panel::config::logs::startups_ago%]",
"detail": {
"logger": "[%key:ui::panel::config::logs::detail::logger%]",
"source": "[%key:ui::panel::config::logs::detail::source%]",
"integration": "[%key:ui::panel::config::integrations::integration%]",
"documentation": "[%key:ui::panel::config::logs::detail::documentation%]",
"issues": "[%key:ui::panel::config::logs::detail::issues%]",
"first_occurred": "[%key:ui::panel::config::logs::detail::first_occurred%]",
"occurrences": "[%key:ui::panel::config::logs::detail::occurrences%]",
"last_logged": "[%key:ui::panel::config::logs::detail::last_logged%]"
}
}
}
} }
} }
} }

View File

@@ -1,3 +1,6 @@
import type { HomeAssistant } from "../types";
import { isIosApp } from "./is_ios";
export const fileDownload = (href: string, filename = ""): void => { export const fileDownload = (href: string, filename = ""): void => {
const a = document.createElement("a"); const a = document.createElement("a");
a.target = "_blank"; a.target = "_blank";
@@ -8,3 +11,6 @@ export const fileDownload = (href: string, filename = ""): void => {
a.dispatchEvent(new MouseEvent("click")); a.dispatchEvent(new MouseEvent("click"));
document.body.removeChild(a); document.body.removeChild(a);
}; };
export const downloadFileSupported = (hass: HomeAssistant): boolean =>
!isIosApp(hass) || !!hass.auth.external?.config.downloadFileSupported;

5
src/util/is_ios.ts Normal file
View File

@@ -0,0 +1,5 @@
import type { HomeAssistant } from "../types";
import { isSafari } from "./is_safari";
export const isIosApp = (hass: HomeAssistant): boolean =>
isSafari && !!hass.auth.external;

388
yarn.lock
View File

@@ -1265,9 +1265,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@codemirror/autocomplete@npm:6.18.1": "@codemirror/autocomplete@npm:6.18.2":
version: 6.18.1 version: 6.18.2
resolution: "@codemirror/autocomplete@npm:6.18.1" resolution: "@codemirror/autocomplete@npm:6.18.2"
dependencies: dependencies:
"@codemirror/language": "npm:^6.0.0" "@codemirror/language": "npm:^6.0.0"
"@codemirror/state": "npm:^6.0.0" "@codemirror/state": "npm:^6.0.0"
@@ -1278,7 +1278,7 @@ __metadata:
"@codemirror/state": ^6.0.0 "@codemirror/state": ^6.0.0
"@codemirror/view": ^6.0.0 "@codemirror/view": ^6.0.0
"@lezer/common": ^1.0.0 "@lezer/common": ^1.0.0
checksum: 10/3b56ac6c57214e3e50c6ed79c12ac1822e3774afb033e0e4fb98dffd252f5ae64e5bed67dc2ad9cbd5d784373031be90995ddb1b36a10c16a2eef6af832041e2 checksum: 10/35bd17afb53e8c99b1342964616f0bcc13f5f06a5d4e2d9936afdaea61742b1c20b3856d513c5d5676e3a9b6fd95e997c842467d21dfa106845e65ab1720b2f4
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1317,14 +1317,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@codemirror/search@npm:6.5.6": "@codemirror/search@npm:6.5.7":
version: 6.5.6 version: 6.5.7
resolution: "@codemirror/search@npm:6.5.6" resolution: "@codemirror/search@npm:6.5.7"
dependencies: dependencies:
"@codemirror/state": "npm:^6.0.0" "@codemirror/state": "npm:^6.0.0"
"@codemirror/view": "npm:^6.0.0" "@codemirror/view": "npm:^6.0.0"
crelt: "npm:^1.0.5" crelt: "npm:^1.0.5"
checksum: 10/6668a34b4617e909617d3d831627d74b7a7985e8cd86d396bfcb3e86262f2310fc029fd6c846f1b8f1e6768e75985c9f1b0b18b31e05341f06b5b75c1ffde38d checksum: 10/0a4c5e23c42231ffb829513940ee43a630585b4277fa8cc919a947f3821c9c2dc095d334bb0e4d51b3ebb50739a34a81ddbcc39ca9c1f6f935fdaa51a86661bf
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1413,150 +1413,150 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/ecma402-abstract@npm:2.2.1": "@formatjs/ecma402-abstract@npm:2.2.3":
version: 2.2.1 version: 2.2.3
resolution: "@formatjs/ecma402-abstract@npm:2.2.1" resolution: "@formatjs/ecma402-abstract@npm:2.2.3"
dependencies: dependencies:
"@formatjs/fast-memoize": "npm:2.2.2" "@formatjs/fast-memoize": "npm:2.2.3"
"@formatjs/intl-localematcher": "npm:0.5.6" "@formatjs/intl-localematcher": "npm:0.5.7"
tslib: "npm:2" tslib: "npm:2"
checksum: 10/8c281e14cb5f12b8697225be6b0ac13d057911e257d3c23928aad985b535df90b7bb2a235aab22753a6e57aef98f00b826514fc3703e69018ccc98c8d9848f38 checksum: 10/d39e9f0d36c296a635f52aa35e07a67b6aa90383a30a046a0508e5d730676399fd0e67188eff463fe2a4d5febc9f567af45788fdf881e070910be7eb9294dd8c
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/fast-memoize@npm:2.2.2": "@formatjs/fast-memoize@npm:2.2.3":
version: 2.2.2 version: 2.2.3
resolution: "@formatjs/fast-memoize@npm:2.2.2" resolution: "@formatjs/fast-memoize@npm:2.2.3"
dependencies: dependencies:
tslib: "npm:2" tslib: "npm:2"
checksum: 10/c6e958753eb41bb0875734762a44126a0d570706a31b32bb409e759cd372184c28e294b02fce0b0f0999c171ef717d513eaf7936862c498d78428b97db446ff8 checksum: 10/a9634acb5e03d051e09881eea5484ab02271f7d6b5f96ae9485674ab3c359aa881bc45fc07a1181ae4b2d6e288dadc169f578d142d698913ebbefa373014cac2
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/icu-messageformat-parser@npm:2.9.1": "@formatjs/icu-messageformat-parser@npm:2.9.3":
version: 2.9.1 version: 2.9.3
resolution: "@formatjs/icu-messageformat-parser@npm:2.9.1" resolution: "@formatjs/icu-messageformat-parser@npm:2.9.3"
dependencies: dependencies:
"@formatjs/ecma402-abstract": "npm:2.2.1" "@formatjs/ecma402-abstract": "npm:2.2.3"
"@formatjs/icu-skeleton-parser": "npm:1.8.5" "@formatjs/icu-skeleton-parser": "npm:1.8.7"
tslib: "npm:2" tslib: "npm:2"
checksum: 10/f52c7c55b1dfc141910089a0494abd98d1c13c0a359cfb3bfa0668a5e2015c0c579bf161978fdb3ab40fa9a7374a37ac062f8710ed285429bf60abde4a5d1183 checksum: 10/b24a3db43e4bf612107e981d5b40c077543d2266a08aac5cf01d5f65bf60527d5d16795e2e30063cb180b1d36d401944cd2ffb3a19d79b0cd28fa59751d19b7c
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/icu-skeleton-parser@npm:1.8.5": "@formatjs/icu-skeleton-parser@npm:1.8.7":
version: 1.8.5 version: 1.8.7
resolution: "@formatjs/icu-skeleton-parser@npm:1.8.5" resolution: "@formatjs/icu-skeleton-parser@npm:1.8.7"
dependencies: dependencies:
"@formatjs/ecma402-abstract": "npm:2.2.1" "@formatjs/ecma402-abstract": "npm:2.2.3"
tslib: "npm:2" tslib: "npm:2"
checksum: 10/5b9c57f80b751483bef8897ff9607a9eb215fd7a8d8ae9fa5c631edf6d16fa4532c853395f20b7f3f38d6d4d1a35b98cd06421291203c7ad333f52077ef2a406 checksum: 10/1a39815e5048f3c12a8d6a5b553271437b62e302724fc15c3b6967dc3e24823fcd9b8d3231a064991e163c147e54e588c571a092d557e93e78e738d218c6ef43
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/intl-datetimeformat@npm:6.16.1": "@formatjs/intl-datetimeformat@npm:6.16.3":
version: 6.16.1 version: 6.16.3
resolution: "@formatjs/intl-datetimeformat@npm:6.16.1" resolution: "@formatjs/intl-datetimeformat@npm:6.16.3"
dependencies: dependencies:
"@formatjs/ecma402-abstract": "npm:2.2.1" "@formatjs/ecma402-abstract": "npm:2.2.3"
"@formatjs/intl-localematcher": "npm:0.5.6" "@formatjs/intl-localematcher": "npm:0.5.7"
tslib: "npm:2" tslib: "npm:2"
checksum: 10/494868322d396e0eede6a27c16047858944f42fd3b45cf5d155f963df62e694b842ac0bef07e23aa73fa55cf143956d642d05ea62a3e762632101451975b5fc4 checksum: 10/4e213611b92eda40aa6053b9458be71fb752f020616bb0e93fc681efc4fc408dfec408ae33ded8678887730f8ee766568f90b6ca57de6e9d8f1de45dda794f08
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/intl-displaynames@npm:6.8.1": "@formatjs/intl-displaynames@npm:6.8.3":
version: 6.8.1 version: 6.8.3
resolution: "@formatjs/intl-displaynames@npm:6.8.1" resolution: "@formatjs/intl-displaynames@npm:6.8.3"
dependencies: dependencies:
"@formatjs/ecma402-abstract": "npm:2.2.1" "@formatjs/ecma402-abstract": "npm:2.2.3"
"@formatjs/intl-localematcher": "npm:0.5.6" "@formatjs/intl-localematcher": "npm:0.5.7"
tslib: "npm:2" tslib: "npm:2"
checksum: 10/627fc625e14b4d1bea5b2bf41e40050eb9775d0f66780e155719e21c062f9b3331d08b488ebcd3608c60999498af5a39e67cb5fd2a6d54a0e7395d7a63bfe643 checksum: 10/46c8d6e6d6d56d5f495c0bfb5784687a0af1ffd9eaeb72c1d9db8e21f8c7eeec346198871f8fe39f6eebfb19d6c3e46cbf92e213e6a6f0dfdb2f55fe96d43bcc
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/intl-enumerator@npm:1.8.1": "@formatjs/intl-enumerator@npm:1.8.3":
version: 1.8.1 version: 1.8.3
resolution: "@formatjs/intl-enumerator@npm:1.8.1" resolution: "@formatjs/intl-enumerator@npm:1.8.3"
dependencies: dependencies:
"@formatjs/ecma402-abstract": "npm:2.2.1" "@formatjs/ecma402-abstract": "npm:2.2.3"
tslib: "npm:2" tslib: "npm:2"
checksum: 10/0e4250de905e757fb88d6ff072968c72ed3a39de8ddaed73c38c0099825f11530c9b8e224573ae6e46cf49f1318e463f40ba2cdfa25cb7415382ba952b570bdc checksum: 10/a51ed7e15835cc1612282de46139d0f49553f004439a728a9118d1b9b15a3d05916e8aad4001e18c4909a3d4287fc07c921540c5ba8f32499f3243ac50d68a42
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/intl-getcanonicallocales@npm:2.5.1": "@formatjs/intl-getcanonicallocales@npm:2.5.2":
version: 2.5.1 version: 2.5.2
resolution: "@formatjs/intl-getcanonicallocales@npm:2.5.1" resolution: "@formatjs/intl-getcanonicallocales@npm:2.5.2"
dependencies: dependencies:
tslib: "npm:2" tslib: "npm:2"
checksum: 10/5e83c0b3574333e5027c3c4f74ea20800e50e36fb8efa69361457b57f618738f478b5d22777ba30a2b7a15bdff60101d8119169c909b33577244747d52e59614 checksum: 10/0d1738181911635d91d4a788d663fadd1aa045f40f0f05ac8b04adc06cd4f5ee3c50aa7c3a50c63ba7572f23e336720340c8240d6070d899e56adf25d0388f1b
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/intl-listformat@npm:7.7.1": "@formatjs/intl-listformat@npm:7.7.3":
version: 7.7.1 version: 7.7.3
resolution: "@formatjs/intl-listformat@npm:7.7.1" resolution: "@formatjs/intl-listformat@npm:7.7.3"
dependencies: dependencies:
"@formatjs/ecma402-abstract": "npm:2.2.1" "@formatjs/ecma402-abstract": "npm:2.2.3"
"@formatjs/intl-localematcher": "npm:0.5.6" "@formatjs/intl-localematcher": "npm:0.5.7"
tslib: "npm:2" tslib: "npm:2"
checksum: 10/a64581f1d2e8e0c0c83c5d56334a3e3786ed251e1a882d7610d2588d8602eacb32c9167032891e2796c30df3437c9ce52c7284786dca6f1f44250301060169ea checksum: 10/52ae02202a2bb0d8c16ea9a8f142d616e6ecb8400aa96ca618896cf529a3e3f5d88d64cb2644ad6a4ba7e17ee013d8fb3463419802afab5ca25afa51151ab62c
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/intl-locale@npm:4.2.1": "@formatjs/intl-locale@npm:4.2.3":
version: 4.2.1 version: 4.2.3
resolution: "@formatjs/intl-locale@npm:4.2.1" resolution: "@formatjs/intl-locale@npm:4.2.3"
dependencies: dependencies:
"@formatjs/ecma402-abstract": "npm:2.2.1" "@formatjs/ecma402-abstract": "npm:2.2.3"
"@formatjs/intl-enumerator": "npm:1.8.1" "@formatjs/intl-enumerator": "npm:1.8.3"
"@formatjs/intl-getcanonicallocales": "npm:2.5.1" "@formatjs/intl-getcanonicallocales": "npm:2.5.2"
tslib: "npm:2" tslib: "npm:2"
checksum: 10/4cba0fbeded2c7c5806528806f176cb833c43765bf1717470f4e001ab42581d5f0b52bf1893afef9597fba96dc3d4659507e490030f231523d460ec6686b9562 checksum: 10/dab4090653e62f1c3453c074c3047d0e22ee6a3d33ac00afa45f1541b8686b453c671755e8faeeb1253b61131071c02506b56c094941efd6195d40163007182e
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/intl-localematcher@npm:0.5.6": "@formatjs/intl-localematcher@npm:0.5.7":
version: 0.5.6 version: 0.5.7
resolution: "@formatjs/intl-localematcher@npm:0.5.6" resolution: "@formatjs/intl-localematcher@npm:0.5.7"
dependencies: dependencies:
tslib: "npm:2" tslib: "npm:2"
checksum: 10/14eac6bb25dcfeedd7960f44dec5a137999729da00b294ddf1133abe760ced4342f37734bc750b4c47f8dd8d5633a7da38d274503f80d7e965bb1f6fb6f2988c checksum: 10/52201f12212e7e9cba1a4f99020da587b13e44e06e03c4ccd4e5ac0829b411e73dfe0904a9039ef81eeabeea04ed8cfae9e727e6791acd0230745b7bd3ad059e
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/intl-numberformat@npm:8.14.1": "@formatjs/intl-numberformat@npm:8.14.3":
version: 8.14.1 version: 8.14.3
resolution: "@formatjs/intl-numberformat@npm:8.14.1" resolution: "@formatjs/intl-numberformat@npm:8.14.3"
dependencies: dependencies:
"@formatjs/ecma402-abstract": "npm:2.2.1" "@formatjs/ecma402-abstract": "npm:2.2.3"
"@formatjs/intl-localematcher": "npm:0.5.6" "@formatjs/intl-localematcher": "npm:0.5.7"
tslib: "npm:2" tslib: "npm:2"
checksum: 10/51152d1b9607a35c64e6089e44b90c7ec90be3b1925ba47ffc559ddb4fd72afae76e83af3d436831ea0fc47dc0e9fee9cd3d576280440f2dce03cb6bd24e0bed checksum: 10/7a4e52ace65589ceb441032a09b88616e71ba4220605e498b1a064f43672cad5cca8c98b72446ffd7d57ef098c658c245c08a16623e0b1bc10940ff7e71069c7
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/intl-pluralrules@npm:5.3.1": "@formatjs/intl-pluralrules@npm:5.3.3":
version: 5.3.1 version: 5.3.3
resolution: "@formatjs/intl-pluralrules@npm:5.3.1" resolution: "@formatjs/intl-pluralrules@npm:5.3.3"
dependencies: dependencies:
"@formatjs/ecma402-abstract": "npm:2.2.1" "@formatjs/ecma402-abstract": "npm:2.2.3"
"@formatjs/intl-localematcher": "npm:0.5.6" "@formatjs/intl-localematcher": "npm:0.5.7"
tslib: "npm:2" tslib: "npm:2"
checksum: 10/fc83c3547a9f0af6331c2970f265234fde967848ff738730f2e87ce816636d8778ead1185f5ecccc692cb8b63c11412dc85deac9d3425f44fe3a6a6c30c8b776 checksum: 10/3679c63aa2b9dde474572998b829ecb5d134f1efe508e1e5a06089480bbff9f2216235a7d5745c434030dc17c8a83385f0639a71a22f1d648fcbc6fff90f57e3
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/intl-relativetimeformat@npm:11.4.1": "@formatjs/intl-relativetimeformat@npm:11.4.3":
version: 11.4.1 version: 11.4.3
resolution: "@formatjs/intl-relativetimeformat@npm:11.4.1" resolution: "@formatjs/intl-relativetimeformat@npm:11.4.3"
dependencies: dependencies:
"@formatjs/ecma402-abstract": "npm:2.2.1" "@formatjs/ecma402-abstract": "npm:2.2.3"
"@formatjs/intl-localematcher": "npm:0.5.6" "@formatjs/intl-localematcher": "npm:0.5.7"
tslib: "npm:2" tslib: "npm:2"
checksum: 10/80817403301baed257fbd8c793b9ed077a2e6dd0414a6895b5bfde3619aebc818f30535da9b560a6186fac783cf09561c495d2c6568a980bd635736194655af5 checksum: 10/7c7548ba133031873683a37566d646e4e3f50ea979773de199b41769df23648be2b44b53975809bd53f97a95d1d44038c0a09b1c031e05f0de6f7bba843b1aad
languageName: node languageName: node
linkType: hard linkType: hard
@@ -3944,7 +3944,41 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/estree@npm:*, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.5": "@types/dom-webcodecs@npm:^0.1.13":
version: 0.1.13
resolution: "@types/dom-webcodecs@npm:0.1.13"
checksum: 10/99cb227416725efd4b22175ef18988ae3fc728480fe6ed2192777d7dba52d18af540b4df49fcfa3cf73753d1dcf9d5399b089702bde215d78679284848b078f7
languageName: node
linkType: hard
"@types/emscripten@npm:^1.39.13":
version: 1.39.13
resolution: "@types/emscripten@npm:1.39.13"
checksum: 10/02c0446150f9cc2c74dc3a551f86ce13df266c33d8b98d11d9f17263e2d98a6a6b4d36bdd15066c4e1547ae1ed2d52eed9420116b4935d119009e0f53ddbb041
languageName: node
linkType: hard
"@types/eslint-scope@npm:^3.7.7":
version: 3.7.7
resolution: "@types/eslint-scope@npm:3.7.7"
dependencies:
"@types/eslint": "npm:*"
"@types/estree": "npm:*"
checksum: 10/e2889a124aaab0b89af1bab5959847c5bec09809209255de0e63b9f54c629a94781daa04adb66bffcdd742f5e25a17614fb933965093c0eea64aacda4309380e
languageName: node
linkType: hard
"@types/eslint@npm:*":
version: 9.6.1
resolution: "@types/eslint@npm:9.6.1"
dependencies:
"@types/estree": "npm:*"
"@types/json-schema": "npm:*"
checksum: 10/719fcd255760168a43d0e306ef87548e1e15bffe361d5f4022b0f266575637acc0ecb85604ac97879ee8ae83c6a6d0613b0ed31d0209ddf22a0fe6d608fc56fe
languageName: node
linkType: hard
"@types/estree@npm:*, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.6":
version: 1.0.6 version: 1.0.6
resolution: "@types/estree@npm:1.0.6" resolution: "@types/estree@npm:1.0.6"
checksum: 10/9d35d475095199c23e05b431bcdd1f6fec7380612aed068b14b2a08aa70494de8a9026765a5a91b1073f636fb0368f6d8973f518a31391d519e20c59388ed88d checksum: 10/9d35d475095199c23e05b431bcdd1f6fec7380612aed068b14b2a08aa70494de8a9026765a5a91b1073f636fb0368f6d8973f518a31391d519e20c59388ed88d
@@ -4090,7 +4124,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": "@types/json-schema@npm:*, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9":
version: 7.0.15 version: 7.0.15
resolution: "@types/json-schema@npm:7.0.15" resolution: "@types/json-schema@npm:7.0.15"
checksum: 10/1a3c3e06236e4c4aab89499c428d585527ce50c24fe8259e8b3926d3df4cfbbbcf306cfc73ddfb66cbafc973116efd15967020b0f738f63e09e64c7d260519e7 checksum: 10/1a3c3e06236e4c4aab89499c428d585527ce50c24fe8259e8b3926d3df4cfbbbcf306cfc73ddfb66cbafc973116efd15967020b0f738f63e09e64c7d260519e7
@@ -5187,15 +5221,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"acorn-import-attributes@npm:^1.9.5":
version: 1.9.5
resolution: "acorn-import-attributes@npm:1.9.5"
peerDependencies:
acorn: ^8
checksum: 10/8bfbfbb6e2467b9b47abb4d095df717ab64fce2525da65eabee073e85e7975fb3a176b6c8bba17c99a7d8ede283a10a590272304eb54a93c4aa1af9790d47a8b
languageName: node
linkType: hard
"acorn-jsx@npm:^5.3.2": "acorn-jsx@npm:^5.3.2":
version: 5.3.2 version: 5.3.2
resolution: "acorn-jsx@npm:5.3.2" resolution: "acorn-jsx@npm:5.3.2"
@@ -5205,12 +5230,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"acorn@npm:^8.5.0, acorn@npm:^8.7.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": "acorn@npm:^8.14.0, acorn@npm:^8.5.0, acorn@npm:^8.8.2, acorn@npm:^8.9.0":
version: 8.13.0 version: 8.14.0
resolution: "acorn@npm:8.13.0" resolution: "acorn@npm:8.14.0"
bin: bin:
acorn: bin/acorn acorn: bin/acorn
checksum: 10/33e3a03114b02b3bc5009463b3d9549b31a90ee38ebccd5e66515830a02acf62a90edcc12abfb6c9fb3837b6c17a3ec9b72b3bf52ac31d8ad8248a4af871e0f5 checksum: 10/6df29c35556782ca9e632db461a7f97947772c6c1d5438a81f0c873a3da3a792487e83e404d1c6c25f70513e91aa18745f6eafb1fcc3a43ecd1920b21dd173d2
languageName: node languageName: node
linkType: hard linkType: hard
@@ -5731,6 +5756,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"barcode-detector@npm:2.2.11":
version: 2.2.11
resolution: "barcode-detector@npm:2.2.11"
dependencies:
"@types/dom-webcodecs": "npm:^0.1.13"
zxing-wasm: "npm:1.2.14"
checksum: 10/91f04ac8a73a5fccf15d08c2b148e3f9584448956de9b2506f5c5c3213ba133c504cd6890926f4fde2b1294e42b9943f979820440bc8373388d5a86cb6c764c5
languageName: node
linkType: hard
"bare-events@npm:^2.2.0": "bare-events@npm:^2.2.0":
version: 2.5.0 version: 2.5.0
resolution: "bare-events@npm:2.5.0" resolution: "bare-events@npm:2.5.0"
@@ -5881,7 +5916,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"browserslist@npm:^4.21.10, browserslist@npm:^4.23.3, browserslist@npm:^4.24.0": "browserslist@npm:^4.23.3, browserslist@npm:^4.24.0":
version: 4.24.0 version: 4.24.0
resolution: "browserslist@npm:4.24.0" resolution: "browserslist@npm:4.24.0"
dependencies: dependencies:
@@ -6540,10 +6575,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"core-js@npm:3.38.1": "core-js@npm:3.39.0":
version: 3.38.1 version: 3.39.0
resolution: "core-js@npm:3.38.1" resolution: "core-js@npm:3.39.0"
checksum: 10/3c25fdf0b2595ed37ceb305213a61e2cf26185f628455e99d1c736dda5f69e2de4de7126e6a1da136f54260c4fcc982c4215e37b5a618790a597930f854c0a37 checksum: 10/a3d34e669783dfc878e545f1983f60d9ff48a3867cd1d7ff8839b849e053002a208c7c14a5ca354b8e0b54982901e2f83dc87c3d9b95de0a94b4071d1c74e5f6
languageName: node languageName: node
linkType: hard linkType: hard
@@ -8716,22 +8751,22 @@ __metadata:
"@babel/runtime": "npm:7.26.0" "@babel/runtime": "npm:7.26.0"
"@braintree/sanitize-url": "npm:7.1.0" "@braintree/sanitize-url": "npm:7.1.0"
"@bundle-stats/plugin-webpack-filter": "npm:4.16.0" "@bundle-stats/plugin-webpack-filter": "npm:4.16.0"
"@codemirror/autocomplete": "npm:6.18.1" "@codemirror/autocomplete": "npm:6.18.2"
"@codemirror/commands": "npm:6.7.1" "@codemirror/commands": "npm:6.7.1"
"@codemirror/language": "npm:6.10.3" "@codemirror/language": "npm:6.10.3"
"@codemirror/legacy-modes": "npm:6.4.1" "@codemirror/legacy-modes": "npm:6.4.1"
"@codemirror/search": "npm:6.5.6" "@codemirror/search": "npm:6.5.7"
"@codemirror/state": "npm:6.4.1" "@codemirror/state": "npm:6.4.1"
"@codemirror/view": "npm:6.34.1" "@codemirror/view": "npm:6.34.1"
"@egjs/hammerjs": "npm:2.0.17" "@egjs/hammerjs": "npm:2.0.17"
"@formatjs/intl-datetimeformat": "npm:6.16.1" "@formatjs/intl-datetimeformat": "npm:6.16.3"
"@formatjs/intl-displaynames": "npm:6.8.1" "@formatjs/intl-displaynames": "npm:6.8.3"
"@formatjs/intl-getcanonicallocales": "npm:2.5.1" "@formatjs/intl-getcanonicallocales": "npm:2.5.2"
"@formatjs/intl-listformat": "npm:7.7.1" "@formatjs/intl-listformat": "npm:7.7.3"
"@formatjs/intl-locale": "npm:4.2.1" "@formatjs/intl-locale": "npm:4.2.3"
"@formatjs/intl-numberformat": "npm:8.14.1" "@formatjs/intl-numberformat": "npm:8.14.3"
"@formatjs/intl-pluralrules": "npm:5.3.1" "@formatjs/intl-pluralrules": "npm:5.3.3"
"@formatjs/intl-relativetimeformat": "npm:11.4.1" "@formatjs/intl-relativetimeformat": "npm:11.4.3"
"@fullcalendar/core": "npm:6.1.15" "@fullcalendar/core": "npm:6.1.15"
"@fullcalendar/daygrid": "npm:6.1.15" "@fullcalendar/daygrid": "npm:6.1.15"
"@fullcalendar/interaction": "npm:6.1.15" "@fullcalendar/interaction": "npm:6.1.15"
@@ -8822,12 +8857,13 @@ __metadata:
app-datepicker: "npm:5.1.1" app-datepicker: "npm:5.1.1"
babel-loader: "npm:9.2.1" babel-loader: "npm:9.2.1"
babel-plugin-template-html-minifier: "npm:4.1.0" babel-plugin-template-html-minifier: "npm:4.1.0"
barcode-detector: "npm:2.2.11"
browserslist-useragent-regexp: "npm:4.1.3" browserslist-useragent-regexp: "npm:4.1.3"
chai: "npm:5.1.2" chai: "npm:5.1.2"
chart.js: "npm:4.4.6" chart.js: "npm:4.4.6"
color-name: "npm:2.0.0" color-name: "npm:2.0.0"
comlink: "npm:4.4.1" comlink: "npm:4.4.1"
core-js: "npm:3.38.1" core-js: "npm:3.39.0"
cropperjs: "npm:1.6.2" cropperjs: "npm:1.6.2"
date-fns: "npm:4.1.0" date-fns: "npm:4.1.0"
date-fns-tz: "npm:3.2.0" date-fns-tz: "npm:3.2.0"
@@ -8862,7 +8898,7 @@ __metadata:
husky: "npm:9.1.6" husky: "npm:9.1.6"
idb-keyval: "npm:6.2.1" idb-keyval: "npm:6.2.1"
instant-mocha: "npm:1.5.3" instant-mocha: "npm:1.5.3"
intl-messageformat: "npm:10.7.3" intl-messageformat: "npm:10.7.5"
js-yaml: "npm:4.1.0" js-yaml: "npm:4.1.0"
jszip: "npm:3.10.1" jszip: "npm:3.10.1"
leaflet: "npm:1.9.4" leaflet: "npm:1.9.4"
@@ -8877,7 +8913,7 @@ __metadata:
map-stream: "npm:0.0.7" map-stream: "npm:0.0.7"
marked: "npm:14.1.3" marked: "npm:14.1.3"
memoize-one: "npm:6.0.0" memoize-one: "npm:6.0.0"
mocha: "npm:10.7.3" mocha: "npm:10.8.2"
node-vibrant: "npm:3.2.1-alpha.1" node-vibrant: "npm:3.2.1-alpha.1"
object-hash: "npm:3.0.0" object-hash: "npm:3.0.0"
open: "npm:10.1.0" open: "npm:10.1.0"
@@ -8913,7 +8949,7 @@ __metadata:
vis-network: "npm:9.1.9" vis-network: "npm:9.1.9"
vue: "npm:2.7.16" vue: "npm:2.7.16"
vue2-daterange-picker: "npm:0.6.8" vue2-daterange-picker: "npm:0.6.8"
webpack: "npm:5.95.0" webpack: "npm:5.96.1"
webpack-cli: "npm:5.1.4" webpack-cli: "npm:5.1.4"
webpack-dev-server: "npm:5.1.0" webpack-dev-server: "npm:5.1.0"
webpack-manifest-plugin: "npm:5.0.0" webpack-manifest-plugin: "npm:5.0.0"
@@ -8921,12 +8957,12 @@ __metadata:
webpackbar: "npm:6.0.1" webpackbar: "npm:6.0.1"
weekstart: "npm:2.0.0" weekstart: "npm:2.0.0"
workbox-build: "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch" workbox-build: "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
workbox-cacheable-response: "npm:7.1.0" workbox-cacheable-response: "npm:7.3.0"
workbox-core: "npm:7.1.0" workbox-core: "npm:7.3.0"
workbox-expiration: "npm:7.1.0" workbox-expiration: "npm:7.3.0"
workbox-precaching: "npm:7.1.0" workbox-precaching: "npm:7.3.0"
workbox-routing: "npm:7.1.0" workbox-routing: "npm:7.3.0"
workbox-strategies: "npm:7.1.0" workbox-strategies: "npm:7.3.0"
xss: "npm:1.0.15" xss: "npm:1.0.15"
languageName: unknown languageName: unknown
linkType: soft linkType: soft
@@ -9312,15 +9348,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"intl-messageformat@npm:10.7.3": "intl-messageformat@npm:10.7.5":
version: 10.7.3 version: 10.7.5
resolution: "intl-messageformat@npm:10.7.3" resolution: "intl-messageformat@npm:10.7.5"
dependencies: dependencies:
"@formatjs/ecma402-abstract": "npm:2.2.1" "@formatjs/ecma402-abstract": "npm:2.2.3"
"@formatjs/fast-memoize": "npm:2.2.2" "@formatjs/fast-memoize": "npm:2.2.3"
"@formatjs/icu-messageformat-parser": "npm:2.9.1" "@formatjs/icu-messageformat-parser": "npm:2.9.3"
tslib: "npm:2" tslib: "npm:2"
checksum: 10/e387f7f37a295d9d386af0c6392ba135a4580e86177161f1f400d470fed1f8c7b3cb6c724cbc2f50a7ded2e20f202977d8bf5e2bbc626f72016a5b5b6752b76d checksum: 10/8880448d62bd0260eafd4ee7ccfabaea573476f28e6d6bf47e027ee9c1d46d4919a076df7abedaf282422ff80ade02b5c637c69cdf739ee405e4837098bac37e
languageName: node languageName: node
linkType: hard linkType: hard
@@ -10966,9 +11002,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"mocha@npm:10.7.3": "mocha@npm:10.8.2":
version: 10.7.3 version: 10.8.2
resolution: "mocha@npm:10.7.3" resolution: "mocha@npm:10.8.2"
dependencies: dependencies:
ansi-colors: "npm:^4.1.3" ansi-colors: "npm:^4.1.3"
browser-stdout: "npm:^1.3.1" browser-stdout: "npm:^1.3.1"
@@ -10993,7 +11029,7 @@ __metadata:
bin: bin:
_mocha: bin/_mocha _mocha: bin/_mocha
mocha: bin/mocha.js mocha: bin/mocha.js
checksum: 10/5757aeb320df2507338bfba41731070ce16d27177c5876672fff4bcc4f7b7bcf1afe6ec761bfded43a5d28032d7b797b8b905b5b44c9420203f3ee71457732c1 checksum: 10/903bbffcb195ef9d36b27db54e3462c5486de1397289e0953735b3530397a139336c452bcf5188c663496c660d2285bbb6c7213290d36d536ad647b6145cb917
languageName: node languageName: node
linkType: hard linkType: hard
@@ -14678,17 +14714,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"webpack@npm:5.95.0": "webpack@npm:5.96.1":
version: 5.95.0 version: 5.96.1
resolution: "webpack@npm:5.95.0" resolution: "webpack@npm:5.96.1"
dependencies: dependencies:
"@types/estree": "npm:^1.0.5" "@types/eslint-scope": "npm:^3.7.7"
"@types/estree": "npm:^1.0.6"
"@webassemblyjs/ast": "npm:^1.12.1" "@webassemblyjs/ast": "npm:^1.12.1"
"@webassemblyjs/wasm-edit": "npm:^1.12.1" "@webassemblyjs/wasm-edit": "npm:^1.12.1"
"@webassemblyjs/wasm-parser": "npm:^1.12.1" "@webassemblyjs/wasm-parser": "npm:^1.12.1"
acorn: "npm:^8.7.1" acorn: "npm:^8.14.0"
acorn-import-attributes: "npm:^1.9.5" browserslist: "npm:^4.24.0"
browserslist: "npm:^4.21.10"
chrome-trace-event: "npm:^1.0.2" chrome-trace-event: "npm:^1.0.2"
enhanced-resolve: "npm:^5.17.1" enhanced-resolve: "npm:^5.17.1"
es-module-lexer: "npm:^1.2.1" es-module-lexer: "npm:^1.2.1"
@@ -14710,7 +14746,7 @@ __metadata:
optional: true optional: true
bin: bin:
webpack: bin/webpack.js webpack: bin/webpack.js
checksum: 10/0377ad3a550b041f26237c96fb55754625b0ce6bae83c1c2447e3262ad056b0b0ad770dcbb92b59f188e9a2bd56155ce910add17dcf023cfbe78bdec774380c1 checksum: 10/d3419ffd198252e1d0301bd0c072cee93172f3e47937c745aa8202691d2f5d529d4ba4a1965d1450ad89a1bcd3c1f70ae09e57232b0d01dd38d69c1060e964d5
languageName: node languageName: node
linkType: hard linkType: hard
@@ -14990,6 +15026,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"workbox-cacheable-response@npm:7.3.0":
version: 7.3.0
resolution: "workbox-cacheable-response@npm:7.3.0"
dependencies:
workbox-core: "npm:7.3.0"
checksum: 10/44cd7bc26e509ca96b1b84e3ff5964296efa645853f114f39789d21c0a214ca5fc047259910b303e220bb4052155cddc5639993fcee076fac496b4895ff17a15
languageName: node
linkType: hard
"workbox-core@npm:7.1.0": "workbox-core@npm:7.1.0":
version: 7.1.0 version: 7.1.0
resolution: "workbox-core@npm:7.1.0" resolution: "workbox-core@npm:7.1.0"
@@ -14997,6 +15042,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"workbox-core@npm:7.3.0":
version: 7.3.0
resolution: "workbox-core@npm:7.3.0"
checksum: 10/228fb7018a0568c329e21d47d84980f93ebfef9b1eb3f40ddc3516ca6ae58d51dc7ca4dddc829332775b59a3079e62d105c5e1c5c312805d177b963f8bf54393
languageName: node
linkType: hard
"workbox-expiration@npm:7.1.0": "workbox-expiration@npm:7.1.0":
version: 7.1.0 version: 7.1.0
resolution: "workbox-expiration@npm:7.1.0" resolution: "workbox-expiration@npm:7.1.0"
@@ -15007,6 +15059,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"workbox-expiration@npm:7.3.0":
version: 7.3.0
resolution: "workbox-expiration@npm:7.3.0"
dependencies:
idb: "npm:^7.0.1"
workbox-core: "npm:7.3.0"
checksum: 10/83e021d700e521a65a89907679d1a580aacc0419428286910ec7c6b0a538326f71f05566434f666ebf6c9fbe819ef3ea81428df1d868f9ea92527afe5d11152d
languageName: node
linkType: hard
"workbox-google-analytics@npm:7.1.0": "workbox-google-analytics@npm:7.1.0":
version: 7.1.0 version: 7.1.0
resolution: "workbox-google-analytics@npm:7.1.0" resolution: "workbox-google-analytics@npm:7.1.0"
@@ -15039,6 +15101,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"workbox-precaching@npm:7.3.0":
version: 7.3.0
resolution: "workbox-precaching@npm:7.3.0"
dependencies:
workbox-core: "npm:7.3.0"
workbox-routing: "npm:7.3.0"
workbox-strategies: "npm:7.3.0"
checksum: 10/d14135c471a45de36438c40eed7cb7157cdb336d4216a775486c6307d1ac316794d64231c2e2d0a4c313bb4a4fec623ab77e391cc458b4f2afa64e2487acb2e8
languageName: node
linkType: hard
"workbox-range-requests@npm:7.1.0": "workbox-range-requests@npm:7.1.0":
version: 7.1.0 version: 7.1.0
resolution: "workbox-range-requests@npm:7.1.0" resolution: "workbox-range-requests@npm:7.1.0"
@@ -15071,6 +15144,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"workbox-routing@npm:7.3.0":
version: 7.3.0
resolution: "workbox-routing@npm:7.3.0"
dependencies:
workbox-core: "npm:7.3.0"
checksum: 10/0d729f9c5cfc5754404ac1f7b729c7740ddc806203792701ac642151fbec939b4aa0fb289eab2295e49180e8154ad9bb1380effb7e0f0362163b79db4291dba7
languageName: node
linkType: hard
"workbox-strategies@npm:7.1.0": "workbox-strategies@npm:7.1.0":
version: 7.1.0 version: 7.1.0
resolution: "workbox-strategies@npm:7.1.0" resolution: "workbox-strategies@npm:7.1.0"
@@ -15080,6 +15162,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"workbox-strategies@npm:7.3.0":
version: 7.3.0
resolution: "workbox-strategies@npm:7.3.0"
dependencies:
workbox-core: "npm:7.3.0"
checksum: 10/61ba672075ef8aaa70ad9221460dab80a7d8920e324e14137460f26ebe8b137e5589fb75c664e0efeaf4402e3d8435a9b1818f9a9c61f88863c0e0315af337e7
languageName: node
linkType: hard
"workbox-streams@npm:7.1.0": "workbox-streams@npm:7.1.0":
version: 7.1.0 version: 7.1.0
resolution: "workbox-streams@npm:7.1.0" resolution: "workbox-streams@npm:7.1.0"
@@ -15399,3 +15490,12 @@ __metadata:
checksum: 10/f2e05b767ed3141e6372a80af9caa4715d60969227f38b1a4370d60bffe153c9c5b33a862905609afc9b375ec57cd40999810d20e5e10229a204e8bde7ef255c checksum: 10/f2e05b767ed3141e6372a80af9caa4715d60969227f38b1a4370d60bffe153c9c5b33a862905609afc9b375ec57cd40999810d20e5e10229a204e8bde7ef255c
languageName: node languageName: node
linkType: hard linkType: hard
"zxing-wasm@npm:1.2.14":
version: 1.2.14
resolution: "zxing-wasm@npm:1.2.14"
dependencies:
"@types/emscripten": "npm:^1.39.13"
checksum: 10/02ea0408553f1aebb412a97c5e11887c58da1b2adff636302232535e752b91bdb3f358411259b6cddd34b341df565336bc03f48895e362f61316018d9bbff8c3
languageName: node
linkType: hard