Compare commits

...

10 Commits

Author SHA1 Message Date
renovate[bot]
9a4469588c
Update dependency typescript-eslint to v8.30.0 (#25099)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-17 17:57:40 +00:00
renovate[bot]
f9eadf08fd
Update vaadinWebComponents monorepo to v24.7.3 (#25087)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-17 19:47:50 +02:00
dependabot[bot]
c630176fcf
Bump http-proxy-middleware from 2.0.7 to 2.0.9 (#25092)
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.7 to 2.0.9.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.9/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.7...v2.0.9)

---
updated-dependencies:
- dependency-name: http-proxy-middleware
  dependency-version: 2.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-17 19:46:20 +02:00
Paul Bottein
0389fbba52
Use ha-combox-box-list-item in all combo box components (#25096) 2025-04-17 17:13:19 +02:00
Paul Bottein
d56c7c41e2
Update entity naming in entity picker (#24971) 2025-04-17 16:43:47 +02:00
Paul Bottein
e74cac697e
Add fit mode support to picture glance card and picture entity card (#25005)
Co-authored-by: karwosts <karwosts@gmail.com>
2025-04-17 13:36:34 +00:00
renovate[bot]
77216e8e76
Update Yarn to v4.9.1 (#25089)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-17 15:28:28 +02:00
Bram Kragten
02a8924f63
Fix max height of video in more info (#25091) 2025-04-17 15:27:57 +02:00
Petar Petrov
9fc28e5abb
ZwaveJS controller migration flow (#25003)
* ZwaveJS migration flow

* Show exact progress in options flow

* progress fix

* Apply suggestions from code review

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* remove unused string

* import fix

* fix selectedDomain

* entryId -> handler

* Update src/translations/en.json

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-04-17 13:32:33 +02:00
Wendelin
933fb1327a
Implement new Z-Wave add device flow (#24667)
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-04-17 13:12:04 +02:00
58 changed files with 3622 additions and 1807 deletions

File diff suppressed because one or more lines are too long

View File

@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.9.0.cjs
yarnPath: .yarn/releases/yarn-4.9.1.cjs

View File

@ -89,8 +89,8 @@
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.8.1",
"@tsparticles/preset-links": "3.2.0",
"@vaadin/combo-box": "24.7.2",
"@vaadin/vaadin-themable-mixin": "24.7.2",
"@vaadin/combo-box": "24.7.3",
"@vaadin/vaadin-themable-mixin": "24.7.3",
"@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
@ -221,7 +221,7 @@
"terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2",
"typescript": "5.8.3",
"typescript-eslint": "8.29.1",
"typescript-eslint": "8.30.0",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.1.1",
"webpack-stats-plugin": "1.1.3",
@ -238,5 +238,5 @@
"globals": "16.0.0",
"tslib": "2.8.1"
},
"packageManager": "yarn@4.9.0"
"packageManager": "yarn@4.9.1"
}

View File

@ -0,0 +1,15 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M76.9105 39.4999C77.739 39.4999 78.4105 38.8283 78.4105 37.9999C78.4105 37.1715 77.739 36.4999 76.9105 36.4999V39.4999ZM37.5 39.4999L76.9105 39.4999V36.4999L37.5 36.4999L37.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M30.8239 22.3365L38.8239 38.8365L30.3239 50.3365" stroke="black" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/>
<mask id="mask0_1110_23734" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
<path d="M45.75 42.075C45.75 42.4462 45.4462 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/>
</mask>
<g mask="url(#mask0_1110_23734)">
<rect x="30" y="27" width="18" height="18" fill="#212121"/>
</g>
<path d="M82 37.9999C82 36.343 83.3431 34.9999 85 34.9999C86.6569 34.9999 88 36.343 88 37.9999C88 39.6567 86.6569 40.9999 85 40.9999C83.3431 40.9999 82 39.6567 82 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
<rect x="23" y="11" width="8" height="8" rx="4" fill="black" fill-opacity="0.32"/>
<rect x="22" y="52" width="8" height="8" rx="4" fill="black" fill-opacity="0.32"/>
<path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="black" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/>
<circle cx="39" cy="36" r="33.5" stroke="black" stroke-opacity="0.12"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,15 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M30.824 22.3365L38.824 38.8365L30.324 50.3365" stroke="white" stroke-opacity="0.24" stroke-width="3" stroke-linecap="round"/>
<mask id="mask0_1180_4955" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
<path d="M45.75 42.075C45.75 42.4462 45.4462 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/>
</mask>
<g mask="url(#mask0_1180_4955)">
<rect x="30" y="27" width="18" height="18" fill="#00AFFF"/>
</g>
<path d="M76.9105 39.4999C77.739 39.4999 78.4105 38.8283 78.4105 37.9999C78.4105 37.1715 77.739 36.4999 76.9105 36.4999V39.4999ZM37.5 39.4999L76.9105 39.4999V36.4999L37.5 36.4999L37.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M82 37.9999C82 36.343 83.3431 34.9999 85 34.9999C86.6569 34.9999 88 36.343 88 37.9999C88 39.6567 86.6569 40.9999 85 40.9999C83.3431 40.9999 82 39.6567 82 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
<rect x="23" y="11" width="8" height="8" rx="4" fill="white" fill-opacity="0.48"/>
<rect x="22" y="52" width="8" height="8" rx="4" fill="white" fill-opacity="0.48"/>
<path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="white" stroke-opacity="0.24" stroke-width="3" stroke-linecap="round"/>
<circle cx="39" cy="36" r="33.5" stroke="white" stroke-opacity="0.24"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,19 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M63.1358 38.5084C63.9608 38.4334 64.5688 37.7037 64.4938 36.8787C64.4188 36.0537 63.6892 35.4457 62.8642 35.5207L63.1358 38.5084ZM46.6358 40.0084L63.1358 38.5084L62.8642 35.5207L46.3642 37.0207L46.6358 40.0084Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<circle cx="47" cy="36" r="34" fill="white"/>
<circle cx="47" cy="36" r="33.5" stroke="black" stroke-opacity="0.12"/>
<path d="M41.8777 12.5216C43.4905 12.1798 45.1631 12 46.8777 12C58.2401 12 67.7582 19.8959 70.2445 30.5M40 59C42.1788 59.6506 44.4874 60 46.8777 60C56.9498 60 65.5728 53.7955 69.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<mask id="mask0_1110_23775" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="38" y="27" width="18" height="18">
<path d="M53.75 42.075C53.75 42.4462 53.4462 42.75 53.075 42.75H40.925C40.5538 42.75 40.25 42.4462 40.25 42.075V36.675C40.25 36.3037 40.4649 35.7851 40.7276 35.5224L46.5224 29.7275C46.7851 29.4649 47.2143 29.4649 47.477 29.7275L53.2724 35.523C53.5351 35.7857 53.75 36.3043 53.75 36.6755V42.075Z" fill="black"/>
</mask>
<g mask="url(#mask0_1110_23775)">
<rect x="38" y="27" width="18" height="18" fill="#212121"/>
</g>
<path d="M63.5 39.4999C64.3284 39.4999 65 38.8283 65 37.9999C65 37.1715 64.3284 36.4999 63.5 36.4999L63.5 39.4999ZM49.5 39.4999L63.5 39.4999L63.5 36.4999L49.5 36.4999L49.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
<rect x="31" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<rect x="30" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<path d="M29.5715 19.5C25.4983 23.801 23 29.6087 23 36C23 41.9085 25.1351 47.3183 28.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M68 37.9999C68 36.343 69.3431 34.9999 71 34.9999C72.6569 34.9999 74 36.343 74 37.9999C74 39.6567 72.6569 40.9999 71 40.9999C69.3431 40.9999 68 39.6567 68 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,19 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M63.1358 38.5084C63.9608 38.4334 64.5688 37.7037 64.4938 36.8787C64.4188 36.0537 63.6892 35.4457 62.8642 35.5207L63.1358 38.5084ZM46.6358 40.0084L63.1358 38.5084L62.8642 35.5207L46.3642 37.0207L46.6358 40.0084Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<circle cx="47" cy="36" r="34" fill="#1C1C1C"/>
<circle cx="47" cy="36" r="33.5" stroke="white" stroke-opacity="0.24"/>
<path d="M41.8777 12.5216C43.4905 12.1798 45.1631 12 46.8777 12C58.2401 12 67.7582 19.8959 70.2445 30.5M40 59C42.1788 59.6506 44.4874 60 46.8777 60C56.9498 60 65.5728 53.7955 69.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<mask id="mask0_1180_4965" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="38" y="27" width="18" height="18">
<path d="M53.75 42.075C53.75 42.4462 53.4462 42.75 53.075 42.75H40.925C40.5538 42.75 40.25 42.4462 40.25 42.075V36.675C40.25 36.3037 40.4649 35.7851 40.7276 35.5224L46.5224 29.7275C46.7851 29.4649 47.2143 29.4649 47.477 29.7275L53.2724 35.523C53.5351 35.7857 53.75 36.3043 53.75 36.6755V42.075Z" fill="black"/>
</mask>
<g mask="url(#mask0_1180_4965)">
<rect x="38" y="27" width="18" height="18" fill="#00AFFF"/>
</g>
<path d="M63.5 39.4999C64.3284 39.4999 65 38.8283 65 37.9999C65 37.1715 64.3284 36.4999 63.5 36.4999L63.5 39.4999ZM49.5 39.4999L63.5 39.4999L63.5 36.4999L49.5 36.4999L49.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
<rect x="31" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<rect x="30" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<path d="M29.5715 19.5C25.4983 23.801 23 29.6087 23 36C23 41.9085 25.1351 47.3183 28.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M68 37.9999C68 36.343 69.3431 34.9999 71 34.9999C72.6569 34.9999 74 36.343 74 37.9999C74 39.6567 72.6569 40.9999 71 40.9999C69.3431 40.9999 68 39.6567 68 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -5,7 +5,7 @@ import { getIntegrationDescriptions } from "../../data/integrations";
import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow";
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
import { showMatterAddDeviceDialog } from "../../panels/config/integrations/integration-panels/matter/show-dialog-add-matter-device";
import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node";
import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/add-node/show-dialog-zwave_js-add-node";
import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import { isComponentLoaded } from "../config/is_component_loaded";

View File

@ -1,7 +1,7 @@
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, html } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
@ -19,7 +19,7 @@ import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-list-item";
import "../ha-combo-box-item";
interface Device {
name: string;
@ -35,11 +35,14 @@ export type HaDevicePickerDeviceFilterFunc = (
export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean;
const rowRenderer: ComboBoxLitRenderer<Device> = (item) =>
html`<ha-list-item .twoline=${!!item.area}>
<span>${item.name}</span>
<span slot="secondary">${item.area}</span>
</ha-list-item>`;
const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`
<ha-combo-box-item type="button">
<span slot="headline">${item.name}</span>
${item.area
? html`<span slot="supporting-text">${item.area}</span>`
: nothing}
</ha-combo-box-item>
`;
@customElement("ha-device-picker")
export class HaDevicePicker extends LitElement {

View File

@ -1,35 +1,78 @@
import "../ha-list-item";
import { mdiMagnify, mdiPlus } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceName } from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeEntityName } from "../../common/entity/compute_entity_name";
import { computeStateName } from "../../common/entity/compute_state_name";
import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
import { fuzzyFilterSort } from "../../common/string/filter/sequence-matching";
import type { ValueChangedEvent, HomeAssistant } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-icon-button";
import "../ha-svg-icon";
import "./state-badge";
import { getEntityContext } from "../../common/entity/get_entity_context";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail";
import { computeRTL } from "../../common/util/compute_rtl";
import { domainToName } from "../../data/integration";
import type { HelperDomain } from "../../panels/config/helpers/const";
import { isHelperDomain } from "../../panels/config/helpers/const";
import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-combo-box-item";
import "../ha-icon-button";
import "../ha-list-item";
import "../ha-svg-icon";
import "./state-badge";
interface HassEntityWithCachedName extends HassEntity, ScorableTextItem {
friendly_name: string;
const FAKE_ENTITY: HassEntity = {
entity_id: "",
state: "",
last_changed: "",
last_updated: "",
context: { id: "", user_id: null, parent_id: null },
attributes: {},
};
interface EntityPickerItem extends HassEntity {
label: string;
primary: string;
secondary?: string;
translated_domain?: string;
show_entity_id?: boolean;
entity_name?: string;
area_name?: string;
device_name?: string;
friendly_name?: string;
sorting_label?: string;
icon_path?: string;
}
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
const CREATE_ID = "___create-new-entity___";
const DOMAIN_STYLE = styleMap({
fontSize: "12px",
fontWeight: "400",
lineHeight: "18px",
alignSelf: "flex-end",
maxWidth: "30%",
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
});
const ENTITY_ID_STYLE = styleMap({
fontFamily: "var(--code-font-family, monospace)",
fontSize: "11px",
});
@customElement("ha-entity-picker")
export class HaEntityPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -106,8 +149,7 @@ export class HaEntityPicker extends LitElement {
@property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false;
@property({ attribute: "item-label-path" }) public itemLabelPath =
"friendly_name";
@property({ attribute: "item-label-path" }) public itemLabelPath = "label";
@state() private _opened = false;
@ -123,30 +165,48 @@ export class HaEntityPicker extends LitElement {
await this.comboBox?.focus();
}
private _initedStates = false;
private _initialItems = false;
private _states: HassEntityWithCachedName[] = [];
private _items: EntityPickerItem[] = [];
private _rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (
item
) =>
html`<ha-list-item graphic="avatar" .twoline=${!!item.entity_id}>
${item.state
? html`<state-badge
slot="graphic"
.stateObj=${item}
.hass=${this.hass}
></state-badge>`
: ""}
<span>${item.friendly_name}</span>
<span slot="secondary"
>${item.entity_id.startsWith(CREATE_ID)
? this.hass.localize("ui.components.entity.entity-picker.new_entity")
: item.entity_id}</span
>
</ha-list-item>`;
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.hass.loadBackendTranslation("title");
}
private _getStates = memoizeOne(
private _rowRenderer: ComboBoxLitRenderer<EntityPickerItem> = (
item,
{ index }
) => html`
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
${item.icon_path
? html`<ha-svg-icon slot="start" .path=${item.icon_path}></ha-svg-icon>`
: html`
<state-badge
slot="start"
.stateObj=${item}
.hass=${this.hass}
></state-badge>
`}
<span slot="headline">${item.primary} </span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${item.entity_id && item.show_entity_id
? html`<span slot="supporting-text" style=${ENTITY_ID_STYLE}
>${item.entity_id}</span
>`
: nothing}
${item.translated_domain && !item.show_entity_id
? html`<div slot="trailing-supporting-text" style=${DOMAIN_STYLE}>
${item.translated_domain}
</div>`
: nothing}
</ha-combo-box-item>
`;
private _getItems = memoizeOne(
(
_opened: boolean,
hass: this["hass"],
@ -158,8 +218,8 @@ export class HaEntityPicker extends LitElement {
includeEntities: this["includeEntities"],
excludeEntities: this["excludeEntities"],
createDomains: this["createDomains"]
): HassEntityWithCachedName[] => {
let states: HassEntityWithCachedName[] = [];
): EntityPickerItem[] => {
let states: EntityPickerItem[] = [];
if (!hass) {
return [];
@ -168,7 +228,7 @@ export class HaEntityPicker extends LitElement {
const createItems = createDomains?.length
? createDomains.map((domain) => {
const newFriendlyName = hass.localize(
const primary = hass.localize(
"ui.components.entity.entity-picker.create_helper",
{
domain: isHelperDomain(domain)
@ -180,16 +240,14 @@ export class HaEntityPicker extends LitElement {
);
return {
...FAKE_ENTITY,
entity_id: CREATE_ID + domain,
state: "on",
last_changed: "",
last_updated: "",
context: { id: "", user_id: null, parent_id: null },
friendly_name: newFriendlyName,
attributes: {
icon: "mdi:plus",
},
strings: [domain, newFriendlyName],
primary: primary,
label: primary,
secondary: this.hass.localize(
"ui.components.entity.entity-picker.new_entity"
),
icon_path: mdiPlus,
};
})
: [];
@ -197,21 +255,14 @@ export class HaEntityPicker extends LitElement {
if (!entityIds.length) {
return [
{
entity_id: "",
state: "",
last_changed: "",
last_updated: "",
context: { id: "", user_id: null, parent_id: null },
friendly_name: this.hass!.localize(
...FAKE_ENTITY,
primary: this.hass!.localize(
"ui.components.entity.entity-picker.no_entities"
),
attributes: {
friendly_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_entities"
),
icon: "mdi:magnify",
},
strings: [],
label: this.hass!.localize(
"ui.components.entity.entity-picker.no_entities"
),
icon_path: mdiMagnify,
},
...createItems,
];
@ -241,19 +292,49 @@ export class HaEntityPicker extends LitElement {
);
}
const isRTL = computeRTL(this.hass);
states = entityIds
.map((key) => {
const friendly_name = computeStateName(hass!.states[key]) || key;
.map<EntityPickerItem>((entityId) => {
const stateObj = hass!.states[entityId];
const { area, device } = getEntityContext(stateObj, hass);
const friendlyName = computeStateName(stateObj); // Keep this for search
const entityName = computeEntityName(stateObj, hass);
const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;
const primary = entityName || deviceName || entityId;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
const translatedDomain = domainToName(
this.hass.localize,
computeDomain(entityId)
);
return {
...hass!.states[key],
friendly_name,
strings: [key, friendly_name],
...hass!.states[entityId],
primary: primary,
secondary:
secondary ||
this.hass.localize("ui.components.device-picker.no_area"),
label: friendlyName,
translated_domain: translatedDomain,
sorting_label: [deviceName, entityName].filter(Boolean).join("-"),
entity_name: entityName || deviceName,
area_name: areaName,
device_name: deviceName,
friendly_name: friendlyName,
show_entity_id: hass.userData?.showEntityIdPicker,
};
})
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.friendly_name,
entityB.friendly_name,
entityA.sorting_label!,
entityB.sorting_label!,
this.hass.locale.language
)
);
@ -291,21 +372,14 @@ export class HaEntityPicker extends LitElement {
if (!states.length) {
return [
{
entity_id: "",
state: "",
last_changed: "",
last_updated: "",
context: { id: "", user_id: null, parent_id: null },
friendly_name: this.hass!.localize(
...FAKE_ENTITY,
primary: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),
attributes: {
friendly_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),
icon: "mdi:magnify",
},
strings: [],
label: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),
icon_path: mdiMagnify,
},
...createItems,
];
@ -331,8 +405,8 @@ export class HaEntityPicker extends LitElement {
}
public willUpdate(changedProps: PropertyValues) {
if (!this._initedStates || (changedProps.has("_opened") && this._opened)) {
this._states = this._getStates(
if (!this._initialItems || (changedProps.has("_opened") && this._opened)) {
this._items = this._getItems(
this._opened,
this.hass,
this.includeDomains,
@ -344,10 +418,10 @@ export class HaEntityPicker extends LitElement {
this.excludeEntities,
this.createDomains
);
if (this._initedStates) {
this.comboBox.filteredItems = this._states;
if (this._initialItems) {
this.comboBox.filteredItems = this._items;
}
this._initedStates = true;
this._initialItems = true;
}
if (changedProps.has("createDomains") && this.createDomains?.length) {
@ -367,7 +441,7 @@ export class HaEntityPicker extends LitElement {
: this.label}
.helper=${this.helper}
.allowCustomValue=${this.allowCustomEntity}
.filteredItems=${this._states}
.filteredItems=${this._items}
.renderer=${this._rowRenderer}
.required=${this.required}
.disabled=${this.disabled}
@ -408,12 +482,49 @@ export class HaEntityPicker extends LitElement {
}
}
private _fuseKeys = [
"entity_name",
"device_name",
"area_name",
"translated_domain",
"friendly_name", // for backwards compatibility
"entity_id", // for technical search
];
private _fuseIndex = memoizeOne((states: EntityPickerItem[]) =>
Fuse.createIndex(this._fuseKeys, states)
);
private _filterChanged(ev: CustomEvent): void {
const target = ev.target as HaComboBox;
const filterString = ev.detail.value.trim().toLowerCase();
target.filteredItems = filterString.length
? fuzzyFilterSort<HassEntityWithCachedName>(filterString, this._states)
: this._states;
const filterString = ev.detail.value.trim().toLowerCase() as string;
const minLength = 2;
const searchTerms = (filterString.split(" ") ?? []).filter(
(term) => term.length >= minLength
);
if (searchTerms.length > 0) {
const index = this._fuseIndex(this._items);
const options: IFuseOptions<EntityPickerItem> = {
isCaseSensitive: false,
threshold: 0.3,
ignoreDiacritics: true,
minMatchCharLength: minLength,
};
const fuse = new Fuse(this._items, options, index);
const results = fuse.search({
$and: searchTerms.map((term) => ({
$or: this._fuseKeys.map((key) => ({ [key]: term })),
})),
});
target.filteredItems = results.map((result) => result.item);
} else {
target.filteredItems = this._items;
}
}
private _setValue(value: string | undefined) {

View File

@ -1,23 +1,23 @@
import "@material/mwc-list/mwc-list-item";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { stringCompare } from "../../common/string/compare";
import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
import { fuzzyFilterSort } from "../../common/string/filter/sequence-matching";
import type { StatisticsMetaData } from "../../data/recorder";
import { getStatisticIds, getStatisticLabel } from "../../data/recorder";
import type { ValueChangedEvent, HomeAssistant } from "../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-combo-box-item";
import "../ha-svg-icon";
import "./state-badge";
import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
import { fuzzyFilterSort } from "../../common/string/filter/sequence-matching";
interface StatisticItem extends ScorableTextItem {
id: string;
@ -99,16 +99,18 @@ export class HaStatisticPicker extends LitElement {
@state() private _filteredItems?: StatisticItem[] = undefined;
private _rowRenderer: ComboBoxLitRenderer<StatisticItem> = (item) =>
html`<mwc-list-item graphic="avatar" twoline>
html`<ha-combo-box-item type="button">
${item.state
? html`<state-badge
slot="graphic"
.stateObj=${item.state}
.hass=${this.hass}
></state-badge>`
: ""}
<span>${item.name}</span>
<span slot="secondary"
? html`
<state-badge
slot="start"
.stateObj=${item.state}
.hass=${this.hass}
></state-badge>
`
: html`<span slot="start" style="width: 32px"></span>`}
<span slot="headline">${item.name}</span>
<span slot="supporting-text"
>${item.id === "" || item.id === "__missing"
? html`<a
target="_blank"
@ -120,7 +122,7 @@ export class HaStatisticPicker extends LitElement {
>`
: item.id}</span
>
</mwc-list-item>`;
</ha-combo-box-item>`;
private _getStatistics = memoizeOne(
(

View File

@ -10,20 +10,23 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-alert";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-list-item";
import "./ha-combo-box-item";
const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (item) =>
html`<ha-list-item twoline graphic="icon">
<span>${item.name}</span>
<span slot="secondary">${item.slug}</span>
const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (item) => html`
<ha-combo-box-item type="button">
<span slot="headline">${item.name}</span>
<span slot="supporting-text">${item.slug}</span>
${item.icon
? html`<img
alt=""
slot="graphic"
.src="/api/hassio/addons/${item.slug}/icon"
/>`
: ""}
</ha-list-item>`;
? html`
<img
alt=""
slot="start"
.src="/api/hassio/addons/${item.slug}/icon"
/>
`
: nothing}
</ha-combo-box-item>
`;
@customElement("ha-addon-picker")
class HaAddonPicker extends LitElement {

View File

@ -25,6 +25,7 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
import "./ha-floor-icon";
import "./ha-icon-button";
import "./ha-list-item";
@ -125,38 +126,38 @@ export class HaAreaFloorPicker extends LitElement {
private _rowRenderer: ComboBoxLitRenderer<FloorAreaEntry> = (item) => {
const rtl = computeRTL(this.hass);
return html`
<ha-list-item
graphic="icon"
<ha-combo-box-item
type="button"
style=${item.type === "area" && item.hasFloor
? rtl
? "--mdc-list-side-padding-right: 48px;"
: "--mdc-list-side-padding-left: 48px;"
? "--md-list-item-leading-space: 48px;"
: ""}
>
${item.type === "area" && item.hasFloor
? html`<ha-tree-indicator
style=${styleMap({
width: "48px",
position: "absolute",
top: "0px",
left: rtl ? undefined : "8px",
right: rtl ? "8px" : undefined,
transform: rtl ? "scaleX(-1)" : "",
})}
.end=${item.lastArea}
slot="graphic"
></ha-tree-indicator>`
? html`
<ha-tree-indicator
style=${styleMap({
width: "48px",
position: "absolute",
top: "0px",
left: rtl ? undefined : "4px",
right: rtl ? "4px" : undefined,
transform: rtl ? "scaleX(-1)" : "",
})}
.end=${item.lastArea}
slot="start"
></ha-tree-indicator>
`
: nothing}
${item.type === "floor"
? html`<ha-floor-icon slot="graphic" .floor=${item}></ha-floor-icon>`
? html`<ha-floor-icon slot="start" .floor=${item}></ha-floor-icon>`
: item.icon
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
: html`<ha-svg-icon
slot="graphic"
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
${item.name}
</ha-list-item>
</ha-combo-box-item>
`;
};

View File

@ -4,7 +4,6 @@ import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, html } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
@ -24,22 +23,21 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
import "./ha-icon-button";
import "./ha-list-item";
import "./ha-svg-icon";
type ScorableAreaRegistryEntry = ScorableTextItem & AreaRegistryEntry;
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (item) =>
html`<ha-list-item
graphic="icon"
class=${classMap({ "add-new": item.area_id === ADD_NEW_ID })}
>
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (item) => html`
<ha-combo-box-item type="button">
${item.icon
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
: html`<ha-svg-icon slot="graphic" .path=${mdiTextureBox}></ha-svg-icon>`}
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
: html`<ha-svg-icon slot="start" .path=${mdiTextureBox}></ha-svg-icon>`}
${item.name}
</ha-list-item>`;
</ha-combo-box-item>
`;
const ADD_NEW_ID = "___ADD_NEW___";
const NO_ITEMS_ID = "___NO_ITEMS___";

View File

@ -1,6 +1,7 @@
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { computeStateName } from "../common/entity/compute_state_name";
import { supportsFeature } from "../common/entity/supports-feature";
@ -32,6 +33,10 @@ export class HaCameraStream extends LitElement {
@property({ attribute: false }) public stateObj?: CameraEntity;
@property({ attribute: false }) public aspectRatio?: number;
@property({ attribute: false }) public fitMode?: "cover" | "contain" | "fill";
@property({ type: Boolean, attribute: "controls" })
public controls = false;
@ -101,6 +106,10 @@ export class HaCameraStream extends LitElement {
: this._connected
? computeMJPEGStreamUrl(this.stateObj)
: this._posterUrl || ""}
style=${styleMap({
aspectRatio: this.aspectRatio,
objectFit: this.fitMode,
})}
alt=${`Preview of the ${computeStateName(this.stateObj)} camera.`}
/>`;
}
@ -117,6 +126,8 @@ export class HaCameraStream extends LitElement {
.posterUrl=${this._posterUrl}
@streams=${this._handleHlsStreams}
class=${stream.visible ? "" : "hidden"}
.aspectRatio=${this.aspectRatio}
.fitMode=${this.fitMode}
></ha-hls-player>`;
}
@ -131,6 +142,8 @@ export class HaCameraStream extends LitElement {
.posterUrl=${this._posterUrl}
@streams=${this._handleWebRtcStreams}
class=${stream.visible ? "" : "hidden"}
.aspectRatio=${this.aspectRatio}
.fitMode=${this.fitMode}
></ha-web-rtc-player>`;
}
@ -259,6 +272,16 @@ export class HaCameraStream extends LitElement {
width: 100%;
}
ha-web-rtc-player {
width: 100%;
height: 100%;
}
ha-hls-player {
width: 100%;
height: 100%;
}
.hidden {
display: none;
}

View File

@ -0,0 +1,46 @@
import { css } from "lit";
import { customElement, property } from "lit/decorators";
import { HaMdListItem } from "./ha-md-list-item";
@customElement("ha-combo-box-item")
export class HaComboBoxItem extends HaMdListItem {
@property({ type: Boolean, reflect: true, attribute: "border-top" })
public borderTop = false;
static override styles = [
...super.styles,
css`
:host {
--md-list-item-one-line-container-height: 48px;
--md-list-item-two-line-container-height: 64px;
}
:host([border-top]) md-item {
border-top: 1px solid var(--divider-color);
}
[slot="start"] {
--paper-item-icon-color: var(--secondary-text-color);
}
[slot="headline"] {
line-height: 22px;
font-size: 14px;
white-space: nowrap;
}
[slot="supporting-text"] {
line-height: 18px;
font-size: 12px;
white-space: nowrap;
}
::slotted(state-badge),
::slotted(img) {
width: 32px;
height: 32px;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-combo-box-item": HaComboBoxItem;
}
}

View File

@ -16,8 +16,8 @@ import { customElement, property, query } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import type { HomeAssistant } from "../types";
import "./ha-combo-box-item";
import "./ha-icon-button";
import "./ha-list-item";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@ -216,10 +216,11 @@ export class HaComboBox extends LitElement {
private _defaultRowRenderer: ComboBoxLitRenderer<
string | Record<string, any>
> = (item) =>
html`<ha-list-item>
> = (item) => html`
<ha-combo-box-item type="button">
${this.itemLabelPath ? item[this.itemLabelPath] : item}
</ha-list-item>`;
</ha-combo-box-item>
`;
private _clearValue(ev: Event) {
ev.stopPropagation();

View File

@ -1,4 +1,3 @@
import "@material/mwc-list/mwc-list-item";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
@ -11,6 +10,7 @@ import type { ValueChangedEvent, HomeAssistant } from "../types";
import { brandsUrl } from "../util/brands-url";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
export interface ConfigEntryExtended extends ConfigEntry {
localized_domain_name?: string;
@ -48,18 +48,20 @@ class HaConfigEntryPicker extends LitElement {
this._getConfigEntries();
}
private _rowRenderer: ComboBoxLitRenderer<ConfigEntryExtended> = (item) =>
html`<mwc-list-item twoline graphic="icon">
<span
>${item.title ||
private _rowRenderer: ComboBoxLitRenderer<ConfigEntryExtended> = (
item
) => html`
<ha-combo-box-item type="button">
<span slot="headline">
${item.title ||
this.hass.localize(
"ui.panel.config.integrations.config_entry.unnamed_entry"
)}</span
>
<span slot="secondary">${item.localized_domain_name}</span>
)}
</span>
<span slot="supporting-text">${item.localized_domain_name}</span>
<img
alt=""
slot="graphic"
slot="start"
src=${brandsUrl({
domain: item.domain,
type: "icon",
@ -70,7 +72,8 @@ class HaConfigEntryPicker extends LitElement {
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
</mwc-list-item>`;
</ha-combo-box-item>
`;
protected render() {
if (!this._configEntries) {

View File

@ -3,7 +3,6 @@ import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, html } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
@ -28,9 +27,9 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
import "./ha-floor-icon";
import "./ha-icon-button";
import "./ha-list-item";
type ScorableFloorRegistryEntry = ScorableTextItem & FloorRegistryEntry;
@ -38,14 +37,12 @@ const ADD_NEW_ID = "___ADD_NEW___";
const NO_FLOORS_ID = "___NO_FLOORS___";
const ADD_NEW_SUGGESTION_ID = "___ADD_NEW_SUGGESTION___";
const rowRenderer: ComboBoxLitRenderer<FloorRegistryEntry> = (item) =>
html`<ha-list-item
graphic="icon"
class=${classMap({ "add-new": item.floor_id === ADD_NEW_ID })}
>
<ha-floor-icon slot="graphic" .floor=${item}></ha-floor-icon>
const rowRenderer: ComboBoxLitRenderer<FloorRegistryEntry> = (item) => html`
<ha-combo-box-item type="button">
<ha-floor-icon slot="start" .floor=${item}></ha-floor-icon>
${item.name}
</ha-list-item>`;
</ha-combo-box-item>
`;
@customElement("ha-floor-picker")
export class HaFloorPicker extends LitElement {

View File

@ -2,12 +2,13 @@ import type HlsType from "hls.js";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
import { nextRender } from "../common/util/render-status";
import { fetchStreamUrl } from "../data/camera";
import type { HomeAssistant } from "../types";
import "./ha-alert";
import { fetchStreamUrl } from "../data/camera";
import { isComponentLoaded } from "../common/config/is_component_loaded";
type HlsLite = Omit<
HlsType,
@ -24,6 +25,10 @@ class HaHLSPlayer extends LitElement {
@property({ attribute: "poster-url" }) public posterUrl?: string;
@property({ attribute: false }) public aspectRatio?: number;
@property({ attribute: false }) public fitMode?: "cover" | "contain" | "fill";
@property({ type: Boolean, attribute: "controls" })
public controls = false;
@ -87,6 +92,11 @@ class HaHLSPlayer extends LitElement {
?playsinline=${this.playsInline}
?controls=${this.controls}
@loadeddata=${this._loadedData}
style=${styleMap({
height: this.aspectRatio == null ? "100%" : "auto",
aspectRatio: this.aspectRatio,
objectFit: this.fitMode,
})}
></video>`
: ""}
`;

View File

@ -11,8 +11,8 @@ import { fireEvent } from "../common/dom/fire_event";
import { customIcons } from "../data/custom_icons";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-combo-box";
import "./ha-list-item";
import "./ha-icon";
import "./ha-combo-box-item";
interface IconItem {
icon: string;
@ -67,11 +67,12 @@ const loadCustomIconItems = async (iconsetPrefix: string) => {
}
};
const rowRenderer: ComboBoxLitRenderer<IconItem | RankedIcon> = (item) =>
html`<ha-list-item graphic="avatar">
<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>
const rowRenderer: ComboBoxLitRenderer<IconItem | RankedIcon> = (item) => html`
<ha-combo-box-item type="button">
<ha-icon .icon=${item.icon} slot="start"></ha-icon>
${item.icon}
</ha-list-item>`;
</ha-combo-box-item>
`;
@customElement("ha-icon-picker")
export class HaIconPicker extends LitElement {

View File

@ -3,7 +3,6 @@ import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
@ -26,8 +25,8 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
import "./ha-icon-button";
import "./ha-list-item";
import "./ha-svg-icon";
type ScorableLabelItem = ScorableTextItem & LabelRegistryEntry;
@ -36,16 +35,14 @@ const ADD_NEW_ID = "___ADD_NEW___";
const NO_LABELS_ID = "___NO_LABELS___";
const ADD_NEW_SUGGESTION_ID = "___ADD_NEW_SUGGESTION___";
const rowRenderer: ComboBoxLitRenderer<LabelRegistryEntry> = (item) =>
html`<ha-list-item
graphic="icon"
class=${classMap({ "add-new": item.label_id === ADD_NEW_ID })}
>
const rowRenderer: ComboBoxLitRenderer<LabelRegistryEntry> = (item) => html`
<ha-combo-box-item type="button">
${item.icon
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
: nothing}
${item.name}
</ha-list-item>`;
</ha-combo-box-item>
`;
@customElement("ha-label-picker")
export class HaLabelPicker extends SubscribeMixin(LitElement) {

View File

@ -1,7 +1,6 @@
import "@material/mwc-list/mwc-list-item";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { titleCase } from "../common/string/title-case";
@ -10,6 +9,7 @@ import type { LovelaceViewRawConfig } from "../data/lovelace/config/view";
import type { HomeAssistant, PanelInfo, ValueChangedEvent } from "../types";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
import "./ha-icon";
interface NavigationItem {
@ -21,11 +21,13 @@ interface NavigationItem {
const DEFAULT_ITEMS: NavigationItem[] = [];
const rowRenderer: ComboBoxLitRenderer<NavigationItem> = (item) => html`
<mwc-list-item graphic="icon" .twoline=${!!item.title}>
<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>
<span>${item.title || item.path}</span>
<span slot="secondary">${item.path}</span>
</mwc-list-item>
<ha-combo-box-item type="button">
<ha-icon .icon=${item.icon} slot="start"></ha-icon>
<span slot="headline">${item.title || item.path}</span>
${item.title
? html`<span slot="supporting-text">${item.path}</span>`
: nothing}
</ha-combo-box-item>
`;
const createViewNavigationItem = (

View File

@ -1,7 +1,5 @@
import "@material/mwc-button/mwc-button";
import { mdiCamera } from "@mdi/js";
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,
@ -12,12 +10,13 @@ import { prepareZXingModule } from "barcode-detector";
import type QrScanner from "qr-scanner";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import type { LocalizeFunc } from "../common/translations/localize";
import { addExternalBarCodeListener } from "../external_app/external_app_entrypoint";
import type { HomeAssistant } from "../types";
import "./ha-alert";
import "./ha-button";
import "./ha-button-menu";
import "./ha-list-item";
import "./ha-spinner";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@ -36,18 +35,22 @@ prepareZXingModule({
class HaQrScanner extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public localize!: LocalizeFunc;
@property() public description?: string;
@property({ attribute: "alternative_option_label" })
public alternativeOptionLabel?: string;
@property() public error?: string;
@property({ attribute: false }) public validate?: (
value: string
) => string | undefined;
@state() private _cameras?: QrScanner.Camera[];
@state() private _manual = false;
@state() private _loading = true;
@state() private _error?: string;
@state() private _warning?: string;
private _qrScanner?: QrScanner;
@ -88,29 +91,40 @@ class HaQrScanner extends LitElement {
this._loadQrScanner();
}
protected updated(changedProps: PropertyValues) {
if (changedProps.has("error") && this.error) {
alert(`error: ${this.error}`);
this._notifyExternalScanner(this.error);
}
}
protected render() {
if (this._nativeBarcodeScanner && !this._manual) {
if (this._nativeBarcodeScanner) {
return nothing;
}
return html`${this.error
? html`<ha-alert alert-type="error">${this.error}</ha-alert>`
: ""}
${navigator.mediaDevices && !this._manual
return html`${this._error || this._warning
? html`<ha-alert
.alertType=${this._error ? "error" : "warning"}
class=${this._error ? "" : "warning"}
>
${this._error || this._warning}
${this._error
? html` <ha-button @click=${this._retry} slot="action">
${this.hass.localize("ui.components.qr-scanner.retry")}
</ha-button>`
: nothing}
</ha-alert>`
: nothing}
${navigator.mediaDevices
? html`<video></video>
<div id="canvas-container">
${this._cameras && this._cameras.length > 1
${this._loading
? html`<div class="loading">
<ha-spinner active></ha-spinner>
</div>`
: nothing}
${!this._loading &&
!this._error &&
this._cameras &&
this._cameras.length > 1
? html`<ha-button-menu fixed @closed=${stopPropagation}>
<ha-icon-button
slot="trigger"
.label=${this.localize(
.label=${this.hass.localize(
"ui.components.qr-scanner.select_camera"
)}
.path=${mdiCamera}
@ -128,25 +142,25 @@ class HaQrScanner extends LitElement {
</ha-button-menu>`
: nothing}
</div>`
: html`${this._manual
? nothing
: html`<ha-alert alert-type="warning">
${!window.isSecureContext
? this.localize(
"ui.components.qr-scanner.only_https_supported"
)
: this.localize("ui.components.qr-scanner.not_supported")}
</ha-alert>`}
<p>${this.localize("ui.components.qr-scanner.manual_input")}</p>
: html`<ha-alert alert-type="warning">
${!window.isSecureContext
? this.hass.localize(
"ui.components.qr-scanner.only_https_supported"
)
: this.hass.localize("ui.components.qr-scanner.not_supported")}
</ha-alert>
<p>${this.hass.localize("ui.components.qr-scanner.manual_input")}</p>
<div class="row">
<ha-textfield
.label=${this.localize("ui.components.qr-scanner.enter_qr_code")}
.label=${this.hass.localize(
"ui.components.qr-scanner.enter_qr_code"
)}
@keyup=${this._manualKeyup}
@paste=${this._manualPaste}
></ha-textfield>
<mwc-button @click=${this._manualSubmit}>
${this.localize("ui.common.submit")}
</mwc-button>
<ha-button @click=${this._manualSubmit}>
${this.hass.localize("ui.common.submit")}
</ha-button>
</div>`}`;
}
@ -165,7 +179,9 @@ class HaQrScanner extends LitElement {
// eslint-disable-next-line @typescript-eslint/naming-convention
const QrScanner = (await import("qr-scanner")).default;
if (!(await QrScanner.hasCamera())) {
this._reportError("No camera found");
this._reportError(
this.hass.localize("ui.components.qr-scanner.no_camera_found")
);
return;
}
QrScanner.WORKER_PATH = "/static/js/qr-scanner-worker.min.js";
@ -181,6 +197,7 @@ class HaQrScanner extends LitElement {
canvas.style.display = "block";
try {
await this._qrScanner.start();
this._loading = false;
} catch (err: any) {
this._reportError(err);
}
@ -193,8 +210,8 @@ class HaQrScanner extends LitElement {
private _qrCodeError = (err: any) => {
if (err.endsWith("No QR code found")) {
this._qrNotFoundCount++;
if (this._qrNotFoundCount === 250) {
this._reportError(err);
if (this._qrNotFoundCount >= 250) {
this._reportWarning(err);
}
return;
}
@ -204,7 +221,17 @@ class HaQrScanner extends LitElement {
};
private _qrCodeScanned = (qrCodeString: string): void => {
this._warning = undefined;
this._qrNotFoundCount = 0;
if (this.validate) {
const validationMessage = this.validate(qrCodeString);
if (validationMessage) {
this._reportWarning(validationMessage);
return;
}
}
fireEvent(this, "qr-code-scanned", { value: qrCodeString });
};
@ -234,7 +261,10 @@ class HaQrScanner extends LitElement {
if (msg.command === "bar_code/scan_result") {
if (msg.payload.format !== "qr_code") {
this._notifyExternalScanner(
`Wrong barcode scanned! ${msg.payload.format}: ${msg.payload.rawValue}, we need a QR code.`
this.hass.localize("ui.components.qr-scanner.wrong_code", {
format: msg.payload.format,
rawValue: msg.payload.rawValue,
})
);
} else {
this._qrCodeScanned(msg.payload.rawValue);
@ -244,7 +274,7 @@ class HaQrScanner extends LitElement {
if (msg.payload.reason === "canceled") {
fireEvent(this, "qr-code-closed");
} else {
this._manual = true;
fireEvent(this, "qr-code-more-options");
}
}
return true;
@ -252,10 +282,17 @@ class HaQrScanner extends LitElement {
this.hass.auth.external!.fireMessage({
type: "bar_code/scan",
payload: {
title: this.title || "Scan QR code",
description: this.description || "Scan a barcode.",
title:
this.title ||
this.hass.localize("ui.components.qr-scanner.app.title"),
description:
this.description ||
this.hass.localize("ui.components.qr-scanner.app.description"),
alternative_option_label:
this.alternativeOptionLabel || "Click to manually enter the barcode",
this.alternativeOptionLabel ||
this.hass.localize(
"ui.components.qr-scanner.app.alternativeOptionLabel"
),
},
});
}
@ -269,25 +306,55 @@ class HaQrScanner extends LitElement {
}
private _notifyExternalScanner(message: string) {
if (!this.hass.auth.external) {
if (!this._nativeBarcodeScanner) {
return;
}
this.hass.auth.external.fireMessage({
this.hass.auth.external!.fireMessage({
type: "bar_code/notify",
payload: {
message,
},
});
this.error = undefined;
this._warning = undefined;
this._error = undefined;
}
private _reportError(message: string) {
fireEvent(this, "qr-code-error", { message });
const canvas = this._qrScanner?.$canvas;
if (canvas) {
canvas.style.display = "none";
}
this._error = message;
}
private _reportWarning(message: string) {
if (this._nativeBarcodeScanner) {
this._notifyExternalScanner(message);
} else {
this._warning = message;
}
}
private async _retry() {
if (this._qrScanner) {
this._loading = true;
this._error = undefined;
this._warning = undefined;
const canvas = this._qrScanner.$canvas;
canvas.style.display = "block";
this._qrNotFoundCount = 0;
await this._qrScanner.start();
this._loading = false;
}
}
static styles = css`
:root {
position: relative;
}
canvas {
width: 100%;
border-radius: 16px;
}
#canvas-container {
position: relative;
@ -312,6 +379,24 @@ class HaQrScanner extends LitElement {
margin-inline-end: 8px;
margin-inline-start: initial;
}
.loading {
display: flex;
position: absolute;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}
ha-alert {
display: block;
}
ha-alert.warning {
position: absolute;
z-index: 1;
background-color: var(--primary-background-color);
top: 0;
width: calc(100% - 48px);
}
`;
}
@ -319,8 +404,8 @@ declare global {
// for fire event
interface HASSDomEvents {
"qr-code-scanned": { value: string };
"qr-code-error": { message: string };
"qr-code-closed": undefined;
"qr-code-more-options": undefined;
}
interface HTMLElementTagNameMap {

View File

@ -7,7 +7,7 @@ import type { LocalizeFunc } from "../common/translations/localize";
import { domainToName } from "../data/integration";
import type { HomeAssistant } from "../types";
import "./ha-combo-box";
import "./ha-list-item";
import "./ha-combo-box-item";
import "./ha-service-icon";
import { getServiceIcons } from "../data/icons";
@ -29,18 +29,19 @@ class HaServicePicker extends LitElement {
}
private _rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> =
(item) =>
html`<ha-list-item twoline graphic="icon">
(item) => html`
<ha-combo-box-item type="button">
<ha-service-icon
slot="graphic"
slot="start"
.hass=${this.hass}
.service=${item.service}
></ha-service-icon>
<span>${item.name}</span>
<span slot="secondary"
<span slot="headline">${item.name}</span>
<span slot="supporting-text"
>${item.name === item.service ? "" : item.service}</span
>
</ha-list-item>`;
</ha-combo-box-item>
`;
protected render() {
return html`

View File

@ -1,8 +1,9 @@
import type { PropertyValues, TemplateResult } from "lit";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event";
import {
addWebRtcCandidate,
@ -26,6 +27,10 @@ class HaWebRtcPlayer extends LitElement {
@property() public entityid?: string;
@property({ attribute: false }) public aspectRatio?: number;
@property({ attribute: false }) public fitMode?: "cover" | "contain" | "fill";
@property({ type: Boolean, attribute: "controls" })
public controls = false;
@ -69,6 +74,11 @@ class HaWebRtcPlayer extends LitElement {
?controls=${this.controls}
poster=${ifDefined(this.posterUrl)}
@loadeddata=${this._loadedData}
style=${styleMap({
height: this.aspectRatio == null ? "100%" : "auto",
aspectRatio: this.aspectRatio,
objectFit: this.fitMode,
})}
></video>
`;
}

View File

@ -3,6 +3,7 @@ import { getOptimisticCollection } from "./collection";
export interface CoreFrontendUserData {
showAdvanced?: boolean;
showEntityIdPicker?: boolean;
}
declare global {

View File

@ -80,7 +80,7 @@ enum QRCodeVersion {
SmartStart = 1,
}
enum Protocols {
export enum Protocols {
ZWave = 0,
ZWaveLongRange = 1,
}
@ -151,12 +151,35 @@ export interface QRProvisioningInformation {
maxInclusionRequestInterval?: number | undefined;
uuid?: string | undefined;
supportedProtocols?: Protocols[] | undefined;
status?: ProvisioningEntryStatus;
}
export interface PlannedProvisioningEntry {
/** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */
dsk: string;
securityClasses: SecurityClass[];
status?: ProvisioningEntryStatus;
}
export enum ProvisioningEntryStatus {
Active = 0,
Inactive = 1,
}
export interface DeviceConfig {
filename: string;
manufacturer: string;
manufacturerId: number;
label: string;
description: string;
devices: {
productType: number;
productId: number;
}[];
firmwareVersion: {
min: string;
max: string;
};
}
export const MINIMUM_QR_STRING_LENGTH = 52;
@ -195,6 +218,7 @@ export interface ZWaveJSController {
is_rebuilding_routes: boolean;
inclusion_state: InclusionState;
nodes: ZWaveJSNodeStatus[];
supports_long_range: boolean;
}
export interface ZWaveJSNodeStatus {
@ -555,7 +579,7 @@ export const zwaveTryParseDskFromQrCode = (
export const zwaveValidateDskAndEnterPin = (
hass: HomeAssistant,
entry_id: string,
pin: string
pin: string | false
) =>
hass.callWS({
type: "zwave_js/validate_dsk_and_enter_pin",
@ -585,19 +609,38 @@ export const zwaveParseQrCode = (
qr_code_string,
});
export const lookupZwaveDevice = (
hass: HomeAssistant,
entry_id: string,
manufacturerId: number,
productType: number,
productId: number,
applicationVersion?: string
): Promise<DeviceConfig> =>
hass.callWS({
type: "zwave_js/lookup_device",
entry_id,
manufacturerId,
productType,
productId,
applicationVersion,
});
export const provisionZwaveSmartStartNode = (
hass: HomeAssistant,
entry_id: string,
qr_provisioning_information?: QRProvisioningInformation,
qr_code_string?: string,
planned_provisioning_entry?: PlannedProvisioningEntry
): Promise<QRProvisioningInformation> =>
protocol?: Protocols,
device_name?: string,
area_id?: string
): Promise<string> =>
hass.callWS({
type: "zwave_js/provision_smart_start_node",
entry_id,
qr_code_string,
qr_provisioning_information,
planned_provisioning_entry,
protocol,
device_name,
area_id,
});
export const unprovisionZwaveSmartStartNode = (
@ -613,6 +656,16 @@ export const unprovisionZwaveSmartStartNode = (
node_id,
});
export const subscribeNewDevices = (
hass: HomeAssistant,
entry_id: string,
callbackFunction: (message: any) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage((message) => callbackFunction(message), {
type: "zwave_js/subscribe_new_devices",
entry_id: entry_id,
});
export const fetchZwaveNodeStatus = (
hass: HomeAssistant,
device_id: string

View File

@ -133,8 +133,8 @@ export class MoreInfoInfo extends LitElement {
[data-domain="camera"] .content {
padding: 0;
/* max height of the video is full screen, minus the height of the header of the dialog and the padding of the dialog (mdc-dialog-max-height: calc(100% - 72px)) */
--video-max-height: calc(100vh - 65px - 72px);
/* max height of the video is full screen, minus the height of the header of the dialog (79px) and the max height of the dialog (mdc-dialog-max-height: calc(100% - 72px)) and the actions bar 60px */
--video-max-height: calc(100vh - 72px - 79px - 60px);
}
more-info-content {

View File

@ -1,17 +1,15 @@
import "@material/mwc-list/mwc-list-item";
import { mdiOpenInNew } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-spinner";
import "../../../components/ha-combo-box";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-markdown";
import "../../../components/ha-password-field";
import "../../../components/ha-spinner";
import "../../../components/ha-textfield";
import type {
ApplicationCredential,
@ -33,11 +31,6 @@ interface Domain {
name: string;
}
const rowRenderer: ComboBoxLitRenderer<Domain> = (item) =>
html`<mwc-list-item>
<span>${item.name}</span>
</mwc-list-item>`;
@customElement("dialog-add-application-credential")
export class DialogAddApplicationCredential extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -171,7 +164,6 @@ export class DialogAddApplicationCredential extends LitElement {
"ui.panel.config.application_credentials.editor.domain"
)}
.value=${this._domain}
.renderer=${rowRenderer}
.items=${this._domains}
item-id-path="id"
item-value-path="id"

View File

@ -4,15 +4,14 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import type { ScorableTextItem } from "../../../common/string/filter/sequence-matching";
import { fuzzyFilterSort } from "../../../common/string/filter/sequence-matching";
import "../../../components/ha-combo-box";
import type { HaComboBox } from "../../../components/ha-combo-box";
import "../../../components/ha-combo-box-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-list-item";
import "../../../components/ha-svg-icon";
import type { CategoryRegistryEntry } from "../../../data/category_registry";
import {
@ -29,16 +28,14 @@ const ADD_NEW_ID = "___ADD_NEW___";
const NO_CATEGORIES_ID = "___NO_CATEGORIES___";
const ADD_NEW_SUGGESTION_ID = "___ADD_NEW_SUGGESTION___";
const rowRenderer: ComboBoxLitRenderer<CategoryRegistryEntry> = (item) =>
html`<ha-list-item
graphic="icon"
class=${classMap({ "add-new": item.category_id === ADD_NEW_ID })}
>
const rowRenderer: ComboBoxLitRenderer<CategoryRegistryEntry> = (item) => html`
<ha-combo-box-item type="button">
${item.icon
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
: html`<ha-svg-icon .path=${mdiTag} slot="graphic"></ha-svg-icon>`}
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
: html`<ha-svg-icon .path=${mdiTag} slot="start"></ha-svg-icon>`}
${item.name}
</ha-list-item>`;
</ha-combo-box-item>
`;
@customElement("ha-category-picker")
export class HaCategoryPicker extends SubscribeMixin(LitElement) {

View File

@ -0,0 +1,54 @@
import type { QRProvisioningInformation } from "../../../../../../data/zwave_js";
export const backButtonStages: Partial<ZWaveJSAddNodeStage>[] = [
"qr_scan",
"select_other_method",
"qr_code_input",
"choose_security_strategy",
"configure_device",
];
export const closeButtonStages: Partial<ZWaveJSAddNodeStage>[] = [
"select_method",
"search_devices",
"search_smart_start_device",
"search_s2_device",
"failed",
"interviewing",
"validate_dsk_enter_pin",
"added_insecure",
"grant_security_classes",
"rename_device",
];
export type ZWaveJSAddNodeStage =
| "loading"
| "qr_scan"
| "select_method"
| "select_other_method"
| "qr_code_input"
| "search_devices"
| "search_smart_start_device"
| "search_s2_device"
| "choose_security_strategy"
| "configure_device"
| "interviewing"
| "failed"
| "timed_out"
| "added_insecure"
| "validate_dsk_enter_pin"
| "grant_security_classes"
| "waiting_for_device"
| "rename_device";
export interface ZWaveJSAddNodeSmartStartOptions {
name: string;
area?: string;
network_type?: string;
}
export interface ZWaveJSAddNodeDevice {
id?: string;
name: string;
provisioningInfo?: QRProvisioningInformation;
}

View File

@ -1,7 +1,9 @@
import { fireEvent } from "../../../../../common/dom/fire_event";
import { fireEvent } from "../../../../../../common/dom/fire_event";
export interface ZWaveJSAddNodeDialogParams {
entry_id: string;
longRangeSupported?: boolean;
inclusionOngoing?: boolean;
dsk?: string;
onStop?: () => void;
}

View File

@ -0,0 +1,65 @@
import { mdiCheckCircleOutline } from "@mdi/js";
import { customElement, property } from "lit/decorators";
import "@shoelace-style/shoelace/dist/components/animation/animation";
import { css, html, LitElement } from "lit";
import type { HomeAssistant } from "../../../../../../types";
import "../../../../../../components/ha-svg-icon";
import "../../../../../../components/ha-alert";
@customElement("zwave-js-add-node-added-insecure")
export class ZWaveJsAddNodeFinished extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "device-name" }) public deviceName?: string;
@property() public reason?;
render() {
return html`
<sl-animation name="zoomIn" .iterations=${1} play>
<ha-svg-icon .path=${mdiCheckCircleOutline}></ha-svg-icon>
</sl-animation>
<ha-alert alert-type="warning">
${this.reason
? this.hass.localize(
`ui.panel.config.zwave_js.add_node.added_insecure.low_security_reason.${this.reason}`
)
: ""}
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.added_insecure.added_insecurely_text",
{
deviceName: html`<b>${this.deviceName}</b>`,
}
)}
<p>
${this.hass.localize(
`ui.panel.config.zwave_js.add_node.added_insecure.try_again_text`
)}
</p>
</ha-alert>
`;
}
static styles = css`
:host {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
ha-svg-icon {
--mdc-icon-size: 96px;
color: var(--warning-color);
}
ha-alert {
margin-top: 16px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"zwave-js-add-node-added-insecure": ZWaveJsAddNodeFinished;
}
}

View File

@ -0,0 +1,99 @@
import { customElement, property } from "lit/decorators";
import { css, html, LitElement, nothing } from "lit";
import { fireEvent } from "../../../../../../common/dom/fire_event";
import type { HaTextField } from "../../../../../../components/ha-textfield";
import "../../../../../../components/ha-textfield";
import "../../../../../../components/ha-alert";
@customElement("zwave-js-add-node-code-input")
export class ZWaveJsAddNodeCodeInput extends LitElement {
@property() public value = "";
@property() public description = "";
@property() public placeholder = "";
@property({ attribute: "reference-key" }) public referenceKey = "";
@property() public error?: string;
@property({ type: Boolean }) public numeric = false;
render() {
return html`
<p>${this.description}</p>
${this.error
? html`<ha-alert alert-type="error">${this.error}</ha-alert>`
: nothing}
<ha-textfield
.placeholder=${this.placeholder}
.value=${this.value}
@input=${this._handleChange}
@keyup=${this._handleKeyup}
required
autofocus
></ha-textfield>
${this.referenceKey
? html`<div>
<span>${this.value.padEnd(5, "·")}</span>${this.referenceKey}
</div> `
: nothing}
`;
}
private _handleKeyup(ev: KeyboardEvent): void {
if (ev.key === "Enter" && this.value) {
fireEvent(this, "z-wave-submit");
}
}
private _handleChange(ev: InputEvent): void {
const inputElement = ev.target as HaTextField;
if (
this.numeric &&
(isNaN(Number(inputElement.value)) || inputElement.value.length > 5)
) {
inputElement.value = this.value;
return;
}
this.value = (ev.target as HaTextField).value;
fireEvent(this, "value-changed", {
value: (ev.target as HaTextField).value,
});
}
static styles = css`
ha-textfield {
width: 100%;
}
ha-alert {
display: block;
margin-bottom: 16px;
}
p {
color: var(--secondary-text-color);
margin-top: 0;
margin-bottom: 16px;
}
div {
font-family: "Roboto Mono", "Consolas", "Menlo", monospace;
margin-top: 16px;
}
div span {
color: var(--primary-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"zwave-js-add-node-code-input": ZWaveJsAddNodeCodeInput;
}
interface HASSDomEvents {
"z-wave-submit";
}
}

View File

@ -0,0 +1,152 @@
import { customElement, property, state } from "lit/decorators";
import { html, LitElement, type PropertyValues } from "lit";
import memoizeOne from "memoize-one";
import type { HomeAssistant } from "../../../../../../types";
import type { LocalizeFunc } from "../../../../../../common/translations/localize";
import type { HaFormSchema } from "../../../../../../components/ha-form/types";
import { fireEvent } from "../../../../../../common/dom/fire_event";
import { Protocols } from "../../../../../../data/zwave_js";
import type { ZWaveJSAddNodeSmartStartOptions } from "./data";
import "../../../../../../components/ha-form/ha-form";
@customElement("zwave-js-add-node-configure-device")
export class ZWaveJsAddNodeConfigureDevice extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "device-name" }) public deviceName = "";
@property({ type: Boolean, attribute: "lr-supported" })
public longRangeSupported = false;
@state() private _options?: ZWaveJSAddNodeSmartStartOptions;
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this._options = {
name: this.deviceName,
};
if (this.longRangeSupported) {
this._options.network_type = Protocols.ZWaveLongRange.toString();
}
fireEvent(this, "value-changed", { value: this._options });
}
}
render() {
return html`
<ha-form
.hass=${this.hass}
.schema=${this._getSchema(this.hass.localize, this.longRangeSupported)}
.data=${this._options!}
@value-changed=${this._setOptions}
.computeLabel=${this._computeLabel}
>
</ha-form>
`;
}
private _getSchema = memoizeOne(
(localize: LocalizeFunc, longRangeSupported: boolean): HaFormSchema[] => {
const schema: HaFormSchema[] = [
{
name: "name",
required: true,
default: this.deviceName,
type: "string",
autofocus: true,
},
{
name: "area",
selector: {
area: {},
},
},
];
if (longRangeSupported) {
schema.push({
name: "network_type",
required: true,
selector: {
select: {
box_max_columns: 1,
mode: "box",
options: [
{
value: Protocols.ZWaveLongRange.toString(),
label: localize(
"ui.panel.config.zwave_js.add_node.configure_device.long_range_label"
),
description: localize(
"ui.panel.config.zwave_js.add_node.configure_device.long_range_description"
),
image: {
src: "/static/images/z-wave-add-node/long-range.svg",
src_dark:
"/static/images/z-wave-add-node/long-range_dark.svg",
flip_rtl: true,
},
},
{
value: Protocols.ZWave.toString(),
label: localize(
"ui.panel.config.zwave_js.add_node.configure_device.mesh_label"
),
description: localize(
"ui.panel.config.zwave_js.add_node.configure_device.mesh_description"
),
image: {
src: "/static/images/z-wave-add-node/mesh.svg",
src_dark: "/static/images/z-wave-add-node/mesh_dark.svg",
flip_rtl: true,
},
},
],
},
},
});
}
return schema;
}
);
private _computeLabel = (schema: HaFormSchema): string | undefined => {
if (schema.name === "network_type") {
return this.hass.localize(
"ui.panel.config.zwave_js.add_node.configure_device.choose_network_type"
);
}
if (schema.name === "name") {
return this.hass.localize(
"ui.panel.config.zwave_js.add_node.configure_device.device_name"
);
}
if (schema.name === "area") {
return this.hass.localize(
"ui.panel.config.zwave_js.add_node.configure_device.device_area"
);
}
return undefined;
};
private _setOptions(event: any) {
this._options = {
...this._options!,
...event.detail.value,
};
fireEvent(this, "value-changed", { value: this._options });
}
}
declare global {
interface HTMLElementTagNameMap {
"zwave-js-add-node-configure-device": ZWaveJsAddNodeConfigureDevice;
}
}

View File

@ -0,0 +1,68 @@
import { customElement, property } from "lit/decorators";
import { css, html, LitElement, nothing } from "lit";
import type { HomeAssistant } from "../../../../../../types";
import type { ZWaveJSAddNodeDevice } from "./data";
import "../../../../../../components/ha-alert";
import "../../../../../../components/ha-button";
@customElement("zwave-js-add-node-failed")
export class ZWaveJsAddNodeFailed extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public error?: string;
@property({ attribute: false }) public device?: ZWaveJSAddNodeDevice;
render() {
return html`
<ha-alert
alert-type="error"
.title=${this.hass.localize(
"ui.panel.config.zwave_js.add_node.inclusion_failed"
)}
>
${this.error ||
this.hass.localize("ui.panel.config.zwave_js.add_node.check_logs")}
</ha-alert>
${this.error
? html`<div class="note">
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.check_logs"
)}
</div>`
: nothing}
${this.device?.id
? html`<a href=${`/config/devices/device/${this.device.id}`}>
<ha-button>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.view_device"
)}
</ha-button>
</a>`
: nothing}
`;
}
static styles = css`
:host {
display: block;
padding: 16px;
}
div.note {
text-align: center;
margin-top: 16px;
font-size: 12px;
color: var(--secondary-text-color);
}
ha-button {
margin-top: 32px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"zwave-js-add-node-failed": ZWaveJsAddNodeFailed;
}
}

View File

@ -0,0 +1,108 @@
import { customElement, property } from "lit/decorators";
import "@shoelace-style/shoelace/dist/components/animation/animation";
import { css, html, LitElement, nothing } from "lit";
import type { HomeAssistant } from "../../../../../../types";
import { SecurityClass } from "../../../../../../data/zwave_js";
import type { HaCheckbox } from "../../../../../../components/ha-checkbox";
import { fireEvent } from "../../../../../../common/dom/fire_event";
import "../../../../../../components/ha-alert";
import "../../../../../../components/ha-formfield";
import "../../../../../../components/ha-checkbox";
@customElement("zwave-js-add-node-grant-security-classes")
export class ZWaveJsAddNodeGrantSecurityClasses extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public error?: string;
@property({ attribute: false }) public securityClassOptions!: SecurityClass[];
@property({ attribute: false })
public selectedSecurityClasses: SecurityClass[] = [];
render() {
return html`
${this.error
? html`<ha-alert alert-type="error"> ${this.error} </ha-alert>`
: nothing}
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.grant_security_classes.description"
)}
</p>
<div class="flex-column">
${this.securityClassOptions
.sort((a, b) => {
// Put highest security classes at the top, S0 at the bottom
if (a === SecurityClass.S0_Legacy) return 1;
if (b === SecurityClass.S0_Legacy) return -1;
return b - a;
})
.map(
(securityClass) =>
html`<ha-formfield
.label=${html`<b
>${this.hass.localize(
`ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.title`
)}</b
>
<div class="secondary">
${this.hass.localize(
`ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.description`
)}
</div>`}
>
<ha-checkbox
@change=${this._handleSecurityClassChange}
.value=${securityClass.toString()}
.checked=${this.selectedSecurityClasses.includes(
securityClass
)}
>
</ha-checkbox>
</ha-formfield>`
)}
</div>
`;
}
private _handleSecurityClassChange(ev: CustomEvent) {
const checkbox = ev.currentTarget as HaCheckbox;
const securityClass = Number(checkbox.value);
if (
checkbox.checked &&
!this.selectedSecurityClasses.includes(securityClass)
) {
fireEvent(this, "value-changed", {
value: [...this.selectedSecurityClasses, securityClass],
});
} else if (!checkbox.checked) {
fireEvent(this, "value-changed", {
value: this.selectedSecurityClasses.filter(
(val) => val !== securityClass
),
});
}
}
static styles = css`
ha-alert {
display: block;
margin-bottom: 16px;
}
.flex-column {
display: flex;
flex-direction: column;
}
.secondary {
color: var(--secondary-text-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"zwave-js-add-node-grant-security-classes": ZWaveJsAddNodeGrantSecurityClasses;
}
}

View File

@ -0,0 +1,46 @@
import { customElement, property } from "lit/decorators";
import { css, html, LitElement, nothing } from "lit";
import "../../../../../../components/ha-fade-in";
import "../../../../../../components/ha-spinner";
@customElement("zwave-js-add-node-loading")
export class ZWaveJsAddNodeLoading extends LitElement {
@property() public description?: string;
@property({ type: Number }) public delay = 0;
render() {
return html`
<ha-fade-in .delay=${this.delay}>
<div class="loading">
<ha-spinner size="large"></ha-spinner>
</div>
${this.description ? html`<p>${this.description}</p>` : nothing}
</ha-fade-in>
`;
}
static styles = css`
ha-fade-in {
display: block;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
}
p {
margin-top: 16px;
color: var(--secondary-text-color);
text-align: center;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"zwave-js-add-node-loading": ZWaveJsAddNodeLoading;
}
}

View File

@ -0,0 +1,164 @@
import "@shoelace-style/shoelace/dist/components/animation/animation";
import { mdiRestart } from "@mdi/js";
import { customElement, property } from "lit/decorators";
import { css, html, LitElement, nothing } from "lit";
import type { HomeAssistant } from "../../../../../../types";
import { fireEvent } from "../../../../../../common/dom/fire_event";
import { InclusionStrategy } from "../../../../../../data/zwave_js";
import "../../../../../../components/ha-spinner";
import "../../../../../../components/ha-button";
import "../../../../../../components/ha-alert";
@customElement("zwave-js-add-node-searching-devices")
export class ZWaveJsAddNodeSearchingDevices extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, attribute: "smart-start" })
public smartStart = false;
@property({ type: Boolean, attribute: "show-security-options" })
public showSecurityOptions = false;
@property({ type: Boolean, attribute: "show-add-another-device" })
public showAddAnotherDevice = false;
@property({ attribute: false }) public inclusionStrategy?: InclusionStrategy;
render() {
let inclusionStrategyTranslationKey = "";
if (this.inclusionStrategy !== undefined) {
switch (this.inclusionStrategy) {
case InclusionStrategy.Security_S0:
inclusionStrategyTranslationKey = "s0";
break;
case InclusionStrategy.Insecure:
inclusionStrategyTranslationKey = "insecure";
break;
default:
inclusionStrategyTranslationKey = "default";
}
}
return html`
<div class="searching-devices">
<div class="searching-spinner">
<div class="spinner">
<ha-spinner></ha-spinner>
</div>
<sl-animation name="pulse" easing="linear" .duration=${2000} play>
<div class="circle"></div>
</sl-animation>
</div>
${this.smartStart
? html`<ha-alert
.title=${this.hass.localize(
"ui.panel.config.zwave_js.add_node.specific_device.turn_on_device"
)}
>
<ha-svg-icon slot="icon" .path=${mdiRestart}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.specific_device.turn_on_device_description"
)}
</ha-alert>
<p class="note">
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.specific_device.close_description"
)}
</p>`
: html`
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
)}
</p>
`}
${this.showSecurityOptions && !inclusionStrategyTranslationKey
? html`<ha-button @click=${this._handleSecurityOptions}>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.security_options"
)}
</ha-button>`
: inclusionStrategyTranslationKey
? html`<span class="note">
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.select_strategy.inclusion_strategy",
{
strategy: this.hass.localize(
`ui.panel.config.zwave_js.add_node.select_strategy.${inclusionStrategyTranslationKey}_label`
),
}
)}
</span>`
: nothing}
${this.showAddAnotherDevice
? html`<ha-button @click=${this._handleAddAnotherDevice}>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.specific_device.add_another_z_wave_device"
)}
</ha-button>`
: nothing}
</div>
`;
}
private _handleSecurityOptions() {
fireEvent(this, "show-z-wave-security-options");
}
private _handleAddAnotherDevice() {
fireEvent(this, "add-another-z-wave-device");
}
static styles = css`
:host {
text-align: center;
display: block;
}
ha-alert {
margin-top: 32px;
display: block;
}
.note {
font-size: 12px;
color: var(--secondary-text-color);
}
.searching-spinner {
margin-left: auto;
margin-right: auto;
position: relative;
width: 128px;
height: 128px;
}
.searching-spinner .circle {
border-radius: 50%;
background-color: var(--light-primary-color);
position: absolute;
width: calc(100% - 32px);
height: calc(100% - 32px);
margin: 16px;
}
.searching-spinner .spinner {
z-index: 1;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
--ha-spinner-divider-color: var(--light-primary-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"zwave-js-add-node-searching-devices": ZWaveJsAddNodeSearchingDevices;
}
interface HASSDomEvents {
"show-z-wave-security-options": undefined;
"add-another-z-wave-device": undefined;
}
}

View File

@ -0,0 +1,112 @@
import { customElement, property } from "lit/decorators";
import { css, html, LitElement, nothing } from "lit";
import { fireEvent } from "../../../../../../common/dom/fire_event";
import type { HomeAssistant } from "../../../../../../types";
import "../../../../../../components/ha-md-list";
import "../../../../../../components/ha-md-list-item";
import "../../../../../../components/ha-alert";
import "../../../../../../components/ha-icon-next";
@customElement("zwave-js-add-node-select-method")
export class ZWaveJsAddNodeSelectMethod extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, attribute: "hide-qr-webcam" })
public hideQrWebcam = false;
render() {
return html`
${!this.hideQrWebcam && !window.isSecureContext
? html`<ha-alert alert-type="warning">
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.select_method.webcam_unsupported"
)}</ha-alert
>`
: nothing}
<ha-md-list>
${!this.hideQrWebcam
? html`<ha-md-list-item
interactive
type="button"
@click=${this._selectMethod}
.value=${"qr_code_webcam"}
.disabled=${!window.isSecureContext}
>
<div slot="headline">
${this.hass.localize(
`ui.panel.config.zwave_js.add_node.select_method.qr_code_webcam`
)}
</div>
<div slot="supporting-text">
${this.hass.localize(
`ui.panel.config.zwave_js.add_node.select_method.qr_code_webcam_description`
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>`
: nothing}
<ha-md-list-item
interactive
type="button"
@click=${this._selectMethod}
.value=${"qr_code_manual"}
>
<div slot="headline">
${this.hass.localize(
`ui.panel.config.zwave_js.add_node.select_method.qr_code_manual`
)}
</div>
<div slot="supporting-text">
${this.hass.localize(
`ui.panel.config.zwave_js.add_node.select_method.qr_code_manual_description`
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
<ha-md-list-item
interactive
type="button"
@click=${this._selectMethod}
.value=${"search_device"}
>
<div slot="headline">
${this.hass.localize(
`ui.panel.config.zwave_js.add_node.select_method.search_device`
)}
</div>
<div slot="supporting-text">
${this.hass.localize(
`ui.panel.config.zwave_js.add_node.select_method.search_device_description`
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
</ha-md-list>
`;
}
private _selectMethod(event: any) {
const method = event.currentTarget.value;
if (method !== "qr_code_webcam" || window.isSecureContext) {
fireEvent(this, "z-wave-method-selected", { method });
}
}
static styles = css`
ha-md-list {
padding: 0;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"zwave-js-add-node-select-method": ZWaveJsAddNodeSelectMethod;
}
interface HASSDomEvents {
"z-wave-method-selected": {
method: "qr_code_webcam" | "qr_code_manual" | "search_device";
};
}
}

View File

@ -0,0 +1,107 @@
import { customElement, property, state } from "lit/decorators";
import { css, html, LitElement } from "lit";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../../common/dom/fire_event";
import type { HomeAssistant } from "../../../../../../types";
import { InclusionStrategy } from "../../../../../../data/zwave_js";
import type { LocalizeFunc } from "../../../../../../common/translations/localize";
import type { HaFormSchema } from "../../../../../../components/ha-form/types";
import "../../../../../../components/ha-form/ha-form";
@customElement("zwave-js-add-node-select-security-strategy")
export class ZWaveJsAddNodeSelectMethod extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() public _inclusionStrategy?: InclusionStrategy;
private _getSchema = memoizeOne((localize: LocalizeFunc): HaFormSchema[] => [
{
name: "strategy",
required: true,
selector: {
select: {
box_max_columns: 1,
mode: "box",
options: [
{
value: InclusionStrategy.Default.toString(),
label: localize(
"ui.panel.config.zwave_js.add_node.select_strategy.default_label"
),
description: localize(
"ui.panel.config.zwave_js.add_node.select_strategy.default_description"
),
},
{
value: InclusionStrategy.Security_S0.toString(),
label: localize(
"ui.panel.config.zwave_js.add_node.select_strategy.s0_label"
),
description: localize(
"ui.panel.config.zwave_js.add_node.select_strategy.s0_description"
),
},
{
value: InclusionStrategy.Insecure.toString(),
label: localize(
"ui.panel.config.zwave_js.add_node.select_strategy.insecure_label"
),
description: localize(
"ui.panel.config.zwave_js.add_node.select_strategy.insecure_description"
),
},
],
},
},
},
]);
render() {
return html`
<ha-form
.schema=${this._getSchema(this.hass.localize)}
.data=${{ strategy: this._inclusionStrategy?.toString() }}
@value-changed=${this._selectStrategy}
.computeLabel=${this._computeLabel}
>
</ha-form>
`;
}
private _computeLabel = () =>
this.hass.localize(
"ui.panel.config.zwave_js.add_node.select_strategy.title"
);
private _selectStrategy(event: any) {
const selectedStrategy = Number(
event.detail.value.strategy
) as InclusionStrategy;
fireEvent(this, "z-wave-strategy-selected", {
strategy: selectedStrategy,
});
}
static styles = css`
:host {
display: block;
padding: 16px;
}
ha-md-list {
padding: 0;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"zwave-js-add-node-select-security-strategy": ZWaveJsAddNodeSelectMethod;
}
interface HASSDomEvents {
"z-wave-strategy-selected": {
strategy: InclusionStrategy;
};
}
}

View File

@ -1,7 +1,7 @@
/* eslint-disable lit/lifecycle-super */
import { customElement } from "lit/decorators";
import { navigate } from "../../../../../common/navigate";
import type { HomeAssistant } from "../../../../../types";
import { navigate } from "../../../../../../common/navigate";
import type { HomeAssistant } from "../../../../../../types";
import { showZWaveJSAddNodeDialog } from "./show-dialog-zwave_js-add-node";
@customElement("zwave_js-add-node")
@ -21,6 +21,7 @@ export class DialogZWaveJSAddNode extends HTMLElement {
replace: true,
}
);
showZWaveJSAddNodeDialog(this, {
entry_id: this.configEntryId,
});

View File

@ -49,7 +49,7 @@ import "../../../../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import { showZWaveJSAddNodeDialog } from "./show-dialog-zwave_js-add-node";
import { showZWaveJSAddNodeDialog } from "./add-node/show-dialog-zwave_js-add-node";
import { showZWaveJSRebuildNetworkRoutesDialog } from "./show-dialog-zwave_js-rebuild-network-routes";
import { showZWaveJSRemoveNodeDialog } from "./show-dialog-zwave_js-remove-node";
import { configTabs } from "./zwave_js-config-router";
@ -101,7 +101,7 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
const inclusion_state = this._network?.controller.inclusion_state;
// show dialog if inclusion/exclusion is already in progress
if (inclusion_state === InclusionState.Including) {
this._addNodeClicked();
this._openInclusionDialog(undefined, true);
} else if (inclusion_state === InclusionState.Excluding) {
this._removeNodeClicked();
}
@ -419,11 +419,6 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
"ui.panel.config.zwave_js.common.rebuild_network_routes"
)}
</ha-button>
<ha-button @click=${this._openOptionFlow}>
${this.hass.localize(
"ui.panel.config.zwave_js.common.reconfigure_server"
)}
</ha-button>
</div>
</ha-card>
<ha-card>
@ -508,7 +503,15 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
@change=${this._handleRestoreFileSelected}
style="display: none"
/>
</div>`}
</div>
<ha-button
@click=${this._openOptionFlow}
class="warning migrate-button"
>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.nvm_backup.migrate"
)}
</ha-button>`}
</div>
</ha-card>
`
@ -743,7 +746,7 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
input.value = "";
}
private _openInclusionDialog(dsk?: string) {
private _openInclusionDialog(dsk?: string, inclusionOngoing = false) {
if (!this._dialogOpen) {
// Unsubscribe from S2 inclusion before opening dialog
if (this._s2InclusionUnsubscribe) {
@ -755,6 +758,8 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
entry_id: this.configEntryId!,
dsk,
onStop: this._handleInclusionDialogClosed,
longRangeSupported: !!this._network?.controller?.supports_long_range,
inclusionOngoing,
});
this._dialogOpen = true;
}
@ -962,6 +967,10 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
.button-content {
pointer-events: none;
}
.migrate-button {
margin-left: auto;
}
`,
];
}

View File

@ -42,7 +42,7 @@ class ZWaveJSConfigRouter extends HassRouterPage {
},
add: {
tag: "zwave_js-add-node",
load: () => import("./zwave_js-add-node"),
load: () => import("./add-node/zwave_js-add-node"),
},
node_config: {
tag: "zwave_js-node-config",

View File

@ -93,8 +93,7 @@ export class HuiAreaCard
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false })
public layout?: string;
@property({ attribute: false }) public layout?: string;
@state() private _config?: AreaCardConfig;

View File

@ -55,6 +55,8 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public layout?: string;
@state() private _config?: PictureEntityCardConfig;
public getCardSize(): number {
@ -155,6 +157,10 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
}
}
const ignoreAspectRatio =
this.layout === "grid" &&
typeof this._config.grid_options?.rows === "number";
return html`
<ha-card>
<hui-image
@ -167,7 +173,9 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
: this._config.camera_image}
.cameraView=${this._config.camera_view}
.entity=${this._config.entity}
.aspectRatio=${this._config.aspect_ratio}
.aspectRatio=${ignoreAspectRatio
? undefined
: this._config.aspect_ratio}
.fitMode=${this._config.fit_mode}
@action=${this._handleAction}
.actionHandler=${actionHandler({

View File

@ -63,6 +63,8 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public layout?: string;
@state() private _config?: PictureGlanceCardConfig;
private _entitiesDialog?: PictureGlanceEntityConfig[];
@ -199,6 +201,10 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
}
}
const ignoreAspectRatio =
this.layout === "grid" &&
typeof this._config.grid_options?.rows === "number";
return html`
<ha-card>
<hui-image
@ -226,7 +232,10 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
.cameraImage=${this._config.camera_image}
.cameraView=${this._config.camera_view}
.entity=${this._config.entity}
.aspectRatio=${this._config.aspect_ratio}
.fitMode=${this._config.fit_mode}
.aspectRatio=${ignoreAspectRatio
? undefined
: this._config.aspect_ratio}
></hui-image>
<div class="box">
${this._config.title
@ -324,6 +333,9 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
height: 100%;
box-sizing: border-box;
}
hui-image {
height: 100%;
}
hui-image.clickable {
cursor: pointer;
}

View File

@ -440,6 +440,7 @@ export interface PictureEntityCardConfig extends LovelaceCardConfig {
state_image?: Record<string, unknown>;
state_filter?: string[];
aspect_ratio?: string;
fit_mode?: "cover" | "contain" | "fill";
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
@ -458,6 +459,7 @@ export interface PictureGlanceCardConfig extends LovelaceCardConfig {
state_image?: Record<string, unknown>;
state_filter?: string[];
aspect_ratio?: string;
fit_mode?: "cover" | "contain" | "fill";
entity?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;

View File

@ -217,6 +217,10 @@ export class HuiImage extends LitElement {
muted
.hass=${this.hass}
.stateObj=${cameraObj}
.fitMode=${this.fitMode}
.aspectRatio=${this._ratio
? this._ratio.w / this._ratio.h
: undefined}
@load=${this._onVideoLoad}
></ha-camera-stream>
`
@ -400,6 +404,12 @@ export class HuiImage extends LitElement {
object-fit: cover;
}
ha-camera-stream {
display: block;
height: 100%;
width: 100%;
}
.progress-container {
display: flex;
justify-content: center;

View File

@ -2,11 +2,24 @@ import { mdiGestureTap } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, boolean, object, optional, string } from "superstruct";
import memoizeOne from "memoize-one";
import {
assert,
assign,
boolean,
enums,
object,
optional,
string,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import { STUB_IMAGE } from "../../cards/hui-picture-entity-card";
import type { PictureEntityCardConfig } from "../../cards/types";
@ -22,7 +35,7 @@ const cardConfigStruct = assign(
image: optional(string()),
name: optional(string()),
camera_image: optional(string()),
camera_view: optional(string()),
camera_view: optional(enums(["auto", "live"])),
aspect_ratio: optional(string()),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
@ -30,74 +43,10 @@ const cardConfigStruct = assign(
show_name: optional(boolean()),
show_state: optional(boolean()),
theme: optional(string()),
fit_mode: optional(string()),
fit_mode: optional(enums(["cover", "contain", "fill"])),
})
);
const SCHEMA = [
{ name: "entity", required: true, selector: { entity: {} } },
{ name: "name", selector: { text: {} } },
{ name: "image", selector: { image: {} } },
{ name: "camera_image", selector: { entity: { domain: "camera" } } },
{
name: "",
type: "grid",
schema: [
{
name: "camera_view",
selector: { select: { options: ["auto", "live"] } },
},
{ name: "aspect_ratio", selector: { text: {} } },
],
},
{
name: "",
type: "grid",
schema: [
{
name: "show_name",
selector: { boolean: {} },
},
{
name: "show_state",
selector: { boolean: {} },
},
],
},
{ name: "theme", selector: { theme: {} } },
{
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})
),
},
],
},
] as const;
@customElement("hui-picture-entity-card-editor")
export class HuiPictureEntityCardEditor
extends LitElement
@ -112,6 +61,99 @@ export class HuiPictureEntityCardEditor
this._config = config;
}
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
[
{ name: "entity", required: true, selector: { entity: {} } },
{ name: "name", selector: { text: {} } },
{ name: "image", selector: { image: {} } },
{ name: "camera_image", selector: { entity: { domain: "camera" } } },
{
name: "",
type: "grid",
schema: [
{
name: "camera_view",
required: true,
selector: {
select: {
options: ["auto", "live"].map((value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.generic.camera_view_options.${value}`
),
})),
mode: "dropdown",
},
},
},
{
name: "fit_mode",
required: true,
selector: {
select: {
options: ["cover", "contain", "fill"].map((value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.generic.fit_mode_options.${value}`
),
})),
mode: "dropdown",
},
},
},
{ name: "aspect_ratio", selector: { text: {} } },
],
},
{
name: "",
type: "grid",
schema: [
{
name: "show_name",
selector: { boolean: {} },
},
{
name: "show_state",
selector: { boolean: {} },
},
],
},
{ name: "theme", selector: { theme: {} } },
{
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})
),
},
],
},
] as const satisfies HaFormSchema[]
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
@ -121,15 +163,19 @@ export class HuiPictureEntityCardEditor
show_state: true,
show_name: true,
camera_view: "auto",
fit_mode: "cover",
...this._config,
};
const schema = this._schema(this.hass.localize);
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${SCHEMA}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback}
@value-changed=${this._valueChanged}
></ha-form>
</div>
@ -152,7 +198,9 @@ export class HuiPictureEntityCardEditor
fireEvent(this, "config-changed", { config });
}
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "theme":
case "tap_action":
@ -170,6 +218,21 @@ export class HuiPictureEntityCardEditor
}
};
private _computeHelperCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "aspect_ratio":
return typeof this._config?.grid_options?.rows === "number"
? this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.aspect_ratio_ignored`
)
: "";
default:
return "";
}
};
static styles: CSSResultGroup = configElementStyle;
}

View File

@ -1,11 +1,24 @@
import { mdiGestureTap } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { array, assert, assign, object, optional, string } from "superstruct";
import { mdiGestureTap } from "@mdi/js";
import memoizeOne from "memoize-one";
import {
array,
assert,
assign,
enums,
object,
optional,
string,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import type { ActionConfig } from "../../../../data/lovelace/config/action";
import type { HomeAssistant } from "../../../../types";
import type { PictureGlanceCardConfig } from "../../cards/types";
@ -26,78 +39,17 @@ const cardConfigStruct = assign(
image: optional(string()),
image_entity: optional(string()),
camera_image: optional(string()),
camera_view: optional(string()),
camera_view: optional(enums(["auto", "live"])),
aspect_ratio: optional(string()),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
entities: array(entitiesConfigStruct),
theme: optional(string()),
fit_mode: optional(enums(["cover", "contain", "fill"])),
})
);
const SCHEMA = [
{ name: "title", selector: { text: {} } },
{ name: "image", selector: { image: {} } },
{
name: "image_entity",
selector: { entity: { domain: ["image", "person"] } },
},
{ name: "camera_image", selector: { entity: { domain: "camera" } } },
{
name: "",
type: "grid",
schema: [
{
name: "camera_view",
selector: { select: { options: ["auto", "live"] } },
},
{ name: "aspect_ratio", selector: { text: {} } },
],
},
{ name: "entity", selector: { entity: {} } },
{ name: "theme", selector: { theme: {} } },
{
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "hold_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: [
{
name: "double_tap_action",
selector: {
ui_action: {
default_action: "none",
},
},
},
],
},
],
},
] as const;
@customElement("hui-picture-glance-card-editor")
export class HuiPictureGlanceCardEditor
extends LitElement
@ -109,6 +61,97 @@ export class HuiPictureGlanceCardEditor
@state() private _configEntities?: EntityConfig[];
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
[
{ name: "title", selector: { text: {} } },
{ name: "image", selector: { image: {} } },
{
name: "image_entity",
selector: { entity: { domain: ["image", "person"] } },
},
{ name: "camera_image", selector: { entity: { domain: "camera" } } },
{
name: "",
type: "grid",
schema: [
{
name: "camera_view",
required: true,
selector: {
select: {
options: ["auto", "live"].map((value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.generic.camera_view_options.${value}`
),
})),
mode: "dropdown",
},
},
},
{
name: "fit_mode",
required: true,
selector: {
select: {
options: ["cover", "contain", "fill"].map((value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.generic.fit_mode_options.${value}`
),
})),
mode: "dropdown",
},
},
},
{ name: "aspect_ratio", selector: { text: {} } },
],
},
{ name: "entity", selector: { entity: {} } },
{ name: "theme", selector: { theme: {} } },
{
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "hold_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: [
{
name: "double_tap_action",
selector: {
ui_action: {
default_action: "none",
},
},
},
],
},
],
},
] as const satisfies HaFormSchema[]
);
public setConfig(config: PictureGlanceCardConfig): void {
assert(config, cardConfigStruct);
this._config = config;
@ -128,14 +171,17 @@ export class HuiPictureGlanceCardEditor
return nothing;
}
const data = { camera_view: "auto", ...this._config };
const data = { camera_view: "auto", fit_mode: "cover", ...this._config };
const schema = this._schema(this.hass.localize);
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${SCHEMA}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback}
@value-changed=${this._valueChanged}
></ha-form>
<div class="card-config">
@ -164,7 +210,9 @@ export class HuiPictureGlanceCardEditor
fireEvent(this, "config-changed", { config: this._config });
}
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "theme":
case "tap_action":
@ -186,6 +234,21 @@ export class HuiPictureGlanceCardEditor
}
};
private _computeHelperCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "aspect_ratio":
return typeof this._config?.grid_options?.rows === "number"
? this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.aspect_ratio_ignored`
)
: "";
default:
return "";
}
};
static styles: CSSResultGroup = configElementStyle;
}

View File

@ -0,0 +1,55 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../components/ha-card";
import "../../components/ha-settings-row";
import "../../components/ha-switch";
import type { CoreFrontendUserData } from "../../data/frontend";
import { getOptimisticFrontendUserDataCollection } from "../../data/frontend";
import type { HomeAssistant } from "../../types";
@customElement("ha-entity-id-picker-row")
class EntityIdPickerRow extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public coreUserData?: CoreFrontendUserData;
protected render(): TemplateResult {
return html`
<ha-settings-row .narrow=${this.narrow}>
<span slot="heading">
${this.hass.localize("ui.panel.profile.entity_id_picker.title")}</span
>
<span slot="description">
${this.hass.localize("ui.panel.profile.entity_id_picker.description")}
</span>
<ha-switch
.checked=${this.coreUserData && this.coreUserData.showEntityIdPicker}
.disabled=${this.coreUserData === undefined}
@change=${this._toggled}
></ha-switch>
</ha-settings-row>
`;
}
private async _toggled(ev) {
getOptimisticFrontendUserDataCollection(this.hass.connection, "core").save({
...this.coreUserData,
showEntityIdPicker: ev.currentTarget.checked,
});
}
static styles = css`
a {
color: var(--primary-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-entity-id-picker-row": EntityIdPickerRow;
}
}

View File

@ -16,6 +16,7 @@ import { haStyle } from "../../resources/styles";
import type { HomeAssistant, Route } from "../../types";
import "./ha-advanced-mode-row";
import "./ha-enable-shortcuts-row";
import "./ha-entity-id-picker-row";
import "./ha-force-narrow-row";
import "./ha-pick-dashboard-row";
import "./ha-pick-first-weekday-row";
@ -156,6 +157,15 @@ class HaProfileSectionGeneral extends LitElement {
></ha-advanced-mode-row>
`
: ""}
${this.hass.user!.is_admin
? html`
<ha-entity-id-picker-row
.hass=${this.hass}
.narrow=${this.narrow}
.coreUserData=${this._coreUserData}
></ha-entity-id-picker-row>
`
: ""}
</ha-card>
<ha-card
.header=${this.hass.localize(

View File

@ -1106,7 +1106,15 @@
"only_https_supported": "You can only use your camera to scan a QR code when using HTTPS.",
"not_supported": "Your browser doesn't support QR scanning.",
"manual_input": "You can scan the QR code with another QR scanner and paste the code in the input below",
"enter_qr_code": "Enter QR code value"
"enter_qr_code": "Enter QR code value",
"retry": "Retry",
"wrong_code": "Wrong barcode scanned! {format}: {rawValue}, we need a QR code.",
"no_camera_found": "No camera found",
"app": {
"title": "Scan QR code",
"description": "Find the code on your device",
"alternativeOptionLabel": "More options"
}
},
"climate-control": {
"temperature_up": "Increase temperature",
@ -5652,7 +5660,6 @@
"back": "Back",
"add_node": "Add device",
"remove_node": "Remove device",
"reconfigure_server": "Re-configure server",
"rebuild_network_routes": "Rebuild network routes",
"in_progress_inclusion_exclusion": "Z-Wave JS is searching for devices",
"cancel_inclusion_exclusion": "Stop searching"
@ -5673,7 +5680,9 @@
"backup_failed": "Failed to download backup",
"restore_complete": "Backup restored",
"restore_failed": "Failed to restore backup",
"downloading": "Downloading backup"
"downloading": "Downloading backup",
"restoring": "Restoring backup",
"migrate": "Migrate controller"
},
"statistics": {
"title": "Controller statistics",
@ -5876,33 +5885,91 @@
},
"add_node": {
"title": "Add a Z-Wave device",
"searching_device": "Searching for devices…",
"follow_device_instructions": "Follow the directions that came with your device to trigger pairing on the device.",
"searching_devices": "Searching for devices",
"follow_device_instructions": "Follow the directions provided with your device to put it into pairing mode (sometimes called \"inclusion mode\")",
"security_options": "Advanced security options",
"choose_inclusion_strategy": "How do you want to add your device",
"qr_code": "QR Code",
"qr_code_paragraph": "If your device supports SmartStart you can scan the QR code for easy pairing.",
"scan_qr_code": "Scan QR code",
"add_device_failed": "Add device failed",
"inclusion_failed": "The device could not be added.",
"getting_device_information": "Getting device information",
"saving_device": "Saving device",
"check_logs": "Please check the logs for more information.",
"inclusion_finished": "The device has been added.",
"provisioning_finished": "The device has been added. Once you power it on, it will become available.",
"view_device": "View Device",
"interview_started": "The device is being interviewed. This may take some time.",
"interview_failed": "The device interview failed. Additional information may be available in the logs.",
"waiting_for_device": "Communicating with the device. Please wait.",
"adding_insecurely": "The device is being added insecurely",
"added_insecurely": "The device was added insecurely",
"added_insecurely_text": "There was an error during secure inclusion. You can try again by excluding the device and adding it again.",
"low_security_reason": {
"0": "Security bootstrapping was canceled by the user.",
"1": "The required security keys were not configured in the driver.",
"2": "No Security S2 user callbacks (or provisioning info) were provided to grant security classes and/or validate the DSK.",
"3": "An expected message was not received within the corresponding timeout.",
"4": "There was no possible match in encryption parameters between the controller and the node.",
"5": "Security bootstrapping was canceled by the included node.",
"6": "The PIN was incorrect, so the included node could not decode the key exchange commands.",
"7": "There was a mismatch in security keys between the controller and the node.",
"8": "Unknown error occurred."
"timeout_error": "No device found after {minutes} minutes. Please check the device and try again.",
"select_method": {
"webcam_unsupported": "You can only use your camera to scan a QR code when using a secure connection with the app or over HTTPS.",
"qr_code_webcam": "Scan QR code using webcam",
"qr_code_webcam_description": "Find and scan the code on your device.",
"qr_code_manual": "Enter QR code manually",
"qr_code_manual_description": "You can scan the QR code with another QR scanner and paste the code.",
"search_device": "Search for device",
"search_device_description": "Use this option to manually include a device that does not have a SmartStart QR code."
},
"qr": {
"manual": {
"title": "Enter QR code manually",
"text": "You can scan the QR code with another QR scanner and paste the code here.",
"placeholder": "Code"
},
"scan_code": "Scan QR code",
"other_add_options": "Other add options",
"invalid_code": "Invalid QR code ({code})",
"unsupported_code": "This QR code is not supported \"{code}\""
},
"specific_device": {
"title": "Searching for device",
"turn_on_device": "Turn on the device",
"turn_on_device_description": "If your device is already turned on, you might need to turn it off and on again. Consult your device manual if you are unsure.",
"add_another_z_wave_device": "Add another Z-Wave device",
"close_description": "Feel free to close this screen, as this process will continue in the background."
},
"select_strategy": {
"title": "Choose security strategy",
"default_label": "Secure if possible",
"default_description": "Requires user interaction during inclusion. Fast and secure with S2 when supported. Allows manually selecting which security keys to grant. Fallback to legacy S0 or no encryption when necessary.",
"s0_label": "Legacy Secure",
"s0_description": "Uses the older S0 security that is secure, but slow due to a lot of overhead. Allows securely including S2 capable devices which fail to be included with S2.",
"insecure_label": "Insecure",
"insecure_description": "Do not use encryption.",
"inclusion_strategy": "Inclusion strategy: {strategy}"
},
"configure_device": {
"title": "Add device",
"device_name": "Name",
"device_area": "Area",
"choose_network_type": "Choose network type",
"long_range_label": "Direct long range",
"long_range_description": "Direct connection to Home Assistant for extended coverage, without a mesh network.",
"mesh_label": "Mesh network",
"mesh_description": "Devices relay signals to each other, enhancing coverage and reliability.",
"add_device": "Add device",
"save_device_failed": "Saving new device info failed"
},
"validate_dsk_pin": {
"title": "Verify key",
"text": "Find the key on your device or manual and enter the 5-digit PIN.",
"placeholder": "PIN"
},
"added_insecure": {
"title": "Device added insecurely",
"text": "The device {name} has been added to your Z-Wave network.",
"added_insecurely_text": "{deviceName} has been added without secure inclusion. You can still use your device.",
"try_again_text": "You can try again by removing the device and adding it again.",
"view_device": "View device",
"low_security_reason": {
"0": "Security bootstrapping was canceled by the user.",
"1": "The required security keys were not configured in the driver.",
"2": "No Security S2 user callbacks (or provisioning info) were provided to grant security classes and/or validate the DSK.",
"3": "An expected message was not received within the corresponding timeout.",
"4": "There was no possible match in encryption parameters between the controller and the node.",
"5": "Security bootstrapping was canceled by the included node.",
"6": "The PIN was incorrect, so the included node could not decode the key exchange commands.",
"7": "There was a mismatch in security keys between the controller and the node.",
"8": "Unknown error occurred."
}
},
"grant_security_classes": {
"title": "Security classes",
"description": "The device has requested the following security classes"
}
},
"provisioned": {
@ -7127,13 +7194,24 @@
"generic": {
"alt_text": "Alternative text",
"aspect_ratio": "Aspect ratio",
"aspect_ratio_ignored": "Will be ignored because the card is resized.",
"attribute": "Attribute",
"camera_image": "Camera entity",
"image_entity": "Image entity",
"camera_view": "Camera view",
"camera_view_options": {
"auto": "Auto",
"live": "Live"
},
"double_tap_action": "Double tap behavior",
"entities": "Entities",
"entity": "Entity",
"fit_mode": "Fit mode",
"fit_mode_options": {
"contain": "Contain",
"cover": "Cover",
"fill": "Fill"
},
"hold_action": "Hold behavior",
"hours_to_show": "Hours to show",
"days_to_show": "Days to show",
@ -7848,6 +7926,10 @@
"description": "Unlocks advanced features.",
"link_promo": "Learn more"
},
"entity_id_picker": {
"title": "Display entity IDs in picker",
"description": "Shows the full entity IDs when selecting entities with a picker."
},
"refresh_tokens": {
"header": "Refresh tokens",
"description": "Each refresh token represents a login session. Refresh tokens will be automatically deleted when you log out. Unused refresh tokens will be automatically deleted after 90 days. The following refresh tokens are currently active for your account.",

292
yarn.lock
View File

@ -5001,15 +5001,15 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/eslint-plugin@npm:8.29.1":
version: 8.29.1
resolution: "@typescript-eslint/eslint-plugin@npm:8.29.1"
"@typescript-eslint/eslint-plugin@npm:8.30.0":
version: 8.30.0
resolution: "@typescript-eslint/eslint-plugin@npm:8.30.0"
dependencies:
"@eslint-community/regexpp": "npm:^4.10.0"
"@typescript-eslint/scope-manager": "npm:8.29.1"
"@typescript-eslint/type-utils": "npm:8.29.1"
"@typescript-eslint/utils": "npm:8.29.1"
"@typescript-eslint/visitor-keys": "npm:8.29.1"
"@typescript-eslint/scope-manager": "npm:8.30.0"
"@typescript-eslint/type-utils": "npm:8.30.0"
"@typescript-eslint/utils": "npm:8.30.0"
"@typescript-eslint/visitor-keys": "npm:8.30.0"
graphemer: "npm:^1.4.0"
ignore: "npm:^5.3.1"
natural-compare: "npm:^1.4.0"
@ -5018,64 +5018,64 @@ __metadata:
"@typescript-eslint/parser": ^8.0.0 || ^8.0.0-alpha.0
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <5.9.0"
checksum: 10/0568894f0ea50e67622605eb347d4e26a41571ad06234478ac695a097e9b3ff6252d6d60034853d7a6b8ec644b5c1d354be21075d691173dd7f2fbecb1102674
checksum: 10/55c7823bae8874e7aa5ff5a82f794e78c47f5ad6e17b021e8ca0bc25a8abf262bb824d0d16da760a24411e53abba0b5bbd159ab430d373dda4c84672edf954a9
languageName: node
linkType: hard
"@typescript-eslint/parser@npm:8.29.1":
version: 8.29.1
resolution: "@typescript-eslint/parser@npm:8.29.1"
"@typescript-eslint/parser@npm:8.30.0":
version: 8.30.0
resolution: "@typescript-eslint/parser@npm:8.30.0"
dependencies:
"@typescript-eslint/scope-manager": "npm:8.29.1"
"@typescript-eslint/types": "npm:8.29.1"
"@typescript-eslint/typescript-estree": "npm:8.29.1"
"@typescript-eslint/visitor-keys": "npm:8.29.1"
"@typescript-eslint/scope-manager": "npm:8.30.0"
"@typescript-eslint/types": "npm:8.30.0"
"@typescript-eslint/typescript-estree": "npm:8.30.0"
"@typescript-eslint/visitor-keys": "npm:8.30.0"
debug: "npm:^4.3.4"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <5.9.0"
checksum: 10/effb4cc24e375e4229e711b3ea8611a205bf81964cb487f518dd4a7d6cecde7324201ce4e2c3cd420e0ce7649899294307da2cd77f145af4efef3a0292189f20
checksum: 10/88cc1492f6ed7723ec5e3561229a7f7082da38b77e30f628a4215a453a204d2c6aa68361f2e1f56058479840d130d5141f0e30057e836fc2b619b773ef4d5e8c
languageName: node
linkType: hard
"@typescript-eslint/scope-manager@npm:8.29.1":
version: 8.29.1
resolution: "@typescript-eslint/scope-manager@npm:8.29.1"
"@typescript-eslint/scope-manager@npm:8.30.0":
version: 8.30.0
resolution: "@typescript-eslint/scope-manager@npm:8.30.0"
dependencies:
"@typescript-eslint/types": "npm:8.29.1"
"@typescript-eslint/visitor-keys": "npm:8.29.1"
checksum: 10/33a02f490b53436729f5ca2e6e0c5b8db72adb455274e5de43bdaada21033e7941aed1d92653321991e186af77f7794dc0ac35d2fce891cdf65a6d3fb192249e
"@typescript-eslint/types": "npm:8.30.0"
"@typescript-eslint/visitor-keys": "npm:8.30.0"
checksum: 10/511aec42d71cb0e38ff8fef12569d332304af308b9e57ddf9d11f9d6886094813170ff9d29dd79575c25d7b2a0f09b1e74a20f4548a243c38259944f26188ff8
languageName: node
linkType: hard
"@typescript-eslint/type-utils@npm:8.29.1":
version: 8.29.1
resolution: "@typescript-eslint/type-utils@npm:8.29.1"
"@typescript-eslint/type-utils@npm:8.30.0":
version: 8.30.0
resolution: "@typescript-eslint/type-utils@npm:8.30.0"
dependencies:
"@typescript-eslint/typescript-estree": "npm:8.29.1"
"@typescript-eslint/utils": "npm:8.29.1"
"@typescript-eslint/typescript-estree": "npm:8.30.0"
"@typescript-eslint/utils": "npm:8.30.0"
debug: "npm:^4.3.4"
ts-api-utils: "npm:^2.0.1"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <5.9.0"
checksum: 10/3774d6fb32c058b3fa607480e5603fdd2919e07d8babbc00aa703f31c6fd6f7fbc25c25ff246e7e6ac6b77ad0e0b66838a7136aebc31503a58fbe509f68ab2f4
checksum: 10/dee013705224abdd4f0f84f8de00bb0f70f3e435d116e6f7dc6fbdce92eaec160e9c61faf25ce51945137f0b75e00e4b5304adaf8289049620cfec3d4c966a1d
languageName: node
linkType: hard
"@typescript-eslint/types@npm:8.29.1":
version: 8.29.1
resolution: "@typescript-eslint/types@npm:8.29.1"
checksum: 10/99ff59e7af3728858af9b7fdc0165955bcf5cd2eefeaafabfbdd7951fbc8ad869cbb7bed7bcbed6c3c50a8661333b33364dc1de0a0c8f3c64d5882339a5d30dd
"@typescript-eslint/types@npm:8.30.0":
version: 8.30.0
resolution: "@typescript-eslint/types@npm:8.30.0"
checksum: 10/c7b01c3d78db145cffcb3703e432fc65758cc140ae442422f9fbd9aea0e2d2d86d210c656bf3767904af8c541c73eb36a745727473493e691195245eee19b03c
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:8.29.1":
version: 8.29.1
resolution: "@typescript-eslint/typescript-estree@npm:8.29.1"
"@typescript-eslint/typescript-estree@npm:8.30.0":
version: 8.30.0
resolution: "@typescript-eslint/typescript-estree@npm:8.30.0"
dependencies:
"@typescript-eslint/types": "npm:8.29.1"
"@typescript-eslint/visitor-keys": "npm:8.29.1"
"@typescript-eslint/types": "npm:8.30.0"
"@typescript-eslint/visitor-keys": "npm:8.30.0"
debug: "npm:^4.3.4"
fast-glob: "npm:^3.3.2"
is-glob: "npm:^4.0.3"
@ -5084,160 +5084,160 @@ __metadata:
ts-api-utils: "npm:^2.0.1"
peerDependencies:
typescript: ">=4.8.4 <5.9.0"
checksum: 10/dded2ebe4c3287443000e3b825e673d0eddb7c48eb2d373c5b7059ea7dbbeba488d7f1de2e42ed7a9299ccff926a65821f2b5594022b49564026ba01c0cc07ab
checksum: 10/1cb95ba8001ed8e90c9bdddc1c852c8e7244a4cd4f332171636f62dd98fe170b1652cd8debb90569c21b0f7dfffd2550ad1ad19c3e8d8fe095b7f09dfd4e6971
languageName: node
linkType: hard
"@typescript-eslint/utils@npm:8.29.1":
version: 8.29.1
resolution: "@typescript-eslint/utils@npm:8.29.1"
"@typescript-eslint/utils@npm:8.30.0":
version: 8.30.0
resolution: "@typescript-eslint/utils@npm:8.30.0"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.4.0"
"@typescript-eslint/scope-manager": "npm:8.29.1"
"@typescript-eslint/types": "npm:8.29.1"
"@typescript-eslint/typescript-estree": "npm:8.29.1"
"@typescript-eslint/scope-manager": "npm:8.30.0"
"@typescript-eslint/types": "npm:8.30.0"
"@typescript-eslint/typescript-estree": "npm:8.30.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <5.9.0"
checksum: 10/1d2c85c97a39e063fe490c0cdb6513716e4735bda0bc937475b44d9224074a5070f888dfed1e75f4a91c8f4d825b44fce90b685e07bda391dfeb2553b9b39a5a
checksum: 10/20a6134584e035f7047248d6e16c543863375a21337a3e0fc6518fa42978da9fe8f49f2c2cbb6940a3d298d2b7cb1dcd38e69cc62869728c634acd9722c14b8b
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:8.29.1":
version: 8.29.1
resolution: "@typescript-eslint/visitor-keys@npm:8.29.1"
"@typescript-eslint/visitor-keys@npm:8.30.0":
version: 8.30.0
resolution: "@typescript-eslint/visitor-keys@npm:8.30.0"
dependencies:
"@typescript-eslint/types": "npm:8.29.1"
"@typescript-eslint/types": "npm:8.30.0"
eslint-visitor-keys: "npm:^4.2.0"
checksum: 10/788290c369c13403692d857e0a464b5c2223e1fef28c717b7dbc61140d33697ce43c7d1a7cb7ac585f2012fc702ce2ec489363d34bf566303d3c48fa494803c5
checksum: 10/b8c843bb0b07bb09a49b53574627ac81b5044cdaa5c58cdcf502528384613493132f05ae7ab82a7477fbb75a6520b4b4b2b638e45c37dadb39f51c9356036d85
languageName: node
linkType: hard
"@vaadin/a11y-base@npm:~24.7.2":
version: 24.7.2
resolution: "@vaadin/a11y-base@npm:24.7.2"
"@vaadin/a11y-base@npm:~24.7.3":
version: 24.7.3
resolution: "@vaadin/a11y-base@npm:24.7.3"
dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0"
"@polymer/polymer": "npm:^3.0.0"
"@vaadin/component-base": "npm:~24.7.2"
"@vaadin/component-base": "npm:~24.7.3"
lit: "npm:^3.0.0"
checksum: 10/f208a92ae3cdb6584b1491a995e01c5da87c6c533a49d7599f1ea451a18ad094f0d14f3b45b4070d298d435b46c42077cb474bfc08f38a45ce4b7d57ca860479
checksum: 10/a3d2e68ea3ee48e1974c2e6691ae8d09dae0fb0f19e6180e398e3cecb5e89d8d5fb57187f30a835a3c2b98ae4dffdffab1de00ffa9228c4389de4499cbebb4a5
languageName: node
linkType: hard
"@vaadin/combo-box@npm:24.7.2":
version: 24.7.2
resolution: "@vaadin/combo-box@npm:24.7.2"
"@vaadin/combo-box@npm:24.7.3":
version: 24.7.3
resolution: "@vaadin/combo-box@npm:24.7.3"
dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0"
"@polymer/polymer": "npm:^3.0.0"
"@vaadin/a11y-base": "npm:~24.7.2"
"@vaadin/component-base": "npm:~24.7.2"
"@vaadin/field-base": "npm:~24.7.2"
"@vaadin/input-container": "npm:~24.7.2"
"@vaadin/item": "npm:~24.7.2"
"@vaadin/lit-renderer": "npm:~24.7.2"
"@vaadin/overlay": "npm:~24.7.2"
"@vaadin/vaadin-lumo-styles": "npm:~24.7.2"
"@vaadin/vaadin-material-styles": "npm:~24.7.2"
"@vaadin/vaadin-themable-mixin": "npm:~24.7.2"
"@vaadin/a11y-base": "npm:~24.7.3"
"@vaadin/component-base": "npm:~24.7.3"
"@vaadin/field-base": "npm:~24.7.3"
"@vaadin/input-container": "npm:~24.7.3"
"@vaadin/item": "npm:~24.7.3"
"@vaadin/lit-renderer": "npm:~24.7.3"
"@vaadin/overlay": "npm:~24.7.3"
"@vaadin/vaadin-lumo-styles": "npm:~24.7.3"
"@vaadin/vaadin-material-styles": "npm:~24.7.3"
"@vaadin/vaadin-themable-mixin": "npm:~24.7.3"
lit: "npm:^3.0.0"
checksum: 10/a5218abc97d9974e2481e7735bd8de08fe36d461a19e298dceaf66b5b2016d7c8cc9c1aa37391bf0be5c7e6e7d59797483d776acd8a87f21c1364cf1ac2e528f
checksum: 10/fe559fac266a317b49b4a33031e9193c6e8d396592148e04ca36363bf138ed45323716d0bce0d0efe7b1a47feb008c16e0bf24a2fe4e61abd9f7d1fe6144b767
languageName: node
linkType: hard
"@vaadin/component-base@npm:~24.7.2":
version: 24.7.2
resolution: "@vaadin/component-base@npm:24.7.2"
"@vaadin/component-base@npm:~24.7.3":
version: 24.7.3
resolution: "@vaadin/component-base@npm:24.7.3"
dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0"
"@polymer/polymer": "npm:^3.0.0"
"@vaadin/vaadin-development-mode-detector": "npm:^2.0.0"
"@vaadin/vaadin-usage-statistics": "npm:^2.1.0"
lit: "npm:^3.0.0"
checksum: 10/9bdb2109e0b795fbe44b0b4848a102e064d902aeae383d25e0fc1c1ec334f9973985cb5697d9e6f1695a94de9729fedf20e058b6cbc63afe36b85cb26a7e209d
checksum: 10/11540081b0b966a970784edaace6ac16fcc3c651439f37ec19e79440850df381c94f8118b0e447650b8ca8ba56445c30a1f4aefb8d5d009936c1b6ca707fd46c
languageName: node
linkType: hard
"@vaadin/field-base@npm:~24.7.2":
version: 24.7.2
resolution: "@vaadin/field-base@npm:24.7.2"
"@vaadin/field-base@npm:~24.7.3":
version: 24.7.3
resolution: "@vaadin/field-base@npm:24.7.3"
dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0"
"@polymer/polymer": "npm:^3.0.0"
"@vaadin/a11y-base": "npm:~24.7.2"
"@vaadin/component-base": "npm:~24.7.2"
"@vaadin/a11y-base": "npm:~24.7.3"
"@vaadin/component-base": "npm:~24.7.3"
lit: "npm:^3.0.0"
checksum: 10/18d72417e8cef2c345fb6098d17f8b59c7fd9e99140ee05927d793d84c0e76786041ebed321d14fe0b5861d496b9a687e60fdf3a80f77963640ea69944501970
checksum: 10/97ef6c9eea49962b2c4526eba4dedc24de67ba2df57d469ff82e449488918165e42b5e98fa212dba82e0fe1e9623889b6a22e0db3442e045caec6cee371c12d4
languageName: node
linkType: hard
"@vaadin/icon@npm:~24.7.2":
version: 24.7.2
resolution: "@vaadin/icon@npm:24.7.2"
"@vaadin/icon@npm:~24.7.3":
version: 24.7.3
resolution: "@vaadin/icon@npm:24.7.3"
dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0"
"@polymer/polymer": "npm:^3.0.0"
"@vaadin/component-base": "npm:~24.7.2"
"@vaadin/vaadin-lumo-styles": "npm:~24.7.2"
"@vaadin/vaadin-themable-mixin": "npm:~24.7.2"
"@vaadin/component-base": "npm:~24.7.3"
"@vaadin/vaadin-lumo-styles": "npm:~24.7.3"
"@vaadin/vaadin-themable-mixin": "npm:~24.7.3"
lit: "npm:^3.0.0"
checksum: 10/7e86cfe73e4ffd24c339f7c24616885b58593a501d8b6e908123dc89623558a676512dd49546091ac511250cf24c54d5abfe1fc5e2e67fdb4d388d4a2941df29
checksum: 10/a3da83bc9effd9eabd7382e4d4fdea1bb412c8fe09bcfc2c17db37184d856c6058e36f5f1bfab25b1c726ed5328a8af338405fc6b5b11b9aff5c6824e282173b
languageName: node
linkType: hard
"@vaadin/input-container@npm:~24.7.2":
version: 24.7.2
resolution: "@vaadin/input-container@npm:24.7.2"
"@vaadin/input-container@npm:~24.7.3":
version: 24.7.3
resolution: "@vaadin/input-container@npm:24.7.3"
dependencies:
"@polymer/polymer": "npm:^3.0.0"
"@vaadin/component-base": "npm:~24.7.2"
"@vaadin/vaadin-lumo-styles": "npm:~24.7.2"
"@vaadin/vaadin-material-styles": "npm:~24.7.2"
"@vaadin/vaadin-themable-mixin": "npm:~24.7.2"
"@vaadin/component-base": "npm:~24.7.3"
"@vaadin/vaadin-lumo-styles": "npm:~24.7.3"
"@vaadin/vaadin-material-styles": "npm:~24.7.3"
"@vaadin/vaadin-themable-mixin": "npm:~24.7.3"
lit: "npm:^3.0.0"
checksum: 10/f5f1d396ad14798c84d5db4915e3bda99c80067255e462fec8bad3a2163ec7420f7571cfca5121b6c5625c3bfe01c1ca3f17d2552689cd7dc383e04a7e93d745
checksum: 10/61f2293927a22396480bd88f84565651146f485a5d9b73ab8a7607931220d5eccf9f87ce208feb6156e716462b4aeb3d03877f13d4229d62a3c60468686973e7
languageName: node
linkType: hard
"@vaadin/item@npm:~24.7.2":
version: 24.7.2
resolution: "@vaadin/item@npm:24.7.2"
"@vaadin/item@npm:~24.7.3":
version: 24.7.3
resolution: "@vaadin/item@npm:24.7.3"
dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0"
"@polymer/polymer": "npm:^3.0.0"
"@vaadin/a11y-base": "npm:~24.7.2"
"@vaadin/component-base": "npm:~24.7.2"
"@vaadin/vaadin-lumo-styles": "npm:~24.7.2"
"@vaadin/vaadin-material-styles": "npm:~24.7.2"
"@vaadin/vaadin-themable-mixin": "npm:~24.7.2"
"@vaadin/a11y-base": "npm:~24.7.3"
"@vaadin/component-base": "npm:~24.7.3"
"@vaadin/vaadin-lumo-styles": "npm:~24.7.3"
"@vaadin/vaadin-material-styles": "npm:~24.7.3"
"@vaadin/vaadin-themable-mixin": "npm:~24.7.3"
lit: "npm:^3.0.0"
checksum: 10/5ca44c455382cb56cd81757b51ac1bbc7c53c97807ca5868f7f1b9e9100ea274772590de55d910b16a1d6771c09d11a4b5fad5cd64b44dd0a73265f286ef16ac
checksum: 10/bb6d7a9576d8204b20b44d6f672f650c227893869a88dc59fb27f5f5b40b21e172523990d6ff90bbdd2324cca4e62a1d711eb27a0971ea91377d94aa89cc72a6
languageName: node
linkType: hard
"@vaadin/lit-renderer@npm:~24.7.2":
version: 24.7.2
resolution: "@vaadin/lit-renderer@npm:24.7.2"
"@vaadin/lit-renderer@npm:~24.7.3":
version: 24.7.3
resolution: "@vaadin/lit-renderer@npm:24.7.3"
dependencies:
lit: "npm:^3.0.0"
checksum: 10/aba4cab64a8249e2386902c5574fcec5f0205f818e7f9120494826b68f4d13dee78f916a6987bfc6ce969d37641ccec251d744dcbe6a79412c87e7e7fc6faf7f
checksum: 10/323fb6c79c6857019db94634e218bf9cc9c7e454ea28ae83c9be0ca70b34cc6cf57e0b931167e05c6387cc76fcd69a1e2b6fce04181020c919dcfdd072d4baae
languageName: node
linkType: hard
"@vaadin/overlay@npm:~24.7.2":
version: 24.7.2
resolution: "@vaadin/overlay@npm:24.7.2"
"@vaadin/overlay@npm:~24.7.3":
version: 24.7.3
resolution: "@vaadin/overlay@npm:24.7.3"
dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0"
"@polymer/polymer": "npm:^3.0.0"
"@vaadin/a11y-base": "npm:~24.7.2"
"@vaadin/component-base": "npm:~24.7.2"
"@vaadin/vaadin-lumo-styles": "npm:~24.7.2"
"@vaadin/vaadin-material-styles": "npm:~24.7.2"
"@vaadin/vaadin-themable-mixin": "npm:~24.7.2"
"@vaadin/a11y-base": "npm:~24.7.3"
"@vaadin/component-base": "npm:~24.7.3"
"@vaadin/vaadin-lumo-styles": "npm:~24.7.3"
"@vaadin/vaadin-material-styles": "npm:~24.7.3"
"@vaadin/vaadin-themable-mixin": "npm:~24.7.3"
lit: "npm:^3.0.0"
checksum: 10/79a49e4db54a757a5d714ecb9761b912aff93abde7fd97c50af16f8e63eedbcefac5d00bc23b8e2431656befcc3258ee827191cd3ec6c179e6444a5b028c70ff
checksum: 10/1a14d94e77ffe68c6d62d5c576b4a6049c2c180b74c9b9114eee5c77f49aacded28ef5a1452938ee7f89151f7db292978156b523ac1c727edf3a464f2eda5b30
languageName: node
linkType: hard
@ -5248,36 +5248,36 @@ __metadata:
languageName: node
linkType: hard
"@vaadin/vaadin-lumo-styles@npm:~24.7.2":
version: 24.7.2
resolution: "@vaadin/vaadin-lumo-styles@npm:24.7.2"
"@vaadin/vaadin-lumo-styles@npm:~24.7.3":
version: 24.7.3
resolution: "@vaadin/vaadin-lumo-styles@npm:24.7.3"
dependencies:
"@polymer/polymer": "npm:^3.0.0"
"@vaadin/component-base": "npm:~24.7.2"
"@vaadin/icon": "npm:~24.7.2"
"@vaadin/vaadin-themable-mixin": "npm:~24.7.2"
checksum: 10/3c89b55c34f9b360bb52f2ed1d7716bc4caf69ca53f8643b4cf0195000b05a945e3b37476716afe9dc59e41676297a2d31380f8cfb3ea6df2f973740f06cb701
"@vaadin/component-base": "npm:~24.7.3"
"@vaadin/icon": "npm:~24.7.3"
"@vaadin/vaadin-themable-mixin": "npm:~24.7.3"
checksum: 10/8a4d14ac3ffe6109c517462e16cb72aebdd7b277c88a96eda6f85925bb9a9d8584753a33e06d64486e2c0d427c339bebaa12cd3b4fe3f9cbb8c19cd716c6c96a
languageName: node
linkType: hard
"@vaadin/vaadin-material-styles@npm:~24.7.2":
version: 24.7.2
resolution: "@vaadin/vaadin-material-styles@npm:24.7.2"
"@vaadin/vaadin-material-styles@npm:~24.7.3":
version: 24.7.3
resolution: "@vaadin/vaadin-material-styles@npm:24.7.3"
dependencies:
"@polymer/polymer": "npm:^3.0.0"
"@vaadin/component-base": "npm:~24.7.2"
"@vaadin/vaadin-themable-mixin": "npm:~24.7.2"
checksum: 10/da94da8155b61cd60acbc8cba81e8ca83e07a5443867c1e2a8177fc938998e0d91aa689a94d104bf8f43523f64a7780c8a5f1f606447a8fca8771bc7c2beca75
"@vaadin/component-base": "npm:~24.7.3"
"@vaadin/vaadin-themable-mixin": "npm:~24.7.3"
checksum: 10/554652d87b41a0090b91c337a495d88e3bca4cdc1daeb2357a413fcb581a761fcba98db00344a5c0df851cb98993758ec03f22f008e38442def4d2378562cf84
languageName: node
linkType: hard
"@vaadin/vaadin-themable-mixin@npm:24.7.2, @vaadin/vaadin-themable-mixin@npm:~24.7.2":
version: 24.7.2
resolution: "@vaadin/vaadin-themable-mixin@npm:24.7.2"
"@vaadin/vaadin-themable-mixin@npm:24.7.3, @vaadin/vaadin-themable-mixin@npm:~24.7.3":
version: 24.7.3
resolution: "@vaadin/vaadin-themable-mixin@npm:24.7.3"
dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0"
lit: "npm:^3.0.0"
checksum: 10/239a1ac07f9b08d527391e89d31fb2a368cabf3bca0cbccde06eed86c050b732b2de6182740328f63769744cdcfcceb0cb15156770811967fb19170d5e9bf9b6
checksum: 10/e176e162fdb374ab4aa5f948f0a4b23f7fa22f617e2fa7b6c73b15df84b57e910d16f2c54add434be30a337313d9a5874945a97c3a86d4989b308a157a8fe4ce
languageName: node
linkType: hard
@ -9455,8 +9455,8 @@ __metadata:
"@types/tar": "npm:6.1.13"
"@types/ua-parser-js": "npm:0.7.39"
"@types/webspeechapi": "npm:0.0.29"
"@vaadin/combo-box": "npm:24.7.2"
"@vaadin/vaadin-themable-mixin": "npm:24.7.2"
"@vaadin/combo-box": "npm:24.7.3"
"@vaadin/vaadin-themable-mixin": "npm:24.7.3"
"@vibrant/color": "npm:4.0.0"
"@vitest/coverage-v8": "npm:3.1.1"
"@vue/web-component-wrapper": "npm:1.3.0"
@ -9540,7 +9540,7 @@ __metadata:
tinykeys: "npm:3.0.0"
ts-lit-plugin: "npm:2.0.2"
typescript: "npm:5.8.3"
typescript-eslint: "npm:8.29.1"
typescript-eslint: "npm:8.30.0"
ua-parser-js: "npm:2.0.3"
vis-data: "npm:7.1.9"
vis-network: "npm:9.1.9"
@ -9709,8 +9709,8 @@ __metadata:
linkType: hard
"http-proxy-middleware@npm:^2.0.7":
version: 2.0.7
resolution: "http-proxy-middleware@npm:2.0.7"
version: 2.0.9
resolution: "http-proxy-middleware@npm:2.0.9"
dependencies:
"@types/http-proxy": "npm:^1.17.8"
http-proxy: "npm:^1.18.1"
@ -9722,7 +9722,7 @@ __metadata:
peerDependenciesMeta:
"@types/express":
optional: true
checksum: 10/4a51bf612b752ad945701995c1c029e9501c97e7224c0cf3f8bf6d48d172d6a8f2b57c20fec469534fdcac3aa8a6f332224a33c6b0d7f387aa2cfff9b67216fd
checksum: 10/4ece416a91d52e96f8136c5f4abfbf7ac2f39becbad21fa8b158a12d7e7d8f808287ff1ae342b903fd1f15f2249dee87fabc09e1f0e73106b83331c496d67660
languageName: node
linkType: hard
@ -14619,17 +14619,17 @@ __metadata:
languageName: node
linkType: hard
"typescript-eslint@npm:8.29.1":
version: 8.29.1
resolution: "typescript-eslint@npm:8.29.1"
"typescript-eslint@npm:8.30.0":
version: 8.30.0
resolution: "typescript-eslint@npm:8.30.0"
dependencies:
"@typescript-eslint/eslint-plugin": "npm:8.29.1"
"@typescript-eslint/parser": "npm:8.29.1"
"@typescript-eslint/utils": "npm:8.29.1"
"@typescript-eslint/eslint-plugin": "npm:8.30.0"
"@typescript-eslint/parser": "npm:8.30.0"
"@typescript-eslint/utils": "npm:8.30.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <5.9.0"
checksum: 10/0073f809d04586ca09a482ac1becfdc16bc76c8a1828954de1886e0a9f3a75350fd7d65cc5f482eb1524f721dd71457496fee1c59c07de1f7f29e135db005252
checksum: 10/e44afc64e14751684127999e149d30582f68c9bd5a09945260e7b5bacaa4c025e932c82eb23dc51a219a4d1c3715ec6b42d4363e6069c0d921c64e1c0f1472a1
languageName: node
linkType: hard