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

View File

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

View File

@@ -23,7 +23,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.1
with: with:
# We must fetch at least the immediate parents so that if this is # We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head. # 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 }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.1
with: with:
ref: dev ref: dev
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.1.0 uses: actions/setup-node@v4.0.4
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -58,12 +58,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.1
with: with:
ref: master ref: master
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.1.0 uses: actions/setup-node@v4.0.4
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@@ -16,10 +16,10 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.1.0 uses: actions/setup-node@v4.0.4
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn 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') if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.1.0 uses: actions/setup-node@v4.0.4
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -88,7 +88,7 @@ class HcLayout extends LitElement {
} }
.card-header { .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-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px); font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em; letter-spacing: -0.012em;

View File

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

View File

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

View File

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

View File

@@ -37,6 +37,7 @@ import "./config/hassio-addon-config";
import "./config/hassio-addon-network"; import "./config/hassio-addon-network";
import "./hassio-addon-router"; import "./hassio-addon-router";
import "./info/hassio-addon-info"; import "./info/hassio-addon-info";
import "./log/hassio-addon-logs";
@customElement("hassio-addon-dashboard") @customElement("hassio-addon-dashboard")
class HassioAddonDashboard extends LitElement { class HassioAddonDashboard extends LitElement {
@@ -160,11 +161,16 @@ class HassioAddonDashboard extends LitElement {
margin-bottom: 24px; margin-bottom: 24px;
width: 600px; width: 600px;
} }
hassio-addon-logs {
max-width: calc(100% - 8px);
min-width: 600px;
}
@media only screen and (max-width: 600px) { @media only screen and (max-width: 600px) {
hassio-addon-info, hassio-addon-info,
hassio-addon-network, hassio-addon-network,
hassio-addon-audio, hassio-addon-audio,
hassio-addon-config { hassio-addon-config,
hassio-addon-logs {
max-width: 100%; max-width: 100%;
min-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-settings-row";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch"; import "../../../../src/components/ha-switch";
import type { HaSwitch } from "../../../../src/components/ha-switch";
import { import {
AddonCapability, AddonCapability,
HassioAddonDetails, HassioAddonDetails,
@@ -1120,28 +1119,12 @@ class HassioAddonInfo extends LitElement {
private async _uninstallClicked(ev: CustomEvent): Promise<void> { private async _uninstallClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any; const button = ev.currentTarget as any;
button.progress = true; button.progress = true;
let removeData = false;
const _removeDataToggled = (e: Event) => {
removeData = (e.target as HaSwitch).checked;
};
const confirmed = await showConfirmationDialog(this, { const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize("dialog.uninstall_addon.title", { title: this.supervisor.localize("dialog.uninstall_addon.title", {
name: this.addon.name, name: this.addon.name,
}), }),
text: html` text: this.supervisor.localize("dialog.uninstall_addon.text"),
<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>
`,
confirmText: this.supervisor.localize("dialog.uninstall_addon.uninstall"), confirmText: this.supervisor.localize("dialog.uninstall_addon.uninstall"),
dismissText: this.supervisor.localize("common.cancel"), dismissText: this.supervisor.localize("common.cancel"),
destructive: true, destructive: true,
@@ -1154,7 +1137,7 @@ class HassioAddonInfo extends LitElement {
this._error = undefined; this._error = undefined;
try { try {
await uninstallHassioAddon(this.hass, this.addon.slug, removeData); await uninstallHassioAddon(this.hass, this.addon.slug);
const eventdata = { const eventdata = {
success: true, success: true,
response: undefined, response: undefined,
@@ -1209,7 +1192,7 @@ class HassioAddonInfo extends LitElement {
padding-inline-start: 8px; padding-inline-start: 8px;
padding-inline-end: initial; padding-inline-end: initial;
font-size: 24px; font-size: 24px;
color: var(--ha-card-header-color, var(--primary-text-color)); color: var(--ha-card-header-color, --primary-text-color);
} }
.addon-version { .addon-version {
float: var(--float-end); float: var(--float-end);

View File

@@ -1,14 +1,12 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; 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 "../../../../src/components/ha-circular-progress";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon"; import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { Supervisor } from "../../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles"; import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style"; import { hassioStyle } from "../../resources/hassio-style";
import "../../../../src/panels/config/logs/error-log-card"; import "./hassio-addon-logs";
import "../../../../src/components/search-input";
import { extractSearchParam } from "../../../../src/common/url/search-params";
@customElement("hassio-addon-log-tab") @customElement("hassio-addon-log-tab")
class HassioAddonLogDashboard extends LitElement { class HassioAddonLogDashboard extends LitElement {
@@ -18,8 +16,6 @@ class HassioAddonLogDashboard extends LitElement {
@property({ attribute: false }) public addon?: HassioAddonDetails; @property({ attribute: false }) public addon?: HassioAddonDetails;
@state() private _filter = extractSearchParam("filter") || "";
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.addon) { if (!this.addon) {
return html` return html`
@@ -27,31 +23,16 @@ class HassioAddonLogDashboard extends LitElement {
`; `;
} }
return html` 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"> <div class="content">
<error-log-card <hassio-addon-logs
.hass=${this.hass} .hass=${this.hass}
.header=${this.addon.name} .supervisor=${this.supervisor}
.provider=${this.addon.slug} .addon=${this.addon}
show ></hassio-addon-logs>
.filter=${this._filter}
>
</error-log-card>
</div> </div>
`; `;
} }
private async _filterChanged(ev) {
this._filter = ev.detail.value;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,
@@ -60,21 +41,7 @@ class HassioAddonLogDashboard extends LitElement {
.content { .content {
margin: auto; margin: auto;
padding: 8px; padding: 8px;
} max-width: 1024px;
.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);
}
} }
`, `,
]; ];

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,5 @@
import { import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
css, import { customElement, property } from "lit/decorators";
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import {
customElement,
property,
query,
state as litState,
} from "lit/decorators";
interface State { interface State {
bold: boolean; bold: boolean;
@@ -23,24 +11,11 @@ interface State {
} }
@customElement("ha-ansi-to-html") @customElement("ha-ansi-to-html")
export class HaAnsiToHtml extends LitElement { class HaAnsiToHtml extends LitElement {
@property() public content!: string; @property() public content!: string;
@query("pre") private _pre?: HTMLPreElement;
@litState() private _filter = "";
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
return html`<pre></pre>`; return html`${this._parseTextToColoredPre(this.content)}`;
}
protected firstUpdated(_changedProperties: PropertyValues): void {
super.firstUpdated(_changedProperties);
// handle initial content
if (this.content) {
this.parseTextToColoredPre(this.content);
}
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
@@ -49,7 +24,6 @@ export class HaAnsiToHtml extends LitElement {
overflow-x: auto; overflow-x: auto;
white-space: pre-wrap; white-space: pre-wrap;
overflow-wrap: break-word; overflow-wrap: break-word;
margin: 0;
} }
.bold { .bold {
font-weight: bold; font-weight: bold;
@@ -111,33 +85,11 @@ export class HaAnsiToHtml extends LitElement {
.bg-white { .bg-white {
background-color: rgb(204, 204, 204); background-color: rgb(204, 204, 204);
} }
::highlight(search-results) {
background-color: var(--primary-color);
color: var(--text-primary-color);
}
`; `;
} }
/** private _parseTextToColoredPre(text) {
* add new lines to the log const pre = document.createElement("pre");
* @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");
// eslint-disable-next-line no-control-regex // eslint-disable-next-line no-control-regex
const re = /\x1b(?:\[(.*?)[@-~]|\].*?(?:\x07|\x1b\\))/g; const re = /\x1b(?:\[(.*?)[@-~]|\].*?(?:\x07|\x1b\\))/g;
let i = 0; let i = 0;
@@ -151,7 +103,7 @@ export class HaAnsiToHtml extends LitElement {
backgroundColor: null, backgroundColor: null,
}; };
const addPart = (content) => { const addSpan = (content) => {
const span = document.createElement("span"); const span = document.createElement("span");
if (state.bold) { if (state.bold) {
span.classList.add("bold"); span.classList.add("bold");
@@ -172,18 +124,15 @@ export class HaAnsiToHtml extends LitElement {
span.classList.add(`bg-${state.backgroundColor}`); span.classList.add(`bg-${state.backgroundColor}`);
} }
span.appendChild(document.createTextNode(content)); span.appendChild(document.createTextNode(content));
lineDiv.appendChild(span); pre.appendChild(span);
}; };
/* eslint-disable no-cond-assign */ /* eslint-disable no-cond-assign */
let match; let match;
// eslint-disable-next-line // eslint-disable-next-line
while ((match = re.exec(line)) !== null) { while ((match = re.exec(text)) !== null) {
const j = match!.index; const j = match!.index;
const substring = line.substring(i, j); addSpan(text.substring(i, j));
if (substring) {
addPart(substring);
}
i = j + match[0].length; i = j + match[0].length;
if (match[1] === undefined) { if (match[1] === undefined) {
@@ -285,93 +234,9 @@ export class HaAnsiToHtml extends LitElement {
} }
}); });
} }
addSpan(text.substring(i));
const substring = line.substring(i); return pre;
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 = "";
}
} }
} }

View File

@@ -43,7 +43,7 @@ export class HaCard extends LitElement {
.card-header, .card-header,
:host ::slotted(.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-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px); font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em; letter-spacing: -0.012em;

View File

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

View File

@@ -216,7 +216,6 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
ha-input-chip { ha-input-chip {
--md-input-chip-selected-container-color: var(--color, var(--grey-color)); --md-input-chip-selected-container-color: var(--color, var(--grey-color));
--ha-input-chip-selected-container-opacity: 0.5; --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-size: 1.5em;
font-weight: bold; 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 { PropertyValues, ReactiveElement } from "lit";
import { parseISO } from "date-fns";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { relativeTime } from "../common/datetime/relative_time"; import { relativeTime } from "../common/datetime/relative_time";
import { capitalizeFirstLetter } from "../common/string/capitalize-first-letter"; import { capitalizeFirstLetter } from "../common/string/capitalize-first-letter";
@@ -59,12 +58,7 @@ class HaRelativeTime extends ReactiveElement {
if (!this.datetime) { if (!this.datetime) {
this.innerHTML = this.hass.localize("ui.components.relative_time.never"); this.innerHTML = this.hass.localize("ui.components.relative_time.never");
} else { } else {
const date = const relTime = relativeTime(new Date(this.datetime), this.hass.locale);
typeof this.datetime === "string"
? parseISO(this.datetime)
: this.datetime;
const relTime = relativeTime(date, this.hass.locale);
this.innerHTML = this.capitalize this.innerHTML = this.capitalize
? capitalizeFirstLetter(relTime) ? capitalizeFirstLetter(relTime)
: relTime; : relTime;

View File

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

View File

@@ -42,17 +42,14 @@ export class HaSettingsRow extends LitElement {
padding-bottom: 8px; padding-bottom: 8px;
padding-left: 0; padding-left: 0;
padding-inline-start: 0; padding-inline-start: 0;
padding-right: 16px; padding-right: 16x;
padding-inline-end: 16px; padding-inline-end: 16px;
overflow: hidden; overflow: hidden;
display: var(--layout-vertical_-_display, flex); display: var(--layout-vertical_-_display);
flex-direction: var(--layout-vertical_-_flex-direction, column); flex-direction: var(--layout-vertical_-_flex-direction);
justify-content: var( justify-content: var(--layout-center-justified_-_justify-content);
--layout-center-justified_-_justify-content, flex: var(--layout-flex_-_flex);
center flex-basis: var(--layout-flex_-_flex-basis);
);
flex: var(--layout-flex_-_flex, 1);
flex-basis: var(--layout-flex_-_flex-basis, 0.000000001px);
} }
.body[three-line] { .body[three-line] {
min-height: var(--paper-item-body-three-line-min-height, 88px); 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; border-bottom: 1px solid transparent;
white-space: nowrap; white-space: nowrap;
font-weight: 400; font-weight: 400;
color: var( color: var(--sidebar-menu-button-text-color, --primary-text-color);
--sidebar-menu-button-text-color,
var(--primary-text-color)
);
border-bottom: 1px solid var(--divider-color); border-bottom: 1px solid var(--divider-color);
background-color: var( background-color: var(
--sidebar-menu-button-background-color, --sidebar-menu-button-background-color,
var(--primary-background-color) --primary-background-color
); );
font-size: 20px; font-size: 20px;
align-items: center; align-items: center;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,6 +14,7 @@ import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import { haStyleDialog } from "../../resources/styles"; import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { VoiceAssistantSetupDialogParams } from "./show-voice-assistant-setup-dialog"; 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-area";
import "./voice-assistant-setup-step-change-wake-word"; import "./voice-assistant-setup-step-change-wake-word";
import "./voice-assistant-setup-step-check"; import "./voice-assistant-setup-step-check";
@@ -33,6 +34,7 @@ export const enum STEP {
PIPELINE, PIPELINE,
SUCCESS, SUCCESS,
CLOUD, CLOUD,
ADDONS,
CHANGE_WAKEWORD, CHANGE_WAKEWORD,
} }
@@ -208,18 +210,22 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
? html`<ha-voice-assistant-setup-step-cloud ? html`<ha-voice-assistant-setup-step-cloud
.hass=${this.hass} .hass=${this.hass}
></ha-voice-assistant-setup-step-cloud>` ></ha-voice-assistant-setup-step-cloud>`
: this._step === STEP.SUCCESS : this._step === STEP.ADDONS
? html`<ha-voice-assistant-setup-step-success ? html`<ha-voice-assistant-setup-step-addons
.hass=${this.hass} .hass=${this.hass}
.assistConfiguration=${this ></ha-voice-assistant-setup-step-addons>`
._assistConfiguration} : this._step === STEP.SUCCESS
.assistEntityId=${this._findDomainEntityId( ? html`<ha-voice-assistant-setup-step-success
this._params.deviceId, .hass=${this.hass}
this.hass.entities, .assistConfiguration=${this
"assist_satellite" ._assistConfiguration}
)} .assistEntityId=${this._findDomainEntityId(
></ha-voice-assistant-setup-step-success>` this._params.deviceId,
: nothing} this.hass.entities,
"assist_satellite"
)}
></ha-voice-assistant-setup-step-success>`
: nothing}
</div> </div>
</ha-dialog> </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]; const device = this.hass.devices[this.deviceId];
return html`<div class="content"> return html`<div class="content">
<img src="/static/images/voice-assistant/area.gif" /> <img src="/static/icons/casita/loving.png" />
<h1>Select area</h1> <h1>Select area</h1>
<p class="secondary"> <p class="secondary">
When you voice assistant knows where it is, it can better control the When you voice assistant knows where it is, it can better control the

View File

@@ -10,7 +10,6 @@ import { STEP } from "./voice-assistant-setup-dialog";
import { AssistantSetupStyles } from "./styles"; import { AssistantSetupStyles } from "./styles";
import "../../components/ha-md-list"; import "../../components/ha-md-list";
import "../../components/ha-md-list-item"; import "../../components/ha-md-list-item";
import { formatLanguageCode } from "../../common/language/format_language";
@customElement("ha-voice-assistant-setup-step-change-wake-word") @customElement("ha-voice-assistant-setup-step-change-wake-word")
export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement { export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
@@ -23,12 +22,11 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
protected override render() { protected override render() {
return html`<div class="padding content"> return html`<div class="padding content">
<img src="/static/images/voice-assistant/change-wake-word.gif" /> <img src="/static/icons/casita/smiling.png" />
<h1>Change wake word</h1> <h1>Change wake word</h1>
<p class="secondary"> <p class="secondary">
Some wake words are better for Some wake words are better for [your language] and voice than others.
${formatLanguageCode(this.hass.locale.language, this.hass.locale)} and Please try them out.
voice than others. Please try them out.
</p> </p>
</div> </div>
<ha-md-list> <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 { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { testAssistSatelliteConnection } from "../../data/assist_satellite"; import { testAssistSatelliteConnection } from "../../data/assist_satellite";
@@ -13,8 +13,6 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
@state() private _status?: "success" | "timeout"; @state() private _status?: "success" | "timeout";
@state() private _showLoader = false;
protected override willUpdate(changedProperties: PropertyValues): void { protected override willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties); super.willUpdate(changedProperties);
if (!this.hasUpdated) { if (!this.hasUpdated) {
@@ -32,48 +30,39 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
protected override render() { protected override render() {
return html`<div class="content"> return html`<div class="content">
${this._status === "timeout" ${this._status === "success"
? html`<img src="/static/images/voice-assistant/error.gif" /> ? html`<img src="/static/icons/casita/smiling.png" />
<h1>The voice assistant is unable to connect to Home Assistant</h1>
<p class="secondary">
To play audio, the voice assistant device has to connect to Home
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" />
<h1>Hi</h1> <h1>Hi</h1>
<p class="secondary"> <p class="secondary">
Over the next couple steps we're going to personalize your voice With a couple of steps we are going to setup your voice assistant.
assistant. </p>`
</p> : this._status === "timeout"
? html`<img src="/static/icons/casita/sad.png" />
${this._showLoader <h1>Voice assistant can not connect to Home Assistant</h1>
? html`<ha-circular-progress <p class="secondary">
indeterminate A good explanation what is happening and what action you should
></ha-circular-progress>` take.
: nothing} `} </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>`; </div>`;
} }
private async _testConnection() { private async _testConnection() {
this._status = undefined; this._status = undefined;
this._showLoader = false;
const timeout = setTimeout(() => {
this._showLoader = true;
}, 3000);
const result = await testAssistSatelliteConnection( const result = await testAssistSatelliteConnection(
this.hass, this.hass,
this.assistEntityId! this.assistEntityId!
); );
clearTimeout(timeout);
this._showLoader = false;
this._status = result.status; this._status = result.status;
} }

View File

@@ -1,9 +1,7 @@
import { mdiEarth, mdiMicrophoneMessage, mdiOpenInNew } from "@mdi/js"; import { html, LitElement } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import { AssistantSetupStyles } from "./styles"; import { AssistantSetupStyles } from "./styles";
@customElement("ha-voice-assistant-setup-step-cloud") @customElement("ha-voice-assistant-setup-step-cloud")
@@ -12,92 +10,22 @@ export class HaVoiceAssistantSetupStepCloud extends LitElement {
protected override render() { protected override render() {
return html`<div class="content"> return html`<div class="content">
<img <img src="/static/images/logo_nabu_casa.png" />
src=${`/static/images/logo_nabu_casa${this.hass.themes?.darkMode ? "_dark" : ""}.png`} <h1>Supercharge your assistant with Home Assistant Cloud</h1>
alt="Nabu Casa logo" <p class="secondary">
/> Speed up and take the load off your system by running your
<h1>The power of Home Assistant Cloud</h1> text-to-speech and speech-to-text in our private and secure cloud.
<div class="features"> Cloud also includes secure remote access to your system while
<div class="feature speech"> supporting the development of Home Assistant.
<div class="logos"> </p>
<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>
</div> </div>
<div class="footer side-by-side"> <div class="footer side-by-side">
<a <a
href="https://www.nabucasa.com" href="https://www.nabucasa.com"
target="_blank" target="_blank"
rel="noreferrer noopenner" 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} <a href="/config/cloud/register" @click=${this._close}
><ha-button unelevated>Try 1 month for free</ha-button></a ><ha-button unelevated>Try 1 month for free</ha-button></a
> >
@@ -108,58 +36,7 @@ export class HaVoiceAssistantSetupStepCloud extends LitElement {
fireEvent(this, "closed"); fireEvent(this, "closed");
} }
static styles = [ static styles = AssistantSetupStyles;
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;
}
`,
];
} }
declare global { declare global {

View File

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

View File

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

View File

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

View File

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

View File

@@ -125,15 +125,7 @@ class HassSubpage extends LitElement {
.main-title { .main-title {
margin: var(--margin-title); margin: var(--margin-title);
line-height: 20px; line-height: 20px;
min-width: 0;
flex-grow: 1; 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 { .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 { ACTION_ICONS, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
import { AutomationClipboard } from "../../../../data/automation"; import { AutomationClipboard } from "../../../../data/automation";
import { validateConfig } from "../../../../data/config"; import { validateConfig } from "../../../../data/config";
import { import { fullEntitiesContext, labelsContext } from "../../../../data/context";
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../../../data/context";
import { EntityRegistryEntry } from "../../../../data/entity_registry"; import { EntityRegistryEntry } from "../../../../data/entity_registry";
import { FloorRegistryEntry } from "../../../../data/floor_registry";
import { LabelRegistryEntry } from "../../../../data/label_registry"; import { LabelRegistryEntry } from "../../../../data/label_registry";
import { import {
Action, Action,
@@ -159,10 +154,6 @@ export default class HaAutomationActionRow extends LitElement {
@consume({ context: labelsContext, subscribe: true }) @consume({ context: labelsContext, subscribe: true })
_labelReg!: LabelRegistryEntry[]; _labelReg!: LabelRegistryEntry[];
@state()
@consume({ context: floorsContext, subscribe: true })
_floorReg!: { [id: string]: FloorRegistryEntry };
@state() private _warnings?: string[]; @state() private _warnings?: string[];
@state() private _uiModeAvailable = true; @state() private _uiModeAvailable = true;
@@ -231,7 +222,6 @@ export default class HaAutomationActionRow extends LitElement {
this.hass, this.hass,
this._entityReg, this._entityReg,
this._labelReg, this._labelReg,
this._floorReg,
this.action this.action
) )
)} )}
@@ -603,7 +593,6 @@ export default class HaAutomationActionRow extends LitElement {
this.hass, this.hass,
this._entityReg, this._entityReg,
this._labelReg, this._labelReg,
this._floorReg,
this.action, this.action,
undefined, undefined,
true true

View File

@@ -1,5 +1,5 @@
import { consume } from "@lit-labs/context"; 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 { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event"; 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() { protected render() {
const deviceId = this._deviceId || this.action.device_id; const deviceId = this._deviceId || this.action.device_id;

View File

@@ -1,5 +1,5 @@
import { consume } from "@lit-labs/context"; 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 { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event"; 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() { protected render() {
const deviceId = this._deviceId || this.condition.device_id; const deviceId = this._deviceId || this.condition.device_id;

View File

@@ -1,5 +1,5 @@
import { consume } from "@lit-labs/context"; 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 { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event"; 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() { protected render() {
const deviceId = this._deviceId || this.trigger.device_id; const deviceId = this._deviceId || this.trigger.device_id;

View File

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

View File

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

View File

@@ -103,7 +103,6 @@ export class HaConfigLabels extends LitElement {
style=" style="
background-color: ${computeCssColor(label.color)}; background-color: ${computeCssColor(label.color)};
border-radius: 10px; border-radius: 10px;
outline: 1px solid var(--outline-color);
width: 20px; width: 20px;
height: 20px;" height: 20px;"
></div>` ></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 "@material/mwc-list/mwc-list-item";
import { import { mdiRefresh, mdiDownload } from "@mdi/js";
mdiArrowCollapseDown,
mdiDownload,
mdiMenuDown,
mdiRefresh,
} from "@mdi/js";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -12,47 +8,27 @@ import {
LitElement, LitElement,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
nothing,
} from "lit"; } from "lit";
import { classMap } from "lit/directives/class-map"; import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
// eslint-disable-next-line import/extensions
import { IntersectionController } from "@lit-labs/observers/intersection-controller.js";
import { customElement, property, state, query } from "lit/decorators";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-ansi-to-html"; import "../../../components/ha-ansi-to-html";
import type { HaAnsiToHtml } from "../../../components/ha-ansi-to-html";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-button";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-select";
import "../../../components/ha-svg-icon"; 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 { getSignedPath } from "../../../data/auth";
import { fetchErrorLog, getErrorLogDownloadUrl } from "../../../data/error_log"; import { fetchErrorLog, getErrorLogDownloadUrl } from "../../../data/error_log";
import { extractApiErrorMessage } from "../../../data/hassio/common"; import { extractApiErrorMessage } from "../../../data/hassio/common";
import { import {
fetchHassioBoots,
fetchHassioLogs, fetchHassioLogs,
fetchHassioLogsFollow,
getHassioLogDownloadUrl, getHassioLogDownloadUrl,
} from "../../../data/hassio/supervisor"; } from "../../../data/hassio/supervisor";
import { HomeAssistant } from "../../../types"; 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 { debounce } from "../../../common/util/debounce";
import { showDownloadLogsDialog } from "./show-dialog-download-logs"; import { fileDownload } from "../../../util/file_download";
import type { HaMenu } from "../../../components/ha-menu";
const NUMBER_OF_LINES = 100;
@customElement("error-log-card") @customElement("error-log-card")
class ErrorLogCard extends LitElement { class ErrorLogCard extends LitElement {
@@ -66,190 +42,52 @@ class ErrorLogCard extends LitElement {
@property({ type: Boolean, attribute: true }) public show = false; @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; @state() private _logHTML?: TemplateResult[] | TemplateResult | string;
@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 _error?: 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 { protected render(): TemplateResult {
return html` return html`
<div class="error-log-intro"> <div class="error-log-intro">
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""} : ""}
<ha-card outlined class=${classMap({ hidden: this.show === false })}> ${this._logHTML
<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
? html` ? 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> <ha-svg-icon .path=${mdiDownload}></ha-svg-icon>
${this.hass.localize("ui.panel.config.logs.download_full_log")} ${this.hass.localize("ui.panel.config.logs.download_full_log")}
</ha-button> </mwc-button>
<mwc-button raised @click=${this._showLogs}> <mwc-button raised @click=${this._refreshLogs}>
${this.hass.localize("ui.panel.config.logs.load_logs")} ${this.hass.localize("ui.panel.config.logs.load_logs")}
</mwc-button> </mwc-button>
` `
@@ -258,337 +96,127 @@ class ErrorLogCard extends LitElement {
`; `;
} }
public connectedCallback() { private _debounceSearch = debounce(
super.connectedCallback(); () => (this._isLogLoaded ? this._refreshLogs() : this._debounceSearch()),
150,
if (this._streamSupported === undefined) { false
this._streamSupported = atLeastVersion( );
this.hass.config.version,
2024,
11
);
}
}
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); 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) { if (this.hass?.config.recovery_mode || this.show) {
this.hass.loadFragmentTranslation("config"); 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) { protected updated(changedProps) {
super.updated(changedProps); super.updated(changedProps);
if (changedProps.has("provider")) {
this._logHTML = undefined;
}
if ( if (
(changedProps.has("show") && this.show) || (changedProps.has("show") && this.show) ||
(changedProps.has("provider") && this.show) (changedProps.has("provider") && this.show)
) { ) {
this._boot = 0; this._refreshLogs();
this._loadLogs(); return;
}
if (this._newLogsIndicator && this._scrolledToBottomController.value) {
this._newLogsIndicator = false;
} }
if (changedProps.has("filter")) { if (changedProps.has("filter")) {
this._debounceSearch(); this._debounceSearch();
} }
if (
changedProps.has("_loadingState") &&
this._loadingState === "loaded" &&
this._scrolledToTopController.value &&
this._firstCursor &&
!this._loadingPrevState
) {
this._loadMoreLogs();
}
} }
disconnectedCallback() { private async _refresh(ev: CustomEvent): Promise<void> {
super.disconnectedCallback(); const button = ev.currentTarget as any;
button.progress = true;
if (this._logStreamAborter) { await this._refreshLogs();
this._logStreamAborter.abort(); button.progress = false;
}
window.removeEventListener(
"connection-status",
this._handleConnectionStatus
);
} }
private async _downloadFullLog(): Promise<void> { private async _downloadFullLog(): Promise<void> {
if (this._streamSupported) { const timeString = new Date().toISOString().replace(/:/g, "-");
showDownloadLogsDialog(this, { const downloadUrl =
header: this.header, this.provider !== "core"
provider: this.provider, ? getHassioLogDownloadUrl(this.provider)
defaultLineCount: this._numberOfLines, : getErrorLogDownloadUrl;
boot: this._boot, const logFileName =
}); this.provider !== "core"
} else { ? `${this.provider}_${timeString}.log`
const timeString = new Date().toISOString().replace(/:/g, "-"); : `home-assistant_${timeString}.log`;
const downloadUrl = const signedUrl = await getSignedPath(this.hass, downloadUrl);
this.provider && this.provider !== "core" fileDownload(signedUrl.path, logFileName);
? 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);
}
} }
private _showLogs(): void { private async _refreshLogs(): Promise<void> {
this.show = true; this._logHTML = this.hass.localize("ui.panel.config.logs.loading_log");
} let log: string;
private async _loadLogs(): Promise<void> { if (this.provider !== "core" && isComponentLoaded(this.hass, "hassio")) {
this._error = undefined; try {
this._loadingState = "loading"; log = await fetchHassioLogs(this.hass, this.provider);
this._loadingPrevState = undefined; if (this.filter) {
this._firstCursor = undefined; log = log
this._numberOfLines = 0; .split("\n")
this._ansiToHtmlElement?.clear(); .filter((entry) =>
entry.toLowerCase().includes(this.filter.toLowerCase())
try { )
if (this._logStreamAborter) { .join("\n");
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 (!log) {
if (!response.body) { this._logHTML = this.hass.localize("ui.panel.config.logs.no_errors");
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";
return; return;
} }
this._firstCursor = response.headers.get("X-First-Cursor")!; this._logHTML = html`<ha-ansi-to-html .content=${log}>
} </ha-ansi-to-html>`;
this._isLogLoaded = true;
const body = await response.text(); return;
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);
} catch (err: any) { } catch (err: any) {
// eslint-disable-next-line no-console this._error = this.hass.localize(
console.error(err); "ui.panel.config.logs.failed_get_logs",
{ provider: this.provider, error: extractApiErrorMessage(err) }
);
return;
} }
} else {
log = await fetchErrorLog(this.hass!);
} }
}
private _toggleBootsMenu() { this._isLogLoaded = true;
if (this._bootsMenu) {
this._bootsMenu.open = !this._bootsMenu.open;
}
}
private _setBoot(ev: any) { const split = log && log.split("\n");
this._boot = ev.target.value;
this._loadLogs(); 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` static styles: CSSResultGroup = css`
@@ -598,18 +226,7 @@ class ErrorLogCard extends LitElement {
} }
ha-card { ha-card {
padding-top: 8px; padding-top: 16px;
position: relative;
}
ha-card.hidden {
display: none;
}
ha-card .action-buttons {
display: flex;
align-items: center;
height: 100%;
} }
.header { .header {
@@ -619,18 +236,21 @@ class ErrorLogCard extends LitElement {
} }
.card-header { .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-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px); font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em; letter-spacing: -0.012em;
line-height: 48px; line-height: 48px;
display: block; display: block;
margin-block-start: 0px; margin-block-start: 0px;
margin-block-end: 0px;
font-weight: normal; font-weight: normal;
white-space: nowrap; }
max-width: calc(100% - 150px);
overflow: hidden; ha-select {
text-overflow: ellipsis; display: block;
max-width: 500px;
width: 100%;
} }
ha-icon-button { ha-icon-button {
@@ -638,24 +258,10 @@ class ErrorLogCard extends LitElement {
} }
.error-log { .error-log {
position: relative;
font-family: var(--code-font-family, monospace); font-family: var(--code-font-family, monospace);
clear: both; clear: both;
text-align: left; text-align: left;
padding-top: 12px; 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 { .error-log > div {
@@ -667,28 +273,6 @@ class ErrorLogCard extends LitElement {
background-color: var(--secondary-background-color); 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 { .error {
color: var(--error-color); color: var(--error-color);
} }
@@ -697,16 +281,8 @@ class ErrorLogCard extends LitElement {
color: var(--warning-color); color: var(--warning-color);
} }
.loading-old { mwc-button {
display: flex; direction: var(--direction);
width: 100%;
justify-content: center;
padding: 16px;
}
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
--md-assist-chip-trailing-space: 8px;
} }
`; `;
} }

View File

@@ -167,7 +167,6 @@ export class HaConfigLogs extends LitElement {
private _selectProvider(ev) { private _selectProvider(ev) {
this._selectedLogProvider = (ev.currentTarget as any).provider; this._selectedLogProvider = (ev.currentTarget as any).provider;
this._filter = "";
navigate(`/config/logs?provider=${this._selectedLogProvider}`); 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 { .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-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px); font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em; letter-spacing: -0.012em;

View File

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

View File

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

View File

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

View File

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

View File

@@ -231,7 +231,7 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
} }
.header { .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); font-size: var(--ha-card-header-font-size, 24px);
line-height: 1.2; line-height: 1.2;
padding-top: 16px; padding-top: 16px;

View File

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

View File

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

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