Compare commits
100 Commits
grid_secti
...
partial_te
Author | SHA1 | Date | |
---|---|---|---|
![]() |
85b24afc10 | ||
![]() |
42f2341e06 | ||
![]() |
48dfa1163b | ||
![]() |
28e12f7fd1 | ||
![]() |
5f58ac4fb6 | ||
![]() |
25e7c4f1b2 | ||
![]() |
00934f2183 | ||
![]() |
d302eaffe6 | ||
![]() |
901f736d5f | ||
![]() |
e55f32ae91 | ||
![]() |
3e0c998e74 | ||
![]() |
597866ff4e | ||
![]() |
64e21e185c | ||
![]() |
7a1838ee1a | ||
![]() |
4debac60ae | ||
![]() |
4af231e62b | ||
![]() |
432cf4a7ed | ||
![]() |
df064967ca | ||
![]() |
dc0cab9307 | ||
![]() |
3180747a0a | ||
![]() |
1542095138 | ||
![]() |
6c1937f247 | ||
![]() |
9db1e52a55 | ||
![]() |
f92c63135c | ||
![]() |
a3bf1a014b | ||
![]() |
31fba48ad5 | ||
![]() |
05dfa1bb1a | ||
![]() |
f0f47aac3b | ||
![]() |
9b42494667 | ||
![]() |
42df951f89 | ||
![]() |
f4996424a2 | ||
![]() |
fd01302d9a | ||
![]() |
2daaa1cb9c | ||
![]() |
9fde175c6b | ||
![]() |
f1b24e847e | ||
![]() |
7a587de54e | ||
![]() |
eb69f95f83 | ||
![]() |
359a3a4af9 | ||
![]() |
a8b75e7814 | ||
![]() |
2b898822d1 | ||
![]() |
fc9a0958d4 | ||
![]() |
5843877cc8 | ||
![]() |
913837f064 | ||
![]() |
386ac5d779 | ||
![]() |
3a1a4ade68 | ||
![]() |
5d49f4007e | ||
![]() |
ca20c2d292 | ||
![]() |
f1ab24da99 | ||
![]() |
e16e851952 | ||
![]() |
0b562a4b16 | ||
![]() |
7734922059 | ||
![]() |
54320c3dbf | ||
![]() |
849cfed669 | ||
![]() |
8932dfd504 | ||
![]() |
b9922b2f8e | ||
![]() |
11fc5bc755 | ||
![]() |
67ac4882f2 | ||
![]() |
413171bb3c | ||
![]() |
b111eb2316 | ||
![]() |
9bafabe3e9 | ||
![]() |
202bc6440b | ||
![]() |
e2a89a55b7 | ||
![]() |
1e05730ec7 | ||
![]() |
885a63d3f6 | ||
![]() |
206fbac618 | ||
![]() |
4509661652 | ||
![]() |
4669decfd0 | ||
![]() |
f05c204da3 | ||
![]() |
338692d2c3 | ||
![]() |
5415690585 | ||
![]() |
418315d20b | ||
![]() |
9bbffb6919 | ||
![]() |
e338d63ec5 | ||
![]() |
264aedbff3 | ||
![]() |
4103ef362c | ||
![]() |
a04a449eb9 | ||
![]() |
08633c197b | ||
![]() |
f8b3a429c6 | ||
![]() |
946c8a59b4 | ||
![]() |
f93c7e1b6e | ||
![]() |
6298534b9c | ||
![]() |
5175b42069 | ||
![]() |
e01e31341b | ||
![]() |
203d900d16 | ||
![]() |
4d9e9aaead | ||
![]() |
82ec308be0 | ||
![]() |
dcafbcb06c | ||
![]() |
aa5f8dc082 | ||
![]() |
4dcae9c69c | ||
![]() |
13a1af97da | ||
![]() |
e3c435fd78 | ||
![]() |
a32dee7071 | ||
![]() |
c098858b73 | ||
![]() |
9e509e3bc9 | ||
![]() |
79ac2a72fa | ||
![]() |
b063840f46 | ||
![]() |
4366308b2b | ||
![]() |
126826e52c | ||
![]() |
e31af5d31b | ||
![]() |
fca97cd734 |
8
.github/workflows/cast_deployment.yaml
vendored
@@ -21,12 +21,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -57,12 +57,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
18
.github/workflows/ci.yaml
vendored
@@ -24,9 +24,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||
- name: Setup lint cache
|
||||
uses: actions/cache@v4.1.1
|
||||
uses: actions/cache@v4.1.2
|
||||
with:
|
||||
path: |
|
||||
node_modules/.cache/prettier
|
||||
@@ -58,9 +58,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -76,9 +76,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -100,9 +100,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
2
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
8
.github/workflows/demo_deployment.yaml
vendored
@@ -22,12 +22,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -58,12 +58,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
4
.github/workflows/design_deployment.yaml
vendored
@@ -16,10 +16,10 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
4
.github/workflows/design_preview.yaml
vendored
@@ -21,10 +21,10 @@ jobs:
|
||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
4
.github/workflows/nightly.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v5
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
4
.github/workflows/release.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
2
.github/workflows/translations.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
@@ -6,4 +6,4 @@ enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.5.0.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.5.1.cjs
|
||||
|
@@ -24,8 +24,11 @@ const convertToJSON = async (
|
||||
) => {
|
||||
let localeData;
|
||||
try {
|
||||
// use "pt" for "pt-BR", because "pt-BR" is unsupported by @formatjs
|
||||
const language = lang === "pt-BR" ? "pt" : lang;
|
||||
|
||||
localeData = await readFile(
|
||||
join(formatjsDir, pkg, subDir, `${lang}.js`),
|
||||
join(formatjsDir, pkg, subDir, `${language}.js`),
|
||||
"utf-8"
|
||||
);
|
||||
} catch (e) {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { ActionDetail } from "@material/mwc-list/mwc-list";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list";
|
||||
import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js";
|
||||
import { Auth, Connection } from "home-assistant-js-websocket";
|
||||
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
||||
@@ -89,8 +90,8 @@ class HcCast extends LitElement {
|
||||
generateDefaultViewConfig({}, {}, {}, {}, () => ""),
|
||||
]
|
||||
).map(
|
||||
(view, idx) =>
|
||||
html`<ha-list-item
|
||||
(view, idx) => html`
|
||||
<ha-list-item
|
||||
graphic="avatar"
|
||||
.activated=${this.castManager.status?.lovelacePath ===
|
||||
(view.path ?? idx)}
|
||||
@@ -108,8 +109,9 @@ class HcCast extends LitElement {
|
||||
: html`<ha-svg-icon
|
||||
slot="item-icon"
|
||||
.path=${mdiViewDashboard}
|
||||
></ha-svg-icon>`}</ha-list-item
|
||||
> `
|
||||
></ha-svg-icon>`}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}</mwc-list
|
||||
>
|
||||
`}
|
||||
|
@@ -88,7 +88,7 @@ class HcLayout extends LitElement {
|
||||
}
|
||||
|
||||
.card-header {
|
||||
color: var(--ha-card-header-color, --primary-text-color);
|
||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||
font-family: var(--ha-card-header-font-family, inherit);
|
||||
font-size: var(--ha-card-header-font-size, 24px);
|
||||
letter-spacing: -0.012em;
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
|
||||
import { getPanelTitleFromUrlPath } from "../../../../src/data/panel";
|
||||
import { Lovelace } from "../../../../src/panels/lovelace/types";
|
||||
import "../../../../src/panels/lovelace/views/hui-view";
|
||||
import "../../../../src/panels/lovelace/views/hui-view-container";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import "./hc-launch-screen";
|
||||
|
||||
@@ -22,8 +23,6 @@ class HcLovelace extends LitElement {
|
||||
|
||||
@property() public urlPath: string | null = null;
|
||||
|
||||
@query("hui-view") private _huiView?: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const index = this._viewIndex;
|
||||
if (index === undefined) {
|
||||
@@ -45,13 +44,24 @@ class HcLovelace extends LitElement {
|
||||
saveConfig: async () => undefined,
|
||||
deleteConfig: async () => undefined,
|
||||
setEditMode: () => undefined,
|
||||
showToast: () => undefined,
|
||||
};
|
||||
|
||||
const viewConfig = this.lovelaceConfig.views[index];
|
||||
const background = viewConfig.background || this.lovelaceConfig.background;
|
||||
|
||||
return html`
|
||||
<hui-view
|
||||
<hui-view-container
|
||||
.hass=${this.hass}
|
||||
.lovelace=${lovelace}
|
||||
.index=${index}
|
||||
></hui-view>
|
||||
.background=${background}
|
||||
.theme=${viewConfig.theme}
|
||||
>
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.lovelace=${lovelace}
|
||||
.index=${index}
|
||||
></hui-view>
|
||||
</hui-view-container>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -81,26 +91,6 @@ class HcLovelace extends LitElement {
|
||||
}${viewTitle || ""}`
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const configBackground =
|
||||
this.lovelaceConfig.views[index].background ||
|
||||
this.lovelaceConfig.background;
|
||||
|
||||
const backgroundStyle =
|
||||
typeof configBackground === "string"
|
||||
? configBackground
|
||||
: configBackground?.image
|
||||
? `center / cover no-repeat url('${configBackground.image}')`
|
||||
: undefined;
|
||||
|
||||
if (backgroundStyle) {
|
||||
this._huiView!.style.setProperty(
|
||||
"--lovelace-background",
|
||||
backgroundStyle
|
||||
);
|
||||
} else {
|
||||
this._huiView!.style.removeProperty("--lovelace-background");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,19 +114,15 @@ class HcLovelace extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
min-height: 100vh;
|
||||
height: 0;
|
||||
hui-view-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
background: var(--primary-background-color);
|
||||
}
|
||||
:host > * {
|
||||
flex: 1;
|
||||
}
|
||||
hui-view {
|
||||
background: var(--lovelace-background, var(--primary-background-color));
|
||||
flex: 1 1 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -142,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
||||
<div class="action">
|
||||
<span>
|
||||
${this._action
|
||||
? describeAction(this.hass, [], [], this._action)
|
||||
? describeAction(this.hass, [], [], {}, this._action)
|
||||
: "<invalid YAML>"}
|
||||
</span>
|
||||
<ha-yaml-editor
|
||||
@@ -155,7 +155,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
||||
${ACTIONS.map(
|
||||
(conf) => html`
|
||||
<div class="action">
|
||||
<span>${describeAction(this.hass, [], [], conf as any)}</span>
|
||||
<span>${describeAction(this.hass, [], [], {}, conf as any)}</span>
|
||||
<pre>${dump(conf)}</pre>
|
||||
</div>
|
||||
`
|
||||
|
@@ -417,7 +417,7 @@ class HassioAddonConfig extends LitElement {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.header h2 {
|
||||
color: var(--ha-card-header-color, --primary-text-color);
|
||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||
font-family: var(--ha-card-header-font-family, inherit);
|
||||
font-size: var(--ha-card-header-font-size, 24px);
|
||||
letter-spacing: -0.012em;
|
||||
|
@@ -37,7 +37,6 @@ import "./config/hassio-addon-config";
|
||||
import "./config/hassio-addon-network";
|
||||
import "./hassio-addon-router";
|
||||
import "./info/hassio-addon-info";
|
||||
import "./log/hassio-addon-logs";
|
||||
|
||||
@customElement("hassio-addon-dashboard")
|
||||
class HassioAddonDashboard extends LitElement {
|
||||
@@ -161,16 +160,11 @@ class HassioAddonDashboard extends LitElement {
|
||||
margin-bottom: 24px;
|
||||
width: 600px;
|
||||
}
|
||||
hassio-addon-logs {
|
||||
max-width: calc(100% - 8px);
|
||||
min-width: 600px;
|
||||
}
|
||||
@media only screen and (max-width: 600px) {
|
||||
hassio-addon-info,
|
||||
hassio-addon-network,
|
||||
hassio-addon-audio,
|
||||
hassio-addon-config,
|
||||
hassio-addon-logs {
|
||||
hassio-addon-config {
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
@@ -2,7 +2,8 @@ import "@material/mwc-button";
|
||||
import {
|
||||
mdiCheckCircle,
|
||||
mdiChip,
|
||||
mdiCircle,
|
||||
mdiPlayCircle,
|
||||
mdiCircleOffOutline,
|
||||
mdiCursorDefaultClickOutline,
|
||||
mdiDocker,
|
||||
mdiExclamationThick,
|
||||
@@ -37,6 +38,7 @@ import "../../../../src/components/ha-markdown";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import "../../../../src/components/ha-switch";
|
||||
import type { HaSwitch } from "../../../../src/components/ha-switch";
|
||||
import {
|
||||
AddonCapability,
|
||||
HassioAddonDetails,
|
||||
@@ -198,7 +200,7 @@ class HassioAddonInfo extends LitElement {
|
||||
"dashboard.addon_running"
|
||||
)}
|
||||
class="running"
|
||||
.path=${mdiCircle}
|
||||
.path=${mdiPlayCircle}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: html`
|
||||
@@ -207,7 +209,7 @@ class HassioAddonInfo extends LitElement {
|
||||
"dashboard.addon_stopped"
|
||||
)}
|
||||
class="stopped"
|
||||
.path=${mdiCircle}
|
||||
.path=${mdiCircleOffOutline}
|
||||
></ha-svg-icon>
|
||||
`}
|
||||
`
|
||||
@@ -1118,12 +1120,28 @@ class HassioAddonInfo extends LitElement {
|
||||
private async _uninstallClicked(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
let removeData = false;
|
||||
const _removeDataToggled = (e: Event) => {
|
||||
removeData = (e.target as HaSwitch).checked;
|
||||
};
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize("dialog.uninstall_addon.title", {
|
||||
name: this.addon.name,
|
||||
}),
|
||||
text: this.supervisor.localize("dialog.uninstall_addon.text"),
|
||||
text: html`
|
||||
<ha-formfield
|
||||
.label=${html`<p>
|
||||
${this.supervisor.localize("dialog.uninstall_addon.remove_data")}
|
||||
</p>`}
|
||||
>
|
||||
<ha-switch
|
||||
@change=${_removeDataToggled}
|
||||
.checked=${removeData}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
`,
|
||||
confirmText: this.supervisor.localize("dialog.uninstall_addon.uninstall"),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
destructive: true,
|
||||
@@ -1136,7 +1154,7 @@ class HassioAddonInfo extends LitElement {
|
||||
|
||||
this._error = undefined;
|
||||
try {
|
||||
await uninstallHassioAddon(this.hass, this.addon.slug);
|
||||
await uninstallHassioAddon(this.hass, this.addon.slug, removeData);
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
@@ -1191,7 +1209,7 @@ class HassioAddonInfo extends LitElement {
|
||||
padding-inline-start: 8px;
|
||||
padding-inline-end: initial;
|
||||
font-size: 24px;
|
||||
color: var(--ha-card-header-color, --primary-text-color);
|
||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||
}
|
||||
.addon-version {
|
||||
float: var(--float-end);
|
||||
|
@@ -1,12 +1,14 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
|
||||
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
import "./hassio-addon-logs";
|
||||
import "../../../../src/panels/config/logs/error-log-card";
|
||||
import "../../../../src/components/search-input";
|
||||
import { extractSearchParam } from "../../../../src/common/url/search-params";
|
||||
|
||||
@customElement("hassio-addon-log-tab")
|
||||
class HassioAddonLogDashboard extends LitElement {
|
||||
@@ -16,6 +18,8 @@ class HassioAddonLogDashboard extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public addon?: HassioAddonDetails;
|
||||
|
||||
@state() private _filter = extractSearchParam("filter") || "";
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.addon) {
|
||||
return html`
|
||||
@@ -23,16 +27,31 @@ class HassioAddonLogDashboard extends LitElement {
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<div class="content">
|
||||
<hassio-addon-logs
|
||||
<div class="search">
|
||||
<search-input
|
||||
@value-changed=${this._filterChanged}
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
.addon=${this.addon}
|
||||
></hassio-addon-logs>
|
||||
.filter=${this._filter}
|
||||
.label=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
></search-input>
|
||||
</div>
|
||||
<div class="content">
|
||||
<error-log-card
|
||||
.hass=${this.hass}
|
||||
.header=${this.addon.name}
|
||||
.provider=${this.addon.slug}
|
||||
show
|
||||
.filter=${this._filter}
|
||||
>
|
||||
</error-log-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _filterChanged(ev) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -41,7 +60,21 @@ class HassioAddonLogDashboard extends LitElement {
|
||||
.content {
|
||||
margin: auto;
|
||||
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);
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -1,90 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@@ -48,6 +48,7 @@ import { showHassioBackupDialog } from "../dialogs/backup/show-dialog-hassio-bac
|
||||
import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hassio-create-backup";
|
||||
import { supervisorTabs } from "../hassio-tabs";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
|
||||
type BackupItem = HassioBackup & {
|
||||
secondary: string;
|
||||
@@ -69,6 +70,8 @@ export class HassioBackups extends LitElement {
|
||||
|
||||
@state() private _backups?: HassioBackup[] = [];
|
||||
|
||||
@state() private _isLoading = false;
|
||||
|
||||
@query("hass-tabs-subpage-data-table", true)
|
||||
private _dataTable!: HaTabsSubpageDataTable;
|
||||
|
||||
@@ -77,15 +80,10 @@ export class HassioBackups extends LitElement {
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.hass && this._firstUpdatedCalled) {
|
||||
this.refreshData();
|
||||
this.fetchBackups();
|
||||
}
|
||||
}
|
||||
|
||||
public async refreshData() {
|
||||
await reloadHassioBackups(this.hass);
|
||||
await this.fetchBackups();
|
||||
}
|
||||
|
||||
private _computeBackupContent = (backup: HassioBackup): string => {
|
||||
if (backup.type === "full") {
|
||||
return this.supervisor.localize("backup.full_backup");
|
||||
@@ -115,7 +113,7 @@ export class HassioBackups extends LitElement {
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (this.hass && this.isConnected) {
|
||||
this.refreshData();
|
||||
this.fetchBackups();
|
||||
}
|
||||
this._firstUpdatedCalled = true;
|
||||
}
|
||||
@@ -175,6 +173,13 @@ export class HassioBackups extends LitElement {
|
||||
if (!this.supervisor) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (this._isLoading) {
|
||||
return html`<hass-loading-screen
|
||||
.message=${this.supervisor.localize("backup.loading_backups")}
|
||||
></hass-loading-screen>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.tabs=${atLeastVersion(this.hass.config.version, 2022, 5)
|
||||
@@ -281,7 +286,7 @@ export class HassioBackups extends LitElement {
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this.refreshData();
|
||||
this.fetchBackups();
|
||||
break;
|
||||
case 1:
|
||||
showHassioBackupLocationDialog(this, { supervisor: this.supervisor });
|
||||
@@ -306,13 +311,15 @@ export class HassioBackups extends LitElement {
|
||||
supervisor: this.supervisor,
|
||||
onDelete: () => this.fetchBackups(),
|
||||
}),
|
||||
reloadBackup: () => this.refreshData(),
|
||||
reloadBackup: () => this.fetchBackups(),
|
||||
});
|
||||
}
|
||||
|
||||
private async fetchBackups() {
|
||||
this._isLoading = true;
|
||||
await reloadHassioBackups(this.hass);
|
||||
this._backups = await fetchHassioBackups(this.hass);
|
||||
this._isLoading = false;
|
||||
}
|
||||
|
||||
private async _deleteSelected() {
|
||||
@@ -339,8 +346,7 @@ export class HassioBackups extends LitElement {
|
||||
});
|
||||
return;
|
||||
}
|
||||
await reloadHassioBackups(this.hass);
|
||||
this._backups = await fetchHassioBackups(this.hass);
|
||||
await this.fetchBackups();
|
||||
this._dataTable.clearSelection();
|
||||
}
|
||||
|
||||
|
@@ -120,10 +120,12 @@ class HassioSupervisorLog extends LitElement {
|
||||
this._error = undefined;
|
||||
|
||||
try {
|
||||
this._content = await fetchHassioLogs(
|
||||
const response = await fetchHassioLogs(
|
||||
this.hass,
|
||||
this._selectedLogProvider
|
||||
);
|
||||
|
||||
this._content = await response.text();
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("system.log.get_logs", {
|
||||
provider: this._selectedLogProvider,
|
||||
|
@@ -1,11 +1,10 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -16,12 +15,12 @@ import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-checkbox";
|
||||
import "../../../src/components/ha-faded";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "../../../src/components/ha-icon-button";
|
||||
import "../../../src/components/ha-markdown";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import "../../../src/components/ha-switch";
|
||||
import type { HaSwitch } from "../../../src/components/ha-switch";
|
||||
import {
|
||||
fetchHassioAddonChangelog,
|
||||
fetchHassioAddonInfo,
|
||||
@@ -42,6 +41,7 @@ import { updateCore } from "../../../src/data/supervisor/core";
|
||||
import { StoreAddon } from "../../../src/data/supervisor/store";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
import { addonArchIsSupported, extractChangelog } from "../util/addon";
|
||||
|
||||
@@ -149,7 +149,7 @@ class UpdateAvailableCard extends LitElement {
|
||||
</ha-markdown>
|
||||
</ha-faded>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
<div class="versions">
|
||||
<p>
|
||||
${this.supervisor.localize(
|
||||
@@ -164,15 +164,17 @@ class UpdateAvailableCard extends LitElement {
|
||||
</div>
|
||||
${["core", "addon"].includes(this._updateType)
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${this.supervisor.localize(
|
||||
"update_available.create_backup"
|
||||
)}
|
||||
>
|
||||
<ha-checkbox checked></ha-checkbox>
|
||||
</ha-formfield>
|
||||
<hr />
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize(
|
||||
"update_available.create_backup"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch id="create_backup" checked></ha-switch>
|
||||
</ha-settings-row>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
`
|
||||
: html`<ha-circular-progress
|
||||
aria-label="Updating"
|
||||
@@ -191,22 +193,24 @@ class UpdateAvailableCard extends LitElement {
|
||||
? html`
|
||||
<div class="card-actions">
|
||||
${changelog
|
||||
? html`<a .href=${changelog} target="_blank" rel="noreferrer">
|
||||
<mwc-button
|
||||
.label=${this.supervisor.localize(
|
||||
"update_available.open_release_notes"
|
||||
)}
|
||||
>
|
||||
</mwc-button>
|
||||
</a>`
|
||||
: ""}
|
||||
? html`
|
||||
<a href=${changelog} target="_blank" rel="noreferrer">
|
||||
<ha-button
|
||||
.label=${this.supervisor.localize(
|
||||
"update_available.open_release_notes"
|
||||
)}
|
||||
>
|
||||
</ha-button>
|
||||
</a>
|
||||
`
|
||||
: nothing}
|
||||
<span></span>
|
||||
<ha-progress-button @click=${this._update} raised>
|
||||
<ha-progress-button @click=${this._update}>
|
||||
${this.supervisor.localize("common.update")}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
@@ -242,9 +246,11 @@ class UpdateAvailableCard extends LitElement {
|
||||
if (this._updateType && !["core", "addon"].includes(this._updateType)) {
|
||||
return false;
|
||||
}
|
||||
const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
|
||||
if (checkbox) {
|
||||
return checkbox.checked;
|
||||
const createBackupSwitch = this.shadowRoot?.getElementById(
|
||||
"create-backup"
|
||||
) as HaSwitch;
|
||||
if (createBackupSwitch) {
|
||||
return createBackupSwitch.checked;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -397,41 +403,50 @@ class UpdateAvailableCard extends LitElement {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
margin: auto;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-top: none;
|
||||
padding: 0 8px 8px;
|
||||
}
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
margin: auto;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
ha-circular-progress {
|
||||
display: block;
|
||||
margin: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
ha-circular-progress {
|
||||
display: block;
|
||||
margin: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
text-align: center;
|
||||
}
|
||||
.progress-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
ha-markdown {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
`;
|
||||
ha-markdown {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
margin-bottom: -16px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: var(--divider-color);
|
||||
border-bottom: none;
|
||||
margin: 16px 0 0 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
62
package.json
@@ -25,24 +25,24 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.25.7",
|
||||
"@babel/runtime": "7.26.0",
|
||||
"@braintree/sanitize-url": "7.1.0",
|
||||
"@codemirror/autocomplete": "6.18.1",
|
||||
"@codemirror/commands": "6.7.0",
|
||||
"@codemirror/commands": "6.7.1",
|
||||
"@codemirror/language": "6.10.3",
|
||||
"@codemirror/legacy-modes": "6.4.1",
|
||||
"@codemirror/search": "6.5.6",
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@codemirror/view": "6.34.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.13.0",
|
||||
"@formatjs/intl-displaynames": "6.6.9",
|
||||
"@formatjs/intl-getcanonicallocales": "2.3.0",
|
||||
"@formatjs/intl-listformat": "7.5.8",
|
||||
"@formatjs/intl-locale": "4.0.1",
|
||||
"@formatjs/intl-numberformat": "8.11.0",
|
||||
"@formatjs/intl-pluralrules": "5.2.15",
|
||||
"@formatjs/intl-relativetimeformat": "11.2.15",
|
||||
"@formatjs/intl-datetimeformat": "6.16.1",
|
||||
"@formatjs/intl-displaynames": "6.8.1",
|
||||
"@formatjs/intl-getcanonicallocales": "2.5.1",
|
||||
"@formatjs/intl-listformat": "7.7.1",
|
||||
"@formatjs/intl-locale": "4.2.1",
|
||||
"@formatjs/intl-numberformat": "8.14.1",
|
||||
"@formatjs/intl-pluralrules": "5.3.1",
|
||||
"@formatjs/intl-relativetimeformat": "11.4.1",
|
||||
"@fullcalendar/core": "6.1.15",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"@fullcalendar/interaction": "6.1.15",
|
||||
@@ -86,11 +86,11 @@
|
||||
"@polymer/paper-item": "3.0.1",
|
||||
"@polymer/paper-listbox": "3.0.1",
|
||||
"@polymer/paper-tabs": "3.1.0",
|
||||
"@polymer/polymer": "3.5.1",
|
||||
"@polymer/polymer": "3.5.2",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.4.11",
|
||||
"@vaadin/vaadin-themable-mixin": "24.4.11",
|
||||
"@vaadin/combo-box": "24.5.1",
|
||||
"@vaadin/vaadin-themable-mixin": "24.5.1",
|
||||
"@vibrant/color": "3.2.1-alpha.1",
|
||||
"@vibrant/core": "3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||
@@ -98,7 +98,7 @@
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"app-datepicker": "5.1.1",
|
||||
"chart.js": "4.4.4",
|
||||
"chart.js": "4.4.5",
|
||||
"color-name": "2.0.0",
|
||||
"comlink": "4.4.1",
|
||||
"core-js": "3.38.1",
|
||||
@@ -114,13 +114,13 @@
|
||||
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
||||
"home-assistant-js-websocket": "9.4.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
"intl-messageformat": "10.6.0",
|
||||
"intl-messageformat": "10.7.3",
|
||||
"js-yaml": "4.1.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
"lit": "2.8.0",
|
||||
"luxon": "3.5.0",
|
||||
"marked": "14.1.2",
|
||||
"marked": "14.1.3",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "3.2.1-alpha.1",
|
||||
"proxy-polyfill": "0.3.2",
|
||||
@@ -151,15 +151,15 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.25.8",
|
||||
"@babel/core": "7.26.0",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.2",
|
||||
"@babel/plugin-proposal-decorators": "7.25.7",
|
||||
"@babel/plugin-transform-runtime": "7.25.7",
|
||||
"@babel/preset-env": "7.25.8",
|
||||
"@babel/preset-typescript": "7.25.7",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.15.1",
|
||||
"@babel/plugin-proposal-decorators": "7.25.9",
|
||||
"@babel/plugin-transform-runtime": "7.25.9",
|
||||
"@babel/preset-env": "7.26.0",
|
||||
"@babel/preset-typescript": "7.26.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.16.0",
|
||||
"@koa/cors": "5.0.0",
|
||||
"@lokalise/node-api": "12.7.0",
|
||||
"@lokalise/node-api": "12.8.0",
|
||||
"@octokit/auth-oauth-device": "7.1.1",
|
||||
"@octokit/plugin-retry": "7.1.2",
|
||||
"@octokit/rest": "21.0.2",
|
||||
@@ -176,11 +176,11 @@
|
||||
"@types/glob": "8.1.0",
|
||||
"@types/html-minifier-terser": "7.0.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/leaflet": "1.9.12",
|
||||
"@types/leaflet": "1.9.14",
|
||||
"@types/leaflet-draw": "1.0.11",
|
||||
"@types/lodash.merge": "4.6.9",
|
||||
"@types/luxon": "3.4.2",
|
||||
"@types/mocha": "10.0.7",
|
||||
"@types/mocha": "10.0.9",
|
||||
"@types/qrcode": "1.5.5",
|
||||
"@types/serve-handler": "6.1.4",
|
||||
"@types/sortablejs": "1.15.8",
|
||||
@@ -194,7 +194,7 @@
|
||||
"babel-loader": "9.2.1",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"chai": "5.1.1",
|
||||
"chai": "5.1.2",
|
||||
"del": "8.0.0",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
@@ -216,7 +216,7 @@
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.6",
|
||||
"instant-mocha": "1.5.2",
|
||||
"instant-mocha": "1.5.3",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "15.2.10",
|
||||
"lit-analyzer": "2.0.3",
|
||||
@@ -224,7 +224,7 @@
|
||||
"lodash.template": "4.5.0",
|
||||
"magic-string": "0.30.12",
|
||||
"map-stream": "0.0.7",
|
||||
"mocha": "10.5.0",
|
||||
"mocha": "10.7.3",
|
||||
"object-hash": "3.0.0",
|
||||
"open": "10.1.0",
|
||||
"pinst": "3.0.0",
|
||||
@@ -233,7 +233,7 @@
|
||||
"rollup-plugin-string": "3.0.0",
|
||||
"rollup-plugin-terser": "7.0.2",
|
||||
"rollup-plugin-visualizer": "5.12.0",
|
||||
"serve-handler": "6.1.5",
|
||||
"serve-handler": "6.1.6",
|
||||
"sinon": "19.0.2",
|
||||
"systemjs": "6.15.1",
|
||||
"tar": "7.4.3",
|
||||
@@ -251,12 +251,12 @@
|
||||
},
|
||||
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
|
||||
"resolutions": {
|
||||
"@polymer/polymer": "patch:@polymer/polymer@3.5.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
|
||||
"@polymer/polymer": "patch:@polymer/polymer@3.5.2#./.yarn/patches/@polymer/polymer/pr-5569.patch",
|
||||
"@material/mwc-button@^0.25.3": "^0.27.0",
|
||||
"lit": "2.8.0",
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "1.6.3",
|
||||
"@fullcalendar/daygrid": "6.1.15"
|
||||
},
|
||||
"packageManager": "yarn@4.5.0"
|
||||
"packageManager": "yarn@4.5.1"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 4.4 KiB |
BIN
public/static/images/logo_nabu_casa_dark.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
public/static/images/voice-assistant/area.gif
Normal file
After Width: | Height: | Size: 372 KiB |
BIN
public/static/images/voice-assistant/change-wake-word.gif
Normal file
After Width: | Height: | Size: 383 KiB |
BIN
public/static/images/voice-assistant/error.gif
Normal file
After Width: | Height: | Size: 377 KiB |
BIN
public/static/images/voice-assistant/heart.gif
Normal file
After Width: | Height: | Size: 389 KiB |
BIN
public/static/images/voice-assistant/hi.gif
Normal file
After Width: | Height: | Size: 379 KiB |
BIN
public/static/images/voice-assistant/ok-nabu.gif
Normal file
After Width: | Height: | Size: 381 KiB |
BIN
public/static/images/voice-assistant/sleep.gif
Normal file
After Width: | Height: | Size: 374 KiB |
BIN
public/static/images/voice-assistant/update.gif
Normal file
After Width: | Height: | Size: 379 KiB |
@@ -34,9 +34,11 @@ export const protocolIntegrationPicked = async (
|
||||
if (domain === "zwave_js") {
|
||||
const entries = options?.config_entry
|
||||
? undefined
|
||||
: await getConfigEntries(hass, {
|
||||
domain,
|
||||
});
|
||||
: (
|
||||
await getConfigEntries(hass, {
|
||||
domain,
|
||||
})
|
||||
).filter((e) => !e.disabled_by);
|
||||
|
||||
if (
|
||||
!isComponentLoaded(hass, "zwave_js") ||
|
||||
@@ -81,9 +83,11 @@ export const protocolIntegrationPicked = async (
|
||||
} else if (domain === "zha") {
|
||||
const entries = options?.config_entry
|
||||
? undefined
|
||||
: await getConfigEntries(hass, {
|
||||
domain,
|
||||
});
|
||||
: (
|
||||
await getConfigEntries(hass, {
|
||||
domain,
|
||||
})
|
||||
).filter((e) => !e.disabled_by);
|
||||
|
||||
if (
|
||||
!isComponentLoaded(hass, "zha") ||
|
||||
@@ -129,9 +133,11 @@ export const protocolIntegrationPicked = async (
|
||||
} else if (domain === "matter") {
|
||||
const entries = options?.config_entry
|
||||
? undefined
|
||||
: await getConfigEntries(hass, {
|
||||
domain,
|
||||
});
|
||||
: (
|
||||
await getConfigEntries(hass, {
|
||||
domain,
|
||||
})
|
||||
).filter((e) => !e.disabled_by);
|
||||
if (
|
||||
!isComponentLoaded(hass, domain) ||
|
||||
(!options?.config_entry && !entries?.length)
|
||||
|
@@ -15,7 +15,6 @@ export type LocalizeKeys =
|
||||
| `ui.card.weather.cardinal_direction.${string}`
|
||||
| `ui.card.lawn_mower.actions.${string}`
|
||||
| `ui.components.calendar.event.rrule.${string}`
|
||||
| `ui.components.logbook.${string}`
|
||||
| `ui.components.selectors.file.${string}`
|
||||
| `ui.dialogs.entity_registry.editor.${string}`
|
||||
| `ui.dialogs.more_info_control.lawn_mower.${string}`
|
||||
|
@@ -108,6 +108,7 @@ class HaDataTableLabels extends LitElement {
|
||||
ha-label {
|
||||
--ha-label-background-color: var(--color, var(--grey-color));
|
||||
--ha-label-background-opacity: 0.5;
|
||||
outline: 1px solid var(--outline-color);
|
||||
}
|
||||
ha-button-menu {
|
||||
border-radius: 10px;
|
||||
|
@@ -1200,6 +1200,7 @@ export class HaDataTable extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
|
||||
.group-header ha-icon-button {
|
||||
|
@@ -254,7 +254,7 @@ class DateRangePickerElement extends WrappedElement {
|
||||
.daterangepicker select.hourselect,
|
||||
.daterangepicker select.minuteselect,
|
||||
.daterangepicker select.secondselect {
|
||||
background: transparent;
|
||||
background: var(--card-background-color);
|
||||
border: 1px solid var(--divider-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
@@ -1,5 +1,17 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
query,
|
||||
state as litState,
|
||||
} from "lit/decorators";
|
||||
|
||||
interface State {
|
||||
bold: boolean;
|
||||
@@ -11,11 +23,24 @@ interface State {
|
||||
}
|
||||
|
||||
@customElement("ha-ansi-to-html")
|
||||
class HaAnsiToHtml extends LitElement {
|
||||
export class HaAnsiToHtml extends LitElement {
|
||||
@property() public content!: string;
|
||||
|
||||
@query("pre") private _pre?: HTMLPreElement;
|
||||
|
||||
@litState() private _filter = "";
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html`${this._parseTextToColoredPre(this.content)}`;
|
||||
return html`<pre></pre>`;
|
||||
}
|
||||
|
||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(_changedProperties);
|
||||
|
||||
// handle initial content
|
||||
if (this.content) {
|
||||
this.parseTextToColoredPre(this.content);
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -24,6 +49,7 @@ class HaAnsiToHtml extends LitElement {
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
margin: 0;
|
||||
}
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
@@ -85,11 +111,33 @@ class HaAnsiToHtml extends LitElement {
|
||||
.bg-white {
|
||||
background-color: rgb(204, 204, 204);
|
||||
}
|
||||
|
||||
::highlight(search-results) {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _parseTextToColoredPre(text) {
|
||||
const pre = document.createElement("pre");
|
||||
/**
|
||||
* add new lines to the log
|
||||
* @param lines log lines
|
||||
* @param top should the new lines be added to the top of the log
|
||||
*/
|
||||
public parseLinesToColoredPre(lines: string[], top = false) {
|
||||
for (const line of lines) {
|
||||
this.parseLineToColoredPre(line, top);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a single line to the log
|
||||
* @param line log line
|
||||
* @param top should the new line be added to the top of the log
|
||||
*/
|
||||
public parseLineToColoredPre(line, top = false) {
|
||||
const lineDiv = document.createElement("div");
|
||||
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const re = /\x1b(?:\[(.*?)[@-~]|\].*?(?:\x07|\x1b\\))/g;
|
||||
let i = 0;
|
||||
@@ -103,7 +151,7 @@ class HaAnsiToHtml extends LitElement {
|
||||
backgroundColor: null,
|
||||
};
|
||||
|
||||
const addSpan = (content) => {
|
||||
const addPart = (content) => {
|
||||
const span = document.createElement("span");
|
||||
if (state.bold) {
|
||||
span.classList.add("bold");
|
||||
@@ -124,15 +172,18 @@ class HaAnsiToHtml extends LitElement {
|
||||
span.classList.add(`bg-${state.backgroundColor}`);
|
||||
}
|
||||
span.appendChild(document.createTextNode(content));
|
||||
pre.appendChild(span);
|
||||
lineDiv.appendChild(span);
|
||||
};
|
||||
|
||||
/* eslint-disable no-cond-assign */
|
||||
let match;
|
||||
// eslint-disable-next-line
|
||||
while ((match = re.exec(text)) !== null) {
|
||||
while ((match = re.exec(line)) !== null) {
|
||||
const j = match!.index;
|
||||
addSpan(text.substring(i, j));
|
||||
const substring = line.substring(i, j);
|
||||
if (substring) {
|
||||
addPart(substring);
|
||||
}
|
||||
i = j + match[0].length;
|
||||
|
||||
if (match[1] === undefined) {
|
||||
@@ -234,9 +285,93 @@ class HaAnsiToHtml extends LitElement {
|
||||
}
|
||||
});
|
||||
}
|
||||
addSpan(text.substring(i));
|
||||
|
||||
return pre;
|
||||
const substring = line.substring(i);
|
||||
if (substring) {
|
||||
addPart(substring);
|
||||
}
|
||||
|
||||
if (top) {
|
||||
this._pre?.prepend(lineDiv);
|
||||
lineDiv.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 500 });
|
||||
} else {
|
||||
this._pre?.appendChild(lineDiv);
|
||||
}
|
||||
|
||||
// filter new lines if a filter is set
|
||||
if (this._filter) {
|
||||
this.filterLines(this._filter);
|
||||
}
|
||||
}
|
||||
|
||||
public parseTextToColoredPre(text) {
|
||||
const lines = text.split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
this.parseLineToColoredPre(line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter lines based on a search string, lines and search string will be converted to lowercase
|
||||
* @param filter the search string
|
||||
* @returns true if there are lines to display
|
||||
*/
|
||||
filterLines(filter: string): boolean {
|
||||
this._filter = filter;
|
||||
const lines = this.shadowRoot?.querySelectorAll("div") || [];
|
||||
let numberOfFoundLines = 0;
|
||||
if (!filter) {
|
||||
lines.forEach((line) => {
|
||||
line.style.display = "";
|
||||
});
|
||||
numberOfFoundLines = lines.length;
|
||||
if (CSS.highlights) {
|
||||
CSS.highlights.delete("search-results");
|
||||
}
|
||||
} else {
|
||||
const highlightRanges: Range[] = [];
|
||||
lines.forEach((line) => {
|
||||
if (!line.textContent?.toLowerCase().includes(filter.toLowerCase())) {
|
||||
line.style.display = "none";
|
||||
} else {
|
||||
line.style.display = "";
|
||||
numberOfFoundLines++;
|
||||
if (CSS.highlights && line.firstChild !== null && line.textContent) {
|
||||
const spansOfLine = line.querySelectorAll("span");
|
||||
spansOfLine.forEach((span) => {
|
||||
const text = span.textContent.toLowerCase();
|
||||
const indices: number[] = [];
|
||||
let startPos = 0;
|
||||
while (startPos < text.length) {
|
||||
const index = text.indexOf(filter.toLowerCase(), startPos);
|
||||
if (index === -1) break;
|
||||
indices.push(index);
|
||||
startPos = index + filter.length;
|
||||
}
|
||||
|
||||
indices.forEach((index) => {
|
||||
const range = new Range();
|
||||
range.setStart(span.firstChild!, index);
|
||||
range.setEnd(span.firstChild!, index + filter.length);
|
||||
highlightRanges.push(range);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
if (CSS.highlights) {
|
||||
CSS.highlights.set("search-results", new Highlight(...highlightRanges));
|
||||
}
|
||||
}
|
||||
|
||||
return !!numberOfFoundLines;
|
||||
}
|
||||
|
||||
public clear() {
|
||||
if (this._pre) {
|
||||
this._pre.innerHTML = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -43,7 +43,7 @@ export class HaCard extends LitElement {
|
||||
|
||||
.card-header,
|
||||
:host ::slotted(.card-header) {
|
||||
color: var(--ha-card-header-color, --primary-text-color);
|
||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||
font-family: var(--ha-card-header-font-family, inherit);
|
||||
font-size: var(--ha-card-header-font-size, 24px);
|
||||
letter-spacing: -0.012em;
|
||||
|
@@ -45,7 +45,7 @@ export class HaControlButton extends LitElement {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
@@ -46,7 +46,7 @@ export class HaHeaderBar extends LitElement {
|
||||
flex: none;
|
||||
}
|
||||
.mdc-top-app-bar__title {
|
||||
padding-inline-start: 20px;
|
||||
padding-inline-start: 24px;
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
`,
|
||||
|
@@ -216,6 +216,7 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
|
||||
ha-input-chip {
|
||||
--md-input-chip-selected-container-color: var(--color, var(--grey-color));
|
||||
--ha-input-chip-selected-container-opacity: 0.5;
|
||||
--md-input-chip-selected-outline-width: 1px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -86,6 +86,11 @@ export class HaMarkdown extends LitElement {
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
hr {
|
||||
border-color: var(--divider-color);
|
||||
border-bottom: none;
|
||||
margin: 16px 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -118,6 +118,7 @@ export class HaPasswordField extends LitElement {
|
||||
.type=${this._unmaskedPassword ? "text" : "password"}
|
||||
.suffix=${html`<div style="width: 24px"></div>`}
|
||||
@input=${this._handleInputChange}
|
||||
@change=${this._reDispatchEvent}
|
||||
></ha-textfield>
|
||||
<ha-icon-button
|
||||
toggles
|
||||
@@ -156,6 +157,12 @@ export class HaPasswordField extends LitElement {
|
||||
this.value = ev.target.value;
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _reDispatchEvent(oldEvent: Event) {
|
||||
const newEvent = new Event(oldEvent.type, oldEvent);
|
||||
this.dispatchEvent(newEvent);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { PropertyValues, ReactiveElement } from "lit";
|
||||
import { parseISO } from "date-fns";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { relativeTime } from "../common/datetime/relative_time";
|
||||
import { capitalizeFirstLetter } from "../common/string/capitalize-first-letter";
|
||||
@@ -58,7 +59,12 @@ class HaRelativeTime extends ReactiveElement {
|
||||
if (!this.datetime) {
|
||||
this.innerHTML = this.hass.localize("ui.components.relative_time.never");
|
||||
} else {
|
||||
const relTime = relativeTime(new Date(this.datetime), this.hass.locale);
|
||||
const date =
|
||||
typeof this.datetime === "string"
|
||||
? parseISO(this.datetime)
|
||||
: this.datetime;
|
||||
|
||||
const relTime = relativeTime(date, this.hass.locale);
|
||||
this.innerHTML = this.capitalize
|
||||
? capitalizeFirstLetter(relTime)
|
||||
: relTime;
|
||||
|
@@ -34,6 +34,7 @@ import {
|
||||
expandLabelTarget,
|
||||
Selector,
|
||||
TargetSelector,
|
||||
TemplateSelector,
|
||||
} from "../data/selector";
|
||||
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
@@ -45,6 +46,7 @@ import "./ha-settings-row";
|
||||
import "./ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "./ha-yaml-editor";
|
||||
import "./ha-service-section-icon";
|
||||
import { hasTemplate } from "../common/string/has-template";
|
||||
|
||||
const attributeFilter = (values: any[], attribute: any) => {
|
||||
if (typeof attribute === "object") {
|
||||
@@ -61,6 +63,11 @@ const showOptionalToggle = (field) =>
|
||||
!field.required &&
|
||||
!("boolean" in field.selector && field.default);
|
||||
|
||||
interface Field extends Omit<HassService["fields"][string], "selector"> {
|
||||
key: string;
|
||||
selector?: Selector;
|
||||
}
|
||||
|
||||
interface ExtHassService extends Omit<HassService, "fields"> {
|
||||
fields: Array<
|
||||
Omit<HassService["fields"][string], "selector"> & {
|
||||
@@ -70,9 +77,12 @@ interface ExtHassService extends Omit<HassService, "fields"> {
|
||||
collapsed?: boolean;
|
||||
}
|
||||
>;
|
||||
flatFields: Array<Field>;
|
||||
hasSelector: string[];
|
||||
}
|
||||
|
||||
const TEMPLATE_SELECTOR: TemplateSelector = { template: {} };
|
||||
|
||||
@customElement("ha-service-control")
|
||||
export class HaServiceControl extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -177,7 +187,7 @@ export class HaServiceControl extends LitElement {
|
||||
if (!this._value.data) {
|
||||
this._value.data = {};
|
||||
}
|
||||
serviceData.fields.forEach((field) => {
|
||||
serviceData.flatFields.forEach((field) => {
|
||||
if (
|
||||
field.selector &&
|
||||
field.required &&
|
||||
@@ -241,22 +251,28 @@ export class HaServiceControl extends LitElement {
|
||||
selector: value.selector as Selector | undefined,
|
||||
}));
|
||||
|
||||
const flatFields: Field[] = [];
|
||||
const hasSelector: string[] = [];
|
||||
fields.forEach((field) => {
|
||||
if ((field as any).fields) {
|
||||
Object.entries((field as any).fields).forEach(([key, subField]) => {
|
||||
flatFields.push({ ...(subField as Field), key });
|
||||
if ((subField as any).selector) {
|
||||
hasSelector.push(key);
|
||||
}
|
||||
});
|
||||
} else if (field.selector) {
|
||||
hasSelector.push(field.key);
|
||||
} else {
|
||||
flatFields.push(field);
|
||||
if (field.selector) {
|
||||
hasSelector.push(field.key);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...serviceDomains[domain][serviceName],
|
||||
fields,
|
||||
flatFields,
|
||||
hasSelector,
|
||||
};
|
||||
}
|
||||
@@ -397,7 +413,7 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
const hasOptional = Boolean(
|
||||
!shouldRenderServiceDataYaml &&
|
||||
serviceData?.fields.some((field) => showOptionalToggle(field))
|
||||
serviceData?.flatFields.some((field) => showOptionalToggle(field))
|
||||
);
|
||||
|
||||
const targetEntities = this._getTargetedEntities(
|
||||
@@ -466,7 +482,8 @@ export class HaServiceControl extends LitElement {
|
||||
>${this.hass.localize(
|
||||
"ui.components.service-control.target_secondary"
|
||||
)}</span
|
||||
><ha-selector
|
||||
>
|
||||
<ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${this._targetSelector(
|
||||
serviceData.target as TargetSelector
|
||||
@@ -627,23 +644,34 @@ export class HaServiceControl extends LitElement {
|
||||
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
|
||||
) || dataField?.description}</span
|
||||
>
|
||||
<ha-selector
|
||||
.disabled=${this.disabled ||
|
||||
(showOptional &&
|
||||
!this._checkedKeys.has(dataField.key) &&
|
||||
(!this._value?.data ||
|
||||
this._value.data[dataField.key] === undefined))}
|
||||
.hass=${this.hass}
|
||||
.selector=${enhancedSelector}
|
||||
.key=${dataField.key}
|
||||
@value-changed=${this._serviceDataChanged}
|
||||
.value=${this._value?.data
|
||||
? this._value.data[dataField.key]
|
||||
: undefined}
|
||||
.placeholder=${dataField.default}
|
||||
.localizeValue=${this._localizeValueCallback}
|
||||
@item-moved=${this._itemMoved}
|
||||
></ha-selector>
|
||||
${hasTemplate(this._value?.data?.[dataField.key])
|
||||
? html`
|
||||
<ha-selector
|
||||
.selector=${TEMPLATE_SELECTOR}
|
||||
.key=${dataField.key}
|
||||
.hass=${this.hass}
|
||||
.value=${this._value?.data?.[dataField.key]}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._serviceDataChanged}
|
||||
></ha-selector>
|
||||
`
|
||||
: html`
|
||||
<ha-selector
|
||||
.disabled=${this.disabled ||
|
||||
(showOptional &&
|
||||
!this._checkedKeys.has(dataField.key) &&
|
||||
(!this._value?.data ||
|
||||
this._value.data[dataField.key] === undefined))}
|
||||
.hass=${this.hass}
|
||||
.selector=${enhancedSelector}
|
||||
.key=${dataField.key}
|
||||
@value-changed=${this._serviceDataChanged}
|
||||
.value=${this._value?.data?.[dataField.key]}
|
||||
.placeholder=${dataField.default}
|
||||
.localizeValue=${this._localizeValueCallback}
|
||||
@item-moved=${this._itemMoved}
|
||||
></ha-selector>
|
||||
`}
|
||||
</ha-settings-row>`
|
||||
: "";
|
||||
};
|
||||
@@ -667,7 +695,7 @@ export class HaServiceControl extends LitElement {
|
||||
const field = this._getServiceInfo(
|
||||
this._value?.action,
|
||||
this.hass.services
|
||||
)?.fields.find((_field) => _field.key === key);
|
||||
)?.flatFields.find((_field) => _field.key === key);
|
||||
|
||||
let defaultValue = field?.default;
|
||||
|
||||
|
@@ -42,14 +42,17 @@ export class HaSettingsRow extends LitElement {
|
||||
padding-bottom: 8px;
|
||||
padding-left: 0;
|
||||
padding-inline-start: 0;
|
||||
padding-right: 16x;
|
||||
padding-right: 16px;
|
||||
padding-inline-end: 16px;
|
||||
overflow: hidden;
|
||||
display: var(--layout-vertical_-_display);
|
||||
flex-direction: var(--layout-vertical_-_flex-direction);
|
||||
justify-content: var(--layout-center-justified_-_justify-content);
|
||||
flex: var(--layout-flex_-_flex);
|
||||
flex-basis: var(--layout-flex_-_flex-basis);
|
||||
display: var(--layout-vertical_-_display, flex);
|
||||
flex-direction: var(--layout-vertical_-_flex-direction, column);
|
||||
justify-content: var(
|
||||
--layout-center-justified_-_justify-content,
|
||||
center
|
||||
);
|
||||
flex: var(--layout-flex_-_flex, 1);
|
||||
flex-basis: var(--layout-flex_-_flex-basis, 0.000000001px);
|
||||
}
|
||||
.body[three-line] {
|
||||
min-height: var(--paper-item-body-three-line-min-height, 88px);
|
||||
|
@@ -859,11 +859,14 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
border-bottom: 1px solid transparent;
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
color: var(--sidebar-menu-button-text-color, --primary-text-color);
|
||||
color: var(
|
||||
--sidebar-menu-button-text-color,
|
||||
var(--primary-text-color)
|
||||
);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
background-color: var(
|
||||
--sidebar-menu-button-background-color,
|
||||
--primary-background-color
|
||||
var(--primary-background-color)
|
||||
);
|
||||
font-size: 20px;
|
||||
align-items: center;
|
||||
|
@@ -1,8 +1,49 @@
|
||||
import { customElement } from "lit/decorators";
|
||||
import { Snackbar } from "@material/mwc-snackbar/mwc-snackbar";
|
||||
import { styles } from "@material/mwc-snackbar/mwc-snackbar.css";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-toast")
|
||||
export class HaToast extends Snackbar {}
|
||||
export class HaToast extends Snackbar {
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
.mdc-snackbar--leading {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mdc-snackbar {
|
||||
margin: 8px;
|
||||
right: calc(8px + env(safe-area-inset-right));
|
||||
bottom: calc(8px + env(safe-area-inset-bottom));
|
||||
left: calc(8px + env(safe-area-inset-left));
|
||||
}
|
||||
|
||||
.mdc-snackbar__surface {
|
||||
min-width: 350px;
|
||||
max-width: 650px;
|
||||
}
|
||||
|
||||
// Revert the default styles set by mwc-snackbar
|
||||
@media (max-width: 480px), (max-width: 344px) {
|
||||
.mdc-snackbar__surface {
|
||||
min-width: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.mdc-snackbar {
|
||||
right: env(safe-area-inset-right);
|
||||
bottom: env(safe-area-inset-bottom);
|
||||
left: env(safe-area-inset-left);
|
||||
}
|
||||
.mdc-snackbar__surface {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
|
@@ -24,7 +24,7 @@ export class HaTopAppBarFixed extends TopAppBarFixedBase {
|
||||
);
|
||||
}
|
||||
.mdc-top-app-bar__title {
|
||||
padding-inline-start: 20px;
|
||||
padding-inline-start: 24px;
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
`,
|
||||
|
@@ -321,7 +321,7 @@ export class TopAppBarBaseBase extends BaseElement {
|
||||
overflow: auto;
|
||||
}
|
||||
.mdc-top-app-bar__title {
|
||||
padding-inline-start: 20px;
|
||||
padding-inline-start: 24px;
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
`,
|
||||
|
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable no-console */
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -11,9 +12,12 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import {
|
||||
addWebRtcCandidate,
|
||||
fetchWebRtcClientConfiguration,
|
||||
handleWebRtcOffer,
|
||||
WebRtcAnswer,
|
||||
WebRTCClientConfiguration,
|
||||
webRtcOffer,
|
||||
WebRtcOfferEvent,
|
||||
} from "../data/camera";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
@@ -27,7 +31,7 @@ import "./ha-alert";
|
||||
class HaWebRtcPlayer extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public entityid!: string;
|
||||
@property() public entityid?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "controls" })
|
||||
public controls = false;
|
||||
@@ -45,12 +49,20 @@ class HaWebRtcPlayer extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@query("#remote-stream", true) private _videoEl!: HTMLVideoElement;
|
||||
@query("#remote-stream") private _videoEl!: HTMLVideoElement;
|
||||
|
||||
private _clientConfig?: WebRTCClientConfiguration;
|
||||
|
||||
private _peerConnection?: RTCPeerConnection;
|
||||
|
||||
private _remoteStream?: MediaStream;
|
||||
|
||||
private _unsub?: Promise<UnsubscribeFunc>;
|
||||
|
||||
private _sessionId?: string;
|
||||
|
||||
private _candidatesList: string[] = [];
|
||||
|
||||
protected override render(): TemplateResult {
|
||||
if (this._error) {
|
||||
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
|
||||
@@ -70,7 +82,7 @@ class HaWebRtcPlayer extends LitElement {
|
||||
|
||||
public override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
if (this.hasUpdated && this.entityid) {
|
||||
this._startWebRtc();
|
||||
}
|
||||
}
|
||||
@@ -80,7 +92,8 @@ class HaWebRtcPlayer extends LitElement {
|
||||
this._cleanUp();
|
||||
}
|
||||
|
||||
protected override updated(changedProperties: PropertyValues<this>) {
|
||||
protected override willUpdate(changedProperties: PropertyValues<this>) {
|
||||
super.willUpdate(changedProperties);
|
||||
if (!changedProperties.has("entityid")) {
|
||||
return;
|
||||
}
|
||||
@@ -88,28 +101,68 @@ class HaWebRtcPlayer extends LitElement {
|
||||
}
|
||||
|
||||
private async _startWebRtc(): Promise<void> {
|
||||
this._cleanUp();
|
||||
|
||||
if (!this.hass || !this.entityid) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.time("WebRTC");
|
||||
|
||||
this._error = undefined;
|
||||
|
||||
console.timeLog("WebRTC", "start clientConfig");
|
||||
|
||||
const clientConfig = await fetchWebRtcClientConfiguration(
|
||||
this._clientConfig = await fetchWebRtcClientConfiguration(
|
||||
this.hass,
|
||||
this.entityid
|
||||
);
|
||||
|
||||
console.timeLog("WebRTC", "end clientConfig", clientConfig);
|
||||
console.timeLog("WebRTC", "end clientConfig", this._clientConfig);
|
||||
|
||||
const peerConnection = new RTCPeerConnection(clientConfig.configuration);
|
||||
this._peerConnection = new RTCPeerConnection(
|
||||
this._clientConfig.configuration
|
||||
);
|
||||
|
||||
if (clientConfig.dataChannel) {
|
||||
if (this._clientConfig.dataChannel) {
|
||||
// Some cameras (such as nest) require a data channel to establish a stream
|
||||
// however, not used by any integrations.
|
||||
peerConnection.createDataChannel(clientConfig.dataChannel);
|
||||
this._peerConnection.createDataChannel(this._clientConfig.dataChannel);
|
||||
}
|
||||
|
||||
this._peerConnection.onnegotiationneeded = this._startNegotiation;
|
||||
|
||||
this._peerConnection.onicecandidate = this._handleIceCandidate;
|
||||
this._peerConnection.oniceconnectionstatechange =
|
||||
this._iceConnectionStateChanged;
|
||||
|
||||
// just for debugging
|
||||
this._peerConnection.onsignalingstatechange = (ev) => {
|
||||
switch ((ev.target as RTCPeerConnection).signalingState) {
|
||||
case "stable":
|
||||
console.timeLog("WebRTC", "ICE negotiation complete");
|
||||
break;
|
||||
default:
|
||||
console.timeLog(
|
||||
"WebRTC",
|
||||
"Signaling state changed",
|
||||
(ev.target as RTCPeerConnection).signalingState
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Setup callbacks to render remote stream once media tracks are discovered.
|
||||
this._remoteStream = new MediaStream();
|
||||
this._peerConnection.ontrack = this._addTrack;
|
||||
|
||||
this._peerConnection.addTransceiver("audio", { direction: "recvonly" });
|
||||
this._peerConnection.addTransceiver("video", { direction: "recvonly" });
|
||||
}
|
||||
|
||||
private _startNegotiation = async () => {
|
||||
if (!this._peerConnection) {
|
||||
return;
|
||||
}
|
||||
peerConnection.addTransceiver("audio", { direction: "recvonly" });
|
||||
peerConnection.addTransceiver("video", { direction: "recvonly" });
|
||||
|
||||
const offerOptions: RTCOfferOptions = {
|
||||
offerToReceiveAudio: true,
|
||||
@@ -119,98 +172,218 @@ class HaWebRtcPlayer extends LitElement {
|
||||
console.timeLog("WebRTC", "start createOffer", offerOptions);
|
||||
|
||||
const offer: RTCSessionDescriptionInit =
|
||||
await peerConnection.createOffer(offerOptions);
|
||||
await this._peerConnection.createOffer(offerOptions);
|
||||
|
||||
if (!this._peerConnection) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.timeLog("WebRTC", "end createOffer", offer);
|
||||
|
||||
console.timeLog("WebRTC", "start setLocalDescription");
|
||||
|
||||
await peerConnection.setLocalDescription(offer);
|
||||
await this._peerConnection.setLocalDescription(offer);
|
||||
|
||||
console.timeLog("WebRTC", "end setLocalDescription");
|
||||
|
||||
console.timeLog("WebRTC", "start iceResolver");
|
||||
|
||||
let candidates = ""; // Build an Offer SDP string with ice candidates
|
||||
const iceResolver = new Promise<void>((resolve) => {
|
||||
peerConnection.addEventListener("icecandidate", (event) => {
|
||||
if (!event.candidate?.candidate) {
|
||||
resolve(); // Gathering complete
|
||||
return;
|
||||
}
|
||||
console.timeLog("WebRTC", "iceResolver candidate", event.candidate);
|
||||
candidates += `a=${event.candidate.candidate}\r\n`;
|
||||
});
|
||||
});
|
||||
await iceResolver;
|
||||
|
||||
console.timeLog("WebRTC", "end iceResolver", candidates);
|
||||
|
||||
const offer_sdp = offer.sdp! + candidates;
|
||||
|
||||
let webRtcAnswer: WebRtcAnswer;
|
||||
try {
|
||||
console.timeLog("WebRTC", "start WebRTCOffer", offer_sdp);
|
||||
webRtcAnswer = await handleWebRtcOffer(
|
||||
this.hass,
|
||||
this.entityid,
|
||||
offer_sdp
|
||||
);
|
||||
console.timeLog("WebRTC", "end webRtcOffer", webRtcAnswer);
|
||||
} catch (err: any) {
|
||||
this._error = "Failed to start WebRTC stream: " + err.message;
|
||||
peerConnection.close();
|
||||
if (!this._peerConnection || !this.entityid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup callbacks to render remote stream once media tracks are discovered.
|
||||
const remoteStream = new MediaStream();
|
||||
peerConnection.addEventListener("track", (event) => {
|
||||
console.timeLog("WebRTC", "track", event);
|
||||
remoteStream.addTrack(event.track);
|
||||
this._videoEl.srcObject = remoteStream;
|
||||
});
|
||||
this._remoteStream = remoteStream;
|
||||
console.timeLog("WebRTC", "end setLocalDescription");
|
||||
|
||||
let candidates = "";
|
||||
|
||||
if (this._clientConfig?.getCandidatesUpfront) {
|
||||
await new Promise<void>((resolve) => {
|
||||
this._peerConnection!.onicegatheringstatechange = (ev: Event) => {
|
||||
const iceGatheringState = (ev.target as RTCPeerConnection)
|
||||
.iceGatheringState;
|
||||
if (iceGatheringState === "complete") {
|
||||
this._peerConnection!.onicegatheringstatechange = null;
|
||||
resolve();
|
||||
}
|
||||
|
||||
console.timeLog(
|
||||
"WebRTC",
|
||||
"Ice gathering state changed",
|
||||
iceGatheringState
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
if (!this._peerConnection || !this.entityid) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
while (this._candidatesList.length) {
|
||||
const candidate = this._candidatesList.pop();
|
||||
if (candidate) {
|
||||
candidates += `a=${candidate}\r\n`;
|
||||
}
|
||||
}
|
||||
|
||||
const offer_sdp = offer.sdp! + candidates;
|
||||
|
||||
console.timeLog("WebRTC", "start webRtcOffer", offer_sdp);
|
||||
|
||||
try {
|
||||
this._unsub = webRtcOffer(this.hass, this.entityid, offer_sdp, (event) =>
|
||||
this._handleOfferEvent(event)
|
||||
);
|
||||
} catch (err: any) {
|
||||
this._error = "Failed to start WebRTC stream: " + err.message;
|
||||
this._cleanUp();
|
||||
}
|
||||
};
|
||||
|
||||
private _iceConnectionStateChanged = () => {
|
||||
console.timeLog(
|
||||
"WebRTC",
|
||||
"ice connection state change",
|
||||
this._peerConnection?.iceConnectionState
|
||||
);
|
||||
if (this._peerConnection?.iceConnectionState === "failed") {
|
||||
this._peerConnection.restartIce();
|
||||
}
|
||||
};
|
||||
|
||||
private async _handleOfferEvent(event: WebRtcOfferEvent) {
|
||||
if (!this.entityid) {
|
||||
return;
|
||||
}
|
||||
if (event.type === "session") {
|
||||
this._sessionId = event.session_id;
|
||||
this._candidatesList.forEach((candidate) =>
|
||||
addWebRtcCandidate(
|
||||
this.hass,
|
||||
this.entityid!,
|
||||
event.session_id,
|
||||
candidate
|
||||
)
|
||||
);
|
||||
this._candidatesList = [];
|
||||
}
|
||||
if (event.type === "answer") {
|
||||
console.timeLog("WebRTC", "answer", event.answer);
|
||||
|
||||
this._handleAnswer(event);
|
||||
}
|
||||
if (event.type === "candidate") {
|
||||
console.timeLog("WebRTC", "remote ice candidate", event.candidate);
|
||||
|
||||
try {
|
||||
await this._peerConnection?.addIceCandidate(
|
||||
new RTCIceCandidate({ candidate: event.candidate, sdpMid: "0" })
|
||||
);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
if (event.type === "error") {
|
||||
this._error = "Failed to start WebRTC stream: " + event.message;
|
||||
this._cleanUp();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleIceCandidate = (event: RTCPeerConnectionIceEvent) => {
|
||||
if (!this.entityid || !event.candidate?.candidate) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.timeLog(
|
||||
"WebRTC",
|
||||
"local ice candidate",
|
||||
event.candidate?.candidate
|
||||
);
|
||||
|
||||
if (this._sessionId) {
|
||||
addWebRtcCandidate(
|
||||
this.hass,
|
||||
this.entityid,
|
||||
this._sessionId,
|
||||
event.candidate?.candidate
|
||||
);
|
||||
} else {
|
||||
this._candidatesList.push(event.candidate?.candidate);
|
||||
}
|
||||
};
|
||||
|
||||
private _addTrack = async (event: RTCTrackEvent) => {
|
||||
if (!this._remoteStream) {
|
||||
return;
|
||||
}
|
||||
this._remoteStream.addTrack(event.track);
|
||||
if (!this.hasUpdated) {
|
||||
await this.updateComplete;
|
||||
}
|
||||
this._videoEl.srcObject = this._remoteStream;
|
||||
};
|
||||
|
||||
private async _handleAnswer(event: WebRtcAnswer) {
|
||||
if (
|
||||
!this._peerConnection?.signalingState ||
|
||||
["stable", "closed"].includes(this._peerConnection.signalingState)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initiate the stream with the remote device
|
||||
const remoteDesc = new RTCSessionDescription({
|
||||
type: "answer",
|
||||
sdp: webRtcAnswer.answer,
|
||||
sdp: event.answer,
|
||||
});
|
||||
try {
|
||||
console.timeLog("WebRTC", "start setRemoteDescription", remoteDesc);
|
||||
await peerConnection.setRemoteDescription(remoteDesc);
|
||||
console.timeLog("WebRTC", "end setRemoteDescription");
|
||||
await this._peerConnection.setRemoteDescription(remoteDesc);
|
||||
} catch (err: any) {
|
||||
this._error = "Failed to connect WebRTC stream: " + err.message;
|
||||
peerConnection.close();
|
||||
return;
|
||||
this._cleanUp();
|
||||
}
|
||||
this._peerConnection = peerConnection;
|
||||
console.timeLog("WebRTC", "end setRemoteDescription");
|
||||
}
|
||||
|
||||
private _cleanUp() {
|
||||
console.timeLog("WebRTC", "stopped");
|
||||
console.timeEnd("WebRTC");
|
||||
|
||||
if (this._remoteStream) {
|
||||
this._remoteStream.getTracks().forEach((track) => {
|
||||
track.stop();
|
||||
});
|
||||
|
||||
this._remoteStream = undefined;
|
||||
}
|
||||
if (this._videoEl) {
|
||||
this._videoEl.removeAttribute("src");
|
||||
this._videoEl.load();
|
||||
const videoEl = this._videoEl;
|
||||
if (videoEl) {
|
||||
videoEl.removeAttribute("src");
|
||||
videoEl.load();
|
||||
}
|
||||
if (this._peerConnection) {
|
||||
this._peerConnection.close();
|
||||
|
||||
this._peerConnection.onnegotiationneeded = null;
|
||||
this._peerConnection.onicecandidate = null;
|
||||
this._peerConnection.oniceconnectionstatechange = null;
|
||||
this._peerConnection.onicegatheringstatechange = null;
|
||||
this._peerConnection.ontrack = null;
|
||||
|
||||
// just for debugging
|
||||
this._peerConnection.onsignalingstatechange = null;
|
||||
|
||||
this._peerConnection = undefined;
|
||||
}
|
||||
this._unsub?.then((unsub) => unsub());
|
||||
this._unsub = undefined;
|
||||
this._sessionId = undefined;
|
||||
this._candidatesList = [];
|
||||
}
|
||||
|
||||
private _loadedData() {
|
||||
console.timeLog("WebRTC", "loadedData");
|
||||
console.timeEnd("WebRTC");
|
||||
// @ts-ignore
|
||||
fireEvent(this, "load");
|
||||
|
||||
console.timeLog("WebRTC", "loadedData");
|
||||
console.timeEnd("WebRTC");
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -43,6 +43,7 @@ class HaEntityMarker extends LitElement {
|
||||
.marker {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
width: 48px;
|
||||
|
@@ -22,8 +22,13 @@ import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_tim
|
||||
import { relativeTime } from "../../common/datetime/relative_time";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { toggleAttribute } from "../../common/dom/toggle_attribute";
|
||||
import { fullEntitiesContext, labelsContext } from "../../data/context";
|
||||
import {
|
||||
floorsContext,
|
||||
fullEntitiesContext,
|
||||
labelsContext,
|
||||
} from "../../data/context";
|
||||
import { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
import { LabelRegistryEntry } from "../../data/label_registry";
|
||||
import { LogbookEntry } from "../../data/logbook";
|
||||
import {
|
||||
@@ -201,6 +206,7 @@ class ActionRenderer {
|
||||
private hass: HomeAssistant,
|
||||
private entityReg: EntityRegistryEntry[],
|
||||
private labelReg: LabelRegistryEntry[],
|
||||
private floorReg: { [id: string]: FloorRegistryEntry },
|
||||
private entries: TemplateResult[],
|
||||
private trace: AutomationTraceExtended,
|
||||
private logbookRenderer: LogbookRenderer,
|
||||
@@ -319,6 +325,7 @@ class ActionRenderer {
|
||||
this.hass,
|
||||
this.entityReg,
|
||||
this.labelReg,
|
||||
this.floorReg,
|
||||
data,
|
||||
actionType
|
||||
),
|
||||
@@ -486,7 +493,13 @@ class ActionRenderer {
|
||||
|
||||
const name =
|
||||
repeatConfig.alias ||
|
||||
describeAction(this.hass, this.entityReg, this.labelReg, repeatConfig);
|
||||
describeAction(
|
||||
this.hass,
|
||||
this.entityReg,
|
||||
this.labelReg,
|
||||
this.floorReg,
|
||||
repeatConfig
|
||||
);
|
||||
|
||||
this._renderEntry(repeatPath, name, undefined, disabled);
|
||||
|
||||
@@ -584,6 +597,7 @@ class ActionRenderer {
|
||||
this.hass,
|
||||
this.entityReg,
|
||||
this.labelReg,
|
||||
this.floorReg,
|
||||
sequenceConfig,
|
||||
"sequence"
|
||||
),
|
||||
@@ -680,6 +694,10 @@ export class HaAutomationTracer extends LitElement {
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
_labelReg!: LabelRegistryEntry[];
|
||||
|
||||
@state()
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
_floorReg!: { [id: string]: FloorRegistryEntry };
|
||||
|
||||
protected render() {
|
||||
if (!this.trace) {
|
||||
return nothing;
|
||||
@@ -697,6 +715,7 @@ export class HaAutomationTracer extends LitElement {
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
this._labelReg,
|
||||
this._floorReg,
|
||||
entries,
|
||||
this.trace,
|
||||
logbookRenderer,
|
||||
|
@@ -39,10 +39,37 @@ export interface Stream {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export type WebRtcOfferEvent =
|
||||
| WebRtcId
|
||||
| WebRtcAnswer
|
||||
| WebRtcCandidate
|
||||
| WebRtcError;
|
||||
|
||||
export interface WebRtcId {
|
||||
type: "session";
|
||||
session_id: string;
|
||||
}
|
||||
|
||||
export interface WebRtcAnswer {
|
||||
type: "answer";
|
||||
answer: string;
|
||||
}
|
||||
|
||||
export interface WebRtcCandidate {
|
||||
type: "candidate";
|
||||
candidate: string;
|
||||
}
|
||||
|
||||
export interface WebRtcError {
|
||||
type: "error";
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface WebRtcOfferResponse {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const cameraUrlWithWidthHeight = (
|
||||
base_url: string,
|
||||
width: number,
|
||||
@@ -94,15 +121,29 @@ export const fetchStreamUrl = async (
|
||||
return stream;
|
||||
};
|
||||
|
||||
export const handleWebRtcOffer = (
|
||||
export const webRtcOffer = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
offer: string
|
||||
entity_id: string,
|
||||
offer: string,
|
||||
callback: (event: WebRtcOfferEvent) => void
|
||||
) =>
|
||||
hass.callWS<WebRtcAnswer>({
|
||||
type: "camera/web_rtc_offer",
|
||||
entity_id: entityId,
|
||||
offer: offer,
|
||||
hass.connection.subscribeMessage<WebRtcOfferEvent>(callback, {
|
||||
type: "camera/webrtc/offer",
|
||||
entity_id,
|
||||
offer,
|
||||
});
|
||||
|
||||
export const addWebRtcCandidate = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string,
|
||||
session_id: string,
|
||||
candidate: string
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "camera/webrtc/candidate",
|
||||
entity_id,
|
||||
session_id,
|
||||
candidate,
|
||||
});
|
||||
|
||||
export const fetchCameraPrefs = (hass: HomeAssistant, entityId: string) =>
|
||||
@@ -137,6 +178,7 @@ export const getEntityIdFromCameraMediaSource = (mediaContentId: string) =>
|
||||
export interface WebRTCClientConfiguration {
|
||||
configuration: RTCConfiguration;
|
||||
dataChannel?: string;
|
||||
getCandidatesUpfront: boolean;
|
||||
}
|
||||
|
||||
export const fetchWebRtcClientConfiguration = async (
|
||||
|
@@ -27,4 +27,6 @@ export const panelsContext = createContext<HomeAssistant["panels"]>("panels");
|
||||
export const fullEntitiesContext =
|
||||
createContext<EntityRegistryEntry[]>("extendedEntities");
|
||||
|
||||
export const floorsContext = createContext<HomeAssistant["floors"]>("floors");
|
||||
|
||||
export const labelsContext = createContext<LabelRegistryEntry[]>("labels");
|
||||
|
@@ -65,7 +65,7 @@ export const countryCurrency = {
|
||||
HK: "HKD",
|
||||
HN: "HNL",
|
||||
HM: "AUD",
|
||||
VE: "VEF",
|
||||
VE: "VED",
|
||||
PR: "USD",
|
||||
PS: "ILS",
|
||||
PW: "USD",
|
||||
|
@@ -358,21 +358,24 @@ export const restartHassioAddon = async (
|
||||
|
||||
export const uninstallHassioAddon = async (
|
||||
hass: HomeAssistant,
|
||||
slug: string
|
||||
) => {
|
||||
slug: string,
|
||||
removeData: boolean
|
||||
): Promise<void> => {
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
await hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/addons/${slug}/uninstall`,
|
||||
method: "post",
|
||||
timeout: null,
|
||||
data: { remove_config: removeData },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await hass.callApi<HassioResponse<void>>(
|
||||
"POST",
|
||||
`hassio/addons/${slug}/uninstall`
|
||||
`hassio/addons/${slug}/uninstall`,
|
||||
{ remove_config: removeData }
|
||||
);
|
||||
};
|
||||
|
||||
|
@@ -4,7 +4,7 @@ import { hassioApiResultExtractor, HassioResponse } from "./common";
|
||||
|
||||
interface IpConfiguration {
|
||||
address: string[];
|
||||
gateway: string;
|
||||
gateway: string | null;
|
||||
method: "disabled" | "static" | "auto";
|
||||
nameservers: string[];
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export interface NetworkInterface {
|
||||
ipv4?: Partial<IpConfiguration>;
|
||||
ipv6?: Partial<IpConfiguration>;
|
||||
type: "ethernet" | "wireless" | "vlan";
|
||||
wifi?: Partial<WifiConfiguration>;
|
||||
wifi?: Partial<WifiConfiguration> | null;
|
||||
}
|
||||
|
||||
interface DockerNetwork {
|
||||
@@ -27,7 +27,7 @@ interface DockerNetwork {
|
||||
interface: string;
|
||||
}
|
||||
|
||||
interface AccessPoint {
|
||||
export interface AccessPoint {
|
||||
mode: "infrastructure" | "mesh" | "adhoc" | "ap";
|
||||
ssid: string;
|
||||
mac: string;
|
||||
@@ -114,3 +114,65 @@ export const accesspointScan = async (
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const parseAddress = (address: string) => {
|
||||
const [ip, cidr] = address.split("/");
|
||||
return { ip, mask: cidrToNetmask(cidr, address.includes(":")) };
|
||||
};
|
||||
|
||||
export const formatAddress = (ip: string, mask: string) =>
|
||||
`${ip}/${netmaskToCidr(mask)}`;
|
||||
|
||||
// Helper functions
|
||||
export const cidrToNetmask = (
|
||||
cidr: string,
|
||||
isIPv6: boolean = false
|
||||
): string => {
|
||||
const bits = parseInt(cidr, 10);
|
||||
if (isIPv6) {
|
||||
const fullMask = "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff";
|
||||
const numGroups = Math.floor(bits / 16);
|
||||
const remainingBits = bits % 16;
|
||||
const lastGroup = remainingBits
|
||||
? parseInt(
|
||||
"1".repeat(remainingBits) + "0".repeat(16 - remainingBits),
|
||||
2
|
||||
).toString(16)
|
||||
: "";
|
||||
return fullMask
|
||||
.split(":")
|
||||
.slice(0, numGroups)
|
||||
.concat(lastGroup)
|
||||
.concat(Array(8 - numGroups - (lastGroup ? 1 : 0)).fill("0"))
|
||||
.join(":");
|
||||
}
|
||||
/* eslint-disable no-bitwise */
|
||||
const mask = ~(2 ** (32 - bits) - 1);
|
||||
return [
|
||||
(mask >>> 24) & 255,
|
||||
(mask >>> 16) & 255,
|
||||
(mask >>> 8) & 255,
|
||||
mask & 255,
|
||||
].join(".");
|
||||
/* eslint-enable no-bitwise */
|
||||
};
|
||||
|
||||
export const netmaskToCidr = (netmask: string): number => {
|
||||
if (netmask.includes(":")) {
|
||||
// IPv6
|
||||
return netmask
|
||||
.split(":")
|
||||
.map((group) =>
|
||||
group ? (parseInt(group, 16).toString(2).match(/1/g) || []).length : 0
|
||||
)
|
||||
.reduce((sum, val) => sum + val, 0);
|
||||
}
|
||||
// IPv4
|
||||
return netmask
|
||||
.split(".")
|
||||
.reduce(
|
||||
(count, octet) =>
|
||||
count + (parseInt(octet, 10).toString(2).match(/1/g) || []).length,
|
||||
0
|
||||
);
|
||||
};
|
||||
|
@@ -65,6 +65,10 @@ export type HassioInfo = {
|
||||
timezone: string;
|
||||
};
|
||||
|
||||
export type HassioBoots = {
|
||||
boots: Record<number, string>;
|
||||
};
|
||||
|
||||
export type HassioPanelInfo = PanelInfo<
|
||||
| undefined
|
||||
| {
|
||||
@@ -177,10 +181,39 @@ export const fetchHassioInfo = async (
|
||||
);
|
||||
};
|
||||
|
||||
export const fetchHassioLogs = async (hass: HomeAssistant, provider: string) =>
|
||||
hass.callApi<string>(
|
||||
export const fetchHassioBoots = async (hass: HomeAssistant) =>
|
||||
hass.callApi<HassioResponse<HassioBoots>>("GET", `hassio/host/logs/boots`);
|
||||
|
||||
export const fetchHassioLogs = async (
|
||||
hass: HomeAssistant,
|
||||
provider: string,
|
||||
range?: string,
|
||||
boot = 0
|
||||
) =>
|
||||
hass.callApiRaw(
|
||||
"GET",
|
||||
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs`
|
||||
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs/boots/${boot}`,
|
||||
undefined,
|
||||
range
|
||||
? {
|
||||
Range: range,
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
|
||||
export const fetchHassioLogsFollow = async (
|
||||
hass: HomeAssistant,
|
||||
provider: string,
|
||||
signal: AbortSignal,
|
||||
lines = 100,
|
||||
boot = 0
|
||||
) =>
|
||||
hass.callApiRaw(
|
||||
"GET",
|
||||
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs/boots/${boot}/follow?lines=${lines}`,
|
||||
undefined,
|
||||
undefined,
|
||||
signal
|
||||
);
|
||||
|
||||
export const getHassioLogDownloadUrl = (provider: string) =>
|
||||
@@ -188,6 +221,15 @@ export const getHassioLogDownloadUrl = (provider: string) =>
|
||||
provider.includes("_") ? `addons/${provider}` : provider
|
||||
}/logs`;
|
||||
|
||||
export const getHassioLogDownloadLinesUrl = (
|
||||
provider: string,
|
||||
lines: number,
|
||||
boot = 0
|
||||
) =>
|
||||
`/api/hassio/${
|
||||
provider.includes("_") ? `addons/${provider}` : provider
|
||||
}/logs/boots/${boot}?lines=${lines}`;
|
||||
|
||||
export const setSupervisorOption = async (
|
||||
hass: HomeAssistant,
|
||||
data: SupervisorOptions
|
||||
|
@@ -22,6 +22,7 @@ export type IntegrationType =
|
||||
|
||||
export interface IntegrationManifest {
|
||||
is_built_in: boolean;
|
||||
overwrites_built_in?: boolean;
|
||||
domain: string;
|
||||
name: string;
|
||||
config_flow: boolean;
|
||||
|
@@ -11,6 +11,7 @@ export interface Integration {
|
||||
iot_class?: string;
|
||||
supported_by?: string;
|
||||
is_built_in?: boolean;
|
||||
overwrites_built_in?: boolean;
|
||||
single_config_entry?: boolean;
|
||||
}
|
||||
|
||||
@@ -23,6 +24,7 @@ export interface Brand {
|
||||
integrations?: Integrations;
|
||||
iot_standards?: IotStandards[];
|
||||
is_built_in?: boolean;
|
||||
overwrites_built_in?: boolean;
|
||||
}
|
||||
|
||||
export interface Brands {
|
||||
|
@@ -50,14 +50,23 @@ export interface LogbookEntry {
|
||||
// Localization mapping for all the triggers in core
|
||||
// in homeassistant.components.homeassistant.triggers
|
||||
//
|
||||
const triggerPhrases = {
|
||||
"numeric state of": "triggered_by_numeric_state_of", // number state trigger
|
||||
"state of": "triggered_by_state_of", // state trigger
|
||||
event: "triggered_by_event", // event trigger
|
||||
time: "triggered_by_time", // time trigger
|
||||
"time pattern": "triggered_by_time_pattern", // time trigger
|
||||
"Home Assistant stopping": "triggered_by_homeassistant_stopping", // stop event
|
||||
"Home Assistant starting": "triggered_by_homeassistant_starting", // start event
|
||||
type TriggerPhraseKeys =
|
||||
| "triggered_by_numeric_state_of"
|
||||
| "triggered_by_state_of"
|
||||
| "triggered_by_event"
|
||||
| "triggered_by_time"
|
||||
| "triggered_by_time_pattern"
|
||||
| "triggered_by_homeassistant_stopping"
|
||||
| "triggered_by_homeassistant_starting";
|
||||
|
||||
const triggerPhrases: Record<TriggerPhraseKeys, string> = {
|
||||
triggered_by_numeric_state_of: "numeric state of", // number state trigger
|
||||
triggered_by_state_of: "state of", // state trigger
|
||||
triggered_by_event: "event", // event trigger
|
||||
triggered_by_time: "time", // time trigger
|
||||
triggered_by_time_pattern: "time pattern", // time trigger
|
||||
triggered_by_homeassistant_stopping: "Home Assistant stopping", // stop event
|
||||
triggered_by_homeassistant_starting: "Home Assistant starting", // start event
|
||||
};
|
||||
|
||||
export const getLogbookDataForContext = async (
|
||||
@@ -167,11 +176,14 @@ export const localizeTriggerSource = (
|
||||
localize: LocalizeFunc,
|
||||
source: string
|
||||
) => {
|
||||
for (const triggerPhrase in triggerPhrases) {
|
||||
if (source.startsWith(triggerPhrase)) {
|
||||
for (const triggerPhraseKey of Object.keys(
|
||||
triggerPhrases
|
||||
) as TriggerPhraseKeys[]) {
|
||||
const phrase = triggerPhrases[triggerPhraseKey];
|
||||
if (source.startsWith(phrase)) {
|
||||
return source.replace(
|
||||
triggerPhrase,
|
||||
`${localize(`ui.components.logbook.${triggerPhrases[triggerPhrase]}`)}`
|
||||
phrase,
|
||||
`${localize(`ui.components.logbook.${triggerPhraseKey}`)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -17,10 +17,6 @@ export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
|
||||
cards?: LovelaceCardConfig[];
|
||||
}
|
||||
|
||||
export interface LovelaceGridSectionConfig extends LovelaceSectionConfig {
|
||||
grid_base?: number;
|
||||
}
|
||||
|
||||
export interface LovelaceStrategySectionConfig
|
||||
extends LovelaceBaseSectionConfig {
|
||||
strategy: LovelaceStrategyConfig;
|
||||
|
@@ -14,6 +14,7 @@ import {
|
||||
computeEntityRegistryName,
|
||||
entityRegistryById,
|
||||
} from "./entity_registry";
|
||||
import { FloorRegistryEntry } from "./floor_registry";
|
||||
import { domainToName } from "./integration";
|
||||
import { LabelRegistryEntry } from "./label_registry";
|
||||
import {
|
||||
@@ -43,6 +44,7 @@ export const describeAction = <T extends ActionType>(
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
labelRegistry: LabelRegistryEntry[],
|
||||
floorRegistry: { [id: string]: FloorRegistryEntry },
|
||||
action: ActionTypes[T],
|
||||
actionType?: T,
|
||||
ignoreAlias = false
|
||||
@@ -52,6 +54,7 @@ export const describeAction = <T extends ActionType>(
|
||||
hass,
|
||||
entityRegistry,
|
||||
labelRegistry,
|
||||
floorRegistry,
|
||||
action,
|
||||
actionType,
|
||||
ignoreAlias
|
||||
@@ -75,6 +78,7 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
labelRegistry: LabelRegistryEntry[],
|
||||
floorRegistry: { [id: string]: FloorRegistryEntry },
|
||||
action: ActionTypes[T],
|
||||
actionType?: T,
|
||||
ignoreAlias = false
|
||||
@@ -164,7 +168,7 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
);
|
||||
}
|
||||
} else if (key === "floor_id") {
|
||||
const floor = hass.floors[targetThing] ?? undefined;
|
||||
const floor = floorRegistry[targetThing] ?? undefined;
|
||||
if (floor?.name) {
|
||||
targets.push(floor.name);
|
||||
} else {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface ThreadRouter {
|
||||
instance_name: string;
|
||||
addresses: [string];
|
||||
border_agent_id: string | null;
|
||||
brand: "google" | "apple" | "homeassistant";
|
||||
|
@@ -8,6 +8,7 @@ import { BINARY_STATE_ON } from "../common/const";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { HomeAssistant } from "../types";
|
||||
@@ -23,13 +24,15 @@ export enum UpdateEntityFeature {
|
||||
|
||||
interface UpdateEntityAttributes extends HassEntityAttributeBase {
|
||||
auto_update: boolean | null;
|
||||
display_precision: number;
|
||||
installed_version: string | null;
|
||||
in_progress: boolean | number;
|
||||
in_progress: boolean;
|
||||
latest_version: string | null;
|
||||
release_summary: string | null;
|
||||
release_url: string | null;
|
||||
skipped_version: string | null;
|
||||
title: string | null;
|
||||
update_percentage: number | null;
|
||||
}
|
||||
|
||||
export interface UpdateEntity extends HassEntityBase {
|
||||
@@ -38,7 +41,7 @@ export interface UpdateEntity extends HassEntityBase {
|
||||
|
||||
export const updateUsesProgress = (entity: UpdateEntity): boolean =>
|
||||
supportsFeature(entity, UpdateEntityFeature.PROGRESS) &&
|
||||
typeof entity.attributes.in_progress === "number";
|
||||
entity.attributes.update_percentage !== null;
|
||||
|
||||
export const updateCanInstall = (
|
||||
entity: UpdateEntity,
|
||||
@@ -49,7 +52,7 @@ export const updateCanInstall = (
|
||||
supportsFeature(entity, UpdateEntityFeature.INSTALL);
|
||||
|
||||
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
|
||||
updateUsesProgress(entity) || !!entity.attributes.in_progress;
|
||||
!!entity.attributes.in_progress;
|
||||
|
||||
export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
|
||||
hass.callWS<string | null>({
|
||||
@@ -183,10 +186,13 @@ export const computeUpdateStateDisplay = (
|
||||
if (updateIsInstalling(stateObj)) {
|
||||
const supportsProgress =
|
||||
supportsFeature(stateObj, UpdateEntityFeature.PROGRESS) &&
|
||||
typeof attributes.in_progress === "number";
|
||||
attributes.update_percentage !== null;
|
||||
if (supportsProgress) {
|
||||
return hass.localize("ui.card.update.installing_with_progress", {
|
||||
progress: attributes.in_progress as number,
|
||||
progress: formatNumber(attributes.update_percentage!, hass.locale, {
|
||||
maximumFractionDigits: attributes.display_precision,
|
||||
minimumFractionDigits: attributes.display_precision,
|
||||
}),
|
||||
});
|
||||
}
|
||||
return hass.localize("ui.card.update.installing");
|
||||
|
@@ -31,6 +31,9 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [
|
||||
"valve",
|
||||
"water_heater",
|
||||
];
|
||||
/** Domains with full height more info dialog */
|
||||
export const DOMAINS_FULL_HEIGHT_MORE_INFO = ["update"];
|
||||
|
||||
/** Domains with separate more info dialog. */
|
||||
export const DOMAINS_WITH_MORE_INFO = [
|
||||
"alarm_control_panel",
|
||||
|
@@ -93,12 +93,13 @@ class MoreInfoCover extends LitElement {
|
||||
supportsFeature(this.stateObj, CoverEntityFeature.CLOSE_TILT) ||
|
||||
supportsFeature(this.stateObj, CoverEntityFeature.STOP_TILT);
|
||||
|
||||
const supportsOpenCloseWithoutStop =
|
||||
const supportsOpenCloseOnly =
|
||||
supportsFeature(this.stateObj, CoverEntityFeature.OPEN) &&
|
||||
supportsFeature(this.stateObj, CoverEntityFeature.CLOSE) &&
|
||||
!supportsFeature(this.stateObj, CoverEntityFeature.STOP) &&
|
||||
!supportsFeature(this.stateObj, CoverEntityFeature.OPEN_TILT) &&
|
||||
!supportsFeature(this.stateObj, CoverEntityFeature.CLOSE_TILT);
|
||||
!supportsTilt &&
|
||||
!supportsPosition &&
|
||||
!supportsTiltPosition;
|
||||
|
||||
return html`
|
||||
<ha-more-info-state-header
|
||||
@@ -133,7 +134,7 @@ class MoreInfoCover extends LitElement {
|
||||
${
|
||||
this._mode === "button"
|
||||
? html`
|
||||
${supportsOpenCloseWithoutStop
|
||||
${supportsOpenCloseOnly
|
||||
? html`
|
||||
<ha-state-control-cover-toggle
|
||||
.stateObj=${this.stateObj}
|
||||
|
@@ -1,15 +1,18 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { BINARY_STATE_OFF } from "../../../common/const";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-checkbox";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-faded";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../../components/ha-switch";
|
||||
import { isUnavailableState } from "../../../data/entity";
|
||||
import {
|
||||
UpdateEntity,
|
||||
@@ -30,6 +33,8 @@ class MoreInfoUpdate extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _markdownLoading = true;
|
||||
|
||||
protected render() {
|
||||
if (
|
||||
!this.hass ||
|
||||
@@ -45,137 +50,174 @@ class MoreInfoUpdate extends LitElement {
|
||||
this.stateObj.attributes.latest_version;
|
||||
|
||||
return html`
|
||||
${this.stateObj.attributes.in_progress
|
||||
? supportsFeature(this.stateObj, UpdateEntityFeature.PROGRESS) &&
|
||||
typeof this.stateObj.attributes.in_progress === "number"
|
||||
? html`<mwc-linear-progress
|
||||
.progress=${this.stateObj.attributes.in_progress / 100}
|
||||
buffer=""
|
||||
></mwc-linear-progress>`
|
||||
: html`<mwc-linear-progress indeterminate></mwc-linear-progress>`
|
||||
: ""}
|
||||
<h3>${this.stateObj.attributes.title}</h3>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
<div class="row">
|
||||
<div class="key">
|
||||
${this.hass.formatEntityAttributeName(
|
||||
this.stateObj,
|
||||
"installed_version"
|
||||
)}
|
||||
<div class="content">
|
||||
${this.stateObj.attributes.in_progress
|
||||
? supportsFeature(this.stateObj, UpdateEntityFeature.PROGRESS) &&
|
||||
this.stateObj.attributes.update_percentage !== null
|
||||
? html`<mwc-linear-progress
|
||||
.progress=${this.stateObj.attributes.update_percentage / 100}
|
||||
buffer=""
|
||||
></mwc-linear-progress>`
|
||||
: html`<mwc-linear-progress indeterminate></mwc-linear-progress>`
|
||||
: nothing}
|
||||
<h3>${this.stateObj.attributes.title}</h3>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
<div class="row">
|
||||
<div class="key">
|
||||
${this.hass.formatEntityAttributeName(
|
||||
this.stateObj,
|
||||
"installed_version"
|
||||
)}
|
||||
</div>
|
||||
<div class="value">
|
||||
${this.stateObj.attributes.installed_version ??
|
||||
this.hass.localize("state.default.unavailable")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="value">
|
||||
${this.stateObj.attributes.installed_version ??
|
||||
this.hass.localize("state.default.unavailable")}
|
||||
<div class="row">
|
||||
<div class="key">
|
||||
${this.hass.formatEntityAttributeName(
|
||||
this.stateObj,
|
||||
"latest_version"
|
||||
)}
|
||||
</div>
|
||||
<div class="value">
|
||||
${this.stateObj.attributes.latest_version ??
|
||||
this.hass.localize("state.default.unavailable")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="key">
|
||||
${this.hass.formatEntityAttributeName(
|
||||
this.stateObj,
|
||||
"latest_version"
|
||||
)}
|
||||
</div>
|
||||
<div class="value">
|
||||
${this.stateObj.attributes.latest_version ??
|
||||
this.hass.localize("state.default.unavailable")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.stateObj.attributes.release_url
|
||||
? html`<div class="row">
|
||||
<div class="key">
|
||||
<a
|
||||
href=${this.stateObj.attributes.release_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.release_announcement"
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
</div>`
|
||||
: ""}
|
||||
${supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES) &&
|
||||
!this._error
|
||||
? this._releaseNotes === undefined
|
||||
? html`<div class="flex center">
|
||||
<ha-circular-progress indeterminate></ha-circular-progress>
|
||||
${this.stateObj.attributes.release_url
|
||||
? html`<div class="row">
|
||||
<div class="key">
|
||||
<a
|
||||
href=${this.stateObj.attributes.release_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.release_announcement"
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
</div>`
|
||||
: html`<hr />
|
||||
<ha-faded>
|
||||
<ha-markdown .content=${this._releaseNotes}></ha-markdown>
|
||||
</ha-faded> `
|
||||
: this.stateObj.attributes.release_summary
|
||||
? html`<hr />
|
||||
<ha-markdown
|
||||
.content=${this.stateObj.attributes.release_summary}
|
||||
></ha-markdown>`
|
||||
: ""}
|
||||
${supportsFeature(this.stateObj, UpdateEntityFeature.BACKUP)
|
||||
? html`<hr />
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup"
|
||||
)}
|
||||
>
|
||||
<ha-checkbox
|
||||
checked
|
||||
.disabled=${updateIsInstalling(this.stateObj)}
|
||||
></ha-checkbox>
|
||||
</ha-formfield> `
|
||||
: ""}
|
||||
<div class="actions">
|
||||
${this.stateObj.state === BINARY_STATE_OFF &&
|
||||
this.stateObj.attributes.skipped_version
|
||||
: nothing}
|
||||
${supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES) &&
|
||||
!this._error
|
||||
? this._releaseNotes === undefined
|
||||
? html`
|
||||
<hr />
|
||||
${this._markdownLoading ? this._renderLoader() : nothing}
|
||||
`
|
||||
: html`
|
||||
<hr />
|
||||
<ha-markdown
|
||||
@content-resize=${this._markdownLoaded}
|
||||
.content=${this._releaseNotes}
|
||||
class=${this._markdownLoading ? "hidden" : ""}
|
||||
></ha-markdown>
|
||||
${this._markdownLoading ? this._renderLoader() : nothing}
|
||||
`
|
||||
: this.stateObj.attributes.release_summary
|
||||
? html`
|
||||
<hr />
|
||||
<ha-markdown
|
||||
@content-resize=${this._markdownLoaded}
|
||||
.content=${this.stateObj.attributes.release_summary}
|
||||
class=${this._markdownLoading ? "hidden" : ""}
|
||||
></ha-markdown>
|
||||
${this._markdownLoading ? this._renderLoader() : nothing}
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="footer">
|
||||
${supportsFeature(this.stateObj, UpdateEntityFeature.BACKUP)
|
||||
? html`
|
||||
<mwc-button @click=${this._handleClearSkipped}>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.clear_skipped"
|
||||
)}
|
||||
</mwc-button>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
id="create_backup"
|
||||
checked
|
||||
.disabled=${updateIsInstalling(this.stateObj)}
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
`
|
||||
: html`
|
||||
<mwc-button
|
||||
@click=${this._handleSkip}
|
||||
.disabled=${skippedVersion ||
|
||||
this.stateObj.state === BINARY_STATE_OFF ||
|
||||
updateIsInstalling(this.stateObj)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.skip"
|
||||
)}
|
||||
</mwc-button>
|
||||
`}
|
||||
${supportsFeature(this.stateObj, UpdateEntityFeature.INSTALL)
|
||||
? html`
|
||||
<mwc-button
|
||||
@click=${this._handleInstall}
|
||||
.disabled=${(this.stateObj.state === BINARY_STATE_OFF &&
|
||||
!skippedVersion) ||
|
||||
updateIsInstalling(this.stateObj)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.install"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
<div class="actions">
|
||||
${this.stateObj.state === BINARY_STATE_OFF &&
|
||||
this.stateObj.attributes.skipped_version
|
||||
? html`
|
||||
<ha-button @click=${this._handleClearSkipped}>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.clear_skipped"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: html`
|
||||
<ha-button
|
||||
@click=${this._handleSkip}
|
||||
.disabled=${skippedVersion ||
|
||||
this.stateObj.state === BINARY_STATE_OFF ||
|
||||
updateIsInstalling(this.stateObj)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.skip"
|
||||
)}
|
||||
</ha-button>
|
||||
`}
|
||||
${supportsFeature(this.stateObj, UpdateEntityFeature.INSTALL)
|
||||
? html`
|
||||
<ha-button
|
||||
@click=${this._handleInstall}
|
||||
.disabled=${(this.stateObj.state === BINARY_STATE_OFF &&
|
||||
!skippedVersion) ||
|
||||
updateIsInstalling(this.stateObj)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.update"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderLoader() {
|
||||
return html`
|
||||
<div class="flex center loader">
|
||||
<ha-circular-progress indeterminate></ha-circular-progress>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
if (supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES)) {
|
||||
updateReleaseNotes(this.hass, this.stateObj!.entity_id)
|
||||
.then((result) => {
|
||||
this._releaseNotes = result;
|
||||
})
|
||||
.catch((err) => {
|
||||
this._error = err.message;
|
||||
});
|
||||
this._fetchReleaseNotes();
|
||||
}
|
||||
}
|
||||
|
||||
private async _markdownLoaded() {
|
||||
if (this._markdownLoading) {
|
||||
this._markdownLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchReleaseNotes() {
|
||||
try {
|
||||
this._releaseNotes = await updateReleaseNotes(
|
||||
this.hass,
|
||||
this.stateObj!.entity_id
|
||||
);
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,9 +225,11 @@ class MoreInfoUpdate extends LitElement {
|
||||
if (!supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
|
||||
return null;
|
||||
}
|
||||
const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
|
||||
if (checkbox) {
|
||||
return checkbox.checked;
|
||||
const createBackupSwitch = this.shadowRoot?.getElementById(
|
||||
"create-backup"
|
||||
) as HaSwitch;
|
||||
if (createBackupSwitch) {
|
||||
return createBackupSwitch.checked;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -234,6 +278,12 @@ class MoreInfoUpdate extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
}
|
||||
hr {
|
||||
border-color: var(--divider-color);
|
||||
border-bottom: none;
|
||||
@@ -248,26 +298,44 @@ class MoreInfoUpdate extends LitElement {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.actions {
|
||||
|
||||
.footer {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
background: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
margin: 8px 0 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
padding: 12px 0;
|
||||
margin-bottom: -24px;
|
||||
z-index: 1;
|
||||
margin: 0 -24px -24px -24px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.actions mwc-button {
|
||||
margin: 0 4px 4px;
|
||||
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;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
@@ -282,6 +350,16 @@ class MoreInfoUpdate extends LitElement {
|
||||
}
|
||||
ha-markdown {
|
||||
direction: ltr;
|
||||
padding-bottom: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
ha-markdown.hidden {
|
||||
display: none;
|
||||
}
|
||||
.loader {
|
||||
height: 80px;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -83,10 +83,11 @@ class MoreInfoValve extends LitElement {
|
||||
supportsFeature(this.stateObj, ValveEntityFeature.CLOSE) ||
|
||||
supportsFeature(this.stateObj, ValveEntityFeature.STOP);
|
||||
|
||||
const supportsOpenCloseWithoutStop =
|
||||
const supportsOpenCloseOnly =
|
||||
supportsFeature(this.stateObj, ValveEntityFeature.OPEN) &&
|
||||
supportsFeature(this.stateObj, ValveEntityFeature.CLOSE) &&
|
||||
!supportsFeature(this.stateObj, ValveEntityFeature.STOP);
|
||||
!supportsFeature(this.stateObj, ValveEntityFeature.STOP) &&
|
||||
!supportsPosition;
|
||||
|
||||
return html`
|
||||
<ha-more-info-state-header
|
||||
@@ -113,7 +114,7 @@ class MoreInfoValve extends LitElement {
|
||||
${
|
||||
this._mode === "button"
|
||||
? html`
|
||||
${supportsOpenCloseWithoutStop
|
||||
${supportsOpenCloseOnly
|
||||
? html`
|
||||
<ha-state-control-valve-toggle
|
||||
.stateObj=${this.stateObj}
|
||||
|
@@ -9,6 +9,7 @@ import {
|
||||
computeShowHistoryComponent,
|
||||
computeShowLogBookComponent,
|
||||
computeShowNewMoreInfo,
|
||||
DOMAINS_FULL_HEIGHT_MORE_INFO,
|
||||
DOMAINS_NO_INFO,
|
||||
DOMAINS_WITH_MORE_INFO,
|
||||
} from "./const";
|
||||
@@ -40,6 +41,8 @@ export class MoreInfoInfo extends LitElement {
|
||||
const entityRegObj = this.hass.entities[entityId];
|
||||
const domain = computeDomain(entityId);
|
||||
const isNewMoreInfo = stateObj && computeShowNewMoreInfo(stateObj);
|
||||
const isFullHeight =
|
||||
isNewMoreInfo || DOMAINS_FULL_HEIGHT_MORE_INFO.includes(domain);
|
||||
|
||||
return html`
|
||||
<div class="container" data-domain=${domain}>
|
||||
@@ -89,7 +92,7 @@ export class MoreInfoInfo extends LitElement {
|
||||
.entityId=${this.entityId}
|
||||
></ha-more-info-logbook>`}
|
||||
<more-info-content
|
||||
?full-height=${isNewMoreInfo}
|
||||
?full-height=${isFullHeight}
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
.entry=${this.entry}
|
||||
|
@@ -39,6 +39,9 @@ export const AssistantSetupStyles = [
|
||||
.footer.full-width ha-button {
|
||||
width: 100%;
|
||||
}
|
||||
.footer.centered {
|
||||
justify-content: center;
|
||||
}
|
||||
.footer.side-by-side {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
@@ -14,7 +14,6 @@ import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { VoiceAssistantSetupDialogParams } from "./show-voice-assistant-setup-dialog";
|
||||
import "./voice-assistant-setup-step-addons";
|
||||
import "./voice-assistant-setup-step-area";
|
||||
import "./voice-assistant-setup-step-change-wake-word";
|
||||
import "./voice-assistant-setup-step-check";
|
||||
@@ -34,7 +33,6 @@ export const enum STEP {
|
||||
PIPELINE,
|
||||
SUCCESS,
|
||||
CLOUD,
|
||||
ADDONS,
|
||||
CHANGE_WAKEWORD,
|
||||
}
|
||||
|
||||
@@ -210,22 +208,18 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
? html`<ha-voice-assistant-setup-step-cloud
|
||||
.hass=${this.hass}
|
||||
></ha-voice-assistant-setup-step-cloud>`
|
||||
: this._step === STEP.ADDONS
|
||||
? html`<ha-voice-assistant-setup-step-addons
|
||||
: this._step === STEP.SUCCESS
|
||||
? html`<ha-voice-assistant-setup-step-success
|
||||
.hass=${this.hass}
|
||||
></ha-voice-assistant-setup-step-addons>`
|
||||
: this._step === STEP.SUCCESS
|
||||
? html`<ha-voice-assistant-setup-step-success
|
||||
.hass=${this.hass}
|
||||
.assistConfiguration=${this
|
||||
._assistConfiguration}
|
||||
.assistEntityId=${this._findDomainEntityId(
|
||||
this._params.deviceId,
|
||||
this.hass.entities,
|
||||
"assist_satellite"
|
||||
)}
|
||||
></ha-voice-assistant-setup-step-success>`
|
||||
: nothing}
|
||||
.assistConfiguration=${this
|
||||
._assistConfiguration}
|
||||
.assistEntityId=${this._findDomainEntityId(
|
||||
this._params.deviceId,
|
||||
this.hass.entities,
|
||||
"assist_satellite"
|
||||
)}
|
||||
></ha-voice-assistant-setup-step-success>`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
|
@@ -1,185 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@@ -16,7 +16,7 @@ export class HaVoiceAssistantSetupStepArea extends LitElement {
|
||||
const device = this.hass.devices[this.deviceId];
|
||||
|
||||
return html`<div class="content">
|
||||
<img src="/static/icons/casita/loving.png" />
|
||||
<img src="/static/images/voice-assistant/area.gif" />
|
||||
<h1>Select area</h1>
|
||||
<p class="secondary">
|
||||
When you voice assistant knows where it is, it can better control the
|
||||
|
@@ -10,6 +10,7 @@ import { STEP } from "./voice-assistant-setup-dialog";
|
||||
import { AssistantSetupStyles } from "./styles";
|
||||
import "../../components/ha-md-list";
|
||||
import "../../components/ha-md-list-item";
|
||||
import { formatLanguageCode } from "../../common/language/format_language";
|
||||
|
||||
@customElement("ha-voice-assistant-setup-step-change-wake-word")
|
||||
export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
|
||||
@@ -22,11 +23,12 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
|
||||
|
||||
protected override render() {
|
||||
return html`<div class="padding content">
|
||||
<img src="/static/icons/casita/smiling.png" />
|
||||
<img src="/static/images/voice-assistant/change-wake-word.gif" />
|
||||
<h1>Change wake word</h1>
|
||||
<p class="secondary">
|
||||
Some wake words are better for [your language] and voice than others.
|
||||
Please try them out.
|
||||
Some wake words are better for
|
||||
${formatLanguageCode(this.hass.locale.language, this.hass.locale)} and
|
||||
voice than others. Please try them out.
|
||||
</p>
|
||||
</div>
|
||||
<ha-md-list>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { html, LitElement, PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { testAssistSatelliteConnection } from "../../data/assist_satellite";
|
||||
@@ -13,6 +13,8 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
|
||||
|
||||
@state() private _status?: "success" | "timeout";
|
||||
|
||||
@state() private _showLoader = false;
|
||||
|
||||
protected override willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
if (!this.hasUpdated) {
|
||||
@@ -30,39 +32,48 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
|
||||
|
||||
protected override render() {
|
||||
return html`<div class="content">
|
||||
${this._status === "success"
|
||||
? html`<img src="/static/icons/casita/smiling.png" />
|
||||
${this._status === "timeout"
|
||||
? html`<img src="/static/images/voice-assistant/error.gif" />
|
||||
<h1>The voice assistant is unable to connect to Home Assistant</h1>
|
||||
<p class="secondary">
|
||||
To play audio, the voice assistant device has to connect to Home
|
||||
Assistant to fetch the files. Our test shows that the device is
|
||||
unable to reach the Home Assistant server.
|
||||
</p>
|
||||
<div class="footer">
|
||||
<a
|
||||
href="https://www.home-assistant.io/docs/configuration/remote/#adding-a-remote-url-to-home-assistant"
|
||||
><ha-button>Help me</ha-button></a
|
||||
>
|
||||
<ha-button @click=${this._testConnection}>Retry</ha-button>
|
||||
</div>`
|
||||
: html`<img src="/static/images/voice-assistant/hi.gif" />
|
||||
<h1>Hi</h1>
|
||||
<p class="secondary">
|
||||
With a couple of steps we are going to setup your voice assistant.
|
||||
</p>`
|
||||
: this._status === "timeout"
|
||||
? html`<img src="/static/icons/casita/sad.png" />
|
||||
<h1>Voice assistant can not connect to Home Assistant</h1>
|
||||
<p class="secondary">
|
||||
A good explanation what is happening and what action you should
|
||||
take.
|
||||
</p>
|
||||
<div class="footer">
|
||||
<a href="#"><ha-button>Help me</ha-button></a>
|
||||
<ha-button @click=${this._testConnection}>Retry</ha-button>
|
||||
</div>`
|
||||
: html`<img src="/static/icons/casita/loading.png" />
|
||||
<h1>Checking...</h1>
|
||||
<p class="secondary">
|
||||
We are checking if the device can reach your Home Assistant
|
||||
instance.
|
||||
</p>
|
||||
<ha-circular-progress indeterminate></ha-circular-progress>`}
|
||||
Over the next couple steps we're going to personalize your voice
|
||||
assistant.
|
||||
</p>
|
||||
|
||||
${this._showLoader
|
||||
? html`<ha-circular-progress
|
||||
indeterminate
|
||||
></ha-circular-progress>`
|
||||
: nothing} `}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private async _testConnection() {
|
||||
this._status = undefined;
|
||||
this._showLoader = false;
|
||||
const timeout = setTimeout(() => {
|
||||
this._showLoader = true;
|
||||
}, 3000);
|
||||
const result = await testAssistSatelliteConnection(
|
||||
this.hass,
|
||||
this.assistEntityId!
|
||||
);
|
||||
clearTimeout(timeout);
|
||||
this._showLoader = false;
|
||||
this._status = result.status;
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { mdiEarth, mdiMicrophoneMessage, mdiOpenInNew } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { brandsUrl } from "../../util/brands-url";
|
||||
import { AssistantSetupStyles } from "./styles";
|
||||
|
||||
@customElement("ha-voice-assistant-setup-step-cloud")
|
||||
@@ -10,22 +12,92 @@ export class HaVoiceAssistantSetupStepCloud extends LitElement {
|
||||
|
||||
protected override render() {
|
||||
return html`<div class="content">
|
||||
<img src="/static/images/logo_nabu_casa.png" />
|
||||
<h1>Supercharge your assistant with Home Assistant Cloud</h1>
|
||||
<p class="secondary">
|
||||
Speed up and take the load off your system by running your
|
||||
text-to-speech and speech-to-text in our private and secure cloud.
|
||||
Cloud also includes secure remote access to your system while
|
||||
supporting the development of Home Assistant.
|
||||
</p>
|
||||
<img
|
||||
src=${`/static/images/logo_nabu_casa${this.hass.themes?.darkMode ? "_dark" : ""}.png`}
|
||||
alt="Nabu Casa logo"
|
||||
/>
|
||||
<h1>The power of Home Assistant Cloud</h1>
|
||||
<div class="features">
|
||||
<div class="feature speech">
|
||||
<div class="logos">
|
||||
<div class="round-icon">
|
||||
<ha-svg-icon .path=${mdiMicrophoneMessage}></ha-svg-icon>
|
||||
</div>
|
||||
</div>
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.assistants.cloud.features.speech.title"
|
||||
)}
|
||||
<span class="no-wrap"></span>
|
||||
</h2>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.assistants.cloud.features.speech.text"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="feature access">
|
||||
<div class="logos">
|
||||
<div class="round-icon">
|
||||
<ha-svg-icon .path=${mdiEarth}></ha-svg-icon>
|
||||
</div>
|
||||
</div>
|
||||
<h2>
|
||||
Remote access
|
||||
<span class="no-wrap"></span>
|
||||
</h2>
|
||||
<p>
|
||||
Secure remote access to your system while supporting the
|
||||
development of Home Assistant.
|
||||
</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="logos">
|
||||
<img
|
||||
alt="Google Assistant"
|
||||
src=${brandsUrl({
|
||||
domain: "google_assistant",
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<img
|
||||
alt="Amazon Alexa"
|
||||
src=${brandsUrl({
|
||||
domain: "alexa",
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
</div>
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.assistants.cloud.features.assistants.title"
|
||||
)}
|
||||
</h2>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.assistants.cloud.features.assistants.text"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer side-by-side">
|
||||
<a
|
||||
href="https://www.nabucasa.com"
|
||||
target="_blank"
|
||||
rel="noreferrer noopenner"
|
||||
><ha-button>Learn more</ha-button></a
|
||||
>
|
||||
<ha-button>
|
||||
<ha-svg-icon .path=${mdiOpenInNew} slot="icon"></ha-svg-icon>
|
||||
nabucasa.com
|
||||
</ha-button>
|
||||
</a>
|
||||
<a href="/config/cloud/register" @click=${this._close}
|
||||
><ha-button unelevated>Try 1 month for free</ha-button></a
|
||||
>
|
||||
@@ -36,7 +108,58 @@ export class HaVoiceAssistantSetupStepCloud extends LitElement {
|
||||
fireEvent(this, "closed");
|
||||
}
|
||||
|
||||
static styles = AssistantSetupStyles;
|
||||
static styles = [
|
||||
AssistantSetupStyles,
|
||||
css`
|
||||
.features {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
.feature {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.feature .logos {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.feature .logos > * {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 0 4px;
|
||||
}
|
||||
.round-icon {
|
||||
border-radius: 50%;
|
||||
color: #6e41ab;
|
||||
background-color: #e8dcf7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
}
|
||||
.access .round-icon {
|
||||
color: #00aef8;
|
||||
background-color: #cceffe;
|
||||
}
|
||||
.feature h2 {
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.feature p {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -32,6 +32,10 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
|
||||
@state() private _showSecond = false;
|
||||
|
||||
@state() private _showThird = false;
|
||||
|
||||
@state() private _showFourth = false;
|
||||
|
||||
protected override willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
@@ -44,63 +48,83 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
super.firstUpdated(changedProperties);
|
||||
setTimeout(() => {
|
||||
this._showFirst = true;
|
||||
}, 1);
|
||||
}, 200);
|
||||
setTimeout(() => {
|
||||
this._showSecond = true;
|
||||
}, 1500);
|
||||
}, 600);
|
||||
setTimeout(() => {
|
||||
this._showThird = true;
|
||||
}, 3000);
|
||||
setTimeout(() => {
|
||||
this._showFourth = true;
|
||||
}, 8000);
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
return html`<div class="padding content">
|
||||
<div class="messages-container">
|
||||
return html`<div class="content">
|
||||
<h1>What hardware do you want to use?</h1>
|
||||
<p class="secondary">
|
||||
How quickly your assistant responds depends on the power of the
|
||||
hardware.
|
||||
</p>
|
||||
<div class="container">
|
||||
<div class="messages-container cloud">
|
||||
<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>
|
||||
<h1>Select system</h1>
|
||||
<p class="secondary">
|
||||
How quickly your voice assistant responds depends on the power of your
|
||||
system.
|
||||
</p>
|
||||
<h2>Home Assistant Cloud</h2>
|
||||
<p>Ideal if you don't have a powerful system at home.</p>
|
||||
<ha-button @click=${this._setupCloud}>Learn more</ha-button>
|
||||
</div>
|
||||
<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"
|
||||
<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>
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/voice_control/voice_remote_local_assistant/"
|
||||
)}
|
||||
rel="noreferrer noopenner"
|
||||
target="_blank"
|
||||
@click=${this._skip}
|
||||
rel="noreferrer noopenner"
|
||||
>
|
||||
Use external system
|
||||
<span slot="supporting-text"
|
||||
>Learn more about how to host it on another system</span
|
||||
<ha-button @click=${this._skip}>
|
||||
<ha-svg-icon .path=${mdiOpenInNew} slot="icon"></ha-svg-icon>
|
||||
Learn more</ha-button
|
||||
>
|
||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>`;
|
||||
</a>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private async _checkCloud() {
|
||||
@@ -217,10 +241,6 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
this._nextStep(STEP.CLOUD);
|
||||
}
|
||||
|
||||
private async _thisSystem() {
|
||||
this._nextStep(STEP.ADDONS);
|
||||
}
|
||||
|
||||
private _skip() {
|
||||
this._nextStep(STEP.SUCCESS);
|
||||
}
|
||||
@@ -232,21 +252,22 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
static styles = [
|
||||
AssistantSetupStyles,
|
||||
css`
|
||||
:host {
|
||||
padding: 0;
|
||||
.container {
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--divider-color);
|
||||
overflow: hidden;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.padding {
|
||||
padding: 24px;
|
||||
.container:last-child {
|
||||
margin-top: 16px;
|
||||
}
|
||||
ha-md-list {
|
||||
width: 100%;
|
||||
text-align: initial;
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
height: 152px;
|
||||
height: 195px;
|
||||
background: var(--input-fill-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.message {
|
||||
white-space: nowrap;
|
||||
@@ -259,21 +280,29 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: width 1s;
|
||||
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;
|
||||
float: var(--float-end);
|
||||
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;
|
||||
@@ -283,12 +312,15 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
margin-right: 24px;
|
||||
margin-inline-end: 24px;
|
||||
margin-inline-start: initial;
|
||||
float: var(--float-start);
|
||||
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;
|
||||
|
@@ -8,7 +8,6 @@ import "../../components/ha-tts-voice-picker";
|
||||
import {
|
||||
AssistPipeline,
|
||||
listAssistPipelines,
|
||||
setAssistPipelinePreferred,
|
||||
updateAssistPipeline,
|
||||
} from "../../data/assist_pipeline";
|
||||
import {
|
||||
@@ -17,13 +16,13 @@ import {
|
||||
setWakeWords,
|
||||
} from "../../data/assist_satellite";
|
||||
import { fetchCloudStatus } from "../../data/cloud";
|
||||
import { InputSelectEntity } from "../../data/input_select";
|
||||
import { setSelectOption } from "../../data/select";
|
||||
import { showVoiceAssistantPipelineDetailDialog } from "../../panels/config/voice-assistants/show-dialog-voice-assistant-pipeline-detail";
|
||||
import "../../panels/lovelace/entity-rows/hui-select-entity-row";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { AssistantSetupStyles } from "./styles";
|
||||
import { STEP } from "./voice-assistant-setup-dialog";
|
||||
import { setSelectOption } from "../../data/select";
|
||||
import { InputSelectEntity } from "../../data/input_select";
|
||||
|
||||
@customElement("ha-voice-assistant-setup-step-success")
|
||||
export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
||||
@@ -67,11 +66,11 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
||||
: undefined;
|
||||
|
||||
return html`<div class="content">
|
||||
<img src="/static/icons/casita/loving.png" />
|
||||
<h1>Ready to assist!</h1>
|
||||
<img src="/static/images/voice-assistant/heart.gif" />
|
||||
<h1>Ready to Assist!</h1>
|
||||
<p class="secondary">
|
||||
Your device is all ready to go! If you want to tweak some more
|
||||
settings, you can change that below.
|
||||
Make any final customizations here. You can always change these in the
|
||||
Voice Assistants section of the settings page.
|
||||
</p>
|
||||
<div class="rows">
|
||||
${this.assistConfiguration &&
|
||||
@@ -233,7 +232,7 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
||||
}
|
||||
|
||||
private async _openPipeline() {
|
||||
const [pipeline, preferred_pipeline] = await this._getPipeline();
|
||||
const [pipeline] = await this._getPipeline();
|
||||
|
||||
if (!pipeline) {
|
||||
return;
|
||||
@@ -245,13 +244,9 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
||||
cloudActiveSubscription:
|
||||
cloudStatus.logged_in && cloudStatus.active_subscription,
|
||||
pipeline,
|
||||
preferred: pipeline.id === preferred_pipeline,
|
||||
updatePipeline: async (values) => {
|
||||
await updateAssistPipeline(this.hass!, pipeline!.id, values);
|
||||
},
|
||||
setPipelinePreferred: async () => {
|
||||
await setAssistPipelinePreferred(this.hass!, pipeline!.id);
|
||||
},
|
||||
hideWakeWord: true,
|
||||
});
|
||||
}
|
||||
|
@@ -2,7 +2,13 @@ import { css, html, LitElement, nothing, PropertyValues } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-circular-progress";
|
||||
import { OFF, ON, UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { ON, UNAVAILABLE } from "../../data/entity";
|
||||
import {
|
||||
updateCanInstall,
|
||||
UpdateEntity,
|
||||
updateIsInstalling,
|
||||
updateUsesProgress,
|
||||
} from "../../data/update";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { AssistantSetupStyles } from "./styles";
|
||||
|
||||
@@ -51,17 +57,19 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[this.updateEntityId];
|
||||
const stateObj = this.hass.states[this.updateEntityId] as
|
||||
| UpdateEntity
|
||||
| undefined;
|
||||
|
||||
const progressIsNumeric =
|
||||
typeof stateObj?.attributes.in_progress === "number";
|
||||
const progressIsNumeric = stateObj && updateUsesProgress(stateObj);
|
||||
|
||||
return html`<div class="content">
|
||||
<img src="/static/icons/casita/loading.png" />
|
||||
<img src="/static/images/voice-assistant/update.gif" />
|
||||
<h1>
|
||||
${stateObj.state === OFF || stateObj.state === UNKNOWN
|
||||
? "Checking for updates"
|
||||
: "Updating your voice assistant"}
|
||||
${stateObj &&
|
||||
(stateObj.state === "unavailable" || updateIsInstalling(stateObj))
|
||||
? "Updating your voice assistant"
|
||||
: "Checking for updates"}
|
||||
</h1>
|
||||
<p class="secondary">
|
||||
We are making sure you have the latest and greatest version of your
|
||||
@@ -69,15 +77,15 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
|
||||
</p>
|
||||
<ha-circular-progress
|
||||
.value=${progressIsNumeric
|
||||
? stateObj.attributes.in_progress / 100
|
||||
? (stateObj.attributes.update_percentage as number) / 100
|
||||
: undefined}
|
||||
.indeterminate=${!progressIsNumeric}
|
||||
></ha-circular-progress>
|
||||
<p>
|
||||
${stateObj.state === "unavailable"
|
||||
${stateObj?.state === UNAVAILABLE
|
||||
? "Restarting voice assistant"
|
||||
: progressIsNumeric
|
||||
? `Installing ${stateObj.attributes.in_progress}%`
|
||||
? `Installing ${stateObj.attributes.update_percentage}%`
|
||||
: ""}
|
||||
</p>
|
||||
</div>`;
|
||||
@@ -88,8 +96,14 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
|
||||
if (!this.updateEntityId) {
|
||||
return;
|
||||
}
|
||||
const updateEntity = this.hass.states[this.updateEntityId];
|
||||
if (updateEntity && this.hass.states[updateEntity.entity_id].state === ON) {
|
||||
const updateEntity = this.hass.states[this.updateEntityId] as
|
||||
| UpdateEntity
|
||||
| undefined;
|
||||
if (
|
||||
updateEntity &&
|
||||
this.hass.states[updateEntity.entity_id].state === ON &&
|
||||
updateCanInstall(updateEntity)
|
||||
) {
|
||||
this._updated = true;
|
||||
await this.hass.callService(
|
||||
"update",
|
||||
|
@@ -65,14 +65,14 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
|
||||
return html`<div class="content">
|
||||
${!this._detected
|
||||
? html`
|
||||
<img src="/static/icons/casita/sleeping.png" />
|
||||
<img src="/static/images/voice-assistant/sleep.gif" />
|
||||
<h1>
|
||||
Say “${this._activeWakeWord(this.assistConfiguration)}” to wake the
|
||||
device up
|
||||
</h1>
|
||||
<p class="secondary">Setup will continue once the device is awake.</p>
|
||||
</div>`
|
||||
: html`<img src="/static/icons/casita/normal.png" />
|
||||
: html`<img src="/static/images/voice-assistant/ok-nabu.gif" />
|
||||
<h1>
|
||||
Say “${this._activeWakeWord(this.assistConfiguration)}” again
|
||||
</h1>
|
||||
@@ -80,7 +80,7 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
|
||||
To make sure the wake word works for you.
|
||||
</p>`}
|
||||
</div>
|
||||
<div class="footer full-width">
|
||||
<div class="footer centered">
|
||||
<ha-button @click=${this._changeWakeWord}>Change wake word</ha-button>
|
||||
</div>`;
|
||||
}
|
||||
|
@@ -125,7 +125,15 @@ class HassSubpage extends LitElement {
|
||||
.main-title {
|
||||
margin: var(--margin-title);
|
||||
line-height: 20px;
|
||||
min-width: 0;
|
||||
flex-grow: 1;
|
||||
overflow-wrap: break-word;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
.content {
|
||||
|
@@ -371,7 +371,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
>
|
||||
<div slot="headline">
|
||||
${localize(
|
||||
"ui.components.subpage-data-table.close_select_mode"
|
||||
"ui.components.subpage-data-table.exit_selection_mode"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
|
@@ -1,13 +1,15 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { property, state, query } from "lit/decorators";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { property, query, state } from "lit/decorators";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import "../components/ha-button";
|
||||
import "../components/ha-toast";
|
||||
import type { HaToast } from "../components/ha-toast";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "../components/ha-button";
|
||||
|
||||
export interface ShowToastParams {
|
||||
// Unique ID for the toast. If a new toast is shown with the same ID as the previous toast, it will be replaced to avoid flickering.
|
||||
id?: string;
|
||||
message: string;
|
||||
action?: ToastActionParams;
|
||||
duration?: number;
|
||||
@@ -27,12 +29,12 @@ class NotificationManager extends LitElement {
|
||||
@query("ha-toast") private _toast!: HaToast | undefined;
|
||||
|
||||
public async showDialog(parameters: ShowToastParams) {
|
||||
if (this._parameters && this._parameters.message !== parameters.message) {
|
||||
this._parameters = undefined;
|
||||
await this.updateComplete;
|
||||
if (!parameters.id || this._parameters?.id !== parameters.id) {
|
||||
this._toast?.close();
|
||||
}
|
||||
|
||||
if (!parameters || parameters.duration === 0) {
|
||||
this._parameters = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -44,10 +46,9 @@ class NotificationManager extends LitElement {
|
||||
) {
|
||||
this._parameters.duration = 4000;
|
||||
}
|
||||
}
|
||||
|
||||
public shouldUpdate(changedProperties) {
|
||||
return !this._toast || changedProperties.has("_parameters");
|
||||
await this.updateComplete;
|
||||
this._toast?.show();
|
||||
}
|
||||
|
||||
private _toastClosed() {
|
||||
@@ -61,7 +62,6 @@ class NotificationManager extends LitElement {
|
||||
return html`
|
||||
<ha-toast
|
||||
leading
|
||||
open
|
||||
dir=${computeRTL(this.hass) ? "rtl" : "ltr"}
|
||||
.labelText=${this._parameters.message}
|
||||
.timeoutMs=${this._parameters.duration!}
|
||||
@@ -77,12 +77,14 @@ class NotificationManager extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
${this._parameters?.dismissable
|
||||
? html`<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
slot="dismiss"
|
||||
></ha-icon-button>`
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
slot="dismiss"
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</ha-toast>
|
||||
`;
|
||||
|
@@ -43,8 +43,13 @@ import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import { ACTION_ICONS, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
|
||||
import { AutomationClipboard } from "../../../../data/automation";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import { fullEntitiesContext, labelsContext } from "../../../../data/context";
|
||||
import {
|
||||
floorsContext,
|
||||
fullEntitiesContext,
|
||||
labelsContext,
|
||||
} from "../../../../data/context";
|
||||
import { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||
import { FloorRegistryEntry } from "../../../../data/floor_registry";
|
||||
import { LabelRegistryEntry } from "../../../../data/label_registry";
|
||||
import {
|
||||
Action,
|
||||
@@ -154,6 +159,10 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
_labelReg!: LabelRegistryEntry[];
|
||||
|
||||
@state()
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
_floorReg!: { [id: string]: FloorRegistryEntry };
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@state() private _uiModeAvailable = true;
|
||||
@@ -222,6 +231,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
this._labelReg,
|
||||
this._floorReg,
|
||||
this.action
|
||||
)
|
||||
)}
|
||||
@@ -593,6 +603,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
this._labelReg,
|
||||
this._floorReg,
|
||||
this.action,
|
||||
undefined,
|
||||
true
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { consume } from "@lit-labs/context";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
@@ -56,6 +56,28 @@ export class HaDeviceAction extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
public shouldUpdate(changedProperties: PropertyValues) {
|
||||
if (!changedProperties.has("action")) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
this.action.device_id &&
|
||||
!(this.action.device_id in this.hass.devices)
|
||||
) {
|
||||
fireEvent(
|
||||
this,
|
||||
"ui-mode-not-available",
|
||||
Error(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.edit_unknown_device"
|
||||
)
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const deviceId = this._deviceId || this.action.device_id;
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { consume } from "@lit-labs/context";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
@@ -57,6 +57,28 @@ export class HaDeviceCondition extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
public shouldUpdate(changedProperties: PropertyValues) {
|
||||
if (!changedProperties.has("condition")) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
this.condition.device_id &&
|
||||
!(this.condition.device_id in this.hass.devices)
|
||||
) {
|
||||
fireEvent(
|
||||
this,
|
||||
"ui-mode-not-available",
|
||||
Error(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.edit_unknown_device"
|
||||
)
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const deviceId = this._deviceId || this.condition.device_id;
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { consume } from "@lit-labs/context";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
@@ -61,6 +61,28 @@ export class HaDeviceTrigger extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
public shouldUpdate(changedProperties: PropertyValues) {
|
||||
if (!changedProperties.has("trigger")) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
this.trigger.device_id &&
|
||||
!(this.trigger.device_id in this.hass.devices)
|
||||
) {
|
||||
fireEvent(
|
||||
this,
|
||||
"ui-mode-not-available",
|
||||
Error(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.edit_unknown_device"
|
||||
)
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const deviceId = this._deviceId || this.trigger.device_id;
|
||||
|
||||
|
@@ -251,9 +251,7 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
display: block;
|
||||
}
|
||||
ha-icon {
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
margin-left: -8px;
|
||||
}
|
||||
.entity-id {
|
||||
color: var(--secondary-text-color);
|
||||
@@ -283,6 +281,9 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
.name {
|
||||
font-size: 14px;
|
||||
}
|
||||
.name:dir(rtl) {
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
.empty {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -302,6 +303,9 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
outline: none;
|
||||
text-decoration: underline;
|
||||
}
|
||||
ha-list-item {
|
||||
height: 40px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -348,6 +348,8 @@ export class HaConfigDevicePage extends LitElement {
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
@error=${this._onImageError}
|
||||
@load=${this._onImageLoad}
|
||||
/>
|
||||
|
||||
${domainToName(this.hass.localize, integration.domain)}
|
||||
|