mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 09:16:38 +00:00
Merge pull request #8433 from home-assistant/dev
This commit is contained in:
commit
216526e391
@ -4,7 +4,7 @@
|
|||||||
"dockerfile": "Dockerfile",
|
"dockerfile": "Dockerfile",
|
||||||
"context": ".."
|
"context": ".."
|
||||||
},
|
},
|
||||||
"appPort": 8123,
|
"appPort": "8124:8123",
|
||||||
"context": "..",
|
"context": "..",
|
||||||
"postCreateCommand": "script/bootstrap",
|
"postCreateCommand": "script/bootstrap",
|
||||||
"extensions": [
|
"extensions": [
|
||||||
|
138
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
138
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
name: Report a bug with the UI, Frontend or Lovelace
|
||||||
|
about: Report an issue related to the Home Assistant frontend.
|
||||||
|
labels: bug
|
||||||
|
title: ""
|
||||||
|
issue_body: true
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Make sure you are running the [latest version of Home Assistant][releases] before reporting an issue.
|
||||||
|
|
||||||
|
If you have a feature or enhancement request for the frontend, please [start an discussion][fr] instead of creating an issue.
|
||||||
|
|
||||||
|
**Please not not report issues for custom Lovelace cards.**
|
||||||
|
|
||||||
|
[fr]: https://github.com/home-assistant/frontend/discussions
|
||||||
|
[releases]: https://github.com/home-assistant/home-assistant/releases
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Checklist
|
||||||
|
description: Please verify that you've followed these steps
|
||||||
|
options:
|
||||||
|
- label: I have updated to the latest available Home Assistant version.
|
||||||
|
required: true
|
||||||
|
- label: I have cleared the cache of my browser.
|
||||||
|
required: true
|
||||||
|
- label: I have tried a different browser to see if it is related to my browser.
|
||||||
|
required: true
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## The problem
|
||||||
|
- type: textarea
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Describe the issue you are experiencing
|
||||||
|
description: Provide a clear and concise description of what the bug is.
|
||||||
|
- type: textarea
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Describe the behavior you expected
|
||||||
|
description: Describe what you expected to happen or it should look/behave.
|
||||||
|
- type: textarea
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce the issue
|
||||||
|
description: |
|
||||||
|
Please tell us exactly how to reproduce your issue.
|
||||||
|
Provide clear and concise step by step instructions and add code snippets if needed.
|
||||||
|
value: |
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
...
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Environment
|
||||||
|
- type: input
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: What version of Home Assistant Core has the issue?
|
||||||
|
placeholder: core-
|
||||||
|
description: >
|
||||||
|
Can be found in the Configuration panel -> Info.
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: What was the last working version of Home Assistant Core?
|
||||||
|
placeholder: core-
|
||||||
|
description: >
|
||||||
|
If known, otherwise leave blank.
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: In which browser are you experiencing the issue with?
|
||||||
|
placeholder: Google Chrome 88.0.4324.150
|
||||||
|
description: >
|
||||||
|
Provide the full name and don't forget to add the version!
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Which operating system are you using to run this browser?
|
||||||
|
placeholder: macOS Big Sur (1.11)
|
||||||
|
description: >
|
||||||
|
Don't forget to add the version!
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
# Details
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: State of relevant entities
|
||||||
|
description: >
|
||||||
|
If your issue is about how an entity is shown in the UI, please add the
|
||||||
|
state and attributes for all situations. You can find this information
|
||||||
|
at Developer Tools -> States.
|
||||||
|
value: |
|
||||||
|
```yaml
|
||||||
|
# Paste your state here.
|
||||||
|
|
||||||
|
```
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Problem-relevant frontend configuration
|
||||||
|
description: >
|
||||||
|
An example configuration that caused the problem for you, e.g., the YAML
|
||||||
|
configuration of the used cards. Fill this out even if it seems
|
||||||
|
unimportant to you. Please be sure to remove personal information like
|
||||||
|
passwords, private URLs and other credentials.
|
||||||
|
value: |
|
||||||
|
```yaml
|
||||||
|
# Paste your YAML here.
|
||||||
|
|
||||||
|
```
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Javascript errors shown in your browser console/inspector
|
||||||
|
description: >
|
||||||
|
If you come across any Javascript or other error logs, e.g., in your
|
||||||
|
browser console/inspector please provide them.
|
||||||
|
value: |
|
||||||
|
```txt
|
||||||
|
# Paste your logs here.
|
||||||
|
|
||||||
|
```
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Additional information
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
If you have any additional information for us, use the field below.
|
||||||
|
Please note, you can attach screenshots or screen recordings here,
|
||||||
|
by dragging and dropping files in the field below.
|
@ -1,7 +1,7 @@
|
|||||||
const webpack = require("webpack");
|
const webpack = require("webpack");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const TerserPlugin = require("terser-webpack-plugin");
|
const TerserPlugin = require("terser-webpack-plugin");
|
||||||
const ManifestPlugin = require("webpack-manifest-plugin");
|
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
|
||||||
const paths = require("./paths.js");
|
const paths = require("./paths.js");
|
||||||
const bundle = require("./bundle");
|
const bundle = require("./bundle");
|
||||||
const log = require("fancy-log");
|
const log = require("fancy-log");
|
||||||
@ -68,7 +68,7 @@ const createWebpackConfig = ({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new ManifestPlugin({
|
new WebpackManifestPlugin({
|
||||||
// Only include the JS of entrypoints
|
// Only include the JS of entrypoints
|
||||||
filter: (file) => file.isInitial && !file.name.endsWith(".map"),
|
filter: (file) => file.isInitial && !file.name.endsWith(".map"),
|
||||||
}),
|
}),
|
||||||
|
@ -48,7 +48,7 @@ class HcCast extends LitElement {
|
|||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (this.lovelaceConfig === undefined) {
|
if (this.lovelaceConfig === undefined) {
|
||||||
return html` <hass-loading-screen no-toolbar></hass-loading-screen>> `;
|
return html`<hass-loading-screen no-toolbar></hass-loading-screen>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const error =
|
const error =
|
||||||
|
@ -11,19 +11,18 @@ import {
|
|||||||
PropertyValues,
|
PropertyValues,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { html, TemplateResult } from "lit-html";
|
import { html, TemplateResult } from "lit-html";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
import { atLeastVersion } from "../../../src/common/config/version";
|
import { atLeastVersion } from "../../../src/common/config/version";
|
||||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||||
import "../../../src/common/search/search-input";
|
import "../../../src/common/search/search-input";
|
||||||
import "../../../src/components/ha-button-menu";
|
import "../../../src/components/ha-button-menu";
|
||||||
import "../../../src/components/ha-svg-icon";
|
import "../../../src/components/ha-svg-icon";
|
||||||
import {
|
import {
|
||||||
fetchHassioAddonsInfo,
|
|
||||||
HassioAddonInfo,
|
HassioAddonInfo,
|
||||||
HassioAddonRepository,
|
HassioAddonRepository,
|
||||||
reloadHassioAddons,
|
reloadHassioAddons,
|
||||||
} from "../../../src/data/hassio/addon";
|
} from "../../../src/data/hassio/addon";
|
||||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||||
import { fetchHassioSupervisorInfo } from "../../../src/data/hassio/supervisor";
|
|
||||||
import "../../../src/layouts/hass-loading-screen";
|
import "../../../src/layouts/hass-loading-screen";
|
||||||
import "../../../src/layouts/hass-tabs-subpage";
|
import "../../../src/layouts/hass-tabs-subpage";
|
||||||
import { HomeAssistant, Route } from "../../../src/types";
|
import { HomeAssistant, Route } from "../../../src/types";
|
||||||
@ -51,46 +50,27 @@ const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
|
|||||||
class HassioAddonStore extends LitElement {
|
class HassioAddonStore extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||||
|
|
||||||
@property({ type: Boolean }) public narrow!: boolean;
|
@property({ type: Boolean }) public narrow!: boolean;
|
||||||
|
|
||||||
@property({ attribute: false }) public route!: Route;
|
@property({ attribute: false }) public route!: Route;
|
||||||
|
|
||||||
@property({ attribute: false }) private _addons?: HassioAddonInfo[];
|
|
||||||
|
|
||||||
@property({ attribute: false }) private _repos?: HassioAddonRepository[];
|
|
||||||
|
|
||||||
@internalProperty() private _filter?: string;
|
@internalProperty() private _filter?: string;
|
||||||
|
|
||||||
public async refreshData() {
|
public async refreshData() {
|
||||||
this._repos = undefined;
|
|
||||||
this._addons = undefined;
|
|
||||||
this._filter = undefined;
|
|
||||||
await reloadHassioAddons(this.hass);
|
await reloadHassioAddons(this.hass);
|
||||||
await this._loadData();
|
await this._loadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const repos: TemplateResult[] = [];
|
let repos: TemplateResult[] = [];
|
||||||
|
|
||||||
if (this._repos) {
|
if (this.supervisor.addon.repositories) {
|
||||||
for (const repo of this._repos) {
|
repos = this.addonRepositories(
|
||||||
const addons = this._addons!.filter(
|
this.supervisor.addon.repositories,
|
||||||
(addon) => addon.repository === repo.slug
|
this.supervisor.addon.addons
|
||||||
);
|
);
|
||||||
|
|
||||||
if (addons.length === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
repos.push(html`
|
|
||||||
<hassio-addon-repository
|
|
||||||
.hass=${this.hass}
|
|
||||||
.repo=${repo}
|
|
||||||
.addons=${addons}
|
|
||||||
.filter=${this._filter!}
|
|
||||||
></hassio-addon-repository>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@ -159,6 +139,27 @@ class HassioAddonStore extends LitElement {
|
|||||||
this._loadData();
|
this._loadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private addonRepositories = memoizeOne(
|
||||||
|
(repositories: HassioAddonRepository[], addons: HassioAddonInfo[]) => {
|
||||||
|
return repositories.sort(sortRepos).map((repo) => {
|
||||||
|
const filteredAddons = addons.filter(
|
||||||
|
(addon) => addon.repository === repo.slug
|
||||||
|
);
|
||||||
|
|
||||||
|
return filteredAddons.length !== 0
|
||||||
|
? html`
|
||||||
|
<hassio-addon-repository
|
||||||
|
.hass=${this.hass}
|
||||||
|
.repo=${repo}
|
||||||
|
.addons=${filteredAddons}
|
||||||
|
.filter=${this._filter!}
|
||||||
|
></hassio-addon-repository>
|
||||||
|
`
|
||||||
|
: html``;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||||
switch (ev.detail.index) {
|
switch (ev.detail.index) {
|
||||||
case 0:
|
case 0:
|
||||||
@ -181,7 +182,7 @@ class HassioAddonStore extends LitElement {
|
|||||||
|
|
||||||
private async _manageRepositories() {
|
private async _manageRepositories() {
|
||||||
showRepositoriesDialog(this, {
|
showRepositoriesDialog(this, {
|
||||||
repos: this._repos!,
|
repos: this.supervisor.addon.repositories,
|
||||||
loadData: () => this._loadData(),
|
loadData: () => this._loadData(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -191,18 +192,8 @@ class HassioAddonStore extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _loadData() {
|
private async _loadData() {
|
||||||
try {
|
fireEvent(this, "supervisor-store-refresh", { store: "addon" });
|
||||||
const [addonsInfo, supervisor] = await Promise.all([
|
fireEvent(this, "supervisor-store-refresh", { store: "supervisor" });
|
||||||
fetchHassioAddonsInfo(this.hass),
|
|
||||||
fetchHassioSupervisorInfo(this.hass),
|
|
||||||
]);
|
|
||||||
fireEvent(this, "supervisor-update", { supervisor });
|
|
||||||
this._repos = addonsInfo.repositories;
|
|
||||||
this._repos.sort(sortRepos);
|
|
||||||
this._addons = addonsInfo.addons;
|
|
||||||
} catch (err) {
|
|
||||||
alert(extractApiErrorMessage(err));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _filterChanged(e) {
|
private async _filterChanged(e) {
|
||||||
|
@ -29,7 +29,7 @@ class HassioAddonConfigDashboard extends LitElement {
|
|||||||
const hasOptions =
|
const hasOptions =
|
||||||
this.addon.options && Object.keys(this.addon.options).length;
|
this.addon.options && Object.keys(this.addon.options).length;
|
||||||
const hasSchema =
|
const hasSchema =
|
||||||
this.addon.schema && Object.keys(this.addon.schema).length;
|
hasOptions && this.addon.schema && Object.keys(this.addon.schema).length;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
@ -109,8 +109,8 @@ class HassioAddonConfig extends LitElement {
|
|||||||
protected firstUpdated(changedProps) {
|
protected firstUpdated(changedProps) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
this._canShowSchema =
|
this._canShowSchema =
|
||||||
this.addon.schema !== null &&
|
Object.keys(this.addon.options).length !== 0 &&
|
||||||
!this.addon.schema.find(
|
!this.addon.schema!.find(
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
(entry) => !SUPPORTED_UI_TYPES.includes(entry.type) || entry.multiple
|
(entry) => !SUPPORTED_UI_TYPES.includes(entry.type) || entry.multiple
|
||||||
);
|
);
|
||||||
@ -150,13 +150,11 @@ class HassioAddonConfig extends LitElement {
|
|||||||
if (this.addon.schema && this._canShowSchema && !this._yamlMode) {
|
if (this.addon.schema && this._canShowSchema && !this._yamlMode) {
|
||||||
this._valid = true;
|
this._valid = true;
|
||||||
this._configHasChanged = true;
|
this._configHasChanged = true;
|
||||||
|
this._options! = ev.detail.value;
|
||||||
} else {
|
} else {
|
||||||
this._configHasChanged = true;
|
this._configHasChanged = true;
|
||||||
this._valid = ev.detail.isValid;
|
this._valid = ev.detail.isValid;
|
||||||
}
|
}
|
||||||
if (this._valid) {
|
|
||||||
this._options! = ev.detail.value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _resetTapped(ev: CustomEvent): Promise<void> {
|
private async _resetTapped(ev: CustomEvent): Promise<void> {
|
||||||
@ -204,8 +202,9 @@ class HassioAddonConfig extends LitElement {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await setHassioAddonOption(this.hass, this.addon.slug, {
|
await setHassioAddonOption(this.hass, this.addon.slug, {
|
||||||
options: this._options!,
|
options: this._yamlMode ? this._editor?.value : this._options,
|
||||||
});
|
});
|
||||||
|
|
||||||
this._configHasChanged = false;
|
this._configHasChanged = false;
|
||||||
const eventdata = {
|
const eventdata = {
|
||||||
success: true,
|
success: true,
|
||||||
|
@ -9,17 +9,24 @@ import {
|
|||||||
CSSResult,
|
CSSResult,
|
||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
|
internalProperty,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||||
|
import { navigate } from "../../../src/common/navigate";
|
||||||
|
import { extractSearchParam } from "../../../src/common/url/search-params";
|
||||||
import "../../../src/components/ha-circular-progress";
|
import "../../../src/components/ha-circular-progress";
|
||||||
import {
|
import {
|
||||||
fetchHassioAddonInfo,
|
fetchHassioAddonInfo,
|
||||||
HassioAddonDetails,
|
HassioAddonDetails,
|
||||||
} from "../../../src/data/hassio/addon";
|
} from "../../../src/data/hassio/addon";
|
||||||
|
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||||
|
import "../../../src/layouts/hass-error-screen";
|
||||||
|
import "../../../src/layouts/hass-loading-screen";
|
||||||
import "../../../src/layouts/hass-tabs-subpage";
|
import "../../../src/layouts/hass-tabs-subpage";
|
||||||
import type { PageNavigation } from "../../../src/layouts/hass-tabs-subpage";
|
import type { PageNavigation } from "../../../src/layouts/hass-tabs-subpage";
|
||||||
import { haStyle } from "../../../src/resources/styles";
|
import { haStyle } from "../../../src/resources/styles";
|
||||||
@ -44,6 +51,8 @@ class HassioAddonDashboard extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public narrow!: boolean;
|
@property({ type: Boolean }) public narrow!: boolean;
|
||||||
|
|
||||||
|
@internalProperty() _error?: string;
|
||||||
|
|
||||||
private _computeTail = memoizeOne((route: Route) => {
|
private _computeTail = memoizeOne((route: Route) => {
|
||||||
const dividerPos = route.path.indexOf("/", 1);
|
const dividerPos = route.path.indexOf("/", 1);
|
||||||
return dividerPos === -1
|
return dividerPos === -1
|
||||||
@ -58,8 +67,14 @@ class HassioAddonDashboard extends LitElement {
|
|||||||
});
|
});
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
|
if (this._error) {
|
||||||
|
return html`<hass-error-screen
|
||||||
|
.error=${this._error}
|
||||||
|
></hass-error-screen>`;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.addon) {
|
if (!this.addon) {
|
||||||
return html`<ha-circular-progress active></ha-circular-progress>`;
|
return html`<hass-loading-screen></hass-loading-screen>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const addonTabs: PageNavigation[] = [
|
const addonTabs: PageNavigation[] = [
|
||||||
@ -156,30 +171,51 @@ class HassioAddonDashboard extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async firstUpdated(): Promise<void> {
|
protected async firstUpdated(): Promise<void> {
|
||||||
await this._routeDataChanged(this.route);
|
if (this.route.path === "") {
|
||||||
|
const addon = extractSearchParam("addon");
|
||||||
|
if (addon) {
|
||||||
|
navigate(this, `/hassio/addon/${addon}`, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _apiCalled(ev): Promise<void> {
|
private async _apiCalled(ev): Promise<void> {
|
||||||
const path: string = ev.detail.path;
|
const pathSplit: string[] = ev.detail.path?.split("/");
|
||||||
|
|
||||||
if (!path) {
|
if (!pathSplit || pathSplit.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const path: string = pathSplit[pathSplit.length - 1];
|
||||||
|
|
||||||
|
if (["uninstall", "install", "update", "start", "stop"].includes(path)) {
|
||||||
|
fireEvent(this, "supervisor-store-refresh", { store: "supervisor" });
|
||||||
|
}
|
||||||
|
|
||||||
if (path === "uninstall") {
|
if (path === "uninstall") {
|
||||||
history.back();
|
window.history.back();
|
||||||
} else {
|
} else {
|
||||||
await this._routeDataChanged(this.route);
|
await this._routeDataChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _routeDataChanged(routeData: Route): Promise<void> {
|
protected updated(changedProperties) {
|
||||||
const addon = routeData.path.split("/")[1];
|
if (changedProperties.has("route") && !this.addon) {
|
||||||
|
this._routeDataChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _routeDataChanged(): Promise<void> {
|
||||||
|
const addon = this.route.path.split("/")[1];
|
||||||
|
if (!addon) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const addoninfo = await fetchHassioAddonInfo(this.hass, addon);
|
const addoninfo = await fetchHassioAddonInfo(this.hass, addon);
|
||||||
this.addon = addoninfo;
|
this.addon = addoninfo;
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
this._error = `Error fetching addon info: ${extractApiErrorMessage(err)}`;
|
||||||
this.addon = undefined;
|
this.addon = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,10 +43,13 @@ import {
|
|||||||
HassioAddonSetOptionParams,
|
HassioAddonSetOptionParams,
|
||||||
HassioAddonSetSecurityParams,
|
HassioAddonSetSecurityParams,
|
||||||
installHassioAddon,
|
installHassioAddon,
|
||||||
|
restartHassioAddon,
|
||||||
setHassioAddonOption,
|
setHassioAddonOption,
|
||||||
setHassioAddonSecurity,
|
setHassioAddonSecurity,
|
||||||
startHassioAddon,
|
startHassioAddon,
|
||||||
|
stopHassioAddon,
|
||||||
uninstallHassioAddon,
|
uninstallHassioAddon,
|
||||||
|
updateHassioAddon,
|
||||||
validateHassioAddonOption,
|
validateHassioAddonOption,
|
||||||
} from "../../../../src/data/hassio/addon";
|
} from "../../../../src/data/hassio/addon";
|
||||||
import {
|
import {
|
||||||
@ -196,13 +199,9 @@ class HassioAddonInfo extends LitElement {
|
|||||||
: ""}
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<ha-call-api-button
|
<ha-progress-button @click=${this._updateClicked}>
|
||||||
.hass=${this.hass}
|
|
||||||
.disabled=${!this.addon.available}
|
|
||||||
path="hassio/addons/${this.addon.slug}/update"
|
|
||||||
>
|
|
||||||
Update
|
Update
|
||||||
</ha-call-api-button>
|
</ha-progress-button>
|
||||||
${this.addon.changelog
|
${this.addon.changelog
|
||||||
? html`
|
? html`
|
||||||
<mwc-button @click=${this._openChangelog}>
|
<mwc-button @click=${this._openChangelog}>
|
||||||
@ -579,20 +578,18 @@ class HassioAddonInfo extends LitElement {
|
|||||||
${this.addon.version
|
${this.addon.version
|
||||||
? this._computeIsRunning
|
? this._computeIsRunning
|
||||||
? html`
|
? html`
|
||||||
<ha-call-api-button
|
<ha-progress-button
|
||||||
class="warning"
|
class="warning"
|
||||||
.hass=${this.hass}
|
@click=${this._stopClicked}
|
||||||
.path="hassio/addons/${this.addon.slug}/stop"
|
|
||||||
>
|
>
|
||||||
Stop
|
Stop
|
||||||
</ha-call-api-button>
|
</ha-progress-button>
|
||||||
<ha-call-api-button
|
<ha-progress-button
|
||||||
class="warning"
|
class="warning"
|
||||||
.hass=${this.hass}
|
@click=${this._restartClicked}
|
||||||
.path="hassio/addons/${this.addon.slug}/restart"
|
|
||||||
>
|
>
|
||||||
Restart
|
Restart
|
||||||
</ha-call-api-button>
|
</ha-progress-button>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<ha-progress-button @click=${this._startClicked}>
|
<ha-progress-button @click=${this._startClicked}>
|
||||||
@ -883,6 +880,82 @@ class HassioAddonInfo extends LitElement {
|
|||||||
button.progress = false;
|
button.progress = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _stopClicked(ev: CustomEvent): Promise<void> {
|
||||||
|
const button = ev.currentTarget as any;
|
||||||
|
button.progress = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await stopHassioAddon(this.hass, this.addon.slug);
|
||||||
|
const eventdata = {
|
||||||
|
success: true,
|
||||||
|
response: undefined,
|
||||||
|
path: "stop",
|
||||||
|
};
|
||||||
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
|
} catch (err) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: "Failed to stop addon",
|
||||||
|
text: extractApiErrorMessage(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
button.progress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _restartClicked(ev: CustomEvent): Promise<void> {
|
||||||
|
const button = ev.currentTarget as any;
|
||||||
|
button.progress = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await restartHassioAddon(this.hass, this.addon.slug);
|
||||||
|
const eventdata = {
|
||||||
|
success: true,
|
||||||
|
response: undefined,
|
||||||
|
path: "stop",
|
||||||
|
};
|
||||||
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
|
} catch (err) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: "Failed to restart addon",
|
||||||
|
text: extractApiErrorMessage(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
button.progress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _updateClicked(ev: CustomEvent): Promise<void> {
|
||||||
|
const button = ev.currentTarget as any;
|
||||||
|
button.progress = true;
|
||||||
|
|
||||||
|
const confirmed = await showConfirmationDialog(this, {
|
||||||
|
title: this.addon.name,
|
||||||
|
text: "Are you sure you want to update this add-on?",
|
||||||
|
confirmText: "update add-on",
|
||||||
|
dismissText: "no",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
button.progress = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._error = undefined;
|
||||||
|
try {
|
||||||
|
await updateHassioAddon(this.hass, this.addon.slug);
|
||||||
|
const eventdata = {
|
||||||
|
success: true,
|
||||||
|
response: undefined,
|
||||||
|
path: "update",
|
||||||
|
};
|
||||||
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
|
} catch (err) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: "Failed to update addon",
|
||||||
|
text: extractApiErrorMessage(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
button.progress = false;
|
||||||
|
}
|
||||||
|
|
||||||
private async _startClicked(ev: CustomEvent): Promise<void> {
|
private async _startClicked(ev: CustomEvent): Promise<void> {
|
||||||
const button = ev.currentTarget as any;
|
const button = ev.currentTarget as any;
|
||||||
button.progress = true;
|
button.progress = true;
|
||||||
@ -891,10 +964,10 @@ class HassioAddonInfo extends LitElement {
|
|||||||
this.hass,
|
this.hass,
|
||||||
this.addon.slug
|
this.addon.slug
|
||||||
);
|
);
|
||||||
if (!validate.data.valid) {
|
if (!validate.valid) {
|
||||||
await showConfirmationDialog(this, {
|
await showConfirmationDialog(this, {
|
||||||
title: "Failed to start addon - configuration validation failed!",
|
title: "Failed to start addon - configuration validation failed!",
|
||||||
text: validate.data.message.split(" Got ")[0],
|
text: validate.message.split(" Got ")[0],
|
||||||
confirm: () => this._openConfiguration(),
|
confirm: () => this._openConfiguration(),
|
||||||
confirmText: "Go to configuration",
|
confirmText: "Go to configuration",
|
||||||
dismissText: "Cancel",
|
dismissText: "Cancel",
|
||||||
@ -914,6 +987,12 @@ class HassioAddonInfo extends LitElement {
|
|||||||
try {
|
try {
|
||||||
await startHassioAddon(this.hass, this.addon.slug);
|
await startHassioAddon(this.hass, this.addon.slug);
|
||||||
this.addon = await fetchHassioAddonInfo(this.hass, this.addon.slug);
|
this.addon = await fetchHassioAddonInfo(this.hass, this.addon.slug);
|
||||||
|
const eventdata = {
|
||||||
|
success: true,
|
||||||
|
response: undefined,
|
||||||
|
path: "start",
|
||||||
|
};
|
||||||
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: "Failed to start addon",
|
title: "Failed to start addon",
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||||
import "../../../src/components/buttons/ha-progress-button";
|
import "../../../src/components/buttons/ha-progress-button";
|
||||||
import "../../../src/components/ha-card";
|
import "../../../src/components/ha-card";
|
||||||
import "../../../src/components/ha-svg-icon";
|
import "../../../src/components/ha-svg-icon";
|
||||||
@ -64,6 +65,7 @@ export class HassioUpdate extends LitElement {
|
|||||||
<div class="card-group">
|
<div class="card-group">
|
||||||
${this._renderUpdateCard(
|
${this._renderUpdateCard(
|
||||||
"Home Assistant Core",
|
"Home Assistant Core",
|
||||||
|
"core",
|
||||||
this.supervisor.core,
|
this.supervisor.core,
|
||||||
"hassio/homeassistant/update",
|
"hassio/homeassistant/update",
|
||||||
`https://${
|
`https://${
|
||||||
@ -72,6 +74,7 @@ export class HassioUpdate extends LitElement {
|
|||||||
)}
|
)}
|
||||||
${this._renderUpdateCard(
|
${this._renderUpdateCard(
|
||||||
"Supervisor",
|
"Supervisor",
|
||||||
|
"supervisor",
|
||||||
this.supervisor.supervisor,
|
this.supervisor.supervisor,
|
||||||
"hassio/supervisor/update",
|
"hassio/supervisor/update",
|
||||||
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisor.supervisor.version_latest}`
|
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisor.supervisor.version_latest}`
|
||||||
@ -79,6 +82,7 @@ export class HassioUpdate extends LitElement {
|
|||||||
${this.supervisor.host.features.includes("hassos")
|
${this.supervisor.host.features.includes("hassos")
|
||||||
? this._renderUpdateCard(
|
? this._renderUpdateCard(
|
||||||
"Operating System",
|
"Operating System",
|
||||||
|
"os",
|
||||||
this.supervisor.os,
|
this.supervisor.os,
|
||||||
"hassio/os/update",
|
"hassio/os/update",
|
||||||
`https://github.com//home-assistant/hassos/releases/tag/${this.supervisor.os.version_latest}`
|
`https://github.com//home-assistant/hassos/releases/tag/${this.supervisor.os.version_latest}`
|
||||||
@ -91,6 +95,7 @@ export class HassioUpdate extends LitElement {
|
|||||||
|
|
||||||
private _renderUpdateCard(
|
private _renderUpdateCard(
|
||||||
name: string,
|
name: string,
|
||||||
|
key: string,
|
||||||
object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo,
|
object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo,
|
||||||
apiPath: string,
|
apiPath: string,
|
||||||
releaseNotesUrl: string
|
releaseNotesUrl: string
|
||||||
@ -116,6 +121,7 @@ export class HassioUpdate extends LitElement {
|
|||||||
<ha-progress-button
|
<ha-progress-button
|
||||||
.apiPath=${apiPath}
|
.apiPath=${apiPath}
|
||||||
.name=${name}
|
.name=${name}
|
||||||
|
.key=${key}
|
||||||
.version=${object.version_latest}
|
.version=${object.version_latest}
|
||||||
@click=${this._confirmUpdate}
|
@click=${this._confirmUpdate}
|
||||||
>
|
>
|
||||||
@ -142,6 +148,7 @@ export class HassioUpdate extends LitElement {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
|
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
|
||||||
|
fireEvent(this, "supervisor-store-refresh", { store: item.key });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Only show an error if the status code was not expected (user behind proxy)
|
// Only show an error if the status code was not expected (user behind proxy)
|
||||||
// or no status at all(connection terminated)
|
// or no status at all(connection terminated)
|
||||||
|
@ -22,7 +22,11 @@ import {
|
|||||||
fetchHassioSnapshotInfo,
|
fetchHassioSnapshotInfo,
|
||||||
HassioSnapshotDetail,
|
HassioSnapshotDetail,
|
||||||
} from "../../../../src/data/hassio/snapshot";
|
} from "../../../../src/data/hassio/snapshot";
|
||||||
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||||
|
import {
|
||||||
|
showAlertDialog,
|
||||||
|
showConfirmationDialog,
|
||||||
|
} from "../../../../src/dialogs/generic/show-dialog-box";
|
||||||
import { PolymerChangedEvent } from "../../../../src/polymer-types";
|
import { PolymerChangedEvent } from "../../../../src/polymer-types";
|
||||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||||
import { HomeAssistant } from "../../../../src/types";
|
import { HomeAssistant } from "../../../../src/types";
|
||||||
@ -75,6 +79,8 @@ interface FolderItem {
|
|||||||
class HassioSnapshotDialog extends LitElement {
|
class HassioSnapshotDialog extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public supervisor?: Supervisor;
|
||||||
|
|
||||||
@internalProperty() private _error?: string;
|
@internalProperty() private _error?: string;
|
||||||
|
|
||||||
@internalProperty() private _onboarding = false;
|
@internalProperty() private _onboarding = false;
|
||||||
@ -102,6 +108,7 @@ class HassioSnapshotDialog extends LitElement {
|
|||||||
|
|
||||||
this._dialogParams = params;
|
this._dialogParams = params;
|
||||||
this._onboarding = params.onboarding ?? false;
|
this._onboarding = params.onboarding ?? false;
|
||||||
|
this.supervisor = params.supervisor;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
@ -298,6 +305,16 @@ class HassioSnapshotDialog extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _partialRestoreClicked() {
|
private async _partialRestoreClicked() {
|
||||||
|
if (
|
||||||
|
this.supervisor !== undefined &&
|
||||||
|
this.supervisor.info.state !== "running"
|
||||||
|
) {
|
||||||
|
await showAlertDialog(this, {
|
||||||
|
title: "Could not restore snapshot",
|
||||||
|
text: `Restoring a snapshot is not possible right now because the system is in ${this.supervisor.info.state} state.`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
!(await showConfirmationDialog(this, {
|
!(await showConfirmationDialog(this, {
|
||||||
title: "Are you sure you want partially to restore this snapshot?",
|
title: "Are you sure you want partially to restore this snapshot?",
|
||||||
@ -359,6 +376,16 @@ class HassioSnapshotDialog extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _fullRestoreClicked() {
|
private async _fullRestoreClicked() {
|
||||||
|
if (
|
||||||
|
this.supervisor !== undefined &&
|
||||||
|
this.supervisor.info.state !== "running"
|
||||||
|
) {
|
||||||
|
await showAlertDialog(this, {
|
||||||
|
title: "Could not restore snapshot",
|
||||||
|
text: `Restoring a snapshot is not possible right now because the system is in ${this.supervisor.info.state} state.`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
!(await showConfirmationDialog(this, {
|
!(await showConfirmationDialog(this, {
|
||||||
title:
|
title:
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
|
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||||
|
|
||||||
export interface HassioSnapshotDialogParams {
|
export interface HassioSnapshotDialogParams {
|
||||||
slug: string;
|
slug: string;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
onboarding?: boolean;
|
onboarding?: boolean;
|
||||||
|
supervisor?: Supervisor;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const showHassioSnapshotDialog = (
|
export const showHassioSnapshotDialog = (
|
||||||
|
@ -3,7 +3,9 @@ import { atLeastVersion } from "../../src/common/config/version";
|
|||||||
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
|
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
|
||||||
import { fireEvent } from "../../src/common/dom/fire_event";
|
import { fireEvent } from "../../src/common/dom/fire_event";
|
||||||
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
|
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
|
||||||
|
import { supervisorStore } from "../../src/data/supervisor/supervisor";
|
||||||
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
||||||
|
import "../../src/layouts/hass-loading-screen";
|
||||||
import { HomeAssistant, Route } from "../../src/types";
|
import { HomeAssistant, Route } from "../../src/types";
|
||||||
import "./hassio-router";
|
import "./hassio-router";
|
||||||
import { SupervisorBaseElement } from "./supervisor-base-element";
|
import { SupervisorBaseElement } from "./supervisor-base-element";
|
||||||
@ -71,8 +73,15 @@ export class HassioMain extends SupervisorBaseElement {
|
|||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.supervisor || !this.hass) {
|
if (!this.supervisor || !this.hass) {
|
||||||
return html``;
|
return html`<hass-loading-screen></hass-loading-screen>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
Object.keys(supervisorStore).some((store) => !this.supervisor![store])
|
||||||
|
) {
|
||||||
|
return html`<hass-loading-screen></hass-loading-screen>`;
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<hassio-router
|
<hassio-router
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
128
hassio/src/hassio-my-redirect.ts
Normal file
128
hassio/src/hassio-my-redirect.ts
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import {
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
internalProperty,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit-element";
|
||||||
|
import { sanitizeUrl } from "@braintree/sanitize-url";
|
||||||
|
import {
|
||||||
|
createSearchParam,
|
||||||
|
extractSearchParamsObject,
|
||||||
|
} from "../../src/common/url/search-params";
|
||||||
|
import "../../src/layouts/hass-error-screen";
|
||||||
|
import {
|
||||||
|
ParamType,
|
||||||
|
Redirect,
|
||||||
|
Redirects,
|
||||||
|
} from "../../src/panels/my/ha-panel-my";
|
||||||
|
import { navigate } from "../../src/common/navigate";
|
||||||
|
import { HomeAssistant, Route } from "../../src/types";
|
||||||
|
|
||||||
|
const REDIRECTS: Redirects = {
|
||||||
|
supervisor_logs: {
|
||||||
|
redirect: "/hassio/system",
|
||||||
|
},
|
||||||
|
supervisor_info: {
|
||||||
|
redirect: "/hassio/system",
|
||||||
|
},
|
||||||
|
supervisor_snapshots: {
|
||||||
|
redirect: "/hassio/snapshots",
|
||||||
|
},
|
||||||
|
supervisor_store: {
|
||||||
|
redirect: "/hassio/store",
|
||||||
|
},
|
||||||
|
supervisor: {
|
||||||
|
redirect: "/hassio/dashboard",
|
||||||
|
},
|
||||||
|
supervisor_addon: {
|
||||||
|
redirect: "/hassio/addon",
|
||||||
|
params: {
|
||||||
|
addon: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
@customElement("hassio-my-redirect")
|
||||||
|
class HassioMyRedirect extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public route!: Route;
|
||||||
|
|
||||||
|
@internalProperty() public _error?: TemplateResult | string;
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
const path = this.route.path.substr(1);
|
||||||
|
const redirect = REDIRECTS[path];
|
||||||
|
|
||||||
|
if (!redirect) {
|
||||||
|
this._error = html`This redirect is not supported by your Home Assistant
|
||||||
|
instance. Check the
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
href="https://my.home-assistant.io/faq.html#supported-pages"
|
||||||
|
>My Home Assistant FAQ</a
|
||||||
|
>
|
||||||
|
for the supported redirects and the version they where introduced.`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url: string;
|
||||||
|
try {
|
||||||
|
url = this._createRedirectUrl(redirect);
|
||||||
|
} catch (err) {
|
||||||
|
this._error = "An unknown error occured";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(this, url, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (this._error) {
|
||||||
|
return html`<hass-error-screen
|
||||||
|
.error=${this._error}
|
||||||
|
></hass-error-screen>`;
|
||||||
|
}
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createRedirectUrl(redirect: Redirect): string {
|
||||||
|
const params = this._createRedirectParams(redirect);
|
||||||
|
return `${redirect.redirect}${params}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createRedirectParams(redirect: Redirect): string {
|
||||||
|
const params = extractSearchParamsObject();
|
||||||
|
if (!redirect.params && !Object.keys(params).length) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const resultParams = {};
|
||||||
|
Object.entries(redirect.params || {}).forEach(([key, type]) => {
|
||||||
|
if (!params[key] || !this._checkParamType(type, params[key])) {
|
||||||
|
throw Error();
|
||||||
|
}
|
||||||
|
resultParams[key] = params[key];
|
||||||
|
});
|
||||||
|
return `?${createSearchParam(resultParams)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _checkParamType(type: ParamType, value: string) {
|
||||||
|
if (type === "string") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (type === "url") {
|
||||||
|
return value && value === sanitizeUrl(value);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hassio-my-redirect": HassioMyRedirect;
|
||||||
|
}
|
||||||
|
}
|
@ -41,6 +41,10 @@ class HassioRouter extends HassRouterPage {
|
|||||||
tag: "hassio-ingress-view",
|
tag: "hassio-ingress-view",
|
||||||
load: () => import("./ingress-view/hassio-ingress-view"),
|
load: () => import("./ingress-view/hassio-ingress-view"),
|
||||||
},
|
},
|
||||||
|
_my_redirect: {
|
||||||
|
tag: "hassio-my-redirect",
|
||||||
|
load: () => import("./hassio-my-redirect"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -49,12 +53,13 @@ class HassioRouter extends HassRouterPage {
|
|||||||
const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail;
|
const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail;
|
||||||
|
|
||||||
el.hass = this.hass;
|
el.hass = this.hass;
|
||||||
el.supervisor = this.supervisor;
|
|
||||||
el.narrow = this.narrow;
|
el.narrow = this.narrow;
|
||||||
el.route = route;
|
el.route = route;
|
||||||
|
|
||||||
if (el.localName === "hassio-ingress-view") {
|
if (el.localName === "hassio-ingress-view") {
|
||||||
el.ingressPanel = this.panel.config && this.panel.config.ingress;
|
el.ingressPanel = this.panel.config && this.panel.config.ingress;
|
||||||
|
} else {
|
||||||
|
el.supervisor = this.supervisor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@ import {
|
|||||||
reloadHassioSnapshots,
|
reloadHassioSnapshots,
|
||||||
} from "../../../src/data/hassio/snapshot";
|
} from "../../../src/data/hassio/snapshot";
|
||||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||||
|
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||||
import "../../../src/layouts/hass-tabs-subpage";
|
import "../../../src/layouts/hass-tabs-subpage";
|
||||||
import { PolymerChangedEvent } from "../../../src/polymer-types";
|
import { PolymerChangedEvent } from "../../../src/polymer-types";
|
||||||
import { haStyle } from "../../../src/resources/styles";
|
import { haStyle } from "../../../src/resources/styles";
|
||||||
@ -211,7 +212,13 @@ class HassioSnapshots extends LitElement {
|
|||||||
: undefined}
|
: undefined}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<ha-progress-button @click=${this._createSnapshot}>
|
<ha-progress-button
|
||||||
|
@click=${this._createSnapshot}
|
||||||
|
title="${this.supervisor.info.state !== "running"
|
||||||
|
? `Creating a snapshot is not possible right now because the system is in ${this.supervisor.info.state} state.`
|
||||||
|
: ""}"
|
||||||
|
.disabled=${this.supervisor.info.state !== "running"}
|
||||||
|
>
|
||||||
Create
|
Create
|
||||||
</ha-progress-button>
|
</ha-progress-button>
|
||||||
</div>
|
</div>
|
||||||
@ -325,6 +332,12 @@ class HassioSnapshots extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _createSnapshot(ev: CustomEvent): Promise<void> {
|
private async _createSnapshot(ev: CustomEvent): Promise<void> {
|
||||||
|
if (this.supervisor.info.state !== "running") {
|
||||||
|
await showAlertDialog(this, {
|
||||||
|
title: "Could not create snapshot",
|
||||||
|
text: `Creating a snapshot is not possible right now because the system is in ${this.supervisor.info.state} state.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
const button = ev.currentTarget as any;
|
const button = ev.currentTarget as any;
|
||||||
button.progress = true;
|
button.progress = true;
|
||||||
|
|
||||||
@ -386,6 +399,7 @@ class HassioSnapshots extends LitElement {
|
|||||||
private _snapshotClicked(ev) {
|
private _snapshotClicked(ev) {
|
||||||
showHassioSnapshotDialog(this, {
|
showHassioSnapshotDialog(this, {
|
||||||
slug: ev.currentTarget!.snapshot.slug,
|
slug: ev.currentTarget!.snapshot.slug,
|
||||||
|
supervisor: this.supervisor,
|
||||||
onDelete: () => this._updateSnapshots(),
|
onDelete: () => this._updateSnapshots(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -395,6 +409,7 @@ class HassioSnapshots extends LitElement {
|
|||||||
showSnapshot: (slug: string) =>
|
showSnapshot: (slug: string) =>
|
||||||
showHassioSnapshotDialog(this, {
|
showHassioSnapshotDialog(this, {
|
||||||
slug,
|
slug,
|
||||||
|
supervisor: this.supervisor,
|
||||||
onDelete: () => this._updateSnapshots(),
|
onDelete: () => this._updateSnapshots(),
|
||||||
}),
|
}),
|
||||||
reloadSnapshot: () => this.refreshData(),
|
reloadSnapshot: () => this.refreshData(),
|
||||||
|
@ -1,4 +1,13 @@
|
|||||||
import { LitElement, property, PropertyValues } from "lit-element";
|
import { Collection, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
import {
|
||||||
|
internalProperty,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
PropertyValues,
|
||||||
|
} from "lit-element";
|
||||||
|
import { atLeastVersion } from "../../src/common/config/version";
|
||||||
|
import { fetchHassioAddonsInfo } from "../../src/data/hassio/addon";
|
||||||
|
import { HassioResponse } from "../../src/data/hassio/common";
|
||||||
import {
|
import {
|
||||||
fetchHassioHassOsInfo,
|
fetchHassioHassOsInfo,
|
||||||
fetchHassioHostInfo,
|
fetchHassioHostInfo,
|
||||||
@ -10,13 +19,20 @@ import {
|
|||||||
fetchHassioInfo,
|
fetchHassioInfo,
|
||||||
fetchHassioSupervisorInfo,
|
fetchHassioSupervisorInfo,
|
||||||
} from "../../src/data/hassio/supervisor";
|
} from "../../src/data/hassio/supervisor";
|
||||||
import { Supervisor } from "../../src/data/supervisor/supervisor";
|
import {
|
||||||
|
getSupervisorEventCollection,
|
||||||
|
subscribeSupervisorEvents,
|
||||||
|
Supervisor,
|
||||||
|
SupervisorObject,
|
||||||
|
supervisorStore,
|
||||||
|
} from "../../src/data/supervisor/supervisor";
|
||||||
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
|
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
|
||||||
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
|
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
"supervisor-update": Partial<Supervisor>;
|
"supervisor-update": Partial<Supervisor>;
|
||||||
|
"supervisor-store-refresh": { store: SupervisorObject };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,6 +41,20 @@ export class SupervisorBaseElement extends urlSyncMixin(
|
|||||||
) {
|
) {
|
||||||
@property({ attribute: false }) public supervisor?: Supervisor;
|
@property({ attribute: false }) public supervisor?: Supervisor;
|
||||||
|
|
||||||
|
@internalProperty() private _unsubs: Record<string, UnsubscribeFunc> = {};
|
||||||
|
|
||||||
|
@internalProperty() private _collections: Record<
|
||||||
|
string,
|
||||||
|
Collection<unknown>
|
||||||
|
> = {};
|
||||||
|
|
||||||
|
public disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
Object.keys(this._unsubs).forEach((unsub) => {
|
||||||
|
this._unsubs[unsub]();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected _updateSupervisor(obj: Partial<Supervisor>): void {
|
protected _updateSupervisor(obj: Partial<Supervisor>): void {
|
||||||
this.supervisor = { ...this.supervisor!, ...obj };
|
this.supervisor = { ...this.supervisor!, ...obj };
|
||||||
}
|
}
|
||||||
@ -32,13 +62,59 @@ export class SupervisorBaseElement extends urlSyncMixin(
|
|||||||
protected firstUpdated(changedProps: PropertyValues): void {
|
protected firstUpdated(changedProps: PropertyValues): void {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
this._initSupervisor();
|
this._initSupervisor();
|
||||||
this.addEventListener("supervisor-update", (ev) =>
|
}
|
||||||
this._updateSupervisor(ev.detail)
|
|
||||||
|
private async _handleSupervisorStoreRefreshEvent(ev) {
|
||||||
|
const store = ev.detail.store;
|
||||||
|
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
|
||||||
|
this._collections[store].refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.hass.callApi<HassioResponse<any>>(
|
||||||
|
"GET",
|
||||||
|
`hassio${supervisorStore[store]}`
|
||||||
);
|
);
|
||||||
|
this._updateSupervisor({ [store]: response.data });
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _initSupervisor(): Promise<void> {
|
private async _initSupervisor(): Promise<void> {
|
||||||
|
this.addEventListener(
|
||||||
|
"supervisor-store-refresh",
|
||||||
|
this._handleSupervisorStoreRefreshEvent
|
||||||
|
);
|
||||||
|
|
||||||
|
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
|
||||||
|
Object.keys(supervisorStore).forEach((store) => {
|
||||||
|
this._unsubs[store] = subscribeSupervisorEvents(
|
||||||
|
this.hass,
|
||||||
|
(data) => this._updateSupervisor({ [store]: data }),
|
||||||
|
store,
|
||||||
|
supervisorStore[store]
|
||||||
|
);
|
||||||
|
if (this._collections[store]) {
|
||||||
|
this._collections[store].refresh();
|
||||||
|
} else {
|
||||||
|
this._collections[store] = getSupervisorEventCollection(
|
||||||
|
this.hass.connection,
|
||||||
|
store,
|
||||||
|
supervisorStore[store]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.supervisor === undefined) {
|
||||||
|
Object.keys(this._collections).forEach((collection) =>
|
||||||
|
this._updateSupervisor({
|
||||||
|
[collection]: this._collections[collection].state,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const [
|
const [
|
||||||
|
addon,
|
||||||
supervisor,
|
supervisor,
|
||||||
host,
|
host,
|
||||||
core,
|
core,
|
||||||
@ -47,6 +123,7 @@ export class SupervisorBaseElement extends urlSyncMixin(
|
|||||||
network,
|
network,
|
||||||
resolution,
|
resolution,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
|
fetchHassioAddonsInfo(this.hass),
|
||||||
fetchHassioSupervisorInfo(this.hass),
|
fetchHassioSupervisorInfo(this.hass),
|
||||||
fetchHassioHostInfo(this.hass),
|
fetchHassioHostInfo(this.hass),
|
||||||
fetchHassioHomeAssistantInfo(this.hass),
|
fetchHassioHomeAssistantInfo(this.hass),
|
||||||
@ -57,6 +134,7 @@ export class SupervisorBaseElement extends urlSyncMixin(
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
this.supervisor = {
|
this.supervisor = {
|
||||||
|
addon,
|
||||||
supervisor,
|
supervisor,
|
||||||
host,
|
host,
|
||||||
core,
|
core,
|
||||||
@ -65,5 +143,9 @@ export class SupervisorBaseElement extends urlSyncMixin(
|
|||||||
network,
|
network,
|
||||||
resolution,
|
resolution,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.addEventListener("supervisor-update", (ev) =>
|
||||||
|
this._updateSupervisor(ev.detail)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
property,
|
property,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
|
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||||
import "../../../src/components/buttons/ha-progress-button";
|
import "../../../src/components/buttons/ha-progress-button";
|
||||||
import "../../../src/components/ha-button-menu";
|
import "../../../src/components/ha-button-menu";
|
||||||
import "../../../src/components/ha-card";
|
import "../../../src/components/ha-card";
|
||||||
@ -166,6 +167,7 @@ class HassioCoreInfo extends LitElement {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await updateCore(this.hass);
|
await updateCore(this.hass);
|
||||||
|
fireEvent(this, "supervisor-store-refresh", { store: "core" });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: "Failed to update Home Assistant Core",
|
title: "Failed to update Home Assistant Core",
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
|
import { atLeastVersion } from "../../../src/common/config/version";
|
||||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||||
import "../../../src/components/buttons/ha-progress-button";
|
import "../../../src/components/buttons/ha-progress-button";
|
||||||
import "../../../src/components/ha-button-menu";
|
import "../../../src/components/ha-button-menu";
|
||||||
@ -26,7 +27,6 @@ import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
|
|||||||
import {
|
import {
|
||||||
changeHostOptions,
|
changeHostOptions,
|
||||||
configSyncOS,
|
configSyncOS,
|
||||||
fetchHassioHostInfo,
|
|
||||||
rebootHost,
|
rebootHost,
|
||||||
shutdownHost,
|
shutdownHost,
|
||||||
updateOS,
|
updateOS,
|
||||||
@ -340,6 +340,7 @@ class HassioHostInfo extends LitElement {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await updateOS(this.hass);
|
await updateOS(this.hass);
|
||||||
|
fireEvent(this, "supervisor-store-refresh", { store: "os" });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: "Failed to update",
|
title: "Failed to update",
|
||||||
@ -368,8 +369,7 @@ class HassioHostInfo extends LitElement {
|
|||||||
if (hostname && hostname !== curHostname) {
|
if (hostname && hostname !== curHostname) {
|
||||||
try {
|
try {
|
||||||
await changeHostOptions(this.hass, { hostname });
|
await changeHostOptions(this.hass, { hostname });
|
||||||
const host = await fetchHassioHostInfo(this.hass);
|
fireEvent(this, "supervisor-store-refresh", { store: "host" });
|
||||||
fireEvent(this, "supervisor-update", { host });
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: "Setting hostname failed",
|
title: "Setting hostname failed",
|
||||||
@ -382,8 +382,7 @@ class HassioHostInfo extends LitElement {
|
|||||||
private async _importFromUSB(): Promise<void> {
|
private async _importFromUSB(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await configSyncOS(this.hass);
|
await configSyncOS(this.hass);
|
||||||
const host = await fetchHassioHostInfo(this.hass);
|
fireEvent(this, "supervisor-store-refresh", { store: "host" });
|
||||||
fireEvent(this, "supervisor-update", { host });
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: "Failed to import from USB",
|
title: "Failed to import from USB",
|
||||||
@ -393,8 +392,12 @@ class HassioHostInfo extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _loadData(): Promise<void> {
|
private async _loadData(): Promise<void> {
|
||||||
const network = await fetchNetworkInfo(this.hass);
|
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
|
||||||
fireEvent(this, "supervisor-update", { network });
|
fireEvent(this, "supervisor-store-refresh", { store: "network" });
|
||||||
|
} else {
|
||||||
|
const network = await fetchNetworkInfo(this.hass);
|
||||||
|
fireEvent(this, "supervisor-update", { network });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
|
@ -19,7 +19,6 @@ import {
|
|||||||
HassioStats,
|
HassioStats,
|
||||||
} from "../../../src/data/hassio/common";
|
} from "../../../src/data/hassio/common";
|
||||||
import {
|
import {
|
||||||
fetchHassioSupervisorInfo,
|
|
||||||
reloadSupervisor,
|
reloadSupervisor,
|
||||||
restartSupervisor,
|
restartSupervisor,
|
||||||
setSupervisorOption,
|
setSupervisorOption,
|
||||||
@ -318,8 +317,7 @@ class HassioSupervisorInfo extends LitElement {
|
|||||||
|
|
||||||
private async _reloadSupervisor(): Promise<void> {
|
private async _reloadSupervisor(): Promise<void> {
|
||||||
await reloadSupervisor(this.hass);
|
await reloadSupervisor(this.hass);
|
||||||
const supervisor = await fetchHassioSupervisorInfo(this.hass);
|
fireEvent(this, "supervisor-store-refresh", { store: "supervisor" });
|
||||||
fireEvent(this, "supervisor-update", { supervisor });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _supervisorRestart(ev: CustomEvent): Promise<void> {
|
private async _supervisorRestart(ev: CustomEvent): Promise<void> {
|
||||||
@ -368,6 +366,7 @@ class HassioSupervisorInfo extends LitElement {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await updateSupervisor(this.hass);
|
await updateSupervisor(this.hass);
|
||||||
|
fireEvent(this, "supervisor-store-refresh", { store: "supervisor" });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: "Failed to update the supervisor",
|
title: "Failed to update the supervisor",
|
||||||
|
@ -101,7 +101,7 @@
|
|||||||
"fuse.js": "^6.0.0",
|
"fuse.js": "^6.0.0",
|
||||||
"google-timezones-json": "^1.0.2",
|
"google-timezones-json": "^1.0.2",
|
||||||
"hls.js": "^0.13.2",
|
"hls.js": "^0.13.2",
|
||||||
"home-assistant-js-websocket": "^5.4.1",
|
"home-assistant-js-websocket": "^5.9.0",
|
||||||
"idb-keyval": "^3.2.0",
|
"idb-keyval": "^3.2.0",
|
||||||
"intl-messageformat": "^8.3.9",
|
"intl-messageformat": "^8.3.9",
|
||||||
"js-yaml": "^3.13.1",
|
"js-yaml": "^3.13.1",
|
||||||
@ -110,7 +110,7 @@
|
|||||||
"lit-element": "^2.4.0",
|
"lit-element": "^2.4.0",
|
||||||
"lit-html": "^1.3.0",
|
"lit-html": "^1.3.0",
|
||||||
"lit-virtualizer": "^0.4.2",
|
"lit-virtualizer": "^0.4.2",
|
||||||
"marked": "^1.1.1",
|
"marked": "2.0.0",
|
||||||
"mdn-polyfills": "^5.16.0",
|
"mdn-polyfills": "^5.16.0",
|
||||||
"memoize-one": "^5.0.2",
|
"memoize-one": "^5.0.2",
|
||||||
"node-vibrant": "3.2.1-alpha.1",
|
"node-vibrant": "3.2.1-alpha.1",
|
||||||
@ -161,7 +161,7 @@
|
|||||||
"@types/js-yaml": "^3.12.1",
|
"@types/js-yaml": "^3.12.1",
|
||||||
"@types/leaflet": "^1.4.3",
|
"@types/leaflet": "^1.4.3",
|
||||||
"@types/leaflet-draw": "^1.0.1",
|
"@types/leaflet-draw": "^1.0.1",
|
||||||
"@types/marked": "^1.1.0",
|
"@types/marked": "^1.2.2",
|
||||||
"@types/memoize-one": "4.1.0",
|
"@types/memoize-one": "4.1.0",
|
||||||
"@types/mocha": "^7.0.2",
|
"@types/mocha": "^7.0.2",
|
||||||
"@types/resize-observer-browser": "^0.1.3",
|
"@types/resize-observer-browser": "^0.1.3",
|
||||||
@ -222,7 +222,7 @@
|
|||||||
"webpack": "5.1.3",
|
"webpack": "5.1.3",
|
||||||
"webpack-cli": "4.1.0",
|
"webpack-cli": "4.1.0",
|
||||||
"webpack-dev-server": "^3.11.0",
|
"webpack-dev-server": "^3.11.0",
|
||||||
"webpack-manifest-plugin": "3.0.0-rc.0",
|
"webpack-manifest-plugin": "~3.0.0",
|
||||||
"workbox-build": "^5.1.3"
|
"workbox-build": "^5.1.3"
|
||||||
},
|
},
|
||||||
"_comment": "Polymer fixed to 3.1 because 3.2 throws on logbook page",
|
"_comment": "Polymer fixed to 3.1 because 3.2 throws on logbook page",
|
||||||
|
2
setup.py
2
setup.py
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="home-assistant-frontend",
|
name="home-assistant-frontend",
|
||||||
version="20210208.0",
|
version="20210222.0",
|
||||||
description="The Home Assistant frontend",
|
description="The Home Assistant frontend",
|
||||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||||
author="The Home Assistant Authors",
|
author="The Home Assistant Authors",
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
export const atLeastVersion = (
|
export const atLeastVersion = (
|
||||||
version: string,
|
version: string,
|
||||||
major: number,
|
major: number,
|
||||||
minor: number
|
minor: number,
|
||||||
|
patch?: number
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const [haMajor, haMinor] = version.split(".", 2);
|
const [haMajor, haMinor, haPatch] = version.split(".", 3);
|
||||||
return (
|
return (
|
||||||
Number(haMajor) > major ||
|
Number(haMajor) > major ||
|
||||||
(Number(haMajor) === major && Number(haMinor) >= minor)
|
(Number(haMajor) === major && Number(haMinor) >= minor) ||
|
||||||
|
(patch !== undefined &&
|
||||||
|
Number(haMajor) === major && Number(haMinor) === minor &&
|
||||||
|
Number(haPatch) >= patch)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,16 +1,12 @@
|
|||||||
import "@material/mwc-icon-button/mwc-icon-button";
|
|
||||||
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
|
|
||||||
import "@polymer/paper-input/paper-input";
|
|
||||||
import "@polymer/paper-item/paper-item";
|
import "@polymer/paper-item/paper-item";
|
||||||
import "@polymer/paper-item/paper-item-body";
|
import "@polymer/paper-item/paper-item-body";
|
||||||
import "@polymer/paper-listbox/paper-listbox";
|
|
||||||
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
|
|
||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResult,
|
CSSResult,
|
||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
|
internalProperty,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
@ -38,7 +34,7 @@ import {
|
|||||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||||
import { PolymerChangedEvent } from "../../polymer-types";
|
import { PolymerChangedEvent } from "../../polymer-types";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import "../ha-svg-icon";
|
import { HaComboBox } from "../ha-combo-box";
|
||||||
|
|
||||||
interface Device {
|
interface Device {
|
||||||
name: string;
|
name: string;
|
||||||
@ -112,10 +108,11 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean }) public disabled?: boolean;
|
||||||
private _opened?: boolean;
|
|
||||||
|
|
||||||
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
|
@internalProperty() private _opened?: boolean;
|
||||||
|
|
||||||
|
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
|
||||||
|
|
||||||
private _init = false;
|
private _init = false;
|
||||||
|
|
||||||
@ -244,15 +241,11 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
public open() {
|
public open() {
|
||||||
this.updateComplete.then(() => {
|
this._comboBox?.open();
|
||||||
(this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public focus() {
|
public focus() {
|
||||||
this.updateComplete.then(() => {
|
this._comboBox?.focus();
|
||||||
this.shadowRoot?.querySelector("paper-input")?.focus();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public hassSubscribe(): UnsubscribeFunc[] {
|
public hassSubscribe(): UnsubscribeFunc[] {
|
||||||
@ -292,70 +285,29 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<vaadin-combo-box-light
|
<ha-combo-box
|
||||||
|
.hass=${this.hass}
|
||||||
|
.label=${this.label === undefined && this.hass
|
||||||
|
? this.hass.localize("ui.components.device-picker.device")
|
||||||
|
: this.label}
|
||||||
|
.value=${this._value}
|
||||||
|
.renderer=${rowRenderer}
|
||||||
|
.disabled=${this.disabled}
|
||||||
item-value-path="id"
|
item-value-path="id"
|
||||||
item-id-path="id"
|
item-id-path="id"
|
||||||
item-label-path="name"
|
item-label-path="name"
|
||||||
.value=${this._value}
|
|
||||||
.renderer=${rowRenderer}
|
|
||||||
@opened-changed=${this._openedChanged}
|
@opened-changed=${this._openedChanged}
|
||||||
@value-changed=${this._deviceChanged}
|
@value-changed=${this._deviceChanged}
|
||||||
>
|
></ha-combo-box>
|
||||||
<paper-input
|
|
||||||
.label=${this.label === undefined && this.hass
|
|
||||||
? this.hass.localize("ui.components.device-picker.device")
|
|
||||||
: this.label}
|
|
||||||
class="input"
|
|
||||||
autocapitalize="none"
|
|
||||||
autocomplete="off"
|
|
||||||
autocorrect="off"
|
|
||||||
spellcheck="false"
|
|
||||||
>
|
|
||||||
${this.value
|
|
||||||
? html`
|
|
||||||
<mwc-icon-button
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.components.device-picker.clear"
|
|
||||||
)}
|
|
||||||
slot="suffix"
|
|
||||||
class="clear-button"
|
|
||||||
@click=${this._clearValue}
|
|
||||||
>
|
|
||||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
|
||||||
</mwc-icon-button>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
|
|
||||||
<mwc-icon-button
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.components.device-picker.show_devices"
|
|
||||||
)}
|
|
||||||
slot="suffix"
|
|
||||||
class="toggle-button"
|
|
||||||
>
|
|
||||||
<ha-svg-icon
|
|
||||||
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
|
|
||||||
></ha-svg-icon>
|
|
||||||
</mwc-icon-button>
|
|
||||||
</paper-input>
|
|
||||||
</vaadin-combo-box-light>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _clearValue(ev: Event) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
this._setValue("");
|
|
||||||
}
|
|
||||||
|
|
||||||
private get _value() {
|
private get _value() {
|
||||||
return this.value || "";
|
return this.value || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
|
|
||||||
this._opened = ev.detail.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _deviceChanged(ev: PolymerChangedEvent<string>) {
|
private _deviceChanged(ev: PolymerChangedEvent<string>) {
|
||||||
|
ev.stopPropagation();
|
||||||
const newValue = ev.detail.value;
|
const newValue = ev.detail.value;
|
||||||
|
|
||||||
if (newValue !== this._value) {
|
if (newValue !== this._value) {
|
||||||
@ -363,6 +315,10 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
|
||||||
|
this._opened = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
private _setValue(value: string) {
|
private _setValue(value: string) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -117,6 +117,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
|
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled?: boolean;
|
||||||
|
|
||||||
@internalProperty() private _areas?: AreaRegistryEntry[];
|
@internalProperty() private _areas?: AreaRegistryEntry[];
|
||||||
|
|
||||||
@internalProperty() private _devices?: DeviceRegistryEntry[];
|
@internalProperty() private _devices?: DeviceRegistryEntry[];
|
||||||
@ -339,6 +341,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
|||||||
item-label-path="name"
|
item-label-path="name"
|
||||||
.value=${this._value}
|
.value=${this._value}
|
||||||
.renderer=${rowRenderer}
|
.renderer=${rowRenderer}
|
||||||
|
.disabled=${this.disabled}
|
||||||
@opened-changed=${this._openedChanged}
|
@opened-changed=${this._openedChanged}
|
||||||
@value-changed=${this._areaChanged}
|
@value-changed=${this._areaChanged}
|
||||||
>
|
>
|
||||||
@ -349,6 +352,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
|||||||
.placeholder=${this.placeholder
|
.placeholder=${this.placeholder
|
||||||
? this._area(this.placeholder)?.name
|
? this._area(this.placeholder)?.name
|
||||||
: undefined}
|
: undefined}
|
||||||
|
.disabled=${this.disabled}
|
||||||
class="input"
|
class="input"
|
||||||
autocapitalize="none"
|
autocapitalize="none"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
@ -1,116 +0,0 @@
|
|||||||
import "@polymer/paper-input/paper-input";
|
|
||||||
import "@polymer/paper-item/paper-item";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
/* eslint-plugin-disable lit */
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
|
|
||||||
import { EventsMixin } from "../mixins/events-mixin";
|
|
||||||
import "./ha-icon-button";
|
|
||||||
|
|
||||||
class HaComboBox extends EventsMixin(PolymerElement) {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
paper-input > ha-icon-button {
|
|
||||||
--mdc-icon-button-size: 24px;
|
|
||||||
padding: 2px;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
[hidden] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<vaadin-combo-box-light
|
|
||||||
items="[[_items]]"
|
|
||||||
item-value-path="[[itemValuePath]]"
|
|
||||||
item-label-path="[[itemLabelPath]]"
|
|
||||||
value="{{value}}"
|
|
||||||
opened="{{opened}}"
|
|
||||||
allow-custom-value="[[allowCustomValue]]"
|
|
||||||
on-change="_fireChanged"
|
|
||||||
>
|
|
||||||
<paper-input
|
|
||||||
autofocus="[[autofocus]]"
|
|
||||||
label="[[label]]"
|
|
||||||
class="input"
|
|
||||||
value="[[value]]"
|
|
||||||
>
|
|
||||||
<ha-icon-button
|
|
||||||
slot="suffix"
|
|
||||||
class="clear-button"
|
|
||||||
icon="hass:close"
|
|
||||||
hidden$="[[!value]]"
|
|
||||||
>Clear</ha-icon-button
|
|
||||||
>
|
|
||||||
<ha-icon-button
|
|
||||||
slot="suffix"
|
|
||||||
class="toggle-button"
|
|
||||||
icon="[[_computeToggleIcon(opened)]]"
|
|
||||||
hidden$="[[!items.length]]"
|
|
||||||
>Toggle</ha-icon-button
|
|
||||||
>
|
|
||||||
</paper-input>
|
|
||||||
<template>
|
|
||||||
<style>
|
|
||||||
paper-item {
|
|
||||||
margin: -5px -10px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<paper-item>[[_computeItemLabel(item, itemLabelPath)]]</paper-item>
|
|
||||||
</template>
|
|
||||||
</vaadin-combo-box-light>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
allowCustomValue: Boolean,
|
|
||||||
items: {
|
|
||||||
type: Object,
|
|
||||||
observer: "_itemsChanged",
|
|
||||||
},
|
|
||||||
_items: Object,
|
|
||||||
itemLabelPath: String,
|
|
||||||
itemValuePath: String,
|
|
||||||
autofocus: Boolean,
|
|
||||||
label: String,
|
|
||||||
opened: {
|
|
||||||
type: Boolean,
|
|
||||||
value: false,
|
|
||||||
observer: "_openedChanged",
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
type: String,
|
|
||||||
notify: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_openedChanged(newVal) {
|
|
||||||
if (!newVal) {
|
|
||||||
this._items = this.items;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_itemsChanged(newVal) {
|
|
||||||
if (!this.opened) {
|
|
||||||
this._items = newVal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeToggleIcon(opened) {
|
|
||||||
return opened ? "hass:menu-up" : "hass:menu-down";
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeItemLabel(item, itemLabelPath) {
|
|
||||||
return itemLabelPath ? item[itemLabelPath] : item;
|
|
||||||
}
|
|
||||||
|
|
||||||
_fireChanged(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
this.fire("change");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("ha-combo-box", HaComboBox);
|
|
181
src/components/ha-combo-box.ts
Normal file
181
src/components/ha-combo-box.ts
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import "@material/mwc-icon-button/mwc-icon-button";
|
||||||
|
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
|
||||||
|
import "@polymer/paper-input/paper-input";
|
||||||
|
import "@polymer/paper-item/paper-item";
|
||||||
|
import "@polymer/paper-item/paper-item-body";
|
||||||
|
import "@polymer/paper-listbox/paper-listbox";
|
||||||
|
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
internalProperty,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
query,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit-element";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { PolymerChangedEvent } from "../polymer-types";
|
||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
import "./ha-svg-icon";
|
||||||
|
|
||||||
|
const defaultRowRenderer = (
|
||||||
|
root: HTMLElement,
|
||||||
|
_owner,
|
||||||
|
model: { item: any }
|
||||||
|
) => {
|
||||||
|
if (!root.firstElementChild) {
|
||||||
|
root.innerHTML = `
|
||||||
|
<style>
|
||||||
|
paper-item {
|
||||||
|
margin: -5px -10px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<paper-item></paper-item>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.querySelector("paper-item")!.textContent = model.item;
|
||||||
|
};
|
||||||
|
|
||||||
|
@customElement("ha-combo-box")
|
||||||
|
export class HaComboBox extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property() public value?: string;
|
||||||
|
|
||||||
|
@property() public items?: [];
|
||||||
|
|
||||||
|
@property() public filteredItems?: [];
|
||||||
|
|
||||||
|
@property({ attribute: "allow-custom-value", type: Boolean })
|
||||||
|
public allowCustomValue?: boolean;
|
||||||
|
|
||||||
|
@property({ attribute: "item-value-path" }) public itemValuePath?: string;
|
||||||
|
|
||||||
|
@property({ attribute: "item-label-path" }) public itemLabelPath?: string;
|
||||||
|
|
||||||
|
@property({ attribute: "item-id-path" }) public itemIdPath?: string;
|
||||||
|
|
||||||
|
@property() public renderer?: (
|
||||||
|
root: HTMLElement,
|
||||||
|
owner: HTMLElement,
|
||||||
|
model: { item: any }
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled?: boolean;
|
||||||
|
|
||||||
|
@internalProperty() private _opened?: boolean;
|
||||||
|
|
||||||
|
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
|
||||||
|
|
||||||
|
public open() {
|
||||||
|
this.updateComplete.then(() => {
|
||||||
|
(this._comboBox as any)?.open();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public focus() {
|
||||||
|
this.updateComplete.then(() => {
|
||||||
|
this.shadowRoot?.querySelector("paper-input")?.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<vaadin-combo-box-light
|
||||||
|
.itemValuePath=${this.itemValuePath}
|
||||||
|
.itemIdPath=${this.itemIdPath}
|
||||||
|
.itemLabelPath=${this.itemLabelPath}
|
||||||
|
.value=${this.value}
|
||||||
|
.items=${this.items}
|
||||||
|
.filteredItems=${this.filteredItems}
|
||||||
|
.renderer=${this.renderer || defaultRowRenderer}
|
||||||
|
.allowCustomValue=${this.allowCustomValue}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
@opened-changed=${this._openedChanged}
|
||||||
|
@filter-changed=${this._filterChanged}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
>
|
||||||
|
<paper-input
|
||||||
|
.label=${this.label}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
class="input"
|
||||||
|
autocapitalize="none"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
spellcheck="false"
|
||||||
|
>
|
||||||
|
${this.value
|
||||||
|
? html`
|
||||||
|
<mwc-icon-button
|
||||||
|
.label=${this.hass.localize("ui.components.combo-box.clear")}
|
||||||
|
slot="suffix"
|
||||||
|
class="clear-button"
|
||||||
|
@click=${this._clearValue}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||||
|
</mwc-icon-button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
|
||||||
|
<mwc-icon-button
|
||||||
|
.label=${this.hass.localize("ui.components.combo-box.show")}
|
||||||
|
slot="suffix"
|
||||||
|
class="toggle-button"
|
||||||
|
>
|
||||||
|
<ha-svg-icon
|
||||||
|
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</mwc-icon-button>
|
||||||
|
</paper-input>
|
||||||
|
</vaadin-combo-box-light>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clearValue(ev: Event) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
fireEvent(this, "value-changed", { value: undefined });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
|
||||||
|
this._opened = ev.detail.value;
|
||||||
|
// @ts-ignore
|
||||||
|
fireEvent(this, ev.type, ev.detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _filterChanged(ev: PolymerChangedEvent<boolean>) {
|
||||||
|
// @ts-ignore
|
||||||
|
fireEvent(this, ev.type, ev.detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueChanged(ev: PolymerChangedEvent<string>) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const newValue = ev.detail.value;
|
||||||
|
|
||||||
|
if (newValue !== this.value) {
|
||||||
|
fireEvent(this, "value-changed", { value: newValue });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return css`
|
||||||
|
paper-input > mwc-icon-button {
|
||||||
|
--mdc-icon-button-size: 24px;
|
||||||
|
padding: 2px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-combo-box": HaComboBox;
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
|
import { mdiEye, mdiEyeOff } from "@mdi/js";
|
||||||
import "@polymer/paper-input/paper-input";
|
import "@polymer/paper-input/paper-input";
|
||||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||||
import {
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
internalProperty,
|
internalProperty,
|
||||||
@ -10,12 +13,13 @@ import {
|
|||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import "../ha-icon-button";
|
import "../ha-svg-icon";
|
||||||
import type {
|
import type {
|
||||||
HaFormElement,
|
HaFormElement,
|
||||||
HaFormStringData,
|
HaFormStringData,
|
||||||
HaFormStringSchema,
|
HaFormStringSchema,
|
||||||
} from "./ha-form";
|
} from "./ha-form";
|
||||||
|
import "@material/mwc-icon-button/mwc-icon-button";
|
||||||
|
|
||||||
@customElement("ha-form-string")
|
@customElement("ha-form-string")
|
||||||
export class HaFormString extends LitElement implements HaFormElement {
|
export class HaFormString extends LitElement implements HaFormElement {
|
||||||
@ -48,16 +52,17 @@ export class HaFormString extends LitElement implements HaFormElement {
|
|||||||
.autoValidate=${this.schema.required}
|
.autoValidate=${this.schema.required}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
<ha-icon-button
|
<mwc-icon-button
|
||||||
toggles
|
toggles
|
||||||
slot="suffix"
|
slot="suffix"
|
||||||
.icon=${this._unmaskedPassword ? "hass:eye-off" : "hass:eye"}
|
|
||||||
id="iconButton"
|
id="iconButton"
|
||||||
title="Click to toggle between masked and clear password"
|
title="Click to toggle between masked and clear password"
|
||||||
@click=${this._toggleUnmaskedPassword}
|
@click=${this._toggleUnmaskedPassword}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
><ha-svg-icon
|
||||||
</ha-icon-button>
|
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</mwc-icon-button>
|
||||||
</paper-input>
|
</paper-input>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
@ -98,6 +103,15 @@ export class HaFormString extends LitElement implements HaFormElement {
|
|||||||
}
|
}
|
||||||
return "text";
|
return "text";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return css`
|
||||||
|
mwc-icon-button {
|
||||||
|
--mdc-icon-button-size: 24px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -202,9 +202,8 @@ export class HaForm extends LitElement implements HaFormElement {
|
|||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const schema = (ev.target as HaFormElement).schema as HaFormSchema;
|
const schema = (ev.target as HaFormElement).schema as HaFormSchema;
|
||||||
const data = this.data as HaFormDataContainer;
|
const data = this.data as HaFormDataContainer;
|
||||||
data[schema.name] = ev.detail.value;
|
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
value: { ...data },
|
value: { ...data, [schema.name]: ev.detail.value },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,8 +21,11 @@ export class HaActionSelector extends LitElement {
|
|||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`<ha-automation-action
|
return html`<ha-automation-action
|
||||||
|
.disabled=${this.disabled}
|
||||||
.actions=${this.value || []}
|
.actions=${this.value || []}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
></ha-automation-action>`;
|
></ha-automation-action>`;
|
||||||
@ -34,6 +37,10 @@ export class HaActionSelector extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
:host([disabled]) ha-automation-action {
|
||||||
|
opacity: var(--light-disabled-opacity);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,8 @@ export class HaAreaSelector extends LitElement {
|
|||||||
|
|
||||||
@internalProperty() public _configEntries?: ConfigEntry[];
|
@internalProperty() public _configEntries?: ConfigEntry[];
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
protected updated(changedProperties) {
|
protected updated(changedProperties) {
|
||||||
if (changedProperties.has("selector")) {
|
if (changedProperties.has("selector")) {
|
||||||
const oldSelector = changedProperties.get("selector");
|
const oldSelector = changedProperties.get("selector");
|
||||||
@ -50,6 +52,7 @@ export class HaAreaSelector extends LitElement {
|
|||||||
.includeDomains=${this.selector.area.entity?.domain
|
.includeDomains=${this.selector.area.entity?.domain
|
||||||
? [this.selector.area.entity.domain]
|
? [this.selector.area.entity.domain]
|
||||||
: undefined}
|
: undefined}
|
||||||
|
.disabled=${this.disabled}
|
||||||
></ha-area-picker>`;
|
></ha-area-picker>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,11 +19,14 @@ export class HaBooleanSelector extends LitElement {
|
|||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html` <ha-formfield alignEnd spaceBetween .label=${this.label}>
|
return html` <ha-formfield alignEnd spaceBetween .label=${this.label}>
|
||||||
<ha-switch
|
<ha-switch
|
||||||
.checked=${this.value}
|
.checked=${this.value}
|
||||||
@change=${this._handleChange}
|
@change=${this._handleChange}
|
||||||
|
.disabled=${this.disabled}
|
||||||
></ha-switch>
|
></ha-switch>
|
||||||
</ha-formfield>`;
|
</ha-formfield>`;
|
||||||
}
|
}
|
||||||
|
@ -23,10 +23,12 @@ export class HaDeviceSelector extends LitElement {
|
|||||||
|
|
||||||
@internalProperty() public _configEntries?: ConfigEntry[];
|
@internalProperty() public _configEntries?: ConfigEntry[];
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
protected updated(changedProperties) {
|
protected updated(changedProperties) {
|
||||||
if (changedProperties.has("selector")) {
|
if (changedProperties.has("selector")) {
|
||||||
const oldSelector = changedProperties.get("selector");
|
const oldSelector = changedProperties.get("selector");
|
||||||
if (oldSelector !== this.selector && this.selector.device.integration) {
|
if (oldSelector !== this.selector && this.selector.device?.integration) {
|
||||||
this._loadConfigEntries();
|
this._loadConfigEntries();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -44,24 +46,25 @@ export class HaDeviceSelector extends LitElement {
|
|||||||
.includeDomains=${this.selector.device.entity?.domain
|
.includeDomains=${this.selector.device.entity?.domain
|
||||||
? [this.selector.device.entity.domain]
|
? [this.selector.device.entity.domain]
|
||||||
: undefined}
|
: undefined}
|
||||||
|
.disabled=${this.disabled}
|
||||||
allow-custom-entity
|
allow-custom-entity
|
||||||
></ha-device-picker>`;
|
></ha-device-picker>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _filterDevices(device: DeviceRegistryEntry): boolean {
|
private _filterDevices(device: DeviceRegistryEntry): boolean {
|
||||||
if (
|
if (
|
||||||
this.selector.device.manufacturer &&
|
this.selector.device?.manufacturer &&
|
||||||
device.manufacturer !== this.selector.device.manufacturer
|
device.manufacturer !== this.selector.device.manufacturer
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
this.selector.device.model &&
|
this.selector.device?.model &&
|
||||||
device.model !== this.selector.device.model
|
device.model !== this.selector.device.model
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (this.selector.device.integration) {
|
if (this.selector.device?.integration) {
|
||||||
if (
|
if (
|
||||||
this._configEntries &&
|
this._configEntries &&
|
||||||
!this._configEntries.some((entry) =>
|
!this._configEntries.some((entry) =>
|
||||||
|
@ -25,12 +25,15 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`<ha-entity-picker
|
return html`<ha-entity-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.entityFilter=${(entity) => this._filterEntities(entity)}
|
.entityFilter=${(entity) => this._filterEntities(entity)}
|
||||||
|
.disabled=${this.disabled}
|
||||||
allow-custom-entity
|
allow-custom-entity
|
||||||
></ha-entity-picker>`;
|
></ha-entity-picker>`;
|
||||||
}
|
}
|
||||||
@ -51,12 +54,12 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _filterEntities(entity: HassEntity): boolean {
|
private _filterEntities(entity: HassEntity): boolean {
|
||||||
if (this.selector.entity.domain) {
|
if (this.selector.entity?.domain) {
|
||||||
if (computeStateDomain(entity) !== this.selector.entity.domain) {
|
if (computeStateDomain(entity) !== this.selector.entity.domain) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.selector.entity.device_class) {
|
if (this.selector.entity?.device_class) {
|
||||||
if (
|
if (
|
||||||
!entity.attributes.device_class ||
|
!entity.attributes.device_class ||
|
||||||
entity.attributes.device_class !== this.selector.entity.device_class
|
entity.attributes.device_class !== this.selector.entity.device_class
|
||||||
@ -64,7 +67,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.selector.entity.integration) {
|
if (this.selector.entity?.integration) {
|
||||||
if (
|
if (
|
||||||
!this._entityPlaformLookup ||
|
!this._entityPlaformLookup ||
|
||||||
this._entityPlaformLookup[entity.entity_id] !==
|
this._entityPlaformLookup[entity.entity_id] !==
|
||||||
|
@ -21,8 +21,12 @@ export class HaNumberSelector extends LitElement {
|
|||||||
|
|
||||||
@property() public value?: number;
|
@property() public value?: number;
|
||||||
|
|
||||||
|
@property() public placeholder?: number;
|
||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`${this.label}
|
return html`${this.label}
|
||||||
${this.selector.number.mode === "slider"
|
${this.selector.number.mode === "slider"
|
||||||
@ -31,6 +35,7 @@ export class HaNumberSelector extends LitElement {
|
|||||||
.max=${this.selector.number.max}
|
.max=${this.selector.number.max}
|
||||||
.value=${this._value}
|
.value=${this._value}
|
||||||
.step=${this.selector.number.step}
|
.step=${this.selector.number.step}
|
||||||
|
.disabled=${this.disabled}
|
||||||
pin
|
pin
|
||||||
ignore-bar-touch
|
ignore-bar-touch
|
||||||
@change=${this._handleSliderChange}
|
@change=${this._handleSliderChange}
|
||||||
@ -42,12 +47,14 @@ export class HaNumberSelector extends LitElement {
|
|||||||
.label=${this.selector.number.mode === "slider"
|
.label=${this.selector.number.mode === "slider"
|
||||||
? undefined
|
? undefined
|
||||||
: this.label}
|
: this.label}
|
||||||
|
.placeholder=${this.placeholder}
|
||||||
.noLabelFloat=${this.selector.number.mode === "slider"}
|
.noLabelFloat=${this.selector.number.mode === "slider"}
|
||||||
class=${classMap({ single: this.selector.number.mode === "box" })}
|
class=${classMap({ single: this.selector.number.mode === "box" })}
|
||||||
.min=${this.selector.number.min}
|
.min=${this.selector.number.min}
|
||||||
.max=${this.selector.number.max}
|
.max=${this.selector.number.max}
|
||||||
.value=${this._value}
|
.value=${this.value}
|
||||||
.step=${this.selector.number.step}
|
.step=${this.selector.number.step}
|
||||||
|
.disabled=${this.disabled}
|
||||||
type="number"
|
type="number"
|
||||||
auto-validate
|
auto-validate
|
||||||
@value-changed=${this._handleInputChange}
|
@value-changed=${this._handleInputChange}
|
||||||
@ -65,16 +72,21 @@ export class HaNumberSelector extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _handleInputChange(ev) {
|
private _handleInputChange(ev) {
|
||||||
const value = ev.detail.value;
|
ev.stopPropagation();
|
||||||
if (this._value === value) {
|
const value =
|
||||||
|
ev.detail.value === "" || isNaN(ev.detail.value)
|
||||||
|
? undefined
|
||||||
|
: Number(ev.detail.value);
|
||||||
|
if (this.value === value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fireEvent(this, "value-changed", { value });
|
fireEvent(this, "value-changed", { value });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleSliderChange(ev) {
|
private _handleSliderChange(ev) {
|
||||||
const value = ev.target.value;
|
ev.stopPropagation();
|
||||||
if (this._value === value) {
|
const value = Number(ev.target.value);
|
||||||
|
if (this.value === value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fireEvent(this, "value-changed", { value });
|
fireEvent(this, "value-changed", { value });
|
||||||
|
@ -11,8 +11,14 @@ export class HaObjectSelector extends LitElement {
|
|||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property() public placeholder?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`<ha-yaml-editor
|
return html`<ha-yaml-editor
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.placeholder=${this.placeholder}
|
||||||
.defaultValue=${this.value}
|
.defaultValue=${this.value}
|
||||||
@value-changed=${this._handleChange}
|
@value-changed=${this._handleChange}
|
||||||
></ha-yaml-editor>`;
|
></ha-yaml-editor>`;
|
||||||
|
78
src/components/ha-selector/ha-selector-select.ts
Normal file
78
src/components/ha-selector/ha-selector-select.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
} from "lit-element";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
import { SelectSelector } from "../../data/selector";
|
||||||
|
import "../ha-paper-dropdown-menu";
|
||||||
|
|
||||||
|
@customElement("ha-selector-select")
|
||||||
|
export class HaSelectSelector extends LitElement {
|
||||||
|
@property() public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public selector!: SelectSelector;
|
||||||
|
|
||||||
|
@property() public value?: string;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`<ha-paper-dropdown-menu
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.label=${this.label}
|
||||||
|
>
|
||||||
|
<paper-listbox
|
||||||
|
slot="dropdown-content"
|
||||||
|
attr-for-selected="item-value"
|
||||||
|
.selected=${this.value}
|
||||||
|
@selected-item-changed=${this._valueChanged}
|
||||||
|
>
|
||||||
|
${this.selector.select.options.map(
|
||||||
|
(item: string) => html`
|
||||||
|
<paper-item .itemValue=${item}>
|
||||||
|
${item}
|
||||||
|
</paper-item>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</paper-listbox>
|
||||||
|
</ha-paper-dropdown-menu>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueChanged(ev) {
|
||||||
|
if (this.disabled || !ev.detail.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: ev.detail.value.itemValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return css`
|
||||||
|
ha-paper-dropdown-menu {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 200px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
paper-listbox {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
paper-item {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-selector-select": HaSelectSelector;
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,11 @@ import "@material/mwc-list/mwc-list-item";
|
|||||||
import "@material/mwc-tab-bar/mwc-tab-bar";
|
import "@material/mwc-tab-bar/mwc-tab-bar";
|
||||||
import "@material/mwc-tab/mwc-tab";
|
import "@material/mwc-tab/mwc-tab";
|
||||||
import "@polymer/paper-input/paper-input";
|
import "@polymer/paper-input/paper-input";
|
||||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
import {
|
||||||
|
HassEntity,
|
||||||
|
HassServiceTarget,
|
||||||
|
UnsubscribeFunc,
|
||||||
|
} from "home-assistant-js-websocket";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResult,
|
CSSResult,
|
||||||
@ -20,7 +24,6 @@ import {
|
|||||||
subscribeEntityRegistry,
|
subscribeEntityRegistry,
|
||||||
} from "../../data/entity_registry";
|
} from "../../data/entity_registry";
|
||||||
import { TargetSelector } from "../../data/selector";
|
import { TargetSelector } from "../../data/selector";
|
||||||
import { Target } from "../../data/target";
|
|
||||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import "../ha-target-picker";
|
import "../ha-target-picker";
|
||||||
@ -31,7 +34,7 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@property() public selector!: TargetSelector;
|
@property() public selector!: TargetSelector;
|
||||||
|
|
||||||
@property() public value?: Target;
|
@property() public value?: HassServiceTarget;
|
||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
@ -39,6 +42,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@internalProperty() private _configEntries?: ConfigEntry[];
|
@internalProperty() private _configEntries?: ConfigEntry[];
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
public hassSubscribe(): UnsubscribeFunc[] {
|
public hassSubscribe(): UnsubscribeFunc[] {
|
||||||
return [
|
return [
|
||||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||||
@ -59,7 +64,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
|||||||
const oldSelector = changedProperties.get("selector");
|
const oldSelector = changedProperties.get("selector");
|
||||||
if (
|
if (
|
||||||
oldSelector !== this.selector &&
|
oldSelector !== this.selector &&
|
||||||
this.selector.target.device?.integration
|
(this.selector.target.device?.integration ||
|
||||||
|
this.selector.target.entity?.integration)
|
||||||
) {
|
) {
|
||||||
this._loadConfigEntries();
|
this._loadConfigEntries();
|
||||||
}
|
}
|
||||||
@ -80,15 +86,20 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
|||||||
.includeDomains=${this.selector.target.entity?.domain
|
.includeDomains=${this.selector.target.entity?.domain
|
||||||
? [this.selector.target.entity.domain]
|
? [this.selector.target.entity.domain]
|
||||||
: undefined}
|
: undefined}
|
||||||
|
.disabled=${this.disabled}
|
||||||
></ha-target-picker>`;
|
></ha-target-picker>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _filterEntities(entity: HassEntity): boolean {
|
private _filterEntities(entity: HassEntity): boolean {
|
||||||
if (this.selector.target.entity?.integration) {
|
if (
|
||||||
|
this.selector.target.entity?.integration ||
|
||||||
|
this.selector.target.device?.integration
|
||||||
|
) {
|
||||||
if (
|
if (
|
||||||
!this._entityPlaformLookup ||
|
!this._entityPlaformLookup ||
|
||||||
this._entityPlaformLookup[entity.entity_id] !==
|
this._entityPlaformLookup[entity.entity_id] !==
|
||||||
this.selector.target.entity.integration
|
(this.selector.target.entity?.integration ||
|
||||||
|
this.selector.target.device?.integration)
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -118,7 +129,10 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
|||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (this.selector.target.device?.integration) {
|
if (
|
||||||
|
this.selector.target.device?.integration ||
|
||||||
|
this.selector.target.entity?.integration
|
||||||
|
) {
|
||||||
if (
|
if (
|
||||||
!this._configEntries?.some((entry) =>
|
!this._configEntries?.some((entry) =>
|
||||||
device.config_entries.includes(entry.entry_id)
|
device.config_entries.includes(entry.entry_id)
|
||||||
@ -132,14 +146,16 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
private async _loadConfigEntries() {
|
private async _loadConfigEntries() {
|
||||||
this._configEntries = (await getConfigEntries(this.hass)).filter(
|
this._configEntries = (await getConfigEntries(this.hass)).filter(
|
||||||
(entry) => entry.domain === this.selector.target.device?.integration
|
(entry) =>
|
||||||
|
entry.domain ===
|
||||||
|
(this.selector.target.device?.integration ||
|
||||||
|
this.selector.target.entity?.integration)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResult {
|
static get styles(): CSSResult {
|
||||||
return css`
|
return css`
|
||||||
ha-target-picker {
|
ha-target-picker {
|
||||||
margin: 0 -8px;
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -13,14 +13,20 @@ export class HaTextSelector extends LitElement {
|
|||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property() public placeholder?: string;
|
||||||
|
|
||||||
@property() public selector!: StringSelector;
|
@property() public selector!: StringSelector;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (this.selector.text?.multiline) {
|
if (this.selector.text?.multiline) {
|
||||||
return html`<paper-textarea
|
return html`<paper-textarea
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.value="${this.value}"
|
.placeholder=${this.placeholder}
|
||||||
@value-changed="${this._handleChange}"
|
.value=${this.value}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
@value-changed=${this._handleChange}
|
||||||
autocapitalize="none"
|
autocapitalize="none"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
@ -29,6 +35,8 @@ export class HaTextSelector extends LitElement {
|
|||||||
return html`<paper-input
|
return html`<paper-input
|
||||||
required
|
required
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
|
.placeholder=${this.placeholder}
|
||||||
|
.disabled=${this.disabled}
|
||||||
@value-changed=${this._handleChange}
|
@value-changed=${this._handleChange}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
></paper-input>`;
|
></paper-input>`;
|
||||||
|
@ -17,6 +17,8 @@ export class HaTimeSelector extends LitElement {
|
|||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const parts = this.value?.split(":") || [];
|
const parts = this.value?.split(":") || [];
|
||||||
const hours = useAMPM ? parts[0] ?? "12" : parts[0] ?? "0";
|
const hours = useAMPM ? parts[0] ?? "12" : parts[0] ?? "0";
|
||||||
@ -29,6 +31,7 @@ export class HaTimeSelector extends LitElement {
|
|||||||
.sec=${parts[2] ?? "00"}
|
.sec=${parts[2] ?? "00"}
|
||||||
.format=${useAMPM ? 12 : 24}
|
.format=${useAMPM ? 12 : 24}
|
||||||
.amPm=${useAMPM && (Number(hours) > 12 ? "PM" : "AM")}
|
.amPm=${useAMPM && (Number(hours) > 12 ? "PM" : "AM")}
|
||||||
|
.disabled=${this.disabled}
|
||||||
@change=${this._timeChanged}
|
@change=${this._timeChanged}
|
||||||
@am-pm-changed=${this._timeChanged}
|
@am-pm-changed=${this._timeChanged}
|
||||||
hide-label
|
hide-label
|
||||||
|
@ -12,6 +12,7 @@ import "./ha-selector-target";
|
|||||||
import "./ha-selector-time";
|
import "./ha-selector-time";
|
||||||
import "./ha-selector-object";
|
import "./ha-selector-object";
|
||||||
import "./ha-selector-text";
|
import "./ha-selector-text";
|
||||||
|
import "./ha-selector-select";
|
||||||
|
|
||||||
@customElement("ha-selector")
|
@customElement("ha-selector")
|
||||||
export class HaSelector extends LitElement {
|
export class HaSelector extends LitElement {
|
||||||
@ -23,6 +24,10 @@ export class HaSelector extends LitElement {
|
|||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property() public placeholder?: any;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
public focus() {
|
public focus() {
|
||||||
const input = this.shadowRoot!.getElementById("selector");
|
const input = this.shadowRoot!.getElementById("selector");
|
||||||
if (!input) {
|
if (!input) {
|
||||||
@ -42,6 +47,8 @@ export class HaSelector extends LitElement {
|
|||||||
selector: this.selector,
|
selector: this.selector,
|
||||||
value: this.value,
|
value: this.value,
|
||||||
label: this.label,
|
label: this.label,
|
||||||
|
placeholder: this.placeholder,
|
||||||
|
disabled: this.disabled,
|
||||||
id: "selector",
|
id: "selector",
|
||||||
})}
|
})}
|
||||||
`;
|
`;
|
||||||
|
407
src/components/ha-service-control.ts
Normal file
407
src/components/ha-service-control.ts
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
import { HassService, HassServiceTarget } from "home-assistant-js-websocket";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
internalProperty,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
PropertyValues,
|
||||||
|
query,
|
||||||
|
} from "lit-element";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
|
import { computeObjectId } from "../common/entity/compute_object_id";
|
||||||
|
import { ENTITY_COMPONENT_DOMAINS } from "../data/entity";
|
||||||
|
import { Selector } from "../data/selector";
|
||||||
|
import { PolymerChangedEvent } from "../polymer-types";
|
||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
import "./ha-selector/ha-selector";
|
||||||
|
import "./ha-service-picker";
|
||||||
|
import "./ha-settings-row";
|
||||||
|
import "./ha-yaml-editor";
|
||||||
|
import "./ha-checkbox";
|
||||||
|
import type { HaYamlEditor } from "./ha-yaml-editor";
|
||||||
|
|
||||||
|
interface ExtHassService extends Omit<HassService, "fields"> {
|
||||||
|
fields: {
|
||||||
|
key: string;
|
||||||
|
name?: string;
|
||||||
|
description: string;
|
||||||
|
required?: boolean;
|
||||||
|
advanced?: boolean;
|
||||||
|
default?: any;
|
||||||
|
example?: any;
|
||||||
|
selector?: Selector;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("ha-service-control")
|
||||||
|
export class HaServiceControl extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public value?: {
|
||||||
|
service: string;
|
||||||
|
target?: HassServiceTarget;
|
||||||
|
data?: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
@property({ reflect: true, type: Boolean }) public narrow!: boolean;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public showAdvanced?: boolean;
|
||||||
|
|
||||||
|
@internalProperty() private _serviceData?: ExtHassService;
|
||||||
|
|
||||||
|
@internalProperty() private _checkedKeys = new Set();
|
||||||
|
|
||||||
|
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
|
||||||
|
|
||||||
|
protected updated(changedProperties: PropertyValues) {
|
||||||
|
if (!changedProperties.has("value")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const oldValue = changedProperties.get("value") as
|
||||||
|
| undefined
|
||||||
|
| this["value"];
|
||||||
|
|
||||||
|
if (oldValue?.service !== this.value?.service) {
|
||||||
|
this._checkedKeys = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._serviceData = this.value?.service
|
||||||
|
? this._getServiceInfo(this.value.service)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
this._serviceData &&
|
||||||
|
"target" in this._serviceData &&
|
||||||
|
(this.value?.data?.entity_id ||
|
||||||
|
this.value?.data?.area_id ||
|
||||||
|
this.value?.data?.device_id)
|
||||||
|
) {
|
||||||
|
const target = {
|
||||||
|
...this.value.target,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.value.data.entity_id && !this.value.target?.entity_id) {
|
||||||
|
target.entity_id = this.value.data.entity_id;
|
||||||
|
}
|
||||||
|
if (this.value.data.area_id && !this.value.target?.area_id) {
|
||||||
|
target.area_id = this.value.data.area_id;
|
||||||
|
}
|
||||||
|
if (this.value.data.device_id && !this.value.target?.device_id) {
|
||||||
|
target.device_id = this.value.data.device_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value = {
|
||||||
|
...this.value,
|
||||||
|
target,
|
||||||
|
data: { ...this.value.data },
|
||||||
|
};
|
||||||
|
|
||||||
|
delete this.value.data!.entity_id;
|
||||||
|
delete this.value.data!.device_id;
|
||||||
|
delete this.value.data!.area_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.value?.data) {
|
||||||
|
const yamlEditor = this._yamlEditor;
|
||||||
|
if (yamlEditor && yamlEditor.value !== this.value.data) {
|
||||||
|
yamlEditor.setValue(this.value.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _domainFilter = memoizeOne((service: string) => {
|
||||||
|
const domain = computeDomain(service);
|
||||||
|
return ENTITY_COMPONENT_DOMAINS.includes(domain) ? [domain] : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
private _getServiceInfo = memoizeOne((service: string):
|
||||||
|
| ExtHassService
|
||||||
|
| undefined => {
|
||||||
|
if (!service) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const domain = computeDomain(service);
|
||||||
|
const serviceName = computeObjectId(service);
|
||||||
|
const serviceDomains = this.hass.services;
|
||||||
|
if (!(domain in serviceDomains)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!(serviceName in serviceDomains[domain])) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = Object.entries(
|
||||||
|
serviceDomains[domain][serviceName].fields
|
||||||
|
).map(([key, value]) => {
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
...value,
|
||||||
|
selector: value.selector as Selector | undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...serviceDomains[domain][serviceName],
|
||||||
|
fields,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
const legacy =
|
||||||
|
this._serviceData?.fields.length &&
|
||||||
|
!this._serviceData.fields.some((field) => field.selector);
|
||||||
|
|
||||||
|
const entityId =
|
||||||
|
legacy &&
|
||||||
|
this._serviceData?.fields.find((field) => field.key === "entity_id");
|
||||||
|
|
||||||
|
const hasOptional = Boolean(
|
||||||
|
!legacy &&
|
||||||
|
this._serviceData?.fields.some(
|
||||||
|
(field) => field.selector && !field.required
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return html`<ha-service-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${this.value?.service}
|
||||||
|
@value-changed=${this._serviceChanged}
|
||||||
|
></ha-service-picker>
|
||||||
|
<p>${this._serviceData?.description}</p>
|
||||||
|
${this._serviceData && "target" in this._serviceData
|
||||||
|
? html`<ha-settings-row .narrow=${this.narrow}>
|
||||||
|
${hasOptional
|
||||||
|
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
||||||
|
: ""}
|
||||||
|
<span slot="heading"
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.components.service-control.target"
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
<span slot="description"
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.components.service-control.target_description"
|
||||||
|
)}</span
|
||||||
|
><ha-selector
|
||||||
|
.hass=${this.hass}
|
||||||
|
.selector=${this._serviceData.target
|
||||||
|
? { target: this._serviceData.target }
|
||||||
|
: {
|
||||||
|
target: {
|
||||||
|
entity: { domain: computeDomain(this.value!.service) },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
@value-changed=${this._targetChanged}
|
||||||
|
.value=${this.value?.target}
|
||||||
|
></ha-selector
|
||||||
|
></ha-settings-row>`
|
||||||
|
: entityId
|
||||||
|
? html`<ha-entity-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${this.value?.data?.entity_id}
|
||||||
|
.label=${entityId.description}
|
||||||
|
.includeDomains=${this._domainFilter(this.value!.service)}
|
||||||
|
@value-changed=${this._entityPicked}
|
||||||
|
allow-custom-entity
|
||||||
|
></ha-entity-picker>`
|
||||||
|
: ""}
|
||||||
|
${legacy
|
||||||
|
? html`<ha-yaml-editor
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.components.service-control.service_data"
|
||||||
|
)}
|
||||||
|
.name=${"data"}
|
||||||
|
.defaultValue=${this.value?.data}
|
||||||
|
@value-changed=${this._dataChanged}
|
||||||
|
></ha-yaml-editor>`
|
||||||
|
: this._serviceData?.fields.map((dataField) =>
|
||||||
|
dataField.selector && (!dataField.advanced || this.showAdvanced)
|
||||||
|
? html`<ha-settings-row .narrow=${this.narrow}>
|
||||||
|
${dataField.required
|
||||||
|
? hasOptional
|
||||||
|
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
||||||
|
: ""
|
||||||
|
: html`<ha-checkbox
|
||||||
|
.key=${dataField.key}
|
||||||
|
.checked=${this._checkedKeys.has(dataField.key) ||
|
||||||
|
(this.value?.data &&
|
||||||
|
this.value.data[dataField.key] !== undefined)}
|
||||||
|
@change=${this._checkboxChanged}
|
||||||
|
slot="prefix"
|
||||||
|
></ha-checkbox>`}
|
||||||
|
<span slot="heading">${dataField.name || dataField.key}</span>
|
||||||
|
<span slot="description">${dataField?.description}</span
|
||||||
|
><ha-selector
|
||||||
|
.disabled=${!dataField.required &&
|
||||||
|
!this._checkedKeys.has(dataField.key) &&
|
||||||
|
(!this.value?.data ||
|
||||||
|
this.value.data[dataField.key] === undefined)}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.selector=${dataField.selector}
|
||||||
|
.key=${dataField.key}
|
||||||
|
@value-changed=${this._serviceDataChanged}
|
||||||
|
.value=${this.value?.data &&
|
||||||
|
this.value.data[dataField.key] !== undefined
|
||||||
|
? this.value.data[dataField.key]
|
||||||
|
: dataField.default}
|
||||||
|
></ha-selector
|
||||||
|
></ha-settings-row>`
|
||||||
|
: ""
|
||||||
|
)} `;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _checkboxChanged(ev) {
|
||||||
|
const checked = ev.currentTarget.checked;
|
||||||
|
const key = ev.currentTarget.key;
|
||||||
|
if (checked) {
|
||||||
|
this._checkedKeys.add(key);
|
||||||
|
} else {
|
||||||
|
this._checkedKeys.delete(key);
|
||||||
|
const data = { ...this.value?.data };
|
||||||
|
|
||||||
|
delete data[key];
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: {
|
||||||
|
...this.value,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.requestUpdate("_checkedKeys");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _serviceChanged(ev: PolymerChangedEvent<string>) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
if (ev.detail.value === this.value?.service) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: { service: ev.detail.value || "" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _entityPicked(ev: CustomEvent) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const newValue = ev.detail.value;
|
||||||
|
if (this.value?.data?.entity_id === newValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let value;
|
||||||
|
if (!newValue && this.value?.data) {
|
||||||
|
value = { ...this.value };
|
||||||
|
delete value.data.entity_id;
|
||||||
|
} else {
|
||||||
|
value = {
|
||||||
|
...this.value,
|
||||||
|
data: { ...this.value?.data, entity_id: ev.detail.value },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _targetChanged(ev: CustomEvent) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const newValue = ev.detail.value;
|
||||||
|
if (this.value?.target === newValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let value;
|
||||||
|
if (!newValue) {
|
||||||
|
value = { ...this.value };
|
||||||
|
delete value.target;
|
||||||
|
} else {
|
||||||
|
value = { ...this.value, target: ev.detail.value };
|
||||||
|
}
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _serviceDataChanged(ev: CustomEvent) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const key = (ev.currentTarget as any).key;
|
||||||
|
const value = ev.detail.value;
|
||||||
|
if (this.value?.data && this.value.data[key] === value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = { ...this.value?.data, [key]: value };
|
||||||
|
|
||||||
|
if (value === "" || value === undefined) {
|
||||||
|
delete data[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: {
|
||||||
|
...this.value,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _dataChanged(ev: CustomEvent) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
if (!ev.detail.isValid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: {
|
||||||
|
...this.value,
|
||||||
|
data: ev.detail.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return css`
|
||||||
|
ha-settings-row {
|
||||||
|
padding: var(--service-control-padding, 0 16px);
|
||||||
|
}
|
||||||
|
ha-settings-row {
|
||||||
|
--paper-time-input-justify-content: flex-end;
|
||||||
|
border-top: var(
|
||||||
|
--service-control-items-border-top,
|
||||||
|
1px solid var(--divider-color)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ha-service-picker,
|
||||||
|
ha-entity-picker,
|
||||||
|
ha-yaml-editor {
|
||||||
|
display: block;
|
||||||
|
margin: var(--service-control-padding, 0 16px);
|
||||||
|
}
|
||||||
|
ha-yaml-editor {
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: var(--service-control-padding, 0 16px);
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
:host(:not([narrow])) ha-settings-row paper-input {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
:host(:not([narrow])) ha-settings-row ha-selector {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
.checkbox-spacer {
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
ha-checkbox {
|
||||||
|
margin-left: -16px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-service-control": HaServiceControl;
|
||||||
|
}
|
||||||
|
}
|
@ -1,60 +0,0 @@
|
|||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
/* eslint-plugin-disable lit */
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
import LocalizeMixin from "../mixins/localize-mixin";
|
|
||||||
import "./ha-combo-box";
|
|
||||||
|
|
||||||
/*
|
|
||||||
* @appliesMixin LocalizeMixin
|
|
||||||
*/
|
|
||||||
class HaServicePicker extends LocalizeMixin(PolymerElement) {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<ha-combo-box
|
|
||||||
label="[[localize('ui.components.service-picker.service')]]"
|
|
||||||
items="[[_services]]"
|
|
||||||
value="{{value}}"
|
|
||||||
allow-custom-value=""
|
|
||||||
></ha-combo-box>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: {
|
|
||||||
type: Object,
|
|
||||||
observer: "_hassChanged",
|
|
||||||
},
|
|
||||||
_services: Array,
|
|
||||||
value: {
|
|
||||||
type: String,
|
|
||||||
notify: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_hassChanged(hass, oldHass) {
|
|
||||||
if (!hass) {
|
|
||||||
this._services = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (oldHass && hass.services === oldHass.services) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const result = [];
|
|
||||||
|
|
||||||
Object.keys(hass.services)
|
|
||||||
.sort()
|
|
||||||
.forEach((domain) => {
|
|
||||||
const services = Object.keys(hass.services[domain]).sort();
|
|
||||||
|
|
||||||
for (let i = 0; i < services.length; i++) {
|
|
||||||
result.push(`${domain}.${services[i]}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this._services = result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("ha-service-picker", HaServicePicker);
|
|
135
src/components/ha-service-picker.ts
Normal file
135
src/components/ha-service-picker.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import { html, internalProperty, LitElement, property } from "lit-element";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { LocalizeFunc } from "../common/translations/localize";
|
||||||
|
import { domainToName } from "../data/integration";
|
||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
import "./ha-combo-box";
|
||||||
|
|
||||||
|
const rowRenderer = (
|
||||||
|
root: HTMLElement,
|
||||||
|
_owner,
|
||||||
|
model: { item: { service: string; name: string } }
|
||||||
|
) => {
|
||||||
|
if (!root.firstElementChild) {
|
||||||
|
root.innerHTML = `
|
||||||
|
<style>
|
||||||
|
paper-item {
|
||||||
|
margin: -10px 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<paper-item>
|
||||||
|
<paper-item-body two-line="">
|
||||||
|
<div class='name'>[[item.name]]</div>
|
||||||
|
<div secondary>[[item.service]]</div>
|
||||||
|
</paper-item-body>
|
||||||
|
</paper-item>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.querySelector(".name")!.textContent = model.item.name;
|
||||||
|
root.querySelector("[secondary]")!.textContent =
|
||||||
|
model.item.name === model.item.service ? "" : model.item.service;
|
||||||
|
};
|
||||||
|
|
||||||
|
class HaServicePicker extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public value?: string;
|
||||||
|
|
||||||
|
@internalProperty() private _filter?: string;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<ha-combo-box
|
||||||
|
.hass=${this.hass}
|
||||||
|
.label=${this.hass.localize("ui.components.service-picker.service")}
|
||||||
|
.filteredItems=${this._filteredServices(
|
||||||
|
this.hass.localize,
|
||||||
|
this.hass.services,
|
||||||
|
this._filter
|
||||||
|
)}
|
||||||
|
.value=${this.value}
|
||||||
|
.renderer=${rowRenderer}
|
||||||
|
item-value-path="service"
|
||||||
|
item-label-path="name"
|
||||||
|
allow-custom-value
|
||||||
|
@filter-changed=${this._filterChanged}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
></ha-combo-box>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _services = memoizeOne(
|
||||||
|
(
|
||||||
|
localize: LocalizeFunc,
|
||||||
|
services: HomeAssistant["services"]
|
||||||
|
): {
|
||||||
|
service: string;
|
||||||
|
name: string;
|
||||||
|
}[] => {
|
||||||
|
if (!services) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const result: { service: string; name: string }[] = [];
|
||||||
|
|
||||||
|
Object.keys(services)
|
||||||
|
.sort()
|
||||||
|
.forEach((domain) => {
|
||||||
|
const services_keys = Object.keys(services[domain]).sort();
|
||||||
|
|
||||||
|
for (const service of services_keys) {
|
||||||
|
result.push({
|
||||||
|
service: `${domain}.${service}`,
|
||||||
|
name: `${domainToName(localize, domain)}: ${
|
||||||
|
services[domain][service].name || service
|
||||||
|
}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private _filteredServices = memoizeOne(
|
||||||
|
(
|
||||||
|
localize: LocalizeFunc,
|
||||||
|
services: HomeAssistant["services"],
|
||||||
|
filter?: string
|
||||||
|
) => {
|
||||||
|
if (!services) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const processedServices = this._services(localize, services);
|
||||||
|
|
||||||
|
if (!filter) {
|
||||||
|
return processedServices;
|
||||||
|
}
|
||||||
|
return processedServices.filter(
|
||||||
|
(service) =>
|
||||||
|
service.service.toLowerCase().includes(filter) ||
|
||||||
|
service.name?.toLowerCase().includes(filter)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private _filterChanged(ev: CustomEvent): void {
|
||||||
|
this._filter = ev.detail.value.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueChanged(ev) {
|
||||||
|
this.value = ev.detail.value;
|
||||||
|
fireEvent(this, "change");
|
||||||
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("ha-service-picker", HaServicePicker);
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-service-picker": HaServicePicker;
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,7 @@ import {
|
|||||||
html,
|
html,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
SVGTemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
|
|
||||||
@customElement("ha-settings-row")
|
@customElement("ha-settings-row")
|
||||||
@ -16,15 +16,18 @@ export class HaSettingsRow extends LitElement {
|
|||||||
@property({ type: Boolean, attribute: "three-line" })
|
@property({ type: Boolean, attribute: "three-line" })
|
||||||
public threeLine = false;
|
public threeLine = false;
|
||||||
|
|
||||||
protected render(): SVGTemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<paper-item-body
|
<div class="prefix-wrap">
|
||||||
?two-line=${!this.threeLine}
|
<slot name="prefix"></slot>
|
||||||
?three-line=${this.threeLine}
|
<paper-item-body
|
||||||
>
|
?two-line=${!this.threeLine}
|
||||||
<slot name="heading"></slot>
|
?three-line=${this.threeLine}
|
||||||
<div secondary><slot name="description"></slot></div>
|
>
|
||||||
</paper-item-body>
|
<slot name="heading"></slot>
|
||||||
|
<div secondary><slot name="description"></slot></div>
|
||||||
|
</paper-item-body>
|
||||||
|
</div>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -45,6 +48,7 @@ export class HaSettingsRow extends LitElement {
|
|||||||
min-height: calc(
|
min-height: calc(
|
||||||
var(--paper-item-body-two-line-min-height, 72px) - 16px
|
var(--paper-item-body-two-line-min-height, 72px) - 16px
|
||||||
);
|
);
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
:host([narrow]) {
|
:host([narrow]) {
|
||||||
align-items: normal;
|
align-items: normal;
|
||||||
@ -58,6 +62,13 @@ export class HaSettingsRow extends LitElement {
|
|||||||
div[secondary] {
|
div[secondary] {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
.prefix-wrap {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
:host([narrow]) .prefix-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,10 @@ import {
|
|||||||
mdiUnfoldMoreVertical,
|
mdiUnfoldMoreVertical,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import "@polymer/paper-tooltip/paper-tooltip";
|
import "@polymer/paper-tooltip/paper-tooltip";
|
||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import {
|
||||||
|
HassServiceTarget,
|
||||||
|
UnsubscribeFunc,
|
||||||
|
} from "home-assistant-js-websocket";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResult,
|
CSSResult,
|
||||||
@ -41,7 +44,6 @@ import {
|
|||||||
EntityRegistryEntry,
|
EntityRegistryEntry,
|
||||||
subscribeEntityRegistry,
|
subscribeEntityRegistry,
|
||||||
} from "../data/entity_registry";
|
} from "../data/entity_registry";
|
||||||
import { Target } from "../data/target";
|
|
||||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import "./device/ha-device-picker";
|
import "./device/ha-device-picker";
|
||||||
@ -56,7 +58,7 @@ import "./ha-svg-icon";
|
|||||||
export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||||
@property() public hass!: HomeAssistant;
|
@property() public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property() public value?: Target;
|
@property() public value?: HassServiceTarget;
|
||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
@ -82,6 +84,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
|
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||||
|
|
||||||
@internalProperty() private _areas?: { [areaId: string]: AreaRegistryEntry };
|
@internalProperty() private _areas?: { [areaId: string]: AreaRegistryEntry };
|
||||||
|
|
||||||
@internalProperty() private _devices?: {
|
@internalProperty() private _devices?: {
|
||||||
@ -436,7 +440,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
type: string,
|
type: string,
|
||||||
id: string
|
id: string
|
||||||
): this["value"] {
|
): this["value"] {
|
||||||
const newVal = ensureArray(value![type])!.filter((val) => val !== id);
|
const newVal = ensureArray(value![type])!.filter(
|
||||||
|
(val) => String(val) !== id
|
||||||
|
);
|
||||||
if (newVal.length) {
|
if (newVal.length) {
|
||||||
return {
|
return {
|
||||||
...value,
|
...value,
|
||||||
@ -530,6 +536,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
.items {
|
.items {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
.mdc-chip-set {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
.mdc-chip.add {
|
.mdc-chip.add {
|
||||||
color: rgba(0, 0, 0, 0.87);
|
color: rgba(0, 0, 0, 0.87);
|
||||||
}
|
}
|
||||||
@ -594,6 +603,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
paper-tooltip.expand {
|
paper-tooltip.expand {
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
|
:host([disabled]) .mdc-chip {
|
||||||
|
opacity: var(--light-disabled-opacity);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,14 +44,14 @@ export class HaYamlEditor extends LitElement {
|
|||||||
|
|
||||||
@internalProperty() private _yaml = "";
|
@internalProperty() private _yaml = "";
|
||||||
|
|
||||||
@query("ha-code-editor", true) private _editor?: HaCodeEditor;
|
@query("ha-code-editor") private _editor?: HaCodeEditor;
|
||||||
|
|
||||||
public setValue(value): void {
|
public setValue(value): void {
|
||||||
try {
|
try {
|
||||||
this._yaml = value && !isEmpty(value) ? safeDump(value) : "";
|
this._yaml = value && !isEmpty(value) ? safeDump(value) : "";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(err);
|
console.error(err, value);
|
||||||
alert(`There was an error converting to YAML: ${err}`);
|
alert(`There was an error converting to YAML: ${err}`);
|
||||||
}
|
}
|
||||||
afterNextRender(() => {
|
afterNextRender(() => {
|
||||||
@ -73,7 +73,7 @@ export class HaYamlEditor extends LitElement {
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
${this.label ? html` <p>${this.label}</p> ` : ""}
|
${this.label ? html`<p>${this.label}</p>` : ""}
|
||||||
<ha-code-editor
|
<ha-code-editor
|
||||||
.value=${this._yaml}
|
.value=${this._yaml}
|
||||||
mode="yaml"
|
mode="yaml"
|
||||||
@ -85,13 +85,13 @@ export class HaYamlEditor extends LitElement {
|
|||||||
|
|
||||||
private _onChange(ev: CustomEvent): void {
|
private _onChange(ev: CustomEvent): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const value = ev.detail.value;
|
this._yaml = ev.detail.value;
|
||||||
let parsed;
|
let parsed;
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
|
|
||||||
if (value) {
|
if (this._yaml) {
|
||||||
try {
|
try {
|
||||||
parsed = safeLoad(value);
|
parsed = safeLoad(this._yaml);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Invalid YAML
|
// Invalid YAML
|
||||||
isValid = false;
|
isValid = false;
|
||||||
@ -107,7 +107,7 @@ export class HaYamlEditor extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get yaml() {
|
get yaml() {
|
||||||
return this._editor?.value;
|
return this._yaml;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,16 +65,18 @@ export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) =>
|
|||||||
export const getConfigFlowHandlers = (hass: HomeAssistant) =>
|
export const getConfigFlowHandlers = (hass: HomeAssistant) =>
|
||||||
hass.callApi<string[]>("GET", "config/config_entries/flow_handlers");
|
hass.callApi<string[]>("GET", "config/config_entries/flow_handlers");
|
||||||
|
|
||||||
const fetchConfigFlowInProgress = (conn) =>
|
export const fetchConfigFlowInProgress = (
|
||||||
|
conn: Connection
|
||||||
|
): Promise<DataEntryFlowProgress[]> =>
|
||||||
conn.sendMessagePromise({
|
conn.sendMessagePromise({
|
||||||
type: "config_entries/flow/progress",
|
type: "config_entries/flow/progress",
|
||||||
});
|
});
|
||||||
|
|
||||||
const subscribeConfigFlowInProgressUpdates = (conn, store) =>
|
const subscribeConfigFlowInProgressUpdates = (conn: Connection, store) =>
|
||||||
conn.subscribeEvents(
|
conn.subscribeEvents(
|
||||||
debounce(
|
debounce(
|
||||||
() =>
|
() =>
|
||||||
fetchConfigFlowInProgress(conn).then((flows) =>
|
fetchConfigFlowInProgress(conn).then((flows: DataEntryFlowProgress[]) =>
|
||||||
store.setState(flows, true)
|
store.setState(flows, true)
|
||||||
),
|
),
|
||||||
500,
|
500,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { atLeastVersion } from "../../common/config/version";
|
||||||
import { HaFormSchema } from "../../components/ha-form/ha-form";
|
import { HaFormSchema } from "../../components/ha-form/ha-form";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import { SupervisorArch } from "../supervisor/supervisor";
|
import { SupervisorArch } from "../supervisor/supervisor";
|
||||||
@ -102,10 +103,28 @@ export interface HassioAddonSetOptionParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const reloadHassioAddons = async (hass: HomeAssistant) => {
|
export const reloadHassioAddons = async (hass: HomeAssistant) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "/addons/reload",
|
||||||
|
method: "post",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
await hass.callApi<HassioResponse<void>>("POST", `hassio/addons/reload`);
|
await hass.callApi<HassioResponse<void>>("POST", `hassio/addons/reload`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchHassioAddonsInfo = async (hass: HomeAssistant) => {
|
export const fetchHassioAddonsInfo = async (
|
||||||
|
hass: HomeAssistant
|
||||||
|
): Promise<HassioAddonsInfo> => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
return await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "/addons",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return hassioApiResultExtractor(
|
return hassioApiResultExtractor(
|
||||||
await hass.callApi<HassioResponse<HassioAddonsInfo>>("GET", `hassio/addons`)
|
await hass.callApi<HassioResponse<HassioAddonsInfo>>("GET", `hassio/addons`)
|
||||||
);
|
);
|
||||||
@ -114,7 +133,15 @@ export const fetchHassioAddonsInfo = async (hass: HomeAssistant) => {
|
|||||||
export const fetchHassioAddonInfo = async (
|
export const fetchHassioAddonInfo = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
slug: string
|
slug: string
|
||||||
) => {
|
): Promise<HassioAddonDetails> => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
return await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/addons/${slug}/info`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return hassioApiResultExtractor(
|
return hassioApiResultExtractor(
|
||||||
await hass.callApi<HassioResponse<HassioAddonDetails>>(
|
await hass.callApi<HassioResponse<HassioAddonDetails>>(
|
||||||
"GET",
|
"GET",
|
||||||
@ -149,6 +176,16 @@ export const setHassioAddonOption = async (
|
|||||||
slug: string,
|
slug: string,
|
||||||
data: HassioAddonSetOptionParams
|
data: HassioAddonSetOptionParams
|
||||||
) => {
|
) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/addons/${slug}/options`,
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await hass.callApi<HassioResponse<void>>(
|
await hass.callApi<HassioResponse<void>>(
|
||||||
"POST",
|
"POST",
|
||||||
`hassio/addons/${slug}/options`,
|
`hassio/addons/${slug}/options`,
|
||||||
@ -159,21 +196,64 @@ export const setHassioAddonOption = async (
|
|||||||
export const validateHassioAddonOption = async (
|
export const validateHassioAddonOption = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
slug: string
|
slug: string
|
||||||
) => {
|
): Promise<{ message: string; valid: boolean }> => {
|
||||||
return await hass.callApi<
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
HassioResponse<{ message: string; valid: boolean }>
|
return await hass.callWS({
|
||||||
>("POST", `hassio/addons/${slug}/options/validate`);
|
type: "supervisor/api",
|
||||||
|
endpoint: `/addons/${slug}/options/validate`,
|
||||||
|
method: "post",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
await hass.callApi<HassioResponse<{ message: string; valid: boolean }>>(
|
||||||
|
"POST",
|
||||||
|
`hassio/addons/${slug}/options/validate`
|
||||||
|
)
|
||||||
|
).data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const startHassioAddon = async (hass: HomeAssistant, slug: string) => {
|
export const startHassioAddon = async (hass: HomeAssistant, slug: string) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
return await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/addons/${slug}/start`,
|
||||||
|
method: "post",
|
||||||
|
timeout: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return hass.callApi<string>("POST", `hassio/addons/${slug}/start`);
|
return hass.callApi<string>("POST", `hassio/addons/${slug}/start`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const stopHassioAddon = async (hass: HomeAssistant, slug: string) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
return await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/addons/${slug}/stop`,
|
||||||
|
method: "post",
|
||||||
|
timeout: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return hass.callApi<string>("POST", `hassio/addons/${slug}/stop`);
|
||||||
|
};
|
||||||
|
|
||||||
export const setHassioAddonSecurity = async (
|
export const setHassioAddonSecurity = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
slug: string,
|
slug: string,
|
||||||
data: HassioAddonSetSecurityParams
|
data: HassioAddonSetSecurityParams
|
||||||
) => {
|
) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/addons/${slug}/security`,
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await hass.callApi<HassioResponse<void>>(
|
await hass.callApi<HassioResponse<void>>(
|
||||||
"POST",
|
"POST",
|
||||||
`hassio/addons/${slug}/security`,
|
`hassio/addons/${slug}/security`,
|
||||||
@ -181,15 +261,61 @@ export const setHassioAddonSecurity = async (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const installHassioAddon = async (hass: HomeAssistant, slug: string) => {
|
export const installHassioAddon = async (
|
||||||
return hass.callApi<HassioResponse<void>>(
|
hass: HomeAssistant,
|
||||||
|
slug: string
|
||||||
|
): Promise<void> => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/addons/${slug}/install`,
|
||||||
|
method: "post",
|
||||||
|
timeout: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.callApi<HassioResponse<void>>(
|
||||||
"POST",
|
"POST",
|
||||||
`hassio/addons/${slug}/install`
|
`hassio/addons/${slug}/install`
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const restartHassioAddon = async (hass: HomeAssistant, slug: string) => {
|
export const updateHassioAddon = async (
|
||||||
return hass.callApi<HassioResponse<void>>(
|
hass: HomeAssistant,
|
||||||
|
slug: string
|
||||||
|
): Promise<void> => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/addons/${slug}/update`,
|
||||||
|
method: "post",
|
||||||
|
timeout: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.callApi<HassioResponse<void>>(
|
||||||
|
"POST",
|
||||||
|
`hassio/addons/${slug}/update`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const restartHassioAddon = async (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
slug: string
|
||||||
|
): Promise<void> => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/addons/${slug}/restart`,
|
||||||
|
method: "post",
|
||||||
|
timeout: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.callApi<HassioResponse<void>>(
|
||||||
"POST",
|
"POST",
|
||||||
`hassio/addons/${slug}/restart`
|
`hassio/addons/${slug}/restart`
|
||||||
);
|
);
|
||||||
@ -199,6 +325,16 @@ export const uninstallHassioAddon = async (
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
slug: string
|
slug: string
|
||||||
) => {
|
) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/addons/${slug}/uninstall`,
|
||||||
|
method: "post",
|
||||||
|
timeout: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await hass.callApi<HassioResponse<void>>(
|
await hass.callApi<HassioResponse<void>>(
|
||||||
"POST",
|
"POST",
|
||||||
`hassio/addons/${slug}/uninstall`
|
`hassio/addons/${slug}/uninstall`
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { atLeastVersion } from "../../common/config/version";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
|
|
||||||
export interface HassioResponse<T> {
|
export interface HassioResponse<T> {
|
||||||
@ -33,6 +34,14 @@ export const fetchHassioStats = async (
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
container: string
|
container: string
|
||||||
): Promise<HassioStats> => {
|
): Promise<HassioStats> => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
return await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/${container}/stats`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return hassioApiResultExtractor(
|
return hassioApiResultExtractor(
|
||||||
await hass.callApi<HassioResponse<HassioStats>>(
|
await hass.callApi<HassioResponse<HassioStats>>(
|
||||||
"GET",
|
"GET",
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { atLeastVersion } from "../../common/config/version";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import { hassioApiResultExtractor, HassioResponse } from "./common";
|
import { hassioApiResultExtractor, HassioResponse } from "./common";
|
||||||
|
|
||||||
@ -5,7 +6,17 @@ interface HassioDockerRegistries {
|
|||||||
[key: string]: { username: string; password?: string };
|
[key: string]: { username: string; password?: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchHassioDockerRegistries = async (hass: HomeAssistant) => {
|
export const fetchHassioDockerRegistries = async (
|
||||||
|
hass: HomeAssistant
|
||||||
|
): Promise<HassioDockerRegistries> => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
return await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/docker/registries`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return hassioApiResultExtractor(
|
return hassioApiResultExtractor(
|
||||||
await hass.callApi<HassioResponse<HassioDockerRegistries>>(
|
await hass.callApi<HassioResponse<HassioDockerRegistries>>(
|
||||||
"GET",
|
"GET",
|
||||||
@ -18,6 +29,16 @@ export const addHassioDockerRegistry = async (
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
data: HassioDockerRegistries
|
data: HassioDockerRegistries
|
||||||
) => {
|
) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/docker/registries`,
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await hass.callApi<HassioResponse<HassioDockerRegistries>>(
|
await hass.callApi<HassioResponse<HassioDockerRegistries>>(
|
||||||
"POST",
|
"POST",
|
||||||
"hassio/docker/registries",
|
"hassio/docker/registries",
|
||||||
@ -29,6 +50,15 @@ export const removeHassioDockerRegistry = async (
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
registry: string
|
registry: string
|
||||||
) => {
|
) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/docker/registries/${registry}`,
|
||||||
|
method: "delete",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await hass.callApi<HassioResponse<void>>(
|
await hass.callApi<HassioResponse<void>>(
|
||||||
"DELETE",
|
"DELETE",
|
||||||
`hassio/docker/registries/${registry}`
|
`hassio/docker/registries/${registry}`
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { atLeastVersion } from "../../common/config/version";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import { hassioApiResultExtractor, HassioResponse } from "./common";
|
import { hassioApiResultExtractor, HassioResponse } from "./common";
|
||||||
|
|
||||||
@ -21,7 +22,17 @@ export interface HassioHardwareInfo {
|
|||||||
audio: Record<string, unknown>;
|
audio: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchHassioHardwareAudio = async (hass: HomeAssistant) => {
|
export const fetchHassioHardwareAudio = async (
|
||||||
|
hass: HomeAssistant
|
||||||
|
): Promise<HassioHardwareAudioList> => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
return await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/hardware/audio`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return hassioApiResultExtractor(
|
return hassioApiResultExtractor(
|
||||||
await hass.callApi<HassioResponse<HassioHardwareAudioList>>(
|
await hass.callApi<HassioResponse<HassioHardwareAudioList>>(
|
||||||
"GET",
|
"GET",
|
||||||
@ -30,7 +41,17 @@ export const fetchHassioHardwareAudio = async (hass: HomeAssistant) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchHassioHardwareInfo = async (hass: HomeAssistant) => {
|
export const fetchHassioHardwareInfo = async (
|
||||||
|
hass: HomeAssistant
|
||||||
|
): Promise<HassioHardwareInfo> => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
return await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/hardware/info`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return hassioApiResultExtractor(
|
return hassioApiResultExtractor(
|
||||||
await hass.callApi<HassioResponse<HassioHardwareInfo>>(
|
await hass.callApi<HassioResponse<HassioHardwareInfo>>(
|
||||||
"GET",
|
"GET",
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { atLeastVersion } from "../../common/config/version";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import { hassioApiResultExtractor, HassioResponse } from "./common";
|
import { hassioApiResultExtractor, HassioResponse } from "./common";
|
||||||
|
|
||||||
@ -23,7 +24,17 @@ export interface HassioHassOSInfo {
|
|||||||
version: string | null;
|
version: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchHassioHostInfo = async (hass: HomeAssistant) => {
|
export const fetchHassioHostInfo = async (
|
||||||
|
hass: HomeAssistant
|
||||||
|
): Promise<HassioHostInfo> => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
return await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "/host/info",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const response = await hass.callApi<HassioResponse<HassioHostInfo>>(
|
const response = await hass.callApi<HassioResponse<HassioHostInfo>>(
|
||||||
"GET",
|
"GET",
|
||||||
"hassio/host/info"
|
"hassio/host/info"
|
||||||
@ -31,7 +42,17 @@ export const fetchHassioHostInfo = async (hass: HomeAssistant) => {
|
|||||||
return hassioApiResultExtractor(response);
|
return hassioApiResultExtractor(response);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchHassioHassOsInfo = async (hass: HomeAssistant) => {
|
export const fetchHassioHassOsInfo = async (
|
||||||
|
hass: HomeAssistant
|
||||||
|
): Promise<HassioHassOSInfo> => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
return await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "/os/info",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return hassioApiResultExtractor(
|
return hassioApiResultExtractor(
|
||||||
await hass.callApi<HassioResponse<HassioHassOSInfo>>(
|
await hass.callApi<HassioResponse<HassioHassOSInfo>>(
|
||||||
"GET",
|
"GET",
|
||||||
@ -41,22 +62,67 @@ export const fetchHassioHassOsInfo = async (hass: HomeAssistant) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const rebootHost = async (hass: HomeAssistant) => {
|
export const rebootHost = async (hass: HomeAssistant) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
return await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "/host/reboot",
|
||||||
|
method: "post",
|
||||||
|
timeout: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return hass.callApi<HassioResponse<void>>("POST", "hassio/host/reboot");
|
return hass.callApi<HassioResponse<void>>("POST", "hassio/host/reboot");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const shutdownHost = async (hass: HomeAssistant) => {
|
export const shutdownHost = async (hass: HomeAssistant) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
return await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "/host/shutdown",
|
||||||
|
method: "post",
|
||||||
|
timeout: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return hass.callApi<HassioResponse<void>>("POST", "hassio/host/shutdown");
|
return hass.callApi<HassioResponse<void>>("POST", "hassio/host/shutdown");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateOS = async (hass: HomeAssistant) => {
|
export const updateOS = async (hass: HomeAssistant) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
return await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "/os/update",
|
||||||
|
method: "post",
|
||||||
|
timeout: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return hass.callApi<HassioResponse<void>>("POST", "hassio/os/update");
|
return hass.callApi<HassioResponse<void>>("POST", "hassio/os/update");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const configSyncOS = async (hass: HomeAssistant) => {
|
export const configSyncOS = async (hass: HomeAssistant) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
return await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "os/config/sync",
|
||||||
|
method: "post",
|
||||||
|
timeout: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return hass.callApi<HassioResponse<void>>("POST", "hassio/os/config/sync");
|
return hass.callApi<HassioResponse<void>>("POST", "hassio/os/config/sync");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const changeHostOptions = async (hass: HomeAssistant, options: any) => {
|
export const changeHostOptions = async (hass: HomeAssistant, options: any) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
return await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "/host/options",
|
||||||
|
method: "post",
|
||||||
|
data: options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return hass.callApi<HassioResponse<void>>(
|
return hass.callApi<HassioResponse<void>>(
|
||||||
"POST",
|
"POST",
|
||||||
"hassio/host/options",
|
"hassio/host/options",
|
||||||
|
@ -1,26 +1,50 @@
|
|||||||
|
import { atLeastVersion } from "../../common/config/version";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import { HassioResponse } from "./common";
|
import { HassioResponse } from "./common";
|
||||||
import { CreateSessionResponse } from "./supervisor";
|
import { CreateSessionResponse } from "./supervisor";
|
||||||
|
|
||||||
export const createHassioSession = async (hass: HomeAssistant) => {
|
function setIngressCookie(session: string): string {
|
||||||
const response = await hass.callApi<HassioResponse<CreateSessionResponse>>(
|
document.cookie = `ingress_session=${session};path=/api/hassio_ingress/;SameSite=Strict${
|
||||||
"POST",
|
|
||||||
"hassio/ingress/session"
|
|
||||||
);
|
|
||||||
document.cookie = `ingress_session=${
|
|
||||||
response.data.session
|
|
||||||
};path=/api/hassio_ingress/;SameSite=Strict${
|
|
||||||
location.protocol === "https:" ? ";Secure" : ""
|
location.protocol === "https:" ? ";Secure" : ""
|
||||||
}`;
|
}`;
|
||||||
return response.data.session;
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createHassioSession = async (
|
||||||
|
hass: HomeAssistant
|
||||||
|
): Promise<string> => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
const wsResponse: { session: string } = await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "/ingress/session",
|
||||||
|
method: "post",
|
||||||
|
});
|
||||||
|
return setIngressCookie(wsResponse.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
const restResponse: { data: { session: string } } = await hass.callApi<
|
||||||
|
HassioResponse<CreateSessionResponse>
|
||||||
|
>("POST", "hassio/ingress/session");
|
||||||
|
return setIngressCookie(restResponse.data.session);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validateHassioSession = async (
|
export const validateHassioSession = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
session: string
|
session: string
|
||||||
) =>
|
): Promise<void> => {
|
||||||
await hass.callApi<HassioResponse<null>>(
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "/ingress/validate_session",
|
||||||
|
method: "post",
|
||||||
|
data: { session },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.callApi<HassioResponse<void>>(
|
||||||
"POST",
|
"POST",
|
||||||
"hassio/ingress/validate_session",
|
"hassio/ingress/validate_session",
|
||||||
{ session }
|
{ session }
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { atLeastVersion } from "../../common/config/version";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import { hassioApiResultExtractor, HassioResponse } from "./common";
|
import { hassioApiResultExtractor, HassioResponse } from "./common";
|
||||||
|
|
||||||
@ -51,7 +52,17 @@ export interface NetworkInfo {
|
|||||||
docker: DockerNetwork;
|
docker: DockerNetwork;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchNetworkInfo = async (hass: HomeAssistant) => {
|
export const fetchNetworkInfo = async (
|
||||||
|
hass: HomeAssistant
|
||||||
|
): Promise<NetworkInfo> => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
return await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "/network/info",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return hassioApiResultExtractor(
|
return hassioApiResultExtractor(
|
||||||
await hass.callApi<HassioResponse<NetworkInfo>>(
|
await hass.callApi<HassioResponse<NetworkInfo>>(
|
||||||
"GET",
|
"GET",
|
||||||
@ -65,6 +76,17 @@ export const updateNetworkInterface = async (
|
|||||||
network_interface: string,
|
network_interface: string,
|
||||||
options: Partial<NetworkInterface>
|
options: Partial<NetworkInterface>
|
||||||
) => {
|
) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/network/interface/${network_interface}/update`,
|
||||||
|
method: "post",
|
||||||
|
data: options,
|
||||||
|
timeout: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await hass.callApi<HassioResponse<NetworkInfo>>(
|
await hass.callApi<HassioResponse<NetworkInfo>>(
|
||||||
"POST",
|
"POST",
|
||||||
`hassio/network/interface/${network_interface}/update`,
|
`hassio/network/interface/${network_interface}/update`,
|
||||||
@ -75,7 +97,16 @@ export const updateNetworkInterface = async (
|
|||||||
export const accesspointScan = async (
|
export const accesspointScan = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
network_interface: string
|
network_interface: string
|
||||||
) => {
|
): Promise<AccessPoints> => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
return await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/network/interface/${network_interface}/accesspoints`,
|
||||||
|
method: "get",
|
||||||
|
timeout: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return hassioApiResultExtractor(
|
return hassioApiResultExtractor(
|
||||||
await hass.callApi<HassioResponse<AccessPoints>>(
|
await hass.callApi<HassioResponse<AccessPoints>>(
|
||||||
"GET",
|
"GET",
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { atLeastVersion } from "../../common/config/version";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import { hassioApiResultExtractor, HassioResponse } from "./common";
|
import { hassioApiResultExtractor, HassioResponse } from "./common";
|
||||||
|
|
||||||
@ -8,7 +9,17 @@ export interface HassioResolution {
|
|||||||
suggestions: string[];
|
suggestions: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchHassioResolution = async (hass: HomeAssistant) => {
|
export const fetchHassioResolution = async (
|
||||||
|
hass: HomeAssistant
|
||||||
|
): Promise<HassioResolution> => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
return await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "/resolution/info",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return hassioApiResultExtractor(
|
return hassioApiResultExtractor(
|
||||||
await hass.callApi<HassioResponse<HassioResolution>>(
|
await hass.callApi<HassioResponse<HassioResolution>>(
|
||||||
"GET",
|
"GET",
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { atLeastVersion } from "../../common/config/version";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import { hassioApiResultExtractor, HassioResponse } from "./common";
|
import { hassioApiResultExtractor, HassioResponse } from "./common";
|
||||||
|
|
||||||
@ -33,7 +34,18 @@ export interface HassioPartialSnapshotCreateParams {
|
|||||||
password?: string;
|
password?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchHassioSnapshots = async (hass: HomeAssistant) => {
|
export const fetchHassioSnapshots = async (
|
||||||
|
hass: HomeAssistant
|
||||||
|
): Promise<HassioSnapshot[]> => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
const data: { snapshots: HassioSnapshot[] } = await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/snapshots`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
return data.snapshots;
|
||||||
|
}
|
||||||
|
|
||||||
return hassioApiResultExtractor(
|
return hassioApiResultExtractor(
|
||||||
await hass.callApi<HassioResponse<{ snapshots: HassioSnapshot[] }>>(
|
await hass.callApi<HassioResponse<{ snapshots: HassioSnapshot[] }>>(
|
||||||
"GET",
|
"GET",
|
||||||
@ -45,8 +57,15 @@ export const fetchHassioSnapshots = async (hass: HomeAssistant) => {
|
|||||||
export const fetchHassioSnapshotInfo = async (
|
export const fetchHassioSnapshotInfo = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
snapshot: string
|
snapshot: string
|
||||||
) => {
|
): Promise<HassioSnapshotDetail> => {
|
||||||
if (hass) {
|
if (hass) {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
return await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/snapshots/${snapshot}/info`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
}
|
||||||
return hassioApiResultExtractor(
|
return hassioApiResultExtractor(
|
||||||
await hass.callApi<HassioResponse<HassioSnapshotDetail>>(
|
await hass.callApi<HassioResponse<HassioSnapshotDetail>>(
|
||||||
"GET",
|
"GET",
|
||||||
@ -63,6 +82,15 @@ export const fetchHassioSnapshotInfo = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const reloadHassioSnapshots = async (hass: HomeAssistant) => {
|
export const reloadHassioSnapshots = async (hass: HomeAssistant) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "/snapshots/reload",
|
||||||
|
method: "post",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await hass.callApi<HassioResponse<void>>("POST", `hassio/snapshots/reload`);
|
await hass.callApi<HassioResponse<void>>("POST", `hassio/snapshots/reload`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -70,6 +98,15 @@ export const createHassioFullSnapshot = async (
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
data: HassioFullSnapshotCreateParams
|
data: HassioFullSnapshotCreateParams
|
||||||
) => {
|
) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "/snapshots/new/full",
|
||||||
|
method: "post",
|
||||||
|
timeout: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
await hass.callApi<HassioResponse<void>>(
|
await hass.callApi<HassioResponse<void>>(
|
||||||
"POST",
|
"POST",
|
||||||
`hassio/snapshots/new/full`,
|
`hassio/snapshots/new/full`,
|
||||||
@ -81,6 +118,17 @@ export const createHassioPartialSnapshot = async (
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
data: HassioFullSnapshotCreateParams
|
data: HassioFullSnapshotCreateParams
|
||||||
) => {
|
) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "/snapshots/new/partial",
|
||||||
|
method: "post",
|
||||||
|
timeout: null,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await hass.callApi<HassioResponse<void>>(
|
await hass.callApi<HassioResponse<void>>(
|
||||||
"POST",
|
"POST",
|
||||||
`hassio/snapshots/new/partial`,
|
`hassio/snapshots/new/partial`,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { atLeastVersion } from "../../common/config/version";
|
||||||
import { HomeAssistant, PanelInfo } from "../../types";
|
import { HomeAssistant, PanelInfo } from "../../types";
|
||||||
import { SupervisorArch } from "../supervisor/supervisor";
|
import { SupervisorArch } from "../supervisor/supervisor";
|
||||||
import { HassioAddonInfo, HassioAddonRepository } from "./addon";
|
import { HassioAddonInfo, HassioAddonRepository } from "./addon";
|
||||||
@ -49,6 +50,15 @@ export type HassioInfo = {
|
|||||||
hostname: string;
|
hostname: string;
|
||||||
logging: string;
|
logging: string;
|
||||||
machine: string;
|
machine: string;
|
||||||
|
state:
|
||||||
|
| "initialize"
|
||||||
|
| "setup"
|
||||||
|
| "startup"
|
||||||
|
| "running"
|
||||||
|
| "freeze"
|
||||||
|
| "shutdown"
|
||||||
|
| "stopping"
|
||||||
|
| "close";
|
||||||
operating_system: string;
|
operating_system: string;
|
||||||
supervisor: string;
|
supervisor: string;
|
||||||
supported: boolean;
|
supported: boolean;
|
||||||
@ -74,18 +84,57 @@ export interface SupervisorOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const reloadSupervisor = async (hass: HomeAssistant) => {
|
export const reloadSupervisor = async (hass: HomeAssistant) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "/supervisor/reload",
|
||||||
|
method: "post",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await hass.callApi<HassioResponse<void>>("POST", `hassio/supervisor/reload`);
|
await hass.callApi<HassioResponse<void>>("POST", `hassio/supervisor/reload`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const restartSupervisor = async (hass: HomeAssistant) => {
|
export const restartSupervisor = async (hass: HomeAssistant) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "/supervisor/restart",
|
||||||
|
method: "post",
|
||||||
|
timeout: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await hass.callApi<HassioResponse<void>>("POST", `hassio/supervisor/restart`);
|
await hass.callApi<HassioResponse<void>>("POST", `hassio/supervisor/restart`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateSupervisor = async (hass: HomeAssistant) => {
|
export const updateSupervisor = async (hass: HomeAssistant) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "/supervisor/update",
|
||||||
|
method: "post",
|
||||||
|
timeout: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await hass.callApi<HassioResponse<void>>("POST", `hassio/supervisor/update`);
|
await hass.callApi<HassioResponse<void>>("POST", `hassio/supervisor/update`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchHassioHomeAssistantInfo = async (hass: HomeAssistant) => {
|
export const fetchHassioHomeAssistantInfo = async (
|
||||||
|
hass: HomeAssistant
|
||||||
|
): Promise<HassioHomeAssistantInfo> => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
return await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "/core/info",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return hassioApiResultExtractor(
|
return hassioApiResultExtractor(
|
||||||
await hass.callApi<HassioResponse<HassioHomeAssistantInfo>>(
|
await hass.callApi<HassioResponse<HassioHomeAssistantInfo>>(
|
||||||
"GET",
|
"GET",
|
||||||
@ -94,7 +143,17 @@ export const fetchHassioHomeAssistantInfo = async (hass: HomeAssistant) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchHassioSupervisorInfo = async (hass: HomeAssistant) => {
|
export const fetchHassioSupervisorInfo = async (
|
||||||
|
hass: HomeAssistant
|
||||||
|
): Promise<HassioSupervisorInfo> => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
return await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "/supervisor/info",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return hassioApiResultExtractor(
|
return hassioApiResultExtractor(
|
||||||
await hass.callApi<HassioResponse<HassioSupervisorInfo>>(
|
await hass.callApi<HassioResponse<HassioSupervisorInfo>>(
|
||||||
"GET",
|
"GET",
|
||||||
@ -103,7 +162,17 @@ export const fetchHassioSupervisorInfo = async (hass: HomeAssistant) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchHassioInfo = async (hass: HomeAssistant) => {
|
export const fetchHassioInfo = async (
|
||||||
|
hass: HomeAssistant
|
||||||
|
): Promise<HassioInfo> => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
return await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "/info",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return hassioApiResultExtractor(
|
return hassioApiResultExtractor(
|
||||||
await hass.callApi<HassioResponse<HassioInfo>>("GET", "hassio/info")
|
await hass.callApi<HassioResponse<HassioInfo>>("GET", "hassio/info")
|
||||||
);
|
);
|
||||||
@ -120,6 +189,16 @@ export const setSupervisorOption = async (
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
data: SupervisorOptions
|
data: SupervisorOptions
|
||||||
) => {
|
) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "/supervisor/options",
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await hass.callApi<HassioResponse<void>>(
|
await hass.callApi<HassioResponse<void>>(
|
||||||
"POST",
|
"POST",
|
||||||
"hassio/supervisor/options",
|
"hassio/supervisor/options",
|
||||||
|
@ -2,6 +2,7 @@ import {
|
|||||||
Connection,
|
Connection,
|
||||||
getCollection,
|
getCollection,
|
||||||
HassEventBase,
|
HassEventBase,
|
||||||
|
HassServiceTarget,
|
||||||
} from "home-assistant-js-websocket";
|
} from "home-assistant-js-websocket";
|
||||||
import { HASSDomEvent } from "../common/dom/fire_event";
|
import { HASSDomEvent } from "../common/dom/fire_event";
|
||||||
import { HuiErrorCard } from "../panels/lovelace/cards/hui-error-card";
|
import { HuiErrorCard } from "../panels/lovelace/cards/hui-error-card";
|
||||||
@ -120,8 +121,8 @@ export interface ToggleActionConfig extends BaseActionConfig {
|
|||||||
export interface CallServiceActionConfig extends BaseActionConfig {
|
export interface CallServiceActionConfig extends BaseActionConfig {
|
||||||
action: "call-service";
|
action: "call-service";
|
||||||
service: string;
|
service: string;
|
||||||
|
target?: HassServiceTarget;
|
||||||
service_data?: {
|
service_data?: {
|
||||||
entity_id?: string | [string];
|
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
HassEntityAttributeBase,
|
HassEntityAttributeBase,
|
||||||
HassEntityBase,
|
HassEntityBase,
|
||||||
|
HassServiceTarget,
|
||||||
} from "home-assistant-js-websocket";
|
} from "home-assistant-js-websocket";
|
||||||
import { computeObjectId } from "../common/entity/compute_object_id";
|
import { computeObjectId } from "../common/entity/compute_object_id";
|
||||||
import { navigate } from "../common/navigate";
|
import { navigate } from "../common/navigate";
|
||||||
@ -36,6 +37,7 @@ export interface EventAction {
|
|||||||
export interface ServiceAction {
|
export interface ServiceAction {
|
||||||
service: string;
|
service: string;
|
||||||
entity_id?: string;
|
entity_id?: string;
|
||||||
|
target?: HassServiceTarget;
|
||||||
data?: Record<string, any>;
|
data?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,8 +8,8 @@ export type Selector =
|
|||||||
| TimeSelector
|
| TimeSelector
|
||||||
| ActionSelector
|
| ActionSelector
|
||||||
| StringSelector
|
| StringSelector
|
||||||
| ObjectSelector;
|
| ObjectSelector
|
||||||
|
| SelectSelector;
|
||||||
export interface EntitySelector {
|
export interface EntitySelector {
|
||||||
entity: {
|
entity: {
|
||||||
integration?: string;
|
integration?: string;
|
||||||
@ -95,3 +95,9 @@ export interface ObjectSelector {
|
|||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
object: {};
|
object: {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SelectSelector {
|
||||||
|
select: {
|
||||||
|
options: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { atLeastVersion } from "../../common/config/version";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import { HassioResponse } from "../hassio/common";
|
import { HassioResponse } from "../hassio/common";
|
||||||
|
|
||||||
@ -6,5 +7,15 @@ export const restartCore = async (hass: HomeAssistant) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const updateCore = async (hass: HomeAssistant) => {
|
export const updateCore = async (hass: HomeAssistant) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: "/core/update",
|
||||||
|
method: "post",
|
||||||
|
timeout: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await hass.callApi<HassioResponse<void>>("POST", `hassio/core/update`);
|
await hass.callApi<HassioResponse<void>>("POST", `hassio/core/update`);
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
import { Connection, getCollection } from "home-assistant-js-websocket";
|
||||||
|
import { Store } from "home-assistant-js-websocket/dist/store";
|
||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
import { HassioAddonsInfo } from "../hassio/addon";
|
||||||
import { HassioHassOSInfo, HassioHostInfo } from "../hassio/host";
|
import { HassioHassOSInfo, HassioHostInfo } from "../hassio/host";
|
||||||
import { NetworkInfo } from "../hassio/network";
|
import { NetworkInfo } from "../hassio/network";
|
||||||
import { HassioResolution } from "../hassio/resolution";
|
import { HassioResolution } from "../hassio/resolution";
|
||||||
@ -7,7 +11,46 @@ import {
|
|||||||
HassioSupervisorInfo,
|
HassioSupervisorInfo,
|
||||||
} from "../hassio/supervisor";
|
} from "../hassio/supervisor";
|
||||||
|
|
||||||
|
export const supervisorWSbaseCommand = {
|
||||||
|
type: "supervisor/api",
|
||||||
|
method: "GET",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const supervisorStore = {
|
||||||
|
host: "/host/info",
|
||||||
|
supervisor: "/supervisor/info",
|
||||||
|
info: "/info",
|
||||||
|
core: "/core/info",
|
||||||
|
network: "/network/info",
|
||||||
|
resolution: "/resolution/info",
|
||||||
|
os: "/os/info",
|
||||||
|
addon: "/addons",
|
||||||
|
};
|
||||||
|
|
||||||
export type SupervisorArch = "armhf" | "armv7" | "aarch64" | "i386" | "amd64";
|
export type SupervisorArch = "armhf" | "armv7" | "aarch64" | "i386" | "amd64";
|
||||||
|
export type SupervisorObject =
|
||||||
|
| "host"
|
||||||
|
| "supervisor"
|
||||||
|
| "info"
|
||||||
|
| "core"
|
||||||
|
| "network"
|
||||||
|
| "resolution"
|
||||||
|
| "os"
|
||||||
|
| "addon";
|
||||||
|
|
||||||
|
interface supervisorApiRequest {
|
||||||
|
endpoint: string;
|
||||||
|
method?: "get" | "post" | "delete" | "put";
|
||||||
|
force_rest?: boolean;
|
||||||
|
data?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SupervisorEvent {
|
||||||
|
event: string;
|
||||||
|
update_key?: SupervisorObject;
|
||||||
|
data?: any;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Supervisor {
|
export interface Supervisor {
|
||||||
host: HassioHostInfo;
|
host: HassioHostInfo;
|
||||||
@ -17,4 +60,77 @@ export interface Supervisor {
|
|||||||
network: NetworkInfo;
|
network: NetworkInfo;
|
||||||
resolution: HassioResolution;
|
resolution: HassioResolution;
|
||||||
os: HassioHassOSInfo;
|
os: HassioHassOSInfo;
|
||||||
|
addon: HassioAddonsInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const supervisorApiWsRequest = <T>(
|
||||||
|
conn: Connection,
|
||||||
|
request: supervisorApiRequest
|
||||||
|
): Promise<T> =>
|
||||||
|
conn.sendMessagePromise<T>({ ...supervisorWSbaseCommand, ...request });
|
||||||
|
|
||||||
|
async function processEvent(
|
||||||
|
conn: Connection,
|
||||||
|
store: Store<any>,
|
||||||
|
event: SupervisorEvent,
|
||||||
|
key: string
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
!event.data ||
|
||||||
|
event.data.event !== "supervisor-update" ||
|
||||||
|
event.data.update_key !== key
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(event.data.data).length === 0) {
|
||||||
|
const data = await supervisorApiWsRequest<any>(conn, {
|
||||||
|
endpoint: supervisorStore[key],
|
||||||
|
});
|
||||||
|
store.setState(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = store.state;
|
||||||
|
if (state === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
store.setState({
|
||||||
|
...state,
|
||||||
|
...event.data.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscribeSupervisorEventUpdates = (
|
||||||
|
conn: Connection,
|
||||||
|
store: Store<unknown>,
|
||||||
|
key: string
|
||||||
|
) =>
|
||||||
|
conn.subscribeEvents(
|
||||||
|
(event) => processEvent(conn, store, event as SupervisorEvent, key),
|
||||||
|
"supervisor_event"
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getSupervisorEventCollection = (
|
||||||
|
conn: Connection,
|
||||||
|
key: string,
|
||||||
|
endpoint: string
|
||||||
|
) =>
|
||||||
|
getCollection(
|
||||||
|
conn,
|
||||||
|
`_supervisor${key}Event`,
|
||||||
|
() => supervisorApiWsRequest(conn, { endpoint }),
|
||||||
|
(connection, store) =>
|
||||||
|
subscribeSupervisorEventUpdates(connection, store, key)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const subscribeSupervisorEvents = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
onChange: (event) => void,
|
||||||
|
key: string,
|
||||||
|
endpoint: string
|
||||||
|
) =>
|
||||||
|
getSupervisorEventCollection(hass.connection, key, endpoint).subscribe(
|
||||||
|
onChange
|
||||||
|
);
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
export interface Target {
|
|
||||||
entity_id?: string[];
|
|
||||||
device_id?: string[];
|
|
||||||
area_id?: string[];
|
|
||||||
}
|
|
@ -22,7 +22,9 @@ import {
|
|||||||
AreaRegistryEntry,
|
AreaRegistryEntry,
|
||||||
subscribeAreaRegistry,
|
subscribeAreaRegistry,
|
||||||
} from "../../data/area_registry";
|
} from "../../data/area_registry";
|
||||||
|
import { fetchConfigFlowInProgress } from "../../data/config_flow";
|
||||||
import type {
|
import type {
|
||||||
|
DataEntryFlowProgress,
|
||||||
DataEntryFlowProgressedEvent,
|
DataEntryFlowProgressedEvent,
|
||||||
DataEntryFlowStep,
|
DataEntryFlowStep,
|
||||||
} from "../../data/data_entry_flow";
|
} from "../../data/data_entry_flow";
|
||||||
@ -41,6 +43,7 @@ import "./step-flow-form";
|
|||||||
import "./step-flow-loading";
|
import "./step-flow-loading";
|
||||||
import "./step-flow-pick-handler";
|
import "./step-flow-pick-handler";
|
||||||
import "./step-flow-progress";
|
import "./step-flow-progress";
|
||||||
|
import "./step-flow-pick-flow";
|
||||||
|
|
||||||
let instance = 0;
|
let instance = 0;
|
||||||
|
|
||||||
@ -76,6 +79,10 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
|
|
||||||
@internalProperty() private _handlers?: string[];
|
@internalProperty() private _handlers?: string[];
|
||||||
|
|
||||||
|
@internalProperty() private _handler?: string;
|
||||||
|
|
||||||
|
@internalProperty() private _flowsInProgress?: DataEntryFlowProgress[];
|
||||||
|
|
||||||
private _unsubAreas?: UnsubscribeFunc;
|
private _unsubAreas?: UnsubscribeFunc;
|
||||||
|
|
||||||
private _unsubDevices?: UnsubscribeFunc;
|
private _unsubDevices?: UnsubscribeFunc;
|
||||||
@ -84,59 +91,93 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
this._params = params;
|
this._params = params;
|
||||||
this._instance = instance++;
|
this._instance = instance++;
|
||||||
|
|
||||||
|
if (params.startFlowHandler) {
|
||||||
|
this._checkFlowsInProgress(params.startFlowHandler);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.continueFlowId) {
|
||||||
|
this._loading = true;
|
||||||
|
const curInstance = this._instance;
|
||||||
|
let step: DataEntryFlowStep;
|
||||||
|
try {
|
||||||
|
step = await params.flowConfig.fetchFlow(
|
||||||
|
this.hass,
|
||||||
|
params.continueFlowId
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
this._step = undefined;
|
||||||
|
this._params = undefined;
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.error"
|
||||||
|
),
|
||||||
|
text: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.could_not_load"
|
||||||
|
),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Happens if second showDialog called
|
||||||
|
if (curInstance !== this._instance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._processStep(step);
|
||||||
|
this._loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Create a new config flow. Show picker
|
// Create a new config flow. Show picker
|
||||||
if (!params.continueFlowId && !params.startFlowHandler) {
|
if (!params.flowConfig.getFlowHandlers) {
|
||||||
if (!params.flowConfig.getFlowHandlers) {
|
throw new Error("No getFlowHandlers defined in flow config");
|
||||||
throw new Error("No getFlowHandlers defined in flow config");
|
}
|
||||||
|
this._step = null;
|
||||||
|
|
||||||
|
// We only load the handlers once
|
||||||
|
if (this._handlers === undefined) {
|
||||||
|
this._loading = true;
|
||||||
|
try {
|
||||||
|
this._handlers = await params.flowConfig.getFlowHandlers(this.hass);
|
||||||
|
} finally {
|
||||||
|
this._loading = false;
|
||||||
}
|
}
|
||||||
this._step = null;
|
|
||||||
|
|
||||||
// We only load the handlers once
|
|
||||||
if (this._handlers === undefined) {
|
|
||||||
this._loading = true;
|
|
||||||
try {
|
|
||||||
this._handlers = await params.flowConfig.getFlowHandlers(this.hass);
|
|
||||||
} finally {
|
|
||||||
this._loading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await this.updateComplete;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
await this.updateComplete;
|
||||||
this._loading = true;
|
|
||||||
const curInstance = this._instance;
|
|
||||||
let step: DataEntryFlowStep;
|
|
||||||
try {
|
|
||||||
step = await (params.continueFlowId
|
|
||||||
? params.flowConfig.fetchFlow(this.hass, params.continueFlowId)
|
|
||||||
: params.flowConfig.createFlow(this.hass, params.startFlowHandler!));
|
|
||||||
} catch (err) {
|
|
||||||
this._step = undefined;
|
|
||||||
this._params = undefined;
|
|
||||||
showAlertDialog(this, {
|
|
||||||
title: "Error",
|
|
||||||
text: "Config flow could not be loaded",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Happens if second showDialog called
|
|
||||||
if (curInstance !== this._instance) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._processStep(step);
|
|
||||||
this._loading = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog() {
|
public closeDialog() {
|
||||||
if (this._step) {
|
if (!this._params) {
|
||||||
this._flowDone();
|
return;
|
||||||
} else if (this._step === null) {
|
}
|
||||||
// Flow aborted during picking flow
|
const flowFinished = Boolean(
|
||||||
this._step = undefined;
|
this._step && ["create_entry", "abort"].includes(this._step.type)
|
||||||
this._params = undefined;
|
);
|
||||||
|
|
||||||
|
// If we created this flow, delete it now.
|
||||||
|
if (this._step && !flowFinished && !this._params.continueFlowId) {
|
||||||
|
this._params.flowConfig.deleteFlow(this.hass, this._step.flow_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._step !== null && this._params.dialogClosedCallback) {
|
||||||
|
this._params.dialogClosedCallback({
|
||||||
|
flowFinished,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this._step = undefined;
|
||||||
|
this._params = undefined;
|
||||||
|
this._devices = undefined;
|
||||||
|
this._flowsInProgress = undefined;
|
||||||
|
this._handler = undefined;
|
||||||
|
if (this._unsubAreas) {
|
||||||
|
this._unsubAreas();
|
||||||
|
this._unsubAreas = undefined;
|
||||||
|
}
|
||||||
|
if (this._unsubDevices) {
|
||||||
|
this._unsubDevices();
|
||||||
|
this._unsubDevices = undefined;
|
||||||
}
|
}
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
}
|
}
|
||||||
@ -156,7 +197,9 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
${this._loading ||
|
${this._loading ||
|
||||||
(this._step === null && this._handlers === undefined)
|
(this._step === null &&
|
||||||
|
this._handlers === undefined &&
|
||||||
|
this._handler === undefined)
|
||||||
? html`
|
? html`
|
||||||
<step-flow-loading
|
<step-flow-loading
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
@ -178,15 +221,22 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
?rtl=${computeRTL(this.hass)}
|
?rtl=${computeRTL(this.hass)}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
${this._step === null
|
${this._step === null
|
||||||
? // Show handler picker
|
? this._handler
|
||||||
html`
|
? html`<step-flow-pick-flow
|
||||||
<step-flow-pick-handler
|
|
||||||
.flowConfig=${this._params.flowConfig}
|
.flowConfig=${this._params.flowConfig}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.handlers=${this._handlers}
|
.handler=${this._handler}
|
||||||
.showAdvanced=${this._params.showAdvanced}
|
.flowsInProgress=${this._flowsInProgress}
|
||||||
></step-flow-pick-handler>
|
></step-flow-pick-flow>`
|
||||||
`
|
: // Show handler picker
|
||||||
|
html`
|
||||||
|
<step-flow-pick-handler
|
||||||
|
.hass=${this.hass}
|
||||||
|
.handlers=${this._handlers}
|
||||||
|
.showAdvanced=${this._params.showAdvanced}
|
||||||
|
@handler-picked=${this._handlerPicked}
|
||||||
|
></step-flow-pick-handler>
|
||||||
|
`
|
||||||
: this._step.type === "form"
|
: this._step.type === "form"
|
||||||
? html`
|
? html`
|
||||||
<step-flow-form
|
<step-flow-form
|
||||||
@ -291,6 +341,43 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _checkFlowsInProgress(handler: string) {
|
||||||
|
this._loading = true;
|
||||||
|
|
||||||
|
const flowsInProgress = (
|
||||||
|
await fetchConfigFlowInProgress(this.hass.connection)
|
||||||
|
).filter((flow) => flow.handler === handler);
|
||||||
|
|
||||||
|
if (!flowsInProgress.length) {
|
||||||
|
let step: DataEntryFlowStep;
|
||||||
|
try {
|
||||||
|
step = await this._params!.flowConfig.createFlow(this.hass, handler);
|
||||||
|
} catch (err) {
|
||||||
|
this._step = undefined;
|
||||||
|
this._params = undefined;
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.error"
|
||||||
|
),
|
||||||
|
text: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.could_not_load"
|
||||||
|
),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._processStep(step);
|
||||||
|
} else {
|
||||||
|
this._step = null;
|
||||||
|
this._handler = handler;
|
||||||
|
this._flowsInProgress = flowsInProgress;
|
||||||
|
}
|
||||||
|
this._loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handlerPicked(ev) {
|
||||||
|
this._checkFlowsInProgress(ev.detail.handler);
|
||||||
|
}
|
||||||
|
|
||||||
private async _processStep(
|
private async _processStep(
|
||||||
step: DataEntryFlowStep | undefined | Promise<DataEntryFlowStep>
|
step: DataEntryFlowStep | undefined | Promise<DataEntryFlowStep>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@ -305,7 +392,7 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (step === undefined) {
|
if (step === undefined) {
|
||||||
this._flowDone();
|
this.closeDialog();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._step = undefined;
|
this._step = undefined;
|
||||||
@ -313,38 +400,6 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
this._step = step;
|
this._step = step;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _flowDone(): void {
|
|
||||||
if (!this._params) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const flowFinished = Boolean(
|
|
||||||
this._step && ["create_entry", "abort"].includes(this._step.type)
|
|
||||||
);
|
|
||||||
|
|
||||||
// If we created this flow, delete it now.
|
|
||||||
if (this._step && !flowFinished && !this._params.continueFlowId) {
|
|
||||||
this._params.flowConfig.deleteFlow(this.hass, this._step.flow_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._params.dialogClosedCallback) {
|
|
||||||
this._params.dialogClosedCallback({
|
|
||||||
flowFinished,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this._step = undefined;
|
|
||||||
this._params = undefined;
|
|
||||||
this._devices = undefined;
|
|
||||||
if (this._unsubAreas) {
|
|
||||||
this._unsubAreas();
|
|
||||||
this._unsubAreas = undefined;
|
|
||||||
}
|
|
||||||
if (this._unsubDevices) {
|
|
||||||
this._unsubDevices();
|
|
||||||
this._unsubDevices = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultArray {
|
static get styles(): CSSResultArray {
|
||||||
return [
|
return [
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
|
130
src/dialogs/config-flow/step-flow-pick-flow.ts
Normal file
130
src/dialogs/config-flow/step-flow-pick-flow.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import "@polymer/paper-item/paper-icon-item";
|
||||||
|
import "@polymer/paper-item";
|
||||||
|
import "@polymer/paper-item/paper-item-body";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit-element";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import "../../components/ha-icon-next";
|
||||||
|
import { localizeConfigFlowTitle } from "../../data/config_flow";
|
||||||
|
import { DataEntryFlowProgress } from "../../data/data_entry_flow";
|
||||||
|
import { domainToName } from "../../data/integration";
|
||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
import { brandsUrl } from "../../util/brands-url";
|
||||||
|
import { FlowConfig } from "./show-dialog-data-entry-flow";
|
||||||
|
import { configFlowContentStyles } from "./styles";
|
||||||
|
|
||||||
|
@customElement("step-flow-pick-flow")
|
||||||
|
class StepFlowPickFlow extends LitElement {
|
||||||
|
public flowConfig!: FlowConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public flowsInProgress!: DataEntryFlowProgress[];
|
||||||
|
|
||||||
|
@property() public handler!: string;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<h2>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.pick_flow_step.title"
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
${this.flowsInProgress.map(
|
||||||
|
(flow) => html` <paper-icon-item
|
||||||
|
@click=${this._flowInProgressPicked}
|
||||||
|
.flow=${flow}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
slot="item-icon"
|
||||||
|
loading="lazy"
|
||||||
|
src=${brandsUrl(flow.handler, "icon", true)}
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<paper-item-body>
|
||||||
|
${localizeConfigFlowTitle(this.hass.localize, flow)}
|
||||||
|
</paper-item-body>
|
||||||
|
<ha-icon-next></ha-icon-next>
|
||||||
|
</paper-icon-item>`
|
||||||
|
)}
|
||||||
|
<paper-item @click=${this._startNewFlowPicked} .handler=${this.handler}>
|
||||||
|
<paper-item-body>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.pick_flow_step.new_flow",
|
||||||
|
"integration",
|
||||||
|
domainToName(this.hass.localize, this.handler)
|
||||||
|
)}
|
||||||
|
</paper-item-body>
|
||||||
|
<ha-icon-next></ha-icon-next>
|
||||||
|
</paper-item>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _startNewFlowPicked(ev) {
|
||||||
|
this._startFlow(ev.currentTarget.handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _startFlow(handler: string) {
|
||||||
|
fireEvent(this, "flow-update", {
|
||||||
|
stepPromise: this.flowConfig.createFlow(this.hass, handler),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _flowInProgressPicked(ev) {
|
||||||
|
const flow: DataEntryFlowProgress = ev.currentTarget.flow;
|
||||||
|
fireEvent(this, "flow-update", {
|
||||||
|
stepPromise: this.flowConfig.fetchFlow(this.hass, flow.flow_id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
configFlowContentStyles,
|
||||||
|
css`
|
||||||
|
img {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
ha-icon-next {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 600px;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
padding-right: 66px;
|
||||||
|
}
|
||||||
|
@media all and (max-height: 900px) {
|
||||||
|
div {
|
||||||
|
max-height: calc(100vh - 134px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
paper-icon-item,
|
||||||
|
paper-item {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"step-flow-pick-flow": StepFlowPickFlow;
|
||||||
|
}
|
||||||
|
}
|
@ -22,7 +22,6 @@ import { domainToName } from "../../data/integration";
|
|||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import { brandsUrl } from "../../util/brands-url";
|
import { brandsUrl } from "../../util/brands-url";
|
||||||
import { documentationUrl } from "../../util/documentation-url";
|
import { documentationUrl } from "../../util/documentation-url";
|
||||||
import { FlowConfig } from "./show-dialog-data-entry-flow";
|
|
||||||
import { configFlowContentStyles } from "./styles";
|
import { configFlowContentStyles } from "./styles";
|
||||||
|
|
||||||
interface HandlerObj {
|
interface HandlerObj {
|
||||||
@ -30,17 +29,24 @@ interface HandlerObj {
|
|||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// for fire event
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"handler-picked": {
|
||||||
|
handler: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@customElement("step-flow-pick-handler")
|
@customElement("step-flow-pick-handler")
|
||||||
class StepFlowPickHandler extends LitElement {
|
class StepFlowPickHandler extends LitElement {
|
||||||
public flowConfig!: FlowConfig;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property() public handlers!: string[];
|
@property() public handlers!: string[];
|
||||||
|
|
||||||
@property() public showAdvanced?: boolean;
|
@property() public showAdvanced?: boolean;
|
||||||
|
|
||||||
@internalProperty() private filter?: string;
|
@internalProperty() private _filter?: string;
|
||||||
|
|
||||||
private _width?: number;
|
private _width?: number;
|
||||||
|
|
||||||
@ -74,7 +80,7 @@ class StepFlowPickHandler extends LitElement {
|
|||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const handlers = this._getHandlers(
|
const handlers = this._getHandlers(
|
||||||
this.handlers,
|
this.handlers,
|
||||||
this.filter,
|
this._filter,
|
||||||
this.hass.localize
|
this.hass.localize
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -82,7 +88,7 @@ class StepFlowPickHandler extends LitElement {
|
|||||||
<h2>${this.hass.localize("ui.panel.config.integrations.new")}</h2>
|
<h2>${this.hass.localize("ui.panel.config.integrations.new")}</h2>
|
||||||
<search-input
|
<search-input
|
||||||
autofocus
|
autofocus
|
||||||
.filter=${this.filter}
|
.filter=${this._filter}
|
||||||
@value-changed=${this._filterChanged}
|
@value-changed=${this._filterChanged}
|
||||||
.label=${this.hass.localize("ui.panel.config.integrations.search")}
|
.label=${this.hass.localize("ui.panel.config.integrations.search")}
|
||||||
></search-input>
|
></search-input>
|
||||||
@ -164,15 +170,12 @@ class StepFlowPickHandler extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _filterChanged(e) {
|
private async _filterChanged(e) {
|
||||||
this.filter = e.detail.value;
|
this._filter = e.detail.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _handlerPicked(ev) {
|
private async _handlerPicked(ev) {
|
||||||
fireEvent(this, "flow-update", {
|
fireEvent(this, "handler-picked", {
|
||||||
stepPromise: this.flowConfig.createFlow(
|
handler: ev.currentTarget.handler.slug,
|
||||||
this.hass,
|
|
||||||
ev.currentTarget.handler.slug
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,6 +198,9 @@ class StepFlowPickHandler extends LitElement {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
max-height: 600px;
|
max-height: 600px;
|
||||||
}
|
}
|
||||||
|
h2 {
|
||||||
|
padding-right: 66px;
|
||||||
|
}
|
||||||
@media all and (max-height: 900px) {
|
@media all and (max-height: 900px) {
|
||||||
div {
|
div {
|
||||||
max-height: calc(100vh - 134px);
|
max-height: calc(100vh - 134px);
|
||||||
|
@ -380,22 +380,24 @@ export class QuickBar extends LitElement {
|
|||||||
QuickBarNavigationItem,
|
QuickBarNavigationItem,
|
||||||
"action"
|
"action"
|
||||||
>[] {
|
>[] {
|
||||||
return Object.keys(this.hass.panels).map((panelKey) => {
|
return Object.keys(this.hass.panels)
|
||||||
const panel = this.hass.panels[panelKey];
|
.filter((panelKey) => panelKey !== "_my_redirect")
|
||||||
const translationKey = getPanelNameTranslationKey(panel);
|
.map((panelKey) => {
|
||||||
|
const panel = this.hass.panels[panelKey];
|
||||||
|
const translationKey = getPanelNameTranslationKey(panel);
|
||||||
|
|
||||||
const text = this.hass.localize(
|
const text = this.hass.localize(
|
||||||
"ui.dialogs.quick-bar.commands.navigation.navigate_to",
|
"ui.dialogs.quick-bar.commands.navigation.navigate_to",
|
||||||
"panel",
|
"panel",
|
||||||
this.hass.localize(translationKey) || panel.title || panel.url_path
|
this.hass.localize(translationKey) || panel.title || panel.url_path
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text,
|
text,
|
||||||
icon: getPanelIcon(panel) || DEFAULT_NAVIGATION_ICON,
|
icon: getPanelIcon(panel) || DEFAULT_NAVIGATION_ICON,
|
||||||
path: `/${panel.url_path}`,
|
path: `/${panel.url_path}`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _generateNavigationConfigSectionCommands(): Partial<
|
private _generateNavigationConfigSectionCommands(): Partial<
|
||||||
|
@ -15,7 +15,8 @@ export const demoConfig: HassConfig = {
|
|||||||
time_zone: "America/Los_Angeles",
|
time_zone: "America/Los_Angeles",
|
||||||
config_dir: "/config",
|
config_dir: "/config",
|
||||||
version: "DEMO",
|
version: "DEMO",
|
||||||
whitelist_external_dirs: [],
|
allowlist_external_dirs: [],
|
||||||
|
allowlist_external_urls: [],
|
||||||
config_source: "storage",
|
config_source: "storage",
|
||||||
safe_mode: false,
|
safe_mode: false,
|
||||||
state: STATE_RUNNING,
|
state: STATE_RUNNING,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<meta name='viewport' content='width=device-width, viewport-fit=cover'>
|
<meta name='viewport' content='width=device-width, user-scalable=no, viewport-fit=cover'>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: Roboto, sans-serif;
|
font-family: Roboto, sans-serif;
|
||||||
|
@ -42,7 +42,6 @@ import "./types/ha-automation-action-wait_template";
|
|||||||
const OPTIONS = [
|
const OPTIONS = [
|
||||||
"condition",
|
"condition",
|
||||||
"delay",
|
"delay",
|
||||||
"device_id",
|
|
||||||
"event",
|
"event",
|
||||||
"scene",
|
"scene",
|
||||||
"service",
|
"service",
|
||||||
@ -50,6 +49,7 @@ const OPTIONS = [
|
|||||||
"wait_for_trigger",
|
"wait_for_trigger",
|
||||||
"repeat",
|
"repeat",
|
||||||
"choose",
|
"choose",
|
||||||
|
"device_id",
|
||||||
];
|
];
|
||||||
|
|
||||||
const getType = (action: Action) => {
|
const getType = (action: Action) => {
|
||||||
@ -99,6 +99,8 @@ export default class HaAutomationActionRow extends LitElement {
|
|||||||
|
|
||||||
@property() public totalActions!: number;
|
@property() public totalActions!: number;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
@internalProperty() private _warnings?: string[];
|
@internalProperty() private _warnings?: string[];
|
||||||
|
|
||||||
@internalProperty() private _uiModeAvailable = true;
|
@internalProperty() private _uiModeAvailable = true;
|
||||||
@ -116,8 +118,9 @@ export default class HaAutomationActionRow extends LitElement {
|
|||||||
this._yamlMode = true;
|
this._yamlMode = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._yamlMode && this._yamlEditor) {
|
const yamlEditor = this._yamlEditor;
|
||||||
this._yamlEditor.setValue(this.action);
|
if (this._yamlMode && yamlEditor && yamlEditor.value !== this.action) {
|
||||||
|
yamlEditor.setValue(this.action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,6 +245,7 @@ export default class HaAutomationActionRow extends LitElement {
|
|||||||
${dynamicElement(`ha-automation-action-${type}`, {
|
${dynamicElement(`ha-automation-action-${type}`, {
|
||||||
hass: this.hass,
|
hass: this.hass,
|
||||||
action: this.action,
|
action: this.action,
|
||||||
|
narrow: this.narrow,
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
|
@ -18,6 +18,8 @@ import { HaDeviceAction } from "./types/ha-automation-action-device_id";
|
|||||||
export default class HaAutomationAction extends LitElement {
|
export default class HaAutomationAction extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
@property() public actions!: Action[];
|
@property() public actions!: Action[];
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
@ -28,6 +30,7 @@ export default class HaAutomationAction extends LitElement {
|
|||||||
.index=${idx}
|
.index=${idx}
|
||||||
.totalActions=${this.actions.length}
|
.totalActions=${this.actions.length}
|
||||||
.action=${action}
|
.action=${action}
|
||||||
|
.narrow=${this.narrow}
|
||||||
@duplicate=${this._duplicateAction}
|
@duplicate=${this._duplicateAction}
|
||||||
@move-action=${this._move}
|
@move-action=${this._move}
|
||||||
@value-changed=${this._actionChanged}
|
@value-changed=${this._actionChanged}
|
||||||
|
@ -22,13 +22,17 @@ export class HaDelayAction extends LitElement implements ActionElement {
|
|||||||
let data: HaFormTimeData = {};
|
let data: HaFormTimeData = {};
|
||||||
|
|
||||||
if (typeof this.action.delay !== "object") {
|
if (typeof this.action.delay !== "object") {
|
||||||
const parts = this.action.delay?.toString().split(":") || [];
|
if (isNaN(this.action.delay)) {
|
||||||
data = {
|
const parts = this.action.delay?.toString().split(":") || [];
|
||||||
hours: Number(parts[0]),
|
data = {
|
||||||
minutes: Number(parts[1]),
|
hours: Number(parts[0]) || 0,
|
||||||
seconds: Number(parts[2]),
|
minutes: Number(parts[1]) || 0,
|
||||||
milliseconds: Number(parts[3]),
|
seconds: Number(parts[2]) || 0,
|
||||||
};
|
milliseconds: Number(parts[3]) || 0,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
data = { seconds: this.action.delay };
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const { days, minutes, seconds, milliseconds } = this.action.delay;
|
const { days, minutes, seconds, milliseconds } = this.action.delay;
|
||||||
let { hours } = this.action.delay || 0;
|
let { hours } = this.action.delay || 0;
|
||||||
@ -46,7 +50,8 @@ export class HaDelayAction extends LitElement implements ActionElement {
|
|||||||
.data=${data}
|
.data=${data}
|
||||||
enableMillisecond
|
enableMillisecond
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
></ha-time-input>
|
>
|
||||||
|
</ha-time-input>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,30 +1,26 @@
|
|||||||
import "@polymer/paper-input/paper-input";
|
import "@polymer/paper-input/paper-input";
|
||||||
import {
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
customElement,
|
customElement,
|
||||||
|
internalProperty,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
query,
|
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { html } from "lit-html";
|
import { html } from "lit-html";
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { any, assert, object, optional, string } from "superstruct";
|
import { any, assert, object, optional, string } from "superstruct";
|
||||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
|
||||||
import { computeObjectId } from "../../../../../common/entity/compute_object_id";
|
|
||||||
import "../../../../../components/entity/ha-entity-picker";
|
|
||||||
import "../../../../../components/ha-service-picker";
|
|
||||||
import "../../../../../components/ha-yaml-editor";
|
|
||||||
import type { HaYamlEditor } from "../../../../../components/ha-yaml-editor";
|
|
||||||
import { ServiceAction } from "../../../../../data/script";
|
import { ServiceAction } from "../../../../../data/script";
|
||||||
import type { PolymerChangedEvent } from "../../../../../polymer-types";
|
|
||||||
import type { HomeAssistant } from "../../../../../types";
|
import type { HomeAssistant } from "../../../../../types";
|
||||||
import { EntityIdOrAll } from "../../../../../common/structs/is-entity-id";
|
import { EntityIdOrAll } from "../../../../../common/structs/is-entity-id";
|
||||||
import { ActionElement, handleChangeEvent } from "../ha-automation-action-row";
|
import { ActionElement } from "../ha-automation-action-row";
|
||||||
|
import "../../../../../components/ha-service-control";
|
||||||
|
|
||||||
const actionStruct = object({
|
const actionStruct = object({
|
||||||
service: optional(string()),
|
service: optional(string()),
|
||||||
entity_id: optional(EntityIdOrAll),
|
entity_id: optional(EntityIdOrAll),
|
||||||
|
target: optional(any()),
|
||||||
data: optional(any()),
|
data: optional(any()),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -34,36 +30,14 @@ export class HaServiceAction extends LitElement implements ActionElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public action!: ServiceAction;
|
@property({ attribute: false }) public action!: ServiceAction;
|
||||||
|
|
||||||
@query("ha-yaml-editor", true) private _yamlEditor?: HaYamlEditor;
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
private _actionData?: ServiceAction["data"];
|
@internalProperty() private _action!: ServiceAction;
|
||||||
|
|
||||||
public static get defaultConfig() {
|
public static get defaultConfig() {
|
||||||
return { service: "", data: {} };
|
return { service: "", data: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
private _domain = memoizeOne((service: string) => [computeDomain(service)]);
|
|
||||||
|
|
||||||
private _getServiceData = memoizeOne((service: string) => {
|
|
||||||
if (!service) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const domain = computeDomain(service);
|
|
||||||
const serviceName = computeObjectId(service);
|
|
||||||
const serviceDomains = this.hass.services;
|
|
||||||
if (!(domain in serviceDomains)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
if (!(serviceName in serviceDomains[domain])) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const fields = serviceDomains[domain][serviceName].fields;
|
|
||||||
return Object.keys(fields).map((field) => {
|
|
||||||
return { key: field, ...fields[field] };
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
protected updated(changedProperties: PropertyValues) {
|
protected updated(changedProperties: PropertyValues) {
|
||||||
if (!changedProperties.has("action")) {
|
if (!changedProperties.has("action")) {
|
||||||
return;
|
return;
|
||||||
@ -73,73 +47,42 @@ export class HaServiceAction extends LitElement implements ActionElement {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
fireEvent(this, "ui-mode-not-available", error);
|
fireEvent(this, "ui-mode-not-available", error);
|
||||||
}
|
}
|
||||||
if (this._actionData && this._actionData !== this.action.data) {
|
if (this.action.entity_id) {
|
||||||
if (this._yamlEditor) {
|
this._action = {
|
||||||
this._yamlEditor.setValue(this.action.data);
|
...this.action,
|
||||||
}
|
data: { ...this.action.data, entity_id: this.action.entity_id },
|
||||||
|
};
|
||||||
|
delete this._action.entity_id;
|
||||||
|
} else {
|
||||||
|
this._action = this.action;
|
||||||
}
|
}
|
||||||
this._actionData = this.action.data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const { service, data, entity_id } = this.action;
|
|
||||||
|
|
||||||
const serviceData = this._getServiceData(service);
|
|
||||||
const entity = serviceData.find((attr) => attr.key === "entity_id");
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-service-picker
|
<ha-service-control
|
||||||
|
.narrow=${this.narrow}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${service}
|
.value=${this._action}
|
||||||
@value-changed=${this._serviceChanged}
|
.showAdvanced=${this.hass.userData?.showAdvanced}
|
||||||
></ha-service-picker>
|
@value-changed=${this._actionChanged}
|
||||||
${entity
|
></ha-service-control>
|
||||||
? html`
|
|
||||||
<ha-entity-picker
|
|
||||||
.hass=${this.hass}
|
|
||||||
.value=${entity_id}
|
|
||||||
.label=${entity.description}
|
|
||||||
@value-changed=${this._entityPicked}
|
|
||||||
.includeDomains=${this._domain(service)}
|
|
||||||
allow-custom-entity
|
|
||||||
></ha-entity-picker>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
<ha-yaml-editor
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.automation.editor.actions.type.service.service_data"
|
|
||||||
)}
|
|
||||||
.name=${"data"}
|
|
||||||
.defaultValue=${data}
|
|
||||||
@value-changed=${this._dataChanged}
|
|
||||||
></ha-yaml-editor>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _dataChanged(ev: CustomEvent): void {
|
private _actionChanged(ev) {
|
||||||
ev.stopPropagation();
|
if (ev.detail.value === this._action) {
|
||||||
if (!ev.detail.isValid) {
|
ev.stopPropagation();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
this._actionData = ev.detail.value;
|
|
||||||
handleChangeEvent(this, ev);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _serviceChanged(ev: PolymerChangedEvent<string>) {
|
static get styles(): CSSResult {
|
||||||
ev.stopPropagation();
|
return css`
|
||||||
if (ev.detail.value === this.action.service) {
|
ha-service-control {
|
||||||
return;
|
display: block;
|
||||||
}
|
margin: 0 -16px;
|
||||||
fireEvent(this, "value-changed", {
|
}
|
||||||
value: { ...this.action, service: ev.detail.value },
|
`;
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _entityPicked(ev: PolymerChangedEvent<string>) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
fireEvent(this, "value-changed", {
|
|
||||||
value: { ...this.action, entity_id: ev.detail.value },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,10 +252,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
|||||||
if (!name) {
|
if (!name) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let newVal = ev.detail.value;
|
const newVal = ev.detail.value;
|
||||||
if (target.type === "number") {
|
|
||||||
newVal = Number(newVal);
|
|
||||||
}
|
|
||||||
if ((this.config![name] || "") === newVal) {
|
if ((this.config![name] || "") === newVal) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ export class HaManualAutomationEditor extends LitElement {
|
|||||||
@property() public stateObj?: HassEntity;
|
@property() public stateObj?: HassEntity;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`<ha-config-section .isWide=${this.isWide}>
|
return html`<ha-config-section vertical .isWide=${this.isWide}>
|
||||||
${!this.narrow
|
${!this.narrow
|
||||||
? html` <span slot="header">${this.config.alias}</span> `
|
? html` <span slot="header">${this.config.alias}</span> `
|
||||||
: ""}
|
: ""}
|
||||||
@ -151,7 +151,7 @@ export class HaManualAutomationEditor extends LitElement {
|
|||||||
</ha-card>
|
</ha-card>
|
||||||
</ha-config-section>
|
</ha-config-section>
|
||||||
|
|
||||||
<ha-config-section .isWide=${this.isWide}>
|
<ha-config-section vertical .isWide=${this.isWide}>
|
||||||
<span slot="header">
|
<span slot="header">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.automation.editor.triggers.header"
|
"ui.panel.config.automation.editor.triggers.header"
|
||||||
@ -180,7 +180,7 @@ export class HaManualAutomationEditor extends LitElement {
|
|||||||
></ha-automation-trigger>
|
></ha-automation-trigger>
|
||||||
</ha-config-section>
|
</ha-config-section>
|
||||||
|
|
||||||
<ha-config-section .isWide=${this.isWide}>
|
<ha-config-section vertical .isWide=${this.isWide}>
|
||||||
<span slot="header">
|
<span slot="header">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.automation.editor.conditions.header"
|
"ui.panel.config.automation.editor.conditions.header"
|
||||||
@ -209,7 +209,7 @@ export class HaManualAutomationEditor extends LitElement {
|
|||||||
></ha-automation-condition>
|
></ha-automation-condition>
|
||||||
</ha-config-section>
|
</ha-config-section>
|
||||||
|
|
||||||
<ha-config-section .isWide=${this.isWide}>
|
<ha-config-section vertical .isWide=${this.isWide}>
|
||||||
<span slot="header">
|
<span slot="header">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.automation.editor.actions.header"
|
"ui.panel.config.automation.editor.actions.header"
|
||||||
@ -235,6 +235,7 @@ export class HaManualAutomationEditor extends LitElement {
|
|||||||
.actions=${this.config.action}
|
.actions=${this.config.action}
|
||||||
@value-changed=${this._actionChanged}
|
@value-changed=${this._actionChanged}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
.narrow=${this.narrow}
|
||||||
></ha-automation-action>
|
></ha-automation-action>
|
||||||
</ha-config-section>`;
|
</ha-config-section>`;
|
||||||
}
|
}
|
||||||
|
@ -211,7 +211,7 @@ class HaBlueprintOverview extends LitElement {
|
|||||||
"ui.panel.config.blueprint.overview.add_blueprint"
|
"ui.panel.config.blueprint.overview.add_blueprint"
|
||||||
)}
|
)}
|
||||||
extended
|
extended
|
||||||
@click=${this._addBlueprint}
|
@click=${this._addBlueprintClicked}
|
||||||
>
|
>
|
||||||
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon>
|
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon>
|
||||||
</ha-fab>
|
</ha-fab>
|
||||||
@ -249,6 +249,10 @@ class HaBlueprintOverview extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _addBlueprintClicked(): void {
|
||||||
|
this._addBlueprint();
|
||||||
|
}
|
||||||
|
|
||||||
private _reload() {
|
private _reload() {
|
||||||
fireEvent(this, "reload-blueprints");
|
fireEvent(this, "reload-blueprints");
|
||||||
}
|
}
|
||||||
|
@ -80,13 +80,16 @@ export class HaConfigSection extends LitElement {
|
|||||||
font-weight: var(--paper-font-subhead_-_font-weight);
|
font-weight: var(--paper-font-subhead_-_font-weight);
|
||||||
line-height: var(--paper-font-subhead_-_line-height);
|
line-height: var(--paper-font-subhead_-_line-height);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
|
||||||
margin-right: 40px;
|
|
||||||
opacity: var(--dark-primary-opacity);
|
opacity: var(--dark-primary-opacity);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.horizontal .intro {
|
||||||
|
max-width: 400px;
|
||||||
|
margin-right: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
margin-top: -24px;
|
margin-top: -24px;
|
||||||
}
|
}
|
||||||
|
@ -221,7 +221,7 @@ export class HaSceneEditor extends SubscribeMixin(
|
|||||||
>
|
>
|
||||||
${this._config
|
${this._config
|
||||||
? html`
|
? html`
|
||||||
<ha-config-section .isWide=${this.isWide}>
|
<ha-config-section vertical .isWide=${this.isWide}>
|
||||||
${!this.narrow
|
${!this.narrow
|
||||||
? html` <span slot="header">${name}</span> `
|
? html` <span slot="header">${name}</span> `
|
||||||
: ""}
|
: ""}
|
||||||
@ -253,7 +253,7 @@ export class HaSceneEditor extends SubscribeMixin(
|
|||||||
</ha-card>
|
</ha-card>
|
||||||
</ha-config-section>
|
</ha-config-section>
|
||||||
|
|
||||||
<ha-config-section .isWide=${this.isWide}>
|
<ha-config-section vertical .isWide=${this.isWide}>
|
||||||
<div slot="header">
|
<div slot="header">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.scene.editor.devices.header"
|
"ui.panel.config.scene.editor.devices.header"
|
||||||
@ -324,7 +324,7 @@ export class HaSceneEditor extends SubscribeMixin(
|
|||||||
|
|
||||||
${this.showAdvanced
|
${this.showAdvanced
|
||||||
? html`
|
? html`
|
||||||
<ha-config-section .isWide=${this.isWide}>
|
<ha-config-section vertical .isWide=${this.isWide}>
|
||||||
<div slot="header">
|
<div slot="header">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.scene.editor.entities.header"
|
"ui.panel.config.scene.editor.entities.header"
|
||||||
|
@ -189,7 +189,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
|||||||
>
|
>
|
||||||
${this._config
|
${this._config
|
||||||
? html`
|
? html`
|
||||||
<ha-config-section .isWide=${this.isWide}>
|
<ha-config-section vertical .isWide=${this.isWide}>
|
||||||
${!this.narrow
|
${!this.narrow
|
||||||
? html`
|
? html`
|
||||||
<span slot="header">${this._config.alias}</span>
|
<span slot="header">${this._config.alias}</span>
|
||||||
@ -313,7 +313,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
|||||||
</ha-card>
|
</ha-card>
|
||||||
</ha-config-section>
|
</ha-config-section>
|
||||||
|
|
||||||
<ha-config-section .isWide=${this.isWide}>
|
<ha-config-section vertical .isWide=${this.isWide}>
|
||||||
<span slot="header">
|
<span slot="header">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.script.editor.sequence"
|
"ui.panel.config.script.editor.sequence"
|
||||||
@ -350,7 +350,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
|||||||
`
|
`
|
||||||
: this._mode === "yaml"
|
: this._mode === "yaml"
|
||||||
? html`
|
? html`
|
||||||
<ha-config-section .isWide=${false}>
|
<ha-config-section vertical .isWide=${false}>
|
||||||
${!this.narrow
|
${!this.narrow
|
||||||
? html`<span slot="header">${this._config?.alias}</span>`
|
? html`<span slot="header">${this._config?.alias}</span>`
|
||||||
: ``}
|
: ``}
|
||||||
|
@ -1,371 +0,0 @@
|
|||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
/* eslint-plugin-disable lit */
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
import { safeDump, safeLoad } from "js-yaml";
|
|
||||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
|
||||||
import "../../../components/buttons/ha-progress-button";
|
|
||||||
import "../../../components/entity/ha-entity-picker";
|
|
||||||
import "../../../components/ha-card";
|
|
||||||
import "../../../components/ha-code-editor";
|
|
||||||
import "../../../components/ha-service-picker";
|
|
||||||
import { ENTITY_COMPONENT_DOMAINS } from "../../../data/entity";
|
|
||||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
|
||||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
|
||||||
import "../../../styles/polymer-ha-style";
|
|
||||||
import "../../../util/app-localstorage-document";
|
|
||||||
|
|
||||||
const ERROR_SENTINEL = {};
|
|
||||||
/*
|
|
||||||
* @appliesMixin LocalizeMixin
|
|
||||||
*/
|
|
||||||
class HaPanelDevService extends LocalizeMixin(PolymerElement) {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style include="ha-style">
|
|
||||||
:host {
|
|
||||||
-ms-user-select: initial;
|
|
||||||
-webkit-user-select: initial;
|
|
||||||
-moz-user-select: initial;
|
|
||||||
display: block;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ha-form {
|
|
||||||
margin-right: 16px;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-progress-button {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-card {
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
margin-top: 12px;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attributes th {
|
|
||||||
text-align: left;
|
|
||||||
background-color: var(--card-background-color);
|
|
||||||
border-bottom: 1px solid var(--primary-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([rtl]) .attributes th {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attributes tr {
|
|
||||||
vertical-align: top;
|
|
||||||
direction: ltr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attributes tr:nth-child(odd) {
|
|
||||||
background-color: var(--table-row-background-color, #eee);
|
|
||||||
}
|
|
||||||
|
|
||||||
.attributes tr:nth-child(even) {
|
|
||||||
background-color: var(--table-row-alternative-background-color, #eee);
|
|
||||||
}
|
|
||||||
|
|
||||||
.attributes td:nth-child(3) {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
margin: 0;
|
|
||||||
font-family: var(--code-font-family, monospace);
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: var(--error-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([rtl]) .desc-container {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([rtl]) .desc-container h3 {
|
|
||||||
direction: ltr;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<app-localstorage-document
|
|
||||||
key="panel-dev-service-state-domain-service"
|
|
||||||
data="{{domainService}}"
|
|
||||||
>
|
|
||||||
</app-localstorage-document>
|
|
||||||
<app-localstorage-document
|
|
||||||
key="[[_computeServiceDataKey(domainService)]]"
|
|
||||||
data="{{serviceData}}"
|
|
||||||
>
|
|
||||||
</app-localstorage-document>
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
<p>
|
|
||||||
[[localize('ui.panel.developer-tools.tabs.services.description')]]
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="ha-form">
|
|
||||||
<ha-service-picker
|
|
||||||
hass="[[hass]]"
|
|
||||||
value="{{domainService}}"
|
|
||||||
></ha-service-picker>
|
|
||||||
<template is="dom-if" if="[[_computeHasEntity(_attributes)]]">
|
|
||||||
<ha-entity-picker
|
|
||||||
hass="[[hass]]"
|
|
||||||
value="[[_computeEntityValue(parsedJSON)]]"
|
|
||||||
on-change="_entityPicked"
|
|
||||||
disabled="[[!validJSON]]"
|
|
||||||
include-domains="[[_computeEntityDomainFilter(_domain)]]"
|
|
||||||
allow-custom-entity
|
|
||||||
></ha-entity-picker>
|
|
||||||
</template>
|
|
||||||
<p>[[localize('ui.panel.developer-tools.tabs.services.data')]]</p>
|
|
||||||
<ha-code-editor
|
|
||||||
mode="yaml"
|
|
||||||
value="[[serviceData]]"
|
|
||||||
error="[[!validJSON]]"
|
|
||||||
on-value-changed="_yamlChanged"
|
|
||||||
></ha-code-editor>
|
|
||||||
<ha-progress-button
|
|
||||||
on-click="_callService"
|
|
||||||
raised
|
|
||||||
disabled="[[!validJSON]]"
|
|
||||||
>
|
|
||||||
[[localize('ui.panel.developer-tools.tabs.services.call_service')]]
|
|
||||||
</ha-progress-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ha-card>
|
|
||||||
<div class="card-header">
|
|
||||||
<template is="dom-if" if="[[!domainService]]">
|
|
||||||
[[localize('ui.panel.developer-tools.tabs.services.select_service')]]
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template is="dom-if" if="[[domainService]]">
|
|
||||||
<template is="dom-if" if="[[!_description]]">
|
|
||||||
[[localize('ui.panel.developer-tools.tabs.services.no_description')]]
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[_description]]">
|
|
||||||
[[_description]]
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="card-content">
|
|
||||||
<template is="dom-if" if="[[_description]]">
|
|
||||||
<template is="dom-if" if="[[!_attributes.length]]">
|
|
||||||
[[localize('ui.panel.developer-tools.tabs.services.no_parameters')]]
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template is="dom-if" if="[[_attributes.length]]">
|
|
||||||
<table class="attributes">
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
[[localize('ui.panel.developer-tools.tabs.services.column_parameter')]]
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
[[localize('ui.panel.developer-tools.tabs.services.column_description')]]
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
[[localize('ui.panel.developer-tools.tabs.services.column_example')]]
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
<template is="dom-repeat" items="[[_attributes]]" as="attribute">
|
|
||||||
<tr>
|
|
||||||
<td><pre>[[attribute.key]]</pre></td>
|
|
||||||
<td>[[attribute.description]]</td>
|
|
||||||
<td>[[attribute.example]]</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</table>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template is="dom-if" if="[[_attributes.length]]">
|
|
||||||
<mwc-button on-click="_fillExampleData">
|
|
||||||
[[localize('ui.panel.developer-tools.tabs.services.fill_example_data')]]
|
|
||||||
</mwc-button>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
|
|
||||||
domainService: {
|
|
||||||
type: String,
|
|
||||||
observer: "_domainServiceChanged",
|
|
||||||
},
|
|
||||||
|
|
||||||
_domain: {
|
|
||||||
type: String,
|
|
||||||
computed: "_computeDomain(domainService)",
|
|
||||||
},
|
|
||||||
|
|
||||||
_service: {
|
|
||||||
type: String,
|
|
||||||
computed: "_computeService(domainService)",
|
|
||||||
},
|
|
||||||
|
|
||||||
serviceData: {
|
|
||||||
type: String,
|
|
||||||
value: "",
|
|
||||||
},
|
|
||||||
|
|
||||||
parsedJSON: {
|
|
||||||
type: Object,
|
|
||||||
computed: "_computeParsedServiceData(serviceData)",
|
|
||||||
},
|
|
||||||
|
|
||||||
validJSON: {
|
|
||||||
type: Boolean,
|
|
||||||
computed: "_computeValidJSON(parsedJSON)",
|
|
||||||
},
|
|
||||||
|
|
||||||
_attributes: {
|
|
||||||
type: Array,
|
|
||||||
computed: "_computeAttributesArray(hass, _domain, _service)",
|
|
||||||
},
|
|
||||||
|
|
||||||
_description: {
|
|
||||||
type: String,
|
|
||||||
computed: "_computeDescription(hass, _domain, _service)",
|
|
||||||
},
|
|
||||||
|
|
||||||
rtl: {
|
|
||||||
reflectToAttribute: true,
|
|
||||||
computed: "_computeRTL(hass)",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_domainServiceChanged() {
|
|
||||||
this.serviceData = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeAttributesArray(hass, domain, service) {
|
|
||||||
const serviceDomains = hass.services;
|
|
||||||
if (!(domain in serviceDomains)) return [];
|
|
||||||
if (!(service in serviceDomains[domain])) return [];
|
|
||||||
|
|
||||||
const fields = serviceDomains[domain][service].fields;
|
|
||||||
return Object.keys(fields).map(function (field) {
|
|
||||||
return { key: field, ...fields[field] };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeDescription(hass, domain, service) {
|
|
||||||
const serviceDomains = hass.services;
|
|
||||||
if (!(domain in serviceDomains)) return undefined;
|
|
||||||
if (!(service in serviceDomains[domain])) return undefined;
|
|
||||||
return serviceDomains[domain][service].description;
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeServiceDataKey(domainService) {
|
|
||||||
return `panel-dev-service-state-servicedata.${domainService}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeDomain(domainService) {
|
|
||||||
return domainService.split(".", 1)[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeService(domainService) {
|
|
||||||
return domainService.split(".", 2)[1] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeParsedServiceData(serviceData) {
|
|
||||||
try {
|
|
||||||
return serviceData.trim() ? safeLoad(serviceData) : {};
|
|
||||||
} catch (err) {
|
|
||||||
return ERROR_SENTINEL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeValidJSON(parsedJSON) {
|
|
||||||
return parsedJSON !== ERROR_SENTINEL;
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeHasEntity(attributes) {
|
|
||||||
return attributes.some((attr) => attr.key === "entity_id");
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeEntityValue(parsedJSON) {
|
|
||||||
return parsedJSON === ERROR_SENTINEL ? "" : parsedJSON.entity_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeEntityDomainFilter(domain) {
|
|
||||||
return ENTITY_COMPONENT_DOMAINS.includes(domain) ? [domain] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_callService(ev) {
|
|
||||||
const button = ev.target;
|
|
||||||
if (this.parsedJSON === ERROR_SENTINEL) {
|
|
||||||
showAlertDialog(this, {
|
|
||||||
text: this.hass.localize(
|
|
||||||
"ui.panel.developer-tools.tabs.services.alert_parsing_yaml",
|
|
||||||
"data",
|
|
||||||
this.serviceData
|
|
||||||
),
|
|
||||||
});
|
|
||||||
button.actionError();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.hass
|
|
||||||
.callService(this._domain, this._service, this.parsedJSON)
|
|
||||||
.then(() => {
|
|
||||||
button.actionSuccess();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
button.actionError();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_fillExampleData() {
|
|
||||||
const example = {};
|
|
||||||
this._attributes.forEach((attribute) => {
|
|
||||||
if (attribute.example) {
|
|
||||||
let value = "";
|
|
||||||
try {
|
|
||||||
value = safeLoad(attribute.example);
|
|
||||||
} catch (err) {
|
|
||||||
value = attribute.example;
|
|
||||||
}
|
|
||||||
example[attribute.key] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.serviceData = safeDump(example);
|
|
||||||
}
|
|
||||||
|
|
||||||
_entityPicked(ev) {
|
|
||||||
this.serviceData = safeDump({
|
|
||||||
...this.parsedJSON,
|
|
||||||
entity_id: ev.target.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_yamlChanged(ev) {
|
|
||||||
this.serviceData = ev.detail.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeRTL(hass) {
|
|
||||||
return computeRTL(hass);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("developer-tools-service", HaPanelDevService);
|
|
350
src/panels/developer-tools/service/developer-tools-service.ts
Normal file
350
src/panels/developer-tools/service/developer-tools-service.ts
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
import { safeLoad } from "js-yaml";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResultArray,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
query,
|
||||||
|
} from "lit-element";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { LocalStorage } from "../../../common/decorators/local-storage";
|
||||||
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
|
import { computeObjectId } from "../../../common/entity/compute_object_id";
|
||||||
|
import "../../../components/buttons/ha-progress-button";
|
||||||
|
import "../../../components/entity/ha-entity-picker";
|
||||||
|
import "../../../components/ha-card";
|
||||||
|
import "../../../components/ha-expansion-panel";
|
||||||
|
import "../../../components/ha-service-control";
|
||||||
|
import "../../../components/ha-service-picker";
|
||||||
|
import "../../../components/ha-yaml-editor";
|
||||||
|
import type { HaYamlEditor } from "../../../components/ha-yaml-editor";
|
||||||
|
import { ServiceAction } from "../../../data/script";
|
||||||
|
import { haStyle } from "../../../resources/styles";
|
||||||
|
import "../../../styles/polymer-ha-style";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import "../../../util/app-localstorage-document";
|
||||||
|
|
||||||
|
class HaPanelDevService extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public narrow!: boolean;
|
||||||
|
|
||||||
|
@LocalStorage("panel-dev-service-state-service-data", true)
|
||||||
|
private _serviceData?: ServiceAction = { service: "", target: {}, data: {} };
|
||||||
|
|
||||||
|
@LocalStorage("panel-dev-service-state-yaml-mode", true)
|
||||||
|
private _yamlMode = false;
|
||||||
|
|
||||||
|
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
|
||||||
|
|
||||||
|
protected firstUpdated(params) {
|
||||||
|
super.firstUpdated(params);
|
||||||
|
if (!this._serviceData?.service) {
|
||||||
|
const domain = Object.keys(this.hass.services).sort()[0];
|
||||||
|
const service = Object.keys(this.hass.services[domain]).sort()[0];
|
||||||
|
this._serviceData = {
|
||||||
|
service: `${domain}.${service}`,
|
||||||
|
target: {},
|
||||||
|
data: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
const { target, fields } = this._fields(
|
||||||
|
this.hass.services,
|
||||||
|
this._serviceData?.service
|
||||||
|
);
|
||||||
|
|
||||||
|
const isValid = this._isValid(this._serviceData, fields, target);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="content">
|
||||||
|
<p>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.developer-tools.tabs.services.description"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
${this._yamlMode
|
||||||
|
? html`<ha-yaml-editor
|
||||||
|
.defaultValue=${this._serviceData}
|
||||||
|
@value-changed=${this._yamlChanged}
|
||||||
|
></ha-yaml-editor>`
|
||||||
|
: html`<ha-card
|
||||||
|
><div>
|
||||||
|
<ha-service-control
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${this._serviceData}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
showAdvanced
|
||||||
|
@value-changed=${this._serviceChanged}
|
||||||
|
></ha-service-control></div
|
||||||
|
></ha-card>`}
|
||||||
|
</div>
|
||||||
|
<div class="button-row">
|
||||||
|
<div class="buttons">
|
||||||
|
<mwc-button @click=${this._toggleYaml}>
|
||||||
|
${this._yamlMode
|
||||||
|
? this.hass.localize(
|
||||||
|
"ui.panel.developer-tools.tabs.services.ui_mode"
|
||||||
|
)
|
||||||
|
: this.hass.localize(
|
||||||
|
"ui.panel.developer-tools.tabs.services.yaml_mode"
|
||||||
|
)}
|
||||||
|
</mwc-button>
|
||||||
|
<mwc-button .disabled=${!isValid} raised @click=${this._callService}>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.developer-tools.tabs.services.call_service"
|
||||||
|
)}
|
||||||
|
</mwc-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${(this._yamlMode ? fields : this._filterSelectorFields(fields)).length
|
||||||
|
? html`<div class="content">
|
||||||
|
<ha-expansion-panel
|
||||||
|
.header=${this._yamlMode
|
||||||
|
? this.hass.localize(
|
||||||
|
"ui.panel.developer-tools.tabs.services.all_parameters"
|
||||||
|
)
|
||||||
|
: this.hass.localize(
|
||||||
|
"ui.panel.developer-tools.tabs.services.yaml_parameters"
|
||||||
|
)}
|
||||||
|
outlined
|
||||||
|
.expanded=${this._yamlMode}
|
||||||
|
>
|
||||||
|
${this._yamlMode && target
|
||||||
|
? html`<h3>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.developer-tools.tabs.services.accepts_target"
|
||||||
|
)}
|
||||||
|
</h3>`
|
||||||
|
: ""}
|
||||||
|
<table class="attributes">
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.developer-tools.tabs.services.column_parameter"
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.developer-tools.tabs.services.column_description"
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.developer-tools.tabs.services.column_example"
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
${fields.map(
|
||||||
|
(field) => html` <tr>
|
||||||
|
<td><pre>${field.key}</pre></td>
|
||||||
|
<td>${field.description}</td>
|
||||||
|
<td>${field.example}</td>
|
||||||
|
</tr>`
|
||||||
|
)}
|
||||||
|
</table>
|
||||||
|
${this._yamlMode
|
||||||
|
? html`<mwc-button @click=${this._fillExampleData}
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.developer-tools.tabs.services.fill_example_data"
|
||||||
|
)}</mwc-button
|
||||||
|
>`
|
||||||
|
: ""}
|
||||||
|
</ha-expansion-panel>
|
||||||
|
</div>`
|
||||||
|
: ""}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _filterSelectorFields = memoizeOne((fields) =>
|
||||||
|
fields.filter((field) => !field.selector)
|
||||||
|
);
|
||||||
|
|
||||||
|
private _isValid = memoizeOne((serviceData, fields, target): boolean => {
|
||||||
|
if (!serviceData?.service) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const domain = computeDomain(serviceData.service);
|
||||||
|
const service = computeObjectId(serviceData.service);
|
||||||
|
if (!domain || !service) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
target &&
|
||||||
|
!serviceData.target &&
|
||||||
|
!serviceData.data?.entity_id &&
|
||||||
|
!serviceData.data?.device_id &&
|
||||||
|
!serviceData.data?.area_id
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const field of fields) {
|
||||||
|
if (
|
||||||
|
field.required &&
|
||||||
|
(!serviceData.data || serviceData.data[field.key] === undefined)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
private _fields = memoizeOne(
|
||||||
|
(
|
||||||
|
serviceDomains: HomeAssistant["services"],
|
||||||
|
domainService: string | undefined
|
||||||
|
): { target: boolean; fields: any[] } => {
|
||||||
|
if (!domainService) {
|
||||||
|
return { target: false, fields: [] };
|
||||||
|
}
|
||||||
|
const domain = computeDomain(domainService);
|
||||||
|
const service = computeObjectId(domainService);
|
||||||
|
if (!(domain in serviceDomains)) {
|
||||||
|
return { target: false, fields: [] };
|
||||||
|
}
|
||||||
|
if (!(service in serviceDomains[domain])) {
|
||||||
|
return { target: false, fields: [] };
|
||||||
|
}
|
||||||
|
const target = "target" in serviceDomains[domain][service];
|
||||||
|
const fields = serviceDomains[domain][service].fields;
|
||||||
|
const result = Object.keys(fields).map((field) => {
|
||||||
|
return { key: field, ...fields[field] };
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
target,
|
||||||
|
fields: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private _callService() {
|
||||||
|
const domain = computeDomain(this._serviceData!.service);
|
||||||
|
const service = computeObjectId(this._serviceData!.service);
|
||||||
|
if (!domain || !service) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.hass.callService(
|
||||||
|
domain,
|
||||||
|
service,
|
||||||
|
this._serviceData!.data,
|
||||||
|
this._serviceData!.target
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _toggleYaml() {
|
||||||
|
this._yamlMode = !this._yamlMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _yamlChanged(ev) {
|
||||||
|
if (!ev.detail.isValid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._serviceChanged(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _serviceChanged(ev) {
|
||||||
|
this._serviceData = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _fillExampleData() {
|
||||||
|
const { fields } = this._fields(
|
||||||
|
this.hass.services,
|
||||||
|
this._serviceData?.service
|
||||||
|
);
|
||||||
|
const example = {};
|
||||||
|
fields.forEach((field) => {
|
||||||
|
if (field.example) {
|
||||||
|
let value = "";
|
||||||
|
try {
|
||||||
|
value = safeLoad(field.example);
|
||||||
|
} catch (err) {
|
||||||
|
value = field.example;
|
||||||
|
}
|
||||||
|
example[field.key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this._serviceData = { ...this._serviceData!, data: example };
|
||||||
|
this._yamlEditor?.setValue(this._serviceData);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultArray {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
css`
|
||||||
|
.content {
|
||||||
|
padding: 16px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
.button-row {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-top: 1px solid var(--divider-color);
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
background: var(--card-background-color);
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-row .buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributes {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributes th {
|
||||||
|
text-align: left;
|
||||||
|
background-color: var(--card-background-color);
|
||||||
|
border-bottom: 1px solid var(--primary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([rtl]) .attributes th {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributes tr {
|
||||||
|
vertical-align: top;
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributes tr:nth-child(odd) {
|
||||||
|
background-color: var(--table-row-background-color, #eee);
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributes tr:nth-child(even) {
|
||||||
|
background-color: var(--table-row-alternative-background-color, #eee);
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributes td:nth-child(3) {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributes td {
|
||||||
|
padding: 4px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("developer-tools-service", HaPanelDevService);
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"developer-tools-service": HaPanelDevService;
|
||||||
|
}
|
||||||
|
}
|
@ -272,6 +272,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
|||||||
height: auto;
|
height: auto;
|
||||||
color: var(--paper-item-icon-color, #44739e);
|
color: var(--paper-item-icon-color, #44739e);
|
||||||
--mdc-icon-size: 100%;
|
--mdc-icon-size: 100%;
|
||||||
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-icon,
|
ha-icon,
|
||||||
|
@ -130,7 +130,12 @@ export const handleAction = async (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const [domain, service] = actionConfig.service.split(".", 2);
|
const [domain, service] = actionConfig.service.split(".", 2);
|
||||||
hass.callService(domain, service, actionConfig.service_data);
|
hass.callService(
|
||||||
|
domain,
|
||||||
|
service,
|
||||||
|
actionConfig.service_data,
|
||||||
|
actionConfig.target
|
||||||
|
);
|
||||||
forwardHaptic("light");
|
forwardHaptic("light");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -15,15 +15,17 @@ import {
|
|||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import "../../../components/ha-help-tooltip";
|
import "../../../components/ha-help-tooltip";
|
||||||
import "../../../components/ha-service-picker";
|
|
||||||
import {
|
import {
|
||||||
ActionConfig,
|
ActionConfig,
|
||||||
CallServiceActionConfig,
|
CallServiceActionConfig,
|
||||||
NavigateActionConfig,
|
NavigateActionConfig,
|
||||||
UrlActionConfig,
|
UrlActionConfig,
|
||||||
} from "../../../data/lovelace";
|
} from "../../../data/lovelace";
|
||||||
|
import { ServiceAction } from "../../../data/script";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { EditorTarget } from "../editor/types";
|
import { EditorTarget } from "../editor/types";
|
||||||
|
import "../../../components/ha-service-control";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
|
||||||
@customElement("hui-action-editor")
|
@customElement("hui-action-editor")
|
||||||
export class HuiActionEditor extends LitElement {
|
export class HuiActionEditor extends LitElement {
|
||||||
@ -47,10 +49,15 @@ export class HuiActionEditor extends LitElement {
|
|||||||
return config.url_path || "";
|
return config.url_path || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
get _service(): string {
|
private _serviceAction = memoizeOne(
|
||||||
const config = this.config as CallServiceActionConfig;
|
(config: CallServiceActionConfig): ServiceAction => {
|
||||||
return config.service || "";
|
return {
|
||||||
}
|
service: config.service || "",
|
||||||
|
data: config.service_data,
|
||||||
|
target: config.target,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this.hass || !this.actions) {
|
if (!this.hass || !this.actions) {
|
||||||
@ -117,17 +124,13 @@ export class HuiActionEditor extends LitElement {
|
|||||||
: ""}
|
: ""}
|
||||||
${this.config?.action === "call-service"
|
${this.config?.action === "call-service"
|
||||||
? html`
|
? html`
|
||||||
<ha-service-picker
|
<ha-service-control
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this._service}
|
.value=${this._serviceAction(this.config)}
|
||||||
.configValue=${"service"}
|
.showAdvanced=${this.hass.userData?.showAdvanced}
|
||||||
@value-changed=${this._valueChanged}
|
narrow
|
||||||
></ha-service-picker>
|
@value-changed=${this._serviceValueChanged}
|
||||||
<b>
|
></ha-service-control>
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.panel.lovelace.editor.action-editor.editor_service_data"
|
|
||||||
)}
|
|
||||||
</b>
|
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
`;
|
`;
|
||||||
@ -174,11 +177,26 @@ export class HuiActionEditor extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _serviceValueChanged(ev: CustomEvent) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: {
|
||||||
|
...this.config!,
|
||||||
|
service: ev.detail.value.service || "",
|
||||||
|
service_data: ev.detail.value.data || {},
|
||||||
|
target: ev.detail.value.target || {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResult {
|
static get styles(): CSSResult {
|
||||||
return css`
|
return css`
|
||||||
.dropdown {
|
.dropdown {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
ha-service-control {
|
||||||
|
--service-control-padding: 0;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,11 @@ export const configElementStyle = css`
|
|||||||
}
|
}
|
||||||
.side-by-side > * {
|
.side-by-side > * {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding-right: 4px;
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
.side-by-side > *:last-child {
|
||||||
|
flex: 1;
|
||||||
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
.suffix {
|
.suffix {
|
||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
|
@ -120,20 +120,27 @@ const actionConfigStructConfirmation = union([
|
|||||||
|
|
||||||
const actionConfigStructUrl = object({
|
const actionConfigStructUrl = object({
|
||||||
action: literal("url"),
|
action: literal("url"),
|
||||||
url_path: string(),
|
url_path: optional(string()),
|
||||||
confirmation: optional(actionConfigStructConfirmation),
|
confirmation: optional(actionConfigStructConfirmation),
|
||||||
});
|
});
|
||||||
|
|
||||||
const actionConfigStructService = object({
|
const actionConfigStructService = object({
|
||||||
action: literal("call-service"),
|
action: literal("call-service"),
|
||||||
service: string(),
|
service: optional(string()),
|
||||||
service_data: optional(object()),
|
service_data: optional(object()),
|
||||||
|
target: optional(
|
||||||
|
object({
|
||||||
|
entity_id: optional(union([string(), array(string())])),
|
||||||
|
device_id: optional(union([string(), array(string())])),
|
||||||
|
area_id: optional(union([string(), array(string())])),
|
||||||
|
})
|
||||||
|
),
|
||||||
confirmation: optional(actionConfigStructConfirmation),
|
confirmation: optional(actionConfigStructConfirmation),
|
||||||
});
|
});
|
||||||
|
|
||||||
const actionConfigStructNavigate = object({
|
const actionConfigStructNavigate = object({
|
||||||
action: literal("navigate"),
|
action: literal("navigate"),
|
||||||
navigation_path: string(),
|
navigation_path: optional(string()),
|
||||||
confirmation: optional(actionConfigStructConfirmation),
|
confirmation: optional(actionConfigStructConfirmation),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -13,22 +13,28 @@ import {
|
|||||||
extractSearchParamsObject,
|
extractSearchParamsObject,
|
||||||
} from "../../common/url/search-params";
|
} from "../../common/url/search-params";
|
||||||
import "../../layouts/hass-error-screen";
|
import "../../layouts/hass-error-screen";
|
||||||
|
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||||
|
import { domainToName } from "../../data/integration";
|
||||||
|
|
||||||
const REDIRECTS = {
|
const REDIRECTS: Redirects = {
|
||||||
info: {
|
developer_states: {
|
||||||
redirect: "/config/info",
|
redirect: "/developer-tools/state",
|
||||||
},
|
},
|
||||||
logs: {
|
developer_services: {
|
||||||
redirect: "/config/logs",
|
redirect: "/developer-tools/service",
|
||||||
},
|
},
|
||||||
profile: {
|
developer_template: {
|
||||||
redirect: "/profile/dashboard",
|
redirect: "/developer-tools/template",
|
||||||
},
|
},
|
||||||
blueprint_import: {
|
developer_events: {
|
||||||
redirect: "/config/blueprint/dashboard/import",
|
redirect: "/developer-tools/event",
|
||||||
params: {
|
},
|
||||||
blueprint_url: "url",
|
cloud: {
|
||||||
},
|
component: "cloud",
|
||||||
|
redirect: "/config/cloud",
|
||||||
|
},
|
||||||
|
integrations: {
|
||||||
|
redirect: "/config/integrations",
|
||||||
},
|
},
|
||||||
config_flow_start: {
|
config_flow_start: {
|
||||||
redirect: "/config/integrations/add",
|
redirect: "/config/integrations/add",
|
||||||
@ -36,12 +42,80 @@ const REDIRECTS = {
|
|||||||
domain: "string",
|
domain: "string",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
devices: {
|
||||||
|
redirect: "/config/devices/dashboard",
|
||||||
|
},
|
||||||
|
entities: {
|
||||||
|
redirect: "/config/entities",
|
||||||
|
},
|
||||||
|
areas: {
|
||||||
|
redirect: "/config/areas/dashboard",
|
||||||
|
},
|
||||||
|
blueprints: {
|
||||||
|
redirect: "/config/blueprint/dashboard",
|
||||||
|
},
|
||||||
|
blueprint_import: {
|
||||||
|
redirect: "/config/blueprint/dashboard/import",
|
||||||
|
params: {
|
||||||
|
blueprint_url: "url",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
automations: {
|
||||||
|
redirect: "/config/automation/dashboard",
|
||||||
|
},
|
||||||
|
scenes: {
|
||||||
|
redirect: "/config/scene/dashboard",
|
||||||
|
},
|
||||||
|
scripts: {
|
||||||
|
redirect: "/config/script/dashboard",
|
||||||
|
},
|
||||||
|
helpers: {
|
||||||
|
redirect: "/config/helpers",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
redirect: "/config/tags",
|
||||||
|
},
|
||||||
|
lovelace_dashboards: {
|
||||||
|
redirect: "/config/lovelace/dashboards",
|
||||||
|
},
|
||||||
|
lovelace_resources: {
|
||||||
|
redirect: "/config/lovelace/resources",
|
||||||
|
},
|
||||||
|
people: {
|
||||||
|
redirect: "/config/person",
|
||||||
|
},
|
||||||
|
zones: {
|
||||||
|
redirect: "/config/zone",
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
redirect: "/config/users",
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
redirect: "/config/core",
|
||||||
|
},
|
||||||
|
server_controls: {
|
||||||
|
redirect: "/config/server_control",
|
||||||
|
},
|
||||||
|
logs: {
|
||||||
|
redirect: "/config/logs",
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
redirect: "/config/info",
|
||||||
|
},
|
||||||
|
customize: {
|
||||||
|
redirect: "/config/customize",
|
||||||
|
},
|
||||||
|
profile: {
|
||||||
|
redirect: "/profile/dashboard",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type ParamType = "url" | "string";
|
export type ParamType = "url" | "string";
|
||||||
|
|
||||||
interface Redirect {
|
export type Redirects = { [key: string]: Redirect };
|
||||||
|
export interface Redirect {
|
||||||
redirect: string;
|
redirect: string;
|
||||||
|
component?: string;
|
||||||
params?: {
|
params?: {
|
||||||
[key: string]: ParamType;
|
[key: string]: ParamType;
|
||||||
};
|
};
|
||||||
@ -53,24 +127,37 @@ class HaPanelMy extends LitElement {
|
|||||||
|
|
||||||
@property() public route!: Route;
|
@property() public route!: Route;
|
||||||
|
|
||||||
@internalProperty() public _error = "";
|
@internalProperty() public _error?: string;
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
const path = this.route.path.substr(1);
|
const path = this.route.path.substr(1);
|
||||||
const redirect: Redirect | undefined = REDIRECTS[path];
|
|
||||||
|
if (path.startsWith("supervisor")) {
|
||||||
|
if (!isComponentLoaded(this.hass, "hassio")) {
|
||||||
|
this._error = "no_supervisor";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigate(
|
||||||
|
this,
|
||||||
|
`/hassio/_my_redirect/${path}${window.location.search}`,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirect = REDIRECTS[path];
|
||||||
|
|
||||||
if (!redirect) {
|
if (!redirect) {
|
||||||
this._error = this.hass.localize(
|
this._error = "not_supported";
|
||||||
"ui.panel.my.not_supported",
|
return;
|
||||||
"link",
|
}
|
||||||
html`<a
|
|
||||||
target="_blank"
|
if (
|
||||||
rel="noreferrer noopener"
|
redirect.component &&
|
||||||
href="https://my.home-assistant.io/faq.html#supported-pages"
|
!isComponentLoaded(this.hass, redirect.component)
|
||||||
>${this.hass.localize("ui.panel.my.faq_link")}</a
|
) {
|
||||||
>`
|
this._error = "no_component";
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,7 +165,7 @@ class HaPanelMy extends LitElement {
|
|||||||
try {
|
try {
|
||||||
url = this._createRedirectUrl(redirect);
|
url = this._createRedirectUrl(redirect);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = this.hass.localize("ui.panel.my.error");
|
this._error = "url_error";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,9 +174,44 @@ class HaPanelMy extends LitElement {
|
|||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (this._error) {
|
if (this._error) {
|
||||||
return html`<hass-error-screen
|
let error = "Unknown error";
|
||||||
.error=${this._error}
|
switch (this._error) {
|
||||||
></hass-error-screen>`;
|
case "not_supported":
|
||||||
|
error =
|
||||||
|
this.hass.localize(
|
||||||
|
"ui.panel.my.not_supported",
|
||||||
|
"link",
|
||||||
|
html`<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
href="https://my.home-assistant.io/faq.html#supported-pages"
|
||||||
|
>${this.hass.localize("ui.panel.my.faq_link")}</a
|
||||||
|
>`
|
||||||
|
) || "This redirect is not supported.";
|
||||||
|
break;
|
||||||
|
case "no_component":
|
||||||
|
error =
|
||||||
|
this.hass.localize(
|
||||||
|
"ui.panel.my.component_not_loaded",
|
||||||
|
"integration",
|
||||||
|
domainToName(
|
||||||
|
this.hass.localize,
|
||||||
|
REDIRECTS[this.route.path.substr(1)].component!
|
||||||
|
)
|
||||||
|
) || "This redirect is not supported.";
|
||||||
|
break;
|
||||||
|
case "no_supervisor":
|
||||||
|
error =
|
||||||
|
this.hass.localize(
|
||||||
|
"ui.panel.my.component_not_loaded",
|
||||||
|
"integration",
|
||||||
|
"Home Assistant Supervisor"
|
||||||
|
) || "This redirect requires Home Assistant Supervisor.";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
error = this.hass.localize("ui.panel.my.error") || "Unknown error";
|
||||||
|
}
|
||||||
|
return html`<hass-error-screen .error=${error}></hass-error-screen>`;
|
||||||
}
|
}
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
281
src/panels/profile/dialog-ha-mfa-module-setup-flow.ts
Normal file
281
src/panels/profile/dialog-ha-mfa-module-setup-flow.ts
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
import "@material/mwc-button";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
internalProperty,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
} from "lit-element";
|
||||||
|
import { html, TemplateResult } from "lit-html";
|
||||||
|
import { localizeKey } from "../../common/translations/localize";
|
||||||
|
import "../../components/ha-circular-progress";
|
||||||
|
import "../../components/ha-form/ha-form";
|
||||||
|
import "../../components/ha-markdown";
|
||||||
|
import {
|
||||||
|
DataEntryFlowStep,
|
||||||
|
DataEntryFlowStepForm,
|
||||||
|
} from "../../data/data_entry_flow";
|
||||||
|
import { haStyleDialog } from "../../resources/styles";
|
||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
import "../../components/ha-dialog";
|
||||||
|
|
||||||
|
let instance = 0;
|
||||||
|
|
||||||
|
@customElement("ha-mfa-module-setup-flow")
|
||||||
|
class HaMfaModuleSetupFlow extends LitElement {
|
||||||
|
@property() public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@internalProperty() private _dialogClosedCallback?: (params: {
|
||||||
|
flowFinished: boolean;
|
||||||
|
}) => void;
|
||||||
|
|
||||||
|
@internalProperty() private _instance?: number;
|
||||||
|
|
||||||
|
@internalProperty() private _loading = false;
|
||||||
|
|
||||||
|
@internalProperty() private _opened = false;
|
||||||
|
|
||||||
|
@internalProperty() private _stepData: any = {};
|
||||||
|
|
||||||
|
@internalProperty() private _step?: DataEntryFlowStep;
|
||||||
|
|
||||||
|
@internalProperty() private _errorMessage?: string;
|
||||||
|
|
||||||
|
public showDialog({ continueFlowId, mfaModuleId, dialogClosedCallback }) {
|
||||||
|
this._instance = instance++;
|
||||||
|
this._dialogClosedCallback = dialogClosedCallback;
|
||||||
|
this._opened = true;
|
||||||
|
|
||||||
|
const fetchStep = continueFlowId
|
||||||
|
? this.hass.callWS({
|
||||||
|
type: "auth/setup_mfa",
|
||||||
|
flow_id: continueFlowId,
|
||||||
|
})
|
||||||
|
: this.hass.callWS({
|
||||||
|
type: "auth/setup_mfa",
|
||||||
|
mfa_module_id: mfaModuleId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const curInstance = this._instance;
|
||||||
|
|
||||||
|
fetchStep.then((step) => {
|
||||||
|
if (curInstance !== this._instance) return;
|
||||||
|
|
||||||
|
this._processStep(step);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog() {
|
||||||
|
// Closed dialog by clicking on the overlay
|
||||||
|
if (this._step) {
|
||||||
|
this._flowDone();
|
||||||
|
}
|
||||||
|
this._opened = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this._opened) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<ha-dialog
|
||||||
|
open
|
||||||
|
.heading=${this._computeStepTitle()}
|
||||||
|
@closing=${this.closeDialog}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
${this._errorMessage
|
||||||
|
? html`<div class="error">${this._errorMessage}</div>`
|
||||||
|
: ""}
|
||||||
|
${!this._step
|
||||||
|
? html`<div class="init-spinner">
|
||||||
|
<ha-circular-progress active></ha-circular-progress>
|
||||||
|
</div>`
|
||||||
|
: html`${this._step.type === "abort"
|
||||||
|
? html` <ha-markdown
|
||||||
|
allowsvg
|
||||||
|
breaks
|
||||||
|
.content=${this.hass.localize(
|
||||||
|
`component.auth.mfa_setup.${this._step.handler}.abort.${this._step.reason}`
|
||||||
|
)}
|
||||||
|
></ha-markdown>`
|
||||||
|
: this._step.type === "create_entry"
|
||||||
|
? html`<p>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.profile.mfa_setup.step_done",
|
||||||
|
"step",
|
||||||
|
this._step.title
|
||||||
|
)}
|
||||||
|
</p>`
|
||||||
|
: this._step.type === "form"
|
||||||
|
? html` <ha-markdown
|
||||||
|
allowsvg
|
||||||
|
breaks
|
||||||
|
.content=${localizeKey(
|
||||||
|
this.hass.localize,
|
||||||
|
`component.auth.mfa_setup.${this._step!.handler}.step.${
|
||||||
|
(this._step! as DataEntryFlowStepForm).step_id
|
||||||
|
}.description`,
|
||||||
|
this._step!.description_placeholders
|
||||||
|
)}
|
||||||
|
></ha-markdown>
|
||||||
|
<ha-form
|
||||||
|
.data=${this._stepData}
|
||||||
|
.schema=${this._step.data_schema}
|
||||||
|
.error=${this._step.errors}
|
||||||
|
.computeLabel=${this._computeLabel}
|
||||||
|
.computeError=${this._computeError}
|
||||||
|
@value-changed=${this._stepDataChanged}
|
||||||
|
></ha-form>`
|
||||||
|
: ""}`}
|
||||||
|
</div>
|
||||||
|
${["abort", "create_entry"].includes(this._step?.type || "")
|
||||||
|
? html`<mwc-button slot="primaryAction" @click=${this.closeDialog}
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.profile.mfa_setup.close"
|
||||||
|
)}</mwc-button
|
||||||
|
>`
|
||||||
|
: ""}
|
||||||
|
${this._step?.type === "form"
|
||||||
|
? html`<mwc-button
|
||||||
|
slot="primaryAction"
|
||||||
|
.disabled=${this._loading}
|
||||||
|
@click=${this._submitStep}
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.profile.mfa_setup.submit"
|
||||||
|
)}</mwc-button
|
||||||
|
>`
|
||||||
|
: ""}
|
||||||
|
</ha-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
haStyleDialog,
|
||||||
|
css`
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
ha-dialog {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
ha-markdown {
|
||||||
|
--markdown-svg-background-color: white;
|
||||||
|
--markdown-svg-color: black;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
ha-markdown a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
.init-spinner {
|
||||||
|
padding: 10px 100px 34px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.submit-spinner {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProperties) {
|
||||||
|
super.firstUpdated(changedProperties);
|
||||||
|
this.hass.loadBackendTranslation("mfa_setup", "auth");
|
||||||
|
this.addEventListener("keypress", (ev) => {
|
||||||
|
if (ev.key === "Enter") {
|
||||||
|
this._submitStep();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _stepDataChanged(ev: CustomEvent) {
|
||||||
|
this._stepData = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _submitStep() {
|
||||||
|
this._loading = true;
|
||||||
|
this._errorMessage = undefined;
|
||||||
|
|
||||||
|
const curInstance = this._instance;
|
||||||
|
|
||||||
|
this.hass
|
||||||
|
.callWS({
|
||||||
|
type: "auth/setup_mfa",
|
||||||
|
flow_id: this._step!.flow_id,
|
||||||
|
user_input: this._stepData,
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
(step) => {
|
||||||
|
if (curInstance !== this._instance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._processStep(step);
|
||||||
|
this._loading = false;
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
this._errorMessage =
|
||||||
|
(err && err.body && err.body.message) || "Unknown error occurred";
|
||||||
|
this._loading = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _processStep(step) {
|
||||||
|
if (!step.errors) step.errors = {};
|
||||||
|
this._step = step;
|
||||||
|
// We got a new form if there are no errors.
|
||||||
|
if (Object.keys(step.errors).length === 0) {
|
||||||
|
this._stepData = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _flowDone() {
|
||||||
|
const flowFinished = Boolean(
|
||||||
|
this._step && ["create_entry", "abort"].includes(this._step.type)
|
||||||
|
);
|
||||||
|
|
||||||
|
this._dialogClosedCallback!({
|
||||||
|
flowFinished,
|
||||||
|
});
|
||||||
|
|
||||||
|
this._errorMessage = undefined;
|
||||||
|
this._step = undefined;
|
||||||
|
this._stepData = {};
|
||||||
|
this._dialogClosedCallback = undefined;
|
||||||
|
this.closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computeStepTitle() {
|
||||||
|
return this._step?.type === "abort"
|
||||||
|
? this.hass.localize("ui.panel.profile.mfa_setup.title_aborted")
|
||||||
|
: this._step?.type === "create_entry"
|
||||||
|
? this.hass.localize("ui.panel.profile.mfa_setup.title_success")
|
||||||
|
: this._step?.type === "form"
|
||||||
|
? this.hass.localize(
|
||||||
|
`component.auth.mfa_setup.${this._step.handler}.step.${this._step.step_id}.title`
|
||||||
|
)
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computeLabel = (schema) =>
|
||||||
|
this.hass.localize(
|
||||||
|
`component.auth.mfa_setup.${this._step!.handler}.step.${
|
||||||
|
(this._step! as DataEntryFlowStepForm).step_id
|
||||||
|
}.data.${schema.name}`
|
||||||
|
) || schema.name;
|
||||||
|
|
||||||
|
private _computeError = (error) =>
|
||||||
|
this.hass.localize(
|
||||||
|
`component.auth.mfa_setup.${this._step!.handler}.error.${error}`
|
||||||
|
) || error;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-mfa-module-setup-flow": HaMfaModuleSetupFlow;
|
||||||
|
}
|
||||||
|
}
|
@ -1,322 +0,0 @@
|
|||||||
import "@material/mwc-button";
|
|
||||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
/* eslint-plugin-disable lit */
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
import "../../components/dialog/ha-paper-dialog";
|
|
||||||
import "../../components/ha-circular-progress";
|
|
||||||
import "../../components/ha-form/ha-form";
|
|
||||||
import "../../components/ha-markdown";
|
|
||||||
import { EventsMixin } from "../../mixins/events-mixin";
|
|
||||||
import LocalizeMixin from "../../mixins/localize-mixin";
|
|
||||||
import "../../styles/polymer-ha-style-dialog";
|
|
||||||
|
|
||||||
let instance = 0;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* @appliesMixin LocalizeMixin
|
|
||||||
* @appliesMixin EventsMixin
|
|
||||||
*/
|
|
||||||
class HaMfaModuleSetupFlow extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style include="ha-style-dialog">
|
|
||||||
.error {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
ha-paper-dialog {
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
ha-markdown {
|
|
||||||
--markdown-svg-background-color: white;
|
|
||||||
--markdown-svg-color: black;
|
|
||||||
display: block;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
ha-markdown a {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
.init-spinner {
|
|
||||||
padding: 10px 100px 34px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.submit-spinner {
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<ha-paper-dialog
|
|
||||||
id="dialog"
|
|
||||||
with-backdrop=""
|
|
||||||
opened="{{_opened}}"
|
|
||||||
on-opened-changed="_openedChanged"
|
|
||||||
>
|
|
||||||
<h2>
|
|
||||||
<template is="dom-if" if="[[_equals(_step.type, 'abort')]]">
|
|
||||||
[[localize('ui.panel.profile.mfa_setup.title_aborted')]]
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[_equals(_step.type, 'create_entry')]]">
|
|
||||||
[[localize('ui.panel.profile.mfa_setup.title_success')]]
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[_equals(_step.type, 'form')]]">
|
|
||||||
[[_computeStepTitle(localize, _step)]]
|
|
||||||
</template>
|
|
||||||
</h2>
|
|
||||||
<paper-dialog-scrollable>
|
|
||||||
<template is="dom-if" if="[[_errorMsg]]">
|
|
||||||
<div class="error">[[_errorMsg]]</div>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[!_step]]">
|
|
||||||
<div class="init-spinner">
|
|
||||||
<ha-circular-progress active></ha-circular-progress>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[_step]]">
|
|
||||||
<template is="dom-if" if="[[_equals(_step.type, 'abort')]]">
|
|
||||||
<ha-markdown
|
|
||||||
allowsvg
|
|
||||||
breaks
|
|
||||||
content="[[_computeStepAbortedReason(localize, _step)]]"
|
|
||||||
></ha-markdown>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template is="dom-if" if="[[_equals(_step.type, 'create_entry')]]">
|
|
||||||
<p>
|
|
||||||
[[localize('ui.panel.profile.mfa_setup.step_done', 'step',
|
|
||||||
_step.title)]]
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template is="dom-if" if="[[_equals(_step.type, 'form')]]">
|
|
||||||
<template
|
|
||||||
is="dom-if"
|
|
||||||
if="[[_computeStepDescription(localize, _step)]]"
|
|
||||||
>
|
|
||||||
<ha-markdown
|
|
||||||
allowsvg
|
|
||||||
breaks
|
|
||||||
content="[[_computeStepDescription(localize, _step)]]"
|
|
||||||
></ha-markdown>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<ha-form
|
|
||||||
data="{{_stepData}}"
|
|
||||||
schema="[[_step.data_schema]]"
|
|
||||||
error="[[_step.errors]]"
|
|
||||||
compute-label="[[_computeLabelCallback(localize, _step)]]"
|
|
||||||
compute-error="[[_computeErrorCallback(localize, _step)]]"
|
|
||||||
></ha-form>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</paper-dialog-scrollable>
|
|
||||||
<div class="buttons">
|
|
||||||
<template is="dom-if" if="[[_equals(_step.type, 'abort')]]">
|
|
||||||
<mwc-button on-click="_flowDone"
|
|
||||||
>[[localize('ui.panel.profile.mfa_setup.close')]]</mwc-button
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[_equals(_step.type, 'create_entry')]]">
|
|
||||||
<mwc-button on-click="_flowDone"
|
|
||||||
>[[localize('ui.panel.profile.mfa_setup.close')]]</mwc-button
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[_equals(_step.type, 'form')]]">
|
|
||||||
<template is="dom-if" if="[[_loading]]">
|
|
||||||
<div class="submit-spinner">
|
|
||||||
<ha-circular-progress active></ha-circular-progress>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[!_loading]]">
|
|
||||||
<mwc-button on-click="_submitStep"
|
|
||||||
>[[localize('ui.panel.profile.mfa_setup.submit')]]</mwc-button
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</ha-paper-dialog>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
_hass: Object,
|
|
||||||
_dialogClosedCallback: Function,
|
|
||||||
_instance: Number,
|
|
||||||
|
|
||||||
_loading: {
|
|
||||||
type: Boolean,
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Error message when can't talk to server etc
|
|
||||||
_errorMsg: String,
|
|
||||||
|
|
||||||
_opened: {
|
|
||||||
type: Boolean,
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
_step: {
|
|
||||||
type: Object,
|
|
||||||
value: null,
|
|
||||||
},
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Store user entered data.
|
|
||||||
*/
|
|
||||||
_stepData: Object,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
ready() {
|
|
||||||
super.ready();
|
|
||||||
this.hass.loadBackendTranslation("mfa_setup", "auth");
|
|
||||||
this.addEventListener("keypress", (ev) => {
|
|
||||||
if (ev.keyCode === 13) {
|
|
||||||
this._submitStep();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
showDialog({ hass, continueFlowId, mfaModuleId, dialogClosedCallback }) {
|
|
||||||
this.hass = hass;
|
|
||||||
this._instance = instance++;
|
|
||||||
this._dialogClosedCallback = dialogClosedCallback;
|
|
||||||
this._createdFromHandler = !!mfaModuleId;
|
|
||||||
this._loading = true;
|
|
||||||
this._opened = true;
|
|
||||||
|
|
||||||
const fetchStep = continueFlowId
|
|
||||||
? this.hass.callWS({
|
|
||||||
type: "auth/setup_mfa",
|
|
||||||
flow_id: continueFlowId,
|
|
||||||
})
|
|
||||||
: this.hass.callWS({
|
|
||||||
type: "auth/setup_mfa",
|
|
||||||
mfa_module_id: mfaModuleId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const curInstance = this._instance;
|
|
||||||
|
|
||||||
fetchStep.then((step) => {
|
|
||||||
if (curInstance !== this._instance) return;
|
|
||||||
|
|
||||||
this._processStep(step);
|
|
||||||
this._loading = false;
|
|
||||||
// When the flow changes, center the dialog.
|
|
||||||
// Don't do it on each step or else the dialog keeps bouncing.
|
|
||||||
setTimeout(() => this.$.dialog.center(), 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_submitStep() {
|
|
||||||
this._loading = true;
|
|
||||||
this._errorMsg = null;
|
|
||||||
|
|
||||||
const curInstance = this._instance;
|
|
||||||
|
|
||||||
this.hass
|
|
||||||
.callWS({
|
|
||||||
type: "auth/setup_mfa",
|
|
||||||
flow_id: this._step.flow_id,
|
|
||||||
user_input: this._stepData,
|
|
||||||
})
|
|
||||||
.then(
|
|
||||||
(step) => {
|
|
||||||
if (curInstance !== this._instance) return;
|
|
||||||
|
|
||||||
this._processStep(step);
|
|
||||||
this._loading = false;
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
this._errorMsg =
|
|
||||||
(err && err.body && err.body.message) || "Unknown error occurred";
|
|
||||||
this._loading = false;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_processStep(step) {
|
|
||||||
if (!step.errors) step.errors = {};
|
|
||||||
this._step = step;
|
|
||||||
// We got a new form if there are no errors.
|
|
||||||
if (Object.keys(step.errors).length === 0) {
|
|
||||||
this._stepData = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_flowDone() {
|
|
||||||
this._opened = false;
|
|
||||||
const flowFinished =
|
|
||||||
this._step && ["create_entry", "abort"].includes(this._step.type);
|
|
||||||
|
|
||||||
if (this._step && !flowFinished && this._createdFromHandler) {
|
|
||||||
// console.log('flow not finish');
|
|
||||||
}
|
|
||||||
|
|
||||||
this._dialogClosedCallback({
|
|
||||||
flowFinished,
|
|
||||||
});
|
|
||||||
|
|
||||||
this._errorMsg = null;
|
|
||||||
this._step = null;
|
|
||||||
this._stepData = {};
|
|
||||||
this._dialogClosedCallback = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_equals(a, b) {
|
|
||||||
return a === b;
|
|
||||||
}
|
|
||||||
|
|
||||||
_openedChanged(ev) {
|
|
||||||
// Closed dialog by clicking on the overlay
|
|
||||||
if (this._step && !ev.detail.value) {
|
|
||||||
this._flowDone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeStepAbortedReason(localize, step) {
|
|
||||||
return localize(
|
|
||||||
`component.auth.mfa_setup.${step.handler}.abort.${step.reason}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeStepTitle(localize, step) {
|
|
||||||
return (
|
|
||||||
localize(
|
|
||||||
`component.auth.mfa_setup.${step.handler}.step.${step.step_id}.title`
|
|
||||||
) || "Setup Multi-factor Authentication"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeStepDescription(localize, step) {
|
|
||||||
const args = [
|
|
||||||
`component.auth.mfa_setup.${step.handler}.step.${step.step_id}.description`,
|
|
||||||
];
|
|
||||||
const placeholders = step.description_placeholders || {};
|
|
||||||
Object.keys(placeholders).forEach((key) => {
|
|
||||||
args.push(key);
|
|
||||||
args.push(placeholders[key]);
|
|
||||||
});
|
|
||||||
return localize(...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeLabelCallback(localize, step) {
|
|
||||||
// Returns a callback for ha-form to calculate labels per schema object
|
|
||||||
return (schema) =>
|
|
||||||
localize(
|
|
||||||
`component.auth.mfa_setup.${step.handler}.step.${step.step_id}.data.${schema.name}`
|
|
||||||
) || schema.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeErrorCallback(localize, step) {
|
|
||||||
// Returns a callback for ha-form to calculate error messages
|
|
||||||
return (error) =>
|
|
||||||
localize(`component.auth.mfa_setup.${step.handler}.error.${error}`) ||
|
|
||||||
error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("ha-mfa-module-setup-flow", HaMfaModuleSetupFlow);
|
|
@ -1,130 +0,0 @@
|
|||||||
import "@material/mwc-button";
|
|
||||||
import "@polymer/paper-item/paper-item";
|
|
||||||
import "@polymer/paper-item/paper-item-body";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
/* eslint-plugin-disable lit */
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
import "../../components/ha-card";
|
|
||||||
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
|
||||||
import { EventsMixin } from "../../mixins/events-mixin";
|
|
||||||
import LocalizeMixin from "../../mixins/localize-mixin";
|
|
||||||
import "../../styles/polymer-ha-style";
|
|
||||||
|
|
||||||
let registeredDialog = false;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* @appliesMixin EventsMixin
|
|
||||||
* @appliesMixin LocalizeMixin
|
|
||||||
*/
|
|
||||||
class HaMfaModulesCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style include="iron-flex ha-style">
|
|
||||||
.error {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
.status {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
.error,
|
|
||||||
.status {
|
|
||||||
position: absolute;
|
|
||||||
top: -4px;
|
|
||||||
}
|
|
||||||
mwc-button {
|
|
||||||
margin-right: -0.57em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<ha-card header="[[localize('ui.panel.profile.mfa.header')]]">
|
|
||||||
<template is="dom-repeat" items="[[mfaModules]]" as="module">
|
|
||||||
<paper-item>
|
|
||||||
<paper-item-body two-line="">
|
|
||||||
<div>[[module.name]]</div>
|
|
||||||
<div secondary="">[[module.id]]</div>
|
|
||||||
</paper-item-body>
|
|
||||||
<template is="dom-if" if="[[module.enabled]]">
|
|
||||||
<mwc-button on-click="_disable"
|
|
||||||
>[[localize('ui.panel.profile.mfa.disable')]]</mwc-button
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[!module.enabled]]">
|
|
||||||
<mwc-button on-click="_enable"
|
|
||||||
>[[localize('ui.panel.profile.mfa.enable')]]</mwc-button
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</paper-item>
|
|
||||||
</template>
|
|
||||||
</ha-card>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: Object,
|
|
||||||
|
|
||||||
_loading: {
|
|
||||||
type: Boolean,
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Error message when can't talk to server etc
|
|
||||||
_statusMsg: String,
|
|
||||||
_errorMsg: String,
|
|
||||||
|
|
||||||
mfaModules: Array,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
|
|
||||||
if (!registeredDialog) {
|
|
||||||
registeredDialog = true;
|
|
||||||
this.fire("register-dialog", {
|
|
||||||
dialogShowEvent: "show-mfa-module-setup-flow",
|
|
||||||
dialogTag: "ha-mfa-module-setup-flow",
|
|
||||||
dialogImport: () => import("./ha-mfa-module-setup-flow"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_enable(ev) {
|
|
||||||
this.fire("show-mfa-module-setup-flow", {
|
|
||||||
hass: this.hass,
|
|
||||||
mfaModuleId: ev.model.module.id,
|
|
||||||
dialogClosedCallback: () => this._refreshCurrentUser(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async _disable(ev) {
|
|
||||||
const mfamodule = ev.model.module;
|
|
||||||
if (
|
|
||||||
!(await showConfirmationDialog(this, {
|
|
||||||
text: this.localize(
|
|
||||||
"ui.panel.profile.mfa.confirm_disable",
|
|
||||||
"name",
|
|
||||||
mfamodule.name
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mfaModuleId = mfamodule.id;
|
|
||||||
|
|
||||||
this.hass
|
|
||||||
.callWS({
|
|
||||||
type: "auth/depose_mfa",
|
|
||||||
mfa_module_id: mfaModuleId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this._refreshCurrentUser();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_refreshCurrentUser() {
|
|
||||||
this.fire("hass-refresh-current-user");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("ha-mfa-modules-card", HaMfaModulesCard);
|
|
101
src/panels/profile/ha-mfa-modules-card.ts
Normal file
101
src/panels/profile/ha-mfa-modules-card.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import "@material/mwc-button";
|
||||||
|
import "@polymer/paper-item/paper-item";
|
||||||
|
import "@polymer/paper-item/paper-item-body";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit-element";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import "../../components/ha-card";
|
||||||
|
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
||||||
|
import { HomeAssistant, MFAModule } from "../../types";
|
||||||
|
import { showMfaModuleSetupFlowDialog } from "./show-ha-mfa-module-setup-flow-dialog";
|
||||||
|
|
||||||
|
@customElement("ha-mfa-modules-card")
|
||||||
|
class HaMfaModulesCard extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public mfaModules!: MFAModule[];
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<ha-card .header=${this.hass.localize("ui.panel.profile.mfa.header")}>
|
||||||
|
${this.mfaModules.map(
|
||||||
|
(module) => html`<paper-item>
|
||||||
|
<paper-item-body two-line="">
|
||||||
|
<div>${module.name}</div>
|
||||||
|
<div secondary>${module.id}</div>
|
||||||
|
</paper-item-body>
|
||||||
|
${module.enabled
|
||||||
|
? html`<mwc-button .module=${module} @click=${this._disable}
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.profile.mfa.disable"
|
||||||
|
)}</mwc-button
|
||||||
|
>`
|
||||||
|
: html`<mwc-button .module=${module} @click=${this._enable}
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.profile.mfa.enable"
|
||||||
|
)}</mwc-button
|
||||||
|
>`}
|
||||||
|
</paper-item>`
|
||||||
|
)}
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return css`
|
||||||
|
mwc-button {
|
||||||
|
margin-right: -0.57em;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _enable(ev) {
|
||||||
|
showMfaModuleSetupFlowDialog(this, {
|
||||||
|
mfaModuleId: ev.currentTarget.module.id,
|
||||||
|
dialogClosedCallback: () => this._refreshCurrentUser(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _disable(ev) {
|
||||||
|
const mfamodule = ev.currentTarget.module;
|
||||||
|
if (
|
||||||
|
!(await showConfirmationDialog(this, {
|
||||||
|
text: this.hass.localize(
|
||||||
|
"ui.panel.profile.mfa.confirm_disable",
|
||||||
|
"name",
|
||||||
|
mfamodule.name
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mfaModuleId = mfamodule.id;
|
||||||
|
|
||||||
|
this.hass
|
||||||
|
.callWS({
|
||||||
|
type: "auth/depose_mfa",
|
||||||
|
mfa_module_id: mfaModuleId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this._refreshCurrentUser();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _refreshCurrentUser() {
|
||||||
|
fireEvent(this, "hass-refresh-current-user");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-mfa-modules-card": HaMfaModulesCard;
|
||||||
|
}
|
||||||
|
}
|
21
src/panels/profile/show-ha-mfa-module-setup-flow-dialog.ts
Normal file
21
src/panels/profile/show-ha-mfa-module-setup-flow-dialog.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
|
||||||
|
export interface MfaModuleSetupFlowDialogParams {
|
||||||
|
continueFlowId?: string;
|
||||||
|
mfaModuleId?: string;
|
||||||
|
dialogClosedCallback: (params: { flowFinished: boolean }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadMfaModuleSetupFlowDialog = () =>
|
||||||
|
import("./dialog-ha-mfa-module-setup-flow");
|
||||||
|
|
||||||
|
export const showMfaModuleSetupFlowDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
dialogParams: MfaModuleSetupFlowDialogParams
|
||||||
|
): void => {
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "ha-mfa-module-setup-flow",
|
||||||
|
dialogImport: loadMfaModuleSetupFlowDialog,
|
||||||
|
dialogParams,
|
||||||
|
});
|
||||||
|
};
|
@ -6,7 +6,6 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|||||||
/* eslint-plugin-disable lit */
|
/* eslint-plugin-disable lit */
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
import "../components/entity/state-info";
|
import "../components/entity/state-info";
|
||||||
import "../components/ha-slider";
|
|
||||||
|
|
||||||
class StateCardNumber extends mixinBehaviors(
|
class StateCardNumber extends mixinBehaviors(
|
||||||
[IronResizableBehavior],
|
[IronResizableBehavior],
|
||||||
@ -16,9 +15,6 @@ class StateCardNumber extends mixinBehaviors(
|
|||||||
return html`
|
return html`
|
||||||
<style include="iron-flex iron-flex-alignment"></style>
|
<style include="iron-flex iron-flex-alignment"></style>
|
||||||
<style>
|
<style>
|
||||||
ha-slider {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
.state {
|
.state {
|
||||||
@apply --paper-font-body1;
|
@apply --paper-font-body1;
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
@ -26,12 +22,6 @@ class StateCardNumber extends mixinBehaviors(
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
}
|
}
|
||||||
.sliderstate {
|
|
||||||
min-width: 45px;
|
|
||||||
}
|
|
||||||
ha-slider[hidden] {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
paper-input {
|
paper-input {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
@ -40,19 +30,6 @@ class StateCardNumber extends mixinBehaviors(
|
|||||||
|
|
||||||
<div class="horizontal justified layout" id="number_card">
|
<div class="horizontal justified layout" id="number_card">
|
||||||
${this.stateInfoTemplate}
|
${this.stateInfoTemplate}
|
||||||
<ha-slider
|
|
||||||
min="[[min]]"
|
|
||||||
max="[[max]]"
|
|
||||||
value="{{value}}"
|
|
||||||
step="[[step]]"
|
|
||||||
hidden="[[hiddenslider]]"
|
|
||||||
pin
|
|
||||||
on-change="selectedValueChanged"
|
|
||||||
on-click="stopPropagation"
|
|
||||||
id="slider"
|
|
||||||
ignore-bar-touch=""
|
|
||||||
>
|
|
||||||
</ha-slider>
|
|
||||||
<paper-input
|
<paper-input
|
||||||
no-label-float=""
|
no-label-float=""
|
||||||
auto-validate=""
|
auto-validate=""
|
||||||
@ -64,19 +41,11 @@ class StateCardNumber extends mixinBehaviors(
|
|||||||
type="number"
|
type="number"
|
||||||
on-change="selectedValueChanged"
|
on-change="selectedValueChanged"
|
||||||
on-click="stopPropagation"
|
on-click="stopPropagation"
|
||||||
hidden="[[hiddenbox]]"
|
|
||||||
>
|
>
|
||||||
</paper-input>
|
</paper-input>
|
||||||
<div class="state" hidden="[[hiddenbox]]">
|
<div class="state">
|
||||||
[[stateObj.attributes.unit_of_measurement]]
|
[[stateObj.attributes.unit_of_measurement]]
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
id="sliderstate"
|
|
||||||
class="state sliderstate"
|
|
||||||
hidden="[[hiddenslider]]"
|
|
||||||
>
|
|
||||||
[[value]] [[stateObj.attributes.unit_of_measurement]]
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -91,31 +60,9 @@ class StateCardNumber extends mixinBehaviors(
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
ready() {
|
|
||||||
super.ready();
|
|
||||||
if (typeof ResizeObserver === "function") {
|
|
||||||
const ro = new ResizeObserver((entries) => {
|
|
||||||
entries.forEach(() => {
|
|
||||||
this.hiddenState();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ro.observe(this.$.number_card);
|
|
||||||
} else {
|
|
||||||
this.addEventListener("iron-resize", this.hiddenState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
hass: Object,
|
hass: Object,
|
||||||
hiddenbox: {
|
|
||||||
type: Boolean,
|
|
||||||
value: true,
|
|
||||||
},
|
|
||||||
hiddenslider: {
|
|
||||||
type: Boolean,
|
|
||||||
value: true,
|
|
||||||
},
|
|
||||||
inDialog: {
|
inDialog: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
value: false,
|
value: false,
|
||||||
@ -138,35 +85,17 @@ class StateCardNumber extends mixinBehaviors(
|
|||||||
},
|
},
|
||||||
step: Number,
|
step: Number,
|
||||||
value: Number,
|
value: Number,
|
||||||
mode: String,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
hiddenState() {
|
|
||||||
if (this.mode !== "slider") return;
|
|
||||||
const sliderwidth = this.$.slider.offsetWidth;
|
|
||||||
if (sliderwidth < 100) {
|
|
||||||
this.$.sliderstate.hidden = true;
|
|
||||||
} else if (sliderwidth >= 145) {
|
|
||||||
this.$.sliderstate.hidden = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stateObjectChanged(newVal) {
|
stateObjectChanged(newVal) {
|
||||||
const prevMode = this.mode;
|
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
min: Number(newVal.attributes.min),
|
min: Number(newVal.attributes.min),
|
||||||
max: Number(newVal.attributes.max),
|
max: Number(newVal.attributes.max),
|
||||||
step: Number(newVal.attributes.step),
|
step: Number(newVal.attributes.step),
|
||||||
value: Number(newVal.state),
|
value: Number(newVal.state),
|
||||||
mode: String(newVal.attributes.mode),
|
|
||||||
maxlength: String(newVal.attributes.max).length,
|
maxlength: String(newVal.attributes.max).length,
|
||||||
hiddenbox: newVal.attributes.mode !== "box",
|
|
||||||
hiddenslider: newVal.attributes.mode !== "slider",
|
|
||||||
});
|
});
|
||||||
if (this.mode === "slider" && prevMode !== "slider") {
|
|
||||||
this.hiddenState();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedValueChanged() {
|
selectedValueChanged() {
|
||||||
|
@ -51,17 +51,24 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
|||||||
enableShortcuts: true,
|
enableShortcuts: true,
|
||||||
moreInfoEntityId: null,
|
moreInfoEntityId: null,
|
||||||
hassUrl: (path = "") => new URL(path, auth.data.hassUrl).toString(),
|
hassUrl: (path = "") => new URL(path, auth.data.hassUrl).toString(),
|
||||||
callService: async (domain, service, serviceData = {}) => {
|
callService: async (domain, service, serviceData = {}, target) => {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log("Calling service", domain, service, serviceData);
|
console.log(
|
||||||
|
"Calling service",
|
||||||
|
domain,
|
||||||
|
service,
|
||||||
|
serviceData,
|
||||||
|
target
|
||||||
|
);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return (await callService(
|
return (await callService(
|
||||||
conn,
|
conn,
|
||||||
domain,
|
domain,
|
||||||
service,
|
service,
|
||||||
serviceData
|
serviceData,
|
||||||
|
target
|
||||||
)) as Promise<ServiceCallResponse>;
|
)) as Promise<ServiceCallResponse>;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
@ -71,6 +78,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
|||||||
domain,
|
domain,
|
||||||
service,
|
service,
|
||||||
serviceData,
|
serviceData,
|
||||||
|
target,
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,17 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
|||||||
this._loadCoreTranslations(getLocalLanguage());
|
this._loadCoreTranslations(getLocalLanguage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps) {
|
||||||
|
super.updated(changedProps);
|
||||||
|
if (!changedProps.has("hass")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const oldHass = changedProps.get("hass");
|
||||||
|
if (this.hass?.panels && oldHass.panels !== this.hass.panels) {
|
||||||
|
this._loadFragmentTranslations(this.hass.language, this.hass.panelUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected hassConnected() {
|
protected hassConnected() {
|
||||||
super.hassConnected();
|
super.hassConnected();
|
||||||
getUserLanguage(this.hass!).then((language) => {
|
getUserLanguage(this.hass!).then((language) => {
|
||||||
@ -204,13 +215,10 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
|||||||
const panelComponent = this.hass?.panels?.[panelUrl]?.component_name;
|
const panelComponent = this.hass?.panels?.[panelUrl]?.component_name;
|
||||||
|
|
||||||
// If it's the first call we don't have panel info yet to check the component.
|
// If it's the first call we don't have panel info yet to check the component.
|
||||||
// If the url is not known it might be a custom lovelace dashboard, so we load lovelace translations
|
|
||||||
const fragment = translationMetadata.fragments.includes(
|
const fragment = translationMetadata.fragments.includes(
|
||||||
panelComponent || panelUrl
|
panelComponent || panelUrl
|
||||||
)
|
)
|
||||||
? panelComponent || panelUrl
|
? panelComponent || panelUrl
|
||||||
: !panelComponent
|
|
||||||
? "lovelace"
|
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (!fragment) {
|
if (!fragment) {
|
||||||
|
@ -424,6 +424,12 @@
|
|||||||
"service-picker": {
|
"service-picker": {
|
||||||
"service": "Service"
|
"service": "Service"
|
||||||
},
|
},
|
||||||
|
"service-control": {
|
||||||
|
"required": "This field is required",
|
||||||
|
"target": "Target",
|
||||||
|
"target_description": "What should this service call target",
|
||||||
|
"service_data": "Service data"
|
||||||
|
},
|
||||||
"related-items": {
|
"related-items": {
|
||||||
"no_related_found": "No related items found.",
|
"no_related_found": "No related items found.",
|
||||||
"integration": "Integration",
|
"integration": "Integration",
|
||||||
@ -806,6 +812,7 @@
|
|||||||
"panel": {
|
"panel": {
|
||||||
"my": {
|
"my": {
|
||||||
"not_supported": "This redirect is not supported by your Home Assistant instance. Check the {link} for the supported redirects and the version they where introduced.",
|
"not_supported": "This redirect is not supported by your Home Assistant instance. Check the {link} for the supported redirects and the version they where introduced.",
|
||||||
|
"component_not_loaded": "This redirect is not supported by your Home Assistant instance. You need the integration {integration} to use this redirect.",
|
||||||
"faq_link": "My Home Assistant FAQ",
|
"faq_link": "My Home Assistant FAQ",
|
||||||
"error": "An unknown error occured"
|
"error": "An unknown error occured"
|
||||||
},
|
},
|
||||||
@ -1400,8 +1407,7 @@
|
|||||||
"type_select": "Action type",
|
"type_select": "Action type",
|
||||||
"type": {
|
"type": {
|
||||||
"service": {
|
"service": {
|
||||||
"label": "Call service",
|
"label": "Call service"
|
||||||
"service_data": "Service data"
|
|
||||||
},
|
},
|
||||||
"delay": {
|
"delay": {
|
||||||
"label": "Delay",
|
"label": "Delay",
|
||||||
@ -1424,7 +1430,7 @@
|
|||||||
"event": {
|
"event": {
|
||||||
"label": "Fire event",
|
"label": "Fire event",
|
||||||
"event": "[%key:ui::panel::config::automation::editor::triggers::type::homeassistant::event%]",
|
"event": "[%key:ui::panel::config::automation::editor::triggers::type::homeassistant::event%]",
|
||||||
"service_data": "[%key:ui::panel::config::automation::editor::actions::type::service::service_data%]"
|
"service_data": "[%key:ui::components::service-control::service_data%]"
|
||||||
},
|
},
|
||||||
"device_id": {
|
"device_id": {
|
||||||
"label": "Device",
|
"label": "Device",
|
||||||
@ -2068,7 +2074,13 @@
|
|||||||
"description": "This step requires you to visit an external website to be completed.",
|
"description": "This step requires you to visit an external website to be completed.",
|
||||||
"open_site": "Open website"
|
"open_site": "Open website"
|
||||||
},
|
},
|
||||||
"loading_first_time": "Please wait while the integration is being installed"
|
"pick_flow_step": {
|
||||||
|
"title": "We discovered these, want to set them up?",
|
||||||
|
"new_flow": "No, set up an other instance of {integration}"
|
||||||
|
},
|
||||||
|
"loading_first_time": "Please wait while the integration is being installed",
|
||||||
|
"error": "Error",
|
||||||
|
"could_not_load": "Config flow could not be loaded"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
@ -2693,7 +2705,6 @@
|
|||||||
"action-editor": {
|
"action-editor": {
|
||||||
"navigation_path": "Navigation Path",
|
"navigation_path": "Navigation Path",
|
||||||
"url_path": "URL Path",
|
"url_path": "URL Path",
|
||||||
"editor_service_data": "Service data can only be entered in the code editor",
|
|
||||||
"actions": {
|
"actions": {
|
||||||
"default_action": "Default Action",
|
"default_action": "Default Action",
|
||||||
"call-service": "Call Service",
|
"call-service": "Call Service",
|
||||||
@ -3272,16 +3283,16 @@
|
|||||||
"services": {
|
"services": {
|
||||||
"title": "Services",
|
"title": "Services",
|
||||||
"description": "The service dev tool allows you to call any available service in Home Assistant.",
|
"description": "The service dev tool allows you to call any available service in Home Assistant.",
|
||||||
"data": "Service Data (YAML, optional)",
|
|
||||||
"call_service": "Call Service",
|
"call_service": "Call Service",
|
||||||
"select_service": "Select a service to see the description",
|
|
||||||
"no_description": "No description is available",
|
|
||||||
"no_parameters": "This service takes no parameters.",
|
|
||||||
"column_parameter": "Parameter",
|
"column_parameter": "Parameter",
|
||||||
"column_description": "Description",
|
"column_description": "Description",
|
||||||
"column_example": "Example",
|
"column_example": "Example",
|
||||||
"fill_example_data": "Fill Example Data",
|
"fill_example_data": "Fill Example Data",
|
||||||
"alert_parsing_yaml": "Error parsing YAML: {data}"
|
"yaml_mode": "Go to YAML mode",
|
||||||
|
"ui_mode": "Go to UI mode",
|
||||||
|
"yaml_parameters": "Parameters only available in YAML mode",
|
||||||
|
"all_parameters": "All available parameters",
|
||||||
|
"accepts_target": "This service accepts a target, for example: `entity_id: light.bed_light`"
|
||||||
},
|
},
|
||||||
"states": {
|
"states": {
|
||||||
"title": "States",
|
"title": "States",
|
||||||
|
@ -3,6 +3,7 @@ import {
|
|||||||
Connection,
|
Connection,
|
||||||
HassConfig,
|
HassConfig,
|
||||||
HassEntities,
|
HassEntities,
|
||||||
|
HassServiceTarget,
|
||||||
HassServices,
|
HassServices,
|
||||||
MessageBase,
|
MessageBase,
|
||||||
} from "home-assistant-js-websocket";
|
} from "home-assistant-js-websocket";
|
||||||
@ -178,6 +179,7 @@ export interface ServiceCallRequest {
|
|||||||
domain: string;
|
domain: string;
|
||||||
service: string;
|
service: string;
|
||||||
serviceData?: Record<string, any>;
|
serviceData?: Record<string, any>;
|
||||||
|
target?: HassServiceTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HomeAssistant {
|
export interface HomeAssistant {
|
||||||
@ -216,7 +218,8 @@ export interface HomeAssistant {
|
|||||||
callService(
|
callService(
|
||||||
domain: ServiceCallRequest["domain"],
|
domain: ServiceCallRequest["domain"],
|
||||||
service: ServiceCallRequest["service"],
|
service: ServiceCallRequest["service"],
|
||||||
serviceData?: ServiceCallRequest["serviceData"]
|
serviceData?: ServiceCallRequest["serviceData"],
|
||||||
|
target?: ServiceCallRequest["target"]
|
||||||
): Promise<ServiceCallResponse>;
|
): Promise<ServiceCallResponse>;
|
||||||
callApi<T>(
|
callApi<T>(
|
||||||
method: "GET" | "POST" | "PUT" | "DELETE",
|
method: "GET" | "POST" | "PUT" | "DELETE",
|
||||||
|
@ -1,20 +1,21 @@
|
|||||||
import * as assert from "assert";
|
import * as assert from "assert";
|
||||||
import { createHassioSession } from "../../src/data/hassio/ingress";
|
import { createHassioSession } from "../../src/data/hassio/ingress";
|
||||||
|
|
||||||
const sessionID = "fhdsu73rh3io4h8f3irhjel8ousafehf8f3yh";
|
|
||||||
|
|
||||||
describe("Create hassio session", function () {
|
describe("Create hassio session", function () {
|
||||||
|
const hass = {
|
||||||
|
config: { version: "1.0.0" },
|
||||||
|
callApi: async function () {
|
||||||
|
return { data: { session: "fhdsu73rh3io4h8f3irhjel8ousafehf8f3yh" } };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
it("Test create session without HTTPS", async function () {
|
it("Test create session without HTTPS", async function () {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
global.document = {};
|
global.document = {};
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
global.location = {};
|
global.location = {};
|
||||||
await createHassioSession({
|
// @ts-ignore
|
||||||
// @ts-ignore
|
await createHassioSession(hass);
|
||||||
callApi: async function () {
|
|
||||||
return { data: { session: sessionID } };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
global.document.cookie,
|
global.document.cookie,
|
||||||
@ -26,12 +27,8 @@ describe("Create hassio session", function () {
|
|||||||
global.document = {};
|
global.document = {};
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
global.location = { protocol: "https:" };
|
global.location = { protocol: "https:" };
|
||||||
await createHassioSession({
|
// @ts-ignore
|
||||||
// @ts-ignore
|
await createHassioSession(hass);
|
||||||
callApi: async function () {
|
|
||||||
return { data: { session: sessionID } };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
global.document.cookie,
|
global.document.cookie,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user