Compare commits

...

58 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
Paul Bottein
a4bb0e04ab Bumped version to 20241031.0 2024-10-31 15:13:21 +01:00
renovate[bot]
1df60056b2 Update dependency chart.js to v4.4.6 (#22612)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-31 13:57:45 +00:00
Paul Bottein
c4bc1f627f Remove get layout options warning in console (#22611) 2024-10-31 13:48:21 +00:00
Wendelin
f4df5852fb Fix hassio repositories tooltip (#22610) 2024-10-31 13:43:51 +00:00
Wendelin
152b665f2e Fix hassio backups translations (#22609)
* Fix hassio backup styling

* Add missing hassio backups data-table translations
2024-10-31 13:57:40 +01:00
Paul Bottein
f1d49aaeb1 Add theme variable for text in heading badge, fix font family (#22606) 2024-10-31 13:45:42 +01:00
Paul Bottein
5db293ce01 Use entity instead of entity_id for more info action (#22603) 2024-10-31 12:59:23 +01:00
Wendelin
744cda3974 Add fallback for missing logs boot result. (#22600)
* Add fallback for missing logs boot result.

* Add fallback for logs full download when boots are missing

* Fix function naming and single boot select in error-log-card
2024-10-31 08:57:27 +01:00
109 changed files with 2388 additions and 712 deletions

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- 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:
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
token: ${{ github.token }}

View File

@@ -55,7 +55,7 @@ jobs:
script/release
- name: Upload release assets
uses: softprops/action-gh-release@v2.0.8
uses: softprops/action-gh-release@v2.0.9
with:
files: |
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 () => {
const staticDir = paths.app_output_static;
copyLocaleData(staticDir);
@@ -143,6 +151,7 @@ gulp.task("copy-static-app", async () => {
copyMapPanel(staticDir);
// Qr Scanner assets
copyZXingWasm(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 { demoLovelaceDescription } from "./description";
import { demoEntitiesSections } from "./entities";
import { demoLovelaceSections } from "./lovelace";
@@ -7,7 +6,6 @@ export const demoSections: DemoConfig = {
authorName: "Home Assistant",
authorUrl: "https://github.com/home-assistant/frontend/",
name: "Home Demo",
description: demoLovelaceDescription,
lovelace: demoLovelaceSections,
entities: demoEntitiesSections,
theme: () => ({}),

View File

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

View File

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

View File

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

View File

@@ -374,6 +374,9 @@ export class HassioBackups extends LitElement {
haStyle,
hassioStyle,
css`
:host {
color: var(--primary-text-color);
}
.table-header {
display: flex;
justify-content: space-between;

View File

@@ -120,9 +120,6 @@ class HassioRepositoriesDialog extends LitElement {
</div>
<div class="delete" slot="end">
<ha-icon-button
.label=${this._dialogParams!.supervisor.localize(
"dialog.repositories.remove"
)}
.disabled=${usedRepositories.includes(repo.slug)}
.slug=${repo.slug}
.path=${usedRepositories.includes(repo.slug)
@@ -146,7 +143,11 @@ class HassioRepositoriesDialog extends LitElement {
</ha-md-list-item>
`
)
: html`<ha-md-list-item> No repositories </ha-md-list-item>`}
: html`<ha-md-list-item
>${this._dialogParams!.supervisor.localize(
"dialog.repositories.no_repositories"
)}</ha-md-list-item
>`}
</ha-md-list>
<div class="layout horizontal bottom">
<ha-textfield
@@ -212,6 +213,7 @@ class HassioRepositoriesDialog extends LitElement {
}
ha-md-list-item {
position: relative;
--md-item-overflow: visible;
}
`,
];

View File

@@ -27,22 +27,22 @@
"dependencies": {
"@babel/runtime": "7.26.0",
"@braintree/sanitize-url": "7.1.0",
"@codemirror/autocomplete": "6.18.1",
"@codemirror/autocomplete": "6.18.2",
"@codemirror/commands": "6.7.1",
"@codemirror/language": "6.10.3",
"@codemirror/legacy-modes": "6.4.1",
"@codemirror/search": "6.5.6",
"@codemirror/search": "6.5.7",
"@codemirror/state": "6.4.1",
"@codemirror/view": "6.34.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.16.1",
"@formatjs/intl-displaynames": "6.8.1",
"@formatjs/intl-getcanonicallocales": "2.5.1",
"@formatjs/intl-listformat": "7.7.1",
"@formatjs/intl-locale": "4.2.1",
"@formatjs/intl-numberformat": "8.14.1",
"@formatjs/intl-pluralrules": "5.3.1",
"@formatjs/intl-relativetimeformat": "11.4.1",
"@formatjs/intl-datetimeformat": "6.16.3",
"@formatjs/intl-displaynames": "6.8.3",
"@formatjs/intl-getcanonicallocales": "2.5.2",
"@formatjs/intl-listformat": "7.7.3",
"@formatjs/intl-locale": "4.2.3",
"@formatjs/intl-numberformat": "8.14.3",
"@formatjs/intl-pluralrules": "5.3.3",
"@formatjs/intl-relativetimeformat": "11.4.3",
"@fullcalendar/core": "6.1.15",
"@fullcalendar/daygrid": "6.1.15",
"@fullcalendar/interaction": "6.1.15",
@@ -98,10 +98,11 @@
"@webcomponents/scoped-custom-element-registry": "0.0.9",
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
"chart.js": "4.4.5",
"barcode-detector": "2.2.11",
"chart.js": "4.4.6",
"color-name": "2.0.0",
"comlink": "4.4.1",
"core-js": "3.38.1",
"core-js": "3.39.0",
"cropperjs": "1.6.2",
"date-fns": "4.1.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",
"home-assistant-js-websocket": "9.4.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.7.3",
"intl-messageformat": "10.7.5",
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
"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",
"vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0",
"workbox-cacheable-response": "7.1.0",
"workbox-core": "7.1.0",
"workbox-expiration": "7.1.0",
"workbox-precaching": "7.1.0",
"workbox-routing": "7.1.0",
"workbox-strategies": "7.1.0",
"workbox-cacheable-response": "7.3.0",
"workbox-core": "7.3.0",
"workbox-expiration": "7.3.0",
"workbox-precaching": "7.3.0",
"workbox-routing": "7.3.0",
"workbox-strategies": "7.3.0",
"xss": "1.0.15"
},
"devDependencies": {
@@ -224,7 +225,7 @@
"lodash.template": "4.5.0",
"magic-string": "0.30.12",
"map-stream": "0.0.7",
"mocha": "10.7.3",
"mocha": "10.8.2",
"object-hash": "3.0.0",
"open": "10.1.0",
"pinst": "3.0.0",
@@ -241,7 +242,7 @@
"transform-async-modules-webpack-plugin": "1.1.1",
"ts-lit-plugin": "2.0.2",
"typescript": "5.6.3",
"webpack": "5.95.0",
"webpack": "5.96.1",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.1.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]
name = "home-assistant-frontend"
version = "20241030.0"
version = "20241106.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"

View File

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

View File

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

View File

@@ -7,6 +7,8 @@ import {
type PropertyValues,
} from "lit";
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 { supportsFeature } from "../common/entity/supports-feature";
import {
@@ -24,6 +26,13 @@ import type { HomeAssistant } from "../types";
import "./ha-hls-player";
import "./ha-web-rtc-player";
const MJPEG_STREAM = "mjpeg";
type Stream = {
type: StreamType | typeof MJPEG_STREAM;
visible: boolean;
};
@customElement("ha-camera-stream")
export class HaCameraStream extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@@ -46,8 +55,6 @@ export class HaCameraStream extends LitElement {
@state() private _capabilities?: CameraCapabilities;
@state() private _streamType?: StreamType;
@state() private _hlsStreams?: { 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 {
if (
changedProps.has("stateObj") &&
!this._shouldRenderMJPEG &&
this.stateObj &&
(changedProps.get("stateObj") as CameraEntity | undefined)?.entity_id !==
this.stateObj.entity_id
@@ -79,50 +85,63 @@ export class HaCameraStream extends LitElement {
if (!this.stateObj) {
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
.src=${__DEMO__
? this.stateObj.attributes.entity_picture!
: this._connected
? computeMJPEGStreamUrl(this.stateObj)
: ""}
: this._posterUrl || ""}
alt=${`Preview of the ${computeStateName(this.stateObj)} camera.`}
/>`;
}
return html`${this._streamType === STREAM_TYPE_HLS ||
(!this._streamType &&
this._capabilities?.frontend_stream_types.includes(STREAM_TYPE_HLS))
? html`<ha-hls-player
autoplay
playsinline
.allowExoPlayer=${this.allowExoPlayer}
.muted=${this.muted}
.controls=${this.controls}
.hass=${this.hass}
.entityid=${this.stateObj.entity_id}
.posterUrl=${this._posterUrl}
@streams=${this._handleHlsStreams}
class=${!this._streamType && this._webRtcStreams ? "hidden" : ""}
></ha-hls-player>`
: nothing}
${this._streamType === STREAM_TYPE_WEB_RTC ||
(!this._streamType &&
this._capabilities?.frontend_stream_types.includes(STREAM_TYPE_WEB_RTC))
? html`<ha-web-rtc-player
autoplay
playsinline
.muted=${this.muted}
.controls=${this.controls}
.hass=${this.hass}
.entityid=${this.stateObj.entity_id}
.posterUrl=${this._posterUrl}
@streams=${this._handleWebRtcStreams}
class=${this._streamType !== STREAM_TYPE_WEB_RTC &&
!this._webRtcStreams
? "hidden"
: ""}
></ha-web-rtc-player>`
: nothing}`;
if (stream.type === STREAM_TYPE_HLS) {
return html`<ha-hls-player
autoplay
playsinline
.allowExoPlayer=${this.allowExoPlayer}
.muted=${this.muted}
.controls=${this.controls}
.hass=${this.hass}
.entityid=${this.stateObj.entity_id}
.posterUrl=${this._posterUrl}
@streams=${this._handleHlsStreams}
class=${stream.visible ? "" : "hidden"}
></ha-hls-player>`;
}
if (stream.type === STREAM_TYPE_WEB_RTC) {
return html`<ha-web-rtc-player
autoplay
playsinline
.muted=${this.muted}
.controls=${this.controls}
.hass=${this.hass}
.entityid=${this.stateObj.entity_id}
.posterUrl=${this._posterUrl}
@streams=${this._handleWebRtcStreams}
class=${stream.visible ? "" : "hidden"}
></ha-web-rtc-player>`;
}
return nothing;
}
private async _getCapabilities() {
@@ -130,35 +149,13 @@ export class HaCameraStream extends LitElement {
this._hlsStreams = undefined;
this._webRtcStreams = undefined;
if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) {
this._capabilities = { frontend_stream_types: [] };
return;
}
this._capabilities = await fetchCameraCapabilities(
this.hass!,
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> {
@@ -177,28 +174,87 @@ export class HaCameraStream extends LitElement {
private _handleHlsStreams(ev: CustomEvent) {
this._hlsStreams = ev.detail;
this._pickStreamType();
}
private _handleWebRtcStreams(ev: CustomEvent) {
this._webRtcStreams = ev.detail;
this._pickStreamType();
}
private _pickStreamType() {
if (!this._hlsStreams || !this._webRtcStreams) {
return;
private _streams = memoizeOne(
(
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 (
(supportedTypes[0] === STREAM_TYPE_HLS &&
hlsStreams?.hasVideo === false) ||
(supportedTypes[0] === STREAM_TYPE_WEB_RTC &&
webRtcStreams?.hasVideo === false)
) {
// stream failed to load, fallback to mjpeg
return [{ type: MJPEG_STREAM, visible: true }];
}
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 },
];
}
if (
this._hlsStreams.hasVideo &&
this._hlsStreams.hasAudio &&
!this._webRtcStreams.hasAudio
) {
this._streamType = STREAM_TYPE_HLS;
} else if (this._webRtcStreams.hasVideo) {
this._streamType = STREAM_TYPE_WEB_RTC;
}
}
);
static get styles(): CSSResultGroup {
return css`

View File

@@ -43,6 +43,7 @@ class HaDurationInput extends LitElement {
.label=${this.label}
.helper=${this.helper}
.required=${this.required}
.clearable=${!this.required && this.data !== undefined}
.autoValidate=${this.required}
.disabled=${this.disabled}
errorMessage="Required"
@@ -67,50 +68,79 @@ class HaDurationInput extends LitElement {
}
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() {
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() {
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() {
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() {
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();
const value = { ...ev.detail.value };
const value = ev.detail.value ? { ...ev.detail.value } : undefined;
if (!this.enableMillisecond && !value.milliseconds) {
// @ts-ignore
delete value.milliseconds;
} else if (value.milliseconds > 999) {
value.seconds += Math.floor(value.milliseconds / 1000);
value.milliseconds %= 1000;
}
if (value) {
value.hours ||= 0;
value.minutes ||= 0;
value.seconds ||= 0;
if (value.seconds > 59) {
value.minutes += Math.floor(value.seconds / 60);
value.seconds %= 60;
}
if ("days" in value) value.days ||= 0;
if ("milliseconds" in value) value.milliseconds ||= 0;
if (value.minutes > 59) {
value.hours += Math.floor(value.minutes / 60);
value.minutes %= 60;
}
if (!this.enableMillisecond && !value.milliseconds) {
// @ts-ignore
delete value.milliseconds;
} else if (value.milliseconds > 999) {
value.seconds += Math.floor(value.milliseconds / 1000);
value.milliseconds %= 1000;
}
if (this.enableDay && value.hours > 24) {
value.days = (value.days ?? 0) + Math.floor(value.hours / 24);
value.hours %= 24;
if (value.seconds > 59) {
value.minutes += Math.floor(value.seconds / 60);
value.seconds %= 60;
}
if (value.minutes > 59) {
value.hours += Math.floor(value.minutes / 60);
value.minutes %= 60;
}
if (this.enableDay && value.hours > 24) {
value.days = (value.days ?? 0) + Math.floor(value.hours / 24);
value.hours %= 24;
}
}
fireEvent(this, "value-changed", {

View File

@@ -1,6 +1,6 @@
import "@material/mwc-formfield";
import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type {
@@ -19,6 +19,8 @@ export class HaFormBoolean extends LitElement implements HaFormElement {
@property() public label!: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@query("ha-checkbox", true) private _input?: HTMLElement;
@@ -37,6 +39,12 @@ export class HaFormBoolean extends LitElement implements HaFormElement {
.disabled=${this.disabled}
@change=${this._valueChanged}
></ha-checkbox>
<span slot="label">
<p class="primary">${this.label}</p>
${this.helper
? html`<p class="secondary">${this.helper}</p>`
: nothing}
</span>
</mwc-formfield>
`;
}
@@ -46,6 +54,28 @@ export class HaFormBoolean extends LitElement implements HaFormElement {
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 {

View File

@@ -25,7 +25,6 @@ export class HaBadge extends LitElement {
static get styles(): CSSResultGroup {
return css`
:host {
color: var(--secondary-text-color);
}
[role="button"] {
cursor: pointer;
@@ -36,11 +35,10 @@ export class HaBadge extends LitElement {
white-space: nowrap;
align-items: center;
gap: 3px;
font-family: Roboto;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
color: var(--ha-heading-badge-text-color, var(--secondary-text-color));
font-size: var(--ha-heading-badge-font-size, 14px);
font-weight: var(--ha-heading-badge-font-weight, 400);
line-height: var(--ha-heading-badge-line-height, 20px);
letter-spacing: 0.1px;
--mdc-icon-size: 14px;
}

View File

@@ -8,7 +8,6 @@ import { customIcons } from "../data/custom_icons";
import type { Chunks, Icons } from "../data/iconsets";
import {
MDI_PREFIXES,
checkCacheVersion,
findIconChunk,
getIcon,
writeCache,
@@ -26,11 +25,6 @@ const mdiDeprecatedIcons: DeprecatedIcon = {};
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 cachedIcons: Record<string, string> = {};

View File

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

View File

@@ -15,6 +15,9 @@ export class HaMdListItem extends MdListItem {
--md-sys-color-on-surface: var(--primary-text-color);
--md-sys-color-on-surface-variant: var(--secondary-text-color);
}
md-item {
overflow: var(--md-item-overflow, hidden);
}
`,
];
}

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 { css, html, LitElement, nothing } from "lit";
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 { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
@@ -16,6 +21,15 @@ import "./ha-list-item";
import "./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")
class HaQrScanner extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -174,7 +188,7 @@ class HaQrScanner extends LitElement {
}
private _qrCodeError = (err: any) => {
if (err === "No QR code found") {
if (err.endsWith("No QR code found")) {
this._qrNotFoundCount++;
if (this._qrNotFoundCount === 250) {
this._reportError(err);

View File

@@ -24,7 +24,7 @@ export class HaToast extends Snackbar {
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) {
.mdc-snackbar__surface {
min-width: inherit;

View File

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

View File

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

View File

@@ -26,7 +26,10 @@ export class HaTraceLogbook extends LitElement {
.entries=${this.logbookEntries}
.narrow=${this.narrow}
></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">
No Logbook entries found for this step.

View File

@@ -291,7 +291,10 @@ export class HaTracePathDetails extends LitElement {
.entries=${entries}
.narrow=${this.narrow}
></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">
${this.hass!.localize(

View File

@@ -28,7 +28,10 @@ export class HaTraceTimeline extends LitElement {
allowPick
>
</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 type { HomeAssistant } from "../../types";
@customElement("hat-logbook-note")
class HatLogbookNote extends LitElement {
@property() public domain = "automation";
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public domain: "automation" | "script" = "automation";
render() {
return html`
Not all shown logbook entries might be related to this ${this.domain}.
`;
if (this.domain === "script") {
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`

View File

@@ -193,7 +193,7 @@ export const fetchHassioLogs = async (
) =>
hass.callApiRaw(
"GET",
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs/boots/${boot}`,
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs${boot !== 0 ? `/boots/${boot}` : ""}`,
undefined,
range
? {
@@ -211,7 +211,7 @@ export const fetchHassioLogsFollow = async (
) =>
hass.callApiRaw(
"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,
signal
@@ -229,7 +229,7 @@ export const getHassioLogDownloadLinesUrl = (
) =>
`/api/hassio/${
provider.includes("_") ? `addons/${provider}` : provider
}/logs/boots/${boot}?lines=${lines}`;
}/logs${boot !== 0 ? `/boots/${boot}` : ""}?lines=${lines}`;
export const setSupervisorOption = async (
hass: HomeAssistant,

View File

@@ -1,4 +1,5 @@
import { clear, get, set, createStore, promisifyRequest } from "idb-keyval";
import memoizeOne from "memoize-one";
import { promiseTimeout } from "../common/util/promise-timeout";
import { iconMetadata } from "../resources/icon-metadata";
import type { IconMeta } from "../types";
@@ -11,7 +12,23 @@ export interface Chunks {
[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"];
@@ -28,7 +45,10 @@ export const getIcon = (iconName: string) =>
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) => {
for (const [iconName_, resolve_, reject_] of toRead) {
promisifyRequest<string | undefined>(store.get(iconName_))
@@ -37,6 +57,7 @@ export const getIcon = (iconName: string) =>
}
toRead = [];
});
};
promiseTimeout(1000, readIcons()).catch((e) => {
// Firefox in private mode doesn't support IDB
@@ -62,6 +83,7 @@ export const findIconChunk = (icon: string): string => {
export const writeCache = async (chunks: Chunks) => {
const keys = Object.keys(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
iconStore("readwrite", (store) => {
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

@@ -28,7 +28,7 @@ export interface UrlActionConfig extends BaseActionConfig {
export interface MoreInfoActionConfig extends BaseActionConfig {
action: "more-info";
entity_id?: string;
entity?: string;
}
export interface AssistActionConfig extends BaseActionConfig {

View File

@@ -209,6 +209,17 @@ export interface ZWaveJSNodeStatus {
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 {
node_id: number;
exclusion: string;
@@ -404,6 +415,25 @@ export interface RequestedGrant {
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 = (
hass: HomeAssistant,
device_or_entry_id: {
@@ -579,6 +609,15 @@ export const fetchZwaveNodeStatus = (
device_id,
});
export const fetchZwaveNodeCapabilities = (
hass: HomeAssistant,
device_id: string
): Promise<ZWaveJSNodeCapabilities> =>
hass.callWS({
type: "zwave_js/node_capabilities",
device_id,
});
export const subscribeZwaveNodeStatus = (
hass: HomeAssistant,
device_id: string,

View File

@@ -1,10 +1,24 @@
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 { 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 "../../../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 { isTiltOnly } from "../../../data/cover";
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 "../../../panels/lovelace/components/hui-timestamp-display";
import type { HomeAssistant } from "../../../types";
@@ -28,18 +42,7 @@ class EntityPreviewRow extends LitElement {
<div class="name" .title=${computeStateName(stateObj)}>
${computeStateName(stateObj)}
</div>
<div class="value">
${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>`;
<div class="value">${this.renderEntityState(stateObj)}</div>`;
}
static get styles(): CSSResultGroup {
@@ -59,8 +62,308 @@ class EntityPreviewRow extends LitElement {
.value {
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 {

View File

@@ -52,59 +52,61 @@ class MoreInfoUpdate extends LitElement {
return html`
<div class="content">
${this.stateObj.attributes.in_progress
? supportsFeature(this.stateObj, UpdateEntityFeature.PROGRESS) &&
this.stateObj.attributes.update_percentage !== null
? html`<mwc-linear-progress
.progress=${this.stateObj.attributes.update_percentage / 100}
buffer=""
></mwc-linear-progress>`
: html`<mwc-linear-progress indeterminate></mwc-linear-progress>`
: nothing}
<h3>${this.stateObj.attributes.title}</h3>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<div class="row">
<div class="key">
${this.hass.formatEntityAttributeName(
this.stateObj,
"installed_version"
)}
<div class="summary">
${this.stateObj.attributes.in_progress
? supportsFeature(this.stateObj, UpdateEntityFeature.PROGRESS) &&
this.stateObj.attributes.update_percentage !== null
? html`<mwc-linear-progress
.progress=${this.stateObj.attributes.update_percentage / 100}
buffer=""
></mwc-linear-progress>`
: html`<mwc-linear-progress indeterminate></mwc-linear-progress>`
: nothing}
<h3>${this.stateObj.attributes.title}</h3>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<div class="row">
<div class="key">
${this.hass.formatEntityAttributeName(
this.stateObj,
"installed_version"
)}
</div>
<div class="value">
${this.stateObj.attributes.installed_version ??
this.hass.localize("state.default.unavailable")}
</div>
</div>
<div class="value">
${this.stateObj.attributes.installed_version ??
this.hass.localize("state.default.unavailable")}
<div class="row">
<div class="key">
${this.hass.formatEntityAttributeName(
this.stateObj,
"latest_version"
)}
</div>
<div class="value">
${this.stateObj.attributes.latest_version ??
this.hass.localize("state.default.unavailable")}
</div>
</div>
</div>
<div class="row">
<div class="key">
${this.hass.formatEntityAttributeName(
this.stateObj,
"latest_version"
)}
</div>
<div class="value">
${this.stateObj.attributes.latest_version ??
this.hass.localize("state.default.unavailable")}
</div>
</div>
${this.stateObj.attributes.release_url
? html`<div class="row">
<div class="key">
<a
href=${this.stateObj.attributes.release_url}
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.release_announcement"
)}
</a>
</div>
</div>`
: nothing}
${this.stateObj.attributes.release_url
? html`<div class="row">
<div class="key">
<a
href=${this.stateObj.attributes.release_url}
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.release_announcement"
)}
</a>
</div>
</div>`
: nothing}
</div>
${supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES) &&
!this._error
? this._releaseNotes === undefined
@@ -143,7 +145,7 @@ class MoreInfoUpdate extends LitElement {
)}
</span>
<ha-switch
id="create_backup"
id="create-backup"
checked
.disabled=${updateIsInstalling(this.stateObj)}
></ha-switch>
@@ -293,6 +295,11 @@ class MoreInfoUpdate extends LitElement {
ha-expansion-panel {
margin: 16px 0;
}
.summary {
margin-bottom: 16px;
}
.row {
margin: 0;
display: flex;
@@ -308,7 +315,9 @@ class MoreInfoUpdate extends LitElement {
);
position: sticky;
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;
display: flex;
flex-direction: column;

View File

@@ -66,6 +66,8 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
private _dialogClosed() {
this._params = undefined;
this._assistConfiguration = undefined;
this._previousSteps = [];
this._nextStep = undefined;
this._step = STEP.INIT;
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];
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>
<p class="secondary">
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() {
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>
<p class="secondary">
Some wake words are better for

View File

@@ -6,6 +6,7 @@ import "../../components/ha-circular-progress";
import { testAssistSatelliteConnection } from "../../data/assist_satellite";
import type { HomeAssistant } from "../../types";
import { AssistantSetupStyles } from "./styles";
import { documentationUrl } from "../../util/documentation-url";
@customElement("ha-voice-assistant-setup-step-check")
export class HaVoiceAssistantSetupStepCheck extends LitElement {
@@ -35,7 +36,7 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
protected override render() {
return html`<div class="content">
${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>
<p class="secondary">
To play audio, the voice assistant device has to connect to Home
@@ -44,12 +45,15 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
</p>
<div class="footer">
<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 @click=${this._testConnection}>Retry</ha-button>
</div>`
: html`<img src="/static/images/voice-assistant/hi.gif" />
: html`<img src="/static/images/voice-assistant/hi.png" />
<h1>Hi</h1>
<p class="secondary">
Over the next couple steps we're going to personalize your voice

View File

@@ -67,7 +67,7 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
: undefined;
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>
<p class="secondary">
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);
return html`<div class="content">
<img src="/static/images/voice-assistant/update.gif" />
<img src="/static/images/voice-assistant/update.png" />
<h1>
${stateObj &&
(stateObj.state === "unavailable" || updateIsInstalling(stateObj))

View File

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

View File

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

View File

@@ -371,11 +371,13 @@ export class HaTabsSubpageDataTable extends LitElement {
</div>
</ha-md-menu-item>
</ha-md-button-menu>
<p>
${localize("ui.components.subpage-data-table.selected", {
selected: this.selected || "0",
})}
</p>
${this.selected !== undefined
? html`<p>
${localize("ui.components.subpage-data-table.selected", {
selected: this.selected || "0",
})}
</p>`
: nothing}
</div>
<div class="center-vertical">
<slot name="selection-bar"></slot>

View File

@@ -16,6 +16,8 @@ import type { ValueChangedEvent } from "../types";
import { onBoardingStyles } from "./styles";
import { debounce } from "../common/util/debounce";
const CHECK_USERNAME_REGEX = /\s|[A-Z]/;
const CREATE_USER_SCHEMA: HaFormSchema[] = [
{
name: "name",
@@ -121,6 +123,7 @@ class OnboardingCreateUser extends LitElement {
ev: ValueChangedEvent<HaFormDataContainer>
): void {
const nameChanged = ev.detail.value.name !== this._newUser.name;
const usernameChanged = ev.detail.value.username !== this._newUser.username;
const passwordChanged =
ev.detail.value.password !== this._newUser.password ||
ev.detail.value.password_confirm !== this._newUser.password_confirm;
@@ -135,6 +138,9 @@ class OnboardingCreateUser extends LitElement {
this._debouncedCheckPasswordMatch();
}
}
if (usernameChanged) {
this._checkUsername();
}
}
private _debouncedCheckPasswordMatch = debounce(
@@ -164,6 +170,21 @@ class OnboardingCreateUser extends LitElement {
const parts = String(this._newUser.name).split(" ");
if (parts.length) {
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}
.data=${this._timeData}
enableMillisecond
required
@value-changed=${this._valueChanged}
></ha-duration-input>`;
}

View File

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

View File

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

View File

@@ -712,8 +712,12 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
private async _duplicate() {
const result = this._readOnly
? await showConfirmationDialog(this, {
title: "Migrate automation?",
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?",
title: this.hass.localize(
"ui.panel.config.automation.picker.migrate_automation"
),
text: this.hass.localize(
"ui.panel.config.automation.picker.migrate_automation_description"
),
})
: await this.confirmUnsavedChanged();
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",
type: "select",

View File

@@ -5,6 +5,7 @@ import {
mdiHospitalBox,
mdiInformation,
mdiUpload,
mdiWrench,
} from "@mdi/js";
import { getConfigEntries } from "../../../../../../data/config_entries";
import type { DeviceRegistryEntry } from "../../../../../../data/device_registry";
@@ -98,6 +99,13 @@ export const getZwaveDeviceActions = async (
showZWaveJSNodeStatisticsDialog(el, {
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) {
this.closeDialog();
const localize = await this.hass.loadBackendTranslation(
"title",
integration.name
);
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.single_config_entry_title"
@@ -591,7 +595,7 @@ class AddIntegrationDialog extends LitElement {
text: this.hass.localize(
"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
);
if (entries.length > 0) {
const localize = await this.hass.loadBackendTranslation(
"title",
this._manifest.name
);
await showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.single_config_entry_title"
@@ -1394,7 +1398,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
text: this.hass.localize(
"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) {
const configEntries = await getConfigEntries(this.hass, { domain });
if (configEntries.length > 0) {
const localize = await this.hass.loadBackendTranslation(
"title",
integration.name
);
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.single_config_entry_title"
@@ -751,7 +755,7 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) {
text: this.hass.localize(
"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>`,
}
)}
<br />
<span
class="link"
type="button"
@@ -57,13 +56,13 @@ class MatterAddDeviceGoogleHome extends LitElement {
)}
</span>
</li>
<li>
${this.hass.localize(
`ui.dialogs.matter-add-device.google_home.redirect`
)}
</li>
</ol>
<br />
<p>
${this.hass.localize(
`ui.dialogs.matter-add-device.google_home.redirect`
)}
</p>
</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
);
this._status = "provisioned";
if (this._params?.addedCallback) {
this._params.addedCallback();
}
} catch (err: any) {
this._error = err.message;
this._status = "failed";
@@ -831,9 +828,6 @@ class DialogZWaveJSAddNode extends LitElement {
if (message.event === "interview completed") {
this._unsubscribe();
this._status = "finished";
if (this._params?.addedCallback) {
this._params.addedCallback();
}
}
if (message.event === "interview stage completed") {
@@ -874,6 +868,9 @@ class DialogZWaveJSAddNode extends LitElement {
}
if (this._entryId) {
stopZwaveInclusion(this.hass, this._entryId);
if (this._params?.onStop) {
this._params.onStop();
}
}
this._requestedGrant = undefined;
this._dsk = undefined;

View File

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

View File

@@ -564,7 +564,8 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
private async _addNodeClicked() {
showZWaveJSAddNodeDialog(this, {
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",
load: () => import("./zwave_js-node-config"),
},
node_installer: {
tag: "zwave_js-node-installer",
load: () => import("./zwave_js-node-installer"),
},
logs: {
tag: "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,21 +2,22 @@ import { mdiClose } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
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-dialog-header";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../components/ha-md-dialog";
import type { HomeAssistant } from "../../../types";
import { haStyle, haStyleDialog } from "../../../resources/styles";
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 } from "../../../data/hassio/supervisor";
import "../../../components/ha-md-select";
import "../../../components/ha-md-select-option";
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 type { DownloadLogsDialogParams } from "./show-dialog-download-logs";
const DEFAULT_LINE_COUNT = 500;
@customElement("dialog-download-logs")
class DownloadLogsDialog extends LitElement {
@@ -24,13 +25,13 @@ class DownloadLogsDialog extends LitElement {
@state() private _dialogParams?: DownloadLogsDialogParams;
@state() private _lineCount = 100;
@state() private _lineCount = DEFAULT_LINE_COUNT;
@query("ha-md-dialog") private _dialogElement!: HaMdDialog;
public showDialog(dialogParams: DownloadLogsDialogParams) {
this._dialogParams = dialogParams;
this._lineCount = this._dialogParams?.defaultLineCount ?? 100;
this._lineCount = this._dialogParams?.defaultLineCount || 500;
}
public closeDialog() {
@@ -39,7 +40,7 @@ class DownloadLogsDialog extends LitElement {
private _dialogClosed() {
this._dialogParams = undefined;
this._lineCount = 100;
this._lineCount = DEFAULT_LINE_COUNT;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -49,7 +50,7 @@ class DownloadLogsDialog extends LitElement {
}
const numberOfLinesOptions = [100, 500, 1000, 5000, 10000];
if (!numberOfLinesOptions.includes(this._lineCount)) {
if (!numberOfLinesOptions.includes(this._lineCount) && this._lineCount) {
numberOfLinesOptions.push(this._lineCount);
numberOfLinesOptions.sort((a, b) => a - b);
}
@@ -64,7 +65,7 @@ class DownloadLogsDialog extends LitElement {
.path=${mdiClose}
></ha-icon-button>
<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 slot="subtitle">
${this._dialogParams.header}${this._dialogParams.boot === 0
@@ -78,28 +79,25 @@ class DownloadLogsDialog extends LitElement {
"ui.panel.config.logs.select_number_of_lines"
)}:
</div>
<ha-select
<ha-md-select
.label=${this.hass.localize("ui.panel.config.logs.lines")}
@selected=${this._setNumberOfLogs}
fixedMenuPosition
naturalMenuWidth
@closed=${stopPropagation}
@change=${this._setNumberOfLogs}
.value=${String(this._lineCount)}
>
${numberOfLinesOptions.map(
(option) => html`
<ha-list-item .value=${String(option)}>
<ha-md-select-option .value=${String(option)}>
${option}
</ha-list-item>
</ha-md-select-option>
`
)}
</ha-select>
</ha-md-select>
</div>
<div slot="actions">
<ha-button @click=${this.closeDialog}>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button @click=${this._dowloadLogs}>
<ha-button @click=${this._downloadLogs}>
${this.hass.localize("ui.common.download")}
</ha-button>
</div>
@@ -107,7 +105,7 @@ class DownloadLogsDialog extends LitElement {
`;
}
private async _dowloadLogs() {
private async _downloadLogs() {
const provider = this._dialogParams!.provider;
const boot = this._dialogParams!.boot;
@@ -137,6 +135,7 @@ class DownloadLogsDialog extends LitElement {
css`
:host {
direction: var(--direction);
--dialog-content-overflow: visible;
}
.content {
display: flex;

View File

@@ -1,9 +1,16 @@
import "@material/mwc-list/mwc-list-item";
import type { ActionDetail } from "@material/mwc-list";
import {
mdiArrowCollapseDown,
mdiDotsVertical,
mdiCircle,
mdiDownload,
mdiFormatListNumbered,
mdiMenuDown,
mdiRefresh,
mdiWrap,
mdiWrapDisabled,
} from "@mdi/js";
import {
css,
@@ -31,6 +38,8 @@ import "../../../components/chips/ha-assist-chip";
import "../../../components/ha-menu";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-md-divider";
import "../../../components/ha-button-menu";
import "../../../components/ha-list-item";
import { getSignedPath } from "../../../data/auth";
@@ -40,10 +49,14 @@ import {
fetchHassioBoots,
fetchHassioLogs,
fetchHassioLogsFollow,
getHassioLogDownloadLinesUrl,
getHassioLogDownloadUrl,
} from "../../../data/hassio/supervisor";
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 { ConnectionStatus } from "../../../data/connection-status";
import { atLeastVersion } from "../../../common/config/version";
@@ -51,6 +64,7 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { debounce } from "../../../common/util/debounce";
import { showDownloadLogsDialog } from "./show-dialog-download-logs";
import type { HaMenu } from "../../../components/ha-menu";
import type { LocalizeFunc } from "../../../common/translations/localize";
const NUMBER_OF_LINES = 100;
@@ -58,6 +72,8 @@ const NUMBER_OF_LINES = 100;
class ErrorLogCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public localizeFunc?: LocalizeFunc<any>;
@property() public filter = "";
@property() public header?: string;
@@ -109,30 +125,42 @@ class ErrorLogCard extends LitElement {
@state() private _boots?: number[];
@state() private _showBootsSelect = false;
@state() private _wrapLines = true;
@state() private _downloadSupported;
@state() private _logsFileLink;
protected render(): TemplateResult {
const localize = this.localizeFunc || this.hass.localize;
return html`
<div class="error-log-intro">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
: nothing}
<ha-card outlined class=${classMap({ hidden: this.show === false })}>
<div class="header">
<h1 class="card-header">
${this.header ||
this.hass.localize("ui.panel.config.logs.show_full_logs")}
${this.header || localize("ui.panel.config.logs.show_full_logs")}
</h1>
<div class="action-buttons">
${this._streamSupported && Array.isArray(this._boots)
${this._streamSupported &&
Array.isArray(this._boots) &&
this._showBootsSelect
? html`
<ha-assist-chip
.title=${localize(
"ui.panel.config.logs.haos_boots_title"
)}
.label=${this._boot === 0
? this.hass.localize("ui.panel.config.logs.current")
? localize("ui.panel.config.logs.current")
: this._boot === -1
? this.hass.localize("ui.panel.config.logs.previous")
: this.hass.localize(
"ui.panel.config.logs.startups_ago",
{ boot: this._boot * -1 }
)}
? localize("ui.panel.config.logs.previous")
: localize("ui.panel.config.logs.startups_ago", {
boot: this._boot * -1,
})}
id="boots-anchor"
@click=${this._toggleBootsMenu}
>
@@ -154,14 +182,10 @@ class ErrorLogCard extends LitElement {
.selected=${boot === this._boot}
>
${boot === 0
? this.hass.localize(
"ui.panel.config.logs.current"
)
? localize("ui.panel.config.logs.current")
: boot === -1
? this.hass.localize(
"ui.panel.config.logs.previous"
)
: this.hass.localize(
? localize("ui.panel.config.logs.previous")
: localize(
"ui.panel.config.logs.startups_ago",
{ boot: boot * -1 }
)}
@@ -176,20 +200,61 @@ class ErrorLogCard extends LitElement {
</ha-menu>
`
: nothing}
${this._downloadSupported
? html`
<ha-icon-button
.path=${mdiDownload}
@click=${this._downloadLogs}
.label=${localize("ui.panel.config.logs.download_logs")}
></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=${mdiDownload}
@click=${this._downloadFullLog}
.label=${this.hass.localize(
"ui.panel.config.logs.download_full_log"
.path=${this._wrapLines ? mdiWrapDisabled : mdiWrap}
@click=${this._toggleLineWrap}
.label=${localize(
`ui.panel.config.logs.${this._wrapLines ? "full_width" : "wrap_lines"}`
)}
></ha-icon-button>
${!this._streamSupported || this._error
? html`<ha-icon-button
.path=${mdiRefresh}
@click=${this._loadLogs}
.label=${this.hass.localize("ui.common.refresh")}
.label=${localize("ui.common.refresh")}
></ha-icon-button>`
: 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 class="card-content error-log">
@@ -202,25 +267,22 @@ class ErrorLogCard extends LitElement {
</div>`
: nothing}
${this._loadingState === "loading"
? html`<div>
${this.hass.localize("ui.panel.config.logs.loading_log")}
</div>`
? html`<div>${localize("ui.panel.config.logs.loading_log")}</div>`
: this._loadingState === "empty"
? html`<div>
${this.hass.localize("ui.panel.config.logs.no_errors")}
</div>`
? html`<div>${localize("ui.panel.config.logs.no_errors")}</div>`
: nothing}
${this._loadingState === "loaded" &&
this.filter &&
this._noSearchResults
? html`<div>
${this.hass.localize(
"ui.panel.config.logs.no_issues_search",
{ term: this.filter }
)}
${localize("ui.panel.config.logs.no_issues_search", {
term: this.filter,
})}
</div>`
: 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>
<ha-button
@@ -236,24 +298,36 @@ class ErrorLogCard extends LitElement {
.path=${mdiArrowCollapseDown}
slot="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
.path=${mdiArrowCollapseDown}
slot="trailingIcon"
></ha-svg-icon>
</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>
${this.show === false
? html`
<ha-button outlined @click=${this._downloadFullLog}>
<ha-svg-icon .path=${mdiDownload}></ha-svg-icon>
${this.hass.localize("ui.panel.config.logs.download_full_log")}
</ha-button>
${this._downloadSupported
? html`
<ha-button outlined @click=${this._downloadLogs}>
<ha-svg-icon .path=${mdiDownload}></ha-svg-icon>
${localize("ui.panel.config.logs.download_logs")}
</ha-button>
`
: nothing}
<mwc-button raised @click=${this._showLogs}>
${this.hass.localize("ui.panel.config.logs.load_logs")}
${localize("ui.panel.config.logs.load_logs")}
</mwc-button>
`
: ""}
: nothing}
</div>
`;
}
@@ -268,6 +342,9 @@ class ErrorLogCard extends LitElement {
11
);
}
if (this._downloadSupported === undefined && this.hass) {
this._downloadSupported = downloadFileSupported(this.hass);
}
}
protected firstUpdated(changedProps: PropertyValues) {
@@ -331,7 +408,7 @@ class ErrorLogCard extends LitElement {
);
}
private async _downloadFullLog(): Promise<void> {
private async _downloadLogs(): Promise<void> {
if (this._streamSupported) {
showDownloadLogsDialog(this, {
header: this.header,
@@ -378,6 +455,18 @@ class ErrorLogCard extends LitElement {
isComponentLoaded(this.hass, "hassio") &&
this.provider
) {
// 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.provider,
@@ -438,6 +527,17 @@ class ErrorLogCard extends LitElement {
} else {
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 {
@@ -461,10 +561,13 @@ class ErrorLogCard extends LitElement {
if (err.name === "AbortError") {
return;
}
this._error = this.hass.localize("ui.panel.config.logs.failed_get_logs", {
provider: this.provider,
error: extractApiErrorMessage(err),
});
this._error = (this.localizeFunc || this.hass.localize)(
"ui.panel.config.logs.failed_get_logs",
{
provider: this.provider,
error: extractApiErrorMessage(err),
}
);
}
}
@@ -570,9 +673,14 @@ class ErrorLogCard extends LitElement {
if (this._streamSupported && isComponentLoaded(this.hass, "hassio")) {
try {
const { data } = await fetchHassioBoots(this.hass);
this._boots = Object.keys(data.boots)
const boots = Object.keys(data.boots)
.map(Number)
.sort((a, b) => b - a);
// only show boots select when there are more than one boot
if (boots.length > 1) {
this._boots = boots;
}
} catch (err: any) {
// eslint-disable-next-line no-console
console.error(err);
@@ -580,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() {
if (this._bootsMenu) {
this._bootsMenu.open = !this._bootsMenu.open;
@@ -592,6 +712,9 @@ class ErrorLogCard extends LitElement {
}
static styles: CSSResultGroup = css`
:host {
direction: var(--direction);
}
.error-log-intro {
text-align: center;
margin: 16px;
@@ -641,7 +764,7 @@ class ErrorLogCard extends LitElement {
position: relative;
font-family: var(--code-font-family, monospace);
clear: both;
text-align: left;
text-align: start;
padding-top: 12px;
padding-bottom: 12px;
overflow-y: scroll;
@@ -708,6 +831,36 @@ class ErrorLogCard extends LitElement {
--ha-assist-chip-container-shape: 10px;
--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() {
const result = this._readOnly
? await showConfirmationDialog(this, {
title: "Migrate script?",
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?",
title: this.hass.localize(
"ui.panel.config.script.picker.migrate_script"
),
text: this.hass.localize(
"ui.panel.config.script.picker.migrate_script_description"
),
})
: await this.confirmUnsavedChanged();
if (result) {

View File

@@ -1,11 +1,11 @@
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../components/ha-card";
import type { HomeAssistant } from "../../../../types";
import { hasConfigChanged } from "../../common/has-changed";
import "../../components/hui-energy-period-selector";
import "../../../../components/ha-card";
import type { LovelaceCard, LovelaceLayoutOptions } from "../../types";
import type { LovelaceCard, LovelaceGridOptions } from "../../types";
import type { EnergyCardBaseConfig } from "../types";
@customElement("hui-energy-date-selection-card")
@@ -21,10 +21,10 @@ export class HuiEnergyDateSelectionCard
return 1;
}
public getLayoutOptions(): LovelaceLayoutOptions {
public getGridOptions(): LovelaceGridOptions {
return {
grid_rows: 1,
grid_columns: 4,
rows: 1,
columns: 12,
};
}
@@ -59,18 +59,12 @@ export class HuiEnergyDateSelectionCard
static get styles(): CSSResultGroup {
return css`
:host {
ha-card {
height: 100%;
display: flex;
flex-direction: column;
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 {
LovelaceCard,
LovelaceCardEditor,
LovelaceLayoutOptions,
LovelaceGridOptions,
} from "../types";
import type { AreaCardConfig } from "./types";
@@ -534,10 +534,11 @@ export class HuiAreaCard
forwardHaptic("light");
}
getLayoutOptions(): LovelaceLayoutOptions {
getGridOptions(): LovelaceGridOptions {
return {
grid_columns: 4,
grid_rows: 3,
columns: 12,
rows: 3,
min_columns: 3,
};
}

View File

@@ -46,7 +46,7 @@ import { createEntityNotFoundWarning } from "../components/hui-warning";
import type {
LovelaceCard,
LovelaceCardEditor,
LovelaceLayoutOptions,
LovelaceGridOptions,
} 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 (
this._config?.show_icon &&
(this._config?.show_name || this._config?.show_state)
) {
return {
grid_rows: 2,
grid_columns: 2,
grid_min_rows: 2,
rows: 2,
columns: 6,
min_columns: 2,
min_rows: 2,
};
}
return {
grid_rows: 1,
grid_columns: 1,
rows: 1,
columns: 3,
min_columns: 2,
min_rows: 1,
};
}

View File

@@ -86,10 +86,11 @@ export class HuiCard extends ReactiveElement {
return this._element.getGridOptions();
}
if (this._element.getLayoutOptions) {
// Disabled for now to avoid spamming the console, need to be re-enabled when hui-card performance are fixed
// eslint-disable-next-line no-console
console.warn(
`This card (${this.config?.type}) is using "getLayoutOptions" and it is deprecated, contact the developer to suggest to use "getGridOptions" instead`
);
// console.warn(
// `This card (${this.config?.type}) is using "getLayoutOptions" and it is deprecated, contact the developer to suggest to use "getGridOptions" instead`
// );
const options = migrateLayoutToGridOptions(
this._element.getLayoutOptions()
);

View File

@@ -33,8 +33,8 @@ import { createEntityNotFoundWarning } from "../components/hui-warning";
import { createHeaderFooterElement } from "../create-element/create-header-footer-element";
import type {
LovelaceCard,
LovelaceGridOptions,
LovelaceHeaderFooter,
LovelaceLayoutOptions,
} from "../types";
import type { HuiErrorCard } from "./hui-error-card";
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 });
}
public getLayoutOptions(): LovelaceLayoutOptions {
public getGridOptions(): LovelaceGridOptions {
return {
grid_columns: 2,
grid_rows: 2,
grid_min_columns: 2,
grid_min_rows: 2,
columns: 6,
rows: 2,
min_columns: 6,
min_rows: 2,
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ import type {
LovelaceCard,
LovelaceCardEditor,
LovelaceHeaderFooter,
LovelaceLayoutOptions,
LovelaceGridOptions,
} from "../types";
import type { HuiErrorCard } from "./hui-error-card";
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 });
}
public getLayoutOptions(): LovelaceLayoutOptions {
public getGridOptions(): LovelaceGridOptions {
return {
grid_columns: 2,
grid_rows: 2,
grid_min_columns: 2,
grid_min_rows: 2,
columns: 6,
rows: 2,
min_columns: 6,
min_rows: 2,
};
}

View File

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

View File

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

View File

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

View File

@@ -95,7 +95,7 @@ export const handleAction = async (
switch (actionConfig.action) {
case "more-info": {
const entityId =
actionConfig.entity_id ||
actionConfig.entity ||
config.entity ||
config.camera_image ||
config.image_entity;

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