Compare commits

..

8 Commits

Author SHA1 Message Date
Paul Bottein
fa29e49985 Prettier 2024-10-22 15:02:36 +02:00
Paul Bottein
19c37ab91c Apply suggestions from code review
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2024-10-22 14:56:41 +02:00
Paul Bottein
a9b4117b1b Add translation and simplify delete method 2024-10-22 10:59:50 +02:00
Paul Bottein
24f9944319 Fix startup notifications 2024-10-22 10:59:50 +02:00
Paul Bottein
2518b1a79d Fix errors 2024-10-22 10:59:50 +02:00
Paul Bottein
d24b9b6ced Improve deletion functions 2024-10-22 10:59:48 +02:00
Paul Bottein
97c69e620c Fix notifications 2024-10-22 10:59:33 +02:00
Paul Bottein
3bea09f161 Use undo notification instead of confirmation dialog for cards and badges 2024-10-22 10:59:32 +02:00
130 changed files with 2208 additions and 3710 deletions

View File

@@ -21,12 +21,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.2.1
with:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -57,12 +57,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.2.1
with:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -24,9 +24,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.2.1
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -37,7 +37,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/cache@v4.1.2
uses: actions/cache@v4.1.1
with:
path: |
node_modules/.cache/prettier
@@ -58,9 +58,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.2.1
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -76,9 +76,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.2.1
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -100,9 +100,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.2.1
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.2.1
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.

View File

@@ -22,12 +22,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.2.1
with:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -58,12 +58,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.2.1
with:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -16,10 +16,10 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.2.1
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -21,10 +21,10 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.2.1
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -20,7 +20,7 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
@@ -28,7 +28,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -23,7 +23,7 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.2.1
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master
@@ -34,7 +34,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.2.1
- name: Upload Translations
run: |

View File

@@ -24,11 +24,8 @@ const convertToJSON = async (
) => {
let localeData;
try {
// use "pt" for "pt-BR", because "pt-BR" is unsupported by @formatjs
const language = lang === "pt-BR" ? "pt" : lang;
localeData = await readFile(
join(formatjsDir, pkg, subDir, `${language}.js`),
join(formatjsDir, pkg, subDir, `${lang}.js`),
"utf-8"
);
} catch (e) {

View File

@@ -1,6 +1,5 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list";
import type { ActionDetail } from "@material/mwc-list/mwc-list";
import { ActionDetail } from "@material/mwc-list/mwc-list";
import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js";
import { Auth, Connection } from "home-assistant-js-websocket";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
@@ -90,8 +89,8 @@ class HcCast extends LitElement {
generateDefaultViewConfig({}, {}, {}, {}, () => ""),
]
).map(
(view, idx) => html`
<ha-list-item
(view, idx) =>
html`<ha-list-item
graphic="avatar"
.activated=${this.castManager.status?.lovelacePath ===
(view.path ?? idx)}
@@ -109,9 +108,8 @@ class HcCast extends LitElement {
: html`<ha-svg-icon
slot="item-icon"
.path=${mdiViewDashboard}
></ha-svg-icon>`}
</ha-list-item>
`
></ha-svg-icon>`}</ha-list-item
> `
)}</mwc-list
>
`}

View File

@@ -88,7 +88,7 @@ class HcLayout extends LitElement {
}
.card-header {
color: var(--ha-card-header-color, var(--primary-text-color));
color: var(--ha-card-header-color, --primary-text-color);
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em;

View File

@@ -1,11 +1,10 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
import { getPanelTitleFromUrlPath } from "../../../../src/data/panel";
import { Lovelace } from "../../../../src/panels/lovelace/types";
import "../../../../src/panels/lovelace/views/hui-view";
import "../../../../src/panels/lovelace/views/hui-view-container";
import { HomeAssistant } from "../../../../src/types";
import "./hc-launch-screen";
@@ -23,6 +22,8 @@ class HcLovelace extends LitElement {
@property() public urlPath: string | null = null;
@query("hui-view") private _huiView?: HTMLElement;
protected render(): TemplateResult {
const index = this._viewIndex;
if (index === undefined) {
@@ -46,22 +47,12 @@ class HcLovelace extends LitElement {
setEditMode: () => undefined,
showToast: () => undefined,
};
const viewConfig = this.lovelaceConfig.views[index];
const background = viewConfig.background || this.lovelaceConfig.background;
return html`
<hui-view-container
<hui-view
.hass=${this.hass}
.background=${background}
.theme=${viewConfig.theme}
>
<hui-view
.hass=${this.hass}
.lovelace=${lovelace}
.index=${index}
></hui-view>
</hui-view-container>
.lovelace=${lovelace}
.index=${index}
></hui-view>
`;
}
@@ -91,6 +82,26 @@ class HcLovelace extends LitElement {
}${viewTitle || ""}`
: undefined,
});
const configBackground =
this.lovelaceConfig.views[index].background ||
this.lovelaceConfig.background;
const backgroundStyle =
typeof configBackground === "string"
? configBackground
: configBackground?.image
? `center / cover no-repeat url('${configBackground.image}')`
: undefined;
if (backgroundStyle) {
this._huiView!.style.setProperty(
"--lovelace-background",
backgroundStyle
);
} else {
this._huiView!.style.removeProperty("--lovelace-background");
}
}
}
}
@@ -114,15 +125,19 @@ class HcLovelace extends LitElement {
static get styles(): CSSResultGroup {
return css`
hui-view-container {
display: flex;
position: relative;
:host {
min-height: 100vh;
height: 0;
display: flex;
flex-direction: column;
box-sizing: border-box;
background: var(--primary-background-color);
}
:host > * {
flex: 1;
}
hui-view {
flex: 1 1 100%;
max-width: 100%;
background: var(--lovelace-background, var(--primary-background-color));
}
`;
}

View File

@@ -142,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement {
<div class="action">
<span>
${this._action
? describeAction(this.hass, [], [], {}, this._action)
? describeAction(this.hass, [], [], this._action)
: "<invalid YAML>"}
</span>
<ha-yaml-editor
@@ -155,7 +155,7 @@ export class DemoAutomationDescribeAction extends LitElement {
${ACTIONS.map(
(conf) => html`
<div class="action">
<span>${describeAction(this.hass, [], [], {}, conf as any)}</span>
<span>${describeAction(this.hass, [], [], conf as any)}</span>
<pre>${dump(conf)}</pre>
</div>
`

View File

@@ -417,7 +417,7 @@ class HassioAddonConfig extends LitElement {
justify-content: space-between;
}
.header h2 {
color: var(--ha-card-header-color, var(--primary-text-color));
color: var(--ha-card-header-color, --primary-text-color);
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em;

View File

@@ -37,6 +37,7 @@ import "./config/hassio-addon-config";
import "./config/hassio-addon-network";
import "./hassio-addon-router";
import "./info/hassio-addon-info";
import "./log/hassio-addon-logs";
@customElement("hassio-addon-dashboard")
class HassioAddonDashboard extends LitElement {
@@ -160,11 +161,16 @@ class HassioAddonDashboard extends LitElement {
margin-bottom: 24px;
width: 600px;
}
hassio-addon-logs {
max-width: calc(100% - 8px);
min-width: 600px;
}
@media only screen and (max-width: 600px) {
hassio-addon-info,
hassio-addon-network,
hassio-addon-audio,
hassio-addon-config {
hassio-addon-config,
hassio-addon-logs {
max-width: 100%;
min-width: 100%;
}

View File

@@ -38,7 +38,6 @@ import "../../../../src/components/ha-markdown";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch";
import type { HaSwitch } from "../../../../src/components/ha-switch";
import {
AddonCapability,
HassioAddonDetails,
@@ -1120,28 +1119,12 @@ class HassioAddonInfo extends LitElement {
private async _uninstallClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
let removeData = false;
const _removeDataToggled = (e: Event) => {
removeData = (e.target as HaSwitch).checked;
};
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize("dialog.uninstall_addon.title", {
name: this.addon.name,
}),
text: html`
<ha-formfield
.label=${html`<p>
${this.supervisor.localize("dialog.uninstall_addon.remove_data")}
</p>`}
>
<ha-switch
@change=${_removeDataToggled}
.checked=${removeData}
haptic
></ha-switch>
</ha-formfield>
`,
text: this.supervisor.localize("dialog.uninstall_addon.text"),
confirmText: this.supervisor.localize("dialog.uninstall_addon.uninstall"),
dismissText: this.supervisor.localize("common.cancel"),
destructive: true,
@@ -1154,7 +1137,7 @@ class HassioAddonInfo extends LitElement {
this._error = undefined;
try {
await uninstallHassioAddon(this.hass, this.addon.slug, removeData);
await uninstallHassioAddon(this.hass, this.addon.slug);
const eventdata = {
success: true,
response: undefined,
@@ -1209,7 +1192,7 @@ class HassioAddonInfo extends LitElement {
padding-inline-start: 8px;
padding-inline-end: initial;
font-size: 24px;
color: var(--ha-card-header-color, var(--primary-text-color));
color: var(--ha-card-header-color, --primary-text-color);
}
.addon-version {
float: var(--float-end);

View File

@@ -1,14 +1,12 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import "../../../../src/components/ha-circular-progress";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
import "../../../../src/panels/config/logs/error-log-card";
import "../../../../src/components/search-input";
import { extractSearchParam } from "../../../../src/common/url/search-params";
import "./hassio-addon-logs";
@customElement("hassio-addon-log-tab")
class HassioAddonLogDashboard extends LitElement {
@@ -18,8 +16,6 @@ class HassioAddonLogDashboard extends LitElement {
@property({ attribute: false }) public addon?: HassioAddonDetails;
@state() private _filter = extractSearchParam("filter") || "";
protected render(): TemplateResult {
if (!this.addon) {
return html`
@@ -27,31 +23,16 @@ class HassioAddonLogDashboard extends LitElement {
`;
}
return html`
<div class="search">
<search-input
@value-changed=${this._filterChanged}
.hass=${this.hass}
.filter=${this._filter}
.label=${this.hass.localize("ui.panel.config.logs.search")}
></search-input>
</div>
<div class="content">
<error-log-card
<hassio-addon-logs
.hass=${this.hass}
.header=${this.addon.name}
.provider=${this.addon.slug}
show
.filter=${this._filter}
>
</error-log-card>
.supervisor=${this.supervisor}
.addon=${this.addon}
></hassio-addon-logs>
</div>
`;
}
private async _filterChanged(ev) {
this._filter = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,
@@ -60,21 +41,7 @@ class HassioAddonLogDashboard extends LitElement {
.content {
margin: auto;
padding: 8px;
}
.search {
position: sticky;
top: 0;
z-index: 2;
}
search-input {
display: block;
--mdc-text-field-fill-color: var(--sidebar-background-color);
--mdc-text-field-idle-line-color: var(--divider-color);
}
@media all and (max-width: 870px) {
:host {
--error-log-card-height: calc(100vh - 304px);
}
max-width: 1024px;
}
`,
];

View File

@@ -0,0 +1,90 @@
import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-ansi-to-html";
import "../../../../src/components/ha-card";
import {
fetchHassioAddonLogs,
HassioAddonDetails,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
@customElement("hassio-addon-logs")
class HassioAddonLogs extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@state() private _error?: string;
@state() private _content?: string;
public async connectedCallback(): Promise<void> {
super.connectedCallback();
await this._loadData();
}
protected render(): TemplateResult {
return html`
<h1>${this.addon.name}</h1>
<ha-card outlined>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="card-content">
${this._content
? html`<ha-ansi-to-html
.content=${this._content}
></ha-ansi-to-html>`
: ""}
</div>
<div class="card-actions">
<mwc-button @click=${this._refresh}>
${this.supervisor.localize("common.refresh")}
</mwc-button>
</div>
</ha-card>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
:host,
ha-card {
display: block;
}
`,
];
}
private async _loadData(): Promise<void> {
this._error = undefined;
try {
this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
} catch (err: any) {
this._error = this.supervisor.localize("addon.logs.get_logs", {
error: extractApiErrorMessage(err),
});
}
}
private async _refresh(): Promise<void> {
await this._loadData();
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-logs": HassioAddonLogs;
}
}

View File

@@ -48,7 +48,6 @@ import { showHassioBackupDialog } from "../dialogs/backup/show-dialog-hassio-bac
import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hassio-create-backup";
import { supervisorTabs } from "../hassio-tabs";
import { hassioStyle } from "../resources/hassio-style";
import "../../../src/layouts/hass-loading-screen";
type BackupItem = HassioBackup & {
secondary: string;
@@ -70,8 +69,6 @@ export class HassioBackups extends LitElement {
@state() private _backups?: HassioBackup[] = [];
@state() private _isLoading = false;
@query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable;
@@ -80,10 +77,15 @@ export class HassioBackups extends LitElement {
public connectedCallback(): void {
super.connectedCallback();
if (this.hass && this._firstUpdatedCalled) {
this.fetchBackups();
this.refreshData();
}
}
public async refreshData() {
await reloadHassioBackups(this.hass);
await this.fetchBackups();
}
private _computeBackupContent = (backup: HassioBackup): string => {
if (backup.type === "full") {
return this.supervisor.localize("backup.full_backup");
@@ -113,7 +115,7 @@ export class HassioBackups extends LitElement {
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
if (this.hass && this.isConnected) {
this.fetchBackups();
this.refreshData();
}
this._firstUpdatedCalled = true;
}
@@ -173,13 +175,6 @@ export class HassioBackups extends LitElement {
if (!this.supervisor) {
return nothing;
}
if (this._isLoading) {
return html`<hass-loading-screen
.message=${this.supervisor.localize("backup.loading_backups")}
></hass-loading-screen>`;
}
return html`
<hass-tabs-subpage-data-table
.tabs=${atLeastVersion(this.hass.config.version, 2022, 5)
@@ -286,7 +281,7 @@ export class HassioBackups extends LitElement {
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this.fetchBackups();
this.refreshData();
break;
case 1:
showHassioBackupLocationDialog(this, { supervisor: this.supervisor });
@@ -311,15 +306,13 @@ export class HassioBackups extends LitElement {
supervisor: this.supervisor,
onDelete: () => this.fetchBackups(),
}),
reloadBackup: () => this.fetchBackups(),
reloadBackup: () => this.refreshData(),
});
}
private async fetchBackups() {
this._isLoading = true;
await reloadHassioBackups(this.hass);
this._backups = await fetchHassioBackups(this.hass);
this._isLoading = false;
}
private async _deleteSelected() {
@@ -346,7 +339,8 @@ export class HassioBackups extends LitElement {
});
return;
}
await this.fetchBackups();
await reloadHassioBackups(this.hass);
this._backups = await fetchHassioBackups(this.hass);
this._dataTable.clearSelection();
}

View File

@@ -120,12 +120,10 @@ class HassioSupervisorLog extends LitElement {
this._error = undefined;
try {
const response = await fetchHassioLogs(
this._content = await fetchHassioLogs(
this.hass,
this._selectedLogProvider
);
this._content = await response.text();
} catch (err: any) {
this._error = this.supervisor.localize("system.log.get_logs", {
provider: this._selectedLogProvider,

View File

@@ -1,10 +1,11 @@
import "@material/mwc-list/mwc-list-item";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -15,12 +16,12 @@ import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-faded";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-markdown";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-svg-icon";
import "../../../src/components/ha-switch";
import type { HaSwitch } from "../../../src/components/ha-switch";
import {
fetchHassioAddonChangelog,
fetchHassioAddonInfo,
@@ -41,7 +42,6 @@ import { updateCore } from "../../../src/data/supervisor/core";
import { StoreAddon } from "../../../src/data/supervisor/store";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types";
import { addonArchIsSupported, extractChangelog } from "../util/addon";
@@ -149,7 +149,7 @@ class UpdateAvailableCard extends LitElement {
</ha-markdown>
</ha-faded>
`
: nothing}
: ""}
<div class="versions">
<p>
${this.supervisor.localize(
@@ -164,17 +164,15 @@ class UpdateAvailableCard extends LitElement {
</div>
${["core", "addon"].includes(this._updateType)
? html`
<hr />
<ha-settings-row>
<span slot="heading">
${this.supervisor.localize(
"update_available.create_backup"
)}
</span>
<ha-switch id="create_backup" checked></ha-switch>
</ha-settings-row>
<ha-formfield
.label=${this.supervisor.localize(
"update_available.create_backup"
)}
>
<ha-checkbox checked></ha-checkbox>
</ha-formfield>
`
: nothing}
: ""}
`
: html`<ha-circular-progress
aria-label="Updating"
@@ -193,24 +191,22 @@ class UpdateAvailableCard extends LitElement {
? html`
<div class="card-actions">
${changelog
? html`
<a href=${changelog} target="_blank" rel="noreferrer">
<ha-button
.label=${this.supervisor.localize(
"update_available.open_release_notes"
)}
>
</ha-button>
</a>
`
: nothing}
? html`<a .href=${changelog} target="_blank" rel="noreferrer">
<mwc-button
.label=${this.supervisor.localize(
"update_available.open_release_notes"
)}
>
</mwc-button>
</a>`
: ""}
<span></span>
<ha-progress-button @click=${this._update}>
<ha-progress-button @click=${this._update} raised>
${this.supervisor.localize("common.update")}
</ha-progress-button>
</div>
`
: nothing}
: ""}
</ha-card>
`;
}
@@ -246,11 +242,9 @@ class UpdateAvailableCard extends LitElement {
if (this._updateType && !["core", "addon"].includes(this._updateType)) {
return false;
}
const createBackupSwitch = this.shadowRoot?.getElementById(
"create-backup"
) as HaSwitch;
if (createBackupSwitch) {
return createBackupSwitch.checked;
const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
if (checkbox) {
return checkbox.checked;
}
return true;
}
@@ -403,50 +397,41 @@ class UpdateAvailableCard extends LitElement {
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
display: block;
}
ha-card {
margin: auto;
}
a {
text-decoration: none;
color: var(--primary-text-color);
}
.card-actions {
display: flex;
justify-content: space-between;
}
return css`
:host {
display: block;
}
ha-card {
margin: auto;
}
a {
text-decoration: none;
color: var(--primary-text-color);
}
ha-settings-row {
padding: 0;
}
.card-actions {
display: flex;
justify-content: space-between;
border-top: none;
padding: 0 8px 8px;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
.progress-text {
text-align: center;
}
ha-markdown {
padding-bottom: 8px;
}
ha-settings-row {
padding: 0;
margin-bottom: -16px;
}
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: 16px 0 0 0;
}
`,
];
ha-markdown {
padding-bottom: 8px;
}
`;
}
}

View File

@@ -25,24 +25,24 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.26.0",
"@babel/runtime": "7.25.7",
"@braintree/sanitize-url": "7.1.0",
"@codemirror/autocomplete": "6.18.1",
"@codemirror/commands": "6.7.1",
"@codemirror/commands": "6.7.0",
"@codemirror/language": "6.10.3",
"@codemirror/legacy-modes": "6.4.1",
"@codemirror/search": "6.5.6",
"@codemirror/state": "6.4.1",
"@codemirror/view": "6.34.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.16.1",
"@formatjs/intl-displaynames": "6.8.1",
"@formatjs/intl-getcanonicallocales": "2.5.1",
"@formatjs/intl-listformat": "7.7.1",
"@formatjs/intl-locale": "4.2.1",
"@formatjs/intl-numberformat": "8.14.1",
"@formatjs/intl-pluralrules": "5.3.1",
"@formatjs/intl-relativetimeformat": "11.4.1",
"@formatjs/intl-datetimeformat": "6.14.0",
"@formatjs/intl-displaynames": "6.6.10",
"@formatjs/intl-getcanonicallocales": "2.3.1",
"@formatjs/intl-listformat": "7.5.9",
"@formatjs/intl-locale": "4.0.2",
"@formatjs/intl-numberformat": "8.12.0",
"@formatjs/intl-pluralrules": "5.2.16",
"@formatjs/intl-relativetimeformat": "11.2.16",
"@fullcalendar/core": "6.1.15",
"@fullcalendar/daygrid": "6.1.15",
"@fullcalendar/interaction": "6.1.15",
@@ -89,8 +89,8 @@
"@polymer/polymer": "3.5.2",
"@replit/codemirror-indentation-markers": "6.5.3",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.5.1",
"@vaadin/vaadin-themable-mixin": "24.5.1",
"@vaadin/combo-box": "24.5.0",
"@vaadin/vaadin-themable-mixin": "24.5.0",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -114,7 +114,7 @@
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.4.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.7.3",
"intl-messageformat": "10.7.0",
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
@@ -151,12 +151,12 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.26.0",
"@babel/core": "7.25.8",
"@babel/helper-define-polyfill-provider": "0.6.2",
"@babel/plugin-proposal-decorators": "7.25.9",
"@babel/plugin-transform-runtime": "7.25.9",
"@babel/preset-env": "7.26.0",
"@babel/preset-typescript": "7.26.0",
"@babel/plugin-proposal-decorators": "7.25.7",
"@babel/plugin-transform-runtime": "7.25.7",
"@babel/preset-env": "7.25.8",
"@babel/preset-typescript": "7.25.7",
"@bundle-stats/plugin-webpack-filter": "4.16.0",
"@koa/cors": "5.0.0",
"@lokalise/node-api": "12.8.0",
@@ -176,7 +176,7 @@
"@types/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.2",
"@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.14",
"@types/leaflet": "1.9.13",
"@types/leaflet-draw": "1.0.11",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.4.2",
@@ -194,7 +194,7 @@
"babel-loader": "9.2.1",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"chai": "5.1.2",
"chai": "5.1.1",
"del": "8.0.0",
"eslint": "8.57.1",
"eslint-config-airbnb-base": "15.0.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 KiB

View File

@@ -34,11 +34,9 @@ export const protocolIntegrationPicked = async (
if (domain === "zwave_js") {
const entries = options?.config_entry
? undefined
: (
await getConfigEntries(hass, {
domain,
})
).filter((e) => !e.disabled_by);
: await getConfigEntries(hass, {
domain,
});
if (
!isComponentLoaded(hass, "zwave_js") ||
@@ -83,11 +81,9 @@ export const protocolIntegrationPicked = async (
} else if (domain === "zha") {
const entries = options?.config_entry
? undefined
: (
await getConfigEntries(hass, {
domain,
})
).filter((e) => !e.disabled_by);
: await getConfigEntries(hass, {
domain,
});
if (
!isComponentLoaded(hass, "zha") ||
@@ -133,11 +129,9 @@ export const protocolIntegrationPicked = async (
} else if (domain === "matter") {
const entries = options?.config_entry
? undefined
: (
await getConfigEntries(hass, {
domain,
})
).filter((e) => !e.disabled_by);
: await getConfigEntries(hass, {
domain,
});
if (
!isComponentLoaded(hass, domain) ||
(!options?.config_entry && !entries?.length)

View File

@@ -108,7 +108,6 @@ class HaDataTableLabels extends LitElement {
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
outline: 1px solid var(--outline-color);
}
ha-button-menu {
border-radius: 10px;

View File

@@ -254,7 +254,7 @@ class DateRangePickerElement extends WrappedElement {
.daterangepicker select.hourselect,
.daterangepicker select.minuteselect,
.daterangepicker select.secondselect {
background: var(--card-background-color);
background: transparent;
border: 1px solid var(--divider-color);
color: var(--primary-color);
}

View File

@@ -1,17 +1,5 @@
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import {
customElement,
property,
query,
state as litState,
} from "lit/decorators";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
interface State {
bold: boolean;
@@ -23,24 +11,11 @@ interface State {
}
@customElement("ha-ansi-to-html")
export class HaAnsiToHtml extends LitElement {
class HaAnsiToHtml extends LitElement {
@property() public content!: string;
@query("pre") private _pre?: HTMLPreElement;
@litState() private _filter = "";
protected render(): TemplateResult | void {
return html`<pre></pre>`;
}
protected firstUpdated(_changedProperties: PropertyValues): void {
super.firstUpdated(_changedProperties);
// handle initial content
if (this.content) {
this.parseTextToColoredPre(this.content);
}
return html`${this._parseTextToColoredPre(this.content)}`;
}
static get styles(): CSSResultGroup {
@@ -49,7 +24,6 @@ export class HaAnsiToHtml extends LitElement {
overflow-x: auto;
white-space: pre-wrap;
overflow-wrap: break-word;
margin: 0;
}
.bold {
font-weight: bold;
@@ -111,33 +85,11 @@ export class HaAnsiToHtml extends LitElement {
.bg-white {
background-color: rgb(204, 204, 204);
}
::highlight(search-results) {
background-color: var(--primary-color);
color: var(--text-primary-color);
}
`;
}
/**
* add new lines to the log
* @param lines log lines
* @param top should the new lines be added to the top of the log
*/
public parseLinesToColoredPre(lines: string[], top = false) {
for (const line of lines) {
this.parseLineToColoredPre(line, top);
}
}
/**
* Add a single line to the log
* @param line log line
* @param top should the new line be added to the top of the log
*/
public parseLineToColoredPre(line, top = false) {
const lineDiv = document.createElement("div");
private _parseTextToColoredPre(text) {
const pre = document.createElement("pre");
// eslint-disable-next-line no-control-regex
const re = /\x1b(?:\[(.*?)[@-~]|\].*?(?:\x07|\x1b\\))/g;
let i = 0;
@@ -151,7 +103,7 @@ export class HaAnsiToHtml extends LitElement {
backgroundColor: null,
};
const addPart = (content) => {
const addSpan = (content) => {
const span = document.createElement("span");
if (state.bold) {
span.classList.add("bold");
@@ -172,18 +124,15 @@ export class HaAnsiToHtml extends LitElement {
span.classList.add(`bg-${state.backgroundColor}`);
}
span.appendChild(document.createTextNode(content));
lineDiv.appendChild(span);
pre.appendChild(span);
};
/* eslint-disable no-cond-assign */
let match;
// eslint-disable-next-line
while ((match = re.exec(line)) !== null) {
while ((match = re.exec(text)) !== null) {
const j = match!.index;
const substring = line.substring(i, j);
if (substring) {
addPart(substring);
}
addSpan(text.substring(i, j));
i = j + match[0].length;
if (match[1] === undefined) {
@@ -285,93 +234,9 @@ export class HaAnsiToHtml extends LitElement {
}
});
}
addSpan(text.substring(i));
const substring = line.substring(i);
if (substring) {
addPart(substring);
}
if (top) {
this._pre?.prepend(lineDiv);
lineDiv.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 500 });
} else {
this._pre?.appendChild(lineDiv);
}
// filter new lines if a filter is set
if (this._filter) {
this.filterLines(this._filter);
}
}
public parseTextToColoredPre(text) {
const lines = text.split("\n");
for (const line of lines) {
this.parseLineToColoredPre(line);
}
}
/**
* Filter lines based on a search string, lines and search string will be converted to lowercase
* @param filter the search string
* @returns true if there are lines to display
*/
filterLines(filter: string): boolean {
this._filter = filter;
const lines = this.shadowRoot?.querySelectorAll("div") || [];
let numberOfFoundLines = 0;
if (!filter) {
lines.forEach((line) => {
line.style.display = "";
});
numberOfFoundLines = lines.length;
if (CSS.highlights) {
CSS.highlights.delete("search-results");
}
} else {
const highlightRanges: Range[] = [];
lines.forEach((line) => {
if (!line.textContent?.toLowerCase().includes(filter.toLowerCase())) {
line.style.display = "none";
} else {
line.style.display = "";
numberOfFoundLines++;
if (CSS.highlights && line.firstChild !== null && line.textContent) {
const spansOfLine = line.querySelectorAll("span");
spansOfLine.forEach((span) => {
const text = span.textContent.toLowerCase();
const indices: number[] = [];
let startPos = 0;
while (startPos < text.length) {
const index = text.indexOf(filter.toLowerCase(), startPos);
if (index === -1) break;
indices.push(index);
startPos = index + filter.length;
}
indices.forEach((index) => {
const range = new Range();
range.setStart(span.firstChild!, index);
range.setEnd(span.firstChild!, index + filter.length);
highlightRanges.push(range);
});
});
}
}
});
if (CSS.highlights) {
CSS.highlights.set("search-results", new Highlight(...highlightRanges));
}
}
return !!numberOfFoundLines;
}
public clear() {
if (this._pre) {
this._pre.innerHTML = "";
}
return pre;
}
}

View File

@@ -43,7 +43,7 @@ export class HaCard extends LitElement {
.card-header,
:host ::slotted(.card-header) {
color: var(--ha-card-header-color, var(--primary-text-color));
color: var(--ha-card-header-color, --primary-text-color);
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em;

View File

@@ -46,7 +46,7 @@ export class HaHeaderBar extends LitElement {
flex: none;
}
.mdc-top-app-bar__title {
padding-inline-start: 24px;
padding-inline-start: 20px;
padding-inline-end: initial;
}
`,

View File

@@ -216,7 +216,6 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
ha-input-chip {
--md-input-chip-selected-container-color: var(--color, var(--grey-color));
--ha-input-chip-selected-container-opacity: 0.5;
--md-input-chip-selected-outline-width: 1px;
}
`;
}

View File

@@ -86,11 +86,6 @@ export class HaMarkdown extends LitElement {
font-size: 1.5em;
font-weight: bold;
}
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: 16px 0;
}
`;
}
}

View File

@@ -1,5 +1,4 @@
import { PropertyValues, ReactiveElement } from "lit";
import { parseISO } from "date-fns";
import { customElement, property } from "lit/decorators";
import { relativeTime } from "../common/datetime/relative_time";
import { capitalizeFirstLetter } from "../common/string/capitalize-first-letter";
@@ -59,12 +58,7 @@ class HaRelativeTime extends ReactiveElement {
if (!this.datetime) {
this.innerHTML = this.hass.localize("ui.components.relative_time.never");
} else {
const date =
typeof this.datetime === "string"
? parseISO(this.datetime)
: this.datetime;
const relTime = relativeTime(date, this.hass.locale);
const relTime = relativeTime(new Date(this.datetime), this.hass.locale);
this.innerHTML = this.capitalize
? capitalizeFirstLetter(relTime)
: relTime;

View File

@@ -34,7 +34,6 @@ import {
expandLabelTarget,
Selector,
TargetSelector,
TemplateSelector,
} from "../data/selector";
import { HomeAssistant, ValueChangedEvent } from "../types";
import { documentationUrl } from "../util/documentation-url";
@@ -46,7 +45,6 @@ import "./ha-settings-row";
import "./ha-yaml-editor";
import type { HaYamlEditor } from "./ha-yaml-editor";
import "./ha-service-section-icon";
import { hasTemplate } from "../common/string/has-template";
const attributeFilter = (values: any[], attribute: any) => {
if (typeof attribute === "object") {
@@ -63,11 +61,6 @@ const showOptionalToggle = (field) =>
!field.required &&
!("boolean" in field.selector && field.default);
interface Field extends Omit<HassService["fields"][string], "selector"> {
key: string;
selector?: Selector;
}
interface ExtHassService extends Omit<HassService, "fields"> {
fields: Array<
Omit<HassService["fields"][string], "selector"> & {
@@ -77,12 +70,9 @@ interface ExtHassService extends Omit<HassService, "fields"> {
collapsed?: boolean;
}
>;
flatFields: Array<Field>;
hasSelector: string[];
}
const TEMPLATE_SELECTOR: TemplateSelector = { template: {} };
@customElement("ha-service-control")
export class HaServiceControl extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -187,7 +177,7 @@ export class HaServiceControl extends LitElement {
if (!this._value.data) {
this._value.data = {};
}
serviceData.flatFields.forEach((field) => {
serviceData.fields.forEach((field) => {
if (
field.selector &&
field.required &&
@@ -251,28 +241,22 @@ export class HaServiceControl extends LitElement {
selector: value.selector as Selector | undefined,
}));
const flatFields: Field[] = [];
const hasSelector: string[] = [];
fields.forEach((field) => {
if ((field as any).fields) {
Object.entries((field as any).fields).forEach(([key, subField]) => {
flatFields.push({ ...(subField as Field), key });
if ((subField as any).selector) {
hasSelector.push(key);
}
});
} else {
flatFields.push(field);
if (field.selector) {
hasSelector.push(field.key);
}
} else if (field.selector) {
hasSelector.push(field.key);
}
});
return {
...serviceDomains[domain][serviceName],
fields,
flatFields,
hasSelector,
};
}
@@ -413,7 +397,7 @@ export class HaServiceControl extends LitElement {
const hasOptional = Boolean(
!shouldRenderServiceDataYaml &&
serviceData?.flatFields.some((field) => showOptionalToggle(field))
serviceData?.fields.some((field) => showOptionalToggle(field))
);
const targetEntities = this._getTargetedEntities(
@@ -482,8 +466,7 @@ export class HaServiceControl extends LitElement {
>${this.hass.localize(
"ui.components.service-control.target_secondary"
)}</span
>
<ha-selector
><ha-selector
.hass=${this.hass}
.selector=${this._targetSelector(
serviceData.target as TargetSelector
@@ -644,34 +627,23 @@ export class HaServiceControl extends LitElement {
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
) || dataField?.description}</span
>
${hasTemplate(this._value?.data?.[dataField.key])
? html`
<ha-selector
.selector=${TEMPLATE_SELECTOR}
.key=${dataField.key}
.hass=${this.hass}
.value=${this._value?.data?.[dataField.key]}
.disabled=${this.disabled}
@value-changed=${this._serviceDataChanged}
></ha-selector>
`
: html`
<ha-selector
.disabled=${this.disabled ||
(showOptional &&
!this._checkedKeys.has(dataField.key) &&
(!this._value?.data ||
this._value.data[dataField.key] === undefined))}
.hass=${this.hass}
.selector=${enhancedSelector}
.key=${dataField.key}
@value-changed=${this._serviceDataChanged}
.value=${this._value?.data?.[dataField.key]}
.placeholder=${dataField.default}
.localizeValue=${this._localizeValueCallback}
@item-moved=${this._itemMoved}
></ha-selector>
`}
<ha-selector
.disabled=${this.disabled ||
(showOptional &&
!this._checkedKeys.has(dataField.key) &&
(!this._value?.data ||
this._value.data[dataField.key] === undefined))}
.hass=${this.hass}
.selector=${enhancedSelector}
.key=${dataField.key}
@value-changed=${this._serviceDataChanged}
.value=${this._value?.data
? this._value.data[dataField.key]
: undefined}
.placeholder=${dataField.default}
.localizeValue=${this._localizeValueCallback}
@item-moved=${this._itemMoved}
></ha-selector>
</ha-settings-row>`
: "";
};
@@ -695,7 +667,7 @@ export class HaServiceControl extends LitElement {
const field = this._getServiceInfo(
this._value?.action,
this.hass.services
)?.flatFields.find((_field) => _field.key === key);
)?.fields.find((_field) => _field.key === key);
let defaultValue = field?.default;

View File

@@ -42,17 +42,14 @@ export class HaSettingsRow extends LitElement {
padding-bottom: 8px;
padding-left: 0;
padding-inline-start: 0;
padding-right: 16px;
padding-right: 16x;
padding-inline-end: 16px;
overflow: hidden;
display: var(--layout-vertical_-_display, flex);
flex-direction: var(--layout-vertical_-_flex-direction, column);
justify-content: var(
--layout-center-justified_-_justify-content,
center
);
flex: var(--layout-flex_-_flex, 1);
flex-basis: var(--layout-flex_-_flex-basis, 0.000000001px);
display: var(--layout-vertical_-_display);
flex-direction: var(--layout-vertical_-_flex-direction);
justify-content: var(--layout-center-justified_-_justify-content);
flex: var(--layout-flex_-_flex);
flex-basis: var(--layout-flex_-_flex-basis);
}
.body[three-line] {
min-height: var(--paper-item-body-three-line-min-height, 88px);

View File

@@ -859,14 +859,11 @@ class HaSidebar extends SubscribeMixin(LitElement) {
border-bottom: 1px solid transparent;
white-space: nowrap;
font-weight: 400;
color: var(
--sidebar-menu-button-text-color,
var(--primary-text-color)
);
color: var(--sidebar-menu-button-text-color, --primary-text-color);
border-bottom: 1px solid var(--divider-color);
background-color: var(
--sidebar-menu-button-background-color,
var(--primary-background-color)
--primary-background-color
);
font-size: 20px;
align-items: center;

View File

@@ -24,7 +24,7 @@ export class HaTopAppBarFixed extends TopAppBarFixedBase {
);
}
.mdc-top-app-bar__title {
padding-inline-start: 24px;
padding-inline-start: 20px;
padding-inline-end: initial;
}
`,

View File

@@ -321,7 +321,7 @@ export class TopAppBarBaseBase extends BaseElement {
overflow: auto;
}
.mdc-top-app-bar__title {
padding-inline-start: 24px;
padding-inline-start: 20px;
padding-inline-end: initial;
}
`,

View File

@@ -1,5 +1,4 @@
/* eslint-disable no-console */
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
@@ -12,12 +11,9 @@ import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import {
addWebRtcCandidate,
fetchWebRtcClientConfiguration,
handleWebRtcOffer,
WebRtcAnswer,
WebRTCClientConfiguration,
webRtcOffer,
WebRtcOfferEvent,
} from "../data/camera";
import type { HomeAssistant } from "../types";
import "./ha-alert";
@@ -31,7 +27,7 @@ import "./ha-alert";
class HaWebRtcPlayer extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public entityid?: string;
@property() public entityid!: string;
@property({ type: Boolean, attribute: "controls" })
public controls = false;
@@ -49,20 +45,12 @@ class HaWebRtcPlayer extends LitElement {
@state() private _error?: string;
@query("#remote-stream") private _videoEl!: HTMLVideoElement;
private _clientConfig?: WebRTCClientConfiguration;
@query("#remote-stream", true) private _videoEl!: HTMLVideoElement;
private _peerConnection?: RTCPeerConnection;
private _remoteStream?: MediaStream;
private _unsub?: Promise<UnsubscribeFunc>;
private _sessionId?: string;
private _candidatesList: string[] = [];
protected override render(): TemplateResult {
if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
@@ -82,7 +70,7 @@ class HaWebRtcPlayer extends LitElement {
public override connectedCallback() {
super.connectedCallback();
if (this.hasUpdated && this.entityid) {
if (this.hasUpdated) {
this._startWebRtc();
}
}
@@ -92,8 +80,7 @@ class HaWebRtcPlayer extends LitElement {
this._cleanUp();
}
protected override willUpdate(changedProperties: PropertyValues<this>) {
super.willUpdate(changedProperties);
protected override updated(changedProperties: PropertyValues<this>) {
if (!changedProperties.has("entityid")) {
return;
}
@@ -101,68 +88,28 @@ class HaWebRtcPlayer extends LitElement {
}
private async _startWebRtc(): Promise<void> {
this._cleanUp();
if (!this.hass || !this.entityid) {
return;
}
console.time("WebRTC");
this._error = undefined;
console.timeLog("WebRTC", "start clientConfig");
this._clientConfig = await fetchWebRtcClientConfiguration(
const clientConfig = await fetchWebRtcClientConfiguration(
this.hass,
this.entityid
);
console.timeLog("WebRTC", "end clientConfig", this._clientConfig);
console.timeLog("WebRTC", "end clientConfig", clientConfig);
this._peerConnection = new RTCPeerConnection(
this._clientConfig.configuration
);
const peerConnection = new RTCPeerConnection(clientConfig.configuration);
if (this._clientConfig.dataChannel) {
if (clientConfig.dataChannel) {
// Some cameras (such as nest) require a data channel to establish a stream
// however, not used by any integrations.
this._peerConnection.createDataChannel(this._clientConfig.dataChannel);
}
this._peerConnection.onnegotiationneeded = this._startNegotiation;
this._peerConnection.onicecandidate = this._handleIceCandidate;
this._peerConnection.oniceconnectionstatechange =
this._iceConnectionStateChanged;
// just for debugging
this._peerConnection.onsignalingstatechange = (ev) => {
switch ((ev.target as RTCPeerConnection).signalingState) {
case "stable":
console.timeLog("WebRTC", "ICE negotiation complete");
break;
default:
console.timeLog(
"WebRTC",
"Signaling state changed",
(ev.target as RTCPeerConnection).signalingState
);
}
};
// Setup callbacks to render remote stream once media tracks are discovered.
this._remoteStream = new MediaStream();
this._peerConnection.ontrack = this._addTrack;
this._peerConnection.addTransceiver("audio", { direction: "recvonly" });
this._peerConnection.addTransceiver("video", { direction: "recvonly" });
}
private _startNegotiation = async () => {
if (!this._peerConnection) {
return;
peerConnection.createDataChannel(clientConfig.dataChannel);
}
peerConnection.addTransceiver("audio", { direction: "recvonly" });
peerConnection.addTransceiver("video", { direction: "recvonly" });
const offerOptions: RTCOfferOptions = {
offerToReceiveAudio: true,
@@ -172,218 +119,98 @@ class HaWebRtcPlayer extends LitElement {
console.timeLog("WebRTC", "start createOffer", offerOptions);
const offer: RTCSessionDescriptionInit =
await this._peerConnection.createOffer(offerOptions);
if (!this._peerConnection) {
return;
}
await peerConnection.createOffer(offerOptions);
console.timeLog("WebRTC", "end createOffer", offer);
console.timeLog("WebRTC", "start setLocalDescription");
await this._peerConnection.setLocalDescription(offer);
if (!this._peerConnection || !this.entityid) {
return;
}
await peerConnection.setLocalDescription(offer);
console.timeLog("WebRTC", "end setLocalDescription");
let candidates = "";
console.timeLog("WebRTC", "start iceResolver");
if (this._clientConfig?.getCandidatesUpfront) {
await new Promise<void>((resolve) => {
this._peerConnection!.onicegatheringstatechange = (ev: Event) => {
const iceGatheringState = (ev.target as RTCPeerConnection)
.iceGatheringState;
if (iceGatheringState === "complete") {
this._peerConnection!.onicegatheringstatechange = null;
resolve();
}
console.timeLog(
"WebRTC",
"Ice gathering state changed",
iceGatheringState
);
};
let candidates = ""; // Build an Offer SDP string with ice candidates
const iceResolver = new Promise<void>((resolve) => {
peerConnection.addEventListener("icecandidate", (event) => {
if (!event.candidate?.candidate) {
resolve(); // Gathering complete
return;
}
console.timeLog("WebRTC", "iceResolver candidate", event.candidate);
candidates += `a=${event.candidate.candidate}\r\n`;
});
});
await iceResolver;
if (!this._peerConnection || !this.entityid) {
return;
}
}
while (this._candidatesList.length) {
const candidate = this._candidatesList.pop();
if (candidate) {
candidates += `a=${candidate}\r\n`;
}
}
console.timeLog("WebRTC", "end iceResolver", candidates);
const offer_sdp = offer.sdp! + candidates;
console.timeLog("WebRTC", "start webRtcOffer", offer_sdp);
let webRtcAnswer: WebRtcAnswer;
try {
this._unsub = webRtcOffer(this.hass, this.entityid, offer_sdp, (event) =>
this._handleOfferEvent(event)
);
} catch (err: any) {
this._error = "Failed to start WebRTC stream: " + err.message;
this._cleanUp();
}
};
private _iceConnectionStateChanged = () => {
console.timeLog(
"WebRTC",
"ice connection state change",
this._peerConnection?.iceConnectionState
);
if (this._peerConnection?.iceConnectionState === "failed") {
this._peerConnection.restartIce();
}
};
private async _handleOfferEvent(event: WebRtcOfferEvent) {
if (!this.entityid) {
return;
}
if (event.type === "session") {
this._sessionId = event.session_id;
this._candidatesList.forEach((candidate) =>
addWebRtcCandidate(
this.hass,
this.entityid!,
event.session_id,
candidate
)
);
this._candidatesList = [];
}
if (event.type === "answer") {
console.timeLog("WebRTC", "answer", event.answer);
this._handleAnswer(event);
}
if (event.type === "candidate") {
console.timeLog("WebRTC", "remote ice candidate", event.candidate);
try {
await this._peerConnection?.addIceCandidate(
new RTCIceCandidate({ candidate: event.candidate, sdpMid: "0" })
);
} catch (err: any) {
console.error(err);
}
}
if (event.type === "error") {
this._error = "Failed to start WebRTC stream: " + event.message;
this._cleanUp();
}
}
private _handleIceCandidate = (event: RTCPeerConnectionIceEvent) => {
if (!this.entityid || !event.candidate?.candidate) {
return;
}
console.timeLog(
"WebRTC",
"local ice candidate",
event.candidate?.candidate
);
if (this._sessionId) {
addWebRtcCandidate(
console.timeLog("WebRTC", "start WebRTCOffer", offer_sdp);
webRtcAnswer = await handleWebRtcOffer(
this.hass,
this.entityid,
this._sessionId,
event.candidate?.candidate
offer_sdp
);
} else {
this._candidatesList.push(event.candidate?.candidate);
}
};
private _addTrack = async (event: RTCTrackEvent) => {
if (!this._remoteStream) {
console.timeLog("WebRTC", "end webRtcOffer", webRtcAnswer);
} catch (err: any) {
this._error = "Failed to start WebRTC stream: " + err.message;
peerConnection.close();
return;
}
this._remoteStream.addTrack(event.track);
if (!this.hasUpdated) {
await this.updateComplete;
}
this._videoEl.srcObject = this._remoteStream;
};
private async _handleAnswer(event: WebRtcAnswer) {
if (
!this._peerConnection?.signalingState ||
["stable", "closed"].includes(this._peerConnection.signalingState)
) {
return;
}
// Setup callbacks to render remote stream once media tracks are discovered.
const remoteStream = new MediaStream();
peerConnection.addEventListener("track", (event) => {
console.timeLog("WebRTC", "track", event);
remoteStream.addTrack(event.track);
this._videoEl.srcObject = remoteStream;
});
this._remoteStream = remoteStream;
// Initiate the stream with the remote device
const remoteDesc = new RTCSessionDescription({
type: "answer",
sdp: event.answer,
sdp: webRtcAnswer.answer,
});
try {
console.timeLog("WebRTC", "start setRemoteDescription", remoteDesc);
await this._peerConnection.setRemoteDescription(remoteDesc);
await peerConnection.setRemoteDescription(remoteDesc);
console.timeLog("WebRTC", "end setRemoteDescription");
} catch (err: any) {
this._error = "Failed to connect WebRTC stream: " + err.message;
this._cleanUp();
peerConnection.close();
return;
}
console.timeLog("WebRTC", "end setRemoteDescription");
this._peerConnection = peerConnection;
}
private _cleanUp() {
console.timeLog("WebRTC", "stopped");
console.timeEnd("WebRTC");
if (this._remoteStream) {
this._remoteStream.getTracks().forEach((track) => {
track.stop();
});
this._remoteStream = undefined;
}
const videoEl = this._videoEl;
if (videoEl) {
videoEl.removeAttribute("src");
videoEl.load();
if (this._videoEl) {
this._videoEl.removeAttribute("src");
this._videoEl.load();
}
if (this._peerConnection) {
this._peerConnection.close();
this._peerConnection.onnegotiationneeded = null;
this._peerConnection.onicecandidate = null;
this._peerConnection.oniceconnectionstatechange = null;
this._peerConnection.onicegatheringstatechange = null;
this._peerConnection.ontrack = null;
// just for debugging
this._peerConnection.onsignalingstatechange = null;
this._peerConnection = undefined;
}
this._unsub?.then((unsub) => unsub());
this._unsub = undefined;
this._sessionId = undefined;
this._candidatesList = [];
}
private _loadedData() {
// @ts-ignore
fireEvent(this, "load");
console.timeLog("WebRTC", "loadedData");
console.timeEnd("WebRTC");
// @ts-ignore
fireEvent(this, "load");
}
static get styles(): CSSResultGroup {

View File

@@ -43,7 +43,6 @@ class HaEntityMarker extends LitElement {
.marker {
display: flex;
justify-content: center;
text-align: center;
align-items: center;
box-sizing: border-box;
width: 48px;

View File

@@ -22,13 +22,8 @@ import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_tim
import { relativeTime } from "../../common/datetime/relative_time";
import { fireEvent } from "../../common/dom/fire_event";
import { toggleAttribute } from "../../common/dom/toggle_attribute";
import {
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../data/context";
import { fullEntitiesContext, labelsContext } from "../../data/context";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { FloorRegistryEntry } from "../../data/floor_registry";
import { LabelRegistryEntry } from "../../data/label_registry";
import { LogbookEntry } from "../../data/logbook";
import {
@@ -206,7 +201,6 @@ class ActionRenderer {
private hass: HomeAssistant,
private entityReg: EntityRegistryEntry[],
private labelReg: LabelRegistryEntry[],
private floorReg: { [id: string]: FloorRegistryEntry },
private entries: TemplateResult[],
private trace: AutomationTraceExtended,
private logbookRenderer: LogbookRenderer,
@@ -325,7 +319,6 @@ class ActionRenderer {
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
data,
actionType
),
@@ -493,13 +486,7 @@ class ActionRenderer {
const name =
repeatConfig.alias ||
describeAction(
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
repeatConfig
);
describeAction(this.hass, this.entityReg, this.labelReg, repeatConfig);
this._renderEntry(repeatPath, name, undefined, disabled);
@@ -597,7 +584,6 @@ class ActionRenderer {
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
sequenceConfig,
"sequence"
),
@@ -694,10 +680,6 @@ export class HaAutomationTracer extends LitElement {
@consume({ context: labelsContext, subscribe: true })
_labelReg!: LabelRegistryEntry[];
@state()
@consume({ context: floorsContext, subscribe: true })
_floorReg!: { [id: string]: FloorRegistryEntry };
protected render() {
if (!this.trace) {
return nothing;
@@ -715,7 +697,6 @@ export class HaAutomationTracer extends LitElement {
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
entries,
this.trace,
logbookRenderer,

View File

@@ -39,37 +39,10 @@ export interface Stream {
url: string;
}
export type WebRtcOfferEvent =
| WebRtcId
| WebRtcAnswer
| WebRtcCandidate
| WebRtcError;
export interface WebRtcId {
type: "session";
session_id: string;
}
export interface WebRtcAnswer {
type: "answer";
answer: string;
}
export interface WebRtcCandidate {
type: "candidate";
candidate: string;
}
export interface WebRtcError {
type: "error";
code: string;
message: string;
}
export interface WebRtcOfferResponse {
id: string;
}
export const cameraUrlWithWidthHeight = (
base_url: string,
width: number,
@@ -121,29 +94,15 @@ export const fetchStreamUrl = async (
return stream;
};
export const webRtcOffer = (
export const handleWebRtcOffer = (
hass: HomeAssistant,
entity_id: string,
offer: string,
callback: (event: WebRtcOfferEvent) => void
entityId: string,
offer: string
) =>
hass.connection.subscribeMessage<WebRtcOfferEvent>(callback, {
type: "camera/webrtc/offer",
entity_id,
offer,
});
export const addWebRtcCandidate = (
hass: HomeAssistant,
entity_id: string,
session_id: string,
candidate: string
) =>
hass.callWS({
type: "camera/webrtc/candidate",
entity_id,
session_id,
candidate,
hass.callWS<WebRtcAnswer>({
type: "camera/web_rtc_offer",
entity_id: entityId,
offer: offer,
});
export const fetchCameraPrefs = (hass: HomeAssistant, entityId: string) =>
@@ -178,7 +137,6 @@ export const getEntityIdFromCameraMediaSource = (mediaContentId: string) =>
export interface WebRTCClientConfiguration {
configuration: RTCConfiguration;
dataChannel?: string;
getCandidatesUpfront: boolean;
}
export const fetchWebRtcClientConfiguration = async (

View File

@@ -27,6 +27,4 @@ export const panelsContext = createContext<HomeAssistant["panels"]>("panels");
export const fullEntitiesContext =
createContext<EntityRegistryEntry[]>("extendedEntities");
export const floorsContext = createContext<HomeAssistant["floors"]>("floors");
export const labelsContext = createContext<LabelRegistryEntry[]>("labels");

View File

@@ -65,7 +65,7 @@ export const countryCurrency = {
HK: "HKD",
HN: "HNL",
HM: "AUD",
VE: "VED",
VE: "VEF",
PR: "USD",
PS: "ILS",
PW: "USD",

View File

@@ -358,24 +358,21 @@ export const restartHassioAddon = async (
export const uninstallHassioAddon = async (
hass: HomeAssistant,
slug: string,
removeData: boolean
): Promise<void> => {
slug: string
) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/uninstall`,
method: "post",
timeout: null,
data: { remove_config: removeData },
});
return;
}
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}/uninstall`,
{ remove_config: removeData }
`hassio/addons/${slug}/uninstall`
);
};

View File

@@ -17,7 +17,7 @@ export interface NetworkInterface {
ipv4?: Partial<IpConfiguration>;
ipv6?: Partial<IpConfiguration>;
type: "ethernet" | "wireless" | "vlan";
wifi?: Partial<WifiConfiguration> | null;
wifi?: Partial<WifiConfiguration>;
}
interface DockerNetwork {
@@ -27,7 +27,7 @@ interface DockerNetwork {
interface: string;
}
export interface AccessPoint {
interface AccessPoint {
mode: "infrastructure" | "mesh" | "adhoc" | "ap";
ssid: string;
mac: string;

View File

@@ -65,10 +65,6 @@ export type HassioInfo = {
timezone: string;
};
export type HassioBoots = {
boots: Record<number, string>;
};
export type HassioPanelInfo = PanelInfo<
| undefined
| {
@@ -181,39 +177,10 @@ export const fetchHassioInfo = async (
);
};
export const fetchHassioBoots = async (hass: HomeAssistant) =>
hass.callApi<HassioResponse<HassioBoots>>("GET", `hassio/host/logs/boots`);
export const fetchHassioLogs = async (
hass: HomeAssistant,
provider: string,
range?: string,
boot = 0
) =>
hass.callApiRaw(
export const fetchHassioLogs = async (hass: HomeAssistant, provider: string) =>
hass.callApi<string>(
"GET",
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs/boots/${boot}`,
undefined,
range
? {
Range: range,
}
: undefined
);
export const fetchHassioLogsFollow = async (
hass: HomeAssistant,
provider: string,
signal: AbortSignal,
lines = 100,
boot = 0
) =>
hass.callApiRaw(
"GET",
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs/boots/${boot}/follow?lines=${lines}`,
undefined,
undefined,
signal
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs`
);
export const getHassioLogDownloadUrl = (provider: string) =>
@@ -221,15 +188,6 @@ export const getHassioLogDownloadUrl = (provider: string) =>
provider.includes("_") ? `addons/${provider}` : provider
}/logs`;
export const getHassioLogDownloadLinesUrl = (
provider: string,
lines: number,
boot = 0
) =>
`/api/hassio/${
provider.includes("_") ? `addons/${provider}` : provider
}/logs/boots/${boot}?lines=${lines}`;
export const setSupervisorOption = async (
hass: HomeAssistant,
data: SupervisorOptions

View File

@@ -14,7 +14,6 @@ import {
computeEntityRegistryName,
entityRegistryById,
} from "./entity_registry";
import { FloorRegistryEntry } from "./floor_registry";
import { domainToName } from "./integration";
import { LabelRegistryEntry } from "./label_registry";
import {
@@ -44,7 +43,6 @@ export const describeAction = <T extends ActionType>(
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
labelRegistry: LabelRegistryEntry[],
floorRegistry: { [id: string]: FloorRegistryEntry },
action: ActionTypes[T],
actionType?: T,
ignoreAlias = false
@@ -54,7 +52,6 @@ export const describeAction = <T extends ActionType>(
hass,
entityRegistry,
labelRegistry,
floorRegistry,
action,
actionType,
ignoreAlias
@@ -78,7 +75,6 @@ const tryDescribeAction = <T extends ActionType>(
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
labelRegistry: LabelRegistryEntry[],
floorRegistry: { [id: string]: FloorRegistryEntry },
action: ActionTypes[T],
actionType?: T,
ignoreAlias = false
@@ -168,7 +164,7 @@ const tryDescribeAction = <T extends ActionType>(
);
}
} else if (key === "floor_id") {
const floor = floorRegistry[targetThing] ?? undefined;
const floor = hass.floors[targetThing] ?? undefined;
if (floor?.name) {
targets.push(floor.name);
} else {

View File

@@ -8,7 +8,6 @@ import { BINARY_STATE_ON } from "../common/const";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { supportsFeature } from "../common/entity/supports-feature";
import { formatNumber } from "../common/number/format_number";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { HomeAssistant } from "../types";
@@ -24,15 +23,13 @@ export enum UpdateEntityFeature {
interface UpdateEntityAttributes extends HassEntityAttributeBase {
auto_update: boolean | null;
display_precision: number;
installed_version: string | null;
in_progress: boolean;
in_progress: boolean | number;
latest_version: string | null;
release_summary: string | null;
release_url: string | null;
skipped_version: string | null;
title: string | null;
update_percentage: number | null;
}
export interface UpdateEntity extends HassEntityBase {
@@ -41,7 +38,7 @@ export interface UpdateEntity extends HassEntityBase {
export const updateUsesProgress = (entity: UpdateEntity): boolean =>
supportsFeature(entity, UpdateEntityFeature.PROGRESS) &&
entity.attributes.update_percentage !== null;
typeof entity.attributes.in_progress === "number";
export const updateCanInstall = (
entity: UpdateEntity,
@@ -52,7 +49,7 @@ export const updateCanInstall = (
supportsFeature(entity, UpdateEntityFeature.INSTALL);
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
!!entity.attributes.in_progress;
updateUsesProgress(entity) || !!entity.attributes.in_progress;
export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
hass.callWS<string | null>({
@@ -186,13 +183,10 @@ export const computeUpdateStateDisplay = (
if (updateIsInstalling(stateObj)) {
const supportsProgress =
supportsFeature(stateObj, UpdateEntityFeature.PROGRESS) &&
attributes.update_percentage !== null;
typeof attributes.in_progress === "number";
if (supportsProgress) {
return hass.localize("ui.card.update.installing_with_progress", {
progress: formatNumber(attributes.update_percentage!, hass.locale, {
maximumFractionDigits: attributes.display_precision,
minimumFractionDigits: attributes.display_precision,
}),
progress: attributes.in_progress as number,
});
}
return hass.localize("ui.card.update.installing");

View File

@@ -31,9 +31,6 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [
"valve",
"water_heater",
];
/** Domains with full height more info dialog */
export const DOMAINS_FULL_HEIGHT_MORE_INFO = ["update"];
/** Domains with separate more info dialog. */
export const DOMAINS_WITH_MORE_INFO = [
"alarm_control_panel",

View File

@@ -93,13 +93,12 @@ class MoreInfoCover extends LitElement {
supportsFeature(this.stateObj, CoverEntityFeature.CLOSE_TILT) ||
supportsFeature(this.stateObj, CoverEntityFeature.STOP_TILT);
const supportsOpenCloseOnly =
const supportsOpenCloseWithoutStop =
supportsFeature(this.stateObj, CoverEntityFeature.OPEN) &&
supportsFeature(this.stateObj, CoverEntityFeature.CLOSE) &&
!supportsFeature(this.stateObj, CoverEntityFeature.STOP) &&
!supportsTilt &&
!supportsPosition &&
!supportsTiltPosition;
!supportsFeature(this.stateObj, CoverEntityFeature.OPEN_TILT) &&
!supportsFeature(this.stateObj, CoverEntityFeature.CLOSE_TILT);
return html`
<ha-more-info-state-header
@@ -134,7 +133,7 @@ class MoreInfoCover extends LitElement {
${
this._mode === "button"
? html`
${supportsOpenCloseOnly
${supportsOpenCloseWithoutStop
? html`
<ha-state-control-cover-toggle
.stateObj=${this.stateObj}

View File

@@ -1,18 +1,15 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-linear-progress/mwc-linear-progress";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { BINARY_STATE_OFF } from "../../../common/const";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-checkbox";
import "../../../components/ha-circular-progress";
import "../../../components/ha-faded";
import "../../../components/ha-formfield";
import "../../../components/ha-markdown";
import "../../../components/ha-settings-row";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import { isUnavailableState } from "../../../data/entity";
import {
UpdateEntity,
@@ -33,8 +30,6 @@ class MoreInfoUpdate extends LitElement {
@state() private _error?: string;
@state() private _markdownLoading = true;
protected render() {
if (
!this.hass ||
@@ -50,174 +45,137 @@ class MoreInfoUpdate extends LitElement {
this.stateObj.attributes.latest_version;
return html`
<div class="content">
${this.stateObj.attributes.in_progress
? supportsFeature(this.stateObj, UpdateEntityFeature.PROGRESS) &&
this.stateObj.attributes.update_percentage !== null
? html`<mwc-linear-progress
.progress=${this.stateObj.attributes.update_percentage / 100}
buffer=""
></mwc-linear-progress>`
: html`<mwc-linear-progress indeterminate></mwc-linear-progress>`
: nothing}
<h3>${this.stateObj.attributes.title}</h3>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<div class="row">
<div class="key">
${this.hass.formatEntityAttributeName(
this.stateObj,
"installed_version"
)}
</div>
<div class="value">
${this.stateObj.attributes.installed_version ??
this.hass.localize("state.default.unavailable")}
</div>
${this.stateObj.attributes.in_progress
? supportsFeature(this.stateObj, UpdateEntityFeature.PROGRESS) &&
typeof this.stateObj.attributes.in_progress === "number"
? html`<mwc-linear-progress
.progress=${this.stateObj.attributes.in_progress / 100}
buffer=""
></mwc-linear-progress>`
: html`<mwc-linear-progress indeterminate></mwc-linear-progress>`
: ""}
<h3>${this.stateObj.attributes.title}</h3>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="row">
<div class="key">
${this.hass.formatEntityAttributeName(
this.stateObj,
"installed_version"
)}
</div>
<div class="row">
<div class="key">
${this.hass.formatEntityAttributeName(
this.stateObj,
"latest_version"
)}
</div>
<div class="value">
${this.stateObj.attributes.latest_version ??
this.hass.localize("state.default.unavailable")}
</div>
<div class="value">
${this.stateObj.attributes.installed_version ??
this.hass.localize("state.default.unavailable")}
</div>
</div>
<div class="row">
<div class="key">
${this.hass.formatEntityAttributeName(
this.stateObj,
"latest_version"
)}
</div>
<div class="value">
${this.stateObj.attributes.latest_version ??
this.hass.localize("state.default.unavailable")}
</div>
</div>
${this.stateObj.attributes.release_url
? html`<div class="row">
<div class="key">
<a
href=${this.stateObj.attributes.release_url}
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.release_announcement"
)}
</a>
</div>
${this.stateObj.attributes.release_url
? html`<div class="row">
<div class="key">
<a
href=${this.stateObj.attributes.release_url}
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.release_announcement"
)}
</a>
</div>
</div>`
: ""}
${supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES) &&
!this._error
? this._releaseNotes === undefined
? html`<div class="flex center">
<ha-circular-progress indeterminate></ha-circular-progress>
</div>`
: nothing}
${supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES) &&
!this._error
? this._releaseNotes === undefined
? html`
<hr />
${this._markdownLoading ? this._renderLoader() : nothing}
`
: html`
<hr />
<ha-markdown
@content-resize=${this._markdownLoaded}
.content=${this._releaseNotes}
class=${this._markdownLoading ? "hidden" : ""}
></ha-markdown>
${this._markdownLoading ? this._renderLoader() : nothing}
`
: this.stateObj.attributes.release_summary
? html`
<hr />
<ha-markdown
@content-resize=${this._markdownLoaded}
.content=${this.stateObj.attributes.release_summary}
class=${this._markdownLoading ? "hidden" : ""}
></ha-markdown>
${this._markdownLoading ? this._renderLoader() : nothing}
`
: nothing}
</div>
<div class="footer">
${supportsFeature(this.stateObj, UpdateEntityFeature.BACKUP)
: html`<hr />
<ha-faded>
<ha-markdown .content=${this._releaseNotes}></ha-markdown>
</ha-faded> `
: this.stateObj.attributes.release_summary
? html`<hr />
<ha-markdown
.content=${this.stateObj.attributes.release_summary}
></ha-markdown>`
: ""}
${supportsFeature(this.stateObj, UpdateEntityFeature.BACKUP)
? html`<hr />
<ha-formfield
.label=${this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup"
)}
>
<ha-checkbox
checked
.disabled=${updateIsInstalling(this.stateObj)}
></ha-checkbox>
</ha-formfield> `
: ""}
<div class="actions">
${this.stateObj.state === BINARY_STATE_OFF &&
this.stateObj.attributes.skipped_version
? html`
<ha-settings-row>
<span slot="heading">
${this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup"
)}
</span>
<ha-switch
id="create_backup"
checked
.disabled=${updateIsInstalling(this.stateObj)}
></ha-switch>
</ha-settings-row>
<mwc-button @click=${this._handleClearSkipped}>
${this.hass.localize(
"ui.dialogs.more_info_control.update.clear_skipped"
)}
</mwc-button>
`
: nothing}
<div class="actions">
${this.stateObj.state === BINARY_STATE_OFF &&
this.stateObj.attributes.skipped_version
? html`
<ha-button @click=${this._handleClearSkipped}>
${this.hass.localize(
"ui.dialogs.more_info_control.update.clear_skipped"
)}
</ha-button>
`
: html`
<ha-button
@click=${this._handleSkip}
.disabled=${skippedVersion ||
this.stateObj.state === BINARY_STATE_OFF ||
updateIsInstalling(this.stateObj)}
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.skip"
)}
</ha-button>
`}
${supportsFeature(this.stateObj, UpdateEntityFeature.INSTALL)
? html`
<ha-button
@click=${this._handleInstall}
.disabled=${(this.stateObj.state === BINARY_STATE_OFF &&
!skippedVersion) ||
updateIsInstalling(this.stateObj)}
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.update"
)}
</ha-button>
`
: nothing}
</div>
</div>
`;
}
private _renderLoader() {
return html`
<div class="flex center loader">
<ha-circular-progress indeterminate></ha-circular-progress>
: html`
<mwc-button
@click=${this._handleSkip}
.disabled=${skippedVersion ||
this.stateObj.state === BINARY_STATE_OFF ||
updateIsInstalling(this.stateObj)}
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.skip"
)}
</mwc-button>
`}
${supportsFeature(this.stateObj, UpdateEntityFeature.INSTALL)
? html`
<mwc-button
@click=${this._handleInstall}
.disabled=${(this.stateObj.state === BINARY_STATE_OFF &&
!skippedVersion) ||
updateIsInstalling(this.stateObj)}
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.install"
)}
</mwc-button>
`
: ""}
</div>
`;
}
protected firstUpdated(): void {
if (supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES)) {
this._fetchReleaseNotes();
}
}
private async _markdownLoaded() {
if (this._markdownLoading) {
this._markdownLoading = false;
}
}
private async _fetchReleaseNotes() {
try {
this._releaseNotes = await updateReleaseNotes(
this.hass,
this.stateObj!.entity_id
);
} catch (err: any) {
this._error = err.message;
updateReleaseNotes(this.hass, this.stateObj!.entity_id)
.then((result) => {
this._releaseNotes = result;
})
.catch((err) => {
this._error = err.message;
});
}
}
@@ -225,11 +183,9 @@ class MoreInfoUpdate extends LitElement {
if (!supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
return null;
}
const createBackupSwitch = this.shadowRoot?.getElementById(
"create-backup"
) as HaSwitch;
if (createBackupSwitch) {
return createBackupSwitch.checked;
const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
if (checkbox) {
return checkbox.checked;
}
return true;
}
@@ -278,12 +234,6 @@ class MoreInfoUpdate extends LitElement {
static get styles(): CSSResultGroup {
return css`
:host {
display: flex;
flex-direction: column;
flex: 1;
justify-content: space-between;
}
hr {
border-color: var(--divider-color);
border-bottom: none;
@@ -298,44 +248,26 @@ class MoreInfoUpdate extends LitElement {
flex-direction: row;
justify-content: space-between;
}
.footer {
.actions {
border-top: 1px solid var(--divider-color);
background: var(
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
margin: 8px 0 0;
display: flex;
flex-wrap: wrap;
justify-content: center;
position: sticky;
bottom: 0;
margin: 0 -24px -24px -24px;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
z-index: 10;
}
ha-settings-row {
width: 100%;
padding: 0 24px;
box-sizing: border-box;
margin-bottom: -16px;
margin-top: -4px;
}
.actions {
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-end;
box-sizing: border-box;
padding: 12px;
padding: 12px 0;
margin-bottom: -24px;
z-index: 1;
gap: 8px;
}
.actions mwc-button {
margin: 0 4px 4px;
}
a {
color: var(--primary-color);
}
@@ -350,16 +282,6 @@ class MoreInfoUpdate extends LitElement {
}
ha-markdown {
direction: ltr;
padding-bottom: 16px;
box-sizing: border-box;
}
ha-markdown.hidden {
display: none;
}
.loader {
height: 80px;
box-sizing: border-box;
padding-bottom: 16px;
}
`;
}

View File

@@ -83,11 +83,10 @@ class MoreInfoValve extends LitElement {
supportsFeature(this.stateObj, ValveEntityFeature.CLOSE) ||
supportsFeature(this.stateObj, ValveEntityFeature.STOP);
const supportsOpenCloseOnly =
const supportsOpenCloseWithoutStop =
supportsFeature(this.stateObj, ValveEntityFeature.OPEN) &&
supportsFeature(this.stateObj, ValveEntityFeature.CLOSE) &&
!supportsFeature(this.stateObj, ValveEntityFeature.STOP) &&
!supportsPosition;
!supportsFeature(this.stateObj, ValveEntityFeature.STOP);
return html`
<ha-more-info-state-header
@@ -114,7 +113,7 @@ class MoreInfoValve extends LitElement {
${
this._mode === "button"
? html`
${supportsOpenCloseOnly
${supportsOpenCloseWithoutStop
? html`
<ha-state-control-valve-toggle
.stateObj=${this.stateObj}

View File

@@ -9,7 +9,6 @@ import {
computeShowHistoryComponent,
computeShowLogBookComponent,
computeShowNewMoreInfo,
DOMAINS_FULL_HEIGHT_MORE_INFO,
DOMAINS_NO_INFO,
DOMAINS_WITH_MORE_INFO,
} from "./const";
@@ -41,8 +40,6 @@ export class MoreInfoInfo extends LitElement {
const entityRegObj = this.hass.entities[entityId];
const domain = computeDomain(entityId);
const isNewMoreInfo = stateObj && computeShowNewMoreInfo(stateObj);
const isFullHeight =
isNewMoreInfo || DOMAINS_FULL_HEIGHT_MORE_INFO.includes(domain);
return html`
<div class="container" data-domain=${domain}>
@@ -92,7 +89,7 @@ export class MoreInfoInfo extends LitElement {
.entityId=${this.entityId}
></ha-more-info-logbook>`}
<more-info-content
?full-height=${isFullHeight}
?full-height=${isNewMoreInfo}
.stateObj=${stateObj}
.hass=${this.hass}
.entry=${this.entry}

View File

@@ -39,9 +39,6 @@ export const AssistantSetupStyles = [
.footer.full-width ha-button {
width: 100%;
}
.footer.centered {
justify-content: center;
}
.footer.side-by-side {
justify-content: space-between;
}

View File

@@ -14,6 +14,7 @@ import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { VoiceAssistantSetupDialogParams } from "./show-voice-assistant-setup-dialog";
import "./voice-assistant-setup-step-addons";
import "./voice-assistant-setup-step-area";
import "./voice-assistant-setup-step-change-wake-word";
import "./voice-assistant-setup-step-check";
@@ -33,6 +34,7 @@ export const enum STEP {
PIPELINE,
SUCCESS,
CLOUD,
ADDONS,
CHANGE_WAKEWORD,
}
@@ -208,18 +210,22 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
? html`<ha-voice-assistant-setup-step-cloud
.hass=${this.hass}
></ha-voice-assistant-setup-step-cloud>`
: this._step === STEP.SUCCESS
? html`<ha-voice-assistant-setup-step-success
: this._step === STEP.ADDONS
? html`<ha-voice-assistant-setup-step-addons
.hass=${this.hass}
.assistConfiguration=${this
._assistConfiguration}
.assistEntityId=${this._findDomainEntityId(
this._params.deviceId,
this.hass.entities,
"assist_satellite"
)}
></ha-voice-assistant-setup-step-success>`
: nothing}
></ha-voice-assistant-setup-step-addons>`
: this._step === STEP.SUCCESS
? html`<ha-voice-assistant-setup-step-success
.hass=${this.hass}
.assistConfiguration=${this
._assistConfiguration}
.assistEntityId=${this._findDomainEntityId(
this._params.deviceId,
this.hass.entities,
"assist_satellite"
)}
></ha-voice-assistant-setup-step-success>`
: nothing}
</div>
</ha-dialog>
`;

View File

@@ -0,0 +1,185 @@
import { css, html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import { AssistantSetupStyles } from "./styles";
import { STEP } from "./voice-assistant-setup-dialog";
import { documentationUrl } from "../../util/documentation-url";
@customElement("ha-voice-assistant-setup-step-addons")
export class HaVoiceAssistantSetupStepAddons extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _showFirst = false;
@state() private _showSecond = false;
@state() private _showThird = false;
@state() private _showFourth = false;
protected override firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
setTimeout(() => {
this._showFirst = true;
}, 200);
setTimeout(() => {
this._showSecond = true;
}, 600);
setTimeout(() => {
this._showThird = true;
}, 3000);
setTimeout(() => {
this._showFourth = true;
}, 8000);
}
protected override render() {
return html`<div class="content">
<h1>Local</h1>
<p class="secondary">
Are you sure you want to use the local voice assistant? It requires a
powerful device to run. If you device is not powerful enough, Home
Assistant cloud might be a better option.
</p>
<h3>Raspberry Pi 4</h3>
<div class="messages-container rpi">
<div class="message user ${this._showThird ? "show" : ""}">
${!this._showThird ? "…" : "Turn on the lights in the bedroom"}
</div>
${this._showThird
? html`<div class="timing user">3 seconds</div>`
: nothing}
${this._showThird
? html`<div class="message hass ${this._showFourth ? "show" : ""}">
${!this._showFourth ? "…" : "Turned on the lights"}
</div>`
: nothing}
${this._showFourth
? html`<div class="timing hass">5 seconds</div>`
: nothing}
</div>
<h3>Home Assistant Cloud</h3>
<div class="messages-container cloud">
<div class="message user ${this._showFirst ? "show" : ""}">
${!this._showFirst ? "…" : "Turn on the lights in the bedroom"}
</div>
${this._showFirst
? html`<div class="timing user">0.2 seconds</div>`
: nothing}
${this._showFirst
? html` <div class="message hass ${this._showSecond ? "show" : ""}">
${!this._showSecond ? "…" : "Turned on the lights"}
</div>`
: nothing}
${this._showSecond
? html`<div class="timing hass">0.4 seconds</div>`
: nothing}
</div>
</div>
<div class="footer side-by-side">
<ha-button @click=${this._goToCloud}
>Try Home Assistant Cloud</ha-button
>
<a
href=${documentationUrl(
this.hass,
"/voice_control/voice_remote_local_assistant/"
)}
target="_blank"
rel="noreferrer noopenner"
>
<ha-button @click=${this._skip} unelevated>Learn more</ha-button>
</a>
</div>`;
}
private _goToCloud() {
fireEvent(this, "next-step", { step: STEP.CLOUD });
}
private _skip() {
fireEvent(this, "next-step", { step: STEP.SUCCESS });
}
static styles = [
AssistantSetupStyles,
css`
.messages-container {
padding: 24px;
box-sizing: border-box;
height: 195px;
background: var(--input-fill-color);
border-radius: 16px;
border: 1px solid var(--divider-color);
display: flex;
flex-direction: column;
}
.message {
white-space: nowrap;
font-size: 18px;
clear: both;
margin: 8px 0;
padding: 8px;
border-radius: 15px;
height: 36px;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
width: 30px;
}
.rpi .message {
transition: width 1s;
}
.cloud .message {
transition: width 0.5s;
}
.message.user {
margin-left: 24px;
margin-inline-start: 24px;
margin-inline-end: initial;
align-self: self-end;
text-align: right;
border-bottom-right-radius: 0px;
background-color: var(--primary-color);
color: var(--text-primary-color);
direction: var(--direction);
}
.timing.user {
align-self: self-end;
}
.message.user.show {
width: 295px;
}
.message.hass {
margin-right: 24px;
margin-inline-end: 24px;
margin-inline-start: initial;
align-self: self-start;
border-bottom-left-radius: 0px;
background-color: var(--secondary-background-color);
color: var(--primary-text-color);
direction: var(--direction);
}
.timing.hass {
align-self: self-start;
}
.message.hass.show {
width: 184px;
}
.footer {
margin-top: 24px;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-voice-assistant-setup-step-addons": HaVoiceAssistantSetupStepAddons;
}
}

View File

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

View File

@@ -10,7 +10,6 @@ import { STEP } from "./voice-assistant-setup-dialog";
import { AssistantSetupStyles } from "./styles";
import "../../components/ha-md-list";
import "../../components/ha-md-list-item";
import { formatLanguageCode } from "../../common/language/format_language";
@customElement("ha-voice-assistant-setup-step-change-wake-word")
export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
@@ -23,12 +22,11 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
protected override render() {
return html`<div class="padding content">
<img src="/static/images/voice-assistant/change-wake-word.gif" />
<img src="/static/icons/casita/smiling.png" />
<h1>Change wake word</h1>
<p class="secondary">
Some wake words are better for
${formatLanguageCode(this.hass.locale.language, this.hass.locale)} and
voice than others. Please try them out.
Some wake words are better for [your language] and voice than others.
Please try them out.
</p>
</div>
<ha-md-list>

View File

@@ -1,4 +1,4 @@
import { html, LitElement, nothing, PropertyValues } from "lit";
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { testAssistSatelliteConnection } from "../../data/assist_satellite";
@@ -13,8 +13,6 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
@state() private _status?: "success" | "timeout";
@state() private _showLoader = false;
protected override willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (!this.hasUpdated) {
@@ -32,48 +30,39 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
protected override render() {
return html`<div class="content">
${this._status === "timeout"
? html`<img src="/static/images/voice-assistant/error.gif" />
<h1>The voice assistant is unable to connect to Home Assistant</h1>
<p class="secondary">
To play audio, the voice assistant device has to connect to Home
Assistant to fetch the files. Our test shows that the device is
unable to reach the Home Assistant server.
</p>
<div class="footer">
<a
href="https://www.home-assistant.io/docs/configuration/remote/#adding-a-remote-url-to-home-assistant"
><ha-button>Help me</ha-button></a
>
<ha-button @click=${this._testConnection}>Retry</ha-button>
</div>`
: html`<img src="/static/images/voice-assistant/hi.gif" />
${this._status === "success"
? html`<img src="/static/icons/casita/smiling.png" />
<h1>Hi</h1>
<p class="secondary">
Over the next couple steps we're going to personalize your voice
assistant.
</p>
${this._showLoader
? html`<ha-circular-progress
indeterminate
></ha-circular-progress>`
: nothing} `}
With a couple of steps we are going to setup your voice assistant.
</p>`
: this._status === "timeout"
? html`<img src="/static/icons/casita/sad.png" />
<h1>Voice assistant can not connect to Home Assistant</h1>
<p class="secondary">
A good explanation what is happening and what action you should
take.
</p>
<div class="footer">
<a href="#"><ha-button>Help me</ha-button></a>
<ha-button @click=${this._testConnection}>Retry</ha-button>
</div>`
: html`<img src="/static/icons/casita/loading.png" />
<h1>Checking...</h1>
<p class="secondary">
We are checking if the device can reach your Home Assistant
instance.
</p>
<ha-circular-progress indeterminate></ha-circular-progress>`}
</div>`;
}
private async _testConnection() {
this._status = undefined;
this._showLoader = false;
const timeout = setTimeout(() => {
this._showLoader = true;
}, 3000);
const result = await testAssistSatelliteConnection(
this.hass,
this.assistEntityId!
);
clearTimeout(timeout);
this._showLoader = false;
this._status = result.status;
}

View File

@@ -1,9 +1,7 @@
import { mdiEarth, mdiMicrophoneMessage, mdiOpenInNew } from "@mdi/js";
import { css, html, LitElement } from "lit";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import { AssistantSetupStyles } from "./styles";
@customElement("ha-voice-assistant-setup-step-cloud")
@@ -12,92 +10,22 @@ export class HaVoiceAssistantSetupStepCloud extends LitElement {
protected override render() {
return html`<div class="content">
<img
src=${`/static/images/logo_nabu_casa${this.hass.themes?.darkMode ? "_dark" : ""}.png`}
alt="Nabu Casa logo"
/>
<h1>The power of Home Assistant Cloud</h1>
<div class="features">
<div class="feature speech">
<div class="logos">
<div class="round-icon">
<ha-svg-icon .path=${mdiMicrophoneMessage}></ha-svg-icon>
</div>
</div>
<h2>
${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.cloud.features.speech.title"
)}
<span class="no-wrap"></span>
</h2>
<p>
${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.cloud.features.speech.text"
)}
</p>
</div>
<div class="feature access">
<div class="logos">
<div class="round-icon">
<ha-svg-icon .path=${mdiEarth}></ha-svg-icon>
</div>
</div>
<h2>
Remote access
<span class="no-wrap"></span>
</h2>
<p>
Secure remote access to your system while supporting the
development of Home Assistant.
</p>
</div>
<div class="feature">
<div class="logos">
<img
alt="Google Assistant"
src=${brandsUrl({
domain: "google_assistant",
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<img
alt="Amazon Alexa"
src=${brandsUrl({
domain: "alexa",
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
</div>
<h2>
${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.cloud.features.assistants.title"
)}
</h2>
<p>
${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.cloud.features.assistants.text"
)}
</p>
</div>
</div>
<img src="/static/images/logo_nabu_casa.png" />
<h1>Supercharge your assistant with Home Assistant Cloud</h1>
<p class="secondary">
Speed up and take the load off your system by running your
text-to-speech and speech-to-text in our private and secure cloud.
Cloud also includes secure remote access to your system while
supporting the development of Home Assistant.
</p>
</div>
<div class="footer side-by-side">
<a
href="https://www.nabucasa.com"
target="_blank"
rel="noreferrer noopenner"
><ha-button>Learn more</ha-button></a
>
<ha-button>
<ha-svg-icon .path=${mdiOpenInNew} slot="icon"></ha-svg-icon>
nabucasa.com
</ha-button>
</a>
<a href="/config/cloud/register" @click=${this._close}
><ha-button unelevated>Try 1 month for free</ha-button></a
>
@@ -108,58 +36,7 @@ export class HaVoiceAssistantSetupStepCloud extends LitElement {
fireEvent(this, "closed");
}
static styles = [
AssistantSetupStyles,
css`
.features {
display: flex;
flex-direction: column;
grid-gap: 16px;
padding: 16px;
}
.feature {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
margin-bottom: 16px;
}
.feature .logos {
margin-bottom: 16px;
}
.feature .logos > * {
width: 40px;
height: 40px;
margin: 0 4px;
}
.round-icon {
border-radius: 50%;
color: #6e41ab;
background-color: #e8dcf7;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.access .round-icon {
color: #00aef8;
background-color: #cceffe;
}
.feature h2 {
font-weight: 500;
font-size: 16px;
line-height: 24px;
margin-top: 0;
margin-bottom: 8px;
}
.feature p {
font-weight: 400;
font-size: 14px;
line-height: 20px;
margin: 0;
}
`,
];
static styles = AssistantSetupStyles;
}
declare global {

View File

@@ -32,10 +32,6 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
@state() private _showSecond = false;
@state() private _showThird = false;
@state() private _showFourth = false;
protected override willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
@@ -48,83 +44,63 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
super.firstUpdated(changedProperties);
setTimeout(() => {
this._showFirst = true;
}, 200);
}, 1);
setTimeout(() => {
this._showSecond = true;
}, 600);
setTimeout(() => {
this._showThird = true;
}, 3000);
setTimeout(() => {
this._showFourth = true;
}, 8000);
}, 1500);
}
protected override render() {
return html`<div class="content">
<h1>What hardware do you want to use?</h1>
<p class="secondary">
How quickly your assistant responds depends on the power of the
hardware.
</p>
<div class="container">
<div class="messages-container cloud">
return html`<div class="padding content">
<div class="messages-container">
<div class="message user ${this._showFirst ? "show" : ""}">
${!this._showFirst ? "…" : "Turn on the lights in the bedroom"}
</div>
${this._showFirst
? html`<div class="timing user">0.2 seconds</div>`
: nothing}
${this._showFirst
? html` <div class="message hass ${this._showSecond ? "show" : ""}">
${!this._showSecond ? "…" : "Turned on the lights"}
</div>`
: nothing}
${this._showSecond
? html`<div class="timing hass">0.4 seconds</div>`
: nothing}
</div>
<h2>Home Assistant Cloud</h2>
<p>Ideal if you don't have a powerful system at home.</p>
<ha-button @click=${this._setupCloud}>Learn more</ha-button>
</div>
<div class="container">
<div class="messages-container rpi">
<div class="message user ${this._showThird ? "show" : ""}">
${!this._showThird ? "…" : "Turn on the lights in the bedroom"}
</div>
${this._showThird
? html`<div class="timing user">3 seconds</div>`
: nothing}
${this._showThird
? html`<div class="message hass ${this._showFourth ? "show" : ""}">
${!this._showFourth ? "…" : "Turned on the lights"}
</div>`
: nothing}
${this._showFourth
? html`<div class="timing hass">5 seconds</div>`
: nothing}
</div>
<h2>Do-it-yourself</h2>
<p>
Install add-ons or containers to run it on your own system. Powerful
hardware is needed for fast responses.
<h1>Select system</h1>
<p class="secondary">
How quickly your voice assistant responds depends on the power of your
system.
</p>
<a
</div>
<ha-md-list>
<ha-md-list-item interactive type="button" @click=${this._setupCloud}>
Home Assistant Cloud
<span slot="supporting-text"
>Ideal if you don't have a powerful system at home</span
>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
<ha-md-list-item interactive type="button" @click=${this._thisSystem}>
On this system
<span slot="supporting-text"
>Local setup with the Whisper and Piper add-ons</span
>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
<ha-md-list-item
interactive
type="link"
href=${documentationUrl(
this.hass,
"/voice_control/voice_remote_local_assistant/"
)}
target="_blank"
rel="noreferrer noopenner"
target="_blank"
@click=${this._skip}
>
<ha-button @click=${this._skip}>
<ha-svg-icon .path=${mdiOpenInNew} slot="icon"></ha-svg-icon>
Learn more</ha-button
Use external system
<span slot="supporting-text"
>Learn more about how to host it on another system</span
>
</a>
</div>
</div>`;
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-md-list-item>
</ha-md-list>`;
}
private async _checkCloud() {
@@ -241,6 +217,10 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
this._nextStep(STEP.CLOUD);
}
private async _thisSystem() {
this._nextStep(STEP.ADDONS);
}
private _skip() {
this._nextStep(STEP.SUCCESS);
}
@@ -252,22 +232,21 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
static styles = [
AssistantSetupStyles,
css`
.container {
border-radius: 16px;
border: 1px solid var(--divider-color);
overflow: hidden;
padding-bottom: 16px;
:host {
padding: 0;
}
.container:last-child {
margin-top: 16px;
.padding {
padding: 24px;
}
ha-md-list {
width: 100%;
text-align: initial;
}
.messages-container {
padding: 24px;
box-sizing: border-box;
height: 195px;
background: var(--input-fill-color);
display: flex;
flex-direction: column;
height: 152px;
}
.message {
white-space: nowrap;
@@ -280,29 +259,21 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
width: 30px;
}
.rpi .message {
transition: width 1s;
}
.cloud .message {
transition: width 0.5s;
width: 30px;
}
.message.user {
margin-left: 24px;
margin-inline-start: 24px;
margin-inline-end: initial;
align-self: self-end;
float: var(--float-end);
text-align: right;
border-bottom-right-radius: 0px;
background-color: var(--primary-color);
color: var(--text-primary-color);
direction: var(--direction);
}
.timing.user {
align-self: self-end;
}
.message.user.show {
width: 295px;
@@ -312,15 +283,12 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
margin-right: 24px;
margin-inline-end: 24px;
margin-inline-start: initial;
align-self: self-start;
float: var(--float-start);
border-bottom-left-radius: 0px;
background-color: var(--secondary-background-color);
color: var(--primary-text-color);
direction: var(--direction);
}
.timing.hass {
align-self: self-start;
}
.message.hass.show {
width: 184px;

View File

@@ -66,11 +66,11 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
: undefined;
return html`<div class="content">
<img src="/static/images/voice-assistant/heart.gif" />
<h1>Ready to Assist!</h1>
<img src="/static/icons/casita/loving.png" />
<h1>Ready to assist!</h1>
<p class="secondary">
Make any final customizations here. You can always change these in the
Voice Assistants section of the settings page.
Your device is all ready to go! If you want to tweak some more
settings, you can change that below.
</p>
<div class="rows">
${this.assistConfiguration &&

View File

@@ -2,13 +2,7 @@ import { css, html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-circular-progress";
import { ON, UNAVAILABLE } from "../../data/entity";
import {
updateCanInstall,
UpdateEntity,
updateIsInstalling,
updateUsesProgress,
} from "../../data/update";
import { OFF, ON, UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { HomeAssistant } from "../../types";
import { AssistantSetupStyles } from "./styles";
@@ -57,19 +51,17 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
return nothing;
}
const stateObj = this.hass.states[this.updateEntityId] as
| UpdateEntity
| undefined;
const stateObj = this.hass.states[this.updateEntityId];
const progressIsNumeric = stateObj && updateUsesProgress(stateObj);
const progressIsNumeric =
typeof stateObj?.attributes.in_progress === "number";
return html`<div class="content">
<img src="/static/images/voice-assistant/update.gif" />
<img src="/static/icons/casita/loading.png" />
<h1>
${stateObj &&
(stateObj.state === "unavailable" || updateIsInstalling(stateObj))
? "Updating your voice assistant"
: "Checking for updates"}
${stateObj.state === OFF || stateObj.state === UNKNOWN
? "Checking for updates"
: "Updating your voice assistant"}
</h1>
<p class="secondary">
We are making sure you have the latest and greatest version of your
@@ -77,15 +69,15 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
</p>
<ha-circular-progress
.value=${progressIsNumeric
? (stateObj.attributes.update_percentage as number) / 100
? stateObj.attributes.in_progress / 100
: undefined}
.indeterminate=${!progressIsNumeric}
></ha-circular-progress>
<p>
${stateObj?.state === UNAVAILABLE
${stateObj.state === "unavailable"
? "Restarting voice assistant"
: progressIsNumeric
? `Installing ${stateObj.attributes.update_percentage}%`
? `Installing ${stateObj.attributes.in_progress}%`
: ""}
</p>
</div>`;
@@ -96,14 +88,8 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
if (!this.updateEntityId) {
return;
}
const updateEntity = this.hass.states[this.updateEntityId] as
| UpdateEntity
| undefined;
if (
updateEntity &&
this.hass.states[updateEntity.entity_id].state === ON &&
updateCanInstall(updateEntity)
) {
const updateEntity = this.hass.states[this.updateEntityId];
if (updateEntity && this.hass.states[updateEntity.entity_id].state === ON) {
this._updated = true;
await this.hass.callService(
"update",

View File

@@ -65,14 +65,14 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
return html`<div class="content">
${!this._detected
? html`
<img src="/static/images/voice-assistant/sleep.gif" />
<img src="/static/icons/casita/sleeping.png" />
<h1>
Say “${this._activeWakeWord(this.assistConfiguration)}” to wake the
device up
</h1>
<p class="secondary">Setup will continue once the device is awake.</p>
</div>`
: html`<img src="/static/images/voice-assistant/ok-nabu.gif" />
: html`<img src="/static/icons/casita/normal.png" />
<h1>
Say “${this._activeWakeWord(this.assistConfiguration)}” again
</h1>
@@ -80,7 +80,7 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
To make sure the wake word works for you.
</p>`}
</div>
<div class="footer centered">
<div class="footer full-width">
<ha-button @click=${this._changeWakeWord}>Change wake word</ha-button>
</div>`;
}

View File

@@ -125,15 +125,7 @@ class HassSubpage extends LitElement {
.main-title {
margin: var(--margin-title);
line-height: 20px;
min-width: 0;
flex-grow: 1;
overflow-wrap: break-word;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
padding-bottom: 1px;
}
.content {

View File

@@ -43,13 +43,8 @@ import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { ACTION_ICONS, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
import { AutomationClipboard } from "../../../../data/automation";
import { validateConfig } from "../../../../data/config";
import {
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../../../data/context";
import { fullEntitiesContext, labelsContext } from "../../../../data/context";
import { EntityRegistryEntry } from "../../../../data/entity_registry";
import { FloorRegistryEntry } from "../../../../data/floor_registry";
import { LabelRegistryEntry } from "../../../../data/label_registry";
import {
Action,
@@ -159,10 +154,6 @@ export default class HaAutomationActionRow extends LitElement {
@consume({ context: labelsContext, subscribe: true })
_labelReg!: LabelRegistryEntry[];
@state()
@consume({ context: floorsContext, subscribe: true })
_floorReg!: { [id: string]: FloorRegistryEntry };
@state() private _warnings?: string[];
@state() private _uiModeAvailable = true;
@@ -231,7 +222,6 @@ export default class HaAutomationActionRow extends LitElement {
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
this.action
)
)}
@@ -603,7 +593,6 @@ export default class HaAutomationActionRow extends LitElement {
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
this.action,
undefined,
true

View File

@@ -1,5 +1,5 @@
import { consume } from "@lit-labs/context";
import { css, html, LitElement, PropertyValues } from "lit";
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";
@@ -56,28 +56,6 @@ export class HaDeviceAction extends LitElement {
}
);
public shouldUpdate(changedProperties: PropertyValues) {
if (!changedProperties.has("action")) {
return true;
}
if (
this.action.device_id &&
!(this.action.device_id in this.hass.devices)
) {
fireEvent(
this,
"ui-mode-not-available",
Error(
this.hass.localize(
"ui.panel.config.automation.editor.edit_unknown_device"
)
)
);
return false;
}
return true;
}
protected render() {
const deviceId = this._deviceId || this.action.device_id;

View File

@@ -1,5 +1,5 @@
import { consume } from "@lit-labs/context";
import { css, html, LitElement, PropertyValues } from "lit";
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";
@@ -57,28 +57,6 @@ export class HaDeviceCondition extends LitElement {
}
);
public shouldUpdate(changedProperties: PropertyValues) {
if (!changedProperties.has("condition")) {
return true;
}
if (
this.condition.device_id &&
!(this.condition.device_id in this.hass.devices)
) {
fireEvent(
this,
"ui-mode-not-available",
Error(
this.hass.localize(
"ui.panel.config.automation.editor.edit_unknown_device"
)
)
);
return false;
}
return true;
}
protected render() {
const deviceId = this._deviceId || this.condition.device_id;

View File

@@ -1,5 +1,5 @@
import { consume } from "@lit-labs/context";
import { css, html, LitElement, PropertyValues } from "lit";
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";
@@ -61,28 +61,6 @@ export class HaDeviceTrigger extends LitElement {
}
);
public shouldUpdate(changedProperties: PropertyValues) {
if (!changedProperties.has("trigger")) {
return true;
}
if (
this.trigger.device_id &&
!(this.trigger.device_id in this.hass.devices)
) {
fireEvent(
this,
"ui-mode-not-available",
Error(
this.hass.localize(
"ui.panel.config.automation.editor.edit_unknown_device"
)
)
);
return false;
}
return true;
}
protected render() {
const deviceId = this._deviceId || this.trigger.device_id;

View File

@@ -251,7 +251,9 @@ export class HaDeviceEntitiesCard extends LitElement {
display: block;
}
ha-icon {
margin-left: -8px;
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
}
.entity-id {
color: var(--secondary-text-color);
@@ -281,9 +283,6 @@ export class HaDeviceEntitiesCard extends LitElement {
.name {
font-size: 14px;
}
.name:dir(rtl) {
margin-inline-start: 8px;
}
.empty {
text-align: center;
}
@@ -303,9 +302,6 @@ export class HaDeviceEntitiesCard extends LitElement {
outline: none;
text-decoration: underline;
}
ha-list-item {
height: 40px;
}
`;
}
}

View File

@@ -9,7 +9,6 @@ import {
SecurityClass,
unprovisionZwaveSmartStartNode,
} from "../../../../../data/zwave_js";
import { LocalizeFunc } from "../../../../../common/translations/localize";
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
import "../../../../../layouts/hass-tabs-subpage-data-table";
import { HomeAssistant, Route } from "../../../../../types";
@@ -34,7 +33,7 @@ class ZWaveJSProvisioned extends LitElement {
.narrow=${this.narrow}
.route=${this.route}
.tabs=${configTabs}
.columns=${this._columns(this.hass.localize)}
.columns=${this._columns(this.narrow)}
.data=${this._provisioningEntries}
>
</hass-tabs-subpage-data-table>
@@ -42,12 +41,11 @@ class ZWaveJSProvisioned extends LitElement {
}
private _columns = memoizeOne(
(
localize: LocalizeFunc
): DataTableColumnContainer<ZwaveJSProvisioningEntry> => ({
(narrow: boolean): DataTableColumnContainer<ZwaveJSProvisioningEntry> => ({
included: {
showNarrow: true,
title: localize("ui.panel.config.zwave_js.provisioned.included"),
title: this.hass.localize(
"ui.panel.config.zwave_js.provisioned.included"
),
type: "icon",
template: (entry) =>
entry.nodeId
@@ -69,16 +67,16 @@ class ZWaveJSProvisioned extends LitElement {
`,
},
dsk: {
main: true,
title: localize("ui.panel.config.zwave_js.provisioned.dsk"),
title: this.hass.localize("ui.panel.config.zwave_js.provisioned.dsk"),
sortable: true,
filterable: true,
flex: 2,
},
security_classes: {
title: localize(
title: this.hass.localize(
"ui.panel.config.zwave_js.provisioned.security_classes"
),
hidden: narrow,
filterable: true,
sortable: true,
template: (entry) => {
@@ -93,8 +91,9 @@ class ZWaveJSProvisioned extends LitElement {
},
},
unprovision: {
showNarrow: true,
title: localize("ui.panel.config.zwave_js.provisioned.unprovison"),
title: this.hass.localize(
"ui.panel.config.zwave_js.provisioned.unprovison"
),
type: "icon-button",
template: (entry) => html`
<ha-icon-button

View File

@@ -103,7 +103,6 @@ export class HaConfigLabels extends LitElement {
style="
background-color: ${computeCssColor(label.color)};
border-radius: 10px;
outline: 1px solid var(--outline-color);
width: 20px;
height: 20px;"
></div>`

View File

@@ -1,155 +0,0 @@
import { mdiClose } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import "../../../components/ha-md-dialog";
import "../../../components/ha-button";
import "../../../components/ha-dialog-header";
import "../../../components/ha-icon-button";
import type { HaMdDialog } from "../../../components/ha-md-dialog";
import { HomeAssistant } from "../../../types";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import { fireEvent } from "../../../common/dom/fire_event";
import { DownloadLogsDialogParams } from "./show-dialog-download-logs";
import "../../../components/ha-select";
import "../../../components/ha-list-item";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { getHassioLogDownloadLinesUrl } from "../../../data/hassio/supervisor";
import { getSignedPath } from "../../../data/auth";
import { fileDownload } from "../../../util/file_download";
@customElement("dialog-download-logs")
class DownloadLogsDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _dialogParams?: DownloadLogsDialogParams;
@state() private _lineCount = 100;
@query("ha-md-dialog") private _dialogElement!: HaMdDialog;
public showDialog(dialogParams: DownloadLogsDialogParams) {
this._dialogParams = dialogParams;
this._lineCount = this._dialogParams?.defaultLineCount ?? 100;
}
public closeDialog() {
this._dialogElement.close();
}
private _dialogClosed() {
this._dialogParams = undefined;
this._lineCount = 100;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._dialogParams) {
return nothing;
}
const numberOfLinesOptions = [100, 500, 1000, 5000, 10000];
if (!numberOfLinesOptions.includes(this._lineCount)) {
numberOfLinesOptions.push(this._lineCount);
numberOfLinesOptions.sort((a, b) => a - b);
}
return html`
<ha-md-dialog open @closed=${this._dialogClosed}>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
@click=${this.closeDialog}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title" id="dialog-light-color-favorite-title">
${this.hass.localize("ui.panel.config.logs.download_full_log")}
</span>
<span slot="subtitle">
${this._dialogParams.header}${this._dialogParams.boot === 0
? ""
: `${this._dialogParams.boot === -1 ? this.hass.localize("ui.panel.config.logs.previous") : this.hass.localize("ui.panel.config.logs.startups_ago", { boot: this._dialogParams.boot * -1 })}`}
</span>
</ha-dialog-header>
<div slot="content" class="content">
<div>
${this.hass.localize(
"ui.panel.config.logs.select_number_of_lines"
)}:
</div>
<ha-select
.label=${this.hass.localize("ui.panel.config.logs.lines")}
@selected=${this._setNumberOfLogs}
fixedMenuPosition
naturalMenuWidth
@closed=${stopPropagation}
.value=${String(this._lineCount)}
>
${numberOfLinesOptions.map(
(option) => html`
<ha-list-item .value=${String(option)}>
${option}
</ha-list-item>
`
)}
</ha-select>
</div>
<div slot="actions">
<ha-button @click=${this.closeDialog}>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button @click=${this._dowloadLogs}>
${this.hass.localize("ui.common.download")}
</ha-button>
</div>
</ha-md-dialog>
`;
}
private async _dowloadLogs() {
const provider = this._dialogParams!.provider;
const boot = this._dialogParams!.boot;
const timeString = new Date().toISOString().replace(/:/g, "-");
const downloadUrl = getHassioLogDownloadLinesUrl(
provider,
this._lineCount,
boot
);
const logFileName =
provider !== "core"
? `${provider}_${timeString}.log`
: `home-assistant_${timeString}.log`;
const signedUrl = await getSignedPath(this.hass, downloadUrl);
fileDownload(signedUrl.path, logFileName);
this.closeDialog();
}
private _setNumberOfLogs(ev) {
this._lineCount = Number(ev.target.value);
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
:host {
direction: var(--direction);
}
.content {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-download-logs": DownloadLogsDialog;
}
}

View File

@@ -1,10 +1,6 @@
import "@material/mwc-button";
import "@material/mwc-list/mwc-list-item";
import {
mdiArrowCollapseDown,
mdiDownload,
mdiMenuDown,
mdiRefresh,
} from "@mdi/js";
import { mdiRefresh, mdiDownload } from "@mdi/js";
import {
css,
CSSResultGroup,
@@ -12,47 +8,27 @@ import {
LitElement,
PropertyValues,
TemplateResult,
nothing,
} from "lit";
import { classMap } from "lit/directives/class-map";
// eslint-disable-next-line import/extensions
import { IntersectionController } from "@lit-labs/observers/intersection-controller.js";
import { customElement, property, state, query } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-alert";
import "../../../components/ha-ansi-to-html";
import type { HaAnsiToHtml } from "../../../components/ha-ansi-to-html";
import "../../../components/ha-card";
import "../../../components/ha-button";
import "../../../components/ha-icon-button";
import "../../../components/ha-select";
import "../../../components/ha-svg-icon";
import "../../../components/ha-circular-progress";
import "../../../components/chips/ha-assist-chip";
import "../../../components/ha-menu";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-md-divider";
import { getSignedPath } from "../../../data/auth";
import { fetchErrorLog, getErrorLogDownloadUrl } from "../../../data/error_log";
import { extractApiErrorMessage } from "../../../data/hassio/common";
import {
fetchHassioBoots,
fetchHassioLogs,
fetchHassioLogsFollow,
getHassioLogDownloadUrl,
} from "../../../data/hassio/supervisor";
import { HomeAssistant } from "../../../types";
import { fileDownload } from "../../../util/file_download";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import { ConnectionStatus } from "../../../data/connection-status";
import { atLeastVersion } from "../../../common/config/version";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { debounce } from "../../../common/util/debounce";
import { showDownloadLogsDialog } from "./show-dialog-download-logs";
import type { HaMenu } from "../../../components/ha-menu";
const NUMBER_OF_LINES = 100;
import { fileDownload } from "../../../util/file_download";
@customElement("error-log-card")
class ErrorLogCard extends LitElement {
@@ -66,190 +42,52 @@ class ErrorLogCard extends LitElement {
@property({ type: Boolean, attribute: true }) public show = false;
@query(".error-log") private _logElement?: HTMLElement;
@state() private _isLogLoaded = false;
@query("#scroll-top-marker") private _scrollTopMarkerElement?: HTMLElement;
@query("#scroll-bottom-marker")
private _scrollBottomMarkerElement?: HTMLElement;
@query("ha-ansi-to-html") private _ansiToHtmlElement?: HaAnsiToHtml;
@query("#boots-menu") private _bootsMenu?: HaMenu;
@state() private _firstCursor?: string;
@state() private _scrolledToBottomController =
new IntersectionController<boolean>(this, {
callback(this: IntersectionController<boolean>, entries) {
return entries[0].isIntersecting;
},
});
@state() private _scrolledToTopController =
new IntersectionController<boolean>(this, {});
@state() private _newLogsIndicator?: boolean;
@state() private _logHTML?: TemplateResult[] | TemplateResult | string;
@state() private _error?: string;
@state() private _logStreamAborter?: AbortController;
@state() private _streamSupported?: boolean;
@state() private _loadingState: "loading" | "empty" | "loaded" = "loading";
@state() private _loadingPrevState?: "loading" | "end" | "loaded";
@state() private _noSearchResults: boolean = false;
@state() private _numberOfLines?: number;
@state() private _boot = 0;
@state() private _boots?: number[];
protected render(): TemplateResult {
return html`
<div class="error-log-intro">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<ha-card outlined class=${classMap({ hidden: this.show === false })}>
<div class="header">
<h1 class="card-header">
${this.header ||
this.hass.localize("ui.panel.config.logs.show_full_logs")}
</h1>
<div class="action-buttons">
${this._streamSupported && Array.isArray(this._boots)
? html`
<ha-assist-chip
.label=${this._boot === 0
? this.hass.localize("ui.panel.config.logs.current")
: this._boot === -1
? this.hass.localize("ui.panel.config.logs.previous")
: this.hass.localize(
"ui.panel.config.logs.startups_ago",
{ boot: this._boot * -1 }
)}
id="boots-anchor"
@click=${this._toggleBootsMenu}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon
></ha-assist-chip>
<ha-menu
anchor="boots-anchor"
id="boots-menu"
positioning="fixed"
>
${this._boots.map(
(boot) => html`
<ha-md-menu-item
.value=${boot}
@click=${this._setBoot}
.selected=${boot === this._boot}
>
${boot === 0
? this.hass.localize(
"ui.panel.config.logs.current"
)
: boot === -1
? this.hass.localize(
"ui.panel.config.logs.previous"
)
: this.hass.localize(
"ui.panel.config.logs.startups_ago",
{ boot: boot * -1 }
)}
</ha-md-menu-item>
${boot === 0
? html`<ha-md-divider
role="separator"
></ha-md-divider>`
: nothing}
`
)}
</ha-menu>
`
: nothing}
<ha-icon-button
.path=${mdiDownload}
@click=${this._downloadFullLog}
.label=${this.hass.localize(
"ui.panel.config.logs.download_full_log"
)}
></ha-icon-button>
${!this._streamSupported || this._error
? html`<ha-icon-button
.path=${mdiRefresh}
@click=${this._loadLogs}
.label=${this.hass.localize("ui.common.refresh")}
></ha-icon-button>`
: nothing}
</div>
</div>
<div class="card-content error-log">
<div id="scroll-top-marker"></div>
${this._loadingPrevState === "loading"
? html`<div class="loading-old">
<ha-circular-progress
.indeterminate=${this._loadingPrevState === "loading"}
></ha-circular-progress>
</div>`
: nothing}
${this._loadingState === "loading"
? html`<div>
${this.hass.localize("ui.panel.config.logs.loading_log")}
</div>`
: this._loadingState === "empty"
? html`<div>
${this.hass.localize("ui.panel.config.logs.no_errors")}
</div>`
: nothing}
${this._loadingState === "loaded" &&
this.filter &&
this._noSearchResults
? html`<div>
${this.hass.localize(
"ui.panel.config.logs.no_issues_search",
{ term: this.filter }
)}
</div>`
: nothing}
<ha-ansi-to-html></ha-ansi-to-html>
<div id="scroll-bottom-marker"></div>
</div>
<ha-button
class="new-logs-indicator ${classMap({
visible:
(this._newLogsIndicator &&
!this._scrolledToBottomController.value) ||
false,
})}"
@click=${this._scrollToBottom}
>
<ha-svg-icon
.path=${mdiArrowCollapseDown}
slot="icon"
></ha-svg-icon>
${this.hass.localize("ui.panel.config.logs.scroll_down_button")}
<ha-svg-icon
.path=${mdiArrowCollapseDown}
slot="trailingIcon"
></ha-svg-icon>
</ha-button>
</ha-card>
${this.show === false
${this._logHTML
? html`
<ha-button outlined @click=${this._downloadFullLog}>
<ha-card outlined>
<div class="header">
<h1 class="card-header">
${this.header ||
this.hass.localize("ui.panel.config.logs.show_full_logs")}
</h1>
<div>
<ha-icon-button
.path=${mdiRefresh}
@click=${this._refresh}
.label=${this.hass.localize("ui.common.refresh")}
></ha-icon-button>
<ha-icon-button
.path=${mdiDownload}
@click=${this._downloadFullLog}
.label=${this.hass.localize(
"ui.panel.config.logs.download_full_log"
)}
></ha-icon-button>
</div>
</div>
<div class="card-content error-log">${this._logHTML}</div>
</ha-card>
`
: ""}
${!this._logHTML
? html`
<mwc-button outlined @click=${this._downloadFullLog}>
<ha-svg-icon .path=${mdiDownload}></ha-svg-icon>
${this.hass.localize("ui.panel.config.logs.download_full_log")}
</ha-button>
<mwc-button raised @click=${this._showLogs}>
</mwc-button>
<mwc-button raised @click=${this._refreshLogs}>
${this.hass.localize("ui.panel.config.logs.load_logs")}
</mwc-button>
`
@@ -258,337 +96,127 @@ class ErrorLogCard extends LitElement {
`;
}
public connectedCallback() {
super.connectedCallback();
if (this._streamSupported === undefined) {
this._streamSupported = atLeastVersion(
this.hass.config.version,
2024,
11
);
}
}
private _debounceSearch = debounce(
() => (this._isLogLoaded ? this._refreshLogs() : this._debounceSearch()),
150,
false
);
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._scrolledToBottomController.observe(this._scrollBottomMarkerElement!);
this._scrolledToTopController.callback = this._handleTopScroll;
this._scrolledToTopController.observe(this._scrollTopMarkerElement!);
window.addEventListener("connection-status", this._handleConnectionStatus);
if (this.hass?.config.recovery_mode || this.show) {
this.hass.loadFragmentTranslation("config");
this._refreshLogs();
}
// just needs to be loaded once, because only the host endpoints provide boots information
this._loadBoots();
}
protected updated(changedProps) {
super.updated(changedProps);
if (changedProps.has("provider")) {
this._logHTML = undefined;
}
if (
(changedProps.has("show") && this.show) ||
(changedProps.has("provider") && this.show)
) {
this._boot = 0;
this._loadLogs();
}
if (this._newLogsIndicator && this._scrolledToBottomController.value) {
this._newLogsIndicator = false;
this._refreshLogs();
return;
}
if (changedProps.has("filter")) {
this._debounceSearch();
}
if (
changedProps.has("_loadingState") &&
this._loadingState === "loaded" &&
this._scrolledToTopController.value &&
this._firstCursor &&
!this._loadingPrevState
) {
this._loadMoreLogs();
}
}
disconnectedCallback() {
super.disconnectedCallback();
private async _refresh(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
if (this._logStreamAborter) {
this._logStreamAborter.abort();
}
window.removeEventListener(
"connection-status",
this._handleConnectionStatus
);
await this._refreshLogs();
button.progress = false;
}
private async _downloadFullLog(): Promise<void> {
if (this._streamSupported) {
showDownloadLogsDialog(this, {
header: this.header,
provider: this.provider,
defaultLineCount: this._numberOfLines,
boot: this._boot,
});
} else {
const timeString = new Date().toISOString().replace(/:/g, "-");
const downloadUrl =
this.provider && this.provider !== "core"
? getHassioLogDownloadUrl(this.provider)
: getErrorLogDownloadUrl;
const logFileName =
this.provider && this.provider !== "core"
? `${this.provider}_${timeString}.log`
: `home-assistant_${timeString}.log`;
const signedUrl = await getSignedPath(this.hass, downloadUrl);
fileDownload(signedUrl.path, logFileName);
}
const timeString = new Date().toISOString().replace(/:/g, "-");
const downloadUrl =
this.provider !== "core"
? getHassioLogDownloadUrl(this.provider)
: getErrorLogDownloadUrl;
const logFileName =
this.provider !== "core"
? `${this.provider}_${timeString}.log`
: `home-assistant_${timeString}.log`;
const signedUrl = await getSignedPath(this.hass, downloadUrl);
fileDownload(signedUrl.path, logFileName);
}
private _showLogs(): void {
this.show = true;
}
private async _refreshLogs(): Promise<void> {
this._logHTML = this.hass.localize("ui.panel.config.logs.loading_log");
let log: string;
private async _loadLogs(): Promise<void> {
this._error = undefined;
this._loadingState = "loading";
this._loadingPrevState = undefined;
this._firstCursor = undefined;
this._numberOfLines = 0;
this._ansiToHtmlElement?.clear();
try {
if (this._logStreamAborter) {
this._logStreamAborter.abort();
}
this._logStreamAborter = new AbortController();
if (
this._streamSupported &&
isComponentLoaded(this.hass, "hassio") &&
this.provider
) {
const response = await fetchHassioLogsFollow(
this.hass,
this.provider,
this._logStreamAborter.signal,
NUMBER_OF_LINES,
this._boot
);
if (response.headers.has("X-First-Cursor")) {
this._firstCursor = response.headers.get("X-First-Cursor")!;
if (this.provider !== "core" && isComponentLoaded(this.hass, "hassio")) {
try {
log = await fetchHassioLogs(this.hass, this.provider);
if (this.filter) {
log = log
.split("\n")
.filter((entry) =>
entry.toLowerCase().includes(this.filter.toLowerCase())
)
.join("\n");
}
if (!response.body) {
throw new Error("No stream body found");
}
this._loadingState = "empty";
let tempLogLine = "";
const reader = response.body.getReader();
const decoder = new TextDecoder();
let done = false;
while (!done) {
// eslint-disable-next-line no-await-in-loop
const { value, done: readerDone } = await reader.read();
done = readerDone;
if (value) {
const chunk = decoder.decode(value, { stream: !done });
const scrolledToBottom = this._scrolledToBottomController.value;
const lines = `${tempLogLine}${chunk}`
.split("\n")
.filter((line) => line.trim() !== "");
// handle edge case where the last line is not complete
if (chunk.endsWith("\n")) {
tempLogLine = "";
} else {
tempLogLine = lines.splice(-1, 1)[0];
}
if (lines.length) {
this._ansiToHtmlElement?.parseLinesToColoredPre(lines);
this._numberOfLines += lines.length;
if (this._loadingState === "empty") {
// delay to avoid loading older logs immediately
setTimeout(() => {
this._loadingState = "loaded";
}, 100);
}
}
if (scrolledToBottom && this._logElement) {
this._scrollToBottom();
} else {
this._newLogsIndicator = true;
}
}
}
} else {
// fallback to old method
this._streamSupported = false;
let logs = "";
if (isComponentLoaded(this.hass, "hassio") && this.provider) {
const repsonse = await fetchHassioLogs(this.hass, this.provider);
logs = await repsonse.text();
} else {
logs = await fetchErrorLog(this.hass);
}
if (logs) {
this._ansiToHtmlElement?.parseTextToColoredPre(logs);
this._loadingState = "loaded";
this._scrollToBottom();
}
}
} catch (err: any) {
if (err.name === "AbortError") {
return;
}
this._error = this.hass.localize("ui.panel.config.logs.failed_get_logs", {
provider: this.provider,
error: extractApiErrorMessage(err),
});
}
}
private _debounceSearch = debounce(() => {
this._noSearchResults = !this._ansiToHtmlElement?.filterLines(this.filter);
if (!this.filter) {
this._scrollToBottom();
}
}, 150);
private _debounceScrollToBottom = debounce(() => {
this._logElement!.scrollTop = this._logElement!.scrollHeight;
}, 300);
private _scrollToBottom(): void {
if (this._logElement) {
this._newLogsIndicator = false;
if (this.provider !== "core") {
this._logElement!.scrollTo(0, this._logElement!.scrollHeight);
} else {
this._debounceScrollToBottom();
}
}
}
private _handleConnectionStatus = (ev: HASSDomEvent<ConnectionStatus>) => {
if (ev.detail === "disconnected" && this._logStreamAborter) {
this._logStreamAborter.abort();
}
if (ev.detail === "connected" && this.show) {
this._loadLogs();
}
};
private async _loadMoreLogs() {
if (
this._firstCursor &&
this._loadingPrevState !== "loading" &&
this._loadingState === "loaded" &&
this._logElement
) {
const scrolledToBottom = this._scrolledToBottomController.value;
const scrollPositionFromBottom =
this._logElement.scrollHeight - this._logElement.scrollTop;
this._loadingPrevState = "loading";
const response = await fetchHassioLogs(
this.hass,
this.provider,
`entries=${this._firstCursor}:-100:100`,
this._boot
);
if (response.headers.has("X-First-Cursor")) {
if (this._firstCursor === response.headers.get("X-First-Cursor")!) {
this._loadingPrevState = "end";
if (!log) {
this._logHTML = this.hass.localize("ui.panel.config.logs.no_errors");
return;
}
this._firstCursor = response.headers.get("X-First-Cursor")!;
}
const body = await response.text();
if (body) {
const lines = body
.split("\n")
.filter((line) => line.trim() !== "")
.reverse();
this._ansiToHtmlElement?.parseLinesToColoredPre(lines, true);
this._numberOfLines! += lines.length;
this._loadingPrevState = "loaded";
} else {
this._loadingPrevState = "end";
}
if (scrolledToBottom) {
this._scrollToBottom();
} else if (this._loadingPrevState !== "end" && this._logElement) {
window.requestAnimationFrame(() => {
this._logElement!.scrollTop =
this._logElement!.scrollHeight - scrollPositionFromBottom;
});
}
}
}
private _handleTopScroll = (entries) => {
const isVisible = entries[0].isIntersecting;
if (
this._firstCursor &&
isVisible &&
this._loadingState === "loaded" &&
(!this._loadingPrevState || this._loadingPrevState === "loaded") &&
!this.filter
) {
this._loadMoreLogs();
}
return isVisible;
};
private async _loadBoots() {
if (this._streamSupported && isComponentLoaded(this.hass, "hassio")) {
try {
const { data } = await fetchHassioBoots(this.hass);
this._boots = Object.keys(data.boots)
.map(Number)
.sort((a, b) => b - a);
this._logHTML = html`<ha-ansi-to-html .content=${log}>
</ha-ansi-to-html>`;
this._isLogLoaded = true;
return;
} catch (err: any) {
// eslint-disable-next-line no-console
console.error(err);
this._error = this.hass.localize(
"ui.panel.config.logs.failed_get_logs",
{ provider: this.provider, error: extractApiErrorMessage(err) }
);
return;
}
} else {
log = await fetchErrorLog(this.hass!);
}
}
private _toggleBootsMenu() {
if (this._bootsMenu) {
this._bootsMenu.open = !this._bootsMenu.open;
}
}
this._isLogLoaded = true;
private _setBoot(ev: any) {
this._boot = ev.target.value;
this._loadLogs();
const split = log && log.split("\n");
this._logHTML = split
? (this.filter
? split.filter((entry) => {
if (this.filter) {
return entry.toLowerCase().includes(this.filter.toLowerCase());
}
return entry;
})
: split
).map((entry) => {
if (entry.includes("INFO"))
return html`<div class="info">${entry}</div>`;
if (entry.includes("WARNING"))
return html`<div class="warning">${entry}</div>`;
if (
entry.includes("ERROR") ||
entry.includes("FATAL") ||
entry.includes("CRITICAL")
)
return html`<div class="error">${entry}</div>`;
return html`<div>${entry}</div>`;
})
: this.hass.localize("ui.panel.config.logs.no_errors");
}
static styles: CSSResultGroup = css`
@@ -598,18 +226,7 @@ class ErrorLogCard extends LitElement {
}
ha-card {
padding-top: 8px;
position: relative;
}
ha-card.hidden {
display: none;
}
ha-card .action-buttons {
display: flex;
align-items: center;
height: 100%;
padding-top: 16px;
}
.header {
@@ -619,18 +236,21 @@ class ErrorLogCard extends LitElement {
}
.card-header {
color: var(--ha-card-header-color, var(--primary-text-color));
color: var(--ha-card-header-color, --primary-text-color);
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em;
line-height: 48px;
display: block;
margin-block-start: 0px;
margin-block-end: 0px;
font-weight: normal;
white-space: nowrap;
max-width: calc(100% - 150px);
overflow: hidden;
text-overflow: ellipsis;
}
ha-select {
display: block;
max-width: 500px;
width: 100%;
}
ha-icon-button {
@@ -638,24 +258,10 @@ class ErrorLogCard extends LitElement {
}
.error-log {
position: relative;
font-family: var(--code-font-family, monospace);
clear: both;
text-align: left;
padding-top: 12px;
padding-bottom: 12px;
overflow-y: scroll;
min-height: var(--error-log-card-height, calc(100vh - 240px));
max-height: var(--error-log-card-height, calc(100vh - 240px));
border-top: 1px solid var(--divider-color);
}
@media all and (max-width: 870px) {
.error-log {
min-height: var(--error-log-card-height, calc(100vh - 190px));
max-height: var(--error-log-card-height, calc(100vh - 190px));
}
}
.error-log > div {
@@ -667,28 +273,6 @@ class ErrorLogCard extends LitElement {
background-color: var(--secondary-background-color);
}
.new-logs-indicator {
--mdc-theme-primary: var(--text-primary-color);
overflow: hidden;
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 0;
background-color: var(--primary-color);
border-radius: 8px;
transition: height 0.4s ease-out;
display: flex;
justify-content: space-between;
align-items: center;
}
.new-logs-indicator.visible {
height: 24px;
}
.error {
color: var(--error-color);
}
@@ -697,16 +281,8 @@ class ErrorLogCard extends LitElement {
color: var(--warning-color);
}
.loading-old {
display: flex;
width: 100%;
justify-content: center;
padding: 16px;
}
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
--md-assist-chip-trailing-space: 8px;
mwc-button {
direction: var(--direction);
}
`;
}

View File

@@ -167,7 +167,6 @@ export class HaConfigLogs extends LitElement {
private _selectProvider(ev) {
this._selectedLogProvider = (ev.currentTarget as any).provider;
this._filter = "";
navigate(`/config/logs?provider=${this._selectedLogProvider}`);
}

View File

@@ -1,19 +0,0 @@
import { fireEvent } from "../../../common/dom/fire_event";
export interface DownloadLogsDialogParams {
header?: string;
provider: string;
defaultLineCount?: number;
boot: number;
}
export const showDownloadLogsDialog = (
element: HTMLElement,
dialogParams: DownloadLogsDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-download-logs",
dialogImport: () => import("./dialog-download-logs"),
dialogParams,
});
};

View File

@@ -212,7 +212,7 @@ export class SystemLogCard extends LitElement {
}
.card-header {
color: var(--ha-card-header-color, var(--primary-text-color));
color: var(--ha-card-header-color, --primary-text-color);
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em;

View File

@@ -1,6 +1,6 @@
import "@material/mwc-tab";
import "@material/mwc-tab-bar";
import { mdiDeleteOutline, mdiPlus, mdiMenuDown, mdiWifi } from "@mdi/js";
import { mdiDeleteOutline, mdiPlus, mdiMenuDown } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
@@ -20,7 +20,7 @@ import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
import { extractApiErrorMessage } from "../../../data/hassio/common";
import {
AccessPoint,
AccessPoints,
accesspointScan,
fetchNetworkInfo,
formatAddress,
@@ -58,7 +58,7 @@ const PREDEFINED_DNS = {
export class HassioNetwork extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _accessPoints: AccessPoint[] = [];
@state() private _accessPoints?: AccessPoints;
@state() private _curTabIndex = 0;
@@ -113,7 +113,7 @@ export class HassioNetwork extends LitElement {
</mwc-tab>`
)}
</mwc-tab-bar>`
: nothing}
: ""}
${cache(this._renderTab())}
</ha-card>
`;
@@ -121,6 +121,9 @@ export class HassioNetwork extends LitElement {
private _renderTab() {
return html`<div class="card-content">
${IP_VERSIONS.map((version) =>
this._interface![version] ? this._renderIPConfiguration(version) : ""
)}
${this._interface?.type === "wireless"
? html`
<ha-expansion-panel
@@ -128,17 +131,15 @@ export class HassioNetwork extends LitElement {
"ui.panel.config.network.supervisor.wifi"
)}
outlined
.expanded=${!this._interface?.wifi?.ssid}
>
${this._interface?.wifi?.ssid
? html`<p>
<ha-svg-icon slot="icon" .path=${mdiWifi}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.network.supervisor.connected_to",
{ ssid: this._interface?.wifi?.ssid }
)}
</p>`
: nothing}
: ""}
<ha-button
class="scan"
@click=${this._scanForAP}
@@ -150,34 +151,37 @@ export class HassioNetwork extends LitElement {
: this.hass.localize(
"ui.panel.config.network.supervisor.scan_ap"
)}
<ha-svg-icon slot="icon" .path=${mdiWifi}></ha-svg-icon>
</ha-button>
${this._accessPoints.length
${this._accessPoints &&
this._accessPoints.accesspoints &&
this._accessPoints.accesspoints.length !== 0
? html`
<mwc-list>
${this._accessPoints.map(
(ap) => html`
<ha-list-item
twoline
@click=${this._selectAP}
.activated=${ap.ssid ===
this._wifiConfiguration?.ssid}
.ap=${ap}
>
<span>${ap.ssid}</span>
<span slot="secondary">
${ap.mac} -
${this.hass.localize(
"ui.panel.config.network.supervisor.signal_strength"
)}:
${ap.signal}
</span>
</ha-list-item>
`
)}
${this._accessPoints.accesspoints
.filter((ap) => ap.ssid)
.map(
(ap) => html`
<ha-list-item
twoline
@click=${this._selectAP}
.activated=${ap.ssid ===
this._wifiConfiguration?.ssid}
.ap=${ap}
>
<span>${ap.ssid}</span>
<span slot="secondary">
${ap.mac} -
${this.hass.localize(
"ui.panel.config.network.supervisor.signal_strength"
)}:
${ap.signal}
</span>
</ha-list-item>
`
)}
</mwc-list>
`
: nothing}
: ""}
${this._wifiConfiguration
? html`
<div class="radio-row">
@@ -240,24 +244,19 @@ export class HassioNetwork extends LitElement {
>
</ha-password-field>
`
: nothing}
: ""}
`
: nothing}
: ""}
</ha-expansion-panel>
`
: nothing}
${IP_VERSIONS.map((version) =>
this._interface![version]
? this._renderIPConfiguration(version)
: nothing
)}
: ""}
${this._dirty
? html`<ha-alert alert-type="warning">
${this.hass.localize(
"ui.panel.config.network.supervisor.warning"
)}
</ha-alert>`
: nothing}
: ""}
</div>
<div class="card-actions">
<ha-button @click=${this._updateNetwork} .disabled=${!this._dirty}>
@@ -266,19 +265,11 @@ export class HassioNetwork extends LitElement {
</ha-circular-progress>`
: this.hass.localize("ui.common.save")}
</ha-button>
<ha-button @click=${this._clear}>
${this.hass.localize("ui.panel.config.network.supervisor.reset")}
</ha-button>
</div>`;
}
private _selectAP(event) {
this._wifiConfiguration = event.currentTarget.ap;
IP_VERSIONS.forEach((version) => {
if (this._interface![version]!.method === "disabled") {
this._interface![version]!.method = "auto";
}
});
this._dirty = true;
}
@@ -288,22 +279,10 @@ export class HassioNetwork extends LitElement {
}
this._scanning = true;
try {
const aps = await accesspointScan(this.hass, this._interface.interface);
this._accessPoints = [];
aps.accesspoints?.forEach((ap) => {
if (ap.ssid) {
// filter out duplicates
const existing = this._accessPoints.find((a) => a.ssid === ap.ssid);
if (!existing) {
this._accessPoints.push(ap);
} else if (ap.signal > existing.signal) {
this._accessPoints = this._accessPoints.filter(
(a) => a.ssid !== ap.ssid
);
this._accessPoints.push(ap);
}
}
});
this._accessPoints = await accesspointScan(
this.hass,
this._interface.interface
);
} catch (err: any) {
showAlertDialog(this, {
title: "Failed to scan for accesspoints",
@@ -315,13 +294,6 @@ export class HassioNetwork extends LitElement {
}
private _renderIPConfiguration(version: string) {
const watingForSSID =
this._interface?.type === "wireless" &&
!this._wifiConfiguration?.ssid &&
!this._interface.wifi?.ssid;
if (watingForSSID) {
return nothing;
}
const nameservers = this._interface![version]?.nameservers || [];
if (nameservers.length === 0) {
nameservers.push(""); // always show input
@@ -512,7 +484,7 @@ export class HassioNetwork extends LitElement {
</ha-list-item>
</ha-button-menu>
`
: nothing}
: ""}
</ha-expansion-panel>
`;
}
@@ -557,13 +529,9 @@ export class HassioNetwork extends LitElement {
}
interfaceOptions.enabled =
// at least one ip version is enabled
(interfaceOptions.ipv4?.method !== "disabled" ||
interfaceOptions.ipv6?.method !== "disabled") &&
// require connection if this is a wireless interface
(this._interface!.type !== "wireless" ||
this._wifiConfiguration !== undefined ||
!!this._interface!.wifi);
this._wifiConfiguration !== undefined ||
interfaceOptions.ipv4?.method !== "disabled" ||
interfaceOptions.ipv6?.method !== "disabled";
try {
await updateNetworkInterface(
@@ -572,7 +540,6 @@ export class HassioNetwork extends LitElement {
interfaceOptions
);
this._dirty = false;
await this._fetchNetworkInfo();
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
@@ -585,20 +552,6 @@ export class HassioNetwork extends LitElement {
}
}
private async _clear() {
await this._fetchNetworkInfo();
this._interface!.ipv4!.method = "auto";
this._interface!.ipv4!.nameservers = [];
this._interface!.ipv6!.method = "auto";
this._interface!.ipv6!.nameservers = [];
// removing the connection will disable the interface
// this is the only way to forget the wifi network right now
this._interface!.wifi = null;
this._wifiConfiguration = undefined;
this._dirty = true;
this.requestUpdate("_interface");
}
private async _handleTabActivated(ev: CustomEvent): Promise<void> {
if (this._dirty) {
const confirm = await showConfirmationDialog(this, {

View File

@@ -292,9 +292,6 @@ export class HaConfigPerson extends LitElement {
align-items: center;
justify-content: space-around;
}
mwc-list:has(+ .empty) {
display: none;
}
`;
}
}

View File

@@ -21,9 +21,11 @@ import "../../../components/ha-list-item";
import "../../../components/ha-svg-icon";
import "../../../components/ha-switch";
import {
AssistDevice,
AssistPipeline,
createAssistPipeline,
deleteAssistPipeline,
listAssistDevices,
listAssistPipelines,
setAssistPipelinePreferred,
updateAssistPipeline,
@@ -40,7 +42,6 @@ import { documentationUrl } from "../../../util/documentation-url";
import { showVoiceAssistantPipelineDetailDialog } from "./show-dialog-voice-assistant-pipeline-detail";
import { showVoiceCommandDialog } from "../../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { computeDomain } from "../../../common/entity/compute_domain";
@customElement("assist-pref")
export class AssistPref extends LitElement {
@@ -57,7 +58,7 @@ export class AssistPref extends LitElement {
@state() private _preferred: string | null = null;
@state() private _pipelineEntitiesCount = 0;
@state() private _devices: AssistDevice[] = [];
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
@@ -66,9 +67,9 @@ export class AssistPref extends LitElement {
this._pipelines = pipelines.pipelines;
this._preferred = pipelines.preferred_pipeline;
});
this._pipelineEntitiesCount = Object.values(this.hass.entities).filter(
(entity) => computeDomain(entity.entity_id) === "assist_satellite"
).length;
listAssistDevices(this.hass).then((devices) => {
this._devices = devices;
});
}
private _exposedEntitiesCount = memoizeOne(
@@ -204,13 +205,13 @@ export class AssistPref extends LitElement {
)}
</ha-button>
</a>
${this._pipelineEntitiesCount > 0
${this._devices?.length
? html`
<a href="/config/voice-assistants/assist/devices">
<ha-button>
${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.pipeline.assist_devices",
{ number: this._pipelineEntitiesCount }
{ number: this._devices.length }
)}
</ha-button>
</a>

View File

@@ -529,7 +529,7 @@ class HaPanelDevAction extends LitElement {
) {
return false;
}
return false;
return hasTemplate(val);
})))
) {
this._yamlMode = true;

View File

@@ -18,7 +18,6 @@ import { HomeAssistant } from "../../types";
import "../lovelace/components/hui-energy-period-selector";
import { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
import { navigate } from "../../common/navigate";
import {
getEnergyDataCollection,
@@ -109,18 +108,14 @@ class PanelEnergy extends LitElement {
</hui-energy-period-selector>
</div>
</div>
<hui-view-container
.hass=${this.hass}
@reload-energy-panel=${this._reloadView}
>
<div id="view" @reload-energy-panel=${this._reloadView}>
<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
></hui-view>
</hui-view-container>
</div>
`;
}
@@ -193,7 +188,9 @@ class PanelEnergy extends LitElement {
row.push(type);
row.push(unit.normalize("NFKD"));
times.forEach((t) => {
if (n < stats[stat].length && stats[stat][n].start === t) {
if (stats[stat][n].start > t) {
row.push("");
} else if (n < stats[stat].length && stats[stat][n].start === t) {
row.push((stats[stat][n].change ?? "").toString());
n++;
} else {
@@ -394,19 +391,23 @@ class PanelEnergy extends LitElement {
line-height: 20px;
flex-grow: 1;
}
hui-view-container {
#view {
position: relative;
display: flex;
padding-top: calc(var(--header-height) + env(safe-area-inset-top));
min-height: 100vh;
box-sizing: border-box;
padding-top: calc(var(--header-height) + env(safe-area-inset-top));
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-inline-start: env(safe-area-inset-left);
padding-inline-end: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
background: var(
--lovelace-background,
var(--primary-background-color)
);
}
hui-view {
#view > * {
flex: 1 1 100%;
max-width: 100%;
}

View File

@@ -231,7 +231,7 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
}
.header {
color: var(--ha-card-header-color, var(--primary-text-color));
color: var(--ha-card-header-color, --primary-text-color);
font-size: var(--ha-card-header-font-size, 24px);
line-height: 1.2;
padding-top: 16px;

View File

@@ -50,11 +50,6 @@ interface MapEntityConfig extends EntityConfig {
focus?: boolean;
}
interface GeoEntity {
entity_id: string;
focus: boolean;
}
@customElement("hui-map-card")
class HuiMapCard extends LitElement implements LovelaceCard {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -337,32 +332,23 @@ class HuiMapCard extends LitElement implements LovelaceCard {
return color;
}
private _getSourceEntities(states?: HassEntities): GeoEntity[] {
private _getSourceEntities(states?: HassEntities): string[] {
if (!states || !this._config?.geo_location_sources) {
return [];
}
const sourceObjs = this._config.geo_location_sources.map((source) =>
typeof source === "string" ? { source } : source
);
const geoEntities: GeoEntity[] = [];
const geoEntities: string[] = [];
// Calculate visible geo location sources
const allSource = sourceObjs.find((s) => s.source === "all");
const includesAll = this._config.geo_location_sources.includes("all");
for (const stateObj of Object.values(states)) {
const sourceObj = sourceObjs.find(
(s) => s.source === stateObj.attributes.source
);
if (
computeDomain(stateObj.entity_id) === "geo_location" &&
(allSource || sourceObj)
(includesAll ||
this._config.geo_location_sources.includes(
stateObj.attributes.source
))
) {
geoEntities.push({
entity_id: stateObj.entity_id,
focus: sourceObj
? (sourceObj.focus ?? true)
: (allSource?.focus ?? true),
});
geoEntities.push(stateObj.entity_id);
}
}
return geoEntities;
@@ -378,9 +364,8 @@ class HuiMapCard extends LitElement implements LovelaceCard {
name: entityConf.name,
})),
...this._getSourceEntities(this.hass?.states).map((entity) => ({
entity_id: entity.entity_id,
focus: entity.focus,
color: this._getColor(entity.entity_id),
entity_id: entity,
color: this._getColor(entity),
})),
];
}

View File

@@ -298,11 +298,6 @@ export interface LogbookCardConfig extends LovelaceCardConfig {
theme?: string;
}
interface GeoLocationSourceConfig {
source: string;
focus?: boolean;
}
export interface MapCardConfig extends LovelaceCardConfig {
type: "map";
title?: string;
@@ -312,7 +307,7 @@ export interface MapCardConfig extends LovelaceCardConfig {
default_zoom?: number;
entities?: Array<EntityConfig | string>;
hours_to_show?: number;
geo_location_sources?: Array<GeoLocationSourceConfig | string>;
geo_location_sources?: string[];
dark_mode?: boolean;
theme_mode?: ThemeMode;
}

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