Compare commits

...

8 Commits

Author SHA1 Message Date
dependabot[bot]
2c136e00f5 Bump tar from 7.5.7 to 7.5.8 (#29735)
* Bump tar from 7.5.7 to 7.5.8

Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.7 to 7.5.8.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.7...v7.5.8)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.8
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

* dedupe

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-02-20 07:05:32 +00:00
RoboMagus
6f82478598 Fix header tab height (#29736)
Fix header tabs to header height
2026-02-20 08:48:43 +02:00
Aidan Timson
1093bd890f Add missing helper to ha-select, remove unused attr (#29729) 2026-02-19 18:54:29 +01:00
Aidan Timson
456c638750 Use ha-scrollbar in config dashboard (#29724)
* Use ha-scrollbar in config dashboard

* Remove padding

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Add padding to bottom

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-19 18:52:32 +01:00
Aidan Timson
60ca50deb4 Add a drag handle visual indicator to bottom sheet (#29707)
* Add drag handle to bottom sheet

* Remove locks

* Fix rounded corners

* Restore original functionality, keep visual indicator

* Add padding to combo box

* Apply suggestion from @wendevlin

* Fix prettier

* Shorter height

Co-authored-by: Marcin Bauer <marcinbauer85@gmail.com>

* Half width

Co-authored-by: Marcin Bauer <marcinbauer85@gmail.com>

* Restore after rebase

* Reduce space for picker

---------

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
Co-authored-by: Wendelin <w@pe8.at>
Co-authored-by: Marcin Bauer <marcinbauer85@gmail.com>
2026-02-19 14:24:09 +01:00
Matthias de Baat
2064ab4141 Reorganize Z-Wave settings page (#29697)
* Reorganize ZWave settings

* Next iteration

* Made more consistent with Zigbee settings page

* Update text

* Updates on the provisioned devices page

* Add identifier when you have multiple networks

* Update to force remove button

* Update button text

* Update rebuild text

* Update remove foreign device button text
2026-02-19 13:45:35 +02:00
karwosts
d34c42e587 Refine supported actions in button heading badge (#29718) 2026-02-19 12:49:30 +02:00
Joakim Sørensen
5da7bf6fba Add repository handling for missing addons in HaConfigAppDashboard (#29722)
* Add repository handling for missing addons in HaConfigAppDashboard

* Implement feedback

* More adjustments

* minor adjustment
2026-02-19 10:36:34 +00:00
23 changed files with 1518 additions and 748 deletions

View File

@@ -210,7 +210,7 @@
"rspack-manifest-plugin": "5.2.1",
"serve": "14.2.5",
"sinon": "21.0.1",
"tar": "7.5.7",
"tar": "7.5.8",
"terser-webpack-plugin": "5.3.16",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.3",

View File

@@ -220,6 +220,7 @@ export class HaAdaptiveDialog extends LitElement {
return [
css`
ha-bottom-sheet {
--ha-bottom-sheet-border-radius: var(--ha-border-radius-2xl);
--ha-bottom-sheet-surface-background: var(
--ha-dialog-surface-background,
var(--card-background-color, var(--ha-color-surface-default))

View File

@@ -197,6 +197,9 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
without-header
@touchstart=${this._handleTouchStart}
>
<div class="handle-wrapper" aria-hidden="true">
<div class="handle"></div>
</div>
<slot name="header"></slot>
<div class="content-wrapper">
<div id="body" class="body ha-scrollbar">
@@ -372,6 +375,7 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
wa-drawer::part(body) {
max-width: var(--ha-bottom-sheet-max-width);
width: 100%;
position: relative;
border-top-left-radius: var(
--ha-bottom-sheet-border-radius,
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
@@ -394,6 +398,35 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
display: flex;
flex-direction: column;
}
:host([prevent-scrim-close]) .handle-wrapper {
display: none;
}
.handle-wrapper {
position: absolute;
top: 0;
inset-inline-start: 0;
width: 100%;
padding-bottom: 2px;
display: flex;
justify-content: center;
align-items: center;
pointer-events: none;
z-index: 1;
}
.handle-wrapper .handle {
height: 16px;
width: 200px;
display: flex;
justify-content: center;
align-items: center;
}
.handle-wrapper .handle::after {
content: "";
border-radius: var(--ha-border-radius-md);
height: 4px;
background: var(--ha-bottom-sheet-handle-color, var(--divider-color));
width: 40px;
}
.content-wrapper {
position: relative;
flex: 1;

View File

@@ -194,7 +194,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
.image=${this.image}
.label=${label}
.placeholder=${this.placeholder}
.helper=${this.helper}
.value=${this.value}
.valueRenderer=${this.valueRenderer}
.required=${this.required}

View File

@@ -796,7 +796,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
:host {
display: flex;
flex-direction: column;
padding-top: var(--ha-space-3);
padding-top: var(--ha-space-4);
flex: 1;
}

View File

@@ -126,6 +126,7 @@ export class HaPickerField extends PickerMixin(LitElement) {
);
}
ha-combo-box-item {
position: relative;
background-color: var(--mdc-text-field-fill-color, whitesmoke);
border-radius: var(--ha-border-radius-sm);
border-end-end-radius: 0;

View File

@@ -5,6 +5,7 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-dropdown";
import "./ha-dropdown-item";
import "./ha-input-helper-text";
import "./ha-picker-field";
import type { HaPickerField } from "./ha-picker-field";
import "./ha-svg-icon";
@@ -75,7 +76,7 @@ export class HaSelect extends LitElement {
protected override render() {
if (this.disabled) {
return this._renderField();
return html`${this._renderField()}${this._renderHelper()}`;
}
return html`
@@ -116,6 +117,7 @@ export class HaSelect extends LitElement {
)
: html`<slot></slot>`}
</ha-dropdown>
${this._renderHelper()}
`;
}
@@ -131,7 +133,6 @@ export class HaSelect extends LitElement {
aria-label=${ifDefined(this.label)}
@clear=${this._clearValue}
.label=${this.label}
.helper=${this.helper}
.value=${valueLabel}
.required=${this.required}
.disabled=${this.disabled}
@@ -144,6 +145,14 @@ export class HaSelect extends LitElement {
`;
}
private _renderHelper() {
return this.helper
? html`<ha-input-helper-text .disabled=${this.disabled}
>${this.helper}</ha-input-helper-text
>`
: nothing;
}
private _handleSelect(ev: CustomEvent<{ item: { value: string } }>) {
ev.stopPropagation();
const value = ev.detail.item.value;
@@ -194,6 +203,11 @@ export class HaSelect extends LitElement {
ha-dropdown::part(menu) {
min-width: var(--select-menu-width);
}
ha-input-helper-text {
display: block;
margin: var(--ha-space-2) 0 0;
}
`;
}
declare global {

View File

@@ -9,10 +9,16 @@ import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate";
import { extractSearchParam } from "../../../common/url/search-params";
import type { HassioAddonDetails } from "../../../data/hassio/addon";
import { fetchHassioAddonInfo } from "../../../data/hassio/addon";
import { extractApiErrorMessage } from "../../../data/hassio/common";
import {
addStoreRepository,
fetchSupervisorStore,
} from "../../../data/supervisor/store";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage";
@@ -39,6 +45,8 @@ class HaConfigAppDashboard extends LitElement {
@state() private _fromStore = false;
@state() private _loading = true;
private _computeTail = memoizeOne((route: Route) => {
const pathParts = route.path.split("/").filter(Boolean);
// Path is like /<slug>/info or /<slug>/config
@@ -53,8 +61,15 @@ class HaConfigAppDashboard extends LitElement {
protected async firstUpdated(): Promise<void> {
this._fromStore = extractSearchParam("store") === "true";
await this._loadAddon();
const repositoryUrl = extractSearchParam("repository_url");
if (repositoryUrl) {
navigate(`/config/app/${this.route.path.split("/")[1]}`, {
replace: true,
});
}
await this._loadAddon(repositoryUrl);
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
this._loading = false;
}
protected updated(changedProperties: PropertyValues) {
@@ -63,7 +78,7 @@ class HaConfigAppDashboard extends LitElement {
const oldSlug = oldRoute?.path.split("/")[1];
const newSlug = this.route.path.split("/")[1];
if (oldSlug !== newSlug && newSlug) {
if (oldSlug !== newSlug && newSlug && !this._loading) {
this._loadAddon();
}
}
@@ -138,7 +153,7 @@ class HaConfigAppDashboard extends LitElement {
`;
}
private async _loadAddon(): Promise<void> {
private async _loadAddon(repositoryUrl?: string | null): Promise<void> {
const slug = this.route.path.split("/")[1];
if (!slug) {
this._error = "No addon specified";
@@ -148,10 +163,56 @@ class HaConfigAppDashboard extends LitElement {
try {
this._addon = await fetchHassioAddonInfo(this.hass, slug);
} catch (err: any) {
this._error = `Error loading addon: ${extractApiErrorMessage(err)}`;
if (repositoryUrl) {
try {
await this._handleMissingRepository(slug, repositoryUrl);
if (this._addon) {
// Clear error if we successfully added the repository and loaded the addon
this._error = undefined;
return;
}
} catch (addRepoErr: any) {
this._error = extractApiErrorMessage(addRepoErr);
return;
}
}
this._error = `Error loading app: ${extractApiErrorMessage(err)}`;
}
}
private async _handleMissingRepository(
slug: string,
repositoryUrl: string
): Promise<void> {
const storeInfo = await fetchSupervisorStore(this.hass);
if (storeInfo.repositories.some((repo) => repo.source === repositoryUrl)) {
// Repository is already installed, addon just doesn't exist
return;
}
if (
!(await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.apps.my.add_repository_title"
),
text: this.hass.localize(
"ui.panel.config.apps.my.add_repository_description",
{ repository: repositoryUrl }
),
confirmText: this.hass.localize("ui.common.add"),
dismissText: this.hass.localize("ui.common.cancel"),
}))
) {
this._error = this.hass.localize(
"ui.panel.config.apps.my.error_repository_not_found"
);
return;
}
await addStoreRepository(this.hass, repositoryUrl);
this._addon = await fetchHassioAddonInfo(this.hass, slug);
}
private async _apiCalled(ev): Promise<void> {
if (!ev.detail.success) {
return;

View File

@@ -2,17 +2,26 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { extractSearchParam } from "../../../common/url/search-params";
import "../../../components/ha-analytics";
import "../../../components/ha-card";
import "../../../components/ha-settings-row";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-spinner";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import type { Analytics } from "../../../data/analytics";
import {
getAnalyticsDetails,
setAnalyticsPreferences,
} from "../../../data/analytics";
import { getConfigEntries } from "../../../data/config_entries";
import type { LabPreviewFeature } from "../../../data/labs";
import { subscribeLabFeature } from "../../../data/labs";
import {
fetchZwaveDataCollectionStatus,
setZwaveDataCollectionPreference,
} from "../../../data/zwave_js";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
@@ -28,6 +37,12 @@ class ConfigAnalytics extends SubscribeMixin(LitElement) {
@state() private _snapshotsLabEnabled = false;
@state() private _zwaveEntryId?: string;
@state() private _zwaveDataCollectionOptIn?: boolean;
@state() private _highlightedSection?: string;
protected render(): TemplateResult {
const error = this._error
? this._error
@@ -83,25 +98,77 @@ class ConfigAnalytics extends SubscribeMixin(LitElement) {
}
)}
</p>
<ha-settings-row>
<span slot="heading" data-for="snapshots">
${this.hass.localize(
`ui.panel.config.analytics.preferences.snapshots.title`
)}
</span>
<span slot="description" data-for="snapshots">
${this.hass.localize(
`ui.panel.config.analytics.preferences.snapshots.description`
)}
</span>
<ha-switch
@change=${this._handleDeviceRowClick}
.checked=${!!this._analyticsDetails?.preferences.snapshots}
.disabled=${this._analyticsDetails === undefined}
name="snapshots"
>
</ha-switch>
</ha-settings-row>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
`ui.panel.config.analytics.preferences.snapshots.title`
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
`ui.panel.config.analytics.preferences.snapshots.description`
)}
</span>
<ha-switch
slot="end"
@change=${this._handleDeviceRowClick}
.checked=${!!this._analyticsDetails?.preferences.snapshots}
.disabled=${this._analyticsDetails === undefined}
></ha-switch>
</ha-md-list-item>
</ha-md-list>
</div>
</ha-card>`
: nothing}
${this._zwaveEntryId !== undefined
? html`<ha-card
outlined
data-section="zwave"
class=${this._highlightedSection === "zwave" ? "highlighted" : ""}
.header=${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.data_collection.title"
)}
>
<div class="card-content">
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.data_collection.info",
{
documentation_link: html`<a
target="_blank"
href="https://zwave-js.github.io/node-zwave-js/#/data-collection/data-collection"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.data_collection.documentation_link"
)}</a
>`,
}
)}
</p>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.data_collection.toggle_title"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.data_collection.toggle_description"
)}
</span>
${this._zwaveDataCollectionOptIn !== undefined
? html`
<ha-switch
slot="end"
@change=${this._zwaveDataCollectionToggled}
.checked=${this._zwaveDataCollectionOptIn === true}
></ha-switch>
`
: html`<ha-spinner slot="end" size="small"></ha-spinner>`}
</ha-md-list-item>
</ha-md-list>
</div>
</ha-card>`
: nothing}
@@ -123,6 +190,10 @@ class ConfigAnalytics extends SubscribeMixin(LitElement) {
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
const section = extractSearchParam("section");
if (section) {
this._highlightedSection = section;
}
if (isComponentLoaded(this.hass, "analytics")) {
this._load();
}
@@ -135,6 +206,47 @@ class ConfigAnalytics extends SubscribeMixin(LitElement) {
} catch (err: any) {
this._error = err.message || err;
}
this._loadZwaveDataCollection();
}
private async _loadZwaveDataCollection() {
if (!isComponentLoaded(this.hass, "zwave_js")) {
return;
}
try {
const entries = await getConfigEntries(this.hass, {
domain: "zwave_js",
});
const entry = entries.find((e) => !e.disabled_by);
if (entry) {
this._zwaveEntryId = entry.entry_id;
const status = await fetchZwaveDataCollectionStatus(
this.hass,
entry.entry_id
);
this._zwaveDataCollectionOptIn =
status.opted_in === true || status.enabled === true;
if (this._highlightedSection === "zwave") {
this.updateComplete.then(() => {
this._scrollToSection("zwave");
});
}
}
} catch {
// Z-Wave data collection status is optional
}
}
private _scrollToSection(section: string): void {
const card = this.shadowRoot?.querySelector(
`[data-section="${section}"]`
) as HTMLElement;
if (card) {
card.scrollIntoView({ behavior: "smooth", block: "center" });
setTimeout(() => {
this._highlightedSection = undefined;
}, 3000);
}
}
private async _save() {
@@ -162,6 +274,14 @@ class ConfigAnalytics extends SubscribeMixin(LitElement) {
this._save();
}
private _zwaveDataCollectionToggled(ev: Event) {
setZwaveDataCollectionPreference(
this.hass,
this._zwaveEntryId!,
(ev.target as HTMLInputElement).checked
);
}
private _preferencesChanged(event: CustomEvent): void {
this._analyticsDetails = {
...this._analyticsDetails!,
@@ -178,15 +298,38 @@ class ConfigAnalytics extends SubscribeMixin(LitElement) {
color: var(--error-color);
}
ha-settings-row {
padding: 0;
}
p {
margin-top: 0;
}
ha-card:not(:first-of-type) {
margin-top: 24px;
}
ha-md-list {
background: none;
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
}
ha-card {
transition: box-shadow 0.3s ease;
}
ha-card.highlighted {
animation: highlight-fade 2.5s ease-out forwards;
}
@keyframes highlight-fade {
0% {
box-shadow:
0 0 0 var(--ha-border-width-md) var(--primary-color),
0 0 var(--ha-shadow-blur-lg) rgba(var(--rgb-primary-color), 0.4);
}
100% {
box-shadow:
0 0 0 var(--ha-border-width-md) transparent,
0 0 0 transparent;
}
}
`,
];
}

View File

@@ -36,7 +36,7 @@ import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar";
import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart";
import { showShortcutsDialog } from "../../../dialogs/shortcuts/show-shortcuts-dialog";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import { haStyle, haStyleScrollbar } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { isMac } from "../../../util/is_mac";
@@ -255,88 +255,90 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
</ha-dropdown-item>
</ha-dropdown>
<ha-config-section
.narrow=${this.narrow}
.isWide=${this.isWide}
full-width
>
${repairsIssues.length || canInstallUpdates.length
? html`<ha-card outlined>
${repairsIssues.length
? html`
<ha-config-repairs
<div class="content ha-scrollbar">
<ha-config-section
.narrow=${this.narrow}
.isWide=${this.isWide}
full-width
>
${repairsIssues.length || canInstallUpdates.length
? html`<ha-card outlined>
${repairsIssues.length
? html`
<ha-config-repairs
.hass=${this.hass}
.narrow=${this.narrow}
.total=${totalRepairIssues}
.repairsIssues=${repairsIssues}
></ha-config-repairs>
${totalRepairIssues > repairsIssues.length
? html`
<ha-assist-chip
href="/config/repairs"
.label=${this.hass.localize(
"ui.panel.config.repairs.more_repairs",
{
count:
totalRepairIssues - repairsIssues.length,
}
)}
>
</ha-assist-chip>
`
: ""}
`
: ""}
${repairsIssues.length && canInstallUpdates.length
? html`<hr />`
: ""}
${canInstallUpdates.length
? html`
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.total=${totalUpdates}
.updateEntities=${canInstallUpdates}
.isInstallable=${true}
></ha-config-updates>
${totalUpdates > canInstallUpdates.length
? html`
<ha-assist-chip
href="/config/updates"
label=${this.hass.localize(
"ui.panel.config.updates.more_updates",
{
count:
totalUpdates - canInstallUpdates.length,
}
)}
>
</ha-assist-chip>
`
: ""}
`
: ""}
</ha-card>`
: ""}
${this._pages(
this.cloudStatus,
isComponentLoaded(this.hass, "cloud"),
this.hass.auth.external?.config.hasSettingsScreen
).map((categoryPages) =>
categoryPages.length === 0
? nothing
: html`
<ha-card outlined>
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.total=${totalRepairIssues}
.repairsIssues=${repairsIssues}
></ha-config-repairs>
${totalRepairIssues > repairsIssues.length
? html`
<ha-assist-chip
href="/config/repairs"
.label=${this.hass.localize(
"ui.panel.config.repairs.more_repairs",
{
count:
totalRepairIssues - repairsIssues.length,
}
)}
>
</ha-assist-chip>
`
: ""}
`
: ""}
${repairsIssues.length && canInstallUpdates.length
? html`<hr />`
: ""}
${canInstallUpdates.length
? html`
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.total=${totalUpdates}
.updateEntities=${canInstallUpdates}
.isInstallable=${true}
></ha-config-updates>
${totalUpdates > canInstallUpdates.length
? html`
<ha-assist-chip
href="/config/updates"
label=${this.hass.localize(
"ui.panel.config.updates.more_updates",
{
count:
totalUpdates - canInstallUpdates.length,
}
)}
>
</ha-assist-chip>
`
: ""}
`
: ""}
</ha-card>`
: ""}
${this._pages(
this.cloudStatus,
isComponentLoaded(this.hass, "cloud"),
this.hass.auth.external?.config.hasSettingsScreen
).map((categoryPages) =>
categoryPages.length === 0
? nothing
: html`
<ha-card outlined>
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.pages=${categoryPages}
></ha-config-navigation>
</ha-card>
`
)}
<ha-tip .hass=${this.hass}>${this._tip}</ha-tip>
</ha-config-section>
.pages=${categoryPages}
></ha-config-navigation>
</ha-card>
`
)}
<ha-tip .hass=${this.hass}>${this._tip}</ha-tip>
</ha-config-section>
</div>
</ha-top-app-bar-fixed>
`;
}
@@ -392,7 +394,36 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleScrollbar,
css`
:host {
display: block;
height: 100%;
}
ha-top-app-bar-fixed {
height: 100%;
overflow: hidden;
}
.content {
height: calc(
100vh - var(--header-height, 0px) - var(
--safe-area-inset-top,
0px
) - var(--safe-area-inset-bottom, 0px)
);
height: calc(
100dvh - var(--header-height, 0px) - var(
--safe-area-inset-top,
0px
) - var(--safe-area-inset-bottom, 0px)
);
padding-bottom: var(--ha-space-5);
box-sizing: border-box;
overflow-x: hidden;
}
:host(:not([narrow])) ha-card:last-child {
margin-bottom: 24px;
}

View File

@@ -1,27 +1,7 @@
import { mdiNetwork, mdiServerNetwork, mdiTextBoxOutline } from "@mdi/js";
import { customElement, property } from "lit/decorators";
import type { RouterOptions } from "../../../../../layouts/hass-router-page";
import { HassRouterPage } from "../../../../../layouts/hass-router-page";
import type { HomeAssistant } from "../../../../../types";
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
export const configTabs: PageNavigation[] = [
{
translationKey: "ui.panel.config.zwave_js.navigation.network",
path: `/config/zwave_js/dashboard`,
iconPath: mdiServerNetwork,
},
{
translationKey: "ui.panel.config.zwave_js.navigation.logs",
path: `/config/zwave_js/logs`,
iconPath: mdiTextBoxOutline,
},
{
translationKey: "ui.panel.config.zwave_js.navigation.visualization",
path: `/config/zwave_js/visualization`,
iconPath: mdiNetwork,
},
];
@customElement("zwave_js-config-router")
class ZWaveJSConfigRouter extends HassRouterPage {
@@ -77,6 +57,10 @@ class ZWaveJSConfigRouter extends HassRouterPage {
tag: "zwave_js-node-installer",
load: () => import("./zwave_js-node-installer"),
},
statistics: {
tag: "zwave_js-controller-statistics",
load: () => import("./zwave_js-controller-statistics"),
},
logs: {
tag: "zwave_js-logs",
load: () => import("./zwave_js-logs"),
@@ -85,6 +69,14 @@ class ZWaveJSConfigRouter extends HassRouterPage {
tag: "zwave_js-provisioned",
load: () => import("./zwave_js-provisioned"),
},
"network-info": {
tag: "zwave_js-network-info-page",
load: () => import("./zwave_js-network-info-page"),
},
options: {
tag: "zwave_js-options-page",
load: () => import("./zwave_js-options-page"),
},
visualization: {
tag: "zwave_js-network-visualization",
load: () => import("./zwave_js-network-visualization"),

View File

@@ -0,0 +1,132 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../components/ha-card";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import type { ZWaveJSControllerStatisticsUpdatedMessage } from "../../../../../data/zwave_js";
import { subscribeZwaveControllerStatistics } from "../../../../../data/zwave_js";
import "../../../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
@customElement("zwave_js-controller-statistics")
class ZWaveJSControllerStatistics extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ attribute: false }) public configEntryId!: string;
@state()
private _statistics?: ZWaveJSControllerStatisticsUpdatedMessage;
public hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeZwaveControllerStatistics(
this.hass,
this.configEntryId,
(message) => {
if (!this.hasUpdated) {
return;
}
this._statistics = message;
}
),
];
}
protected render() {
if (!this._statistics) {
return nothing;
}
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize(
"ui.panel.config.zwave_js.navigation.statistics"
)}
back-path="/config/zwave_js/dashboard?config_entry=${this
.configEntryId}"
>
<div class="container">
<ha-card>
<ha-md-list>
${this._renderStat("messages_tx")}
${this._renderStat("messages_rx")}
${this._renderStat("messages_dropped_tx")}
${this._renderStat("messages_dropped_rx")}
${this._renderStat("nak")} ${this._renderStat("can")}
${this._renderStat("timeout_ack")}
${this._renderStat("timeout_response")}
${this._renderStat("timeout_callback")}
</ha-md-list>
</ha-card>
</div>
</hass-subpage>
`;
}
private _renderStat(key: string) {
return html`
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
`ui.panel.config.zwave_js.dashboard.statistics.${key}.label`
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
`ui.panel.config.zwave_js.dashboard.statistics.${key}.tooltip`
)}
</span>
<span slot="end">${this._statistics?.[key] ?? 0}</span>
</ha-md-list-item>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
ha-card {
margin: auto;
margin-top: var(--ha-space-4);
max-width: 600px;
}
ha-md-list {
background: none;
padding: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
}
span[slot="end"] {
font-size: 0.95em;
color: var(--primary-text-color);
}
.container {
padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zwave_js-controller-statistics": ZWaveJSControllerStatistics;
}
}

View File

@@ -4,6 +4,7 @@ import type { CSSResultArray } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { capitalizeFirstLetter } from "../../../../../common/string/capitalize-first-letter";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-select";
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
@@ -13,12 +14,11 @@ import {
setZWaveJSLogLevel,
subscribeZWaveJSLogs,
} from "../../../../../data/zwave_js";
import "../../../../../layouts/hass-tabs-subpage";
import "../../../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import { fileDownload } from "../../../../../util/file_download";
import { configTabs } from "./zwave_js-config-router";
@customElement("zwave_js-logs")
class ZWaveJSLogs extends SubscribeMixin(LitElement) {
@@ -62,11 +62,12 @@ class ZWaveJSLogs extends SubscribeMixin(LitElement) {
protected render() {
return html`
<hass-tabs-subpage
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${configTabs}
.header=${this.hass.localize("ui.panel.config.zwave_js.logs.caption")}
back-path="/config/zwave_js/dashboard?config_entry=${this
.configEntryId}"
>
<div class="container">
<ha-card>
@@ -110,7 +111,7 @@ class ZWaveJSLogs extends SubscribeMixin(LitElement) {
</ha-card>
<textarea readonly></textarea>
</div>
</hass-tabs-subpage>
</hass-subpage>
`;
}

View File

@@ -0,0 +1,133 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../components/ha-card";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import type { ZWaveJSNetwork } from "../../../../../data/zwave_js";
import { fetchZwaveNetworkStatus } from "../../../../../data/zwave_js";
import "../../../../../layouts/hass-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import { formatHomeIdAsHex } from "./functions";
@customElement("zwave_js-network-info-page")
class ZWaveJSNetworkInfoPage extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ attribute: false }) public configEntryId!: string;
@state() private _network?: ZWaveJSNetwork;
protected async firstUpdated() {
if (this.hass && this.configEntryId) {
this._network = await fetchZwaveNetworkStatus(this.hass, {
entry_id: this.configEntryId,
});
}
}
protected render(): TemplateResult {
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.network_info_title"
)}
back-path="/config/zwave_js/dashboard?config_entry=${this
.configEntryId}"
>
<div class="container">
<ha-card>
${this._network
? html`<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.home_id"
)}
</span>
<span slot="supporting-text">
${formatHomeIdAsHex(this._network.controller.home_id)}
</span>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.driver_version"
)}
</span>
<span slot="supporting-text">
${this._network.client.driver_version}
</span>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.server_version"
)}
</span>
<span slot="supporting-text">
${this._network.client.server_version}
</span>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.server_url"
)}
</span>
<span slot="supporting-text">
${this._network.client.ws_server_url}
</span>
</ha-md-list-item>
</ha-md-list>`
: nothing}
</ha-card>
</div>
</hass-subpage>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.container {
padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4);
}
ha-card {
max-width: 600px;
margin: auto;
}
ha-md-list {
background: none;
padding: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
--md-list-item-supporting-text-size: var(
--md-list-item-label-text-size,
var(--md-sys-typescale-body-large-size, 1rem)
);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zwave_js-network-info-page": ZWaveJSNetworkInfoPage;
}
}

View File

@@ -24,10 +24,9 @@ import {
NodeStatus,
subscribeZwaveNodeStatistics,
} from "../../../../../data/zwave_js";
import "../../../../../layouts/hass-tabs-subpage";
import "../../../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
import type { HomeAssistant, Route } from "../../../../../types";
import { configTabs } from "./zwave_js-config-router";
@customElement("zwave_js-network-visualization")
export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
@@ -72,11 +71,14 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
protected render() {
return html`
<hass-tabs-subpage
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${configTabs}
.header=${this.hass.localize(
"ui.panel.config.zwave_js.navigation.visualization"
)}
back-path="/config/zwave_js/dashboard?config_entry=${this
.configEntryId}"
>
<ha-network-graph
.hass=${this.hass}
@@ -86,8 +88,8 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
)}
.tooltipFormatter=${this._tooltipFormatter}
@chart-click=${this._handleChartClick}
></ha-network-graph
></hass-tabs-subpage>
></ha-network-graph>
</hass-subpage>
`;
}

View File

@@ -41,7 +41,7 @@ import {
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
import "../../../../../layouts/hass-error-screen";
import "../../../../../layouts/hass-loading-screen";
import "../../../../../layouts/hass-tabs-subpage";
import "../../../../../layouts/hass-subpage";
import { haStyle } from "../../../../../resources/styles";
import type {
HomeAssistant,
@@ -49,7 +49,6 @@ import type {
ValueChangedEvent,
} from "../../../../../types";
import "../../../ha-config-section";
import { configTabs } from "./zwave_js-config-router";
import "./zwave_js-custom-param";
const icons = {
@@ -116,11 +115,14 @@ class ZWaveJSNodeConfig extends LitElement {
: "";
return html`
<hass-tabs-subpage
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${configTabs}
.header=${this.hass.localize(
"ui.panel.config.zwave_js.node_config.header"
)}
back-path="/config/zwave_js/dashboard?config_entry=${this
.configEntryId}"
>
<ha-config-section
.narrow=${this.narrow}
@@ -226,7 +228,7 @@ class ZWaveJSNodeConfig extends LitElement {
></zwave_js-custom-param>
</ha-card>
</ha-config-section>
</hass-tabs-subpage>
</hass-subpage>
`;
}

View File

@@ -0,0 +1,165 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import type {
ZWaveJSClient,
ZWaveJSNetwork,
} from "../../../../../data/zwave_js";
import {
fetchZwaveNetworkStatus,
InclusionState,
} from "../../../../../data/zwave_js";
import "../../../../../layouts/hass-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import { showZWaveJSRebuildNetworkRoutesDialog } from "./show-dialog-zwave_js-rebuild-network-routes";
import { showZWaveJSRemoveNodeDialog } from "./show-dialog-zwave_js-remove-node";
@customElement("zwave_js-options-page")
class ZWaveJSOptionsPage extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ attribute: false }) public configEntryId!: string;
@state() private _network?: ZWaveJSNetwork;
@state() private _status?: ZWaveJSClient["state"];
protected async firstUpdated() {
if (this.hass && this.configEntryId) {
const network = await fetchZwaveNetworkStatus(this.hass, {
entry_id: this.configEntryId,
});
this._network = network;
this._status = network.client.state;
}
}
protected render() {
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.options_title"
)}
back-path="/config/zwave_js/dashboard?config_entry=${this
.configEntryId}"
>
<div class="container">
<ha-card>
${this._network
? html`<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.zwave_js.common.rebuild_network_routes"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.rebuild_routes_description"
)}
</span>
<ha-button
appearance="plain"
slot="end"
size="small"
@click=${this._rebuildNetworkRoutesClicked}
.disabled=${this._status === "disconnected"}
>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.rebuild_routes_action"
)}
</ha-button>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.zwave_js.common.remove_node"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.remove_node_description"
)}
</span>
<ha-button
appearance="plain"
slot="end"
size="small"
@click=${this._removeNodeClicked}
.disabled=${this._status !== "connected" ||
(this._network?.controller.inclusion_state !==
InclusionState.Idle &&
this._network?.controller.inclusion_state !==
InclusionState.SmartStart)}
>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.remove_node_action"
)}
</ha-button>
</ha-md-list-item>
</ha-md-list>`
: nothing}
</ha-card>
</div>
</hass-subpage>
`;
}
private _rebuildNetworkRoutesClicked() {
showZWaveJSRebuildNetworkRoutesDialog(this, {
entry_id: this.configEntryId,
});
}
private _removeNodeClicked() {
showZWaveJSRemoveNodeDialog(this, {
entryId: this.configEntryId,
skipConfirmation:
this._network?.controller.inclusion_state === InclusionState.Excluding,
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.container {
padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4);
}
ha-card {
max-width: 600px;
margin: auto;
}
ha-md-list {
background: none;
padding: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zwave_js-options-page": ZWaveJSOptionsPage;
}
}

View File

@@ -1,8 +1,10 @@
import { mdiCheckCircle, mdiCloseCircleOutline, mdiDelete } from "@mdi/js";
import { mdiCheck, mdiDelete } from "@mdi/js";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeDeviceName } from "../../../../../common/entity/compute_device_name";
import type { DataTableColumnContainer } from "../../../../../components/data-table/ha-data-table";
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
import type { ZwaveJSProvisioningEntry } from "../../../../../data/zwave_js";
import {
fetchZwaveProvisioningEntries,
@@ -14,7 +16,6 @@ import type { LocalizeFunc } from "../../../../../common/translations/localize";
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
import "../../../../../layouts/hass-tabs-subpage-data-table";
import type { HomeAssistant, Route } from "../../../../../types";
import { configTabs } from "./zwave_js-config-router";
@customElement("zwave_js-provisioned")
class ZWaveJSProvisioned extends LitElement {
@@ -28,15 +29,26 @@ class ZWaveJSProvisioned extends LitElement {
@state() private _provisioningEntries: ZwaveJSProvisioningEntry[] = [];
@state() private _nodeIdToDevice: Record<number, DeviceRegistryEntry> = {};
protected render() {
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${configTabs}
.tabs=${[
{
path: `/config/zwave_js/provisioned?config_entry=${this.configEntryId}`,
name: this.hass.localize(
"ui.panel.config.zwave_js.provisioned.caption"
),
},
]}
back-path="/config/zwave_js/dashboard?config_entry=${this
.configEntryId}"
.columns=${this._columns(this.hass.localize)}
.data=${this._provisioningEntries}
.data=${this._getData(this._provisioningEntries, this._nodeIdToDevice)}
>
</hass-tabs-subpage-data-table>
`;
@@ -46,39 +58,13 @@ class ZWaveJSProvisioned extends LitElement {
(
localize: LocalizeFunc
): DataTableColumnContainer<ZwaveJSProvisioningEntry> => ({
included: {
showNarrow: true,
title: localize("ui.panel.config.zwave_js.provisioned.included"),
type: "icon",
template: (entry) =>
entry.nodeId
? html`
<ha-svg-icon
.label=${this.hass.localize(
"ui.panel.config.zwave_js.provisioned.included"
)}
.path=${mdiCheckCircle}
></ha-svg-icon>
`
: html`
<ha-svg-icon
.label=${this.hass.localize(
"ui.panel.config.zwave_js.provisioned.not_included"
)}
.path=${mdiCloseCircleOutline}
></ha-svg-icon>
`,
},
active: {
title: localize("ui.panel.config.zwave_js.provisioned.active"),
type: "icon",
template: (entry) =>
entry.status === ProvisioningEntryStatus.Active
? html`<ha-svg-icon .path=${mdiCheckCircle}></ha-svg-icon>`
: html`<ha-svg-icon .path=${mdiCloseCircleOutline}></ha-svg-icon>`,
name: {
title: localize("ui.panel.config.zwave_js.provisioned.name"),
main: true,
sortable: true,
filterable: true,
},
dsk: {
main: true,
title: localize("ui.panel.config.zwave_js.provisioned.dsk"),
sortable: true,
filterable: true,
@@ -101,10 +87,35 @@ class ZWaveJSProvisioned extends LitElement {
.join(", ");
},
},
included: {
title: localize("ui.panel.config.zwave_js.provisioned.included"),
sortable: true,
type: "icon",
minWidth: "120px",
maxWidth: "120px",
template: (entry) =>
entry.nodeId
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
: html``,
},
active: {
title: localize("ui.panel.config.zwave_js.provisioned.active"),
sortable: true,
type: "icon",
minWidth: "120px",
maxWidth: "120px",
template: (entry) =>
entry.status === ProvisioningEntryStatus.Active
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
: html``,
},
unprovision: {
showNarrow: true,
title: localize("ui.panel.config.zwave_js.provisioned.unprovision"),
title: "",
label: localize("ui.panel.config.zwave_js.provisioned.unprovision"),
type: "icon-button",
showNarrow: true,
moveable: false,
hideable: false,
template: (entry) => html`
<ha-icon-button
.label=${this.hass.localize(
@@ -119,12 +130,50 @@ class ZWaveJSProvisioned extends LitElement {
})
);
private _getData = memoizeOne(
(
entries: ZwaveJSProvisioningEntry[],
nodeIdToDevice: Record<number, DeviceRegistryEntry>
) =>
entries.map((entry) => {
const device = entry.nodeId ? nodeIdToDevice[entry.nodeId] : undefined;
return {
...entry,
name: device ? computeDeviceName(device) || "—" : "—",
};
})
);
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._fetchData();
}
private async _fetchData() {
private _fetchData() {
this._buildNodeIdToDeviceMap();
this._fetchProvisioningEntries();
}
private _buildNodeIdToDeviceMap() {
const map: Record<number, DeviceRegistryEntry> = {};
for (const device of Object.values(this.hass.devices)) {
if (!device.config_entries.includes(this.configEntryId)) {
continue;
}
for (const [domain, id] of device.identifiers) {
if (domain === "zwave_js") {
const nodeId = parseInt(id.split("-")[1]);
if (!isNaN(nodeId)) {
map[nodeId] = device;
break;
}
}
}
}
this._nodeIdToDevice = map;
}
private async _fetchProvisioningEntries() {
this._provisioningEntries = await fetchZwaveProvisioningEntries(
this.hass!,
this.configEntryId
@@ -154,7 +203,7 @@ class ZWaveJSProvisioned extends LitElement {
}
await unprovisionZwaveSmartStartNode(this.hass, this.configEntryId, dsk);
this._fetchData();
this._fetchProvisioningEntries();
};
}

View File

@@ -17,15 +17,25 @@ import type { LovelaceGenericElementEditor } from "../../types";
import "../conditions/ha-card-conditions-editor";
import { configElementStyle } from "../config-elements/config-elements-style";
import { actionConfigStruct } from "../structs/action-struct";
import type { UiAction } from "../../components/hui-action-editor";
import { supportedActions } from "../../components/hui-action-editor";
const ACTIONS: UiAction[] = [
"navigate",
"url",
"perform-action",
"assist",
"none",
];
const buttonConfigStruct = object({
type: optional(string()),
text: optional(string()),
icon: optional(string()),
color: optional(string()),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
tap_action: optional(supportedActions(actionConfigStruct, ACTIONS)),
hold_action: optional(supportedActions(actionConfigStruct, ACTIONS)),
double_tap_action: optional(supportedActions(actionConfigStruct, ACTIONS)),
visibility: optional(array(any())),
});
@@ -78,6 +88,7 @@ export class HuiButtonHeadingBadgeEditor
name: "tap_action",
selector: {
ui_action: {
actions: ACTIONS,
default_action: "none",
},
},
@@ -91,6 +102,7 @@ export class HuiButtonHeadingBadgeEditor
name: action,
selector: {
ui_action: {
actions: ACTIONS,
default_action: "none" as const,
},
},

View File

@@ -1419,6 +1419,7 @@ class HUIRoot extends LitElement {
}
ha-tab-group-tab {
--ha-tab-group-tab-height: var(--header-height, 56px);
height: var(--ha-tab-group-tab-height);
}
.tab-bar ha-tab-group-tab {
--ha-tab-group-tab-height: var(--tab-bar-height, 56px);

View File

@@ -2614,6 +2614,11 @@
"password": "Password"
}
},
"my": {
"add_repository_title": "Add app repository?",
"add_repository_description": "This app requires a repository that is currently not known. Do you want to add the repository {repository}?",
"error_repository_not_found": "The repository for this app was not found"
},
"panel": {
"info": "Info",
"documentation": "Documentation",
@@ -6937,9 +6942,10 @@
},
"zwave_js": {
"navigation": {
"network": "Network",
"general": "Z-Wave",
"statistics": "Statistics",
"logs": "Logs",
"visualization": "Visualization"
"visualization": "Z-Wave visualization"
},
"common": {
"network": "Network",
@@ -6948,36 +6954,64 @@
"source": "Source",
"back": "Back",
"add_node": "Add device",
"remove_node": "Remove device",
"remove_node": "Remove foreign device",
"remove_a_node": "Remove a device",
"rebuild_network_routes": "Rebuild network routes",
"rebuild_network_routes": "Discover and assign new routes",
"in_progress_inclusion_exclusion": "Z-Wave JS is searching for devices",
"cancel_inclusion_exclusion": "Stop searching"
},
"dashboard": {
"network_card_title": "My network",
"show_map": "Show map",
"options_title": "Options",
"options_description": "Manage network routes and remove devices",
"network_info_title": "Network information",
"network_info_description": "View driver, server, and home ID details",
"visualization_description": "Visualize the network topology and device connections",
"statistics_description": "View adapter and network communication statistics",
"logs_description": "View and download Z-Wave logs",
"analytics_title": "Analytics",
"analytics_description": "Share anonymized telemetry and statistics with Z-Wave JS",
"analytics_on": "On",
"analytics_off": "Off",
"driver_version": "Driver version",
"server_version": "Server version",
"home_id": "Home ID",
"server_url": "Server URL",
"devices": "{count} {count, plural,\n one {device}\n other {devices}\n}",
"device_count": "{count} {count, plural,\n one {device}\n other {devices}\n}",
"entity_count": "{count} {count, plural,\n one {entity}\n other {entities}\n}",
"provisioned_devices": "Provisioned devices",
"not_ready": "{count} not ready",
"provisioned_count": "{count} {count, plural,\n one {provisioned device}\n other {provisioned devices}\n}",
"not_included": "{count} not included",
"devices_offline": "{count} offline",
"rebuild_routes_description": "Rebuilding routes creates heavy traffic and may degrade performance for minutes to hours",
"rebuild_routes_action": "Rebuild",
"remove_node_description": "Useful when a device is stuck in another network and must be excluded before it can be included again",
"remove_node_action": "Remove",
"nvm_backup": {
"title": "Backup and restore",
"description": "Back up or restore your Z-Wave adapter's non-volatile memory (NVM). The NVM contains your network information including paired devices. It's recommended to create a backup before making any major changes to your Z-Wave network.",
"download_backup": "Download backup",
"download_backup_description": "Create and download a backup of your Z-Wave adapter",
"download_action": "Download",
"restore_backup": "Restore from backup",
"restore_backup_description": "Restore a previously downloaded backup to your Z-Wave adapter",
"restore_action": "Restore",
"backup_failed": "Failed to download backup",
"restore_complete": "Backup restored",
"restore_failed": "Failed to restore backup",
"creating": "Creating backup",
"restoring": "Restoring backup",
"migrate": "Migrate adapter"
"migrate": "Migrate adapter",
"migrate_description": "Move your Z-Wave network to a different adapter",
"migrate_action": "Migrate"
},
"data_collection": {
"title": "Third-party data reporting",
"description": "Enable the reporting of anonymized telemetry and statistics to the Z-Wave JS organization. This data will be used to focus development efforts and improve the user experience. Information about the data that is collected and how it is used, including an example of the data collected, can be found in the {documentation_link}.",
"documentation_link": "Z-Wave JS data collection documentation"
"title": "Z-Wave JS analytics",
"info": "Enable the reporting of anonymized telemetry and statistics to the Z-Wave JS organization. This data will be used to focus development efforts and improve the user experience. Information about the data that is collected and how it is used, including an example of the data collected, can be found in the {documentation_link}.",
"documentation_link": "Z-Wave JS data collection documentation",
"toggle_title": "Data reporting",
"toggle_description": "Share anonymized telemetry and statistics with Z-Wave JS"
},
"statistics": {
"title": "Adapter statistics",
@@ -7174,9 +7208,10 @@
"default": "Default"
},
"network_status": {
"connected": "status: connected",
"connecting": "status: connecting",
"unknown": "status: unknown"
"online": "Online",
"offline": "Offline",
"online_named": "{name} online",
"offline_named": "{name} offline"
},
"add_node": {
"title": "Add a Z-Wave device",
@@ -7267,13 +7302,16 @@
}
},
"provisioned": {
"caption": "Provisioned devices",
"name": "Name",
"dsk": "DSK",
"security_classes": "Security classes",
"unprovision": "Unprovision",
"included": "Included",
"not_included": "Not included",
"confirm_unprovision_title": "Remove device?",
"confirm_unprovision_text": "{name} will be permanently removed from Home Assistant and your Z-Wave network.",
"confirm_unprovision_text": "This device will be permanently removed from your Z-Wave network.",
"confirm_unprovision_text_included": "This device will be permanently removed from Home Assistant and your Z-Wave network.",
"active": "Active"
},
"security_classes": {
@@ -7391,7 +7429,8 @@
}
},
"logs": {
"title": "Z-Wave JS logs",
"caption": "Logs",
"title": "Z-Wave logs",
"log_level": "Log level",
"subscribed_to_logs": "Subscribed to Z-Wave JS log messages…",
"log_level_changed": "Log level changed to: {level}",
@@ -7636,12 +7675,12 @@
},
"analytics": {
"caption": "Analytics",
"header": "Home Assistant analytics",
"header": "Home Assistant",
"description": "Learn how to share data to improve Home Assistant",
"preferences": {
"base": {
"title": "Basic analytics",
"description": "This includes information about your system."
"description": "This includes information about your system"
},
"usage": {
"title": "Usage",
@@ -7649,16 +7688,16 @@
},
"statistics": {
"title": "Statistical data",
"description": "Counts containing total number of datapoints."
"description": "Counts containing total number of datapoints"
},
"diagnostics": {
"title": "Diagnostics",
"description": "Share crash reports when unexpected errors occur."
"description": "Share crash reports when unexpected errors occur"
},
"snapshots": {
"title": "Devices",
"description": "Generic information about your devices.",
"header": "Device analytics",
"description": "Generic information about your devices",
"header": "Device database",
"info": "Anonymously share data about your devices to help build the Open Home Foundation's device database. This free, open source resource helps users find useful information about smart home devices. Only device-specific details (like model or manufacturer) are shared — never personally identifying information (like the names you assign). Learn more about the device database and how we process your data in our {data_use_statement}, which you accept by opting in.",
"data_use_statement": "Data Use Statement",
"alert": {

View File

@@ -9302,7 +9302,7 @@ __metadata:
sortablejs: "patch:sortablejs@npm%3A1.15.6#~/.yarn/patches/sortablejs-npm-1.15.6-3235a8f83b.patch"
stacktrace-js: "npm:2.0.2"
superstruct: "npm:2.0.2"
tar: "npm:7.5.7"
tar: "npm:7.5.8"
terser-webpack-plugin: "npm:5.3.16"
tinykeys: "npm:3.0.0"
ts-lit-plugin: "npm:2.0.2"
@@ -13722,16 +13722,16 @@ __metadata:
languageName: node
linkType: hard
"tar@npm:7.5.7, tar@npm:^7.5.2":
version: 7.5.7
resolution: "tar@npm:7.5.7"
"tar@npm:7.5.8, tar@npm:^7.5.2":
version: 7.5.8
resolution: "tar@npm:7.5.8"
dependencies:
"@isaacs/fs-minipass": "npm:^4.0.0"
chownr: "npm:^3.0.0"
minipass: "npm:^7.1.2"
minizlib: "npm:^3.1.0"
yallist: "npm:^5.0.0"
checksum: 10/0d6938dd32fe5c0f17c8098d92bd9889ee0ed9d11f12381b8146b6e8c87bb5aa49feec7abc42463f0597503d8e89e4c4c0b42bff1a5a38444e918b4878b7fd21
checksum: 10/5fddc22e0fd03e73d5e9e922e71d8681f85443dee4f21403059a757e186ae4004abc9a709cdc7f4143d7d75758a2935f7306b3cc193123d46b6f786dd2b99c2a
languageName: node
linkType: hard