Compare commits
8 Commits
partial_te
...
delete-car
Author | SHA1 | Date | |
---|---|---|---|
![]() |
fa29e49985 | ||
![]() |
19c37ab91c | ||
![]() |
a9b4117b1b | ||
![]() |
24f9944319 | ||
![]() |
2518b1a79d | ||
![]() |
d24b9b6ced | ||
![]() |
97c69e620c | ||
![]() |
3bea09f161 |
8
.github/workflows/cast_deployment.yaml
vendored
@@ -21,12 +21,12 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.1
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.1.0
|
uses: actions/setup-node@v4.0.4
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -57,12 +57,12 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.1
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.1.0
|
uses: actions/setup-node@v4.0.4
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
18
.github/workflows/ci.yaml
vendored
@@ -24,9 +24,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.1
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.1.0
|
uses: actions/setup-node@v4.0.4
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
- name: Build resources
|
- name: Build resources
|
||||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||||
- name: Setup lint cache
|
- name: Setup lint cache
|
||||||
uses: actions/cache@v4.1.2
|
uses: actions/cache@v4.1.1
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
node_modules/.cache/prettier
|
node_modules/.cache/prettier
|
||||||
@@ -58,9 +58,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.1
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.1.0
|
uses: actions/setup-node@v4.0.4
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -76,9 +76,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.1
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.1.0
|
uses: actions/setup-node@v4.0.4
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -100,9 +100,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.1
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.1.0
|
uses: actions/setup-node@v4.0.4
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
2
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.1
|
||||||
with:
|
with:
|
||||||
# We must fetch at least the immediate parents so that if this is
|
# We must fetch at least the immediate parents so that if this is
|
||||||
# a pull request then we can checkout the head.
|
# a pull request then we can checkout the head.
|
||||||
|
8
.github/workflows/demo_deployment.yaml
vendored
@@ -22,12 +22,12 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.1
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.1.0
|
uses: actions/setup-node@v4.0.4
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -58,12 +58,12 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.1
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.1.0
|
uses: actions/setup-node@v4.0.4
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
4
.github/workflows/design_deployment.yaml
vendored
@@ -16,10 +16,10 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.1
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.1.0
|
uses: actions/setup-node@v4.0.4
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
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')
|
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.1
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.1.0
|
uses: actions/setup-node@v4.0.4
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
4
.github/workflows/nightly.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.1
|
||||||
|
|
||||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
python-version: ${{ env.PYTHON_VERSION }}
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.1.0
|
uses: actions/setup-node@v4.0.4
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
4
.github/workflows/release.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
contents: write # Required to upload release assets
|
contents: write # Required to upload release assets
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.1
|
||||||
|
|
||||||
- name: Verify version
|
- name: Verify version
|
||||||
uses: home-assistant/actions/helpers/verify-version@master
|
uses: home-assistant/actions/helpers/verify-version@master
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
python-version: ${{ env.PYTHON_VERSION }}
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.1.0
|
uses: actions/setup-node@v4.0.4
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
2
.github/workflows/translations.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.1
|
||||||
|
|
||||||
- name: Upload Translations
|
- name: Upload Translations
|
||||||
run: |
|
run: |
|
||||||
|
@@ -24,11 +24,8 @@ const convertToJSON = async (
|
|||||||
) => {
|
) => {
|
||||||
let localeData;
|
let localeData;
|
||||||
try {
|
try {
|
||||||
// use "pt" for "pt-BR", because "pt-BR" is unsupported by @formatjs
|
|
||||||
const language = lang === "pt-BR" ? "pt" : lang;
|
|
||||||
|
|
||||||
localeData = await readFile(
|
localeData = await readFile(
|
||||||
join(formatjsDir, pkg, subDir, `${language}.js`),
|
join(formatjsDir, pkg, subDir, `${lang}.js`),
|
||||||
"utf-8"
|
"utf-8"
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import "@material/mwc-button/mwc-button";
|
import "@material/mwc-button/mwc-button";
|
||||||
import "@material/mwc-list/mwc-list";
|
import { ActionDetail } from "@material/mwc-list/mwc-list";
|
||||||
import type { ActionDetail } from "@material/mwc-list/mwc-list";
|
|
||||||
import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js";
|
import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js";
|
||||||
import { Auth, Connection } from "home-assistant-js-websocket";
|
import { Auth, Connection } from "home-assistant-js-websocket";
|
||||||
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
||||||
@@ -90,8 +89,8 @@ class HcCast extends LitElement {
|
|||||||
generateDefaultViewConfig({}, {}, {}, {}, () => ""),
|
generateDefaultViewConfig({}, {}, {}, {}, () => ""),
|
||||||
]
|
]
|
||||||
).map(
|
).map(
|
||||||
(view, idx) => html`
|
(view, idx) =>
|
||||||
<ha-list-item
|
html`<ha-list-item
|
||||||
graphic="avatar"
|
graphic="avatar"
|
||||||
.activated=${this.castManager.status?.lovelacePath ===
|
.activated=${this.castManager.status?.lovelacePath ===
|
||||||
(view.path ?? idx)}
|
(view.path ?? idx)}
|
||||||
@@ -109,9 +108,8 @@ class HcCast extends LitElement {
|
|||||||
: html`<ha-svg-icon
|
: html`<ha-svg-icon
|
||||||
slot="item-icon"
|
slot="item-icon"
|
||||||
.path=${mdiViewDashboard}
|
.path=${mdiViewDashboard}
|
||||||
></ha-svg-icon>`}
|
></ha-svg-icon>`}</ha-list-item
|
||||||
</ha-list-item>
|
> `
|
||||||
`
|
|
||||||
)}</mwc-list
|
)}</mwc-list
|
||||||
>
|
>
|
||||||
`}
|
`}
|
||||||
|
@@ -88,7 +88,7 @@ class HcLayout extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
color: var(--ha-card-header-color, --primary-text-color);
|
||||||
font-family: var(--ha-card-header-font-family, inherit);
|
font-family: var(--ha-card-header-font-family, inherit);
|
||||||
font-size: var(--ha-card-header-font-size, 24px);
|
font-size: var(--ha-card-header-font-size, 24px);
|
||||||
letter-spacing: -0.012em;
|
letter-spacing: -0.012em;
|
||||||
|
@@ -1,11 +1,10 @@
|
|||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
import { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
|
import { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
|
||||||
import { getPanelTitleFromUrlPath } from "../../../../src/data/panel";
|
import { getPanelTitleFromUrlPath } from "../../../../src/data/panel";
|
||||||
import { Lovelace } from "../../../../src/panels/lovelace/types";
|
import { Lovelace } from "../../../../src/panels/lovelace/types";
|
||||||
import "../../../../src/panels/lovelace/views/hui-view";
|
import "../../../../src/panels/lovelace/views/hui-view";
|
||||||
import "../../../../src/panels/lovelace/views/hui-view-container";
|
|
||||||
import { HomeAssistant } from "../../../../src/types";
|
import { HomeAssistant } from "../../../../src/types";
|
||||||
import "./hc-launch-screen";
|
import "./hc-launch-screen";
|
||||||
|
|
||||||
@@ -23,6 +22,8 @@ class HcLovelace extends LitElement {
|
|||||||
|
|
||||||
@property() public urlPath: string | null = null;
|
@property() public urlPath: string | null = null;
|
||||||
|
|
||||||
|
@query("hui-view") private _huiView?: HTMLElement;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const index = this._viewIndex;
|
const index = this._viewIndex;
|
||||||
if (index === undefined) {
|
if (index === undefined) {
|
||||||
@@ -46,22 +47,12 @@ class HcLovelace extends LitElement {
|
|||||||
setEditMode: () => undefined,
|
setEditMode: () => undefined,
|
||||||
showToast: () => undefined,
|
showToast: () => undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const viewConfig = this.lovelaceConfig.views[index];
|
|
||||||
const background = viewConfig.background || this.lovelaceConfig.background;
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<hui-view-container
|
<hui-view
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.background=${background}
|
.lovelace=${lovelace}
|
||||||
.theme=${viewConfig.theme}
|
.index=${index}
|
||||||
>
|
></hui-view>
|
||||||
<hui-view
|
|
||||||
.hass=${this.hass}
|
|
||||||
.lovelace=${lovelace}
|
|
||||||
.index=${index}
|
|
||||||
></hui-view>
|
|
||||||
</hui-view-container>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +82,26 @@ class HcLovelace extends LitElement {
|
|||||||
}${viewTitle || ""}`
|
}${viewTitle || ""}`
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const configBackground =
|
||||||
|
this.lovelaceConfig.views[index].background ||
|
||||||
|
this.lovelaceConfig.background;
|
||||||
|
|
||||||
|
const backgroundStyle =
|
||||||
|
typeof configBackground === "string"
|
||||||
|
? configBackground
|
||||||
|
: configBackground?.image
|
||||||
|
? `center / cover no-repeat url('${configBackground.image}')`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (backgroundStyle) {
|
||||||
|
this._huiView!.style.setProperty(
|
||||||
|
"--lovelace-background",
|
||||||
|
backgroundStyle
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this._huiView!.style.removeProperty("--lovelace-background");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,15 +125,19 @@ class HcLovelace extends LitElement {
|
|||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
hui-view-container {
|
:host {
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
background: var(--primary-background-color);
|
||||||
|
}
|
||||||
|
:host > * {
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
hui-view {
|
hui-view {
|
||||||
flex: 1 1 100%;
|
background: var(--lovelace-background, var(--primary-background-color));
|
||||||
max-width: 100%;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -142,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
|||||||
<div class="action">
|
<div class="action">
|
||||||
<span>
|
<span>
|
||||||
${this._action
|
${this._action
|
||||||
? describeAction(this.hass, [], [], {}, this._action)
|
? describeAction(this.hass, [], [], this._action)
|
||||||
: "<invalid YAML>"}
|
: "<invalid YAML>"}
|
||||||
</span>
|
</span>
|
||||||
<ha-yaml-editor
|
<ha-yaml-editor
|
||||||
@@ -155,7 +155,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
|||||||
${ACTIONS.map(
|
${ACTIONS.map(
|
||||||
(conf) => html`
|
(conf) => html`
|
||||||
<div class="action">
|
<div class="action">
|
||||||
<span>${describeAction(this.hass, [], [], {}, conf as any)}</span>
|
<span>${describeAction(this.hass, [], [], conf as any)}</span>
|
||||||
<pre>${dump(conf)}</pre>
|
<pre>${dump(conf)}</pre>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
@@ -417,7 +417,7 @@ class HassioAddonConfig extends LitElement {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
.header h2 {
|
.header h2 {
|
||||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
color: var(--ha-card-header-color, --primary-text-color);
|
||||||
font-family: var(--ha-card-header-font-family, inherit);
|
font-family: var(--ha-card-header-font-family, inherit);
|
||||||
font-size: var(--ha-card-header-font-size, 24px);
|
font-size: var(--ha-card-header-font-size, 24px);
|
||||||
letter-spacing: -0.012em;
|
letter-spacing: -0.012em;
|
||||||
|
@@ -37,6 +37,7 @@ import "./config/hassio-addon-config";
|
|||||||
import "./config/hassio-addon-network";
|
import "./config/hassio-addon-network";
|
||||||
import "./hassio-addon-router";
|
import "./hassio-addon-router";
|
||||||
import "./info/hassio-addon-info";
|
import "./info/hassio-addon-info";
|
||||||
|
import "./log/hassio-addon-logs";
|
||||||
|
|
||||||
@customElement("hassio-addon-dashboard")
|
@customElement("hassio-addon-dashboard")
|
||||||
class HassioAddonDashboard extends LitElement {
|
class HassioAddonDashboard extends LitElement {
|
||||||
@@ -160,11 +161,16 @@ class HassioAddonDashboard extends LitElement {
|
|||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
width: 600px;
|
width: 600px;
|
||||||
}
|
}
|
||||||
|
hassio-addon-logs {
|
||||||
|
max-width: calc(100% - 8px);
|
||||||
|
min-width: 600px;
|
||||||
|
}
|
||||||
@media only screen and (max-width: 600px) {
|
@media only screen and (max-width: 600px) {
|
||||||
hassio-addon-info,
|
hassio-addon-info,
|
||||||
hassio-addon-network,
|
hassio-addon-network,
|
||||||
hassio-addon-audio,
|
hassio-addon-audio,
|
||||||
hassio-addon-config {
|
hassio-addon-config,
|
||||||
|
hassio-addon-logs {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
}
|
}
|
||||||
|
@@ -38,7 +38,6 @@ import "../../../../src/components/ha-markdown";
|
|||||||
import "../../../../src/components/ha-settings-row";
|
import "../../../../src/components/ha-settings-row";
|
||||||
import "../../../../src/components/ha-svg-icon";
|
import "../../../../src/components/ha-svg-icon";
|
||||||
import "../../../../src/components/ha-switch";
|
import "../../../../src/components/ha-switch";
|
||||||
import type { HaSwitch } from "../../../../src/components/ha-switch";
|
|
||||||
import {
|
import {
|
||||||
AddonCapability,
|
AddonCapability,
|
||||||
HassioAddonDetails,
|
HassioAddonDetails,
|
||||||
@@ -1120,28 +1119,12 @@ class HassioAddonInfo extends LitElement {
|
|||||||
private async _uninstallClicked(ev: CustomEvent): Promise<void> {
|
private async _uninstallClicked(ev: CustomEvent): Promise<void> {
|
||||||
const button = ev.currentTarget as any;
|
const button = ev.currentTarget as any;
|
||||||
button.progress = true;
|
button.progress = true;
|
||||||
let removeData = false;
|
|
||||||
const _removeDataToggled = (e: Event) => {
|
|
||||||
removeData = (e.target as HaSwitch).checked;
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmed = await showConfirmationDialog(this, {
|
const confirmed = await showConfirmationDialog(this, {
|
||||||
title: this.supervisor.localize("dialog.uninstall_addon.title", {
|
title: this.supervisor.localize("dialog.uninstall_addon.title", {
|
||||||
name: this.addon.name,
|
name: this.addon.name,
|
||||||
}),
|
}),
|
||||||
text: html`
|
text: this.supervisor.localize("dialog.uninstall_addon.text"),
|
||||||
<ha-formfield
|
|
||||||
.label=${html`<p>
|
|
||||||
${this.supervisor.localize("dialog.uninstall_addon.remove_data")}
|
|
||||||
</p>`}
|
|
||||||
>
|
|
||||||
<ha-switch
|
|
||||||
@change=${_removeDataToggled}
|
|
||||||
.checked=${removeData}
|
|
||||||
haptic
|
|
||||||
></ha-switch>
|
|
||||||
</ha-formfield>
|
|
||||||
`,
|
|
||||||
confirmText: this.supervisor.localize("dialog.uninstall_addon.uninstall"),
|
confirmText: this.supervisor.localize("dialog.uninstall_addon.uninstall"),
|
||||||
dismissText: this.supervisor.localize("common.cancel"),
|
dismissText: this.supervisor.localize("common.cancel"),
|
||||||
destructive: true,
|
destructive: true,
|
||||||
@@ -1154,7 +1137,7 @@ class HassioAddonInfo extends LitElement {
|
|||||||
|
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
try {
|
try {
|
||||||
await uninstallHassioAddon(this.hass, this.addon.slug, removeData);
|
await uninstallHassioAddon(this.hass, this.addon.slug);
|
||||||
const eventdata = {
|
const eventdata = {
|
||||||
success: true,
|
success: true,
|
||||||
response: undefined,
|
response: undefined,
|
||||||
@@ -1209,7 +1192,7 @@ class HassioAddonInfo extends LitElement {
|
|||||||
padding-inline-start: 8px;
|
padding-inline-start: 8px;
|
||||||
padding-inline-end: initial;
|
padding-inline-end: initial;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
color: var(--ha-card-header-color, --primary-text-color);
|
||||||
}
|
}
|
||||||
.addon-version {
|
.addon-version {
|
||||||
float: var(--float-end);
|
float: var(--float-end);
|
||||||
|
@@ -1,14 +1,12 @@
|
|||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import "../../../../src/components/ha-circular-progress";
|
import "../../../../src/components/ha-circular-progress";
|
||||||
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
|
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
|
||||||
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||||
import { haStyle } from "../../../../src/resources/styles";
|
import { haStyle } from "../../../../src/resources/styles";
|
||||||
import { HomeAssistant } from "../../../../src/types";
|
import { HomeAssistant } from "../../../../src/types";
|
||||||
import { hassioStyle } from "../../resources/hassio-style";
|
import { hassioStyle } from "../../resources/hassio-style";
|
||||||
import "../../../../src/panels/config/logs/error-log-card";
|
import "./hassio-addon-logs";
|
||||||
import "../../../../src/components/search-input";
|
|
||||||
import { extractSearchParam } from "../../../../src/common/url/search-params";
|
|
||||||
|
|
||||||
@customElement("hassio-addon-log-tab")
|
@customElement("hassio-addon-log-tab")
|
||||||
class HassioAddonLogDashboard extends LitElement {
|
class HassioAddonLogDashboard extends LitElement {
|
||||||
@@ -18,8 +16,6 @@ class HassioAddonLogDashboard extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public addon?: HassioAddonDetails;
|
@property({ attribute: false }) public addon?: HassioAddonDetails;
|
||||||
|
|
||||||
@state() private _filter = extractSearchParam("filter") || "";
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this.addon) {
|
if (!this.addon) {
|
||||||
return html`
|
return html`
|
||||||
@@ -27,31 +23,16 @@ class HassioAddonLogDashboard extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<div class="search">
|
|
||||||
<search-input
|
|
||||||
@value-changed=${this._filterChanged}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.filter=${this._filter}
|
|
||||||
.label=${this.hass.localize("ui.panel.config.logs.search")}
|
|
||||||
></search-input>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<error-log-card
|
<hassio-addon-logs
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.header=${this.addon.name}
|
.supervisor=${this.supervisor}
|
||||||
.provider=${this.addon.slug}
|
.addon=${this.addon}
|
||||||
show
|
></hassio-addon-logs>
|
||||||
.filter=${this._filter}
|
|
||||||
>
|
|
||||||
</error-log-card>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _filterChanged(ev) {
|
|
||||||
this._filter = ev.detail.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
@@ -60,21 +41,7 @@ class HassioAddonLogDashboard extends LitElement {
|
|||||||
.content {
|
.content {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
max-width: 1024px;
|
||||||
.search {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
search-input {
|
|
||||||
display: block;
|
|
||||||
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
|
||||||
--mdc-text-field-idle-line-color: var(--divider-color);
|
|
||||||
}
|
|
||||||
@media all and (max-width: 870px) {
|
|
||||||
:host {
|
|
||||||
--error-log-card-height: calc(100vh - 304px);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
90
hassio/src/addon-view/log/hassio-addon-logs.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import "@material/mwc-button";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import "../../../../src/components/ha-alert";
|
||||||
|
import "../../../../src/components/ha-ansi-to-html";
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
|
import {
|
||||||
|
fetchHassioAddonLogs,
|
||||||
|
HassioAddonDetails,
|
||||||
|
} from "../../../../src/data/hassio/addon";
|
||||||
|
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||||
|
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||||
|
import { haStyle } from "../../../../src/resources/styles";
|
||||||
|
import { HomeAssistant } from "../../../../src/types";
|
||||||
|
import { hassioStyle } from "../../resources/hassio-style";
|
||||||
|
|
||||||
|
@customElement("hassio-addon-logs")
|
||||||
|
class HassioAddonLogs extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
||||||
|
|
||||||
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
@state() private _content?: string;
|
||||||
|
|
||||||
|
public async connectedCallback(): Promise<void> {
|
||||||
|
super.connectedCallback();
|
||||||
|
await this._loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<h1>${this.addon.name}</h1>
|
||||||
|
<ha-card outlined>
|
||||||
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
|
: ""}
|
||||||
|
<div class="card-content">
|
||||||
|
${this._content
|
||||||
|
? html`<ha-ansi-to-html
|
||||||
|
.content=${this._content}
|
||||||
|
></ha-ansi-to-html>`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<mwc-button @click=${this._refresh}>
|
||||||
|
${this.supervisor.localize("common.refresh")}
|
||||||
|
</mwc-button>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
hassioStyle,
|
||||||
|
css`
|
||||||
|
:host,
|
||||||
|
ha-card {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _loadData(): Promise<void> {
|
||||||
|
this._error = undefined;
|
||||||
|
try {
|
||||||
|
this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
|
||||||
|
} catch (err: any) {
|
||||||
|
this._error = this.supervisor.localize("addon.logs.get_logs", {
|
||||||
|
error: extractApiErrorMessage(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _refresh(): Promise<void> {
|
||||||
|
await this._loadData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hassio-addon-logs": HassioAddonLogs;
|
||||||
|
}
|
||||||
|
}
|
@@ -48,7 +48,6 @@ import { showHassioBackupDialog } from "../dialogs/backup/show-dialog-hassio-bac
|
|||||||
import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hassio-create-backup";
|
import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hassio-create-backup";
|
||||||
import { supervisorTabs } from "../hassio-tabs";
|
import { supervisorTabs } from "../hassio-tabs";
|
||||||
import { hassioStyle } from "../resources/hassio-style";
|
import { hassioStyle } from "../resources/hassio-style";
|
||||||
import "../../../src/layouts/hass-loading-screen";
|
|
||||||
|
|
||||||
type BackupItem = HassioBackup & {
|
type BackupItem = HassioBackup & {
|
||||||
secondary: string;
|
secondary: string;
|
||||||
@@ -70,8 +69,6 @@ export class HassioBackups extends LitElement {
|
|||||||
|
|
||||||
@state() private _backups?: HassioBackup[] = [];
|
@state() private _backups?: HassioBackup[] = [];
|
||||||
|
|
||||||
@state() private _isLoading = false;
|
|
||||||
|
|
||||||
@query("hass-tabs-subpage-data-table", true)
|
@query("hass-tabs-subpage-data-table", true)
|
||||||
private _dataTable!: HaTabsSubpageDataTable;
|
private _dataTable!: HaTabsSubpageDataTable;
|
||||||
|
|
||||||
@@ -80,10 +77,15 @@ export class HassioBackups extends LitElement {
|
|||||||
public connectedCallback(): void {
|
public connectedCallback(): void {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
if (this.hass && this._firstUpdatedCalled) {
|
if (this.hass && this._firstUpdatedCalled) {
|
||||||
this.fetchBackups();
|
this.refreshData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async refreshData() {
|
||||||
|
await reloadHassioBackups(this.hass);
|
||||||
|
await this.fetchBackups();
|
||||||
|
}
|
||||||
|
|
||||||
private _computeBackupContent = (backup: HassioBackup): string => {
|
private _computeBackupContent = (backup: HassioBackup): string => {
|
||||||
if (backup.type === "full") {
|
if (backup.type === "full") {
|
||||||
return this.supervisor.localize("backup.full_backup");
|
return this.supervisor.localize("backup.full_backup");
|
||||||
@@ -113,7 +115,7 @@ export class HassioBackups extends LitElement {
|
|||||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||||
super.firstUpdated(changedProperties);
|
super.firstUpdated(changedProperties);
|
||||||
if (this.hass && this.isConnected) {
|
if (this.hass && this.isConnected) {
|
||||||
this.fetchBackups();
|
this.refreshData();
|
||||||
}
|
}
|
||||||
this._firstUpdatedCalled = true;
|
this._firstUpdatedCalled = true;
|
||||||
}
|
}
|
||||||
@@ -173,13 +175,6 @@ export class HassioBackups extends LitElement {
|
|||||||
if (!this.supervisor) {
|
if (!this.supervisor) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._isLoading) {
|
|
||||||
return html`<hass-loading-screen
|
|
||||||
.message=${this.supervisor.localize("backup.loading_backups")}
|
|
||||||
></hass-loading-screen>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<hass-tabs-subpage-data-table
|
<hass-tabs-subpage-data-table
|
||||||
.tabs=${atLeastVersion(this.hass.config.version, 2022, 5)
|
.tabs=${atLeastVersion(this.hass.config.version, 2022, 5)
|
||||||
@@ -286,7 +281,7 @@ export class HassioBackups extends LitElement {
|
|||||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||||
switch (ev.detail.index) {
|
switch (ev.detail.index) {
|
||||||
case 0:
|
case 0:
|
||||||
this.fetchBackups();
|
this.refreshData();
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
showHassioBackupLocationDialog(this, { supervisor: this.supervisor });
|
showHassioBackupLocationDialog(this, { supervisor: this.supervisor });
|
||||||
@@ -311,15 +306,13 @@ export class HassioBackups extends LitElement {
|
|||||||
supervisor: this.supervisor,
|
supervisor: this.supervisor,
|
||||||
onDelete: () => this.fetchBackups(),
|
onDelete: () => this.fetchBackups(),
|
||||||
}),
|
}),
|
||||||
reloadBackup: () => this.fetchBackups(),
|
reloadBackup: () => this.refreshData(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchBackups() {
|
private async fetchBackups() {
|
||||||
this._isLoading = true;
|
|
||||||
await reloadHassioBackups(this.hass);
|
await reloadHassioBackups(this.hass);
|
||||||
this._backups = await fetchHassioBackups(this.hass);
|
this._backups = await fetchHassioBackups(this.hass);
|
||||||
this._isLoading = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _deleteSelected() {
|
private async _deleteSelected() {
|
||||||
@@ -346,7 +339,8 @@ export class HassioBackups extends LitElement {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.fetchBackups();
|
await reloadHassioBackups(this.hass);
|
||||||
|
this._backups = await fetchHassioBackups(this.hass);
|
||||||
this._dataTable.clearSelection();
|
this._dataTable.clearSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -120,12 +120,10 @@ class HassioSupervisorLog extends LitElement {
|
|||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetchHassioLogs(
|
this._content = await fetchHassioLogs(
|
||||||
this.hass,
|
this.hass,
|
||||||
this._selectedLogProvider
|
this._selectedLogProvider
|
||||||
);
|
);
|
||||||
|
|
||||||
this._content = await response.text();
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this._error = this.supervisor.localize("system.log.get_logs", {
|
this._error = this.supervisor.localize("system.log.get_logs", {
|
||||||
provider: this._selectedLogProvider,
|
provider: this._selectedLogProvider,
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
|
import "@material/mwc-list/mwc-list-item";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
html,
|
html,
|
||||||
LitElement,
|
LitElement,
|
||||||
nothing,
|
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
|
nothing,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
@@ -15,12 +16,12 @@ import "../../../src/components/ha-button-menu";
|
|||||||
import "../../../src/components/ha-card";
|
import "../../../src/components/ha-card";
|
||||||
import "../../../src/components/ha-checkbox";
|
import "../../../src/components/ha-checkbox";
|
||||||
import "../../../src/components/ha-faded";
|
import "../../../src/components/ha-faded";
|
||||||
|
import "../../../src/components/ha-formfield";
|
||||||
import "../../../src/components/ha-icon-button";
|
import "../../../src/components/ha-icon-button";
|
||||||
import "../../../src/components/ha-markdown";
|
import "../../../src/components/ha-markdown";
|
||||||
import "../../../src/components/ha-settings-row";
|
import "../../../src/components/ha-settings-row";
|
||||||
import "../../../src/components/ha-svg-icon";
|
import "../../../src/components/ha-svg-icon";
|
||||||
import "../../../src/components/ha-switch";
|
import "../../../src/components/ha-switch";
|
||||||
import type { HaSwitch } from "../../../src/components/ha-switch";
|
|
||||||
import {
|
import {
|
||||||
fetchHassioAddonChangelog,
|
fetchHassioAddonChangelog,
|
||||||
fetchHassioAddonInfo,
|
fetchHassioAddonInfo,
|
||||||
@@ -41,7 +42,6 @@ import { updateCore } from "../../../src/data/supervisor/core";
|
|||||||
import { StoreAddon } from "../../../src/data/supervisor/store";
|
import { StoreAddon } from "../../../src/data/supervisor/store";
|
||||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||||
import { haStyle } from "../../../src/resources/styles";
|
|
||||||
import { HomeAssistant, Route } from "../../../src/types";
|
import { HomeAssistant, Route } from "../../../src/types";
|
||||||
import { addonArchIsSupported, extractChangelog } from "../util/addon";
|
import { addonArchIsSupported, extractChangelog } from "../util/addon";
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ class UpdateAvailableCard extends LitElement {
|
|||||||
</ha-markdown>
|
</ha-markdown>
|
||||||
</ha-faded>
|
</ha-faded>
|
||||||
`
|
`
|
||||||
: nothing}
|
: ""}
|
||||||
<div class="versions">
|
<div class="versions">
|
||||||
<p>
|
<p>
|
||||||
${this.supervisor.localize(
|
${this.supervisor.localize(
|
||||||
@@ -164,17 +164,15 @@ class UpdateAvailableCard extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
${["core", "addon"].includes(this._updateType)
|
${["core", "addon"].includes(this._updateType)
|
||||||
? html`
|
? html`
|
||||||
<hr />
|
<ha-formfield
|
||||||
<ha-settings-row>
|
.label=${this.supervisor.localize(
|
||||||
<span slot="heading">
|
"update_available.create_backup"
|
||||||
${this.supervisor.localize(
|
)}
|
||||||
"update_available.create_backup"
|
>
|
||||||
)}
|
<ha-checkbox checked></ha-checkbox>
|
||||||
</span>
|
</ha-formfield>
|
||||||
<ha-switch id="create_backup" checked></ha-switch>
|
|
||||||
</ha-settings-row>
|
|
||||||
`
|
`
|
||||||
: nothing}
|
: ""}
|
||||||
`
|
`
|
||||||
: html`<ha-circular-progress
|
: html`<ha-circular-progress
|
||||||
aria-label="Updating"
|
aria-label="Updating"
|
||||||
@@ -193,24 +191,22 @@ class UpdateAvailableCard extends LitElement {
|
|||||||
? html`
|
? html`
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
${changelog
|
${changelog
|
||||||
? html`
|
? html`<a .href=${changelog} target="_blank" rel="noreferrer">
|
||||||
<a href=${changelog} target="_blank" rel="noreferrer">
|
<mwc-button
|
||||||
<ha-button
|
.label=${this.supervisor.localize(
|
||||||
.label=${this.supervisor.localize(
|
"update_available.open_release_notes"
|
||||||
"update_available.open_release_notes"
|
)}
|
||||||
)}
|
>
|
||||||
>
|
</mwc-button>
|
||||||
</ha-button>
|
</a>`
|
||||||
</a>
|
: ""}
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
<span></span>
|
<span></span>
|
||||||
<ha-progress-button @click=${this._update}>
|
<ha-progress-button @click=${this._update} raised>
|
||||||
${this.supervisor.localize("common.update")}
|
${this.supervisor.localize("common.update")}
|
||||||
</ha-progress-button>
|
</ha-progress-button>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: ""}
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -246,11 +242,9 @@ class UpdateAvailableCard extends LitElement {
|
|||||||
if (this._updateType && !["core", "addon"].includes(this._updateType)) {
|
if (this._updateType && !["core", "addon"].includes(this._updateType)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const createBackupSwitch = this.shadowRoot?.getElementById(
|
const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
|
||||||
"create-backup"
|
if (checkbox) {
|
||||||
) as HaSwitch;
|
return checkbox.checked;
|
||||||
if (createBackupSwitch) {
|
|
||||||
return createBackupSwitch.checked;
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -403,50 +397,41 @@ class UpdateAvailableCard extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return css`
|
||||||
haStyle,
|
:host {
|
||||||
css`
|
display: block;
|
||||||
:host {
|
}
|
||||||
display: block;
|
ha-card {
|
||||||
}
|
margin: auto;
|
||||||
ha-card {
|
}
|
||||||
margin: auto;
|
a {
|
||||||
}
|
text-decoration: none;
|
||||||
a {
|
color: var(--primary-text-color);
|
||||||
text-decoration: none;
|
}
|
||||||
color: var(--primary-text-color);
|
ha-settings-row {
|
||||||
}
|
padding: 0;
|
||||||
.card-actions {
|
}
|
||||||
display: flex;
|
.card-actions {
|
||||||
justify-content: space-between;
|
display: flex;
|
||||||
}
|
justify-content: space-between;
|
||||||
|
border-top: none;
|
||||||
|
padding: 0 8px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
ha-circular-progress {
|
ha-circular-progress {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 32px;
|
margin: 32px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-text {
|
.progress-text {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-markdown {
|
ha-markdown {
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
`;
|
||||||
ha-settings-row {
|
|
||||||
padding: 0;
|
|
||||||
margin-bottom: -16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
border-color: var(--divider-color);
|
|
||||||
border-bottom: none;
|
|
||||||
margin: 16px 0 0 0;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
40
package.json
@@ -25,24 +25,24 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.26.0",
|
"@babel/runtime": "7.25.7",
|
||||||
"@braintree/sanitize-url": "7.1.0",
|
"@braintree/sanitize-url": "7.1.0",
|
||||||
"@codemirror/autocomplete": "6.18.1",
|
"@codemirror/autocomplete": "6.18.1",
|
||||||
"@codemirror/commands": "6.7.1",
|
"@codemirror/commands": "6.7.0",
|
||||||
"@codemirror/language": "6.10.3",
|
"@codemirror/language": "6.10.3",
|
||||||
"@codemirror/legacy-modes": "6.4.1",
|
"@codemirror/legacy-modes": "6.4.1",
|
||||||
"@codemirror/search": "6.5.6",
|
"@codemirror/search": "6.5.6",
|
||||||
"@codemirror/state": "6.4.1",
|
"@codemirror/state": "6.4.1",
|
||||||
"@codemirror/view": "6.34.1",
|
"@codemirror/view": "6.34.1",
|
||||||
"@egjs/hammerjs": "2.0.17",
|
"@egjs/hammerjs": "2.0.17",
|
||||||
"@formatjs/intl-datetimeformat": "6.16.1",
|
"@formatjs/intl-datetimeformat": "6.14.0",
|
||||||
"@formatjs/intl-displaynames": "6.8.1",
|
"@formatjs/intl-displaynames": "6.6.10",
|
||||||
"@formatjs/intl-getcanonicallocales": "2.5.1",
|
"@formatjs/intl-getcanonicallocales": "2.3.1",
|
||||||
"@formatjs/intl-listformat": "7.7.1",
|
"@formatjs/intl-listformat": "7.5.9",
|
||||||
"@formatjs/intl-locale": "4.2.1",
|
"@formatjs/intl-locale": "4.0.2",
|
||||||
"@formatjs/intl-numberformat": "8.14.1",
|
"@formatjs/intl-numberformat": "8.12.0",
|
||||||
"@formatjs/intl-pluralrules": "5.3.1",
|
"@formatjs/intl-pluralrules": "5.2.16",
|
||||||
"@formatjs/intl-relativetimeformat": "11.4.1",
|
"@formatjs/intl-relativetimeformat": "11.2.16",
|
||||||
"@fullcalendar/core": "6.1.15",
|
"@fullcalendar/core": "6.1.15",
|
||||||
"@fullcalendar/daygrid": "6.1.15",
|
"@fullcalendar/daygrid": "6.1.15",
|
||||||
"@fullcalendar/interaction": "6.1.15",
|
"@fullcalendar/interaction": "6.1.15",
|
||||||
@@ -89,8 +89,8 @@
|
|||||||
"@polymer/polymer": "3.5.2",
|
"@polymer/polymer": "3.5.2",
|
||||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||||
"@thomasloven/round-slider": "0.6.0",
|
"@thomasloven/round-slider": "0.6.0",
|
||||||
"@vaadin/combo-box": "24.5.1",
|
"@vaadin/combo-box": "24.5.0",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.5.1",
|
"@vaadin/vaadin-themable-mixin": "24.5.0",
|
||||||
"@vibrant/color": "3.2.1-alpha.1",
|
"@vibrant/color": "3.2.1-alpha.1",
|
||||||
"@vibrant/core": "3.2.1-alpha.1",
|
"@vibrant/core": "3.2.1-alpha.1",
|
||||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
||||||
"home-assistant-js-websocket": "9.4.0",
|
"home-assistant-js-websocket": "9.4.0",
|
||||||
"idb-keyval": "6.2.1",
|
"idb-keyval": "6.2.1",
|
||||||
"intl-messageformat": "10.7.3",
|
"intl-messageformat": "10.7.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"leaflet": "1.9.4",
|
"leaflet": "1.9.4",
|
||||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||||
@@ -151,12 +151,12 @@
|
|||||||
"xss": "1.0.15"
|
"xss": "1.0.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.26.0",
|
"@babel/core": "7.25.8",
|
||||||
"@babel/helper-define-polyfill-provider": "0.6.2",
|
"@babel/helper-define-polyfill-provider": "0.6.2",
|
||||||
"@babel/plugin-proposal-decorators": "7.25.9",
|
"@babel/plugin-proposal-decorators": "7.25.7",
|
||||||
"@babel/plugin-transform-runtime": "7.25.9",
|
"@babel/plugin-transform-runtime": "7.25.7",
|
||||||
"@babel/preset-env": "7.26.0",
|
"@babel/preset-env": "7.25.8",
|
||||||
"@babel/preset-typescript": "7.26.0",
|
"@babel/preset-typescript": "7.25.7",
|
||||||
"@bundle-stats/plugin-webpack-filter": "4.16.0",
|
"@bundle-stats/plugin-webpack-filter": "4.16.0",
|
||||||
"@koa/cors": "5.0.0",
|
"@koa/cors": "5.0.0",
|
||||||
"@lokalise/node-api": "12.8.0",
|
"@lokalise/node-api": "12.8.0",
|
||||||
@@ -176,7 +176,7 @@
|
|||||||
"@types/glob": "8.1.0",
|
"@types/glob": "8.1.0",
|
||||||
"@types/html-minifier-terser": "7.0.2",
|
"@types/html-minifier-terser": "7.0.2",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/leaflet": "1.9.14",
|
"@types/leaflet": "1.9.13",
|
||||||
"@types/leaflet-draw": "1.0.11",
|
"@types/leaflet-draw": "1.0.11",
|
||||||
"@types/lodash.merge": "4.6.9",
|
"@types/lodash.merge": "4.6.9",
|
||||||
"@types/luxon": "3.4.2",
|
"@types/luxon": "3.4.2",
|
||||||
@@ -194,7 +194,7 @@
|
|||||||
"babel-loader": "9.2.1",
|
"babel-loader": "9.2.1",
|
||||||
"babel-plugin-template-html-minifier": "4.1.0",
|
"babel-plugin-template-html-minifier": "4.1.0",
|
||||||
"browserslist-useragent-regexp": "4.1.3",
|
"browserslist-useragent-regexp": "4.1.3",
|
||||||
"chai": "5.1.2",
|
"chai": "5.1.1",
|
||||||
"del": "8.0.0",
|
"del": "8.0.0",
|
||||||
"eslint": "8.57.1",
|
"eslint": "8.57.1",
|
||||||
"eslint-config-airbnb-base": "15.0.0",
|
"eslint-config-airbnb-base": "15.0.0",
|
||||||
|
BIN
public/static/icons/casita/loading.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
public/static/icons/casita/loving.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
public/static/icons/casita/normal.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
public/static/icons/casita/sad.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
public/static/icons/casita/sleeping.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
public/static/icons/casita/smiling.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 372 KiB |
Before Width: | Height: | Size: 383 KiB |
Before Width: | Height: | Size: 377 KiB |
Before Width: | Height: | Size: 389 KiB |
Before Width: | Height: | Size: 379 KiB |
Before Width: | Height: | Size: 381 KiB |
Before Width: | Height: | Size: 374 KiB |
Before Width: | Height: | Size: 379 KiB |
@@ -34,11 +34,9 @@ export const protocolIntegrationPicked = async (
|
|||||||
if (domain === "zwave_js") {
|
if (domain === "zwave_js") {
|
||||||
const entries = options?.config_entry
|
const entries = options?.config_entry
|
||||||
? undefined
|
? undefined
|
||||||
: (
|
: await getConfigEntries(hass, {
|
||||||
await getConfigEntries(hass, {
|
domain,
|
||||||
domain,
|
});
|
||||||
})
|
|
||||||
).filter((e) => !e.disabled_by);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isComponentLoaded(hass, "zwave_js") ||
|
!isComponentLoaded(hass, "zwave_js") ||
|
||||||
@@ -83,11 +81,9 @@ export const protocolIntegrationPicked = async (
|
|||||||
} else if (domain === "zha") {
|
} else if (domain === "zha") {
|
||||||
const entries = options?.config_entry
|
const entries = options?.config_entry
|
||||||
? undefined
|
? undefined
|
||||||
: (
|
: await getConfigEntries(hass, {
|
||||||
await getConfigEntries(hass, {
|
domain,
|
||||||
domain,
|
});
|
||||||
})
|
|
||||||
).filter((e) => !e.disabled_by);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isComponentLoaded(hass, "zha") ||
|
!isComponentLoaded(hass, "zha") ||
|
||||||
@@ -133,11 +129,9 @@ export const protocolIntegrationPicked = async (
|
|||||||
} else if (domain === "matter") {
|
} else if (domain === "matter") {
|
||||||
const entries = options?.config_entry
|
const entries = options?.config_entry
|
||||||
? undefined
|
? undefined
|
||||||
: (
|
: await getConfigEntries(hass, {
|
||||||
await getConfigEntries(hass, {
|
domain,
|
||||||
domain,
|
});
|
||||||
})
|
|
||||||
).filter((e) => !e.disabled_by);
|
|
||||||
if (
|
if (
|
||||||
!isComponentLoaded(hass, domain) ||
|
!isComponentLoaded(hass, domain) ||
|
||||||
(!options?.config_entry && !entries?.length)
|
(!options?.config_entry && !entries?.length)
|
||||||
|
@@ -108,7 +108,6 @@ class HaDataTableLabels extends LitElement {
|
|||||||
ha-label {
|
ha-label {
|
||||||
--ha-label-background-color: var(--color, var(--grey-color));
|
--ha-label-background-color: var(--color, var(--grey-color));
|
||||||
--ha-label-background-opacity: 0.5;
|
--ha-label-background-opacity: 0.5;
|
||||||
outline: 1px solid var(--outline-color);
|
|
||||||
}
|
}
|
||||||
ha-button-menu {
|
ha-button-menu {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
@@ -254,7 +254,7 @@ class DateRangePickerElement extends WrappedElement {
|
|||||||
.daterangepicker select.hourselect,
|
.daterangepicker select.hourselect,
|
||||||
.daterangepicker select.minuteselect,
|
.daterangepicker select.minuteselect,
|
||||||
.daterangepicker select.secondselect {
|
.daterangepicker select.secondselect {
|
||||||
background: var(--card-background-color);
|
background: transparent;
|
||||||
border: 1px solid var(--divider-color);
|
border: 1px solid var(--divider-color);
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
@@ -1,17 +1,5 @@
|
|||||||
import {
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
css,
|
import { customElement, property } from "lit/decorators";
|
||||||
CSSResultGroup,
|
|
||||||
html,
|
|
||||||
LitElement,
|
|
||||||
PropertyValues,
|
|
||||||
TemplateResult,
|
|
||||||
} from "lit";
|
|
||||||
import {
|
|
||||||
customElement,
|
|
||||||
property,
|
|
||||||
query,
|
|
||||||
state as litState,
|
|
||||||
} from "lit/decorators";
|
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
bold: boolean;
|
bold: boolean;
|
||||||
@@ -23,24 +11,11 @@ interface State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@customElement("ha-ansi-to-html")
|
@customElement("ha-ansi-to-html")
|
||||||
export class HaAnsiToHtml extends LitElement {
|
class HaAnsiToHtml extends LitElement {
|
||||||
@property() public content!: string;
|
@property() public content!: string;
|
||||||
|
|
||||||
@query("pre") private _pre?: HTMLPreElement;
|
|
||||||
|
|
||||||
@litState() private _filter = "";
|
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
return html`<pre></pre>`;
|
return html`${this._parseTextToColoredPre(this.content)}`;
|
||||||
}
|
|
||||||
|
|
||||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
|
||||||
super.firstUpdated(_changedProperties);
|
|
||||||
|
|
||||||
// handle initial content
|
|
||||||
if (this.content) {
|
|
||||||
this.parseTextToColoredPre(this.content);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
@@ -49,7 +24,6 @@ export class HaAnsiToHtml extends LitElement {
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
.bold {
|
.bold {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -111,33 +85,11 @@ export class HaAnsiToHtml extends LitElement {
|
|||||||
.bg-white {
|
.bg-white {
|
||||||
background-color: rgb(204, 204, 204);
|
background-color: rgb(204, 204, 204);
|
||||||
}
|
}
|
||||||
|
|
||||||
::highlight(search-results) {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: var(--text-primary-color);
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private _parseTextToColoredPre(text) {
|
||||||
* add new lines to the log
|
const pre = document.createElement("pre");
|
||||||
* @param lines log lines
|
|
||||||
* @param top should the new lines be added to the top of the log
|
|
||||||
*/
|
|
||||||
public parseLinesToColoredPre(lines: string[], top = false) {
|
|
||||||
for (const line of lines) {
|
|
||||||
this.parseLineToColoredPre(line, top);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a single line to the log
|
|
||||||
* @param line log line
|
|
||||||
* @param top should the new line be added to the top of the log
|
|
||||||
*/
|
|
||||||
public parseLineToColoredPre(line, top = false) {
|
|
||||||
const lineDiv = document.createElement("div");
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-control-regex
|
// eslint-disable-next-line no-control-regex
|
||||||
const re = /\x1b(?:\[(.*?)[@-~]|\].*?(?:\x07|\x1b\\))/g;
|
const re = /\x1b(?:\[(.*?)[@-~]|\].*?(?:\x07|\x1b\\))/g;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
@@ -151,7 +103,7 @@ export class HaAnsiToHtml extends LitElement {
|
|||||||
backgroundColor: null,
|
backgroundColor: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const addPart = (content) => {
|
const addSpan = (content) => {
|
||||||
const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
if (state.bold) {
|
if (state.bold) {
|
||||||
span.classList.add("bold");
|
span.classList.add("bold");
|
||||||
@@ -172,18 +124,15 @@ export class HaAnsiToHtml extends LitElement {
|
|||||||
span.classList.add(`bg-${state.backgroundColor}`);
|
span.classList.add(`bg-${state.backgroundColor}`);
|
||||||
}
|
}
|
||||||
span.appendChild(document.createTextNode(content));
|
span.appendChild(document.createTextNode(content));
|
||||||
lineDiv.appendChild(span);
|
pre.appendChild(span);
|
||||||
};
|
};
|
||||||
|
|
||||||
/* eslint-disable no-cond-assign */
|
/* eslint-disable no-cond-assign */
|
||||||
let match;
|
let match;
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
while ((match = re.exec(line)) !== null) {
|
while ((match = re.exec(text)) !== null) {
|
||||||
const j = match!.index;
|
const j = match!.index;
|
||||||
const substring = line.substring(i, j);
|
addSpan(text.substring(i, j));
|
||||||
if (substring) {
|
|
||||||
addPart(substring);
|
|
||||||
}
|
|
||||||
i = j + match[0].length;
|
i = j + match[0].length;
|
||||||
|
|
||||||
if (match[1] === undefined) {
|
if (match[1] === undefined) {
|
||||||
@@ -285,93 +234,9 @@ export class HaAnsiToHtml extends LitElement {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
addSpan(text.substring(i));
|
||||||
|
|
||||||
const substring = line.substring(i);
|
return pre;
|
||||||
if (substring) {
|
|
||||||
addPart(substring);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (top) {
|
|
||||||
this._pre?.prepend(lineDiv);
|
|
||||||
lineDiv.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 500 });
|
|
||||||
} else {
|
|
||||||
this._pre?.appendChild(lineDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter new lines if a filter is set
|
|
||||||
if (this._filter) {
|
|
||||||
this.filterLines(this._filter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public parseTextToColoredPre(text) {
|
|
||||||
const lines = text.split("\n");
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
this.parseLineToColoredPre(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter lines based on a search string, lines and search string will be converted to lowercase
|
|
||||||
* @param filter the search string
|
|
||||||
* @returns true if there are lines to display
|
|
||||||
*/
|
|
||||||
filterLines(filter: string): boolean {
|
|
||||||
this._filter = filter;
|
|
||||||
const lines = this.shadowRoot?.querySelectorAll("div") || [];
|
|
||||||
let numberOfFoundLines = 0;
|
|
||||||
if (!filter) {
|
|
||||||
lines.forEach((line) => {
|
|
||||||
line.style.display = "";
|
|
||||||
});
|
|
||||||
numberOfFoundLines = lines.length;
|
|
||||||
if (CSS.highlights) {
|
|
||||||
CSS.highlights.delete("search-results");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const highlightRanges: Range[] = [];
|
|
||||||
lines.forEach((line) => {
|
|
||||||
if (!line.textContent?.toLowerCase().includes(filter.toLowerCase())) {
|
|
||||||
line.style.display = "none";
|
|
||||||
} else {
|
|
||||||
line.style.display = "";
|
|
||||||
numberOfFoundLines++;
|
|
||||||
if (CSS.highlights && line.firstChild !== null && line.textContent) {
|
|
||||||
const spansOfLine = line.querySelectorAll("span");
|
|
||||||
spansOfLine.forEach((span) => {
|
|
||||||
const text = span.textContent.toLowerCase();
|
|
||||||
const indices: number[] = [];
|
|
||||||
let startPos = 0;
|
|
||||||
while (startPos < text.length) {
|
|
||||||
const index = text.indexOf(filter.toLowerCase(), startPos);
|
|
||||||
if (index === -1) break;
|
|
||||||
indices.push(index);
|
|
||||||
startPos = index + filter.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
indices.forEach((index) => {
|
|
||||||
const range = new Range();
|
|
||||||
range.setStart(span.firstChild!, index);
|
|
||||||
range.setEnd(span.firstChild!, index + filter.length);
|
|
||||||
highlightRanges.push(range);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (CSS.highlights) {
|
|
||||||
CSS.highlights.set("search-results", new Highlight(...highlightRanges));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return !!numberOfFoundLines;
|
|
||||||
}
|
|
||||||
|
|
||||||
public clear() {
|
|
||||||
if (this._pre) {
|
|
||||||
this._pre.innerHTML = "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -43,7 +43,7 @@ export class HaCard extends LitElement {
|
|||||||
|
|
||||||
.card-header,
|
.card-header,
|
||||||
:host ::slotted(.card-header) {
|
:host ::slotted(.card-header) {
|
||||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
color: var(--ha-card-header-color, --primary-text-color);
|
||||||
font-family: var(--ha-card-header-font-family, inherit);
|
font-family: var(--ha-card-header-font-family, inherit);
|
||||||
font-size: var(--ha-card-header-font-size, 24px);
|
font-size: var(--ha-card-header-font-size, 24px);
|
||||||
letter-spacing: -0.012em;
|
letter-spacing: -0.012em;
|
||||||
|
@@ -46,7 +46,7 @@ export class HaHeaderBar extends LitElement {
|
|||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
.mdc-top-app-bar__title {
|
.mdc-top-app-bar__title {
|
||||||
padding-inline-start: 24px;
|
padding-inline-start: 20px;
|
||||||
padding-inline-end: initial;
|
padding-inline-end: initial;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
@@ -216,7 +216,6 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
|
|||||||
ha-input-chip {
|
ha-input-chip {
|
||||||
--md-input-chip-selected-container-color: var(--color, var(--grey-color));
|
--md-input-chip-selected-container-color: var(--color, var(--grey-color));
|
||||||
--ha-input-chip-selected-container-opacity: 0.5;
|
--ha-input-chip-selected-container-opacity: 0.5;
|
||||||
--md-input-chip-selected-outline-width: 1px;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -86,11 +86,6 @@ export class HaMarkdown extends LitElement {
|
|||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
hr {
|
|
||||||
border-color: var(--divider-color);
|
|
||||||
border-bottom: none;
|
|
||||||
margin: 16px 0;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { PropertyValues, ReactiveElement } from "lit";
|
import { PropertyValues, ReactiveElement } from "lit";
|
||||||
import { parseISO } from "date-fns";
|
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { relativeTime } from "../common/datetime/relative_time";
|
import { relativeTime } from "../common/datetime/relative_time";
|
||||||
import { capitalizeFirstLetter } from "../common/string/capitalize-first-letter";
|
import { capitalizeFirstLetter } from "../common/string/capitalize-first-letter";
|
||||||
@@ -59,12 +58,7 @@ class HaRelativeTime extends ReactiveElement {
|
|||||||
if (!this.datetime) {
|
if (!this.datetime) {
|
||||||
this.innerHTML = this.hass.localize("ui.components.relative_time.never");
|
this.innerHTML = this.hass.localize("ui.components.relative_time.never");
|
||||||
} else {
|
} else {
|
||||||
const date =
|
const relTime = relativeTime(new Date(this.datetime), this.hass.locale);
|
||||||
typeof this.datetime === "string"
|
|
||||||
? parseISO(this.datetime)
|
|
||||||
: this.datetime;
|
|
||||||
|
|
||||||
const relTime = relativeTime(date, this.hass.locale);
|
|
||||||
this.innerHTML = this.capitalize
|
this.innerHTML = this.capitalize
|
||||||
? capitalizeFirstLetter(relTime)
|
? capitalizeFirstLetter(relTime)
|
||||||
: relTime;
|
: relTime;
|
||||||
|
@@ -34,7 +34,6 @@ import {
|
|||||||
expandLabelTarget,
|
expandLabelTarget,
|
||||||
Selector,
|
Selector,
|
||||||
TargetSelector,
|
TargetSelector,
|
||||||
TemplateSelector,
|
|
||||||
} from "../data/selector";
|
} from "../data/selector";
|
||||||
import { HomeAssistant, ValueChangedEvent } from "../types";
|
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
import { documentationUrl } from "../util/documentation-url";
|
import { documentationUrl } from "../util/documentation-url";
|
||||||
@@ -46,7 +45,6 @@ import "./ha-settings-row";
|
|||||||
import "./ha-yaml-editor";
|
import "./ha-yaml-editor";
|
||||||
import type { HaYamlEditor } from "./ha-yaml-editor";
|
import type { HaYamlEditor } from "./ha-yaml-editor";
|
||||||
import "./ha-service-section-icon";
|
import "./ha-service-section-icon";
|
||||||
import { hasTemplate } from "../common/string/has-template";
|
|
||||||
|
|
||||||
const attributeFilter = (values: any[], attribute: any) => {
|
const attributeFilter = (values: any[], attribute: any) => {
|
||||||
if (typeof attribute === "object") {
|
if (typeof attribute === "object") {
|
||||||
@@ -63,11 +61,6 @@ const showOptionalToggle = (field) =>
|
|||||||
!field.required &&
|
!field.required &&
|
||||||
!("boolean" in field.selector && field.default);
|
!("boolean" in field.selector && field.default);
|
||||||
|
|
||||||
interface Field extends Omit<HassService["fields"][string], "selector"> {
|
|
||||||
key: string;
|
|
||||||
selector?: Selector;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExtHassService extends Omit<HassService, "fields"> {
|
interface ExtHassService extends Omit<HassService, "fields"> {
|
||||||
fields: Array<
|
fields: Array<
|
||||||
Omit<HassService["fields"][string], "selector"> & {
|
Omit<HassService["fields"][string], "selector"> & {
|
||||||
@@ -77,12 +70,9 @@ interface ExtHassService extends Omit<HassService, "fields"> {
|
|||||||
collapsed?: boolean;
|
collapsed?: boolean;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
flatFields: Array<Field>;
|
|
||||||
hasSelector: string[];
|
hasSelector: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const TEMPLATE_SELECTOR: TemplateSelector = { template: {} };
|
|
||||||
|
|
||||||
@customElement("ha-service-control")
|
@customElement("ha-service-control")
|
||||||
export class HaServiceControl extends LitElement {
|
export class HaServiceControl extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -187,7 +177,7 @@ export class HaServiceControl extends LitElement {
|
|||||||
if (!this._value.data) {
|
if (!this._value.data) {
|
||||||
this._value.data = {};
|
this._value.data = {};
|
||||||
}
|
}
|
||||||
serviceData.flatFields.forEach((field) => {
|
serviceData.fields.forEach((field) => {
|
||||||
if (
|
if (
|
||||||
field.selector &&
|
field.selector &&
|
||||||
field.required &&
|
field.required &&
|
||||||
@@ -251,28 +241,22 @@ export class HaServiceControl extends LitElement {
|
|||||||
selector: value.selector as Selector | undefined,
|
selector: value.selector as Selector | undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const flatFields: Field[] = [];
|
|
||||||
const hasSelector: string[] = [];
|
const hasSelector: string[] = [];
|
||||||
fields.forEach((field) => {
|
fields.forEach((field) => {
|
||||||
if ((field as any).fields) {
|
if ((field as any).fields) {
|
||||||
Object.entries((field as any).fields).forEach(([key, subField]) => {
|
Object.entries((field as any).fields).forEach(([key, subField]) => {
|
||||||
flatFields.push({ ...(subField as Field), key });
|
|
||||||
if ((subField as any).selector) {
|
if ((subField as any).selector) {
|
||||||
hasSelector.push(key);
|
hasSelector.push(key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else if (field.selector) {
|
||||||
flatFields.push(field);
|
hasSelector.push(field.key);
|
||||||
if (field.selector) {
|
|
||||||
hasSelector.push(field.key);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...serviceDomains[domain][serviceName],
|
...serviceDomains[domain][serviceName],
|
||||||
fields,
|
fields,
|
||||||
flatFields,
|
|
||||||
hasSelector,
|
hasSelector,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -413,7 +397,7 @@ export class HaServiceControl extends LitElement {
|
|||||||
|
|
||||||
const hasOptional = Boolean(
|
const hasOptional = Boolean(
|
||||||
!shouldRenderServiceDataYaml &&
|
!shouldRenderServiceDataYaml &&
|
||||||
serviceData?.flatFields.some((field) => showOptionalToggle(field))
|
serviceData?.fields.some((field) => showOptionalToggle(field))
|
||||||
);
|
);
|
||||||
|
|
||||||
const targetEntities = this._getTargetedEntities(
|
const targetEntities = this._getTargetedEntities(
|
||||||
@@ -482,8 +466,7 @@ export class HaServiceControl extends LitElement {
|
|||||||
>${this.hass.localize(
|
>${this.hass.localize(
|
||||||
"ui.components.service-control.target_secondary"
|
"ui.components.service-control.target_secondary"
|
||||||
)}</span
|
)}</span
|
||||||
>
|
><ha-selector
|
||||||
<ha-selector
|
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.selector=${this._targetSelector(
|
.selector=${this._targetSelector(
|
||||||
serviceData.target as TargetSelector
|
serviceData.target as TargetSelector
|
||||||
@@ -644,34 +627,23 @@ export class HaServiceControl extends LitElement {
|
|||||||
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
|
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
|
||||||
) || dataField?.description}</span
|
) || dataField?.description}</span
|
||||||
>
|
>
|
||||||
${hasTemplate(this._value?.data?.[dataField.key])
|
<ha-selector
|
||||||
? html`
|
.disabled=${this.disabled ||
|
||||||
<ha-selector
|
(showOptional &&
|
||||||
.selector=${TEMPLATE_SELECTOR}
|
!this._checkedKeys.has(dataField.key) &&
|
||||||
.key=${dataField.key}
|
(!this._value?.data ||
|
||||||
.hass=${this.hass}
|
this._value.data[dataField.key] === undefined))}
|
||||||
.value=${this._value?.data?.[dataField.key]}
|
.hass=${this.hass}
|
||||||
.disabled=${this.disabled}
|
.selector=${enhancedSelector}
|
||||||
@value-changed=${this._serviceDataChanged}
|
.key=${dataField.key}
|
||||||
></ha-selector>
|
@value-changed=${this._serviceDataChanged}
|
||||||
`
|
.value=${this._value?.data
|
||||||
: html`
|
? this._value.data[dataField.key]
|
||||||
<ha-selector
|
: undefined}
|
||||||
.disabled=${this.disabled ||
|
.placeholder=${dataField.default}
|
||||||
(showOptional &&
|
.localizeValue=${this._localizeValueCallback}
|
||||||
!this._checkedKeys.has(dataField.key) &&
|
@item-moved=${this._itemMoved}
|
||||||
(!this._value?.data ||
|
></ha-selector>
|
||||||
this._value.data[dataField.key] === undefined))}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.selector=${enhancedSelector}
|
|
||||||
.key=${dataField.key}
|
|
||||||
@value-changed=${this._serviceDataChanged}
|
|
||||||
.value=${this._value?.data?.[dataField.key]}
|
|
||||||
.placeholder=${dataField.default}
|
|
||||||
.localizeValue=${this._localizeValueCallback}
|
|
||||||
@item-moved=${this._itemMoved}
|
|
||||||
></ha-selector>
|
|
||||||
`}
|
|
||||||
</ha-settings-row>`
|
</ha-settings-row>`
|
||||||
: "";
|
: "";
|
||||||
};
|
};
|
||||||
@@ -695,7 +667,7 @@ export class HaServiceControl extends LitElement {
|
|||||||
const field = this._getServiceInfo(
|
const field = this._getServiceInfo(
|
||||||
this._value?.action,
|
this._value?.action,
|
||||||
this.hass.services
|
this.hass.services
|
||||||
)?.flatFields.find((_field) => _field.key === key);
|
)?.fields.find((_field) => _field.key === key);
|
||||||
|
|
||||||
let defaultValue = field?.default;
|
let defaultValue = field?.default;
|
||||||
|
|
||||||
|
@@ -42,17 +42,14 @@ export class HaSettingsRow extends LitElement {
|
|||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-inline-start: 0;
|
padding-inline-start: 0;
|
||||||
padding-right: 16px;
|
padding-right: 16x;
|
||||||
padding-inline-end: 16px;
|
padding-inline-end: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: var(--layout-vertical_-_display, flex);
|
display: var(--layout-vertical_-_display);
|
||||||
flex-direction: var(--layout-vertical_-_flex-direction, column);
|
flex-direction: var(--layout-vertical_-_flex-direction);
|
||||||
justify-content: var(
|
justify-content: var(--layout-center-justified_-_justify-content);
|
||||||
--layout-center-justified_-_justify-content,
|
flex: var(--layout-flex_-_flex);
|
||||||
center
|
flex-basis: var(--layout-flex_-_flex-basis);
|
||||||
);
|
|
||||||
flex: var(--layout-flex_-_flex, 1);
|
|
||||||
flex-basis: var(--layout-flex_-_flex-basis, 0.000000001px);
|
|
||||||
}
|
}
|
||||||
.body[three-line] {
|
.body[three-line] {
|
||||||
min-height: var(--paper-item-body-three-line-min-height, 88px);
|
min-height: var(--paper-item-body-three-line-min-height, 88px);
|
||||||
|
@@ -859,14 +859,11 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
border-bottom: 1px solid transparent;
|
border-bottom: 1px solid transparent;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: var(
|
color: var(--sidebar-menu-button-text-color, --primary-text-color);
|
||||||
--sidebar-menu-button-text-color,
|
|
||||||
var(--primary-text-color)
|
|
||||||
);
|
|
||||||
border-bottom: 1px solid var(--divider-color);
|
border-bottom: 1px solid var(--divider-color);
|
||||||
background-color: var(
|
background-color: var(
|
||||||
--sidebar-menu-button-background-color,
|
--sidebar-menu-button-background-color,
|
||||||
var(--primary-background-color)
|
--primary-background-color
|
||||||
);
|
);
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@@ -24,7 +24,7 @@ export class HaTopAppBarFixed extends TopAppBarFixedBase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
.mdc-top-app-bar__title {
|
.mdc-top-app-bar__title {
|
||||||
padding-inline-start: 24px;
|
padding-inline-start: 20px;
|
||||||
padding-inline-end: initial;
|
padding-inline-end: initial;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
@@ -321,7 +321,7 @@ export class TopAppBarBaseBase extends BaseElement {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
.mdc-top-app-bar__title {
|
.mdc-top-app-bar__title {
|
||||||
padding-inline-start: 24px;
|
padding-inline-start: 20px;
|
||||||
padding-inline-end: initial;
|
padding-inline-end: initial;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
@@ -12,12 +11,9 @@ import { customElement, property, query, state } from "lit/decorators";
|
|||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import {
|
import {
|
||||||
addWebRtcCandidate,
|
|
||||||
fetchWebRtcClientConfiguration,
|
fetchWebRtcClientConfiguration,
|
||||||
|
handleWebRtcOffer,
|
||||||
WebRtcAnswer,
|
WebRtcAnswer,
|
||||||
WebRTCClientConfiguration,
|
|
||||||
webRtcOffer,
|
|
||||||
WebRtcOfferEvent,
|
|
||||||
} from "../data/camera";
|
} from "../data/camera";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-alert";
|
import "./ha-alert";
|
||||||
@@ -31,7 +27,7 @@ import "./ha-alert";
|
|||||||
class HaWebRtcPlayer extends LitElement {
|
class HaWebRtcPlayer extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property() public entityid?: string;
|
@property() public entityid!: string;
|
||||||
|
|
||||||
@property({ type: Boolean, attribute: "controls" })
|
@property({ type: Boolean, attribute: "controls" })
|
||||||
public controls = false;
|
public controls = false;
|
||||||
@@ -49,20 +45,12 @@ class HaWebRtcPlayer extends LitElement {
|
|||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
@query("#remote-stream") private _videoEl!: HTMLVideoElement;
|
@query("#remote-stream", true) private _videoEl!: HTMLVideoElement;
|
||||||
|
|
||||||
private _clientConfig?: WebRTCClientConfiguration;
|
|
||||||
|
|
||||||
private _peerConnection?: RTCPeerConnection;
|
private _peerConnection?: RTCPeerConnection;
|
||||||
|
|
||||||
private _remoteStream?: MediaStream;
|
private _remoteStream?: MediaStream;
|
||||||
|
|
||||||
private _unsub?: Promise<UnsubscribeFunc>;
|
|
||||||
|
|
||||||
private _sessionId?: string;
|
|
||||||
|
|
||||||
private _candidatesList: string[] = [];
|
|
||||||
|
|
||||||
protected override render(): TemplateResult {
|
protected override render(): TemplateResult {
|
||||||
if (this._error) {
|
if (this._error) {
|
||||||
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
|
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
|
||||||
@@ -82,7 +70,7 @@ class HaWebRtcPlayer extends LitElement {
|
|||||||
|
|
||||||
public override connectedCallback() {
|
public override connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
if (this.hasUpdated && this.entityid) {
|
if (this.hasUpdated) {
|
||||||
this._startWebRtc();
|
this._startWebRtc();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,8 +80,7 @@ class HaWebRtcPlayer extends LitElement {
|
|||||||
this._cleanUp();
|
this._cleanUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override willUpdate(changedProperties: PropertyValues<this>) {
|
protected override updated(changedProperties: PropertyValues<this>) {
|
||||||
super.willUpdate(changedProperties);
|
|
||||||
if (!changedProperties.has("entityid")) {
|
if (!changedProperties.has("entityid")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -101,68 +88,28 @@ class HaWebRtcPlayer extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _startWebRtc(): Promise<void> {
|
private async _startWebRtc(): Promise<void> {
|
||||||
this._cleanUp();
|
|
||||||
|
|
||||||
if (!this.hass || !this.entityid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.time("WebRTC");
|
console.time("WebRTC");
|
||||||
|
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
|
|
||||||
console.timeLog("WebRTC", "start clientConfig");
|
console.timeLog("WebRTC", "start clientConfig");
|
||||||
|
|
||||||
this._clientConfig = await fetchWebRtcClientConfiguration(
|
const clientConfig = await fetchWebRtcClientConfiguration(
|
||||||
this.hass,
|
this.hass,
|
||||||
this.entityid
|
this.entityid
|
||||||
);
|
);
|
||||||
|
|
||||||
console.timeLog("WebRTC", "end clientConfig", this._clientConfig);
|
console.timeLog("WebRTC", "end clientConfig", clientConfig);
|
||||||
|
|
||||||
this._peerConnection = new RTCPeerConnection(
|
const peerConnection = new RTCPeerConnection(clientConfig.configuration);
|
||||||
this._clientConfig.configuration
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this._clientConfig.dataChannel) {
|
if (clientConfig.dataChannel) {
|
||||||
// Some cameras (such as nest) require a data channel to establish a stream
|
// Some cameras (such as nest) require a data channel to establish a stream
|
||||||
// however, not used by any integrations.
|
// however, not used by any integrations.
|
||||||
this._peerConnection.createDataChannel(this._clientConfig.dataChannel);
|
peerConnection.createDataChannel(clientConfig.dataChannel);
|
||||||
}
|
|
||||||
|
|
||||||
this._peerConnection.onnegotiationneeded = this._startNegotiation;
|
|
||||||
|
|
||||||
this._peerConnection.onicecandidate = this._handleIceCandidate;
|
|
||||||
this._peerConnection.oniceconnectionstatechange =
|
|
||||||
this._iceConnectionStateChanged;
|
|
||||||
|
|
||||||
// just for debugging
|
|
||||||
this._peerConnection.onsignalingstatechange = (ev) => {
|
|
||||||
switch ((ev.target as RTCPeerConnection).signalingState) {
|
|
||||||
case "stable":
|
|
||||||
console.timeLog("WebRTC", "ICE negotiation complete");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.timeLog(
|
|
||||||
"WebRTC",
|
|
||||||
"Signaling state changed",
|
|
||||||
(ev.target as RTCPeerConnection).signalingState
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Setup callbacks to render remote stream once media tracks are discovered.
|
|
||||||
this._remoteStream = new MediaStream();
|
|
||||||
this._peerConnection.ontrack = this._addTrack;
|
|
||||||
|
|
||||||
this._peerConnection.addTransceiver("audio", { direction: "recvonly" });
|
|
||||||
this._peerConnection.addTransceiver("video", { direction: "recvonly" });
|
|
||||||
}
|
|
||||||
|
|
||||||
private _startNegotiation = async () => {
|
|
||||||
if (!this._peerConnection) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
peerConnection.addTransceiver("audio", { direction: "recvonly" });
|
||||||
|
peerConnection.addTransceiver("video", { direction: "recvonly" });
|
||||||
|
|
||||||
const offerOptions: RTCOfferOptions = {
|
const offerOptions: RTCOfferOptions = {
|
||||||
offerToReceiveAudio: true,
|
offerToReceiveAudio: true,
|
||||||
@@ -172,218 +119,98 @@ class HaWebRtcPlayer extends LitElement {
|
|||||||
console.timeLog("WebRTC", "start createOffer", offerOptions);
|
console.timeLog("WebRTC", "start createOffer", offerOptions);
|
||||||
|
|
||||||
const offer: RTCSessionDescriptionInit =
|
const offer: RTCSessionDescriptionInit =
|
||||||
await this._peerConnection.createOffer(offerOptions);
|
await peerConnection.createOffer(offerOptions);
|
||||||
|
|
||||||
if (!this._peerConnection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.timeLog("WebRTC", "end createOffer", offer);
|
console.timeLog("WebRTC", "end createOffer", offer);
|
||||||
|
|
||||||
console.timeLog("WebRTC", "start setLocalDescription");
|
console.timeLog("WebRTC", "start setLocalDescription");
|
||||||
|
|
||||||
await this._peerConnection.setLocalDescription(offer);
|
await peerConnection.setLocalDescription(offer);
|
||||||
|
|
||||||
if (!this._peerConnection || !this.entityid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.timeLog("WebRTC", "end setLocalDescription");
|
console.timeLog("WebRTC", "end setLocalDescription");
|
||||||
|
|
||||||
let candidates = "";
|
console.timeLog("WebRTC", "start iceResolver");
|
||||||
|
|
||||||
if (this._clientConfig?.getCandidatesUpfront) {
|
let candidates = ""; // Build an Offer SDP string with ice candidates
|
||||||
await new Promise<void>((resolve) => {
|
const iceResolver = new Promise<void>((resolve) => {
|
||||||
this._peerConnection!.onicegatheringstatechange = (ev: Event) => {
|
peerConnection.addEventListener("icecandidate", (event) => {
|
||||||
const iceGatheringState = (ev.target as RTCPeerConnection)
|
if (!event.candidate?.candidate) {
|
||||||
.iceGatheringState;
|
resolve(); // Gathering complete
|
||||||
if (iceGatheringState === "complete") {
|
return;
|
||||||
this._peerConnection!.onicegatheringstatechange = null;
|
}
|
||||||
resolve();
|
console.timeLog("WebRTC", "iceResolver candidate", event.candidate);
|
||||||
}
|
candidates += `a=${event.candidate.candidate}\r\n`;
|
||||||
|
|
||||||
console.timeLog(
|
|
||||||
"WebRTC",
|
|
||||||
"Ice gathering state changed",
|
|
||||||
iceGatheringState
|
|
||||||
);
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
await iceResolver;
|
||||||
|
|
||||||
if (!this._peerConnection || !this.entityid) {
|
console.timeLog("WebRTC", "end iceResolver", candidates);
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (this._candidatesList.length) {
|
|
||||||
const candidate = this._candidatesList.pop();
|
|
||||||
if (candidate) {
|
|
||||||
candidates += `a=${candidate}\r\n`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const offer_sdp = offer.sdp! + candidates;
|
const offer_sdp = offer.sdp! + candidates;
|
||||||
|
|
||||||
console.timeLog("WebRTC", "start webRtcOffer", offer_sdp);
|
let webRtcAnswer: WebRtcAnswer;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this._unsub = webRtcOffer(this.hass, this.entityid, offer_sdp, (event) =>
|
console.timeLog("WebRTC", "start WebRTCOffer", offer_sdp);
|
||||||
this._handleOfferEvent(event)
|
webRtcAnswer = await handleWebRtcOffer(
|
||||||
);
|
|
||||||
} catch (err: any) {
|
|
||||||
this._error = "Failed to start WebRTC stream: " + err.message;
|
|
||||||
this._cleanUp();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private _iceConnectionStateChanged = () => {
|
|
||||||
console.timeLog(
|
|
||||||
"WebRTC",
|
|
||||||
"ice connection state change",
|
|
||||||
this._peerConnection?.iceConnectionState
|
|
||||||
);
|
|
||||||
if (this._peerConnection?.iceConnectionState === "failed") {
|
|
||||||
this._peerConnection.restartIce();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private async _handleOfferEvent(event: WebRtcOfferEvent) {
|
|
||||||
if (!this.entityid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.type === "session") {
|
|
||||||
this._sessionId = event.session_id;
|
|
||||||
this._candidatesList.forEach((candidate) =>
|
|
||||||
addWebRtcCandidate(
|
|
||||||
this.hass,
|
|
||||||
this.entityid!,
|
|
||||||
event.session_id,
|
|
||||||
candidate
|
|
||||||
)
|
|
||||||
);
|
|
||||||
this._candidatesList = [];
|
|
||||||
}
|
|
||||||
if (event.type === "answer") {
|
|
||||||
console.timeLog("WebRTC", "answer", event.answer);
|
|
||||||
|
|
||||||
this._handleAnswer(event);
|
|
||||||
}
|
|
||||||
if (event.type === "candidate") {
|
|
||||||
console.timeLog("WebRTC", "remote ice candidate", event.candidate);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this._peerConnection?.addIceCandidate(
|
|
||||||
new RTCIceCandidate({ candidate: event.candidate, sdpMid: "0" })
|
|
||||||
);
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (event.type === "error") {
|
|
||||||
this._error = "Failed to start WebRTC stream: " + event.message;
|
|
||||||
this._cleanUp();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleIceCandidate = (event: RTCPeerConnectionIceEvent) => {
|
|
||||||
if (!this.entityid || !event.candidate?.candidate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.timeLog(
|
|
||||||
"WebRTC",
|
|
||||||
"local ice candidate",
|
|
||||||
event.candidate?.candidate
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this._sessionId) {
|
|
||||||
addWebRtcCandidate(
|
|
||||||
this.hass,
|
this.hass,
|
||||||
this.entityid,
|
this.entityid,
|
||||||
this._sessionId,
|
offer_sdp
|
||||||
event.candidate?.candidate
|
|
||||||
);
|
);
|
||||||
} else {
|
console.timeLog("WebRTC", "end webRtcOffer", webRtcAnswer);
|
||||||
this._candidatesList.push(event.candidate?.candidate);
|
} catch (err: any) {
|
||||||
}
|
this._error = "Failed to start WebRTC stream: " + err.message;
|
||||||
};
|
peerConnection.close();
|
||||||
|
|
||||||
private _addTrack = async (event: RTCTrackEvent) => {
|
|
||||||
if (!this._remoteStream) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._remoteStream.addTrack(event.track);
|
|
||||||
if (!this.hasUpdated) {
|
|
||||||
await this.updateComplete;
|
|
||||||
}
|
|
||||||
this._videoEl.srcObject = this._remoteStream;
|
|
||||||
};
|
|
||||||
|
|
||||||
private async _handleAnswer(event: WebRtcAnswer) {
|
// Setup callbacks to render remote stream once media tracks are discovered.
|
||||||
if (
|
const remoteStream = new MediaStream();
|
||||||
!this._peerConnection?.signalingState ||
|
peerConnection.addEventListener("track", (event) => {
|
||||||
["stable", "closed"].includes(this._peerConnection.signalingState)
|
console.timeLog("WebRTC", "track", event);
|
||||||
) {
|
remoteStream.addTrack(event.track);
|
||||||
return;
|
this._videoEl.srcObject = remoteStream;
|
||||||
}
|
});
|
||||||
|
this._remoteStream = remoteStream;
|
||||||
|
|
||||||
// Initiate the stream with the remote device
|
// Initiate the stream with the remote device
|
||||||
const remoteDesc = new RTCSessionDescription({
|
const remoteDesc = new RTCSessionDescription({
|
||||||
type: "answer",
|
type: "answer",
|
||||||
sdp: event.answer,
|
sdp: webRtcAnswer.answer,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
console.timeLog("WebRTC", "start setRemoteDescription", remoteDesc);
|
console.timeLog("WebRTC", "start setRemoteDescription", remoteDesc);
|
||||||
await this._peerConnection.setRemoteDescription(remoteDesc);
|
await peerConnection.setRemoteDescription(remoteDesc);
|
||||||
|
console.timeLog("WebRTC", "end setRemoteDescription");
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this._error = "Failed to connect WebRTC stream: " + err.message;
|
this._error = "Failed to connect WebRTC stream: " + err.message;
|
||||||
this._cleanUp();
|
peerConnection.close();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
console.timeLog("WebRTC", "end setRemoteDescription");
|
this._peerConnection = peerConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _cleanUp() {
|
private _cleanUp() {
|
||||||
console.timeLog("WebRTC", "stopped");
|
|
||||||
console.timeEnd("WebRTC");
|
|
||||||
|
|
||||||
if (this._remoteStream) {
|
if (this._remoteStream) {
|
||||||
this._remoteStream.getTracks().forEach((track) => {
|
this._remoteStream.getTracks().forEach((track) => {
|
||||||
track.stop();
|
track.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
this._remoteStream = undefined;
|
this._remoteStream = undefined;
|
||||||
}
|
}
|
||||||
const videoEl = this._videoEl;
|
if (this._videoEl) {
|
||||||
if (videoEl) {
|
this._videoEl.removeAttribute("src");
|
||||||
videoEl.removeAttribute("src");
|
this._videoEl.load();
|
||||||
videoEl.load();
|
|
||||||
}
|
}
|
||||||
if (this._peerConnection) {
|
if (this._peerConnection) {
|
||||||
this._peerConnection.close();
|
this._peerConnection.close();
|
||||||
|
|
||||||
this._peerConnection.onnegotiationneeded = null;
|
|
||||||
this._peerConnection.onicecandidate = null;
|
|
||||||
this._peerConnection.oniceconnectionstatechange = null;
|
|
||||||
this._peerConnection.onicegatheringstatechange = null;
|
|
||||||
this._peerConnection.ontrack = null;
|
|
||||||
|
|
||||||
// just for debugging
|
|
||||||
this._peerConnection.onsignalingstatechange = null;
|
|
||||||
|
|
||||||
this._peerConnection = undefined;
|
this._peerConnection = undefined;
|
||||||
}
|
}
|
||||||
this._unsub?.then((unsub) => unsub());
|
|
||||||
this._unsub = undefined;
|
|
||||||
this._sessionId = undefined;
|
|
||||||
this._candidatesList = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _loadedData() {
|
private _loadedData() {
|
||||||
// @ts-ignore
|
|
||||||
fireEvent(this, "load");
|
|
||||||
|
|
||||||
console.timeLog("WebRTC", "loadedData");
|
console.timeLog("WebRTC", "loadedData");
|
||||||
console.timeEnd("WebRTC");
|
console.timeEnd("WebRTC");
|
||||||
|
// @ts-ignore
|
||||||
|
fireEvent(this, "load");
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
|
@@ -43,7 +43,6 @@ class HaEntityMarker extends LitElement {
|
|||||||
.marker {
|
.marker {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 48px;
|
width: 48px;
|
||||||
|
@@ -22,13 +22,8 @@ import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_tim
|
|||||||
import { relativeTime } from "../../common/datetime/relative_time";
|
import { relativeTime } from "../../common/datetime/relative_time";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { toggleAttribute } from "../../common/dom/toggle_attribute";
|
import { toggleAttribute } from "../../common/dom/toggle_attribute";
|
||||||
import {
|
import { fullEntitiesContext, labelsContext } from "../../data/context";
|
||||||
floorsContext,
|
|
||||||
fullEntitiesContext,
|
|
||||||
labelsContext,
|
|
||||||
} from "../../data/context";
|
|
||||||
import { EntityRegistryEntry } from "../../data/entity_registry";
|
import { EntityRegistryEntry } from "../../data/entity_registry";
|
||||||
import { FloorRegistryEntry } from "../../data/floor_registry";
|
|
||||||
import { LabelRegistryEntry } from "../../data/label_registry";
|
import { LabelRegistryEntry } from "../../data/label_registry";
|
||||||
import { LogbookEntry } from "../../data/logbook";
|
import { LogbookEntry } from "../../data/logbook";
|
||||||
import {
|
import {
|
||||||
@@ -206,7 +201,6 @@ class ActionRenderer {
|
|||||||
private hass: HomeAssistant,
|
private hass: HomeAssistant,
|
||||||
private entityReg: EntityRegistryEntry[],
|
private entityReg: EntityRegistryEntry[],
|
||||||
private labelReg: LabelRegistryEntry[],
|
private labelReg: LabelRegistryEntry[],
|
||||||
private floorReg: { [id: string]: FloorRegistryEntry },
|
|
||||||
private entries: TemplateResult[],
|
private entries: TemplateResult[],
|
||||||
private trace: AutomationTraceExtended,
|
private trace: AutomationTraceExtended,
|
||||||
private logbookRenderer: LogbookRenderer,
|
private logbookRenderer: LogbookRenderer,
|
||||||
@@ -325,7 +319,6 @@ class ActionRenderer {
|
|||||||
this.hass,
|
this.hass,
|
||||||
this.entityReg,
|
this.entityReg,
|
||||||
this.labelReg,
|
this.labelReg,
|
||||||
this.floorReg,
|
|
||||||
data,
|
data,
|
||||||
actionType
|
actionType
|
||||||
),
|
),
|
||||||
@@ -493,13 +486,7 @@ class ActionRenderer {
|
|||||||
|
|
||||||
const name =
|
const name =
|
||||||
repeatConfig.alias ||
|
repeatConfig.alias ||
|
||||||
describeAction(
|
describeAction(this.hass, this.entityReg, this.labelReg, repeatConfig);
|
||||||
this.hass,
|
|
||||||
this.entityReg,
|
|
||||||
this.labelReg,
|
|
||||||
this.floorReg,
|
|
||||||
repeatConfig
|
|
||||||
);
|
|
||||||
|
|
||||||
this._renderEntry(repeatPath, name, undefined, disabled);
|
this._renderEntry(repeatPath, name, undefined, disabled);
|
||||||
|
|
||||||
@@ -597,7 +584,6 @@ class ActionRenderer {
|
|||||||
this.hass,
|
this.hass,
|
||||||
this.entityReg,
|
this.entityReg,
|
||||||
this.labelReg,
|
this.labelReg,
|
||||||
this.floorReg,
|
|
||||||
sequenceConfig,
|
sequenceConfig,
|
||||||
"sequence"
|
"sequence"
|
||||||
),
|
),
|
||||||
@@ -694,10 +680,6 @@ export class HaAutomationTracer extends LitElement {
|
|||||||
@consume({ context: labelsContext, subscribe: true })
|
@consume({ context: labelsContext, subscribe: true })
|
||||||
_labelReg!: LabelRegistryEntry[];
|
_labelReg!: LabelRegistryEntry[];
|
||||||
|
|
||||||
@state()
|
|
||||||
@consume({ context: floorsContext, subscribe: true })
|
|
||||||
_floorReg!: { [id: string]: FloorRegistryEntry };
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.trace) {
|
if (!this.trace) {
|
||||||
return nothing;
|
return nothing;
|
||||||
@@ -715,7 +697,6 @@ export class HaAutomationTracer extends LitElement {
|
|||||||
this.hass,
|
this.hass,
|
||||||
this._entityReg,
|
this._entityReg,
|
||||||
this._labelReg,
|
this._labelReg,
|
||||||
this._floorReg,
|
|
||||||
entries,
|
entries,
|
||||||
this.trace,
|
this.trace,
|
||||||
logbookRenderer,
|
logbookRenderer,
|
||||||
|
@@ -39,37 +39,10 @@ export interface Stream {
|
|||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WebRtcOfferEvent =
|
|
||||||
| WebRtcId
|
|
||||||
| WebRtcAnswer
|
|
||||||
| WebRtcCandidate
|
|
||||||
| WebRtcError;
|
|
||||||
|
|
||||||
export interface WebRtcId {
|
|
||||||
type: "session";
|
|
||||||
session_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WebRtcAnswer {
|
export interface WebRtcAnswer {
|
||||||
type: "answer";
|
|
||||||
answer: string;
|
answer: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WebRtcCandidate {
|
|
||||||
type: "candidate";
|
|
||||||
candidate: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WebRtcError {
|
|
||||||
type: "error";
|
|
||||||
code: string;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WebRtcOfferResponse {
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const cameraUrlWithWidthHeight = (
|
export const cameraUrlWithWidthHeight = (
|
||||||
base_url: string,
|
base_url: string,
|
||||||
width: number,
|
width: number,
|
||||||
@@ -121,29 +94,15 @@ export const fetchStreamUrl = async (
|
|||||||
return stream;
|
return stream;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const webRtcOffer = (
|
export const handleWebRtcOffer = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entity_id: string,
|
entityId: string,
|
||||||
offer: string,
|
offer: string
|
||||||
callback: (event: WebRtcOfferEvent) => void
|
|
||||||
) =>
|
) =>
|
||||||
hass.connection.subscribeMessage<WebRtcOfferEvent>(callback, {
|
hass.callWS<WebRtcAnswer>({
|
||||||
type: "camera/webrtc/offer",
|
type: "camera/web_rtc_offer",
|
||||||
entity_id,
|
entity_id: entityId,
|
||||||
offer,
|
offer: offer,
|
||||||
});
|
|
||||||
|
|
||||||
export const addWebRtcCandidate = (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entity_id: string,
|
|
||||||
session_id: string,
|
|
||||||
candidate: string
|
|
||||||
) =>
|
|
||||||
hass.callWS({
|
|
||||||
type: "camera/webrtc/candidate",
|
|
||||||
entity_id,
|
|
||||||
session_id,
|
|
||||||
candidate,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fetchCameraPrefs = (hass: HomeAssistant, entityId: string) =>
|
export const fetchCameraPrefs = (hass: HomeAssistant, entityId: string) =>
|
||||||
@@ -178,7 +137,6 @@ export const getEntityIdFromCameraMediaSource = (mediaContentId: string) =>
|
|||||||
export interface WebRTCClientConfiguration {
|
export interface WebRTCClientConfiguration {
|
||||||
configuration: RTCConfiguration;
|
configuration: RTCConfiguration;
|
||||||
dataChannel?: string;
|
dataChannel?: string;
|
||||||
getCandidatesUpfront: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchWebRtcClientConfiguration = async (
|
export const fetchWebRtcClientConfiguration = async (
|
||||||
|
@@ -27,6 +27,4 @@ export const panelsContext = createContext<HomeAssistant["panels"]>("panels");
|
|||||||
export const fullEntitiesContext =
|
export const fullEntitiesContext =
|
||||||
createContext<EntityRegistryEntry[]>("extendedEntities");
|
createContext<EntityRegistryEntry[]>("extendedEntities");
|
||||||
|
|
||||||
export const floorsContext = createContext<HomeAssistant["floors"]>("floors");
|
|
||||||
|
|
||||||
export const labelsContext = createContext<LabelRegistryEntry[]>("labels");
|
export const labelsContext = createContext<LabelRegistryEntry[]>("labels");
|
||||||
|
@@ -65,7 +65,7 @@ export const countryCurrency = {
|
|||||||
HK: "HKD",
|
HK: "HKD",
|
||||||
HN: "HNL",
|
HN: "HNL",
|
||||||
HM: "AUD",
|
HM: "AUD",
|
||||||
VE: "VED",
|
VE: "VEF",
|
||||||
PR: "USD",
|
PR: "USD",
|
||||||
PS: "ILS",
|
PS: "ILS",
|
||||||
PW: "USD",
|
PW: "USD",
|
||||||
|
@@ -358,24 +358,21 @@ export const restartHassioAddon = async (
|
|||||||
|
|
||||||
export const uninstallHassioAddon = async (
|
export const uninstallHassioAddon = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
slug: string,
|
slug: string
|
||||||
removeData: boolean
|
) => {
|
||||||
): Promise<void> => {
|
|
||||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
await hass.callWS({
|
await hass.callWS({
|
||||||
type: "supervisor/api",
|
type: "supervisor/api",
|
||||||
endpoint: `/addons/${slug}/uninstall`,
|
endpoint: `/addons/${slug}/uninstall`,
|
||||||
method: "post",
|
method: "post",
|
||||||
timeout: null,
|
timeout: null,
|
||||||
data: { remove_config: removeData },
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await hass.callApi<HassioResponse<void>>(
|
await hass.callApi<HassioResponse<void>>(
|
||||||
"POST",
|
"POST",
|
||||||
`hassio/addons/${slug}/uninstall`,
|
`hassio/addons/${slug}/uninstall`
|
||||||
{ remove_config: removeData }
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -17,7 +17,7 @@ export interface NetworkInterface {
|
|||||||
ipv4?: Partial<IpConfiguration>;
|
ipv4?: Partial<IpConfiguration>;
|
||||||
ipv6?: Partial<IpConfiguration>;
|
ipv6?: Partial<IpConfiguration>;
|
||||||
type: "ethernet" | "wireless" | "vlan";
|
type: "ethernet" | "wireless" | "vlan";
|
||||||
wifi?: Partial<WifiConfiguration> | null;
|
wifi?: Partial<WifiConfiguration>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DockerNetwork {
|
interface DockerNetwork {
|
||||||
@@ -27,7 +27,7 @@ interface DockerNetwork {
|
|||||||
interface: string;
|
interface: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AccessPoint {
|
interface AccessPoint {
|
||||||
mode: "infrastructure" | "mesh" | "adhoc" | "ap";
|
mode: "infrastructure" | "mesh" | "adhoc" | "ap";
|
||||||
ssid: string;
|
ssid: string;
|
||||||
mac: string;
|
mac: string;
|
||||||
|
@@ -65,10 +65,6 @@ export type HassioInfo = {
|
|||||||
timezone: string;
|
timezone: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HassioBoots = {
|
|
||||||
boots: Record<number, string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type HassioPanelInfo = PanelInfo<
|
export type HassioPanelInfo = PanelInfo<
|
||||||
| undefined
|
| undefined
|
||||||
| {
|
| {
|
||||||
@@ -181,39 +177,10 @@ export const fetchHassioInfo = async (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchHassioBoots = async (hass: HomeAssistant) =>
|
export const fetchHassioLogs = async (hass: HomeAssistant, provider: string) =>
|
||||||
hass.callApi<HassioResponse<HassioBoots>>("GET", `hassio/host/logs/boots`);
|
hass.callApi<string>(
|
||||||
|
|
||||||
export const fetchHassioLogs = async (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
provider: string,
|
|
||||||
range?: string,
|
|
||||||
boot = 0
|
|
||||||
) =>
|
|
||||||
hass.callApiRaw(
|
|
||||||
"GET",
|
"GET",
|
||||||
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs/boots/${boot}`,
|
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs`
|
||||||
undefined,
|
|
||||||
range
|
|
||||||
? {
|
|
||||||
Range: range,
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
export const fetchHassioLogsFollow = async (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
provider: string,
|
|
||||||
signal: AbortSignal,
|
|
||||||
lines = 100,
|
|
||||||
boot = 0
|
|
||||||
) =>
|
|
||||||
hass.callApiRaw(
|
|
||||||
"GET",
|
|
||||||
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs/boots/${boot}/follow?lines=${lines}`,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
signal
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getHassioLogDownloadUrl = (provider: string) =>
|
export const getHassioLogDownloadUrl = (provider: string) =>
|
||||||
@@ -221,15 +188,6 @@ export const getHassioLogDownloadUrl = (provider: string) =>
|
|||||||
provider.includes("_") ? `addons/${provider}` : provider
|
provider.includes("_") ? `addons/${provider}` : provider
|
||||||
}/logs`;
|
}/logs`;
|
||||||
|
|
||||||
export const getHassioLogDownloadLinesUrl = (
|
|
||||||
provider: string,
|
|
||||||
lines: number,
|
|
||||||
boot = 0
|
|
||||||
) =>
|
|
||||||
`/api/hassio/${
|
|
||||||
provider.includes("_") ? `addons/${provider}` : provider
|
|
||||||
}/logs/boots/${boot}?lines=${lines}`;
|
|
||||||
|
|
||||||
export const setSupervisorOption = async (
|
export const setSupervisorOption = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
data: SupervisorOptions
|
data: SupervisorOptions
|
||||||
|
@@ -14,7 +14,6 @@ import {
|
|||||||
computeEntityRegistryName,
|
computeEntityRegistryName,
|
||||||
entityRegistryById,
|
entityRegistryById,
|
||||||
} from "./entity_registry";
|
} from "./entity_registry";
|
||||||
import { FloorRegistryEntry } from "./floor_registry";
|
|
||||||
import { domainToName } from "./integration";
|
import { domainToName } from "./integration";
|
||||||
import { LabelRegistryEntry } from "./label_registry";
|
import { LabelRegistryEntry } from "./label_registry";
|
||||||
import {
|
import {
|
||||||
@@ -44,7 +43,6 @@ export const describeAction = <T extends ActionType>(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityRegistry: EntityRegistryEntry[],
|
entityRegistry: EntityRegistryEntry[],
|
||||||
labelRegistry: LabelRegistryEntry[],
|
labelRegistry: LabelRegistryEntry[],
|
||||||
floorRegistry: { [id: string]: FloorRegistryEntry },
|
|
||||||
action: ActionTypes[T],
|
action: ActionTypes[T],
|
||||||
actionType?: T,
|
actionType?: T,
|
||||||
ignoreAlias = false
|
ignoreAlias = false
|
||||||
@@ -54,7 +52,6 @@ export const describeAction = <T extends ActionType>(
|
|||||||
hass,
|
hass,
|
||||||
entityRegistry,
|
entityRegistry,
|
||||||
labelRegistry,
|
labelRegistry,
|
||||||
floorRegistry,
|
|
||||||
action,
|
action,
|
||||||
actionType,
|
actionType,
|
||||||
ignoreAlias
|
ignoreAlias
|
||||||
@@ -78,7 +75,6 @@ const tryDescribeAction = <T extends ActionType>(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityRegistry: EntityRegistryEntry[],
|
entityRegistry: EntityRegistryEntry[],
|
||||||
labelRegistry: LabelRegistryEntry[],
|
labelRegistry: LabelRegistryEntry[],
|
||||||
floorRegistry: { [id: string]: FloorRegistryEntry },
|
|
||||||
action: ActionTypes[T],
|
action: ActionTypes[T],
|
||||||
actionType?: T,
|
actionType?: T,
|
||||||
ignoreAlias = false
|
ignoreAlias = false
|
||||||
@@ -168,7 +164,7 @@ const tryDescribeAction = <T extends ActionType>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (key === "floor_id") {
|
} else if (key === "floor_id") {
|
||||||
const floor = floorRegistry[targetThing] ?? undefined;
|
const floor = hass.floors[targetThing] ?? undefined;
|
||||||
if (floor?.name) {
|
if (floor?.name) {
|
||||||
targets.push(floor.name);
|
targets.push(floor.name);
|
||||||
} else {
|
} else {
|
||||||
|
@@ -8,7 +8,6 @@ import { BINARY_STATE_ON } from "../common/const";
|
|||||||
import { computeDomain } from "../common/entity/compute_domain";
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||||
import { supportsFeature } from "../common/entity/supports-feature";
|
import { supportsFeature } from "../common/entity/supports-feature";
|
||||||
import { formatNumber } from "../common/number/format_number";
|
|
||||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
@@ -24,15 +23,13 @@ export enum UpdateEntityFeature {
|
|||||||
|
|
||||||
interface UpdateEntityAttributes extends HassEntityAttributeBase {
|
interface UpdateEntityAttributes extends HassEntityAttributeBase {
|
||||||
auto_update: boolean | null;
|
auto_update: boolean | null;
|
||||||
display_precision: number;
|
|
||||||
installed_version: string | null;
|
installed_version: string | null;
|
||||||
in_progress: boolean;
|
in_progress: boolean | number;
|
||||||
latest_version: string | null;
|
latest_version: string | null;
|
||||||
release_summary: string | null;
|
release_summary: string | null;
|
||||||
release_url: string | null;
|
release_url: string | null;
|
||||||
skipped_version: string | null;
|
skipped_version: string | null;
|
||||||
title: string | null;
|
title: string | null;
|
||||||
update_percentage: number | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateEntity extends HassEntityBase {
|
export interface UpdateEntity extends HassEntityBase {
|
||||||
@@ -41,7 +38,7 @@ export interface UpdateEntity extends HassEntityBase {
|
|||||||
|
|
||||||
export const updateUsesProgress = (entity: UpdateEntity): boolean =>
|
export const updateUsesProgress = (entity: UpdateEntity): boolean =>
|
||||||
supportsFeature(entity, UpdateEntityFeature.PROGRESS) &&
|
supportsFeature(entity, UpdateEntityFeature.PROGRESS) &&
|
||||||
entity.attributes.update_percentage !== null;
|
typeof entity.attributes.in_progress === "number";
|
||||||
|
|
||||||
export const updateCanInstall = (
|
export const updateCanInstall = (
|
||||||
entity: UpdateEntity,
|
entity: UpdateEntity,
|
||||||
@@ -52,7 +49,7 @@ export const updateCanInstall = (
|
|||||||
supportsFeature(entity, UpdateEntityFeature.INSTALL);
|
supportsFeature(entity, UpdateEntityFeature.INSTALL);
|
||||||
|
|
||||||
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
|
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
|
||||||
!!entity.attributes.in_progress;
|
updateUsesProgress(entity) || !!entity.attributes.in_progress;
|
||||||
|
|
||||||
export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
|
export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
|
||||||
hass.callWS<string | null>({
|
hass.callWS<string | null>({
|
||||||
@@ -186,13 +183,10 @@ export const computeUpdateStateDisplay = (
|
|||||||
if (updateIsInstalling(stateObj)) {
|
if (updateIsInstalling(stateObj)) {
|
||||||
const supportsProgress =
|
const supportsProgress =
|
||||||
supportsFeature(stateObj, UpdateEntityFeature.PROGRESS) &&
|
supportsFeature(stateObj, UpdateEntityFeature.PROGRESS) &&
|
||||||
attributes.update_percentage !== null;
|
typeof attributes.in_progress === "number";
|
||||||
if (supportsProgress) {
|
if (supportsProgress) {
|
||||||
return hass.localize("ui.card.update.installing_with_progress", {
|
return hass.localize("ui.card.update.installing_with_progress", {
|
||||||
progress: formatNumber(attributes.update_percentage!, hass.locale, {
|
progress: attributes.in_progress as number,
|
||||||
maximumFractionDigits: attributes.display_precision,
|
|
||||||
minimumFractionDigits: attributes.display_precision,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return hass.localize("ui.card.update.installing");
|
return hass.localize("ui.card.update.installing");
|
||||||
|
@@ -31,9 +31,6 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [
|
|||||||
"valve",
|
"valve",
|
||||||
"water_heater",
|
"water_heater",
|
||||||
];
|
];
|
||||||
/** Domains with full height more info dialog */
|
|
||||||
export const DOMAINS_FULL_HEIGHT_MORE_INFO = ["update"];
|
|
||||||
|
|
||||||
/** Domains with separate more info dialog. */
|
/** Domains with separate more info dialog. */
|
||||||
export const DOMAINS_WITH_MORE_INFO = [
|
export const DOMAINS_WITH_MORE_INFO = [
|
||||||
"alarm_control_panel",
|
"alarm_control_panel",
|
||||||
|
@@ -93,13 +93,12 @@ class MoreInfoCover extends LitElement {
|
|||||||
supportsFeature(this.stateObj, CoverEntityFeature.CLOSE_TILT) ||
|
supportsFeature(this.stateObj, CoverEntityFeature.CLOSE_TILT) ||
|
||||||
supportsFeature(this.stateObj, CoverEntityFeature.STOP_TILT);
|
supportsFeature(this.stateObj, CoverEntityFeature.STOP_TILT);
|
||||||
|
|
||||||
const supportsOpenCloseOnly =
|
const supportsOpenCloseWithoutStop =
|
||||||
supportsFeature(this.stateObj, CoverEntityFeature.OPEN) &&
|
supportsFeature(this.stateObj, CoverEntityFeature.OPEN) &&
|
||||||
supportsFeature(this.stateObj, CoverEntityFeature.CLOSE) &&
|
supportsFeature(this.stateObj, CoverEntityFeature.CLOSE) &&
|
||||||
!supportsFeature(this.stateObj, CoverEntityFeature.STOP) &&
|
!supportsFeature(this.stateObj, CoverEntityFeature.STOP) &&
|
||||||
!supportsTilt &&
|
!supportsFeature(this.stateObj, CoverEntityFeature.OPEN_TILT) &&
|
||||||
!supportsPosition &&
|
!supportsFeature(this.stateObj, CoverEntityFeature.CLOSE_TILT);
|
||||||
!supportsTiltPosition;
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-more-info-state-header
|
<ha-more-info-state-header
|
||||||
@@ -134,7 +133,7 @@ class MoreInfoCover extends LitElement {
|
|||||||
${
|
${
|
||||||
this._mode === "button"
|
this._mode === "button"
|
||||||
? html`
|
? html`
|
||||||
${supportsOpenCloseOnly
|
${supportsOpenCloseWithoutStop
|
||||||
? html`
|
? html`
|
||||||
<ha-state-control-cover-toggle
|
<ha-state-control-cover-toggle
|
||||||
.stateObj=${this.stateObj}
|
.stateObj=${this.stateObj}
|
||||||
|
@@ -1,18 +1,15 @@
|
|||||||
|
import "@material/mwc-button/mwc-button";
|
||||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { BINARY_STATE_OFF } from "../../../common/const";
|
import { BINARY_STATE_OFF } from "../../../common/const";
|
||||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||||
import "../../../components/ha-alert";
|
import "../../../components/ha-alert";
|
||||||
import "../../../components/ha-button";
|
|
||||||
import "../../../components/ha-checkbox";
|
import "../../../components/ha-checkbox";
|
||||||
import "../../../components/ha-circular-progress";
|
import "../../../components/ha-circular-progress";
|
||||||
import "../../../components/ha-faded";
|
import "../../../components/ha-faded";
|
||||||
import "../../../components/ha-formfield";
|
import "../../../components/ha-formfield";
|
||||||
import "../../../components/ha-markdown";
|
import "../../../components/ha-markdown";
|
||||||
import "../../../components/ha-settings-row";
|
|
||||||
import "../../../components/ha-switch";
|
|
||||||
import type { HaSwitch } from "../../../components/ha-switch";
|
|
||||||
import { isUnavailableState } from "../../../data/entity";
|
import { isUnavailableState } from "../../../data/entity";
|
||||||
import {
|
import {
|
||||||
UpdateEntity,
|
UpdateEntity,
|
||||||
@@ -33,8 +30,6 @@ class MoreInfoUpdate extends LitElement {
|
|||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
@state() private _markdownLoading = true;
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (
|
if (
|
||||||
!this.hass ||
|
!this.hass ||
|
||||||
@@ -50,174 +45,137 @@ class MoreInfoUpdate extends LitElement {
|
|||||||
this.stateObj.attributes.latest_version;
|
this.stateObj.attributes.latest_version;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="content">
|
${this.stateObj.attributes.in_progress
|
||||||
${this.stateObj.attributes.in_progress
|
? supportsFeature(this.stateObj, UpdateEntityFeature.PROGRESS) &&
|
||||||
? supportsFeature(this.stateObj, UpdateEntityFeature.PROGRESS) &&
|
typeof this.stateObj.attributes.in_progress === "number"
|
||||||
this.stateObj.attributes.update_percentage !== null
|
? html`<mwc-linear-progress
|
||||||
? html`<mwc-linear-progress
|
.progress=${this.stateObj.attributes.in_progress / 100}
|
||||||
.progress=${this.stateObj.attributes.update_percentage / 100}
|
buffer=""
|
||||||
buffer=""
|
></mwc-linear-progress>`
|
||||||
></mwc-linear-progress>`
|
: html`<mwc-linear-progress indeterminate></mwc-linear-progress>`
|
||||||
: html`<mwc-linear-progress indeterminate></mwc-linear-progress>`
|
: ""}
|
||||||
: nothing}
|
<h3>${this.stateObj.attributes.title}</h3>
|
||||||
<h3>${this.stateObj.attributes.title}</h3>
|
${this._error
|
||||||
${this._error
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
: ""}
|
||||||
: nothing}
|
<div class="row">
|
||||||
<div class="row">
|
<div class="key">
|
||||||
<div class="key">
|
${this.hass.formatEntityAttributeName(
|
||||||
${this.hass.formatEntityAttributeName(
|
this.stateObj,
|
||||||
this.stateObj,
|
"installed_version"
|
||||||
"installed_version"
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class="value">
|
|
||||||
${this.stateObj.attributes.installed_version ??
|
|
||||||
this.hass.localize("state.default.unavailable")}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="value">
|
||||||
<div class="key">
|
${this.stateObj.attributes.installed_version ??
|
||||||
${this.hass.formatEntityAttributeName(
|
this.hass.localize("state.default.unavailable")}
|
||||||
this.stateObj,
|
|
||||||
"latest_version"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class="value">
|
|
||||||
${this.stateObj.attributes.latest_version ??
|
|
||||||
this.hass.localize("state.default.unavailable")}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="key">
|
||||||
|
${this.hass.formatEntityAttributeName(
|
||||||
|
this.stateObj,
|
||||||
|
"latest_version"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="value">
|
||||||
|
${this.stateObj.attributes.latest_version ??
|
||||||
|
this.hass.localize("state.default.unavailable")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
${this.stateObj.attributes.release_url
|
${this.stateObj.attributes.release_url
|
||||||
? html`<div class="row">
|
? html`<div class="row">
|
||||||
<div class="key">
|
<div class="key">
|
||||||
<a
|
<a
|
||||||
href=${this.stateObj.attributes.release_url}
|
href=${this.stateObj.attributes.release_url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.dialogs.more_info_control.update.release_announcement"
|
"ui.dialogs.more_info_control.update.release_announcement"
|
||||||
)}
|
)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>`
|
||||||
|
: ""}
|
||||||
|
${supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES) &&
|
||||||
|
!this._error
|
||||||
|
? this._releaseNotes === undefined
|
||||||
|
? html`<div class="flex center">
|
||||||
|
<ha-circular-progress indeterminate></ha-circular-progress>
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: html`<hr />
|
||||||
${supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES) &&
|
<ha-faded>
|
||||||
!this._error
|
<ha-markdown .content=${this._releaseNotes}></ha-markdown>
|
||||||
? this._releaseNotes === undefined
|
</ha-faded> `
|
||||||
? html`
|
: this.stateObj.attributes.release_summary
|
||||||
<hr />
|
? html`<hr />
|
||||||
${this._markdownLoading ? this._renderLoader() : nothing}
|
<ha-markdown
|
||||||
`
|
.content=${this.stateObj.attributes.release_summary}
|
||||||
: html`
|
></ha-markdown>`
|
||||||
<hr />
|
: ""}
|
||||||
<ha-markdown
|
${supportsFeature(this.stateObj, UpdateEntityFeature.BACKUP)
|
||||||
@content-resize=${this._markdownLoaded}
|
? html`<hr />
|
||||||
.content=${this._releaseNotes}
|
<ha-formfield
|
||||||
class=${this._markdownLoading ? "hidden" : ""}
|
.label=${this.hass.localize(
|
||||||
></ha-markdown>
|
"ui.dialogs.more_info_control.update.create_backup"
|
||||||
${this._markdownLoading ? this._renderLoader() : nothing}
|
)}
|
||||||
`
|
>
|
||||||
: this.stateObj.attributes.release_summary
|
<ha-checkbox
|
||||||
? html`
|
checked
|
||||||
<hr />
|
.disabled=${updateIsInstalling(this.stateObj)}
|
||||||
<ha-markdown
|
></ha-checkbox>
|
||||||
@content-resize=${this._markdownLoaded}
|
</ha-formfield> `
|
||||||
.content=${this.stateObj.attributes.release_summary}
|
: ""}
|
||||||
class=${this._markdownLoading ? "hidden" : ""}
|
<div class="actions">
|
||||||
></ha-markdown>
|
${this.stateObj.state === BINARY_STATE_OFF &&
|
||||||
${this._markdownLoading ? this._renderLoader() : nothing}
|
this.stateObj.attributes.skipped_version
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
|
||||||
<div class="footer">
|
|
||||||
${supportsFeature(this.stateObj, UpdateEntityFeature.BACKUP)
|
|
||||||
? html`
|
? html`
|
||||||
<ha-settings-row>
|
<mwc-button @click=${this._handleClearSkipped}>
|
||||||
<span slot="heading">
|
${this.hass.localize(
|
||||||
${this.hass.localize(
|
"ui.dialogs.more_info_control.update.clear_skipped"
|
||||||
"ui.dialogs.more_info_control.update.create_backup"
|
)}
|
||||||
)}
|
</mwc-button>
|
||||||
</span>
|
|
||||||
<ha-switch
|
|
||||||
id="create_backup"
|
|
||||||
checked
|
|
||||||
.disabled=${updateIsInstalling(this.stateObj)}
|
|
||||||
></ha-switch>
|
|
||||||
</ha-settings-row>
|
|
||||||
`
|
`
|
||||||
: nothing}
|
: html`
|
||||||
<div class="actions">
|
<mwc-button
|
||||||
${this.stateObj.state === BINARY_STATE_OFF &&
|
@click=${this._handleSkip}
|
||||||
this.stateObj.attributes.skipped_version
|
.disabled=${skippedVersion ||
|
||||||
? html`
|
this.stateObj.state === BINARY_STATE_OFF ||
|
||||||
<ha-button @click=${this._handleClearSkipped}>
|
updateIsInstalling(this.stateObj)}
|
||||||
${this.hass.localize(
|
>
|
||||||
"ui.dialogs.more_info_control.update.clear_skipped"
|
${this.hass.localize(
|
||||||
)}
|
"ui.dialogs.more_info_control.update.skip"
|
||||||
</ha-button>
|
)}
|
||||||
`
|
</mwc-button>
|
||||||
: html`
|
`}
|
||||||
<ha-button
|
${supportsFeature(this.stateObj, UpdateEntityFeature.INSTALL)
|
||||||
@click=${this._handleSkip}
|
? html`
|
||||||
.disabled=${skippedVersion ||
|
<mwc-button
|
||||||
this.stateObj.state === BINARY_STATE_OFF ||
|
@click=${this._handleInstall}
|
||||||
updateIsInstalling(this.stateObj)}
|
.disabled=${(this.stateObj.state === BINARY_STATE_OFF &&
|
||||||
>
|
!skippedVersion) ||
|
||||||
${this.hass.localize(
|
updateIsInstalling(this.stateObj)}
|
||||||
"ui.dialogs.more_info_control.update.skip"
|
>
|
||||||
)}
|
${this.hass.localize(
|
||||||
</ha-button>
|
"ui.dialogs.more_info_control.update.install"
|
||||||
`}
|
)}
|
||||||
${supportsFeature(this.stateObj, UpdateEntityFeature.INSTALL)
|
</mwc-button>
|
||||||
? html`
|
`
|
||||||
<ha-button
|
: ""}
|
||||||
@click=${this._handleInstall}
|
|
||||||
.disabled=${(this.stateObj.state === BINARY_STATE_OFF &&
|
|
||||||
!skippedVersion) ||
|
|
||||||
updateIsInstalling(this.stateObj)}
|
|
||||||
>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.dialogs.more_info_control.update.update"
|
|
||||||
)}
|
|
||||||
</ha-button>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _renderLoader() {
|
|
||||||
return html`
|
|
||||||
<div class="flex center loader">
|
|
||||||
<ha-circular-progress indeterminate></ha-circular-progress>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated(): void {
|
protected firstUpdated(): void {
|
||||||
if (supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES)) {
|
if (supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES)) {
|
||||||
this._fetchReleaseNotes();
|
updateReleaseNotes(this.hass, this.stateObj!.entity_id)
|
||||||
}
|
.then((result) => {
|
||||||
}
|
this._releaseNotes = result;
|
||||||
|
})
|
||||||
private async _markdownLoaded() {
|
.catch((err) => {
|
||||||
if (this._markdownLoading) {
|
this._error = err.message;
|
||||||
this._markdownLoading = false;
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _fetchReleaseNotes() {
|
|
||||||
try {
|
|
||||||
this._releaseNotes = await updateReleaseNotes(
|
|
||||||
this.hass,
|
|
||||||
this.stateObj!.entity_id
|
|
||||||
);
|
|
||||||
} catch (err: any) {
|
|
||||||
this._error = err.message;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,11 +183,9 @@ class MoreInfoUpdate extends LitElement {
|
|||||||
if (!supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
|
if (!supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const createBackupSwitch = this.shadowRoot?.getElementById(
|
const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
|
||||||
"create-backup"
|
if (checkbox) {
|
||||||
) as HaSwitch;
|
return checkbox.checked;
|
||||||
if (createBackupSwitch) {
|
|
||||||
return createBackupSwitch.checked;
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -278,12 +234,6 @@ class MoreInfoUpdate extends LitElement {
|
|||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
:host {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
hr {
|
hr {
|
||||||
border-color: var(--divider-color);
|
border-color: var(--divider-color);
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
@@ -298,44 +248,26 @@ class MoreInfoUpdate extends LitElement {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
.actions {
|
||||||
.footer {
|
|
||||||
border-top: 1px solid var(--divider-color);
|
border-top: 1px solid var(--divider-color);
|
||||||
background: var(
|
background: var(
|
||||||
--ha-dialog-surface-background,
|
--ha-dialog-surface-background,
|
||||||
var(--mdc-theme-surface, #fff)
|
var(--mdc-theme-surface, #fff)
|
||||||
);
|
);
|
||||||
|
margin: 8px 0 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
margin: 0 -24px -24px -24px;
|
padding: 12px 0;
|
||||||
box-sizing: border-box;
|
margin-bottom: -24px;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
overflow: hidden;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-settings-row {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0 24px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin-bottom: -16px;
|
|
||||||
margin-top: -4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: flex-end;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 12px;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
gap: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actions mwc-button {
|
||||||
|
margin: 0 4px 4px;
|
||||||
|
}
|
||||||
a {
|
a {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
@@ -350,16 +282,6 @@ class MoreInfoUpdate extends LitElement {
|
|||||||
}
|
}
|
||||||
ha-markdown {
|
ha-markdown {
|
||||||
direction: ltr;
|
direction: ltr;
|
||||||
padding-bottom: 16px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
ha-markdown.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.loader {
|
|
||||||
height: 80px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -83,11 +83,10 @@ class MoreInfoValve extends LitElement {
|
|||||||
supportsFeature(this.stateObj, ValveEntityFeature.CLOSE) ||
|
supportsFeature(this.stateObj, ValveEntityFeature.CLOSE) ||
|
||||||
supportsFeature(this.stateObj, ValveEntityFeature.STOP);
|
supportsFeature(this.stateObj, ValveEntityFeature.STOP);
|
||||||
|
|
||||||
const supportsOpenCloseOnly =
|
const supportsOpenCloseWithoutStop =
|
||||||
supportsFeature(this.stateObj, ValveEntityFeature.OPEN) &&
|
supportsFeature(this.stateObj, ValveEntityFeature.OPEN) &&
|
||||||
supportsFeature(this.stateObj, ValveEntityFeature.CLOSE) &&
|
supportsFeature(this.stateObj, ValveEntityFeature.CLOSE) &&
|
||||||
!supportsFeature(this.stateObj, ValveEntityFeature.STOP) &&
|
!supportsFeature(this.stateObj, ValveEntityFeature.STOP);
|
||||||
!supportsPosition;
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-more-info-state-header
|
<ha-more-info-state-header
|
||||||
@@ -114,7 +113,7 @@ class MoreInfoValve extends LitElement {
|
|||||||
${
|
${
|
||||||
this._mode === "button"
|
this._mode === "button"
|
||||||
? html`
|
? html`
|
||||||
${supportsOpenCloseOnly
|
${supportsOpenCloseWithoutStop
|
||||||
? html`
|
? html`
|
||||||
<ha-state-control-valve-toggle
|
<ha-state-control-valve-toggle
|
||||||
.stateObj=${this.stateObj}
|
.stateObj=${this.stateObj}
|
||||||
|
@@ -9,7 +9,6 @@ import {
|
|||||||
computeShowHistoryComponent,
|
computeShowHistoryComponent,
|
||||||
computeShowLogBookComponent,
|
computeShowLogBookComponent,
|
||||||
computeShowNewMoreInfo,
|
computeShowNewMoreInfo,
|
||||||
DOMAINS_FULL_HEIGHT_MORE_INFO,
|
|
||||||
DOMAINS_NO_INFO,
|
DOMAINS_NO_INFO,
|
||||||
DOMAINS_WITH_MORE_INFO,
|
DOMAINS_WITH_MORE_INFO,
|
||||||
} from "./const";
|
} from "./const";
|
||||||
@@ -41,8 +40,6 @@ export class MoreInfoInfo extends LitElement {
|
|||||||
const entityRegObj = this.hass.entities[entityId];
|
const entityRegObj = this.hass.entities[entityId];
|
||||||
const domain = computeDomain(entityId);
|
const domain = computeDomain(entityId);
|
||||||
const isNewMoreInfo = stateObj && computeShowNewMoreInfo(stateObj);
|
const isNewMoreInfo = stateObj && computeShowNewMoreInfo(stateObj);
|
||||||
const isFullHeight =
|
|
||||||
isNewMoreInfo || DOMAINS_FULL_HEIGHT_MORE_INFO.includes(domain);
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="container" data-domain=${domain}>
|
<div class="container" data-domain=${domain}>
|
||||||
@@ -92,7 +89,7 @@ export class MoreInfoInfo extends LitElement {
|
|||||||
.entityId=${this.entityId}
|
.entityId=${this.entityId}
|
||||||
></ha-more-info-logbook>`}
|
></ha-more-info-logbook>`}
|
||||||
<more-info-content
|
<more-info-content
|
||||||
?full-height=${isFullHeight}
|
?full-height=${isNewMoreInfo}
|
||||||
.stateObj=${stateObj}
|
.stateObj=${stateObj}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.entry=${this.entry}
|
.entry=${this.entry}
|
||||||
|
@@ -39,9 +39,6 @@ export const AssistantSetupStyles = [
|
|||||||
.footer.full-width ha-button {
|
.footer.full-width ha-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.footer.centered {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.footer.side-by-side {
|
.footer.side-by-side {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,7 @@ import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
|||||||
import { haStyleDialog } from "../../resources/styles";
|
import { haStyleDialog } from "../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { VoiceAssistantSetupDialogParams } from "./show-voice-assistant-setup-dialog";
|
import { VoiceAssistantSetupDialogParams } from "./show-voice-assistant-setup-dialog";
|
||||||
|
import "./voice-assistant-setup-step-addons";
|
||||||
import "./voice-assistant-setup-step-area";
|
import "./voice-assistant-setup-step-area";
|
||||||
import "./voice-assistant-setup-step-change-wake-word";
|
import "./voice-assistant-setup-step-change-wake-word";
|
||||||
import "./voice-assistant-setup-step-check";
|
import "./voice-assistant-setup-step-check";
|
||||||
@@ -33,6 +34,7 @@ export const enum STEP {
|
|||||||
PIPELINE,
|
PIPELINE,
|
||||||
SUCCESS,
|
SUCCESS,
|
||||||
CLOUD,
|
CLOUD,
|
||||||
|
ADDONS,
|
||||||
CHANGE_WAKEWORD,
|
CHANGE_WAKEWORD,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,18 +210,22 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
|||||||
? html`<ha-voice-assistant-setup-step-cloud
|
? html`<ha-voice-assistant-setup-step-cloud
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
></ha-voice-assistant-setup-step-cloud>`
|
></ha-voice-assistant-setup-step-cloud>`
|
||||||
: this._step === STEP.SUCCESS
|
: this._step === STEP.ADDONS
|
||||||
? html`<ha-voice-assistant-setup-step-success
|
? html`<ha-voice-assistant-setup-step-addons
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.assistConfiguration=${this
|
></ha-voice-assistant-setup-step-addons>`
|
||||||
._assistConfiguration}
|
: this._step === STEP.SUCCESS
|
||||||
.assistEntityId=${this._findDomainEntityId(
|
? html`<ha-voice-assistant-setup-step-success
|
||||||
this._params.deviceId,
|
.hass=${this.hass}
|
||||||
this.hass.entities,
|
.assistConfiguration=${this
|
||||||
"assist_satellite"
|
._assistConfiguration}
|
||||||
)}
|
.assistEntityId=${this._findDomainEntityId(
|
||||||
></ha-voice-assistant-setup-step-success>`
|
this._params.deviceId,
|
||||||
: nothing}
|
this.hass.entities,
|
||||||
|
"assist_satellite"
|
||||||
|
)}
|
||||||
|
></ha-voice-assistant-setup-step-success>`
|
||||||
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
</ha-dialog>
|
</ha-dialog>
|
||||||
`;
|
`;
|
||||||
|
@@ -0,0 +1,185 @@
|
|||||||
|
import { css, html, LitElement, nothing, PropertyValues } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
import { AssistantSetupStyles } from "./styles";
|
||||||
|
import { STEP } from "./voice-assistant-setup-dialog";
|
||||||
|
import { documentationUrl } from "../../util/documentation-url";
|
||||||
|
|
||||||
|
@customElement("ha-voice-assistant-setup-step-addons")
|
||||||
|
export class HaVoiceAssistantSetupStepAddons extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _showFirst = false;
|
||||||
|
|
||||||
|
@state() private _showSecond = false;
|
||||||
|
|
||||||
|
@state() private _showThird = false;
|
||||||
|
|
||||||
|
@state() private _showFourth = false;
|
||||||
|
|
||||||
|
protected override firstUpdated(changedProperties: PropertyValues) {
|
||||||
|
super.firstUpdated(changedProperties);
|
||||||
|
setTimeout(() => {
|
||||||
|
this._showFirst = true;
|
||||||
|
}, 200);
|
||||||
|
setTimeout(() => {
|
||||||
|
this._showSecond = true;
|
||||||
|
}, 600);
|
||||||
|
setTimeout(() => {
|
||||||
|
this._showThird = true;
|
||||||
|
}, 3000);
|
||||||
|
setTimeout(() => {
|
||||||
|
this._showFourth = true;
|
||||||
|
}, 8000);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override render() {
|
||||||
|
return html`<div class="content">
|
||||||
|
<h1>Local</h1>
|
||||||
|
<p class="secondary">
|
||||||
|
Are you sure you want to use the local voice assistant? It requires a
|
||||||
|
powerful device to run. If you device is not powerful enough, Home
|
||||||
|
Assistant cloud might be a better option.
|
||||||
|
</p>
|
||||||
|
<h3>Raspberry Pi 4</h3>
|
||||||
|
<div class="messages-container rpi">
|
||||||
|
<div class="message user ${this._showThird ? "show" : ""}">
|
||||||
|
${!this._showThird ? "…" : "Turn on the lights in the bedroom"}
|
||||||
|
</div>
|
||||||
|
${this._showThird
|
||||||
|
? html`<div class="timing user">3 seconds</div>`
|
||||||
|
: nothing}
|
||||||
|
${this._showThird
|
||||||
|
? html`<div class="message hass ${this._showFourth ? "show" : ""}">
|
||||||
|
${!this._showFourth ? "…" : "Turned on the lights"}
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
|
${this._showFourth
|
||||||
|
? html`<div class="timing hass">5 seconds</div>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
<h3>Home Assistant Cloud</h3>
|
||||||
|
<div class="messages-container cloud">
|
||||||
|
<div class="message user ${this._showFirst ? "show" : ""}">
|
||||||
|
${!this._showFirst ? "…" : "Turn on the lights in the bedroom"}
|
||||||
|
</div>
|
||||||
|
${this._showFirst
|
||||||
|
? html`<div class="timing user">0.2 seconds</div>`
|
||||||
|
: nothing}
|
||||||
|
${this._showFirst
|
||||||
|
? html` <div class="message hass ${this._showSecond ? "show" : ""}">
|
||||||
|
${!this._showSecond ? "…" : "Turned on the lights"}
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
|
${this._showSecond
|
||||||
|
? html`<div class="timing hass">0.4 seconds</div>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer side-by-side">
|
||||||
|
<ha-button @click=${this._goToCloud}
|
||||||
|
>Try Home Assistant Cloud</ha-button
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href=${documentationUrl(
|
||||||
|
this.hass,
|
||||||
|
"/voice_control/voice_remote_local_assistant/"
|
||||||
|
)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopenner"
|
||||||
|
>
|
||||||
|
<ha-button @click=${this._skip} unelevated>Learn more</ha-button>
|
||||||
|
</a>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _goToCloud() {
|
||||||
|
fireEvent(this, "next-step", { step: STEP.CLOUD });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _skip() {
|
||||||
|
fireEvent(this, "next-step", { step: STEP.SUCCESS });
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = [
|
||||||
|
AssistantSetupStyles,
|
||||||
|
css`
|
||||||
|
.messages-container {
|
||||||
|
padding: 24px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 195px;
|
||||||
|
background: var(--input-fill-color);
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.message {
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 18px;
|
||||||
|
clear: both;
|
||||||
|
margin: 8px 0;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 15px;
|
||||||
|
height: 36px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
|
.rpi .message {
|
||||||
|
transition: width 1s;
|
||||||
|
}
|
||||||
|
.cloud .message {
|
||||||
|
transition: width 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user {
|
||||||
|
margin-left: 24px;
|
||||||
|
margin-inline-start: 24px;
|
||||||
|
margin-inline-end: initial;
|
||||||
|
align-self: self-end;
|
||||||
|
text-align: right;
|
||||||
|
border-bottom-right-radius: 0px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
.timing.user {
|
||||||
|
align-self: self-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user.show {
|
||||||
|
width: 295px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.hass {
|
||||||
|
margin-right: 24px;
|
||||||
|
margin-inline-end: 24px;
|
||||||
|
margin-inline-start: initial;
|
||||||
|
align-self: self-start;
|
||||||
|
border-bottom-left-radius: 0px;
|
||||||
|
background-color: var(--secondary-background-color);
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
.timing.hass {
|
||||||
|
align-self: self-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.hass.show {
|
||||||
|
width: 184px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-voice-assistant-setup-step-addons": HaVoiceAssistantSetupStepAddons;
|
||||||
|
}
|
||||||
|
}
|
@@ -16,7 +16,7 @@ export class HaVoiceAssistantSetupStepArea extends LitElement {
|
|||||||
const device = this.hass.devices[this.deviceId];
|
const device = this.hass.devices[this.deviceId];
|
||||||
|
|
||||||
return html`<div class="content">
|
return html`<div class="content">
|
||||||
<img src="/static/images/voice-assistant/area.gif" />
|
<img src="/static/icons/casita/loving.png" />
|
||||||
<h1>Select area</h1>
|
<h1>Select area</h1>
|
||||||
<p class="secondary">
|
<p class="secondary">
|
||||||
When you voice assistant knows where it is, it can better control the
|
When you voice assistant knows where it is, it can better control the
|
||||||
|
@@ -10,7 +10,6 @@ import { STEP } from "./voice-assistant-setup-dialog";
|
|||||||
import { AssistantSetupStyles } from "./styles";
|
import { AssistantSetupStyles } from "./styles";
|
||||||
import "../../components/ha-md-list";
|
import "../../components/ha-md-list";
|
||||||
import "../../components/ha-md-list-item";
|
import "../../components/ha-md-list-item";
|
||||||
import { formatLanguageCode } from "../../common/language/format_language";
|
|
||||||
|
|
||||||
@customElement("ha-voice-assistant-setup-step-change-wake-word")
|
@customElement("ha-voice-assistant-setup-step-change-wake-word")
|
||||||
export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
|
export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
|
||||||
@@ -23,12 +22,11 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
|
|||||||
|
|
||||||
protected override render() {
|
protected override render() {
|
||||||
return html`<div class="padding content">
|
return html`<div class="padding content">
|
||||||
<img src="/static/images/voice-assistant/change-wake-word.gif" />
|
<img src="/static/icons/casita/smiling.png" />
|
||||||
<h1>Change wake word</h1>
|
<h1>Change wake word</h1>
|
||||||
<p class="secondary">
|
<p class="secondary">
|
||||||
Some wake words are better for
|
Some wake words are better for [your language] and voice than others.
|
||||||
${formatLanguageCode(this.hass.locale.language, this.hass.locale)} and
|
Please try them out.
|
||||||
voice than others. Please try them out.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ha-md-list>
|
<ha-md-list>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { html, LitElement, nothing, PropertyValues } from "lit";
|
import { html, LitElement, PropertyValues } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { testAssistSatelliteConnection } from "../../data/assist_satellite";
|
import { testAssistSatelliteConnection } from "../../data/assist_satellite";
|
||||||
@@ -13,8 +13,6 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
|
|||||||
|
|
||||||
@state() private _status?: "success" | "timeout";
|
@state() private _status?: "success" | "timeout";
|
||||||
|
|
||||||
@state() private _showLoader = false;
|
|
||||||
|
|
||||||
protected override willUpdate(changedProperties: PropertyValues): void {
|
protected override willUpdate(changedProperties: PropertyValues): void {
|
||||||
super.willUpdate(changedProperties);
|
super.willUpdate(changedProperties);
|
||||||
if (!this.hasUpdated) {
|
if (!this.hasUpdated) {
|
||||||
@@ -32,48 +30,39 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
|
|||||||
|
|
||||||
protected override render() {
|
protected override render() {
|
||||||
return html`<div class="content">
|
return html`<div class="content">
|
||||||
${this._status === "timeout"
|
${this._status === "success"
|
||||||
? html`<img src="/static/images/voice-assistant/error.gif" />
|
? html`<img src="/static/icons/casita/smiling.png" />
|
||||||
<h1>The voice assistant is unable to connect to Home Assistant</h1>
|
|
||||||
<p class="secondary">
|
|
||||||
To play audio, the voice assistant device has to connect to Home
|
|
||||||
Assistant to fetch the files. Our test shows that the device is
|
|
||||||
unable to reach the Home Assistant server.
|
|
||||||
</p>
|
|
||||||
<div class="footer">
|
|
||||||
<a
|
|
||||||
href="https://www.home-assistant.io/docs/configuration/remote/#adding-a-remote-url-to-home-assistant"
|
|
||||||
><ha-button>Help me</ha-button></a
|
|
||||||
>
|
|
||||||
<ha-button @click=${this._testConnection}>Retry</ha-button>
|
|
||||||
</div>`
|
|
||||||
: html`<img src="/static/images/voice-assistant/hi.gif" />
|
|
||||||
<h1>Hi</h1>
|
<h1>Hi</h1>
|
||||||
<p class="secondary">
|
<p class="secondary">
|
||||||
Over the next couple steps we're going to personalize your voice
|
With a couple of steps we are going to setup your voice assistant.
|
||||||
assistant.
|
</p>`
|
||||||
</p>
|
: this._status === "timeout"
|
||||||
|
? html`<img src="/static/icons/casita/sad.png" />
|
||||||
${this._showLoader
|
<h1>Voice assistant can not connect to Home Assistant</h1>
|
||||||
? html`<ha-circular-progress
|
<p class="secondary">
|
||||||
indeterminate
|
A good explanation what is happening and what action you should
|
||||||
></ha-circular-progress>`
|
take.
|
||||||
: nothing} `}
|
</p>
|
||||||
|
<div class="footer">
|
||||||
|
<a href="#"><ha-button>Help me</ha-button></a>
|
||||||
|
<ha-button @click=${this._testConnection}>Retry</ha-button>
|
||||||
|
</div>`
|
||||||
|
: html`<img src="/static/icons/casita/loading.png" />
|
||||||
|
<h1>Checking...</h1>
|
||||||
|
<p class="secondary">
|
||||||
|
We are checking if the device can reach your Home Assistant
|
||||||
|
instance.
|
||||||
|
</p>
|
||||||
|
<ha-circular-progress indeterminate></ha-circular-progress>`}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _testConnection() {
|
private async _testConnection() {
|
||||||
this._status = undefined;
|
this._status = undefined;
|
||||||
this._showLoader = false;
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
this._showLoader = true;
|
|
||||||
}, 3000);
|
|
||||||
const result = await testAssistSatelliteConnection(
|
const result = await testAssistSatelliteConnection(
|
||||||
this.hass,
|
this.hass,
|
||||||
this.assistEntityId!
|
this.assistEntityId!
|
||||||
);
|
);
|
||||||
clearTimeout(timeout);
|
|
||||||
this._showLoader = false;
|
|
||||||
this._status = result.status;
|
this._status = result.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,9 +1,7 @@
|
|||||||
import { mdiEarth, mdiMicrophoneMessage, mdiOpenInNew } from "@mdi/js";
|
import { html, LitElement } from "lit";
|
||||||
import { css, html, LitElement } from "lit";
|
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import { brandsUrl } from "../../util/brands-url";
|
|
||||||
import { AssistantSetupStyles } from "./styles";
|
import { AssistantSetupStyles } from "./styles";
|
||||||
|
|
||||||
@customElement("ha-voice-assistant-setup-step-cloud")
|
@customElement("ha-voice-assistant-setup-step-cloud")
|
||||||
@@ -12,92 +10,22 @@ export class HaVoiceAssistantSetupStepCloud extends LitElement {
|
|||||||
|
|
||||||
protected override render() {
|
protected override render() {
|
||||||
return html`<div class="content">
|
return html`<div class="content">
|
||||||
<img
|
<img src="/static/images/logo_nabu_casa.png" />
|
||||||
src=${`/static/images/logo_nabu_casa${this.hass.themes?.darkMode ? "_dark" : ""}.png`}
|
<h1>Supercharge your assistant with Home Assistant Cloud</h1>
|
||||||
alt="Nabu Casa logo"
|
<p class="secondary">
|
||||||
/>
|
Speed up and take the load off your system by running your
|
||||||
<h1>The power of Home Assistant Cloud</h1>
|
text-to-speech and speech-to-text in our private and secure cloud.
|
||||||
<div class="features">
|
Cloud also includes secure remote access to your system while
|
||||||
<div class="feature speech">
|
supporting the development of Home Assistant.
|
||||||
<div class="logos">
|
</p>
|
||||||
<div class="round-icon">
|
|
||||||
<ha-svg-icon .path=${mdiMicrophoneMessage}></ha-svg-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h2>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.voice_assistants.assistants.cloud.features.speech.title"
|
|
||||||
)}
|
|
||||||
<span class="no-wrap"></span>
|
|
||||||
</h2>
|
|
||||||
<p>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.voice_assistants.assistants.cloud.features.speech.text"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature access">
|
|
||||||
<div class="logos">
|
|
||||||
<div class="round-icon">
|
|
||||||
<ha-svg-icon .path=${mdiEarth}></ha-svg-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h2>
|
|
||||||
Remote access
|
|
||||||
<span class="no-wrap"></span>
|
|
||||||
</h2>
|
|
||||||
<p>
|
|
||||||
Secure remote access to your system while supporting the
|
|
||||||
development of Home Assistant.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature">
|
|
||||||
<div class="logos">
|
|
||||||
<img
|
|
||||||
alt="Google Assistant"
|
|
||||||
src=${brandsUrl({
|
|
||||||
domain: "google_assistant",
|
|
||||||
type: "icon",
|
|
||||||
darkOptimized: this.hass.themes?.darkMode,
|
|
||||||
})}
|
|
||||||
crossorigin="anonymous"
|
|
||||||
referrerpolicy="no-referrer"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
alt="Amazon Alexa"
|
|
||||||
src=${brandsUrl({
|
|
||||||
domain: "alexa",
|
|
||||||
type: "icon",
|
|
||||||
darkOptimized: this.hass.themes?.darkMode,
|
|
||||||
})}
|
|
||||||
crossorigin="anonymous"
|
|
||||||
referrerpolicy="no-referrer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<h2>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.voice_assistants.assistants.cloud.features.assistants.title"
|
|
||||||
)}
|
|
||||||
</h2>
|
|
||||||
<p>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.voice_assistants.assistants.cloud.features.assistants.text"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="footer side-by-side">
|
<div class="footer side-by-side">
|
||||||
<a
|
<a
|
||||||
href="https://www.nabucasa.com"
|
href="https://www.nabucasa.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopenner"
|
rel="noreferrer noopenner"
|
||||||
|
><ha-button>Learn more</ha-button></a
|
||||||
>
|
>
|
||||||
<ha-button>
|
|
||||||
<ha-svg-icon .path=${mdiOpenInNew} slot="icon"></ha-svg-icon>
|
|
||||||
nabucasa.com
|
|
||||||
</ha-button>
|
|
||||||
</a>
|
|
||||||
<a href="/config/cloud/register" @click=${this._close}
|
<a href="/config/cloud/register" @click=${this._close}
|
||||||
><ha-button unelevated>Try 1 month for free</ha-button></a
|
><ha-button unelevated>Try 1 month for free</ha-button></a
|
||||||
>
|
>
|
||||||
@@ -108,58 +36,7 @@ export class HaVoiceAssistantSetupStepCloud extends LitElement {
|
|||||||
fireEvent(this, "closed");
|
fireEvent(this, "closed");
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = [
|
static styles = AssistantSetupStyles;
|
||||||
AssistantSetupStyles,
|
|
||||||
css`
|
|
||||||
.features {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
grid-gap: 16px;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
.feature {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.feature .logos {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.feature .logos > * {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
margin: 0 4px;
|
|
||||||
}
|
|
||||||
.round-icon {
|
|
||||||
border-radius: 50%;
|
|
||||||
color: #6e41ab;
|
|
||||||
background-color: #e8dcf7;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
.access .round-icon {
|
|
||||||
color: #00aef8;
|
|
||||||
background-color: #cceffe;
|
|
||||||
}
|
|
||||||
.feature h2 {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 24px;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.feature p {
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 20px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@@ -32,10 +32,6 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
|||||||
|
|
||||||
@state() private _showSecond = false;
|
@state() private _showSecond = false;
|
||||||
|
|
||||||
@state() private _showThird = false;
|
|
||||||
|
|
||||||
@state() private _showFourth = false;
|
|
||||||
|
|
||||||
protected override willUpdate(changedProperties: PropertyValues): void {
|
protected override willUpdate(changedProperties: PropertyValues): void {
|
||||||
super.willUpdate(changedProperties);
|
super.willUpdate(changedProperties);
|
||||||
|
|
||||||
@@ -48,83 +44,63 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
|||||||
super.firstUpdated(changedProperties);
|
super.firstUpdated(changedProperties);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this._showFirst = true;
|
this._showFirst = true;
|
||||||
}, 200);
|
}, 1);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this._showSecond = true;
|
this._showSecond = true;
|
||||||
}, 600);
|
}, 1500);
|
||||||
setTimeout(() => {
|
|
||||||
this._showThird = true;
|
|
||||||
}, 3000);
|
|
||||||
setTimeout(() => {
|
|
||||||
this._showFourth = true;
|
|
||||||
}, 8000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override render() {
|
protected override render() {
|
||||||
return html`<div class="content">
|
return html`<div class="padding content">
|
||||||
<h1>What hardware do you want to use?</h1>
|
<div class="messages-container">
|
||||||
<p class="secondary">
|
|
||||||
How quickly your assistant responds depends on the power of the
|
|
||||||
hardware.
|
|
||||||
</p>
|
|
||||||
<div class="container">
|
|
||||||
<div class="messages-container cloud">
|
|
||||||
<div class="message user ${this._showFirst ? "show" : ""}">
|
<div class="message user ${this._showFirst ? "show" : ""}">
|
||||||
${!this._showFirst ? "…" : "Turn on the lights in the bedroom"}
|
${!this._showFirst ? "…" : "Turn on the lights in the bedroom"}
|
||||||
</div>
|
</div>
|
||||||
${this._showFirst
|
|
||||||
? html`<div class="timing user">0.2 seconds</div>`
|
|
||||||
: nothing}
|
|
||||||
${this._showFirst
|
${this._showFirst
|
||||||
? html` <div class="message hass ${this._showSecond ? "show" : ""}">
|
? html` <div class="message hass ${this._showSecond ? "show" : ""}">
|
||||||
${!this._showSecond ? "…" : "Turned on the lights"}
|
${!this._showSecond ? "…" : "Turned on the lights"}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing}
|
||||||
${this._showSecond
|
|
||||||
? html`<div class="timing hass">0.4 seconds</div>`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
</div>
|
||||||
<h2>Home Assistant Cloud</h2>
|
<h1>Select system</h1>
|
||||||
<p>Ideal if you don't have a powerful system at home.</p>
|
<p class="secondary">
|
||||||
<ha-button @click=${this._setupCloud}>Learn more</ha-button>
|
How quickly your voice assistant responds depends on the power of your
|
||||||
</div>
|
system.
|
||||||
<div class="container">
|
|
||||||
<div class="messages-container rpi">
|
|
||||||
<div class="message user ${this._showThird ? "show" : ""}">
|
|
||||||
${!this._showThird ? "…" : "Turn on the lights in the bedroom"}
|
|
||||||
</div>
|
|
||||||
${this._showThird
|
|
||||||
? html`<div class="timing user">3 seconds</div>`
|
|
||||||
: nothing}
|
|
||||||
${this._showThird
|
|
||||||
? html`<div class="message hass ${this._showFourth ? "show" : ""}">
|
|
||||||
${!this._showFourth ? "…" : "Turned on the lights"}
|
|
||||||
</div>`
|
|
||||||
: nothing}
|
|
||||||
${this._showFourth
|
|
||||||
? html`<div class="timing hass">5 seconds</div>`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
|
||||||
<h2>Do-it-yourself</h2>
|
|
||||||
<p>
|
|
||||||
Install add-ons or containers to run it on your own system. Powerful
|
|
||||||
hardware is needed for fast responses.
|
|
||||||
</p>
|
</p>
|
||||||
<a
|
</div>
|
||||||
|
<ha-md-list>
|
||||||
|
<ha-md-list-item interactive type="button" @click=${this._setupCloud}>
|
||||||
|
Home Assistant Cloud
|
||||||
|
<span slot="supporting-text"
|
||||||
|
>Ideal if you don't have a powerful system at home</span
|
||||||
|
>
|
||||||
|
<ha-icon-next slot="end"></ha-icon-next>
|
||||||
|
</ha-md-list-item>
|
||||||
|
<ha-md-list-item interactive type="button" @click=${this._thisSystem}>
|
||||||
|
On this system
|
||||||
|
<span slot="supporting-text"
|
||||||
|
>Local setup with the Whisper and Piper add-ons</span
|
||||||
|
>
|
||||||
|
<ha-icon-next slot="end"></ha-icon-next>
|
||||||
|
</ha-md-list-item>
|
||||||
|
<ha-md-list-item
|
||||||
|
interactive
|
||||||
|
type="link"
|
||||||
href=${documentationUrl(
|
href=${documentationUrl(
|
||||||
this.hass,
|
this.hass,
|
||||||
"/voice_control/voice_remote_local_assistant/"
|
"/voice_control/voice_remote_local_assistant/"
|
||||||
)}
|
)}
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopenner"
|
rel="noreferrer noopenner"
|
||||||
|
target="_blank"
|
||||||
|
@click=${this._skip}
|
||||||
>
|
>
|
||||||
<ha-button @click=${this._skip}>
|
Use external system
|
||||||
<ha-svg-icon .path=${mdiOpenInNew} slot="icon"></ha-svg-icon>
|
<span slot="supporting-text"
|
||||||
Learn more</ha-button
|
>Learn more about how to host it on another system</span
|
||||||
>
|
>
|
||||||
</a>
|
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||||
</div>
|
</ha-md-list-item>
|
||||||
</div>`;
|
</ha-md-list>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _checkCloud() {
|
private async _checkCloud() {
|
||||||
@@ -241,6 +217,10 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
|||||||
this._nextStep(STEP.CLOUD);
|
this._nextStep(STEP.CLOUD);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _thisSystem() {
|
||||||
|
this._nextStep(STEP.ADDONS);
|
||||||
|
}
|
||||||
|
|
||||||
private _skip() {
|
private _skip() {
|
||||||
this._nextStep(STEP.SUCCESS);
|
this._nextStep(STEP.SUCCESS);
|
||||||
}
|
}
|
||||||
@@ -252,22 +232,21 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
|||||||
static styles = [
|
static styles = [
|
||||||
AssistantSetupStyles,
|
AssistantSetupStyles,
|
||||||
css`
|
css`
|
||||||
.container {
|
:host {
|
||||||
border-radius: 16px;
|
padding: 0;
|
||||||
border: 1px solid var(--divider-color);
|
|
||||||
overflow: hidden;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
}
|
}
|
||||||
.container:last-child {
|
.padding {
|
||||||
margin-top: 16px;
|
padding: 24px;
|
||||||
}
|
}
|
||||||
|
ha-md-list {
|
||||||
|
width: 100%;
|
||||||
|
text-align: initial;
|
||||||
|
}
|
||||||
|
|
||||||
.messages-container {
|
.messages-container {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: 195px;
|
height: 152px;
|
||||||
background: var(--input-fill-color);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
.message {
|
.message {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -280,29 +259,21 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
width: 30px;
|
|
||||||
}
|
|
||||||
.rpi .message {
|
|
||||||
transition: width 1s;
|
transition: width 1s;
|
||||||
}
|
width: 30px;
|
||||||
.cloud .message {
|
|
||||||
transition: width 0.5s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.message.user {
|
.message.user {
|
||||||
margin-left: 24px;
|
margin-left: 24px;
|
||||||
margin-inline-start: 24px;
|
margin-inline-start: 24px;
|
||||||
margin-inline-end: initial;
|
margin-inline-end: initial;
|
||||||
align-self: self-end;
|
float: var(--float-end);
|
||||||
text-align: right;
|
text-align: right;
|
||||||
border-bottom-right-radius: 0px;
|
border-bottom-right-radius: 0px;
|
||||||
background-color: var(--primary-color);
|
background-color: var(--primary-color);
|
||||||
color: var(--text-primary-color);
|
color: var(--text-primary-color);
|
||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
.timing.user {
|
|
||||||
align-self: self-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.user.show {
|
.message.user.show {
|
||||||
width: 295px;
|
width: 295px;
|
||||||
@@ -312,15 +283,12 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
|||||||
margin-right: 24px;
|
margin-right: 24px;
|
||||||
margin-inline-end: 24px;
|
margin-inline-end: 24px;
|
||||||
margin-inline-start: initial;
|
margin-inline-start: initial;
|
||||||
align-self: self-start;
|
float: var(--float-start);
|
||||||
border-bottom-left-radius: 0px;
|
border-bottom-left-radius: 0px;
|
||||||
background-color: var(--secondary-background-color);
|
background-color: var(--secondary-background-color);
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
.timing.hass {
|
|
||||||
align-self: self-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.hass.show {
|
.message.hass.show {
|
||||||
width: 184px;
|
width: 184px;
|
||||||
|
@@ -66,11 +66,11 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return html`<div class="content">
|
return html`<div class="content">
|
||||||
<img src="/static/images/voice-assistant/heart.gif" />
|
<img src="/static/icons/casita/loving.png" />
|
||||||
<h1>Ready to Assist!</h1>
|
<h1>Ready to assist!</h1>
|
||||||
<p class="secondary">
|
<p class="secondary">
|
||||||
Make any final customizations here. You can always change these in the
|
Your device is all ready to go! If you want to tweak some more
|
||||||
Voice Assistants section of the settings page.
|
settings, you can change that below.
|
||||||
</p>
|
</p>
|
||||||
<div class="rows">
|
<div class="rows">
|
||||||
${this.assistConfiguration &&
|
${this.assistConfiguration &&
|
||||||
|
@@ -2,13 +2,7 @@ import { css, html, LitElement, nothing, PropertyValues } from "lit";
|
|||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import "../../components/ha-circular-progress";
|
import "../../components/ha-circular-progress";
|
||||||
import { ON, UNAVAILABLE } from "../../data/entity";
|
import { OFF, ON, UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||||
import {
|
|
||||||
updateCanInstall,
|
|
||||||
UpdateEntity,
|
|
||||||
updateIsInstalling,
|
|
||||||
updateUsesProgress,
|
|
||||||
} from "../../data/update";
|
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import { AssistantSetupStyles } from "./styles";
|
import { AssistantSetupStyles } from "./styles";
|
||||||
|
|
||||||
@@ -57,19 +51,17 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stateObj = this.hass.states[this.updateEntityId] as
|
const stateObj = this.hass.states[this.updateEntityId];
|
||||||
| UpdateEntity
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
const progressIsNumeric = stateObj && updateUsesProgress(stateObj);
|
const progressIsNumeric =
|
||||||
|
typeof stateObj?.attributes.in_progress === "number";
|
||||||
|
|
||||||
return html`<div class="content">
|
return html`<div class="content">
|
||||||
<img src="/static/images/voice-assistant/update.gif" />
|
<img src="/static/icons/casita/loading.png" />
|
||||||
<h1>
|
<h1>
|
||||||
${stateObj &&
|
${stateObj.state === OFF || stateObj.state === UNKNOWN
|
||||||
(stateObj.state === "unavailable" || updateIsInstalling(stateObj))
|
? "Checking for updates"
|
||||||
? "Updating your voice assistant"
|
: "Updating your voice assistant"}
|
||||||
: "Checking for updates"}
|
|
||||||
</h1>
|
</h1>
|
||||||
<p class="secondary">
|
<p class="secondary">
|
||||||
We are making sure you have the latest and greatest version of your
|
We are making sure you have the latest and greatest version of your
|
||||||
@@ -77,15 +69,15 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
|
|||||||
</p>
|
</p>
|
||||||
<ha-circular-progress
|
<ha-circular-progress
|
||||||
.value=${progressIsNumeric
|
.value=${progressIsNumeric
|
||||||
? (stateObj.attributes.update_percentage as number) / 100
|
? stateObj.attributes.in_progress / 100
|
||||||
: undefined}
|
: undefined}
|
||||||
.indeterminate=${!progressIsNumeric}
|
.indeterminate=${!progressIsNumeric}
|
||||||
></ha-circular-progress>
|
></ha-circular-progress>
|
||||||
<p>
|
<p>
|
||||||
${stateObj?.state === UNAVAILABLE
|
${stateObj.state === "unavailable"
|
||||||
? "Restarting voice assistant"
|
? "Restarting voice assistant"
|
||||||
: progressIsNumeric
|
: progressIsNumeric
|
||||||
? `Installing ${stateObj.attributes.update_percentage}%`
|
? `Installing ${stateObj.attributes.in_progress}%`
|
||||||
: ""}
|
: ""}
|
||||||
</p>
|
</p>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -96,14 +88,8 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
|
|||||||
if (!this.updateEntityId) {
|
if (!this.updateEntityId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const updateEntity = this.hass.states[this.updateEntityId] as
|
const updateEntity = this.hass.states[this.updateEntityId];
|
||||||
| UpdateEntity
|
if (updateEntity && this.hass.states[updateEntity.entity_id].state === ON) {
|
||||||
| undefined;
|
|
||||||
if (
|
|
||||||
updateEntity &&
|
|
||||||
this.hass.states[updateEntity.entity_id].state === ON &&
|
|
||||||
updateCanInstall(updateEntity)
|
|
||||||
) {
|
|
||||||
this._updated = true;
|
this._updated = true;
|
||||||
await this.hass.callService(
|
await this.hass.callService(
|
||||||
"update",
|
"update",
|
||||||
|
@@ -65,14 +65,14 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
|
|||||||
return html`<div class="content">
|
return html`<div class="content">
|
||||||
${!this._detected
|
${!this._detected
|
||||||
? html`
|
? html`
|
||||||
<img src="/static/images/voice-assistant/sleep.gif" />
|
<img src="/static/icons/casita/sleeping.png" />
|
||||||
<h1>
|
<h1>
|
||||||
Say “${this._activeWakeWord(this.assistConfiguration)}” to wake the
|
Say “${this._activeWakeWord(this.assistConfiguration)}” to wake the
|
||||||
device up
|
device up
|
||||||
</h1>
|
</h1>
|
||||||
<p class="secondary">Setup will continue once the device is awake.</p>
|
<p class="secondary">Setup will continue once the device is awake.</p>
|
||||||
</div>`
|
</div>`
|
||||||
: html`<img src="/static/images/voice-assistant/ok-nabu.gif" />
|
: html`<img src="/static/icons/casita/normal.png" />
|
||||||
<h1>
|
<h1>
|
||||||
Say “${this._activeWakeWord(this.assistConfiguration)}” again
|
Say “${this._activeWakeWord(this.assistConfiguration)}” again
|
||||||
</h1>
|
</h1>
|
||||||
@@ -80,7 +80,7 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
|
|||||||
To make sure the wake word works for you.
|
To make sure the wake word works for you.
|
||||||
</p>`}
|
</p>`}
|
||||||
</div>
|
</div>
|
||||||
<div class="footer centered">
|
<div class="footer full-width">
|
||||||
<ha-button @click=${this._changeWakeWord}>Change wake word</ha-button>
|
<ha-button @click=${this._changeWakeWord}>Change wake word</ha-button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
@@ -125,15 +125,7 @@ class HassSubpage extends LitElement {
|
|||||||
.main-title {
|
.main-title {
|
||||||
margin: var(--margin-title);
|
margin: var(--margin-title);
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
min-width: 0;
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow-wrap: break-word;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
padding-bottom: 1px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
@@ -43,13 +43,8 @@ import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
|||||||
import { ACTION_ICONS, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
|
import { ACTION_ICONS, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
|
||||||
import { AutomationClipboard } from "../../../../data/automation";
|
import { AutomationClipboard } from "../../../../data/automation";
|
||||||
import { validateConfig } from "../../../../data/config";
|
import { validateConfig } from "../../../../data/config";
|
||||||
import {
|
import { fullEntitiesContext, labelsContext } from "../../../../data/context";
|
||||||
floorsContext,
|
|
||||||
fullEntitiesContext,
|
|
||||||
labelsContext,
|
|
||||||
} from "../../../../data/context";
|
|
||||||
import { EntityRegistryEntry } from "../../../../data/entity_registry";
|
import { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||||
import { FloorRegistryEntry } from "../../../../data/floor_registry";
|
|
||||||
import { LabelRegistryEntry } from "../../../../data/label_registry";
|
import { LabelRegistryEntry } from "../../../../data/label_registry";
|
||||||
import {
|
import {
|
||||||
Action,
|
Action,
|
||||||
@@ -159,10 +154,6 @@ export default class HaAutomationActionRow extends LitElement {
|
|||||||
@consume({ context: labelsContext, subscribe: true })
|
@consume({ context: labelsContext, subscribe: true })
|
||||||
_labelReg!: LabelRegistryEntry[];
|
_labelReg!: LabelRegistryEntry[];
|
||||||
|
|
||||||
@state()
|
|
||||||
@consume({ context: floorsContext, subscribe: true })
|
|
||||||
_floorReg!: { [id: string]: FloorRegistryEntry };
|
|
||||||
|
|
||||||
@state() private _warnings?: string[];
|
@state() private _warnings?: string[];
|
||||||
|
|
||||||
@state() private _uiModeAvailable = true;
|
@state() private _uiModeAvailable = true;
|
||||||
@@ -231,7 +222,6 @@ export default class HaAutomationActionRow extends LitElement {
|
|||||||
this.hass,
|
this.hass,
|
||||||
this._entityReg,
|
this._entityReg,
|
||||||
this._labelReg,
|
this._labelReg,
|
||||||
this._floorReg,
|
|
||||||
this.action
|
this.action
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
@@ -603,7 +593,6 @@ export default class HaAutomationActionRow extends LitElement {
|
|||||||
this.hass,
|
this.hass,
|
||||||
this._entityReg,
|
this._entityReg,
|
||||||
this._labelReg,
|
this._labelReg,
|
||||||
this._floorReg,
|
|
||||||
this.action,
|
this.action,
|
||||||
undefined,
|
undefined,
|
||||||
true
|
true
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { consume } from "@lit-labs/context";
|
import { consume } from "@lit-labs/context";
|
||||||
import { css, html, LitElement, PropertyValues } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||||
@@ -56,28 +56,6 @@ export class HaDeviceAction extends LitElement {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
public shouldUpdate(changedProperties: PropertyValues) {
|
|
||||||
if (!changedProperties.has("action")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
this.action.device_id &&
|
|
||||||
!(this.action.device_id in this.hass.devices)
|
|
||||||
) {
|
|
||||||
fireEvent(
|
|
||||||
this,
|
|
||||||
"ui-mode-not-available",
|
|
||||||
Error(
|
|
||||||
this.hass.localize(
|
|
||||||
"ui.panel.config.automation.editor.edit_unknown_device"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const deviceId = this._deviceId || this.action.device_id;
|
const deviceId = this._deviceId || this.action.device_id;
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { consume } from "@lit-labs/context";
|
import { consume } from "@lit-labs/context";
|
||||||
import { css, html, LitElement, PropertyValues } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||||
@@ -57,28 +57,6 @@ export class HaDeviceCondition extends LitElement {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
public shouldUpdate(changedProperties: PropertyValues) {
|
|
||||||
if (!changedProperties.has("condition")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
this.condition.device_id &&
|
|
||||||
!(this.condition.device_id in this.hass.devices)
|
|
||||||
) {
|
|
||||||
fireEvent(
|
|
||||||
this,
|
|
||||||
"ui-mode-not-available",
|
|
||||||
Error(
|
|
||||||
this.hass.localize(
|
|
||||||
"ui.panel.config.automation.editor.edit_unknown_device"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const deviceId = this._deviceId || this.condition.device_id;
|
const deviceId = this._deviceId || this.condition.device_id;
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { consume } from "@lit-labs/context";
|
import { consume } from "@lit-labs/context";
|
||||||
import { css, html, LitElement, PropertyValues } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||||
@@ -61,28 +61,6 @@ export class HaDeviceTrigger extends LitElement {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
public shouldUpdate(changedProperties: PropertyValues) {
|
|
||||||
if (!changedProperties.has("trigger")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
this.trigger.device_id &&
|
|
||||||
!(this.trigger.device_id in this.hass.devices)
|
|
||||||
) {
|
|
||||||
fireEvent(
|
|
||||||
this,
|
|
||||||
"ui-mode-not-available",
|
|
||||||
Error(
|
|
||||||
this.hass.localize(
|
|
||||||
"ui.panel.config.automation.editor.edit_unknown_device"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const deviceId = this._deviceId || this.trigger.device_id;
|
const deviceId = this._deviceId || this.trigger.device_id;
|
||||||
|
|
||||||
|
@@ -251,7 +251,9 @@ export class HaDeviceEntitiesCard extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
ha-icon {
|
ha-icon {
|
||||||
margin-left: -8px;
|
margin-left: 8px;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
margin-inline-end: initial;
|
||||||
}
|
}
|
||||||
.entity-id {
|
.entity-id {
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
@@ -281,9 +283,6 @@ export class HaDeviceEntitiesCard extends LitElement {
|
|||||||
.name {
|
.name {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
.name:dir(rtl) {
|
|
||||||
margin-inline-start: 8px;
|
|
||||||
}
|
|
||||||
.empty {
|
.empty {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -303,9 +302,6 @@ export class HaDeviceEntitiesCard extends LitElement {
|
|||||||
outline: none;
|
outline: none;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
ha-list-item {
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,7 +9,6 @@ import {
|
|||||||
SecurityClass,
|
SecurityClass,
|
||||||
unprovisionZwaveSmartStartNode,
|
unprovisionZwaveSmartStartNode,
|
||||||
} from "../../../../../data/zwave_js";
|
} from "../../../../../data/zwave_js";
|
||||||
import { LocalizeFunc } from "../../../../../common/translations/localize";
|
|
||||||
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||||
import "../../../../../layouts/hass-tabs-subpage-data-table";
|
import "../../../../../layouts/hass-tabs-subpage-data-table";
|
||||||
import { HomeAssistant, Route } from "../../../../../types";
|
import { HomeAssistant, Route } from "../../../../../types";
|
||||||
@@ -34,7 +33,7 @@ class ZWaveJSProvisioned extends LitElement {
|
|||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
.route=${this.route}
|
.route=${this.route}
|
||||||
.tabs=${configTabs}
|
.tabs=${configTabs}
|
||||||
.columns=${this._columns(this.hass.localize)}
|
.columns=${this._columns(this.narrow)}
|
||||||
.data=${this._provisioningEntries}
|
.data=${this._provisioningEntries}
|
||||||
>
|
>
|
||||||
</hass-tabs-subpage-data-table>
|
</hass-tabs-subpage-data-table>
|
||||||
@@ -42,12 +41,11 @@ class ZWaveJSProvisioned extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _columns = memoizeOne(
|
private _columns = memoizeOne(
|
||||||
(
|
(narrow: boolean): DataTableColumnContainer<ZwaveJSProvisioningEntry> => ({
|
||||||
localize: LocalizeFunc
|
|
||||||
): DataTableColumnContainer<ZwaveJSProvisioningEntry> => ({
|
|
||||||
included: {
|
included: {
|
||||||
showNarrow: true,
|
title: this.hass.localize(
|
||||||
title: localize("ui.panel.config.zwave_js.provisioned.included"),
|
"ui.panel.config.zwave_js.provisioned.included"
|
||||||
|
),
|
||||||
type: "icon",
|
type: "icon",
|
||||||
template: (entry) =>
|
template: (entry) =>
|
||||||
entry.nodeId
|
entry.nodeId
|
||||||
@@ -69,16 +67,16 @@ class ZWaveJSProvisioned extends LitElement {
|
|||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
dsk: {
|
dsk: {
|
||||||
main: true,
|
title: this.hass.localize("ui.panel.config.zwave_js.provisioned.dsk"),
|
||||||
title: localize("ui.panel.config.zwave_js.provisioned.dsk"),
|
|
||||||
sortable: true,
|
sortable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
flex: 2,
|
flex: 2,
|
||||||
},
|
},
|
||||||
security_classes: {
|
security_classes: {
|
||||||
title: localize(
|
title: this.hass.localize(
|
||||||
"ui.panel.config.zwave_js.provisioned.security_classes"
|
"ui.panel.config.zwave_js.provisioned.security_classes"
|
||||||
),
|
),
|
||||||
|
hidden: narrow,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
template: (entry) => {
|
template: (entry) => {
|
||||||
@@ -93,8 +91,9 @@ class ZWaveJSProvisioned extends LitElement {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
unprovision: {
|
unprovision: {
|
||||||
showNarrow: true,
|
title: this.hass.localize(
|
||||||
title: localize("ui.panel.config.zwave_js.provisioned.unprovison"),
|
"ui.panel.config.zwave_js.provisioned.unprovison"
|
||||||
|
),
|
||||||
type: "icon-button",
|
type: "icon-button",
|
||||||
template: (entry) => html`
|
template: (entry) => html`
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
|
@@ -103,7 +103,6 @@ export class HaConfigLabels extends LitElement {
|
|||||||
style="
|
style="
|
||||||
background-color: ${computeCssColor(label.color)};
|
background-color: ${computeCssColor(label.color)};
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
outline: 1px solid var(--outline-color);
|
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;"
|
height: 20px;"
|
||||||
></div>`
|
></div>`
|
||||||
|
@@ -1,155 +0,0 @@
|
|||||||
import { mdiClose } from "@mdi/js";
|
|
||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
|
||||||
import "../../../components/ha-md-dialog";
|
|
||||||
import "../../../components/ha-button";
|
|
||||||
import "../../../components/ha-dialog-header";
|
|
||||||
import "../../../components/ha-icon-button";
|
|
||||||
import type { HaMdDialog } from "../../../components/ha-md-dialog";
|
|
||||||
import { HomeAssistant } from "../../../types";
|
|
||||||
import { haStyle, haStyleDialog } from "../../../resources/styles";
|
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
|
||||||
import { DownloadLogsDialogParams } from "./show-dialog-download-logs";
|
|
||||||
import "../../../components/ha-select";
|
|
||||||
import "../../../components/ha-list-item";
|
|
||||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
|
||||||
import { getHassioLogDownloadLinesUrl } from "../../../data/hassio/supervisor";
|
|
||||||
import { getSignedPath } from "../../../data/auth";
|
|
||||||
import { fileDownload } from "../../../util/file_download";
|
|
||||||
|
|
||||||
@customElement("dialog-download-logs")
|
|
||||||
class DownloadLogsDialog extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@state() private _dialogParams?: DownloadLogsDialogParams;
|
|
||||||
|
|
||||||
@state() private _lineCount = 100;
|
|
||||||
|
|
||||||
@query("ha-md-dialog") private _dialogElement!: HaMdDialog;
|
|
||||||
|
|
||||||
public showDialog(dialogParams: DownloadLogsDialogParams) {
|
|
||||||
this._dialogParams = dialogParams;
|
|
||||||
this._lineCount = this._dialogParams?.defaultLineCount ?? 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
public closeDialog() {
|
|
||||||
this._dialogElement.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _dialogClosed() {
|
|
||||||
this._dialogParams = undefined;
|
|
||||||
this._lineCount = 100;
|
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
if (!this._dialogParams) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
const numberOfLinesOptions = [100, 500, 1000, 5000, 10000];
|
|
||||||
if (!numberOfLinesOptions.includes(this._lineCount)) {
|
|
||||||
numberOfLinesOptions.push(this._lineCount);
|
|
||||||
numberOfLinesOptions.sort((a, b) => a - b);
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<ha-md-dialog open @closed=${this._dialogClosed}>
|
|
||||||
<ha-dialog-header slot="headline">
|
|
||||||
<ha-icon-button
|
|
||||||
slot="navigationIcon"
|
|
||||||
@click=${this.closeDialog}
|
|
||||||
.label=${this.hass.localize("ui.common.close")}
|
|
||||||
.path=${mdiClose}
|
|
||||||
></ha-icon-button>
|
|
||||||
<span slot="title" id="dialog-light-color-favorite-title">
|
|
||||||
${this.hass.localize("ui.panel.config.logs.download_full_log")}
|
|
||||||
</span>
|
|
||||||
<span slot="subtitle">
|
|
||||||
${this._dialogParams.header}${this._dialogParams.boot === 0
|
|
||||||
? ""
|
|
||||||
: ` ⸱ ${this._dialogParams.boot === -1 ? this.hass.localize("ui.panel.config.logs.previous") : this.hass.localize("ui.panel.config.logs.startups_ago", { boot: this._dialogParams.boot * -1 })}`}
|
|
||||||
</span>
|
|
||||||
</ha-dialog-header>
|
|
||||||
<div slot="content" class="content">
|
|
||||||
<div>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.logs.select_number_of_lines"
|
|
||||||
)}:
|
|
||||||
</div>
|
|
||||||
<ha-select
|
|
||||||
.label=${this.hass.localize("ui.panel.config.logs.lines")}
|
|
||||||
@selected=${this._setNumberOfLogs}
|
|
||||||
fixedMenuPosition
|
|
||||||
naturalMenuWidth
|
|
||||||
@closed=${stopPropagation}
|
|
||||||
.value=${String(this._lineCount)}
|
|
||||||
>
|
|
||||||
${numberOfLinesOptions.map(
|
|
||||||
(option) => html`
|
|
||||||
<ha-list-item .value=${String(option)}>
|
|
||||||
${option}
|
|
||||||
</ha-list-item>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</ha-select>
|
|
||||||
</div>
|
|
||||||
<div slot="actions">
|
|
||||||
<ha-button @click=${this.closeDialog}>
|
|
||||||
${this.hass.localize("ui.common.cancel")}
|
|
||||||
</ha-button>
|
|
||||||
<ha-button @click=${this._dowloadLogs}>
|
|
||||||
${this.hass.localize("ui.common.download")}
|
|
||||||
</ha-button>
|
|
||||||
</div>
|
|
||||||
</ha-md-dialog>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _dowloadLogs() {
|
|
||||||
const provider = this._dialogParams!.provider;
|
|
||||||
const boot = this._dialogParams!.boot;
|
|
||||||
|
|
||||||
const timeString = new Date().toISOString().replace(/:/g, "-");
|
|
||||||
const downloadUrl = getHassioLogDownloadLinesUrl(
|
|
||||||
provider,
|
|
||||||
this._lineCount,
|
|
||||||
boot
|
|
||||||
);
|
|
||||||
const logFileName =
|
|
||||||
provider !== "core"
|
|
||||||
? `${provider}_${timeString}.log`
|
|
||||||
: `home-assistant_${timeString}.log`;
|
|
||||||
const signedUrl = await getSignedPath(this.hass, downloadUrl);
|
|
||||||
fileDownload(signedUrl.path, logFileName);
|
|
||||||
this.closeDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _setNumberOfLogs(ev) {
|
|
||||||
this._lineCount = Number(ev.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return [
|
|
||||||
haStyle,
|
|
||||||
haStyleDialog,
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
direction: var(--direction);
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"dialog-download-logs": DownloadLogsDialog;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,10 +1,6 @@
|
|||||||
|
import "@material/mwc-button";
|
||||||
import "@material/mwc-list/mwc-list-item";
|
import "@material/mwc-list/mwc-list-item";
|
||||||
import {
|
import { mdiRefresh, mdiDownload } from "@mdi/js";
|
||||||
mdiArrowCollapseDown,
|
|
||||||
mdiDownload,
|
|
||||||
mdiMenuDown,
|
|
||||||
mdiRefresh,
|
|
||||||
} from "@mdi/js";
|
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
@@ -12,47 +8,27 @@ import {
|
|||||||
LitElement,
|
LitElement,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
nothing,
|
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
// eslint-disable-next-line import/extensions
|
|
||||||
import { IntersectionController } from "@lit-labs/observers/intersection-controller.js";
|
|
||||||
import { customElement, property, state, query } from "lit/decorators";
|
|
||||||
import "../../../components/ha-alert";
|
import "../../../components/ha-alert";
|
||||||
import "../../../components/ha-ansi-to-html";
|
import "../../../components/ha-ansi-to-html";
|
||||||
import type { HaAnsiToHtml } from "../../../components/ha-ansi-to-html";
|
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import "../../../components/ha-button";
|
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
|
import "../../../components/ha-select";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import "../../../components/ha-circular-progress";
|
|
||||||
import "../../../components/chips/ha-assist-chip";
|
|
||||||
import "../../../components/ha-menu";
|
|
||||||
import "../../../components/ha-md-menu-item";
|
|
||||||
import "../../../components/ha-md-divider";
|
|
||||||
|
|
||||||
import { getSignedPath } from "../../../data/auth";
|
import { getSignedPath } from "../../../data/auth";
|
||||||
|
|
||||||
import { fetchErrorLog, getErrorLogDownloadUrl } from "../../../data/error_log";
|
import { fetchErrorLog, getErrorLogDownloadUrl } from "../../../data/error_log";
|
||||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||||
import {
|
import {
|
||||||
fetchHassioBoots,
|
|
||||||
fetchHassioLogs,
|
fetchHassioLogs,
|
||||||
fetchHassioLogsFollow,
|
|
||||||
getHassioLogDownloadUrl,
|
getHassioLogDownloadUrl,
|
||||||
} from "../../../data/hassio/supervisor";
|
} from "../../../data/hassio/supervisor";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { fileDownload } from "../../../util/file_download";
|
|
||||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
|
||||||
import { ConnectionStatus } from "../../../data/connection-status";
|
|
||||||
import { atLeastVersion } from "../../../common/config/version";
|
|
||||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
|
||||||
import { debounce } from "../../../common/util/debounce";
|
import { debounce } from "../../../common/util/debounce";
|
||||||
import { showDownloadLogsDialog } from "./show-dialog-download-logs";
|
import { fileDownload } from "../../../util/file_download";
|
||||||
import type { HaMenu } from "../../../components/ha-menu";
|
|
||||||
|
|
||||||
const NUMBER_OF_LINES = 100;
|
|
||||||
|
|
||||||
@customElement("error-log-card")
|
@customElement("error-log-card")
|
||||||
class ErrorLogCard extends LitElement {
|
class ErrorLogCard extends LitElement {
|
||||||
@@ -66,190 +42,52 @@ class ErrorLogCard extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean, attribute: true }) public show = false;
|
@property({ type: Boolean, attribute: true }) public show = false;
|
||||||
|
|
||||||
@query(".error-log") private _logElement?: HTMLElement;
|
@state() private _isLogLoaded = false;
|
||||||
|
|
||||||
@query("#scroll-top-marker") private _scrollTopMarkerElement?: HTMLElement;
|
@state() private _logHTML?: TemplateResult[] | TemplateResult | string;
|
||||||
|
|
||||||
@query("#scroll-bottom-marker")
|
|
||||||
private _scrollBottomMarkerElement?: HTMLElement;
|
|
||||||
|
|
||||||
@query("ha-ansi-to-html") private _ansiToHtmlElement?: HaAnsiToHtml;
|
|
||||||
|
|
||||||
@query("#boots-menu") private _bootsMenu?: HaMenu;
|
|
||||||
|
|
||||||
@state() private _firstCursor?: string;
|
|
||||||
|
|
||||||
@state() private _scrolledToBottomController =
|
|
||||||
new IntersectionController<boolean>(this, {
|
|
||||||
callback(this: IntersectionController<boolean>, entries) {
|
|
||||||
return entries[0].isIntersecting;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
@state() private _scrolledToTopController =
|
|
||||||
new IntersectionController<boolean>(this, {});
|
|
||||||
|
|
||||||
@state() private _newLogsIndicator?: boolean;
|
|
||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
@state() private _logStreamAborter?: AbortController;
|
|
||||||
|
|
||||||
@state() private _streamSupported?: boolean;
|
|
||||||
|
|
||||||
@state() private _loadingState: "loading" | "empty" | "loaded" = "loading";
|
|
||||||
|
|
||||||
@state() private _loadingPrevState?: "loading" | "end" | "loaded";
|
|
||||||
|
|
||||||
@state() private _noSearchResults: boolean = false;
|
|
||||||
|
|
||||||
@state() private _numberOfLines?: number;
|
|
||||||
|
|
||||||
@state() private _boot = 0;
|
|
||||||
|
|
||||||
@state() private _boots?: number[];
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="error-log-intro">
|
<div class="error-log-intro">
|
||||||
${this._error
|
${this._error
|
||||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
: ""}
|
: ""}
|
||||||
<ha-card outlined class=${classMap({ hidden: this.show === false })}>
|
${this._logHTML
|
||||||
<div class="header">
|
|
||||||
<h1 class="card-header">
|
|
||||||
${this.header ||
|
|
||||||
this.hass.localize("ui.panel.config.logs.show_full_logs")}
|
|
||||||
</h1>
|
|
||||||
<div class="action-buttons">
|
|
||||||
${this._streamSupported && Array.isArray(this._boots)
|
|
||||||
? html`
|
|
||||||
<ha-assist-chip
|
|
||||||
.label=${this._boot === 0
|
|
||||||
? this.hass.localize("ui.panel.config.logs.current")
|
|
||||||
: this._boot === -1
|
|
||||||
? this.hass.localize("ui.panel.config.logs.previous")
|
|
||||||
: this.hass.localize(
|
|
||||||
"ui.panel.config.logs.startups_ago",
|
|
||||||
{ boot: this._boot * -1 }
|
|
||||||
)}
|
|
||||||
id="boots-anchor"
|
|
||||||
@click=${this._toggleBootsMenu}
|
|
||||||
>
|
|
||||||
<ha-svg-icon
|
|
||||||
slot="trailing-icon"
|
|
||||||
.path=${mdiMenuDown}
|
|
||||||
></ha-svg-icon
|
|
||||||
></ha-assist-chip>
|
|
||||||
<ha-menu
|
|
||||||
anchor="boots-anchor"
|
|
||||||
id="boots-menu"
|
|
||||||
positioning="fixed"
|
|
||||||
>
|
|
||||||
${this._boots.map(
|
|
||||||
(boot) => html`
|
|
||||||
<ha-md-menu-item
|
|
||||||
.value=${boot}
|
|
||||||
@click=${this._setBoot}
|
|
||||||
.selected=${boot === this._boot}
|
|
||||||
>
|
|
||||||
${boot === 0
|
|
||||||
? this.hass.localize(
|
|
||||||
"ui.panel.config.logs.current"
|
|
||||||
)
|
|
||||||
: boot === -1
|
|
||||||
? this.hass.localize(
|
|
||||||
"ui.panel.config.logs.previous"
|
|
||||||
)
|
|
||||||
: this.hass.localize(
|
|
||||||
"ui.panel.config.logs.startups_ago",
|
|
||||||
{ boot: boot * -1 }
|
|
||||||
)}
|
|
||||||
</ha-md-menu-item>
|
|
||||||
${boot === 0
|
|
||||||
? html`<ha-md-divider
|
|
||||||
role="separator"
|
|
||||||
></ha-md-divider>`
|
|
||||||
: nothing}
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</ha-menu>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
<ha-icon-button
|
|
||||||
.path=${mdiDownload}
|
|
||||||
@click=${this._downloadFullLog}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.logs.download_full_log"
|
|
||||||
)}
|
|
||||||
></ha-icon-button>
|
|
||||||
${!this._streamSupported || this._error
|
|
||||||
? html`<ha-icon-button
|
|
||||||
.path=${mdiRefresh}
|
|
||||||
@click=${this._loadLogs}
|
|
||||||
.label=${this.hass.localize("ui.common.refresh")}
|
|
||||||
></ha-icon-button>`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-content error-log">
|
|
||||||
<div id="scroll-top-marker"></div>
|
|
||||||
${this._loadingPrevState === "loading"
|
|
||||||
? html`<div class="loading-old">
|
|
||||||
<ha-circular-progress
|
|
||||||
.indeterminate=${this._loadingPrevState === "loading"}
|
|
||||||
></ha-circular-progress>
|
|
||||||
</div>`
|
|
||||||
: nothing}
|
|
||||||
${this._loadingState === "loading"
|
|
||||||
? html`<div>
|
|
||||||
${this.hass.localize("ui.panel.config.logs.loading_log")}
|
|
||||||
</div>`
|
|
||||||
: this._loadingState === "empty"
|
|
||||||
? html`<div>
|
|
||||||
${this.hass.localize("ui.panel.config.logs.no_errors")}
|
|
||||||
</div>`
|
|
||||||
: nothing}
|
|
||||||
${this._loadingState === "loaded" &&
|
|
||||||
this.filter &&
|
|
||||||
this._noSearchResults
|
|
||||||
? html`<div>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.logs.no_issues_search",
|
|
||||||
{ term: this.filter }
|
|
||||||
)}
|
|
||||||
</div>`
|
|
||||||
: nothing}
|
|
||||||
<ha-ansi-to-html></ha-ansi-to-html>
|
|
||||||
<div id="scroll-bottom-marker"></div>
|
|
||||||
</div>
|
|
||||||
<ha-button
|
|
||||||
class="new-logs-indicator ${classMap({
|
|
||||||
visible:
|
|
||||||
(this._newLogsIndicator &&
|
|
||||||
!this._scrolledToBottomController.value) ||
|
|
||||||
false,
|
|
||||||
})}"
|
|
||||||
@click=${this._scrollToBottom}
|
|
||||||
>
|
|
||||||
<ha-svg-icon
|
|
||||||
.path=${mdiArrowCollapseDown}
|
|
||||||
slot="icon"
|
|
||||||
></ha-svg-icon>
|
|
||||||
${this.hass.localize("ui.panel.config.logs.scroll_down_button")}
|
|
||||||
<ha-svg-icon
|
|
||||||
.path=${mdiArrowCollapseDown}
|
|
||||||
slot="trailingIcon"
|
|
||||||
></ha-svg-icon>
|
|
||||||
</ha-button>
|
|
||||||
</ha-card>
|
|
||||||
${this.show === false
|
|
||||||
? html`
|
? html`
|
||||||
<ha-button outlined @click=${this._downloadFullLog}>
|
<ha-card outlined>
|
||||||
|
<div class="header">
|
||||||
|
<h1 class="card-header">
|
||||||
|
${this.header ||
|
||||||
|
this.hass.localize("ui.panel.config.logs.show_full_logs")}
|
||||||
|
</h1>
|
||||||
|
<div>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiRefresh}
|
||||||
|
@click=${this._refresh}
|
||||||
|
.label=${this.hass.localize("ui.common.refresh")}
|
||||||
|
></ha-icon-button>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiDownload}
|
||||||
|
@click=${this._downloadFullLog}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.logs.download_full_log"
|
||||||
|
)}
|
||||||
|
></ha-icon-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content error-log">${this._logHTML}</div>
|
||||||
|
</ha-card>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${!this._logHTML
|
||||||
|
? html`
|
||||||
|
<mwc-button outlined @click=${this._downloadFullLog}>
|
||||||
<ha-svg-icon .path=${mdiDownload}></ha-svg-icon>
|
<ha-svg-icon .path=${mdiDownload}></ha-svg-icon>
|
||||||
${this.hass.localize("ui.panel.config.logs.download_full_log")}
|
${this.hass.localize("ui.panel.config.logs.download_full_log")}
|
||||||
</ha-button>
|
</mwc-button>
|
||||||
<mwc-button raised @click=${this._showLogs}>
|
<mwc-button raised @click=${this._refreshLogs}>
|
||||||
${this.hass.localize("ui.panel.config.logs.load_logs")}
|
${this.hass.localize("ui.panel.config.logs.load_logs")}
|
||||||
</mwc-button>
|
</mwc-button>
|
||||||
`
|
`
|
||||||
@@ -258,337 +96,127 @@ class ErrorLogCard extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public connectedCallback() {
|
private _debounceSearch = debounce(
|
||||||
super.connectedCallback();
|
() => (this._isLogLoaded ? this._refreshLogs() : this._debounceSearch()),
|
||||||
|
150,
|
||||||
if (this._streamSupported === undefined) {
|
false
|
||||||
this._streamSupported = atLeastVersion(
|
);
|
||||||
this.hass.config.version,
|
|
||||||
2024,
|
|
||||||
11
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues) {
|
protected firstUpdated(changedProps: PropertyValues) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
|
|
||||||
this._scrolledToBottomController.observe(this._scrollBottomMarkerElement!);
|
|
||||||
|
|
||||||
this._scrolledToTopController.callback = this._handleTopScroll;
|
|
||||||
this._scrolledToTopController.observe(this._scrollTopMarkerElement!);
|
|
||||||
|
|
||||||
window.addEventListener("connection-status", this._handleConnectionStatus);
|
|
||||||
|
|
||||||
if (this.hass?.config.recovery_mode || this.show) {
|
if (this.hass?.config.recovery_mode || this.show) {
|
||||||
this.hass.loadFragmentTranslation("config");
|
this.hass.loadFragmentTranslation("config");
|
||||||
|
this._refreshLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
// just needs to be loaded once, because only the host endpoints provide boots information
|
|
||||||
this._loadBoots();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProps) {
|
protected updated(changedProps) {
|
||||||
super.updated(changedProps);
|
super.updated(changedProps);
|
||||||
|
|
||||||
|
if (changedProps.has("provider")) {
|
||||||
|
this._logHTML = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(changedProps.has("show") && this.show) ||
|
(changedProps.has("show") && this.show) ||
|
||||||
(changedProps.has("provider") && this.show)
|
(changedProps.has("provider") && this.show)
|
||||||
) {
|
) {
|
||||||
this._boot = 0;
|
this._refreshLogs();
|
||||||
this._loadLogs();
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (this._newLogsIndicator && this._scrolledToBottomController.value) {
|
|
||||||
this._newLogsIndicator = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedProps.has("filter")) {
|
if (changedProps.has("filter")) {
|
||||||
this._debounceSearch();
|
this._debounceSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
changedProps.has("_loadingState") &&
|
|
||||||
this._loadingState === "loaded" &&
|
|
||||||
this._scrolledToTopController.value &&
|
|
||||||
this._firstCursor &&
|
|
||||||
!this._loadingPrevState
|
|
||||||
) {
|
|
||||||
this._loadMoreLogs();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
private async _refresh(ev: CustomEvent): Promise<void> {
|
||||||
super.disconnectedCallback();
|
const button = ev.currentTarget as any;
|
||||||
|
button.progress = true;
|
||||||
|
|
||||||
if (this._logStreamAborter) {
|
await this._refreshLogs();
|
||||||
this._logStreamAborter.abort();
|
button.progress = false;
|
||||||
}
|
|
||||||
|
|
||||||
window.removeEventListener(
|
|
||||||
"connection-status",
|
|
||||||
this._handleConnectionStatus
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _downloadFullLog(): Promise<void> {
|
private async _downloadFullLog(): Promise<void> {
|
||||||
if (this._streamSupported) {
|
const timeString = new Date().toISOString().replace(/:/g, "-");
|
||||||
showDownloadLogsDialog(this, {
|
const downloadUrl =
|
||||||
header: this.header,
|
this.provider !== "core"
|
||||||
provider: this.provider,
|
? getHassioLogDownloadUrl(this.provider)
|
||||||
defaultLineCount: this._numberOfLines,
|
: getErrorLogDownloadUrl;
|
||||||
boot: this._boot,
|
const logFileName =
|
||||||
});
|
this.provider !== "core"
|
||||||
} else {
|
? `${this.provider}_${timeString}.log`
|
||||||
const timeString = new Date().toISOString().replace(/:/g, "-");
|
: `home-assistant_${timeString}.log`;
|
||||||
const downloadUrl =
|
const signedUrl = await getSignedPath(this.hass, downloadUrl);
|
||||||
this.provider && this.provider !== "core"
|
fileDownload(signedUrl.path, logFileName);
|
||||||
? getHassioLogDownloadUrl(this.provider)
|
|
||||||
: getErrorLogDownloadUrl;
|
|
||||||
const logFileName =
|
|
||||||
this.provider && this.provider !== "core"
|
|
||||||
? `${this.provider}_${timeString}.log`
|
|
||||||
: `home-assistant_${timeString}.log`;
|
|
||||||
const signedUrl = await getSignedPath(this.hass, downloadUrl);
|
|
||||||
fileDownload(signedUrl.path, logFileName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _showLogs(): void {
|
private async _refreshLogs(): Promise<void> {
|
||||||
this.show = true;
|
this._logHTML = this.hass.localize("ui.panel.config.logs.loading_log");
|
||||||
}
|
let log: string;
|
||||||
|
|
||||||
private async _loadLogs(): Promise<void> {
|
if (this.provider !== "core" && isComponentLoaded(this.hass, "hassio")) {
|
||||||
this._error = undefined;
|
try {
|
||||||
this._loadingState = "loading";
|
log = await fetchHassioLogs(this.hass, this.provider);
|
||||||
this._loadingPrevState = undefined;
|
if (this.filter) {
|
||||||
this._firstCursor = undefined;
|
log = log
|
||||||
this._numberOfLines = 0;
|
.split("\n")
|
||||||
this._ansiToHtmlElement?.clear();
|
.filter((entry) =>
|
||||||
|
entry.toLowerCase().includes(this.filter.toLowerCase())
|
||||||
try {
|
)
|
||||||
if (this._logStreamAborter) {
|
.join("\n");
|
||||||
this._logStreamAborter.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._logStreamAborter = new AbortController();
|
|
||||||
|
|
||||||
if (
|
|
||||||
this._streamSupported &&
|
|
||||||
isComponentLoaded(this.hass, "hassio") &&
|
|
||||||
this.provider
|
|
||||||
) {
|
|
||||||
const response = await fetchHassioLogsFollow(
|
|
||||||
this.hass,
|
|
||||||
this.provider,
|
|
||||||
this._logStreamAborter.signal,
|
|
||||||
NUMBER_OF_LINES,
|
|
||||||
this._boot
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.headers.has("X-First-Cursor")) {
|
|
||||||
this._firstCursor = response.headers.get("X-First-Cursor")!;
|
|
||||||
}
|
}
|
||||||
|
if (!log) {
|
||||||
if (!response.body) {
|
this._logHTML = this.hass.localize("ui.panel.config.logs.no_errors");
|
||||||
throw new Error("No stream body found");
|
|
||||||
}
|
|
||||||
|
|
||||||
this._loadingState = "empty";
|
|
||||||
|
|
||||||
let tempLogLine = "";
|
|
||||||
|
|
||||||
const reader = response.body.getReader();
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
let done = false;
|
|
||||||
|
|
||||||
while (!done) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
const { value, done: readerDone } = await reader.read();
|
|
||||||
done = readerDone;
|
|
||||||
|
|
||||||
if (value) {
|
|
||||||
const chunk = decoder.decode(value, { stream: !done });
|
|
||||||
const scrolledToBottom = this._scrolledToBottomController.value;
|
|
||||||
const lines = `${tempLogLine}${chunk}`
|
|
||||||
.split("\n")
|
|
||||||
.filter((line) => line.trim() !== "");
|
|
||||||
|
|
||||||
// handle edge case where the last line is not complete
|
|
||||||
if (chunk.endsWith("\n")) {
|
|
||||||
tempLogLine = "";
|
|
||||||
} else {
|
|
||||||
tempLogLine = lines.splice(-1, 1)[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lines.length) {
|
|
||||||
this._ansiToHtmlElement?.parseLinesToColoredPre(lines);
|
|
||||||
this._numberOfLines += lines.length;
|
|
||||||
|
|
||||||
if (this._loadingState === "empty") {
|
|
||||||
// delay to avoid loading older logs immediately
|
|
||||||
setTimeout(() => {
|
|
||||||
this._loadingState = "loaded";
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scrolledToBottom && this._logElement) {
|
|
||||||
this._scrollToBottom();
|
|
||||||
} else {
|
|
||||||
this._newLogsIndicator = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// fallback to old method
|
|
||||||
this._streamSupported = false;
|
|
||||||
let logs = "";
|
|
||||||
if (isComponentLoaded(this.hass, "hassio") && this.provider) {
|
|
||||||
const repsonse = await fetchHassioLogs(this.hass, this.provider);
|
|
||||||
logs = await repsonse.text();
|
|
||||||
} else {
|
|
||||||
logs = await fetchErrorLog(this.hass);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (logs) {
|
|
||||||
this._ansiToHtmlElement?.parseTextToColoredPre(logs);
|
|
||||||
this._loadingState = "loaded";
|
|
||||||
this._scrollToBottom();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
if (err.name === "AbortError") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._error = this.hass.localize("ui.panel.config.logs.failed_get_logs", {
|
|
||||||
provider: this.provider,
|
|
||||||
error: extractApiErrorMessage(err),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _debounceSearch = debounce(() => {
|
|
||||||
this._noSearchResults = !this._ansiToHtmlElement?.filterLines(this.filter);
|
|
||||||
|
|
||||||
if (!this.filter) {
|
|
||||||
this._scrollToBottom();
|
|
||||||
}
|
|
||||||
}, 150);
|
|
||||||
|
|
||||||
private _debounceScrollToBottom = debounce(() => {
|
|
||||||
this._logElement!.scrollTop = this._logElement!.scrollHeight;
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
private _scrollToBottom(): void {
|
|
||||||
if (this._logElement) {
|
|
||||||
this._newLogsIndicator = false;
|
|
||||||
if (this.provider !== "core") {
|
|
||||||
this._logElement!.scrollTo(0, this._logElement!.scrollHeight);
|
|
||||||
} else {
|
|
||||||
this._debounceScrollToBottom();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleConnectionStatus = (ev: HASSDomEvent<ConnectionStatus>) => {
|
|
||||||
if (ev.detail === "disconnected" && this._logStreamAborter) {
|
|
||||||
this._logStreamAborter.abort();
|
|
||||||
}
|
|
||||||
if (ev.detail === "connected" && this.show) {
|
|
||||||
this._loadLogs();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private async _loadMoreLogs() {
|
|
||||||
if (
|
|
||||||
this._firstCursor &&
|
|
||||||
this._loadingPrevState !== "loading" &&
|
|
||||||
this._loadingState === "loaded" &&
|
|
||||||
this._logElement
|
|
||||||
) {
|
|
||||||
const scrolledToBottom = this._scrolledToBottomController.value;
|
|
||||||
const scrollPositionFromBottom =
|
|
||||||
this._logElement.scrollHeight - this._logElement.scrollTop;
|
|
||||||
this._loadingPrevState = "loading";
|
|
||||||
const response = await fetchHassioLogs(
|
|
||||||
this.hass,
|
|
||||||
this.provider,
|
|
||||||
`entries=${this._firstCursor}:-100:100`,
|
|
||||||
this._boot
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.headers.has("X-First-Cursor")) {
|
|
||||||
if (this._firstCursor === response.headers.get("X-First-Cursor")!) {
|
|
||||||
this._loadingPrevState = "end";
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._firstCursor = response.headers.get("X-First-Cursor")!;
|
this._logHTML = html`<ha-ansi-to-html .content=${log}>
|
||||||
}
|
</ha-ansi-to-html>`;
|
||||||
|
this._isLogLoaded = true;
|
||||||
const body = await response.text();
|
return;
|
||||||
|
|
||||||
if (body) {
|
|
||||||
const lines = body
|
|
||||||
.split("\n")
|
|
||||||
.filter((line) => line.trim() !== "")
|
|
||||||
.reverse();
|
|
||||||
|
|
||||||
this._ansiToHtmlElement?.parseLinesToColoredPre(lines, true);
|
|
||||||
this._numberOfLines! += lines.length;
|
|
||||||
this._loadingPrevState = "loaded";
|
|
||||||
} else {
|
|
||||||
this._loadingPrevState = "end";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scrolledToBottom) {
|
|
||||||
this._scrollToBottom();
|
|
||||||
} else if (this._loadingPrevState !== "end" && this._logElement) {
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
this._logElement!.scrollTop =
|
|
||||||
this._logElement!.scrollHeight - scrollPositionFromBottom;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleTopScroll = (entries) => {
|
|
||||||
const isVisible = entries[0].isIntersecting;
|
|
||||||
if (
|
|
||||||
this._firstCursor &&
|
|
||||||
isVisible &&
|
|
||||||
this._loadingState === "loaded" &&
|
|
||||||
(!this._loadingPrevState || this._loadingPrevState === "loaded") &&
|
|
||||||
!this.filter
|
|
||||||
) {
|
|
||||||
this._loadMoreLogs();
|
|
||||||
}
|
|
||||||
return isVisible;
|
|
||||||
};
|
|
||||||
|
|
||||||
private async _loadBoots() {
|
|
||||||
if (this._streamSupported && isComponentLoaded(this.hass, "hassio")) {
|
|
||||||
try {
|
|
||||||
const { data } = await fetchHassioBoots(this.hass);
|
|
||||||
this._boots = Object.keys(data.boots)
|
|
||||||
.map(Number)
|
|
||||||
.sort((a, b) => b - a);
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// eslint-disable-next-line no-console
|
this._error = this.hass.localize(
|
||||||
console.error(err);
|
"ui.panel.config.logs.failed_get_logs",
|
||||||
|
{ provider: this.provider, error: extractApiErrorMessage(err) }
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
log = await fetchErrorLog(this.hass!);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private _toggleBootsMenu() {
|
this._isLogLoaded = true;
|
||||||
if (this._bootsMenu) {
|
|
||||||
this._bootsMenu.open = !this._bootsMenu.open;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _setBoot(ev: any) {
|
const split = log && log.split("\n");
|
||||||
this._boot = ev.target.value;
|
|
||||||
this._loadLogs();
|
this._logHTML = split
|
||||||
|
? (this.filter
|
||||||
|
? split.filter((entry) => {
|
||||||
|
if (this.filter) {
|
||||||
|
return entry.toLowerCase().includes(this.filter.toLowerCase());
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
})
|
||||||
|
: split
|
||||||
|
).map((entry) => {
|
||||||
|
if (entry.includes("INFO"))
|
||||||
|
return html`<div class="info">${entry}</div>`;
|
||||||
|
|
||||||
|
if (entry.includes("WARNING"))
|
||||||
|
return html`<div class="warning">${entry}</div>`;
|
||||||
|
|
||||||
|
if (
|
||||||
|
entry.includes("ERROR") ||
|
||||||
|
entry.includes("FATAL") ||
|
||||||
|
entry.includes("CRITICAL")
|
||||||
|
)
|
||||||
|
return html`<div class="error">${entry}</div>`;
|
||||||
|
|
||||||
|
return html`<div>${entry}</div>`;
|
||||||
|
})
|
||||||
|
: this.hass.localize("ui.panel.config.logs.no_errors");
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles: CSSResultGroup = css`
|
static styles: CSSResultGroup = css`
|
||||||
@@ -598,18 +226,7 @@ class ErrorLogCard extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ha-card {
|
ha-card {
|
||||||
padding-top: 8px;
|
padding-top: 16px;
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-card.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-card .action-buttons {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
@@ -619,18 +236,21 @@ class ErrorLogCard extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
color: var(--ha-card-header-color, --primary-text-color);
|
||||||
font-family: var(--ha-card-header-font-family, inherit);
|
font-family: var(--ha-card-header-font-family, inherit);
|
||||||
font-size: var(--ha-card-header-font-size, 24px);
|
font-size: var(--ha-card-header-font-size, 24px);
|
||||||
letter-spacing: -0.012em;
|
letter-spacing: -0.012em;
|
||||||
line-height: 48px;
|
line-height: 48px;
|
||||||
display: block;
|
display: block;
|
||||||
margin-block-start: 0px;
|
margin-block-start: 0px;
|
||||||
|
margin-block-end: 0px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
white-space: nowrap;
|
}
|
||||||
max-width: calc(100% - 150px);
|
|
||||||
overflow: hidden;
|
ha-select {
|
||||||
text-overflow: ellipsis;
|
display: block;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-icon-button {
|
ha-icon-button {
|
||||||
@@ -638,24 +258,10 @@ class ErrorLogCard extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.error-log {
|
.error-log {
|
||||||
position: relative;
|
|
||||||
font-family: var(--code-font-family, monospace);
|
font-family: var(--code-font-family, monospace);
|
||||||
clear: both;
|
clear: both;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
padding-bottom: 12px;
|
|
||||||
overflow-y: scroll;
|
|
||||||
min-height: var(--error-log-card-height, calc(100vh - 240px));
|
|
||||||
max-height: var(--error-log-card-height, calc(100vh - 240px));
|
|
||||||
|
|
||||||
border-top: 1px solid var(--divider-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media all and (max-width: 870px) {
|
|
||||||
.error-log {
|
|
||||||
min-height: var(--error-log-card-height, calc(100vh - 190px));
|
|
||||||
max-height: var(--error-log-card-height, calc(100vh - 190px));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-log > div {
|
.error-log > div {
|
||||||
@@ -667,28 +273,6 @@ class ErrorLogCard extends LitElement {
|
|||||||
background-color: var(--secondary-background-color);
|
background-color: var(--secondary-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-logs-indicator {
|
|
||||||
--mdc-theme-primary: var(--text-primary-color);
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 0;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
transition: height 0.4s ease-out;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-logs-indicator.visible {
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
color: var(--error-color);
|
color: var(--error-color);
|
||||||
}
|
}
|
||||||
@@ -697,16 +281,8 @@ class ErrorLogCard extends LitElement {
|
|||||||
color: var(--warning-color);
|
color: var(--warning-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-old {
|
mwc-button {
|
||||||
display: flex;
|
direction: var(--direction);
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-assist-chip {
|
|
||||||
--ha-assist-chip-container-shape: 10px;
|
|
||||||
--md-assist-chip-trailing-space: 8px;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -167,7 +167,6 @@ export class HaConfigLogs extends LitElement {
|
|||||||
|
|
||||||
private _selectProvider(ev) {
|
private _selectProvider(ev) {
|
||||||
this._selectedLogProvider = (ev.currentTarget as any).provider;
|
this._selectedLogProvider = (ev.currentTarget as any).provider;
|
||||||
this._filter = "";
|
|
||||||
navigate(`/config/logs?provider=${this._selectedLogProvider}`);
|
navigate(`/config/logs?provider=${this._selectedLogProvider}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,19 +0,0 @@
|
|||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
|
||||||
|
|
||||||
export interface DownloadLogsDialogParams {
|
|
||||||
header?: string;
|
|
||||||
provider: string;
|
|
||||||
defaultLineCount?: number;
|
|
||||||
boot: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const showDownloadLogsDialog = (
|
|
||||||
element: HTMLElement,
|
|
||||||
dialogParams: DownloadLogsDialogParams
|
|
||||||
): void => {
|
|
||||||
fireEvent(element, "show-dialog", {
|
|
||||||
dialogTag: "dialog-download-logs",
|
|
||||||
dialogImport: () => import("./dialog-download-logs"),
|
|
||||||
dialogParams,
|
|
||||||
});
|
|
||||||
};
|
|
@@ -212,7 +212,7 @@ export class SystemLogCard extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
color: var(--ha-card-header-color, --primary-text-color);
|
||||||
font-family: var(--ha-card-header-font-family, inherit);
|
font-family: var(--ha-card-header-font-family, inherit);
|
||||||
font-size: var(--ha-card-header-font-size, 24px);
|
font-size: var(--ha-card-header-font-size, 24px);
|
||||||
letter-spacing: -0.012em;
|
letter-spacing: -0.012em;
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import "@material/mwc-tab";
|
import "@material/mwc-tab";
|
||||||
import "@material/mwc-tab-bar";
|
import "@material/mwc-tab-bar";
|
||||||
import { mdiDeleteOutline, mdiPlus, mdiMenuDown, mdiWifi } from "@mdi/js";
|
import { mdiDeleteOutline, mdiPlus, mdiMenuDown } from "@mdi/js";
|
||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { cache } from "lit/directives/cache";
|
import { cache } from "lit/directives/cache";
|
||||||
@@ -20,7 +20,7 @@ import "../../../components/ha-textfield";
|
|||||||
import type { HaTextField } from "../../../components/ha-textfield";
|
import type { HaTextField } from "../../../components/ha-textfield";
|
||||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||||
import {
|
import {
|
||||||
AccessPoint,
|
AccessPoints,
|
||||||
accesspointScan,
|
accesspointScan,
|
||||||
fetchNetworkInfo,
|
fetchNetworkInfo,
|
||||||
formatAddress,
|
formatAddress,
|
||||||
@@ -58,7 +58,7 @@ const PREDEFINED_DNS = {
|
|||||||
export class HassioNetwork extends LitElement {
|
export class HassioNetwork extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@state() private _accessPoints: AccessPoint[] = [];
|
@state() private _accessPoints?: AccessPoints;
|
||||||
|
|
||||||
@state() private _curTabIndex = 0;
|
@state() private _curTabIndex = 0;
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ export class HassioNetwork extends LitElement {
|
|||||||
</mwc-tab>`
|
</mwc-tab>`
|
||||||
)}
|
)}
|
||||||
</mwc-tab-bar>`
|
</mwc-tab-bar>`
|
||||||
: nothing}
|
: ""}
|
||||||
${cache(this._renderTab())}
|
${cache(this._renderTab())}
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
@@ -121,6 +121,9 @@ export class HassioNetwork extends LitElement {
|
|||||||
|
|
||||||
private _renderTab() {
|
private _renderTab() {
|
||||||
return html`<div class="card-content">
|
return html`<div class="card-content">
|
||||||
|
${IP_VERSIONS.map((version) =>
|
||||||
|
this._interface![version] ? this._renderIPConfiguration(version) : ""
|
||||||
|
)}
|
||||||
${this._interface?.type === "wireless"
|
${this._interface?.type === "wireless"
|
||||||
? html`
|
? html`
|
||||||
<ha-expansion-panel
|
<ha-expansion-panel
|
||||||
@@ -128,17 +131,15 @@ export class HassioNetwork extends LitElement {
|
|||||||
"ui.panel.config.network.supervisor.wifi"
|
"ui.panel.config.network.supervisor.wifi"
|
||||||
)}
|
)}
|
||||||
outlined
|
outlined
|
||||||
.expanded=${!this._interface?.wifi?.ssid}
|
|
||||||
>
|
>
|
||||||
${this._interface?.wifi?.ssid
|
${this._interface?.wifi?.ssid
|
||||||
? html`<p>
|
? html`<p>
|
||||||
<ha-svg-icon slot="icon" .path=${mdiWifi}></ha-svg-icon>
|
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.network.supervisor.connected_to",
|
"ui.panel.config.network.supervisor.connected_to",
|
||||||
{ ssid: this._interface?.wifi?.ssid }
|
{ ssid: this._interface?.wifi?.ssid }
|
||||||
)}
|
)}
|
||||||
</p>`
|
</p>`
|
||||||
: nothing}
|
: ""}
|
||||||
<ha-button
|
<ha-button
|
||||||
class="scan"
|
class="scan"
|
||||||
@click=${this._scanForAP}
|
@click=${this._scanForAP}
|
||||||
@@ -150,34 +151,37 @@ export class HassioNetwork extends LitElement {
|
|||||||
: this.hass.localize(
|
: this.hass.localize(
|
||||||
"ui.panel.config.network.supervisor.scan_ap"
|
"ui.panel.config.network.supervisor.scan_ap"
|
||||||
)}
|
)}
|
||||||
<ha-svg-icon slot="icon" .path=${mdiWifi}></ha-svg-icon>
|
|
||||||
</ha-button>
|
</ha-button>
|
||||||
${this._accessPoints.length
|
${this._accessPoints &&
|
||||||
|
this._accessPoints.accesspoints &&
|
||||||
|
this._accessPoints.accesspoints.length !== 0
|
||||||
? html`
|
? html`
|
||||||
<mwc-list>
|
<mwc-list>
|
||||||
${this._accessPoints.map(
|
${this._accessPoints.accesspoints
|
||||||
(ap) => html`
|
.filter((ap) => ap.ssid)
|
||||||
<ha-list-item
|
.map(
|
||||||
twoline
|
(ap) => html`
|
||||||
@click=${this._selectAP}
|
<ha-list-item
|
||||||
.activated=${ap.ssid ===
|
twoline
|
||||||
this._wifiConfiguration?.ssid}
|
@click=${this._selectAP}
|
||||||
.ap=${ap}
|
.activated=${ap.ssid ===
|
||||||
>
|
this._wifiConfiguration?.ssid}
|
||||||
<span>${ap.ssid}</span>
|
.ap=${ap}
|
||||||
<span slot="secondary">
|
>
|
||||||
${ap.mac} -
|
<span>${ap.ssid}</span>
|
||||||
${this.hass.localize(
|
<span slot="secondary">
|
||||||
"ui.panel.config.network.supervisor.signal_strength"
|
${ap.mac} -
|
||||||
)}:
|
${this.hass.localize(
|
||||||
${ap.signal}
|
"ui.panel.config.network.supervisor.signal_strength"
|
||||||
</span>
|
)}:
|
||||||
</ha-list-item>
|
${ap.signal}
|
||||||
`
|
</span>
|
||||||
)}
|
</ha-list-item>
|
||||||
|
`
|
||||||
|
)}
|
||||||
</mwc-list>
|
</mwc-list>
|
||||||
`
|
`
|
||||||
: nothing}
|
: ""}
|
||||||
${this._wifiConfiguration
|
${this._wifiConfiguration
|
||||||
? html`
|
? html`
|
||||||
<div class="radio-row">
|
<div class="radio-row">
|
||||||
@@ -240,24 +244,19 @@ export class HassioNetwork extends LitElement {
|
|||||||
>
|
>
|
||||||
</ha-password-field>
|
</ha-password-field>
|
||||||
`
|
`
|
||||||
: nothing}
|
: ""}
|
||||||
`
|
`
|
||||||
: nothing}
|
: ""}
|
||||||
</ha-expansion-panel>
|
</ha-expansion-panel>
|
||||||
`
|
`
|
||||||
: nothing}
|
: ""}
|
||||||
${IP_VERSIONS.map((version) =>
|
|
||||||
this._interface![version]
|
|
||||||
? this._renderIPConfiguration(version)
|
|
||||||
: nothing
|
|
||||||
)}
|
|
||||||
${this._dirty
|
${this._dirty
|
||||||
? html`<ha-alert alert-type="warning">
|
? html`<ha-alert alert-type="warning">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.network.supervisor.warning"
|
"ui.panel.config.network.supervisor.warning"
|
||||||
)}
|
)}
|
||||||
</ha-alert>`
|
</ha-alert>`
|
||||||
: nothing}
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<ha-button @click=${this._updateNetwork} .disabled=${!this._dirty}>
|
<ha-button @click=${this._updateNetwork} .disabled=${!this._dirty}>
|
||||||
@@ -266,19 +265,11 @@ export class HassioNetwork extends LitElement {
|
|||||||
</ha-circular-progress>`
|
</ha-circular-progress>`
|
||||||
: this.hass.localize("ui.common.save")}
|
: this.hass.localize("ui.common.save")}
|
||||||
</ha-button>
|
</ha-button>
|
||||||
<ha-button @click=${this._clear}>
|
|
||||||
${this.hass.localize("ui.panel.config.network.supervisor.reset")}
|
|
||||||
</ha-button>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _selectAP(event) {
|
private _selectAP(event) {
|
||||||
this._wifiConfiguration = event.currentTarget.ap;
|
this._wifiConfiguration = event.currentTarget.ap;
|
||||||
IP_VERSIONS.forEach((version) => {
|
|
||||||
if (this._interface![version]!.method === "disabled") {
|
|
||||||
this._interface![version]!.method = "auto";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this._dirty = true;
|
this._dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,22 +279,10 @@ export class HassioNetwork extends LitElement {
|
|||||||
}
|
}
|
||||||
this._scanning = true;
|
this._scanning = true;
|
||||||
try {
|
try {
|
||||||
const aps = await accesspointScan(this.hass, this._interface.interface);
|
this._accessPoints = await accesspointScan(
|
||||||
this._accessPoints = [];
|
this.hass,
|
||||||
aps.accesspoints?.forEach((ap) => {
|
this._interface.interface
|
||||||
if (ap.ssid) {
|
);
|
||||||
// filter out duplicates
|
|
||||||
const existing = this._accessPoints.find((a) => a.ssid === ap.ssid);
|
|
||||||
if (!existing) {
|
|
||||||
this._accessPoints.push(ap);
|
|
||||||
} else if (ap.signal > existing.signal) {
|
|
||||||
this._accessPoints = this._accessPoints.filter(
|
|
||||||
(a) => a.ssid !== ap.ssid
|
|
||||||
);
|
|
||||||
this._accessPoints.push(ap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: "Failed to scan for accesspoints",
|
title: "Failed to scan for accesspoints",
|
||||||
@@ -315,13 +294,6 @@ export class HassioNetwork extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _renderIPConfiguration(version: string) {
|
private _renderIPConfiguration(version: string) {
|
||||||
const watingForSSID =
|
|
||||||
this._interface?.type === "wireless" &&
|
|
||||||
!this._wifiConfiguration?.ssid &&
|
|
||||||
!this._interface.wifi?.ssid;
|
|
||||||
if (watingForSSID) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
const nameservers = this._interface![version]?.nameservers || [];
|
const nameservers = this._interface![version]?.nameservers || [];
|
||||||
if (nameservers.length === 0) {
|
if (nameservers.length === 0) {
|
||||||
nameservers.push(""); // always show input
|
nameservers.push(""); // always show input
|
||||||
@@ -512,7 +484,7 @@ export class HassioNetwork extends LitElement {
|
|||||||
</ha-list-item>
|
</ha-list-item>
|
||||||
</ha-button-menu>
|
</ha-button-menu>
|
||||||
`
|
`
|
||||||
: nothing}
|
: ""}
|
||||||
</ha-expansion-panel>
|
</ha-expansion-panel>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -557,13 +529,9 @@ export class HassioNetwork extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interfaceOptions.enabled =
|
interfaceOptions.enabled =
|
||||||
// at least one ip version is enabled
|
this._wifiConfiguration !== undefined ||
|
||||||
(interfaceOptions.ipv4?.method !== "disabled" ||
|
interfaceOptions.ipv4?.method !== "disabled" ||
|
||||||
interfaceOptions.ipv6?.method !== "disabled") &&
|
interfaceOptions.ipv6?.method !== "disabled";
|
||||||
// require connection if this is a wireless interface
|
|
||||||
(this._interface!.type !== "wireless" ||
|
|
||||||
this._wifiConfiguration !== undefined ||
|
|
||||||
!!this._interface!.wifi);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateNetworkInterface(
|
await updateNetworkInterface(
|
||||||
@@ -572,7 +540,6 @@ export class HassioNetwork extends LitElement {
|
|||||||
interfaceOptions
|
interfaceOptions
|
||||||
);
|
);
|
||||||
this._dirty = false;
|
this._dirty = false;
|
||||||
await this._fetchNetworkInfo();
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: this.hass.localize(
|
title: this.hass.localize(
|
||||||
@@ -585,20 +552,6 @@ export class HassioNetwork extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _clear() {
|
|
||||||
await this._fetchNetworkInfo();
|
|
||||||
this._interface!.ipv4!.method = "auto";
|
|
||||||
this._interface!.ipv4!.nameservers = [];
|
|
||||||
this._interface!.ipv6!.method = "auto";
|
|
||||||
this._interface!.ipv6!.nameservers = [];
|
|
||||||
// removing the connection will disable the interface
|
|
||||||
// this is the only way to forget the wifi network right now
|
|
||||||
this._interface!.wifi = null;
|
|
||||||
this._wifiConfiguration = undefined;
|
|
||||||
this._dirty = true;
|
|
||||||
this.requestUpdate("_interface");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _handleTabActivated(ev: CustomEvent): Promise<void> {
|
private async _handleTabActivated(ev: CustomEvent): Promise<void> {
|
||||||
if (this._dirty) {
|
if (this._dirty) {
|
||||||
const confirm = await showConfirmationDialog(this, {
|
const confirm = await showConfirmationDialog(this, {
|
||||||
|
@@ -292,9 +292,6 @@ export class HaConfigPerson extends LitElement {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
}
|
}
|
||||||
mwc-list:has(+ .empty) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -21,9 +21,11 @@ import "../../../components/ha-list-item";
|
|||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import "../../../components/ha-switch";
|
import "../../../components/ha-switch";
|
||||||
import {
|
import {
|
||||||
|
AssistDevice,
|
||||||
AssistPipeline,
|
AssistPipeline,
|
||||||
createAssistPipeline,
|
createAssistPipeline,
|
||||||
deleteAssistPipeline,
|
deleteAssistPipeline,
|
||||||
|
listAssistDevices,
|
||||||
listAssistPipelines,
|
listAssistPipelines,
|
||||||
setAssistPipelinePreferred,
|
setAssistPipelinePreferred,
|
||||||
updateAssistPipeline,
|
updateAssistPipeline,
|
||||||
@@ -40,7 +42,6 @@ import { documentationUrl } from "../../../util/documentation-url";
|
|||||||
import { showVoiceAssistantPipelineDetailDialog } from "./show-dialog-voice-assistant-pipeline-detail";
|
import { showVoiceAssistantPipelineDetailDialog } from "./show-dialog-voice-assistant-pipeline-detail";
|
||||||
import { showVoiceCommandDialog } from "../../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
|
import { showVoiceCommandDialog } from "../../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
|
||||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
|
||||||
|
|
||||||
@customElement("assist-pref")
|
@customElement("assist-pref")
|
||||||
export class AssistPref extends LitElement {
|
export class AssistPref extends LitElement {
|
||||||
@@ -57,7 +58,7 @@ export class AssistPref extends LitElement {
|
|||||||
|
|
||||||
@state() private _preferred: string | null = null;
|
@state() private _preferred: string | null = null;
|
||||||
|
|
||||||
@state() private _pipelineEntitiesCount = 0;
|
@state() private _devices: AssistDevice[] = [];
|
||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues) {
|
protected firstUpdated(changedProps: PropertyValues) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
@@ -66,9 +67,9 @@ export class AssistPref extends LitElement {
|
|||||||
this._pipelines = pipelines.pipelines;
|
this._pipelines = pipelines.pipelines;
|
||||||
this._preferred = pipelines.preferred_pipeline;
|
this._preferred = pipelines.preferred_pipeline;
|
||||||
});
|
});
|
||||||
this._pipelineEntitiesCount = Object.values(this.hass.entities).filter(
|
listAssistDevices(this.hass).then((devices) => {
|
||||||
(entity) => computeDomain(entity.entity_id) === "assist_satellite"
|
this._devices = devices;
|
||||||
).length;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _exposedEntitiesCount = memoizeOne(
|
private _exposedEntitiesCount = memoizeOne(
|
||||||
@@ -204,13 +205,13 @@ export class AssistPref extends LitElement {
|
|||||||
)}
|
)}
|
||||||
</ha-button>
|
</ha-button>
|
||||||
</a>
|
</a>
|
||||||
${this._pipelineEntitiesCount > 0
|
${this._devices?.length
|
||||||
? html`
|
? html`
|
||||||
<a href="/config/voice-assistants/assist/devices">
|
<a href="/config/voice-assistants/assist/devices">
|
||||||
<ha-button>
|
<ha-button>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.voice_assistants.assistants.pipeline.assist_devices",
|
"ui.panel.config.voice_assistants.assistants.pipeline.assist_devices",
|
||||||
{ number: this._pipelineEntitiesCount }
|
{ number: this._devices.length }
|
||||||
)}
|
)}
|
||||||
</ha-button>
|
</ha-button>
|
||||||
</a>
|
</a>
|
||||||
|
@@ -529,7 +529,7 @@ class HaPanelDevAction extends LitElement {
|
|||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return false;
|
return hasTemplate(val);
|
||||||
})))
|
})))
|
||||||
) {
|
) {
|
||||||
this._yamlMode = true;
|
this._yamlMode = true;
|
||||||
|
@@ -18,7 +18,6 @@ import { HomeAssistant } from "../../types";
|
|||||||
import "../lovelace/components/hui-energy-period-selector";
|
import "../lovelace/components/hui-energy-period-selector";
|
||||||
import { Lovelace } from "../lovelace/types";
|
import { Lovelace } from "../lovelace/types";
|
||||||
import "../lovelace/views/hui-view";
|
import "../lovelace/views/hui-view";
|
||||||
import "../lovelace/views/hui-view-container";
|
|
||||||
import { navigate } from "../../common/navigate";
|
import { navigate } from "../../common/navigate";
|
||||||
import {
|
import {
|
||||||
getEnergyDataCollection,
|
getEnergyDataCollection,
|
||||||
@@ -109,18 +108,14 @@ class PanelEnergy extends LitElement {
|
|||||||
</hui-energy-period-selector>
|
</hui-energy-period-selector>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="view" @reload-energy-panel=${this._reloadView}>
|
||||||
<hui-view-container
|
|
||||||
.hass=${this.hass}
|
|
||||||
@reload-energy-panel=${this._reloadView}
|
|
||||||
>
|
|
||||||
<hui-view
|
<hui-view
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
.lovelace=${this._lovelace}
|
.lovelace=${this._lovelace}
|
||||||
.index=${this._viewIndex}
|
.index=${this._viewIndex}
|
||||||
></hui-view>
|
></hui-view>
|
||||||
</hui-view-container>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +188,9 @@ class PanelEnergy extends LitElement {
|
|||||||
row.push(type);
|
row.push(type);
|
||||||
row.push(unit.normalize("NFKD"));
|
row.push(unit.normalize("NFKD"));
|
||||||
times.forEach((t) => {
|
times.forEach((t) => {
|
||||||
if (n < stats[stat].length && stats[stat][n].start === t) {
|
if (stats[stat][n].start > t) {
|
||||||
|
row.push("");
|
||||||
|
} else if (n < stats[stat].length && stats[stat][n].start === t) {
|
||||||
row.push((stats[stat][n].change ?? "").toString());
|
row.push((stats[stat][n].change ?? "").toString());
|
||||||
n++;
|
n++;
|
||||||
} else {
|
} else {
|
||||||
@@ -394,19 +391,23 @@ class PanelEnergy extends LitElement {
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
hui-view-container {
|
#view {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
padding-top: calc(var(--header-height) + env(safe-area-inset-top));
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding-top: calc(var(--header-height) + env(safe-area-inset-top));
|
|
||||||
padding-left: env(safe-area-inset-left);
|
padding-left: env(safe-area-inset-left);
|
||||||
padding-right: env(safe-area-inset-right);
|
padding-right: env(safe-area-inset-right);
|
||||||
padding-inline-start: env(safe-area-inset-left);
|
padding-inline-start: env(safe-area-inset-left);
|
||||||
padding-inline-end: env(safe-area-inset-right);
|
padding-inline-end: env(safe-area-inset-right);
|
||||||
padding-bottom: env(safe-area-inset-bottom);
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
background: var(
|
||||||
|
--lovelace-background,
|
||||||
|
var(--primary-background-color)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
hui-view {
|
#view > * {
|
||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
@@ -231,7 +231,7 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
color: var(--ha-card-header-color, --primary-text-color);
|
||||||
font-size: var(--ha-card-header-font-size, 24px);
|
font-size: var(--ha-card-header-font-size, 24px);
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
|
@@ -50,11 +50,6 @@ interface MapEntityConfig extends EntityConfig {
|
|||||||
focus?: boolean;
|
focus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GeoEntity {
|
|
||||||
entity_id: string;
|
|
||||||
focus: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("hui-map-card")
|
@customElement("hui-map-card")
|
||||||
class HuiMapCard extends LitElement implements LovelaceCard {
|
class HuiMapCard extends LitElement implements LovelaceCard {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -337,32 +332,23 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getSourceEntities(states?: HassEntities): GeoEntity[] {
|
private _getSourceEntities(states?: HassEntities): string[] {
|
||||||
if (!states || !this._config?.geo_location_sources) {
|
if (!states || !this._config?.geo_location_sources) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceObjs = this._config.geo_location_sources.map((source) =>
|
const geoEntities: string[] = [];
|
||||||
typeof source === "string" ? { source } : source
|
|
||||||
);
|
|
||||||
|
|
||||||
const geoEntities: GeoEntity[] = [];
|
|
||||||
// Calculate visible geo location sources
|
// Calculate visible geo location sources
|
||||||
const allSource = sourceObjs.find((s) => s.source === "all");
|
const includesAll = this._config.geo_location_sources.includes("all");
|
||||||
for (const stateObj of Object.values(states)) {
|
for (const stateObj of Object.values(states)) {
|
||||||
const sourceObj = sourceObjs.find(
|
|
||||||
(s) => s.source === stateObj.attributes.source
|
|
||||||
);
|
|
||||||
if (
|
if (
|
||||||
computeDomain(stateObj.entity_id) === "geo_location" &&
|
computeDomain(stateObj.entity_id) === "geo_location" &&
|
||||||
(allSource || sourceObj)
|
(includesAll ||
|
||||||
|
this._config.geo_location_sources.includes(
|
||||||
|
stateObj.attributes.source
|
||||||
|
))
|
||||||
) {
|
) {
|
||||||
geoEntities.push({
|
geoEntities.push(stateObj.entity_id);
|
||||||
entity_id: stateObj.entity_id,
|
|
||||||
focus: sourceObj
|
|
||||||
? (sourceObj.focus ?? true)
|
|
||||||
: (allSource?.focus ?? true),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return geoEntities;
|
return geoEntities;
|
||||||
@@ -378,9 +364,8 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
name: entityConf.name,
|
name: entityConf.name,
|
||||||
})),
|
})),
|
||||||
...this._getSourceEntities(this.hass?.states).map((entity) => ({
|
...this._getSourceEntities(this.hass?.states).map((entity) => ({
|
||||||
entity_id: entity.entity_id,
|
entity_id: entity,
|
||||||
focus: entity.focus,
|
color: this._getColor(entity),
|
||||||
color: this._getColor(entity.entity_id),
|
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -298,11 +298,6 @@ export interface LogbookCardConfig extends LovelaceCardConfig {
|
|||||||
theme?: string;
|
theme?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GeoLocationSourceConfig {
|
|
||||||
source: string;
|
|
||||||
focus?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MapCardConfig extends LovelaceCardConfig {
|
export interface MapCardConfig extends LovelaceCardConfig {
|
||||||
type: "map";
|
type: "map";
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -312,7 +307,7 @@ export interface MapCardConfig extends LovelaceCardConfig {
|
|||||||
default_zoom?: number;
|
default_zoom?: number;
|
||||||
entities?: Array<EntityConfig | string>;
|
entities?: Array<EntityConfig | string>;
|
||||||
hours_to_show?: number;
|
hours_to_show?: number;
|
||||||
geo_location_sources?: Array<GeoLocationSourceConfig | string>;
|
geo_location_sources?: string[];
|
||||||
dark_mode?: boolean;
|
dark_mode?: boolean;
|
||||||
theme_mode?: ThemeMode;
|
theme_mode?: ThemeMode;
|
||||||
}
|
}
|
||||||
|