mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-17 08:59:43 +00:00
Compare commits
49 Commits
Button-Edi
...
20220329.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2a12172eeb | ||
![]() |
85d3011625 | ||
![]() |
ca22ec6340 | ||
![]() |
61f6e8855b | ||
![]() |
a44b8981e1 | ||
![]() |
b080bca9ce | ||
![]() |
d30e8ee9d8 | ||
![]() |
637e4203e5 | ||
![]() |
2648a53bbc | ||
![]() |
b3fa0cccb4 | ||
![]() |
dd963be723 | ||
![]() |
224df896a1 | ||
![]() |
a58b4fb262 | ||
![]() |
27ca61ec85 | ||
![]() |
859f49f3eb | ||
![]() |
40d878689f | ||
![]() |
420e8fe1ff | ||
![]() |
df96199433 | ||
![]() |
f493280f0a | ||
![]() |
cbd030a379 | ||
![]() |
95b80accc9 | ||
![]() |
c522670815 | ||
![]() |
7b6d3c0e36 | ||
![]() |
504b043159 | ||
![]() |
dffc66ccc3 | ||
![]() |
c7e9ee785d | ||
![]() |
079cc39a6e | ||
![]() |
d6a1d5af79 | ||
![]() |
c0dce08e19 | ||
![]() |
a7a347ed05 | ||
![]() |
2d9b50defc | ||
![]() |
840858b18c | ||
![]() |
afd2e71f6c | ||
![]() |
88af0aa788 | ||
![]() |
49124f6f09 | ||
![]() |
73f5580555 | ||
![]() |
bdde5268c6 | ||
![]() |
15e972c158 | ||
![]() |
0fc4c24f5a | ||
![]() |
9eba50df0c | ||
![]() |
0e0e07437f | ||
![]() |
6ac51ede52 | ||
![]() |
ccf1fb573a | ||
![]() |
fa537968c4 | ||
![]() |
6bf2111a3c | ||
![]() |
f5f8be8276 | ||
![]() |
ddf1cc0733 | ||
![]() |
35a41b3490 | ||
![]() |
f59cb661cd |
15
.github/workflows/release.yaml
vendored
15
.github/workflows/release.yaml
vendored
@@ -10,10 +10,18 @@ env:
|
||||
NODE_VERSION: 14
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
# Set default workflow permissions
|
||||
# All scopes not mentioned here are set to no access
|
||||
# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token
|
||||
permissions:
|
||||
actions: none
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
@@ -47,6 +55,13 @@ jobs:
|
||||
|
||||
script/release
|
||||
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@v0.1.14
|
||||
with:
|
||||
files: |
|
||||
dist/*.whl
|
||||
dist/*.tar.gz
|
||||
|
||||
wheels-init:
|
||||
name: Init wheels build
|
||||
needs: release
|
||||
|
@@ -23,7 +23,7 @@ if [[ "${PULL_REQUEST}" == "true" ]]; then
|
||||
createStatus "pending" "Building design preview" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID"
|
||||
gulp build-gallery
|
||||
if [ $? -eq 0 ]; then
|
||||
createStatus "success" "Build complete" "$DEPLOY_URL"
|
||||
createStatus "success" "Build complete" "$DEPLOY_PRIME_URL"
|
||||
else
|
||||
createStatus "error" "Build failed" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID"
|
||||
fi
|
||||
|
@@ -42,10 +42,11 @@ module.exports = [
|
||||
},
|
||||
{
|
||||
category: "user-test",
|
||||
header: "User Tests",
|
||||
header: "Users",
|
||||
pages: ["user-types", "configuration-menu"],
|
||||
},
|
||||
{
|
||||
category: "design.home-assistant.io",
|
||||
header: "Design Documentation",
|
||||
header: "About",
|
||||
},
|
||||
];
|
||||
|
@@ -45,6 +45,10 @@ class HaGallery extends LitElement {
|
||||
for (const page of group.pages!) {
|
||||
const key = `${group.category}/${page}`;
|
||||
const active = this._page === key;
|
||||
if (!(key in PAGES)) {
|
||||
console.error("Undefined page referenced in sidebar.js:", key);
|
||||
continue;
|
||||
}
|
||||
const title = PAGES[key].metadata.title || page;
|
||||
links.push(html`
|
||||
<a ?active=${active} href=${`#${group.category}/${page}`}>${title}</a>
|
||||
|
@@ -1,18 +1,18 @@
|
||||
/* eslint-disable lit/no-template-arrow */
|
||||
import "@material/mwc-button";
|
||||
import { LitElement, TemplateResult, html } from "lit";
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
|
||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import "../../components/demo-black-white-row";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import "../../components/demo-black-white-row";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("alarm_control_panel", "alarm", "disarmed", {
|
||||
@@ -147,7 +147,9 @@ const SCHEMAS: {
|
||||
{ name: "target", selector: { target: {} } },
|
||||
{ name: "number", selector: { number: { min: 0, max: 10 } } },
|
||||
{ name: "boolean", selector: { boolean: {} } },
|
||||
{ name: "time", selector: { time: {} } },
|
||||
{ name: "time", required: true, selector: { time: {} } },
|
||||
{ name: "datetime", required: true, selector: { datetime: {} } },
|
||||
{ name: "date", required: true, selector: { date: {} } },
|
||||
{ name: "action", selector: { action: {} } },
|
||||
{ name: "text", selector: { text: { multiline: false } } },
|
||||
{ name: "text_multiline", selector: { text: { multiline: true } } },
|
||||
|
@@ -1,20 +1,20 @@
|
||||
/* eslint-disable lit/no-template-arrow */
|
||||
import "@material/mwc-button";
|
||||
import { LitElement, TemplateResult, css, html } from "lit";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import "../../../../src/components/ha-selector/ha-selector";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import { BlueprintInput } from "../../../../src/data/blueprint";
|
||||
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/demo-black-white-row";
|
||||
import { BlueprintInput } from "../../../../src/data/blueprint";
|
||||
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
|
||||
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("alarm_control_panel", "alarm", "disarmed", {
|
||||
@@ -202,6 +202,7 @@ const SCHEMAS: {
|
||||
input: {
|
||||
entity: { name: "Entity", selector: { entity: { multiple: true } } },
|
||||
device: { name: "Device", selector: { device: { multiple: true } } },
|
||||
area: { name: "Area", selector: { area: { multiple: true } } },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
3
gallery/src/pages/more-info/update.markdown
Normal file
3
gallery/src/pages/more-info/update.markdown
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Update
|
||||
---
|
140
gallery/src/pages/more-info/update.ts
Normal file
140
gallery/src/pages/more-info/update.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import {
|
||||
UPDATE_SUPPORT_BACKUP,
|
||||
UPDATE_SUPPORT_PROGRESS,
|
||||
UPDATE_SUPPORT_INSTALL,
|
||||
} from "../../../../src/data/update";
|
||||
import "../../../../src/dialogs/more-info/more-info-content";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import {
|
||||
MockHomeAssistant,
|
||||
provideHass,
|
||||
} from "../../../../src/fake_data/provide_hass";
|
||||
import "../../components/demo-more-infos";
|
||||
|
||||
const base_attributes = {
|
||||
title: "Awesome",
|
||||
current_version: "1.2.2",
|
||||
latest_version: "1.2.3",
|
||||
release_url: "https://home-assistant.io",
|
||||
supported_features: UPDATE_SUPPORT_INSTALL,
|
||||
skipped_version: null,
|
||||
in_progress: false,
|
||||
release_summary:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. In nec metus aliquet, porta mi ut, ultrices odio. Etiam egestas orci tellus, non semper metus blandit tincidunt. Praesent elementum turpis vel tempor pharetra. Sed quis cursus diam. Proin sem justo.",
|
||||
};
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("update", "update1", "on", {
|
||||
...base_attributes,
|
||||
friendly_name: "Update",
|
||||
}),
|
||||
getEntity("update", "update2", "on", {
|
||||
...base_attributes,
|
||||
title: null,
|
||||
friendly_name: "Update without title",
|
||||
}),
|
||||
getEntity("update", "update3", "on", {
|
||||
...base_attributes,
|
||||
release_url: null,
|
||||
friendly_name: "Update without release_url",
|
||||
}),
|
||||
getEntity("update", "update4", "on", {
|
||||
...base_attributes,
|
||||
release_summary: null,
|
||||
friendly_name: "Update without release_summary",
|
||||
}),
|
||||
getEntity("update", "update5", "off", {
|
||||
...base_attributes,
|
||||
current_version: "1.2.3",
|
||||
friendly_name: "No update",
|
||||
}),
|
||||
getEntity("update", "update6", "off", {
|
||||
...base_attributes,
|
||||
skipped_version: "1.2.3",
|
||||
friendly_name: "Skipped version",
|
||||
}),
|
||||
getEntity("update", "update7", "on", {
|
||||
...base_attributes,
|
||||
supported_features:
|
||||
base_attributes.supported_features + UPDATE_SUPPORT_BACKUP,
|
||||
friendly_name: "With backup support",
|
||||
}),
|
||||
getEntity("update", "update8", "on", {
|
||||
...base_attributes,
|
||||
in_progress: true,
|
||||
friendly_name: "With true in_progress",
|
||||
}),
|
||||
getEntity("update", "update9", "on", {
|
||||
...base_attributes,
|
||||
in_progress: 25,
|
||||
supported_features:
|
||||
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
|
||||
friendly_name: "With 25 in_progress",
|
||||
}),
|
||||
getEntity("update", "update10", "on", {
|
||||
...base_attributes,
|
||||
in_progress: 50,
|
||||
supported_features:
|
||||
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
|
||||
friendly_name: "With 50 in_progress",
|
||||
}),
|
||||
getEntity("update", "update11", "on", {
|
||||
...base_attributes,
|
||||
in_progress: 75,
|
||||
supported_features:
|
||||
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
|
||||
friendly_name: "With 75 in_progress",
|
||||
}),
|
||||
getEntity("update", "update12", "unavailable", {
|
||||
...base_attributes,
|
||||
in_progress: 50,
|
||||
friendly_name: "Unavailable",
|
||||
}),
|
||||
getEntity("update", "update13", "on", {
|
||||
...base_attributes,
|
||||
supported_features: 0,
|
||||
friendly_name: "No install support",
|
||||
}),
|
||||
getEntity("update", "update14", "off", {
|
||||
...base_attributes,
|
||||
current_version: null,
|
||||
friendly_name: "Update without current_version",
|
||||
}),
|
||||
getEntity("update", "update15", "off", {
|
||||
...base_attributes,
|
||||
latest_version: null,
|
||||
friendly_name: "Update without latest_version",
|
||||
}),
|
||||
];
|
||||
|
||||
@customElement("demo-more-info-update")
|
||||
class DemoMoreInfoUpdate extends LitElement {
|
||||
@property() public hass!: MockHomeAssistant;
|
||||
|
||||
@query("demo-more-infos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<demo-more-infos
|
||||
.hass=${this.hass}
|
||||
.entities=${ENTITIES.map((ent) => ent.entityId)}
|
||||
></demo-more-infos>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-more-info-update": DemoMoreInfoUpdate;
|
||||
}
|
||||
}
|
17
gallery/src/pages/user-test/user-types.markdown
Normal file
17
gallery/src/pages/user-test/user-types.markdown
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: "User types"
|
||||
---
|
||||
|
||||
We have defined three user types for Home Assistant. They are a lean segmentation of users that helps us make decisions throughout the product. User types differ from traditional personas in that the segmentation criteria aren’t demographic and don’t personify a group into a single character with a fictitious background story.
|
||||
|
||||
# Outgrowers
|
||||
|
||||
Users that outgrow big tech smart home solutions. It just needs to work with easy setup via an app.
|
||||
|
||||
# Tinkerers
|
||||
|
||||
Technoid users in home networking and development that know how to code.
|
||||
|
||||
# Questioner
|
||||
|
||||
Users who want more advanced home automation, but need support to make it work.
|
@@ -45,7 +45,6 @@ import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
import { addonArchIsSupported, extractChangelog } from "../util/addon";
|
||||
|
||||
@@ -55,6 +54,12 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const SUPERVISOR_UPDATE_NAMES = {
|
||||
core: "Home Assistant Core",
|
||||
os: "Home Assistant Operating System",
|
||||
supervisor: "Home Assistant Supervisor",
|
||||
};
|
||||
|
||||
type updateType = "os" | "supervisor" | "core" | "addon";
|
||||
|
||||
const changelogUrl = (
|
||||
|
@@ -72,8 +72,8 @@
|
||||
"@material/mwc-textfield": "0.25.3",
|
||||
"@material/mwc-top-app-bar-fixed": "^0.25.3",
|
||||
"@material/top-app-bar": "14.0.0-canary.261f2db59.0",
|
||||
"@mdi/js": "6.5.95",
|
||||
"@mdi/svg": "6.5.95",
|
||||
"@mdi/js": "6.6.95",
|
||||
"@mdi/svg": "6.6.95",
|
||||
"@polymer/app-layout": "^3.1.0",
|
||||
"@polymer/iron-flex-layout": "^3.0.1",
|
||||
"@polymer/iron-icon": "^3.0.1",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = home-assistant-frontend
|
||||
version = 20220317.0
|
||||
version = 20220329.0
|
||||
author = The Home Assistant Authors
|
||||
author_email = hello@home-assistant.io
|
||||
license = Apache-2.0
|
||||
|
7
setup.py
7
setup.py
@@ -1,7 +0,0 @@
|
||||
"""
|
||||
Entry point for setuptools. Required for editable installs.
|
||||
TODO: Remove file after updating to pip 21.3
|
||||
"""
|
||||
from setuptools import setup
|
||||
|
||||
setup()
|
@@ -187,6 +187,7 @@ export const DOMAINS_WITH_MORE_INFO = [
|
||||
"scene",
|
||||
"sun",
|
||||
"timer",
|
||||
"update",
|
||||
"vacuum",
|
||||
"water_heater",
|
||||
"weather",
|
||||
@@ -200,6 +201,7 @@ export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [
|
||||
"input_text",
|
||||
"number",
|
||||
"scene",
|
||||
"update",
|
||||
"select",
|
||||
];
|
||||
|
||||
|
@@ -1,12 +1,18 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import {
|
||||
updateIsInstalling,
|
||||
UpdateEntity,
|
||||
UPDATE_SUPPORT_PROGRESS,
|
||||
} from "../../data/update";
|
||||
import { formatDate } from "../datetime/format_date";
|
||||
import { formatDateTime } from "../datetime/format_date_time";
|
||||
import { formatTime } from "../datetime/format_time";
|
||||
import { formatNumber, isNumericState } from "../number/format_number";
|
||||
import { LocalizeFunc } from "../translations/localize";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
import { supportsFeature } from "./supports-feature";
|
||||
|
||||
export const computeStateDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
@@ -130,6 +136,28 @@ export const computeStateDisplay = (
|
||||
}
|
||||
}
|
||||
|
||||
if (domain === "update") {
|
||||
// When updating, and entity does not support % show "Installing"
|
||||
// When updating, and entity does support % show "Installing (xx%)"
|
||||
// When update available, show the version
|
||||
// When the latest version is skipped, show the latest version
|
||||
// When update is not available, show "Up-to-date"
|
||||
// When update is not available and there is no latest_version show "Unavailable"
|
||||
return compareState === "on"
|
||||
? updateIsInstalling(stateObj as UpdateEntity)
|
||||
? supportsFeature(stateObj, UPDATE_SUPPORT_PROGRESS)
|
||||
? localize("ui.card.update.installing_with_progress", {
|
||||
progress: stateObj.attributes.in_progress,
|
||||
})
|
||||
: localize("ui.card.update.installing")
|
||||
: stateObj.attributes.latest_version
|
||||
: stateObj.attributes.skipped_version ===
|
||||
stateObj.attributes.latest_version
|
||||
? stateObj.attributes.latest_version ??
|
||||
localize("state.default.unavailable")
|
||||
: localize("ui.card.update.up_to_date");
|
||||
}
|
||||
|
||||
return (
|
||||
// Return device class translation
|
||||
(stateObj.attributes.device_class &&
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
|
||||
export const computeStateDomain = (stateObj: HassEntity) =>
|
||||
|
@@ -26,8 +26,11 @@ import {
|
||||
mdiCheckCircleOutline,
|
||||
mdiCloseCircleOutline,
|
||||
mdiWeatherNight,
|
||||
mdiPackage,
|
||||
mdiPackageDown,
|
||||
} from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { updateIsInstalling, UpdateEntity } from "../../data/update";
|
||||
/**
|
||||
* Return the icon to be used for a domain.
|
||||
*
|
||||
@@ -133,6 +136,13 @@ export const domainIcon = (
|
||||
return stateObj?.state === "above_horizon"
|
||||
? FIXED_DOMAIN_ICONS[domain]
|
||||
: mdiWeatherNight;
|
||||
|
||||
case "update":
|
||||
return compareState === "on"
|
||||
? updateIsInstalling(stateObj as UpdateEntity)
|
||||
? mdiPackageDown
|
||||
: mdiPackageUp
|
||||
: mdiPackage;
|
||||
}
|
||||
|
||||
if (domain in FIXED_DOMAIN_ICONS) {
|
||||
|
@@ -46,6 +46,22 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
@property({ type: Array, attribute: "include-unit-of-measurement" })
|
||||
public includeUnitOfMeasurement?: string[];
|
||||
|
||||
/**
|
||||
* List of allowed entities to show. Will ignore all other filters.
|
||||
* @type {Array}
|
||||
* @attr include-entities
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-entities" })
|
||||
public includeEntities?: string[];
|
||||
|
||||
/**
|
||||
* List of entities to be excluded.
|
||||
* @type {Array}
|
||||
* @attr exclude-entities
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-entities" })
|
||||
public excludeEntities?: string[];
|
||||
|
||||
@property({ attribute: "picked-entity-label" })
|
||||
public pickedEntityLabel?: string;
|
||||
|
||||
@@ -69,6 +85,8 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeEntities=${this.includeEntities}
|
||||
.excludeEntities=${this.excludeEntities}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
||||
.entityFilter=${this._entityFilter}
|
||||
@@ -84,6 +102,8 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeEntities=${this.includeEntities}
|
||||
.excludeEntities=${this.excludeEntities}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
||||
.entityFilter=${this._entityFilter}
|
||||
|
@@ -7,6 +7,7 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-combo-box";
|
||||
@@ -77,6 +78,22 @@ export class HaEntityPicker extends LitElement {
|
||||
@property({ type: Array, attribute: "include-unit-of-measurement" })
|
||||
public includeUnitOfMeasurement?: string[];
|
||||
|
||||
/**
|
||||
* List of allowed entities to show. Will ignore all other filters.
|
||||
* @type {Array}
|
||||
* @attr include-entities
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-entities" })
|
||||
public includeEntities?: string[];
|
||||
|
||||
/**
|
||||
* List of entities to be excluded.
|
||||
* @type {Array}
|
||||
* @attr exclude-entities
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-entities" })
|
||||
public excludeEntities?: string[];
|
||||
|
||||
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
|
||||
@property({ type: Boolean }) public hideClearIcon = false;
|
||||
@@ -109,7 +126,9 @@ export class HaEntityPicker extends LitElement {
|
||||
excludeDomains: this["excludeDomains"],
|
||||
entityFilter: this["entityFilter"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
includeUnitOfMeasurement: this["includeUnitOfMeasurement"]
|
||||
includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
|
||||
includeEntities: this["includeEntities"],
|
||||
excludeEntities: this["excludeEntities"]
|
||||
): HassEntityWithCachedName[] => {
|
||||
let states: HassEntityWithCachedName[] = [];
|
||||
|
||||
@@ -139,6 +158,30 @@ export class HaEntityPicker extends LitElement {
|
||||
];
|
||||
}
|
||||
|
||||
if (includeEntities) {
|
||||
entityIds = entityIds.filter((entityId) =>
|
||||
this.includeEntities!.includes(entityId)
|
||||
);
|
||||
|
||||
return entityIds
|
||||
.map((key) => ({
|
||||
...hass!.states[key],
|
||||
friendly_name: computeStateName(hass!.states[key]) || key,
|
||||
}))
|
||||
.sort((entityA, entityB) =>
|
||||
caseInsensitiveStringCompare(
|
||||
entityA.friendly_name,
|
||||
entityB.friendly_name
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeEntities) {
|
||||
entityIds = entityIds.filter(
|
||||
(entityId) => !excludeEntities!.includes(entityId)
|
||||
);
|
||||
}
|
||||
|
||||
if (includeDomains) {
|
||||
entityIds = entityIds.filter((eid) =>
|
||||
includeDomains.includes(computeDomain(eid))
|
||||
@@ -151,10 +194,17 @@ export class HaEntityPicker extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
states = entityIds.sort().map((key) => ({
|
||||
...hass!.states[key],
|
||||
friendly_name: computeStateName(hass!.states[key]) || key,
|
||||
}));
|
||||
states = entityIds
|
||||
.map((key) => ({
|
||||
...hass!.states[key],
|
||||
friendly_name: computeStateName(hass!.states[key]) || key,
|
||||
}))
|
||||
.sort((entityA, entityB) =>
|
||||
caseInsensitiveStringCompare(
|
||||
entityA.friendly_name,
|
||||
entityB.friendly_name
|
||||
)
|
||||
);
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
states = states.filter(
|
||||
@@ -231,7 +281,9 @@ export class HaEntityPicker extends LitElement {
|
||||
this.excludeDomains,
|
||||
this.entityFilter,
|
||||
this.includeDeviceClasses,
|
||||
this.includeUnitOfMeasurement
|
||||
this.includeUnitOfMeasurement,
|
||||
this.includeEntities,
|
||||
this.excludeEntities
|
||||
);
|
||||
if (this._initedStates) {
|
||||
(this.comboBox as any).filteredItems = this._states;
|
||||
|
160
src/components/ha-areas-picker.ts
Normal file
160
src/components/ha-areas-picker.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { EntityRegistryEntry } from "../data/entity_registry";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-area-picker";
|
||||
|
||||
@customElement("ha-areas-picker")
|
||||
export class HaAreasPicker extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string[];
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-add" })
|
||||
public noAdd?: boolean;
|
||||
|
||||
/**
|
||||
* Show only areas with entities from specific domains.
|
||||
* @type {Array}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show no areas with entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only areas with entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
|
||||
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
|
||||
|
||||
@property({ attribute: "picked-area-label" })
|
||||
public pickedAreaLabel?: string;
|
||||
|
||||
@property({ attribute: "pick-area-label" })
|
||||
public pickAreaLabel?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled?: boolean;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const currentAreas = this._currentAreas;
|
||||
return html`
|
||||
${currentAreas.map(
|
||||
(area) => html`
|
||||
<div>
|
||||
<ha-area-picker
|
||||
.curValue=${area}
|
||||
.noAdd=${this.noAdd}
|
||||
.hass=${this.hass}
|
||||
.value=${area}
|
||||
.label=${this.pickedAreaLabel}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._areaChanged}
|
||||
></ha-area-picker>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<div>
|
||||
<ha-area-picker
|
||||
.noAdd=${this.noAdd}
|
||||
.hass=${this.hass}
|
||||
.label=${this.pickAreaLabel}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.disabled=${this.disabled}
|
||||
.placeholder=${this.placeholder}
|
||||
@value-changed=${this._addArea}
|
||||
></ha-area-picker>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _currentAreas(): string[] {
|
||||
return this.value || [];
|
||||
}
|
||||
|
||||
private async _updateAreas(areas) {
|
||||
this.value = areas;
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: areas,
|
||||
});
|
||||
}
|
||||
|
||||
private _areaChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const curValue = (ev.currentTarget as any).curValue;
|
||||
const newValue = ev.detail.value;
|
||||
if (newValue === curValue) {
|
||||
return;
|
||||
}
|
||||
const currentAreas = this._currentAreas;
|
||||
if (!newValue || currentAreas.includes(newValue)) {
|
||||
this._updateAreas(currentAreas.filter((ent) => ent !== curValue));
|
||||
return;
|
||||
}
|
||||
this._updateAreas(
|
||||
currentAreas.map((ent) => (ent === curValue ? newValue : ent))
|
||||
);
|
||||
}
|
||||
|
||||
private _addArea(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
|
||||
const toAdd = ev.detail.value;
|
||||
if (!toAdd) {
|
||||
return;
|
||||
}
|
||||
(ev.currentTarget as any).value = "";
|
||||
const currentAreas = this._currentAreas;
|
||||
if (currentAreas.includes(toAdd)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateAreas([...currentAreas, toAdd]);
|
||||
}
|
||||
|
||||
static override styles = css`
|
||||
div {
|
||||
margin-top: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-areas-picker": HaAreasPicker;
|
||||
}
|
||||
}
|
@@ -1,12 +1,13 @@
|
||||
import { LitElement, html, TemplateResult, css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "./ha-select";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "./ha-textfield";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import "./ha-select";
|
||||
import "./ha-textfield";
|
||||
|
||||
export interface TimeChangedEvent {
|
||||
days?: number;
|
||||
hours: number;
|
||||
minutes: number;
|
||||
seconds: number;
|
||||
@@ -21,6 +22,11 @@ export class HaBaseTimeInput extends LitElement {
|
||||
*/
|
||||
@property() label?: string;
|
||||
|
||||
/**
|
||||
* Helper for the input
|
||||
*/
|
||||
@property() helper?: string;
|
||||
|
||||
/**
|
||||
* auto validate time inputs
|
||||
*/
|
||||
@@ -41,6 +47,11 @@ export class HaBaseTimeInput extends LitElement {
|
||||
*/
|
||||
@property({ type: Boolean }) disabled = false;
|
||||
|
||||
/**
|
||||
* day
|
||||
*/
|
||||
@property({ type: Number }) days = 0;
|
||||
|
||||
/**
|
||||
* hour
|
||||
*/
|
||||
@@ -61,6 +72,11 @@ export class HaBaseTimeInput extends LitElement {
|
||||
*/
|
||||
@property({ type: Number }) milliseconds = 0;
|
||||
|
||||
/**
|
||||
* Label for the day input
|
||||
*/
|
||||
@property() dayLabel = "";
|
||||
|
||||
/**
|
||||
* Label for the hour input
|
||||
*/
|
||||
@@ -91,6 +107,11 @@ export class HaBaseTimeInput extends LitElement {
|
||||
*/
|
||||
@property({ type: Boolean }) enableMillisecond = false;
|
||||
|
||||
/**
|
||||
* show the day field
|
||||
*/
|
||||
@property({ type: Boolean }) enableDay = false;
|
||||
|
||||
/**
|
||||
* limit hours input
|
||||
*/
|
||||
@@ -110,6 +131,29 @@ export class HaBaseTimeInput extends LitElement {
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : ""}
|
||||
<div class="time-input-wrap">
|
||||
${this.enableDay
|
||||
? html`
|
||||
<ha-textfield
|
||||
id="day"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
.value=${this.days}
|
||||
.label=${this.dayLabel}
|
||||
name="days"
|
||||
@input=${this._valueChanged}
|
||||
@focus=${this._onFocus}
|
||||
no-spinner
|
||||
.required=${this.required}
|
||||
.autoValidate=${this.autoValidate}
|
||||
min="0"
|
||||
.disabled=${this.disabled}
|
||||
suffix=":"
|
||||
class="hasSuffix"
|
||||
>
|
||||
</ha-textfield>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<ha-textfield
|
||||
id="hour"
|
||||
type="number"
|
||||
@@ -207,6 +251,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
<mwc-list-item value="PM">PM</mwc-list-item>
|
||||
</ha-select>`}
|
||||
</div>
|
||||
${this.helper ? html`<div class="helper">${this.helper}</div>` : ""}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -303,6 +348,13 @@ export class HaBaseTimeInput extends LitElement {
|
||||
color: var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87));
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.helper {
|
||||
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
|
||||
font-size: 0.75rem;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -5,6 +5,7 @@ import "./ha-base-time-input";
|
||||
import type { TimeChangedEvent } from "./ha-base-time-input";
|
||||
|
||||
export interface HaDurationData {
|
||||
days?: number;
|
||||
hours?: number;
|
||||
minutes?: number;
|
||||
seconds?: number;
|
||||
@@ -17,10 +18,14 @@ class HaDurationInput extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public required?: boolean;
|
||||
|
||||
@property({ type: Boolean }) public enableMillisecond?: boolean;
|
||||
|
||||
@property({ type: Boolean }) public enableDay?: boolean;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("paper-time-input", true) private _input?: HTMLElement;
|
||||
@@ -35,19 +40,23 @@ class HaDurationInput extends LitElement {
|
||||
return html`
|
||||
<ha-base-time-input
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.required=${this.required}
|
||||
.autoValidate=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
errorMessage="Required"
|
||||
enableSecond
|
||||
.enableMillisecond=${this.enableMillisecond}
|
||||
.enableDay=${this.enableDay}
|
||||
format="24"
|
||||
.days=${this._days}
|
||||
.hours=${this._hours}
|
||||
.minutes=${this._minutes}
|
||||
.seconds=${this._seconds}
|
||||
.milliseconds=${this._milliseconds}
|
||||
@value-changed=${this._durationChanged}
|
||||
noHoursLimit
|
||||
dayLabel="dd"
|
||||
hourLabel="hh"
|
||||
minLabel="mm"
|
||||
secLabel="ss"
|
||||
@@ -56,6 +65,10 @@ class HaDurationInput extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private get _days() {
|
||||
return this.data?.days ? Number(this.data.days) : 0;
|
||||
}
|
||||
|
||||
private get _hours() {
|
||||
return this.data?.hours ? Number(this.data.hours) : 0;
|
||||
}
|
||||
@@ -94,6 +107,11 @@ class HaDurationInput extends LitElement {
|
||||
value.minutes %= 60;
|
||||
}
|
||||
|
||||
if (this.enableDay && value.hours > 24) {
|
||||
value.days = (value.days ?? 0) + Math.floor(value.hours / 24);
|
||||
value.hours %= 24;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
});
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { HaFormSchema } from "./types";
|
||||
import type { Selector } from "../../data/selector";
|
||||
import type { HaFormSchema } from "./types";
|
||||
|
||||
export const computeInitialHaFormData = (
|
||||
schema: HaFormSchema[]
|
||||
@@ -31,6 +32,55 @@ export const computeInitialHaFormData = (
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
};
|
||||
} else if ("selector" in field) {
|
||||
const selector: Selector = field.selector;
|
||||
|
||||
if ("device" in selector) {
|
||||
data[field.name] = selector.device.multiple ? [] : "";
|
||||
} else if ("entity" in selector) {
|
||||
data[field.name] = selector.entity.multiple ? [] : "";
|
||||
} else if ("area" in selector) {
|
||||
data[field.name] = selector.area.multiple ? [] : "";
|
||||
} else if ("boolean" in selector) {
|
||||
data[field.name] = false;
|
||||
} else if (
|
||||
"text" in selector ||
|
||||
"addon" in selector ||
|
||||
"attribute" in selector ||
|
||||
"icon" in selector ||
|
||||
"theme" in selector
|
||||
) {
|
||||
data[field.name] = "";
|
||||
} else if ("number" in selector) {
|
||||
data[field.name] = selector.number.min ?? 0;
|
||||
} else if ("select" in selector) {
|
||||
if (selector.select.options.length) {
|
||||
data[field.name] = selector.select.options[0][0];
|
||||
}
|
||||
} else if ("duration" in selector) {
|
||||
data[field.name] = {
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
};
|
||||
} else if ("time" in selector) {
|
||||
data[field.name] = "00:00:00";
|
||||
} else if ("date" in selector || "datetime" in selector) {
|
||||
const now = new Date().toISOString().slice(0, 10);
|
||||
data[field.name] = `${now} 00:00:00`;
|
||||
} else if ("color_rgb" in selector) {
|
||||
data[field.name] = [0, 0, 0];
|
||||
} else if ("color_temp" in selector) {
|
||||
data[field.name] = selector.color_temp.min_mireds ?? 153;
|
||||
} else if (
|
||||
"action" in selector ||
|
||||
"media" in selector ||
|
||||
"target" in selector
|
||||
) {
|
||||
data[field.name] = {};
|
||||
} else {
|
||||
throw new Error("Selector not supported in initial form data");
|
||||
}
|
||||
}
|
||||
});
|
||||
return data;
|
||||
|
@@ -28,6 +28,8 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
|
||||
@property() public label!: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state() private _unmaskedPassword = false;
|
||||
@@ -53,6 +55,8 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
: "password"}
|
||||
.label=${this.label}
|
||||
.value=${this.data || ""}
|
||||
.helper=${this.helper}
|
||||
helperPersistent
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.schema.required}
|
||||
.autoValidate=${this.schema.required}
|
||||
|
@@ -6,6 +6,7 @@ import { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import { AreaSelector } from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-area-picker";
|
||||
import "../ha-areas-picker";
|
||||
|
||||
@customElement("ha-selector-area")
|
||||
export class HaAreaSelector extends LitElement {
|
||||
@@ -28,27 +29,53 @@ export class HaAreaSelector extends LitElement {
|
||||
oldSelector !== this.selector &&
|
||||
this.selector.area.device?.integration
|
||||
) {
|
||||
this._loadConfigEntries();
|
||||
getConfigEntries(this.hass, {
|
||||
domain: this.selector.area.device.integration,
|
||||
}).then((entries) => {
|
||||
this._configEntries = entries;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
no-add
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.includeDeviceClasses=${this.selector.area.entity?.device_class
|
||||
? [this.selector.area.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.area.entity?.domain
|
||||
? [this.selector.area.entity.domain]
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
></ha-area-picker>`;
|
||||
if (!this.selector.area.multiple) {
|
||||
return html`
|
||||
<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
no-add
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.includeDeviceClasses=${this.selector.area.entity?.device_class
|
||||
? [this.selector.area.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.area.entity?.domain
|
||||
? [this.selector.area.entity.domain]
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
></ha-area-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-areas-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.pickAreaLabel=${this.label}
|
||||
no-add
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.includeDeviceClasses=${this.selector.area.entity?.device_class
|
||||
? [this.selector.area.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.area.entity?.domain
|
||||
? [this.selector.area.entity.domain]
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
></ha-areas-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _filterEntities = (entity: EntityRegistryEntry): boolean => {
|
||||
@@ -85,12 +112,6 @@ export class HaAreaSelector extends LitElement {
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
this._configEntries = (await getConfigEntries(this.hass)).filter(
|
||||
(entry) => entry.domain === this.selector.area.device?.integration
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -26,6 +26,7 @@ export class HaDateTimeSelector extends LitElement {
|
||||
|
||||
protected render() {
|
||||
const values = this.value?.split(" ");
|
||||
|
||||
return html`
|
||||
<ha-date-input
|
||||
.label=${this.label}
|
||||
@@ -37,7 +38,7 @@ export class HaDateTimeSelector extends LitElement {
|
||||
</ha-date-input>
|
||||
<ha-time-input
|
||||
enable-second
|
||||
.value=${values?.[1] || "00:00:00"}
|
||||
.value=${values?.[1] || "0:00:00"}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._valueChanged}
|
||||
|
@@ -25,7 +25,11 @@ export class HaDeviceSelector extends LitElement {
|
||||
if (changedProperties.has("selector")) {
|
||||
const oldSelector = changedProperties.get("selector");
|
||||
if (oldSelector !== this.selector && this.selector.device?.integration) {
|
||||
this._loadConfigEntries();
|
||||
getConfigEntries(this.hass, {
|
||||
domain: this.selector.device.integration,
|
||||
}).then((entries) => {
|
||||
this._configEntries = entries;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,12 +92,6 @@ export class HaDeviceSelector extends LitElement {
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
this._configEntries = (await getConfigEntries(this.hass)).filter(
|
||||
(entry) => entry.domain === this.selector.device.integration
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import "../ha-duration-input";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { DurationSelector } from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import type { DurationSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-duration-input";
|
||||
|
||||
@customElement("ha-selector-duration")
|
||||
export class HaTimeDuration extends LitElement {
|
||||
@@ -14,6 +14,8 @@ export class HaTimeDuration extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
@@ -22,9 +24,11 @@ export class HaTimeDuration extends LitElement {
|
||||
return html`
|
||||
<ha-duration-input
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.data=${this.value}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.enableDay=${this.selector.duration.enable_day}
|
||||
></ha-duration-input>
|
||||
`;
|
||||
}
|
||||
|
@@ -6,8 +6,8 @@ import { subscribeEntityRegistry } from "../../data/entity_registry";
|
||||
import { EntitySelector } from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entity-picker";
|
||||
import "../entity/ha-entities-picker";
|
||||
import "../entity/ha-entity-picker";
|
||||
|
||||
@customElement("ha-selector-entity")
|
||||
export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
||||
@@ -29,6 +29,8 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.includeEntities=${this.selector.entity.include_entities}
|
||||
.excludeEntities=${this.selector.entity.exclude_entities}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.disabled=${this.disabled}
|
||||
allow-custom-entity
|
||||
@@ -41,6 +43,8 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.includeEntities=${this.selector.entity.include_entities}
|
||||
.excludeEntities=${this.selector.entity.exclude_entities}
|
||||
></ha-entities-picker>
|
||||
`;
|
||||
}
|
||||
|
@@ -6,7 +6,6 @@ import type {
|
||||
LocationSelector,
|
||||
LocationSelectorValue,
|
||||
} from "../../data/selector";
|
||||
import "../../panels/lovelace/components/hui-theme-select-editor";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { MarkerLocation } from "../map/ha-locations-editor";
|
||||
import "../map/ha-locations-editor";
|
||||
@@ -52,7 +51,10 @@ export class HaLocationSelector extends LitElement {
|
||||
longitude: value?.longitude || this.hass.config.longitude,
|
||||
radius: selector.location.radius ? value?.radius || 1000 : undefined,
|
||||
radius_color: zoneRadiusColor,
|
||||
icon: selector.location.icon,
|
||||
icon:
|
||||
selector.location.icon || selector.location.radius
|
||||
? "mdi:map-marker-radius"
|
||||
: "mdi:map-marker",
|
||||
location_editable: true,
|
||||
radius_editable: true,
|
||||
},
|
||||
|
@@ -19,6 +19,8 @@ export class HaNumberSelector extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
@@ -46,8 +48,10 @@ export class HaNumberSelector extends LitElement {
|
||||
class=${classMap({ single: this.selector.number.mode === "box" })}
|
||||
.min=${this.selector.number.min}
|
||||
.max=${this.selector.number.max}
|
||||
.value=${this.value || ""}
|
||||
.value=${this.value ?? ""}
|
||||
.step=${this.selector.number.step ?? 1}
|
||||
helperPersistent
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.suffix=${this.selector.number.unit_of_measurement}
|
||||
|
@@ -134,9 +134,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
||||
private async _loadConfigEntries() {
|
||||
this._configEntries = (await getConfigEntries(this.hass)).filter(
|
||||
(entry) =>
|
||||
entry.domain ===
|
||||
(this.selector.target.device?.integration ||
|
||||
this.selector.target.entity?.integration)
|
||||
entry.domain === this.selector.target.device?.integration ||
|
||||
entry.domain === this.selector.target.entity?.integration
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -18,6 +18,8 @@ export class HaTextSelector extends LitElement {
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property() public selector!: StringSelector;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
@@ -32,6 +34,8 @@ export class HaTextSelector extends LitElement {
|
||||
.label=${this.label}
|
||||
.placeholder=${this.placeholder}
|
||||
.value=${this.value || ""}
|
||||
.helper=${this.helper}
|
||||
helperPersistent
|
||||
.disabled=${this.disabled}
|
||||
@input=${this._handleChange}
|
||||
autocapitalize="none"
|
||||
@@ -44,6 +48,8 @@ export class HaTextSelector extends LitElement {
|
||||
return html`<ha-textfield
|
||||
.value=${this.value || ""}
|
||||
.placeholder=${this.placeholder || ""}
|
||||
.helper=${this.helper}
|
||||
helperPersistent
|
||||
.disabled=${this.disabled}
|
||||
.type=${this._unmaskedPassword ? "text" : this.selector.text?.type}
|
||||
@input=${this._handleChange}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import "../../panels/lovelace/components/hui-theme-select-editor";
|
||||
import "../ha-theme-picker";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -18,11 +18,11 @@ export class HaThemeSelector extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<hui-theme-select-editor
|
||||
<ha-theme-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
></hui-theme-select-editor>
|
||||
></ha-theme-picker>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -471,6 +471,7 @@ export class HaServiceControl extends LitElement {
|
||||
}
|
||||
ha-settings-row {
|
||||
--paper-time-input-justify-content: flex-end;
|
||||
--settings-row-content-width: 100%;
|
||||
border-top: var(
|
||||
--service-control-items-border-top,
|
||||
1px solid var(--divider-color)
|
||||
@@ -489,9 +490,6 @@ export class HaServiceControl extends LitElement {
|
||||
margin: var(--service-control-padding, 0 16px);
|
||||
padding: 16px 0;
|
||||
}
|
||||
:host(:not([narrow])) ha-settings-row ha-selector {
|
||||
width: 60%;
|
||||
}
|
||||
.checkbox-spacer {
|
||||
width: 32px;
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ export class HaSettingsRow extends LitElement {
|
||||
<div secondary><slot name="description"></slot></div>
|
||||
</paper-item-body>
|
||||
</div>
|
||||
<slot></slot>
|
||||
<div class="content"><slot></slot></div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,18 @@ export class HaSettingsRow extends LitElement {
|
||||
);
|
||||
flex: 1;
|
||||
}
|
||||
.content {
|
||||
display: contents;
|
||||
}
|
||||
:host(:not([narrow])) .content {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
padding: 16px 0;
|
||||
}
|
||||
.content ::slotted(*) {
|
||||
width: var(--settings-row-content-width);
|
||||
}
|
||||
:host([narrow]) {
|
||||
align-items: normal;
|
||||
flex-direction: column;
|
||||
|
@@ -37,6 +37,7 @@ import { LocalStorage } from "../common/decorators/local-storage";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { toggleAttribute } from "../common/dom/toggle_attribute";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { ActionHandlerDetail } from "../data/lovelace";
|
||||
@@ -44,6 +45,7 @@ import {
|
||||
PersistentNotification,
|
||||
subscribeNotifications,
|
||||
} from "../data/persistent_notification";
|
||||
import { updateCanInstall, UpdateEntity } from "../data/update";
|
||||
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, PanelInfo, Route } from "../types";
|
||||
@@ -68,7 +70,6 @@ const SORT_VALUE_URL_PATHS = {
|
||||
|
||||
const PANEL_ICONS = {
|
||||
calendar: mdiCalendar,
|
||||
config: mdiCog,
|
||||
"developer-tools": mdiHammer,
|
||||
energy: mdiLightningBolt,
|
||||
history: mdiChartBox,
|
||||
@@ -190,6 +191,8 @@ class HaSidebar extends LitElement {
|
||||
|
||||
@state() private _notifications?: PersistentNotification[];
|
||||
|
||||
@state() private _updatesCount = 0;
|
||||
|
||||
@state() private _renderEmptySortable = false;
|
||||
|
||||
private _mouseLeaveTimeout?: number;
|
||||
@@ -235,6 +238,7 @@ class HaSidebar extends LitElement {
|
||||
changedProps.has("narrow") ||
|
||||
changedProps.has("alwaysExpand") ||
|
||||
changedProps.has("_externalConfig") ||
|
||||
changedProps.has("_updatesCount") ||
|
||||
changedProps.has("_notifications") ||
|
||||
changedProps.has("editMode") ||
|
||||
changedProps.has("_renderEmptySortable") ||
|
||||
@@ -290,6 +294,12 @@ class HaSidebar extends LitElement {
|
||||
toggleAttribute(this, "rtl", computeRTL(this.hass));
|
||||
}
|
||||
|
||||
this._updatesCount = Object.values(this.hass.states).filter(
|
||||
(entity) =>
|
||||
computeStateDomain(entity) === "update" &&
|
||||
updateCanInstall(entity as UpdateEntity)
|
||||
).length;
|
||||
|
||||
if (!SUPPORT_SCROLL_IF_NEEDED) {
|
||||
return;
|
||||
}
|
||||
@@ -387,35 +397,37 @@ class HaSidebar extends LitElement {
|
||||
icon?: string | null,
|
||||
iconPath?: string | null
|
||||
) {
|
||||
return html`
|
||||
<a
|
||||
role="option"
|
||||
href=${`/${urlPath}`}
|
||||
data-panel=${urlPath}
|
||||
tabindex="-1"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item>
|
||||
${iconPath
|
||||
? html`<ha-svg-icon
|
||||
slot="item-icon"
|
||||
.path=${iconPath}
|
||||
></ha-svg-icon>`
|
||||
: html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`}
|
||||
<span class="item-text">${title}</span>
|
||||
</paper-icon-item>
|
||||
${this.editMode
|
||||
? html`<ha-icon-button
|
||||
.label=${this.hass.localize("ui.sidebar.hide_panel")}
|
||||
.path=${mdiClose}
|
||||
class="hide-panel"
|
||||
.panel=${urlPath}
|
||||
@click=${this._hidePanel}
|
||||
></ha-icon-button>`
|
||||
: ""}
|
||||
</a>
|
||||
`;
|
||||
return urlPath === "config"
|
||||
? this._renderConfiguration(title)
|
||||
: html`
|
||||
<a
|
||||
role="option"
|
||||
href=${`/${urlPath}`}
|
||||
data-panel=${urlPath}
|
||||
tabindex="-1"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item>
|
||||
${iconPath
|
||||
? html`<ha-svg-icon
|
||||
slot="item-icon"
|
||||
.path=${iconPath}
|
||||
></ha-svg-icon>`
|
||||
: html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`}
|
||||
<span class="item-text">${title}</span>
|
||||
</paper-icon-item>
|
||||
${this.editMode
|
||||
? html`<ha-icon-button
|
||||
.label=${this.hass.localize("ui.sidebar.hide_panel")}
|
||||
.path=${mdiClose}
|
||||
class="hide-panel"
|
||||
.panel=${urlPath}
|
||||
@click=${this._hidePanel}
|
||||
></ha-icon-button>`
|
||||
: ""}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPanelsEdit(beforeSpacer: PanelInfo[]) {
|
||||
@@ -477,6 +489,35 @@ class HaSidebar extends LitElement {
|
||||
return html`<div class="spacer" disabled></div>`;
|
||||
}
|
||||
|
||||
private _renderConfiguration(title: string | null) {
|
||||
return html` <a
|
||||
class="configuration-container"
|
||||
role="option"
|
||||
href="/config"
|
||||
data-panel="config"
|
||||
tabindex="-1"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item class="configuration" role="option">
|
||||
<ha-svg-icon slot="item-icon" .path=${mdiCog}></ha-svg-icon>
|
||||
${!this.alwaysExpand && this._updatesCount > 0
|
||||
? html`
|
||||
<span class="configuration-badge" slot="item-icon">
|
||||
${this._updatesCount}
|
||||
</span>
|
||||
`
|
||||
: ""}
|
||||
<span class="item-text">${title}</span>
|
||||
${this.alwaysExpand && this._updatesCount > 0
|
||||
? html`
|
||||
<span class="configuration-badge">${this._updatesCount}</span>
|
||||
`
|
||||
: ""}
|
||||
</paper-icon-item>
|
||||
</a>`;
|
||||
}
|
||||
|
||||
private _renderNotifications() {
|
||||
let notificationCount = this._notifications
|
||||
? this._notifications.length
|
||||
@@ -953,18 +994,21 @@ class HaSidebar extends LitElement {
|
||||
height: 1px;
|
||||
background-color: var(--divider-color);
|
||||
}
|
||||
.notifications-container {
|
||||
.notifications-container,
|
||||
.configuration-container {
|
||||
display: flex;
|
||||
margin-left: env(safe-area-inset-left);
|
||||
}
|
||||
:host([rtl]) .notifications-container {
|
||||
:host([rtl]) .notifications-container,
|
||||
:host([rtl]) .configuration-container {
|
||||
margin-left: initial;
|
||||
margin-right: env(safe-area-inset-right);
|
||||
}
|
||||
.notifications {
|
||||
cursor: pointer;
|
||||
}
|
||||
.notifications .item-text {
|
||||
.notifications .item-text,
|
||||
.configuration .item-text {
|
||||
flex: 1;
|
||||
}
|
||||
.profile {
|
||||
@@ -988,7 +1032,8 @@ class HaSidebar extends LitElement {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
.notification-badge,
|
||||
.configuration-badge {
|
||||
min-width: 20px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
@@ -999,7 +1044,11 @@ class HaSidebar extends LitElement {
|
||||
padding: 0px 6px;
|
||||
color: var(--text-accent-color, var(--text-primary-color));
|
||||
}
|
||||
ha-svg-icon + .notification-badge {
|
||||
.configuration-badge {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
ha-svg-icon + .notification-badge,
|
||||
ha-svg-icon + .configuration-badge {
|
||||
position: absolute;
|
||||
bottom: 14px;
|
||||
left: 26px;
|
||||
|
@@ -2,13 +2,13 @@ import "@material/mwc-button";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import "../../../components/ha-select";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import "./ha-select";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("hui-theme-select-editor")
|
||||
export class HuiThemeSelectEditor extends LitElement {
|
||||
@customElement("ha-theme-picker")
|
||||
export class HaThemePicker extends LitElement {
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
@@ -19,11 +19,7 @@ export class HuiThemeSelectEditor extends LitElement {
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
`${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`}
|
||||
this.hass!.localize("ui.components.theme_picker.theme")}
|
||||
.value=${this.value}
|
||||
@selected=${this._changed}
|
||||
@closed=${stopPropagation}
|
||||
@@ -32,7 +28,7 @@ export class HuiThemeSelectEditor extends LitElement {
|
||||
>
|
||||
<mwc-list-item value="remove"
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.no_theme"
|
||||
"ui.components.theme_picker.no_theme"
|
||||
)}</mwc-list-item
|
||||
>
|
||||
${Object.keys(this.hass!.themes.themes)
|
||||
@@ -64,6 +60,6 @@ export class HuiThemeSelectEditor extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-theme-select-editor": HuiThemeSelectEditor;
|
||||
"ha-theme-picker": HaThemePicker;
|
||||
}
|
||||
}
|
@@ -488,6 +488,14 @@ export class HaMap extends ReactiveElement {
|
||||
text-align: center;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.leaflet-pane {
|
||||
z-index: 0 !important;
|
||||
}
|
||||
.leaflet-control,
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
z-index: 1 !important;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -62,6 +62,7 @@ export interface ContextConstraint {
|
||||
export interface BaseTrigger {
|
||||
platform: string;
|
||||
id?: string;
|
||||
variables?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface StateTrigger extends BaseTrigger {
|
||||
|
@@ -7,6 +7,7 @@ import {
|
||||
HistoryResult,
|
||||
LineChartUnit,
|
||||
TimelineEntity,
|
||||
entityIdHistoryNeedsAttributes,
|
||||
} from "./history";
|
||||
|
||||
export interface CacheConfig {
|
||||
@@ -53,7 +54,17 @@ export const getRecent = (
|
||||
return cache.data;
|
||||
}
|
||||
|
||||
const prom = fetchRecent(hass, entityId, startTime, endTime).then(
|
||||
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
|
||||
const prom = fetchRecent(
|
||||
hass,
|
||||
entityId,
|
||||
startTime,
|
||||
endTime,
|
||||
false,
|
||||
undefined,
|
||||
true,
|
||||
noAttributes
|
||||
).then(
|
||||
(stateHistory) => computeHistory(hass, stateHistory, localize),
|
||||
(err) => {
|
||||
delete RECENT_CACHE[entityId];
|
||||
@@ -120,6 +131,7 @@ export const getRecentWithCache = (
|
||||
}
|
||||
|
||||
const curCacheProm = cache.prom;
|
||||
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
|
||||
|
||||
const genProm = async () => {
|
||||
let fetchedHistory: HassEntity[][];
|
||||
@@ -132,7 +144,10 @@ export const getRecentWithCache = (
|
||||
entityId,
|
||||
toFetchStartTime,
|
||||
endTime,
|
||||
appendingToCache
|
||||
appendingToCache,
|
||||
undefined,
|
||||
true,
|
||||
noAttributes
|
||||
),
|
||||
]);
|
||||
fetchedHistory = results[1];
|
||||
|
@@ -34,8 +34,24 @@ export const ERROR_STATES: ConfigEntry["state"][] = [
|
||||
"setup_retry",
|
||||
];
|
||||
|
||||
export const getConfigEntries = (hass: HomeAssistant) =>
|
||||
hass.callApi<ConfigEntry[]>("GET", "config/config_entries/entry");
|
||||
export const getConfigEntries = (
|
||||
hass: HomeAssistant,
|
||||
filters?: { type?: "helper" | "integration"; domain?: string }
|
||||
): Promise<ConfigEntry[]> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters) {
|
||||
if (filters.type) {
|
||||
params.append("type", filters.type);
|
||||
}
|
||||
if (filters.domain) {
|
||||
params.append("domain", filters.domain);
|
||||
}
|
||||
}
|
||||
return hass.callApi<ConfigEntry[]>(
|
||||
"GET",
|
||||
`config/config_entries/entry?${params.toString()}`
|
||||
);
|
||||
};
|
||||
|
||||
export const updateConfigEntry = (
|
||||
hass: HomeAssistant,
|
||||
|
@@ -65,8 +65,14 @@ export const ignoreConfigFlow = (
|
||||
export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) =>
|
||||
hass.callApi("DELETE", `config/config_entries/flow/${flowId}`);
|
||||
|
||||
export const getConfigFlowHandlers = (hass: HomeAssistant) =>
|
||||
hass.callApi<string[]>("GET", "config/config_entries/flow_handlers");
|
||||
export const getConfigFlowHandlers = (
|
||||
hass: HomeAssistant,
|
||||
type?: "helper" | "integration"
|
||||
) =>
|
||||
hass.callApi<string[]>(
|
||||
"GET",
|
||||
`config/config_entries/flow_handlers${type ? `?type=${type}` : ""}`
|
||||
);
|
||||
|
||||
export const fetchConfigFlowInProgress = (
|
||||
conn: Connection
|
||||
|
@@ -12,7 +12,12 @@ import { subscribeOne } from "../common/util/subscribe-one";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { ConfigEntry, getConfigEntries } from "./config_entries";
|
||||
import { subscribeEntityRegistry } from "./entity_registry";
|
||||
import { fetchStatistics, Statistics } from "./history";
|
||||
import {
|
||||
fetchStatistics,
|
||||
Statistics,
|
||||
StatisticsMetaData,
|
||||
getStatisticMetadata,
|
||||
} from "./history";
|
||||
|
||||
const energyCollectionKeys: (string | undefined)[] = [];
|
||||
|
||||
@@ -136,6 +141,7 @@ export interface GasSourceTypeEnergyPreference {
|
||||
entity_energy_from: string | null;
|
||||
entity_energy_price: string | null;
|
||||
number_energy_price: number | null;
|
||||
unit_of_measurement?: string | null;
|
||||
}
|
||||
|
||||
type EnergySource =
|
||||
@@ -241,14 +247,14 @@ const getEnergyData = async (
|
||||
end?: Date
|
||||
): Promise<EnergyData> => {
|
||||
const [configEntries, entityRegistryEntries, info] = await Promise.all([
|
||||
getConfigEntries(hass),
|
||||
getConfigEntries(hass, { domain: "co2signal" }),
|
||||
subscribeOne(hass.connection, subscribeEntityRegistry),
|
||||
getEnergyInfo(hass),
|
||||
]);
|
||||
|
||||
const co2SignalConfigEntry = configEntries.find(
|
||||
(entry) => entry.domain === "co2signal"
|
||||
);
|
||||
const co2SignalConfigEntry = configEntries.length
|
||||
? configEntries[0]
|
||||
: undefined;
|
||||
|
||||
let co2SignalEntity: string | undefined;
|
||||
|
||||
@@ -271,6 +277,15 @@ const getEnergyData = async (
|
||||
|
||||
const consumptionStatIDs: string[] = [];
|
||||
const statIDs: string[] = [];
|
||||
const gasSources: GasSourceTypeEnergyPreference[] =
|
||||
prefs.energy_sources.filter(
|
||||
(source) => source.type === "gas"
|
||||
) as GasSourceTypeEnergyPreference[];
|
||||
const gasStatisticIdsWithMeta: StatisticsMetaData[] =
|
||||
await getStatisticMetadata(
|
||||
hass,
|
||||
gasSources.map((source) => source.stat_energy_from)
|
||||
);
|
||||
|
||||
for (const source of prefs.energy_sources) {
|
||||
if (source.type === "solar") {
|
||||
@@ -280,6 +295,20 @@ const getEnergyData = async (
|
||||
|
||||
if (source.type === "gas") {
|
||||
statIDs.push(source.stat_energy_from);
|
||||
const entity = hass.states[source.stat_energy_from];
|
||||
if (!entity) {
|
||||
for (const statisticIdWithMeta of gasStatisticIdsWithMeta) {
|
||||
if (
|
||||
statisticIdWithMeta?.statistic_id === source.stat_energy_from &&
|
||||
statisticIdWithMeta?.unit_of_measurement
|
||||
) {
|
||||
source.unit_of_measurement =
|
||||
statisticIdWithMeta?.unit_of_measurement === "Wh"
|
||||
? "kWh"
|
||||
: statisticIdWithMeta?.unit_of_measurement;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (source.stat_cost) {
|
||||
statIDs.push(source.stat_cost);
|
||||
}
|
||||
@@ -559,6 +588,9 @@ export const getEnergyGasUnit = (
|
||||
? "kWh"
|
||||
: entity.attributes.unit_of_measurement;
|
||||
}
|
||||
if (source.unit_of_measurement) {
|
||||
return source.unit_of_measurement;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
71
src/data/helpers_crud.ts
Normal file
71
src/data/helpers_crud.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { fetchCounter, updateCounter, deleteCounter } from "./counter";
|
||||
import {
|
||||
fetchInputBoolean,
|
||||
updateInputBoolean,
|
||||
deleteInputBoolean,
|
||||
} from "./input_boolean";
|
||||
import {
|
||||
fetchInputButton,
|
||||
updateInputButton,
|
||||
deleteInputButton,
|
||||
} from "./input_button";
|
||||
import {
|
||||
fetchInputDateTime,
|
||||
updateInputDateTime,
|
||||
deleteInputDateTime,
|
||||
} from "./input_datetime";
|
||||
import {
|
||||
fetchInputNumber,
|
||||
updateInputNumber,
|
||||
deleteInputNumber,
|
||||
} from "./input_number";
|
||||
import {
|
||||
fetchInputSelect,
|
||||
updateInputSelect,
|
||||
deleteInputSelect,
|
||||
} from "./input_select";
|
||||
import { fetchInputText, updateInputText, deleteInputText } from "./input_text";
|
||||
import { fetchTimer, updateTimer, deleteTimer } from "./timer";
|
||||
|
||||
export const HELPERS_CRUD = {
|
||||
input_boolean: {
|
||||
fetch: fetchInputBoolean,
|
||||
update: updateInputBoolean,
|
||||
delete: deleteInputBoolean,
|
||||
},
|
||||
input_button: {
|
||||
fetch: fetchInputButton,
|
||||
update: updateInputButton,
|
||||
delete: deleteInputButton,
|
||||
},
|
||||
input_text: {
|
||||
fetch: fetchInputText,
|
||||
update: updateInputText,
|
||||
delete: deleteInputText,
|
||||
},
|
||||
input_number: {
|
||||
fetch: fetchInputNumber,
|
||||
update: updateInputNumber,
|
||||
delete: deleteInputNumber,
|
||||
},
|
||||
input_datetime: {
|
||||
fetch: fetchInputDateTime,
|
||||
update: updateInputDateTime,
|
||||
delete: deleteInputDateTime,
|
||||
},
|
||||
input_select: {
|
||||
fetch: fetchInputSelect,
|
||||
update: updateInputSelect,
|
||||
delete: deleteInputSelect,
|
||||
},
|
||||
counter: {
|
||||
fetch: fetchCounter,
|
||||
update: updateCounter,
|
||||
delete: deleteCounter,
|
||||
},
|
||||
timer: {
|
||||
fetch: fetchTimer,
|
||||
update: updateTimer,
|
||||
delete: deleteTimer,
|
||||
},
|
||||
};
|
@@ -1,4 +1,5 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
@@ -7,6 +8,13 @@ import { HomeAssistant } from "../types";
|
||||
import { FrontendLocaleData } from "./translation";
|
||||
|
||||
const DOMAINS_USE_LAST_UPDATED = ["climate", "humidifier", "water_heater"];
|
||||
const NEED_ATTRIBUTE_DOMAINS = [
|
||||
"climate",
|
||||
"humidifier",
|
||||
"input_datetime",
|
||||
"thermostat",
|
||||
"water_heater",
|
||||
];
|
||||
const LINE_ATTRIBUTES_TO_KEEP = [
|
||||
"temperature",
|
||||
"current_temperature",
|
||||
@@ -131,6 +139,13 @@ export interface StatisticsValidationResults {
|
||||
[statisticId: string]: StatisticsValidationResult[];
|
||||
}
|
||||
|
||||
export const entityIdHistoryNeedsAttributes = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
) =>
|
||||
!hass.states[entityId] ||
|
||||
NEED_ATTRIBUTE_DOMAINS.includes(computeDomain(entityId));
|
||||
|
||||
export const fetchRecent = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
@@ -138,7 +153,8 @@ export const fetchRecent = (
|
||||
endTime: Date,
|
||||
skipInitialState = false,
|
||||
significantChangesOnly?: boolean,
|
||||
minimalResponse = true
|
||||
minimalResponse = true,
|
||||
noAttributes?: boolean
|
||||
): Promise<HassEntity[][]> => {
|
||||
let url = "history/period";
|
||||
if (startTime) {
|
||||
@@ -157,7 +173,9 @@ export const fetchRecent = (
|
||||
if (minimalResponse) {
|
||||
url += "&minimal_response";
|
||||
}
|
||||
|
||||
if (noAttributes) {
|
||||
url += "&no_attributes";
|
||||
}
|
||||
return hass.callApi("GET", url);
|
||||
};
|
||||
|
||||
@@ -171,6 +189,10 @@ export const fetchDate = (
|
||||
"GET",
|
||||
`history/period/${startTime.toISOString()}?end_time=${endTime.toISOString()}&minimal_response${
|
||||
entityId ? `&filter_entity_id=${entityId}` : ``
|
||||
}${
|
||||
entityId && !entityIdHistoryNeedsAttributes(hass, entityId)
|
||||
? `&no_attributes`
|
||||
: ``
|
||||
}`
|
||||
);
|
||||
|
||||
@@ -278,6 +300,10 @@ const processLineChartEntities = (
|
||||
};
|
||||
};
|
||||
|
||||
const stateUsesUnits = (state: HassEntity) =>
|
||||
"unit_of_measurement" in state.attributes ||
|
||||
"state_class" in state.attributes;
|
||||
|
||||
export const computeHistory = (
|
||||
hass: HomeAssistant,
|
||||
stateHistory: HassEntity[][],
|
||||
@@ -294,16 +320,18 @@ export const computeHistory = (
|
||||
return;
|
||||
}
|
||||
|
||||
const stateWithUnitorStateClass = stateInfo.find(
|
||||
(state) =>
|
||||
state.attributes &&
|
||||
("unit_of_measurement" in state.attributes ||
|
||||
"state_class" in state.attributes)
|
||||
);
|
||||
const entityId = stateInfo[0].entity_id;
|
||||
const currentState =
|
||||
entityId in hass.states ? hass.states[entityId] : undefined;
|
||||
const stateWithUnitorStateClass =
|
||||
!currentState &&
|
||||
stateInfo.find((state) => state.attributes && stateUsesUnits(state));
|
||||
|
||||
let unit: string | undefined;
|
||||
|
||||
if (stateWithUnitorStateClass) {
|
||||
if (currentState && stateUsesUnits(currentState)) {
|
||||
unit = currentState.attributes.unit_of_measurement || " ";
|
||||
} else if (stateWithUnitorStateClass) {
|
||||
unit = stateWithUnitorStateClass.attributes.unit_of_measurement || " ";
|
||||
} else {
|
||||
unit = {
|
||||
@@ -313,7 +341,7 @@ export const computeHistory = (
|
||||
input_number: "#",
|
||||
number: "#",
|
||||
water_heater: hass.config.unit_system.temperature,
|
||||
}[computeStateDomain(stateInfo[0])];
|
||||
}[computeDomain(entityId)];
|
||||
}
|
||||
|
||||
if (!unit) {
|
||||
@@ -345,6 +373,15 @@ export const getStatisticIds = (
|
||||
statistic_type,
|
||||
});
|
||||
|
||||
export const getStatisticMetadata = (
|
||||
hass: HomeAssistant,
|
||||
statistic_ids?: string[]
|
||||
) =>
|
||||
hass.callWS<StatisticsMetaData[]>({
|
||||
type: "recorder/get_statistics_metadata",
|
||||
statistic_ids,
|
||||
});
|
||||
|
||||
export const fetchStatistics = (
|
||||
hass: HomeAssistant,
|
||||
startTime: Date,
|
||||
@@ -428,3 +465,16 @@ export const statisticsHaveType = (
|
||||
stats: StatisticValue[],
|
||||
type: StatisticType
|
||||
) => stats.some((stat) => stat[type] !== null);
|
||||
|
||||
export const adjustStatisticsSum = (
|
||||
hass: HomeAssistant,
|
||||
statistic_id: string,
|
||||
start_time: string,
|
||||
adjustment: number
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "recorder/adjust_sum_statistics",
|
||||
statistic_id,
|
||||
start_time,
|
||||
adjustment,
|
||||
});
|
||||
|
@@ -1,32 +1,51 @@
|
||||
export type Selector =
|
||||
| ActionSelector
|
||||
| AddonSelector
|
||||
| AreaSelector
|
||||
| AttributeSelector
|
||||
| EntitySelector
|
||||
| BooleanSelector
|
||||
| ColorRGBSelector
|
||||
| ColorTempSelector
|
||||
| DateSelector
|
||||
| DateTimeSelector
|
||||
| DeviceSelector
|
||||
| DurationSelector
|
||||
| AreaSelector
|
||||
| TargetSelector
|
||||
| EntitySelector
|
||||
| IconSelector
|
||||
| LocationSelector
|
||||
| MediaSelector
|
||||
| NumberSelector
|
||||
| BooleanSelector
|
||||
| TimeSelector
|
||||
| ActionSelector
|
||||
| StringSelector
|
||||
| ObjectSelector
|
||||
| SelectSelector
|
||||
| IconSelector
|
||||
| MediaSelector
|
||||
| StringSelector
|
||||
| TargetSelector
|
||||
| ThemeSelector
|
||||
| LocationSelector
|
||||
| ColorTempSelector
|
||||
| ColorRGBSelector;
|
||||
| TimeSelector;
|
||||
|
||||
export interface EntitySelector {
|
||||
entity: {
|
||||
integration?: string;
|
||||
domain?: string | string[];
|
||||
device_class?: string;
|
||||
export interface ActionSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
action: {};
|
||||
}
|
||||
|
||||
export interface AddonSelector {
|
||||
addon: {
|
||||
name?: string;
|
||||
slug?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AreaSelector {
|
||||
area: {
|
||||
entity?: {
|
||||
integration?: EntitySelector["entity"]["integration"];
|
||||
domain?: EntitySelector["entity"]["domain"];
|
||||
device_class?: EntitySelector["entity"]["device_class"];
|
||||
};
|
||||
device?: {
|
||||
integration?: DeviceSelector["device"]["integration"];
|
||||
manufacturer?: DeviceSelector["device"]["manufacturer"];
|
||||
model?: DeviceSelector["device"]["model"];
|
||||
};
|
||||
multiple?: boolean;
|
||||
};
|
||||
}
|
||||
@@ -37,11 +56,23 @@ export interface AttributeSelector {
|
||||
};
|
||||
}
|
||||
|
||||
export interface BooleanSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
boolean: {};
|
||||
}
|
||||
|
||||
export interface ColorRGBSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
color_rgb: {};
|
||||
}
|
||||
|
||||
export interface ColorTempSelector {
|
||||
color_temp: {
|
||||
min_mireds?: number;
|
||||
max_mireds?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DateSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
date: {};
|
||||
@@ -66,44 +97,54 @@ export interface DeviceSelector {
|
||||
}
|
||||
|
||||
export interface DurationSelector {
|
||||
duration: {
|
||||
enable_day?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface EntitySelector {
|
||||
entity: {
|
||||
integration?: string;
|
||||
domain?: string | string[];
|
||||
device_class?: string;
|
||||
multiple?: boolean;
|
||||
include_entities?: string[];
|
||||
exclude_entities?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface IconSelector {
|
||||
icon: {
|
||||
placeholder?: string;
|
||||
fallbackPath?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LocationSelector {
|
||||
location: { radius?: boolean; icon?: string };
|
||||
}
|
||||
|
||||
export interface LocationSelectorValue {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
radius?: number;
|
||||
}
|
||||
|
||||
export interface MediaSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
duration: {};
|
||||
media: {};
|
||||
}
|
||||
|
||||
export interface AddonSelector {
|
||||
addon: {
|
||||
name?: string;
|
||||
slug?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AreaSelector {
|
||||
area: {
|
||||
entity?: {
|
||||
integration?: EntitySelector["entity"]["integration"];
|
||||
domain?: EntitySelector["entity"]["domain"];
|
||||
device_class?: EntitySelector["entity"]["device_class"];
|
||||
};
|
||||
device?: {
|
||||
integration?: DeviceSelector["device"]["integration"];
|
||||
manufacturer?: DeviceSelector["device"]["manufacturer"];
|
||||
model?: DeviceSelector["device"]["model"];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface TargetSelector {
|
||||
target: {
|
||||
entity?: {
|
||||
integration?: EntitySelector["entity"]["integration"];
|
||||
domain?: EntitySelector["entity"]["domain"];
|
||||
device_class?: EntitySelector["entity"]["device_class"];
|
||||
};
|
||||
device?: {
|
||||
integration?: DeviceSelector["device"]["integration"];
|
||||
manufacturer?: DeviceSelector["device"]["manufacturer"];
|
||||
model?: DeviceSelector["device"]["model"];
|
||||
};
|
||||
export interface MediaSelectorValue {
|
||||
entity_id?: string;
|
||||
media_content_id?: string;
|
||||
media_content_type?: string;
|
||||
metadata?: {
|
||||
title?: string;
|
||||
thumbnail?: string | null;
|
||||
media_class?: string;
|
||||
children_media_class?: string | null;
|
||||
navigateIds?: { media_content_type: string; media_content_id: string }[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -117,28 +158,22 @@ export interface NumberSelector {
|
||||
};
|
||||
}
|
||||
|
||||
export interface ColorTempSelector {
|
||||
color_temp: {
|
||||
min_mireds?: number;
|
||||
max_mireds?: number;
|
||||
export interface ObjectSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
object: {};
|
||||
}
|
||||
|
||||
export interface SelectOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface SelectSelector {
|
||||
select: {
|
||||
options: string[] | SelectOption[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface BooleanSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
boolean: {};
|
||||
}
|
||||
|
||||
export interface TimeSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
time: {};
|
||||
}
|
||||
|
||||
export interface ActionSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
action: {};
|
||||
}
|
||||
|
||||
export interface StringSelector {
|
||||
text: {
|
||||
multiline?: boolean;
|
||||
@@ -160,58 +195,25 @@ export interface StringSelector {
|
||||
};
|
||||
}
|
||||
|
||||
export interface ObjectSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
object: {};
|
||||
}
|
||||
|
||||
export interface SelectOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface SelectSelector {
|
||||
select: {
|
||||
options: string[] | SelectOption[];
|
||||
export interface TargetSelector {
|
||||
target: {
|
||||
entity?: {
|
||||
integration?: EntitySelector["entity"]["integration"];
|
||||
domain?: EntitySelector["entity"]["domain"];
|
||||
device_class?: EntitySelector["entity"]["device_class"];
|
||||
};
|
||||
device?: {
|
||||
integration?: DeviceSelector["device"]["integration"];
|
||||
manufacturer?: DeviceSelector["device"]["manufacturer"];
|
||||
model?: DeviceSelector["device"]["model"];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface IconSelector {
|
||||
icon: {
|
||||
placeholder?: string;
|
||||
fallbackPath?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ThemeSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
theme: {};
|
||||
}
|
||||
|
||||
export interface MediaSelector {
|
||||
export interface TimeSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
media: {};
|
||||
}
|
||||
|
||||
export interface LocationSelector {
|
||||
location: { radius?: boolean; icon?: string };
|
||||
}
|
||||
|
||||
export interface LocationSelectorValue {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
radius?: number;
|
||||
}
|
||||
|
||||
export interface MediaSelectorValue {
|
||||
entity_id?: string;
|
||||
media_content_id?: string;
|
||||
media_content_type?: string;
|
||||
metadata?: {
|
||||
title?: string;
|
||||
thumbnail?: string | null;
|
||||
media_class?: string;
|
||||
children_media_class?: string | null;
|
||||
navigateIds?: { media_content_type: string; media_content_id: string }[];
|
||||
};
|
||||
time: {};
|
||||
}
|
||||
|
@@ -1,58 +0,0 @@
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
interface SupervisorBaseAvailableUpdates {
|
||||
panel_path?: string;
|
||||
update_type?: string;
|
||||
version_latest?: string;
|
||||
}
|
||||
|
||||
interface SupervisorAddonAvailableUpdates
|
||||
extends SupervisorBaseAvailableUpdates {
|
||||
update_type?: "addon";
|
||||
icon?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface SupervisorCoreAvailableUpdates
|
||||
extends SupervisorBaseAvailableUpdates {
|
||||
update_type?: "core";
|
||||
}
|
||||
|
||||
interface SupervisorOsAvailableUpdates extends SupervisorBaseAvailableUpdates {
|
||||
update_type?: "os";
|
||||
}
|
||||
|
||||
interface SupervisorSupervisorAvailableUpdates
|
||||
extends SupervisorBaseAvailableUpdates {
|
||||
update_type?: "supervisor";
|
||||
}
|
||||
|
||||
export type SupervisorAvailableUpdates =
|
||||
| SupervisorAddonAvailableUpdates
|
||||
| SupervisorCoreAvailableUpdates
|
||||
| SupervisorOsAvailableUpdates
|
||||
| SupervisorSupervisorAvailableUpdates;
|
||||
|
||||
export interface SupervisorAvailableUpdatesResponse {
|
||||
available_updates: SupervisorAvailableUpdates[];
|
||||
}
|
||||
|
||||
export const fetchSupervisorAvailableUpdates = async (
|
||||
hass: HomeAssistant
|
||||
): Promise<SupervisorAvailableUpdates[]> =>
|
||||
(
|
||||
await hass.callWS<SupervisorAvailableUpdatesResponse>({
|
||||
type: "supervisor/api",
|
||||
endpoint: "/available_updates",
|
||||
method: "get",
|
||||
})
|
||||
).available_updates;
|
||||
|
||||
export const refreshSupervisorAvailableUpdates = async (
|
||||
hass: HomeAssistant
|
||||
): Promise<void> =>
|
||||
hass.callWS<void>({
|
||||
type: "supervisor/api",
|
||||
endpoint: "/refresh_updates",
|
||||
method: "post",
|
||||
});
|
36
src/data/update.ts
Normal file
36
src/data/update.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
|
||||
export const UPDATE_SUPPORT_INSTALL = 1;
|
||||
export const UPDATE_SUPPORT_SPECIFIC_VERSION = 2;
|
||||
export const UPDATE_SUPPORT_PROGRESS = 4;
|
||||
export const UPDATE_SUPPORT_BACKUP = 8;
|
||||
|
||||
interface UpdateEntityAttributes extends HassEntityAttributeBase {
|
||||
current_version: string | null;
|
||||
in_progress: boolean | number;
|
||||
latest_version: string | null;
|
||||
release_summary: string | null;
|
||||
release_url: string | null;
|
||||
skipped_version: string | null;
|
||||
title: string | null;
|
||||
}
|
||||
|
||||
export interface UpdateEntity extends HassEntityBase {
|
||||
attributes: UpdateEntityAttributes;
|
||||
}
|
||||
|
||||
export const updateUsesProgress = (entity: UpdateEntity): boolean =>
|
||||
supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) &&
|
||||
typeof entity.attributes.in_progress === "number";
|
||||
|
||||
export const updateCanInstall = (entity: UpdateEntity): boolean =>
|
||||
supportsFeature(entity, UPDATE_SUPPORT_INSTALL) &&
|
||||
entity.attributes.latest_version !== entity.attributes.current_version &&
|
||||
entity.attributes.latest_version !== entity.attributes.skipped_version;
|
||||
|
||||
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
|
||||
updateUsesProgress(entity) || !!entity.attributes.in_progress;
|
@@ -1,5 +1,5 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { mdiClose, mdiHelpCircle } from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
@@ -14,9 +14,7 @@ import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-form/ha-form";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-markdown";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
@@ -33,6 +31,7 @@ import {
|
||||
} from "../../data/device_registry";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import { showAlertDialog } from "../generic/show-dialog-box";
|
||||
import {
|
||||
DataEntryFlowDialogParams,
|
||||
@@ -236,14 +235,35 @@ class DataEntryFlowDialog extends LitElement {
|
||||
// to reset the element.
|
||||
""
|
||||
: html`
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.dismiss"
|
||||
)}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
?rtl=${computeRTL(this.hass)}
|
||||
></ha-icon-button>
|
||||
<div class="dialog-actions">
|
||||
${["form", "menu", "external"].includes(
|
||||
this._step?.type as any
|
||||
)
|
||||
? html`
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
`/integrations/${this._step!.handler}`
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
><ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.help")}
|
||||
.path=${mdiHelpCircle}
|
||||
?rtl=${computeRTL(this.hass)}
|
||||
></ha-icon-button
|
||||
></a>
|
||||
`
|
||||
: ""}
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.dismiss"
|
||||
)}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
?rtl=${computeRTL(this.hass)}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
${this._step === null
|
||||
? this._handler
|
||||
? html`<step-flow-pick-flow
|
||||
@@ -472,16 +492,19 @@ class DataEntryFlowDialog extends LitElement {
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
ha-icon-button {
|
||||
.dialog-actions {
|
||||
padding: 16px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
ha-icon-button[rtl] {
|
||||
.dialog-actions[rtl] {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
.dialog-actions > * {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -24,7 +24,7 @@ export const showConfigFlowDialog = (
|
||||
loadDevicesAndAreas: true,
|
||||
getFlowHandlers: async (hass) => {
|
||||
const [handlers] = await Promise.all([
|
||||
getConfigFlowHandlers(hass),
|
||||
getConfigFlowHandlers(hass, "integration"),
|
||||
hass.loadBackendTranslation("title", undefined, true),
|
||||
]);
|
||||
|
||||
@@ -91,6 +91,12 @@ export const showConfigFlowDialog = (
|
||||
);
|
||||
},
|
||||
|
||||
renderShowFormStepFieldHelper(hass, step, field) {
|
||||
return hass.localize(
|
||||
`component.${step.handler}.config.step.${step.step_id}.data_description.${field.name}`
|
||||
);
|
||||
},
|
||||
|
||||
renderShowFormStepFieldError(hass, step, error) {
|
||||
return hass.localize(
|
||||
`component.${step.handler}.config.error.${error}`,
|
||||
@@ -189,6 +195,18 @@ export const showConfigFlowDialog = (
|
||||
);
|
||||
},
|
||||
|
||||
renderMenuDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${step.handler}.config.step.${step.step_id}.description`,
|
||||
step.description_placeholders
|
||||
);
|
||||
return description
|
||||
? html`
|
||||
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
|
||||
`
|
||||
: "";
|
||||
},
|
||||
|
||||
renderMenuOption(hass, step, option) {
|
||||
return hass.localize(
|
||||
`component.${step.handler}.config.step.${step.step_id}.menu_options.${option}`,
|
||||
|
@@ -50,6 +50,12 @@ export interface FlowConfig {
|
||||
field: HaFormSchema
|
||||
): string;
|
||||
|
||||
renderShowFormStepFieldHelper(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepForm,
|
||||
field: HaFormSchema
|
||||
): string;
|
||||
|
||||
renderShowFormStepFieldError(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepForm,
|
||||
@@ -83,6 +89,11 @@ export interface FlowConfig {
|
||||
|
||||
renderMenuHeader(hass: HomeAssistant, step: DataEntryFlowStepMenu): string;
|
||||
|
||||
renderMenuDescription(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepMenu
|
||||
): TemplateResult | "";
|
||||
|
||||
renderMenuOption(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepMenu,
|
||||
|
@@ -89,6 +89,12 @@ export const showOptionsFlowDialog = (
|
||||
);
|
||||
},
|
||||
|
||||
renderShowFormStepFieldHelper(hass, step, field) {
|
||||
return hass.localize(
|
||||
`component.${configEntry.domain}.options.step.${step.step_id}.data_description.${field.name}`
|
||||
);
|
||||
},
|
||||
|
||||
renderShowFormStepFieldError(hass, step, error) {
|
||||
return hass.localize(
|
||||
`component.${configEntry.domain}.options.error.${error}`,
|
||||
@@ -142,6 +148,22 @@ export const showOptionsFlowDialog = (
|
||||
);
|
||||
},
|
||||
|
||||
renderMenuDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${step.handler}.option.step.${step.step_id}.description`,
|
||||
step.description_placeholders
|
||||
);
|
||||
return description
|
||||
? html`
|
||||
<ha-markdown
|
||||
allowsvg
|
||||
breaks
|
||||
.content=${description}
|
||||
></ha-markdown>
|
||||
`
|
||||
: "";
|
||||
},
|
||||
|
||||
renderMenuOption(hass, step, option) {
|
||||
return hass.localize(
|
||||
`component.${step.handler}.options.step.${step.step_id}.menu_options.${option}`,
|
||||
|
@@ -54,6 +54,7 @@ class StepFlowForm extends LitElement {
|
||||
.schema=${step.data_schema}
|
||||
.error=${step.errors}
|
||||
.computeLabel=${this._labelCallback}
|
||||
.computeHelper=${this._helperCallback}
|
||||
.computeError=${this._errorCallback}
|
||||
></ha-form>
|
||||
</div>
|
||||
@@ -166,6 +167,9 @@ class StepFlowForm extends LitElement {
|
||||
private _labelCallback = (field: HaFormSchema): string =>
|
||||
this.flowConfig.renderShowFormStepFieldLabel(this.hass, this.step, field);
|
||||
|
||||
private _helperCallback = (field: HaFormSchema): string =>
|
||||
this.flowConfig.renderShowFormStepFieldHelper(this.hass, this.step, field);
|
||||
|
||||
private _errorCallback = (error: string) =>
|
||||
this.flowConfig.renderShowFormStepFieldError(this.hass, this.step, error);
|
||||
|
||||
|
@@ -35,8 +35,14 @@ class StepFlowMenu extends LitElement {
|
||||
translations = this.step.menu_options;
|
||||
}
|
||||
|
||||
const description = this.flowConfig.renderMenuDescription(
|
||||
this.hass,
|
||||
this.step
|
||||
);
|
||||
|
||||
return html`
|
||||
<h2>${this.flowConfig.renderMenuHeader(this.hass, this.step)}</h2>
|
||||
${description ? html`<div class="content">${description}</div>` : ""}
|
||||
<div class="options">
|
||||
${options.map(
|
||||
(option) => html`
|
||||
@@ -69,6 +75,16 @@ class StepFlowMenu extends LitElement {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.content {
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
.content + .options {
|
||||
margin-top: 8px;
|
||||
}
|
||||
mwc-list-item {
|
||||
--mdc-list-side-padding: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -216,15 +216,16 @@ class StepFlowPickHandler extends LitElement {
|
||||
|
||||
if (handler.is_add) {
|
||||
if (handler.slug === "zwave_js") {
|
||||
const entries = await getConfigEntries(this.hass);
|
||||
const entry = entries.find((ent) => ent.domain === "zwave_js");
|
||||
const entries = await getConfigEntries(this.hass, {
|
||||
domain: "zwave_js",
|
||||
});
|
||||
|
||||
if (!entry) {
|
||||
if (!entries.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
showZWaveJSAddNodeDialog(this, {
|
||||
entry_id: entry.entry_id,
|
||||
entry_id: entries[0].entry_id,
|
||||
});
|
||||
} else if (handler.slug === "zha") {
|
||||
navigate("/config/zha/add");
|
||||
|
212
src/dialogs/more-info/controls/more-info-update.ts
Normal file
212
src/dialogs/more-info/controls/more-info-update.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-checkbox";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-markdown";
|
||||
import { UNAVAILABLE_STATES } from "../../../data/entity";
|
||||
import {
|
||||
updateIsInstalling,
|
||||
UpdateEntity,
|
||||
UPDATE_SUPPORT_BACKUP,
|
||||
UPDATE_SUPPORT_INSTALL,
|
||||
UPDATE_SUPPORT_PROGRESS,
|
||||
UPDATE_SUPPORT_SPECIFIC_VERSION,
|
||||
} from "../../../data/update";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("more-info-update")
|
||||
class MoreInfoUpdate extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: UpdateEntity;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
!this.hass ||
|
||||
!this.stateObj ||
|
||||
UNAVAILABLE_STATES.includes(this.stateObj.state)
|
||||
) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const skippedVersion =
|
||||
this.stateObj.attributes.latest_version &&
|
||||
this.stateObj.attributes.skipped_version ===
|
||||
this.stateObj.attributes.latest_version;
|
||||
|
||||
return html`
|
||||
${this.stateObj.attributes.in_progress
|
||||
? supportsFeature(this.stateObj, UPDATE_SUPPORT_PROGRESS) &&
|
||||
typeof this.stateObj.attributes.in_progress === "number"
|
||||
? html`<mwc-linear-progress
|
||||
.progress=${this.stateObj.attributes.in_progress / 100}
|
||||
buffer=""
|
||||
></mwc-linear-progress>`
|
||||
: html`<mwc-linear-progress indeterminate></mwc-linear-progress>`
|
||||
: ""}
|
||||
${this.stateObj.attributes.title
|
||||
? html`<h3>${this.stateObj.attributes.title}</h3>`
|
||||
: ""}
|
||||
|
||||
<div class="row">
|
||||
<div class="key">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.current_version"
|
||||
)}
|
||||
</div>
|
||||
<div class="value">
|
||||
${this.stateObj.attributes.current_version ??
|
||||
this.hass.localize("state.default.unavailable")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="key">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.latest_version"
|
||||
)}
|
||||
</div>
|
||||
<div class="value">
|
||||
${this.stateObj.attributes.latest_version ??
|
||||
this.hass.localize("state.default.unavailable")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.stateObj.attributes.release_url
|
||||
? html`<div class="row">
|
||||
<div class="key">
|
||||
<a
|
||||
href=${this.stateObj.attributes.release_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.release_announcement"
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
</div>`
|
||||
: ""}
|
||||
${this.stateObj.attributes.release_summary
|
||||
? html`<hr />
|
||||
<ha-markdown
|
||||
.content=${this.stateObj.attributes.release_summary}
|
||||
></ha-markdown> `
|
||||
: ""}
|
||||
${supportsFeature(this.stateObj, UPDATE_SUPPORT_BACKUP)
|
||||
? html`<hr />
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup"
|
||||
)}
|
||||
>
|
||||
<ha-checkbox
|
||||
checked
|
||||
.disabled=${updateIsInstalling(this.stateObj)}
|
||||
></ha-checkbox>
|
||||
</ha-formfield> `
|
||||
: ""}
|
||||
<hr />
|
||||
<div class="actions">
|
||||
<mwc-button
|
||||
@click=${this._handleSkip}
|
||||
.disabled=${skippedVersion ||
|
||||
this.stateObj.state === "off" ||
|
||||
updateIsInstalling(this.stateObj)}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.more_info_control.update.skip")}
|
||||
</mwc-button>
|
||||
${supportsFeature(this.stateObj, UPDATE_SUPPORT_INSTALL)
|
||||
? html`
|
||||
<mwc-button
|
||||
@click=${this._handleInstall}
|
||||
.disabled=${(this.stateObj.state === "off" &&
|
||||
!skippedVersion) ||
|
||||
updateIsInstalling(this.stateObj)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.install"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
get _shouldCreateBackup(): boolean | null {
|
||||
if (!supportsFeature(this.stateObj!, UPDATE_SUPPORT_BACKUP)) {
|
||||
return null;
|
||||
}
|
||||
const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
|
||||
if (checkbox) {
|
||||
return checkbox.checked;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _handleInstall(): void {
|
||||
const installData: Record<string, any> = {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
};
|
||||
|
||||
if (this._shouldCreateBackup) {
|
||||
installData.backup = true;
|
||||
}
|
||||
|
||||
if (
|
||||
supportsFeature(this.stateObj!, UPDATE_SUPPORT_SPECIFIC_VERSION) &&
|
||||
this.stateObj!.attributes.latest_version
|
||||
) {
|
||||
installData.version = this.stateObj!.attributes.latest_version;
|
||||
}
|
||||
|
||||
this.hass.callService("update", "install", installData);
|
||||
}
|
||||
|
||||
private _handleSkip(): void {
|
||||
this.hass.callService("update", "skip", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
hr {
|
||||
border-color: var(--divider-color);
|
||||
border-bottom: none;
|
||||
margin: 16px 0;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
margin: 16px 0;
|
||||
}
|
||||
.row {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.actions {
|
||||
margin: 8px 0 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.actions mwc-button {
|
||||
margin: 0 4px 4px;
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"more-info-update": MoreInfoUpdate;
|
||||
}
|
||||
}
|
@@ -25,6 +25,7 @@ const LAZY_LOADED_MORE_INFO_CONTROL = {
|
||||
script: () => import("./controls/more-info-script"),
|
||||
sun: () => import("./controls/more-info-sun"),
|
||||
timer: () => import("./controls/more-info-timer"),
|
||||
update: () => import("./controls/more-info-update"),
|
||||
vacuum: () => import("./controls/more-info-vacuum"),
|
||||
water_heater: () => import("./controls/more-info-water_heater"),
|
||||
weather: () => import("./controls/more-info-weather"),
|
||||
|
@@ -169,8 +169,8 @@ class OnboardingIntegrations extends LitElement {
|
||||
}
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
const entries = await getConfigEntries(this.hass!);
|
||||
// We filter out the config entry for the local weather and rpi_power.
|
||||
const entries = await getConfigEntries(this.hass!, { type: "integration" });
|
||||
// We filter out the config entries that are automatically created during onboarding.
|
||||
// It is one that we create automatically and it will confuse the user
|
||||
// if it starts showing up during onboarding.
|
||||
this._entries = entries.filter(
|
||||
|
@@ -326,12 +326,9 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
}
|
||||
ha-settings-row {
|
||||
--paper-time-input-justify-content: flex-end;
|
||||
--settings-row-content-width: 100%;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
:host(:not([narrow])) ha-settings-row ha-textfield,
|
||||
:host(:not([narrow])) ha-settings-row ha-selector {
|
||||
width: 60%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -59,7 +59,9 @@ class HaConfigAutomation extends HassRouterPage {
|
||||
private _getAutomations = memoizeOne(
|
||||
(states: HassEntities): AutomationEntity[] =>
|
||||
Object.values(states).filter(
|
||||
(entity) => computeStateDomain(entity) === "automation"
|
||||
(entity) =>
|
||||
computeStateDomain(entity) === "automation" &&
|
||||
!entity.attributes.restored
|
||||
) as AutomationEntity[]
|
||||
);
|
||||
|
||||
@@ -87,7 +89,7 @@ class HaConfigAutomation extends HassRouterPage {
|
||||
(!changedProps || changedProps.has("route")) &&
|
||||
this._currentPage !== "dashboard"
|
||||
) {
|
||||
const automationId = this.routeTail.path.substr(1);
|
||||
const automationId = decodeURIComponent(this.routeTail.path.substr(1));
|
||||
pageEl.automationId = automationId === "new" ? null : automationId;
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import {
|
||||
mdiCloudLock,
|
||||
mdiDotsVertical,
|
||||
@@ -5,10 +7,9 @@ import {
|
||||
mdiMagnify,
|
||||
mdiNewBox,
|
||||
} from "@mdi/js";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -18,30 +19,29 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-menu-button";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-menu-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { CloudStatus } from "../../../data/cloud";
|
||||
import {
|
||||
refreshSupervisorAvailableUpdates,
|
||||
SupervisorAvailableUpdates,
|
||||
} from "../../../data/supervisor/root";
|
||||
import { updateCanInstall, UpdateEntity } from "../../../data/update";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import "../../../layouts/ha-app-layout";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import "../ha-config-section";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import "./ha-config-navigation";
|
||||
import "./ha-config-updates";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
|
||||
const randomTip = (hass: HomeAssistant) => {
|
||||
const weighted: string[] = [];
|
||||
@@ -113,9 +113,6 @@ class HaConfigDashboard extends LitElement {
|
||||
|
||||
@property() public cloudStatus?: CloudStatus;
|
||||
|
||||
// null means not available
|
||||
@property() public supervisorUpdates?: SupervisorAvailableUpdates[] | null;
|
||||
|
||||
@property() public showAdvanced!: boolean;
|
||||
|
||||
@state() private _tip?: string;
|
||||
@@ -123,6 +120,9 @@ class HaConfigDashboard extends LitElement {
|
||||
private _notifyUpdates = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const canInstallUpdates = this._filterUpdateEntitiesWithInstall(
|
||||
this.hass.states
|
||||
);
|
||||
return html`
|
||||
<ha-app-layout>
|
||||
<app-header fixed slot="header">
|
||||
@@ -160,50 +160,47 @@ class HaConfigDashboard extends LitElement {
|
||||
.isWide=${this.isWide}
|
||||
full-width
|
||||
>
|
||||
${this.supervisorUpdates === undefined
|
||||
? // Hide everything until updates loaded
|
||||
html``
|
||||
: html`${this.supervisorUpdates?.length
|
||||
? html`<ha-card>
|
||||
<ha-config-updates
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.supervisorUpdates=${this.supervisorUpdates}
|
||||
></ha-config-updates>
|
||||
</ha-card>`
|
||||
: ""}
|
||||
<ha-card>
|
||||
${this.narrow && this.supervisorUpdates?.length
|
||||
? html`<div class="title">
|
||||
${this.hass.localize("panel.config")}
|
||||
</div>`
|
||||
: ""}
|
||||
${this.cloudStatus && isComponentLoaded(this.hass, "cloud")
|
||||
? html`
|
||||
<ha-config-navigation
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.showAdvanced=${this.showAdvanced}
|
||||
.pages=${[
|
||||
{
|
||||
component: "cloud",
|
||||
path: "/config/cloud",
|
||||
name: "Home Assistant Cloud",
|
||||
info: this.cloudStatus,
|
||||
iconPath: mdiCloudLock,
|
||||
iconColor: "#3B808E",
|
||||
},
|
||||
]}
|
||||
></ha-config-navigation>
|
||||
`
|
||||
: ""}
|
||||
${canInstallUpdates.length
|
||||
? html`<ha-card>
|
||||
<ha-config-updates
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.updateEntities=${canInstallUpdates}
|
||||
></ha-config-updates>
|
||||
</ha-card>`
|
||||
: ""}
|
||||
<ha-card>
|
||||
${this.narrow && canInstallUpdates.length
|
||||
? html`<div class="title">
|
||||
${this.hass.localize("panel.config")}
|
||||
</div>`
|
||||
: ""}
|
||||
${this.cloudStatus && isComponentLoaded(this.hass, "cloud")
|
||||
? html`
|
||||
<ha-config-navigation
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.showAdvanced=${this.showAdvanced}
|
||||
.pages=${configSections.dashboard}
|
||||
.pages=${[
|
||||
{
|
||||
component: "cloud",
|
||||
path: "/config/cloud",
|
||||
name: "Home Assistant Cloud",
|
||||
info: this.cloudStatus,
|
||||
iconPath: mdiCloudLock,
|
||||
iconColor: "#3B808E",
|
||||
},
|
||||
]}
|
||||
></ha-config-navigation>
|
||||
</ha-card>`}
|
||||
`
|
||||
: ""}
|
||||
<ha-config-navigation
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.showAdvanced=${this.showAdvanced}
|
||||
.pages=${configSections.dashboard}
|
||||
></ha-config-navigation>
|
||||
</ha-card>
|
||||
<div class="tips">
|
||||
<ha-svg-icon .path=${mdiLightbulbOutline}></ha-svg-icon>
|
||||
<span class="tip-word">Tip!</span>
|
||||
@@ -221,11 +218,11 @@ class HaConfigDashboard extends LitElement {
|
||||
this._tip = randomTip(this.hass);
|
||||
}
|
||||
|
||||
if (!changedProps.has("supervisorUpdates") || !this._notifyUpdates) {
|
||||
if (!changedProps.has("hass") || !this._notifyUpdates) {
|
||||
return;
|
||||
}
|
||||
this._notifyUpdates = false;
|
||||
if (this.supervisorUpdates?.length) {
|
||||
if (this._filterUpdateEntitiesWithInstall(this.hass.states).length) {
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
"ui.panel.config.updates.updates_refreshed"
|
||||
@@ -238,6 +235,44 @@ class HaConfigDashboard extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _filterUpdateEntities = memoizeOne((entities: HassEntities) =>
|
||||
(
|
||||
Object.values(entities).filter(
|
||||
(entity) => computeStateDomain(entity) === "update"
|
||||
) as UpdateEntity[]
|
||||
).sort((a, b) => {
|
||||
if (a.attributes.title === "Home Assistant Core") {
|
||||
return -3;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Core") {
|
||||
return 3;
|
||||
}
|
||||
if (a.attributes.title === "Home Assistant Operating System") {
|
||||
return -2;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Operating System") {
|
||||
return 2;
|
||||
}
|
||||
if (a.attributes.title === "Home Assistant Supervisor") {
|
||||
return -1;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Supervisor") {
|
||||
return 1;
|
||||
}
|
||||
return caseInsensitiveStringCompare(
|
||||
a.attributes.title || a.attributes.friendly_name || "",
|
||||
b.attributes.title || b.attributes.friendly_name || ""
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
private _filterUpdateEntitiesWithInstall = memoizeOne(
|
||||
(entities: HassEntities) =>
|
||||
this._filterUpdateEntities(entities).filter((entity) =>
|
||||
updateCanInstall(entity)
|
||||
)
|
||||
);
|
||||
|
||||
private _showQuickBar(): void {
|
||||
showQuickBar(this, {
|
||||
commandMode: true,
|
||||
@@ -246,20 +281,24 @@ class HaConfigDashboard extends LitElement {
|
||||
}
|
||||
|
||||
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||
const _entities = this._filterUpdateEntities(this.hass.states).map(
|
||||
(entity) => entity.entity_id
|
||||
);
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
if (_entities.length) {
|
||||
this._notifyUpdates = true;
|
||||
await refreshSupervisorAvailableUpdates(this.hass);
|
||||
fireEvent(this, "ha-refresh-supervisor");
|
||||
await this.hass.callService("homeassistant", "update_entity", {
|
||||
entity_id: _entities,
|
||||
});
|
||||
return;
|
||||
}
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.updates.check_unavailable.title"
|
||||
"ui.panel.config.updates.no_update_entities.title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.updates.check_unavailable.description"
|
||||
"ui.panel.config.updates.no_update_entities.description"
|
||||
),
|
||||
warning: true,
|
||||
});
|
||||
|
@@ -1,21 +1,14 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiPackageVariant } from "@mdi/js";
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/entity/state-badge";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-logo-svg";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { SupervisorAvailableUpdates } from "../../../data/supervisor/root";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "../../../components/ha-icon-next";
|
||||
|
||||
export const SUPERVISOR_UPDATE_NAMES = {
|
||||
core: "Home Assistant Core",
|
||||
os: "Home Assistant Operating System",
|
||||
supervisor: "Home Assistant Supervisor",
|
||||
};
|
||||
import type { UpdateEntity } from "../../../data/update";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("ha-config-updates")
|
||||
class HaConfigUpdates extends LitElement {
|
||||
@@ -24,62 +17,60 @@ class HaConfigUpdates extends LitElement {
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false })
|
||||
public supervisorUpdates?: SupervisorAvailableUpdates[] | null;
|
||||
public updateEntities?: UpdateEntity[];
|
||||
|
||||
@state() private _showAll = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.supervisorUpdates?.length) {
|
||||
if (!this.updateEntities?.length) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const updates =
|
||||
this._showAll || this.supervisorUpdates.length <= 3
|
||||
? this.supervisorUpdates
|
||||
: this.supervisorUpdates.slice(0, 2);
|
||||
this._showAll || this.updateEntities.length <= 3
|
||||
? this.updateEntities
|
||||
: this.updateEntities.slice(0, 2);
|
||||
|
||||
return html`
|
||||
<div class="title">
|
||||
${this.hass.localize("ui.panel.config.updates.title", {
|
||||
count: this.supervisorUpdates.length,
|
||||
count: this.updateEntities.length,
|
||||
})}
|
||||
</div>
|
||||
${updates.map(
|
||||
(update) => html`
|
||||
<a href="/hassio${update.panel_path}">
|
||||
<paper-icon-item>
|
||||
<span slot="item-icon" class="icon">
|
||||
${update.update_type === "addon"
|
||||
? update.icon
|
||||
? html`<img src="/api/hassio${update.icon}" />`
|
||||
: html`<ha-svg-icon
|
||||
.path=${mdiPackageVariant}
|
||||
></ha-svg-icon>`
|
||||
: html`<ha-logo-svg></ha-logo-svg>`}
|
||||
</span>
|
||||
<paper-item-body two-line>
|
||||
${update.update_type === "addon"
|
||||
? update.name
|
||||
: SUPERVISOR_UPDATE_NAMES[update.update_type!]}
|
||||
<div secondary>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.updates.version_available",
|
||||
{
|
||||
version_available: update.version_latest,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</paper-item-body>
|
||||
${!this.narrow ? html`<ha-icon-next></ha-icon-next>` : ""}
|
||||
</paper-icon-item>
|
||||
</a>
|
||||
(entity) => html`
|
||||
<paper-icon-item
|
||||
@click=${this._openMoreInfo}
|
||||
.entity_id=${entity.entity_id}
|
||||
>
|
||||
<span slot="item-icon" class="icon">
|
||||
<state-badge
|
||||
.title=${entity.attributes.title ||
|
||||
entity.attributes.friendly_name}
|
||||
.stateObj=${entity}
|
||||
slot="item-icon"
|
||||
></state-badge>
|
||||
</span>
|
||||
<paper-item-body two-line>
|
||||
${entity.attributes.title || entity.attributes.friendly_name}
|
||||
<div secondary>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.updates.version_available",
|
||||
{
|
||||
version_available: entity.attributes.latest_version,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</paper-item-body>
|
||||
${!this.narrow ? html`<ha-icon-next></ha-icon-next>` : ""}
|
||||
</paper-icon-item>
|
||||
`
|
||||
)}
|
||||
${!this._showAll && this.supervisorUpdates.length >= 4
|
||||
${!this._showAll && this.updateEntities.length >= 4
|
||||
? html`
|
||||
<button class="show-more" @click=${this._showAllClicked}>
|
||||
${this.hass.localize("ui.panel.config.updates.more_updates", {
|
||||
count: this.supervisorUpdates!.length - updates.length,
|
||||
count: this.updateEntities!.length - updates.length,
|
||||
})}
|
||||
</button>
|
||||
`
|
||||
@@ -87,6 +78,12 @@ class HaConfigUpdates extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _openMoreInfo(ev: MouseEvent): void {
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId: (ev.currentTarget as any).entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _showAllClicked() {
|
||||
this._showAll = true;
|
||||
}
|
||||
@@ -99,25 +96,11 @@ class HaConfigUpdates extends LitElement {
|
||||
padding: 16px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.icon {
|
||||
display: inline-flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
img,
|
||||
ha-svg-icon,
|
||||
ha-logo-svg {
|
||||
--mdc-icon-size: 32px;
|
||||
max-height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
ha-logo-svg {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-icon-next {
|
||||
color: var(--secondary-text-color);
|
||||
height: 24px;
|
||||
@@ -139,6 +122,9 @@ class HaConfigUpdates extends LitElement {
|
||||
outline: none;
|
||||
text-decoration: underline;
|
||||
}
|
||||
paper-icon-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -58,12 +58,11 @@ export class HaDeviceInfoZWaveJS extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const configEntries = await getConfigEntries(this.hass);
|
||||
const configEntries = await getConfigEntries(this.hass, {
|
||||
domain: "zwave_js",
|
||||
});
|
||||
let zwaveJsConfEntries = 0;
|
||||
for (const entry of configEntries) {
|
||||
if (entry.domain !== "zwave_js") {
|
||||
continue;
|
||||
}
|
||||
if (zwaveJsConfEntries) {
|
||||
this._multipleConfigEntries = true;
|
||||
}
|
||||
|
@@ -121,6 +121,7 @@ export class EnergyGasSettings extends LitElement {
|
||||
showEnergySettingsGasDialog(this, {
|
||||
unit: getEnergyGasUnitCategory(this.hass, this.preferences),
|
||||
saveCallback: async (source) => {
|
||||
delete source.unit_of_measurement;
|
||||
await this._savePreferences({
|
||||
...this.preferences,
|
||||
energy_sources: this.preferences.energy_sources.concat(source),
|
||||
|
@@ -54,7 +54,7 @@ export class EnergyGridSettings extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public validationResult?: EnergyPreferencesValidation;
|
||||
|
||||
@state() private _configEntries?: ConfigEntry[];
|
||||
@state() private _co2ConfigEntry?: ConfigEntry;
|
||||
|
||||
protected firstUpdated() {
|
||||
this._fetchCO2SignalConfigEntries();
|
||||
@@ -195,28 +195,28 @@ export class EnergyGridSettings extends LitElement {
|
||||
"ui.panel.config.energy.grid.grid_carbon_footprint"
|
||||
)}
|
||||
</h3>
|
||||
${this._configEntries?.map(
|
||||
(entry) => html`<div class="row" .entry=${entry}>
|
||||
<img
|
||||
referrerpolicy="no-referrer"
|
||||
src=${brandsUrl({
|
||||
domain: "co2signal",
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
/>
|
||||
<span class="content">${entry.title}</span>
|
||||
<a href=${`/config/integrations#config_entry=${entry.entry_id}`}>
|
||||
<ha-icon-button .path=${mdiPencil}></ha-icon-button>
|
||||
</a>
|
||||
<ha-icon-button
|
||||
@click=${this._removeCO2Sensor}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
)}
|
||||
${this._configEntries?.length === 0
|
||||
? html`
|
||||
${this._co2ConfigEntry
|
||||
? html`<div class="row" .entry=${this._co2ConfigEntry}>
|
||||
<img
|
||||
referrerpolicy="no-referrer"
|
||||
src=${brandsUrl({
|
||||
domain: "co2signal",
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
/>
|
||||
<span class="content">${this._co2ConfigEntry.title}</span>
|
||||
<a
|
||||
href=${`/config/integrations#config_entry=${this._co2ConfigEntry.entry_id}`}
|
||||
>
|
||||
<ha-icon-button .path=${mdiPencil}></ha-icon-button>
|
||||
</a>
|
||||
<ha-icon-button
|
||||
@click=${this._removeCO2Sensor}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
: html`
|
||||
<div class="row border-bottom">
|
||||
<img
|
||||
referrerpolicy="no-referrer"
|
||||
@@ -232,17 +232,15 @@ export class EnergyGridSettings extends LitElement {
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchCO2SignalConfigEntries() {
|
||||
this._configEntries = (await getConfigEntries(this.hass)).filter(
|
||||
(entry) => entry.domain === "co2signal"
|
||||
);
|
||||
const entries = await getConfigEntries(this.hass, { domain: "co2signal" });
|
||||
this._co2ConfigEntry = entries.length ? entries[0] : undefined;
|
||||
}
|
||||
|
||||
private _addCO2Sensor() {
|
||||
|
@@ -176,9 +176,17 @@ export class DialogEnergySolarSettings
|
||||
|
||||
private async _fetchSolarForecastConfigEntries() {
|
||||
const domains = this._params!.info.solar_forecast_domains;
|
||||
this._configEntries = (await getConfigEntries(this.hass)).filter((entry) =>
|
||||
domains.includes(entry.domain)
|
||||
);
|
||||
this._configEntries =
|
||||
domains.length === 0
|
||||
? []
|
||||
: domains.length === 1
|
||||
? await getConfigEntries(this.hass, {
|
||||
type: "integration",
|
||||
domain: domains[0],
|
||||
})
|
||||
: (await getConfigEntries(this.hass, { type: "integration" })).filter(
|
||||
(entry) => domains.includes(entry.domain)
|
||||
);
|
||||
}
|
||||
|
||||
private _handleForecastChanged(ev: CustomEvent) {
|
||||
|
@@ -10,50 +10,11 @@ import { customElement, property, state, query } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
|
||||
import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import {
|
||||
deleteCounter,
|
||||
fetchCounter,
|
||||
updateCounter,
|
||||
} from "../../../../../data/counter";
|
||||
import {
|
||||
ExtEntityRegistryEntry,
|
||||
removeEntityRegistryEntry,
|
||||
} from "../../../../../data/entity_registry";
|
||||
import {
|
||||
deleteInputBoolean,
|
||||
fetchInputBoolean,
|
||||
updateInputBoolean,
|
||||
} from "../../../../../data/input_boolean";
|
||||
import {
|
||||
deleteInputButton,
|
||||
fetchInputButton,
|
||||
updateInputButton,
|
||||
} from "../../../../../data/input_button";
|
||||
import {
|
||||
deleteInputDateTime,
|
||||
fetchInputDateTime,
|
||||
updateInputDateTime,
|
||||
} from "../../../../../data/input_datetime";
|
||||
import {
|
||||
deleteInputNumber,
|
||||
fetchInputNumber,
|
||||
updateInputNumber,
|
||||
} from "../../../../../data/input_number";
|
||||
import {
|
||||
deleteInputSelect,
|
||||
fetchInputSelect,
|
||||
updateInputSelect,
|
||||
} from "../../../../../data/input_select";
|
||||
import {
|
||||
deleteInputText,
|
||||
fetchInputText,
|
||||
updateInputText,
|
||||
} from "../../../../../data/input_text";
|
||||
import {
|
||||
deleteTimer,
|
||||
fetchTimer,
|
||||
updateTimer,
|
||||
} from "../../../../../data/timer";
|
||||
import { HELPERS_CRUD } from "../../../../../data/helpers_crud";
|
||||
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
@@ -69,49 +30,6 @@ import "../../../helpers/forms/ha-timer-form";
|
||||
import "../../entity-registry-basic-editor";
|
||||
import type { HaEntityRegistryBasicEditor } from "../../entity-registry-basic-editor";
|
||||
|
||||
const HELPERS = {
|
||||
input_boolean: {
|
||||
fetch: fetchInputBoolean,
|
||||
update: updateInputBoolean,
|
||||
delete: deleteInputBoolean,
|
||||
},
|
||||
input_button: {
|
||||
fetch: fetchInputButton,
|
||||
update: updateInputButton,
|
||||
delete: deleteInputButton,
|
||||
},
|
||||
input_text: {
|
||||
fetch: fetchInputText,
|
||||
update: updateInputText,
|
||||
delete: deleteInputText,
|
||||
},
|
||||
input_number: {
|
||||
fetch: fetchInputNumber,
|
||||
update: updateInputNumber,
|
||||
delete: deleteInputNumber,
|
||||
},
|
||||
input_datetime: {
|
||||
fetch: fetchInputDateTime,
|
||||
update: updateInputDateTime,
|
||||
delete: deleteInputDateTime,
|
||||
},
|
||||
input_select: {
|
||||
fetch: fetchInputSelect,
|
||||
update: updateInputSelect,
|
||||
delete: deleteInputSelect,
|
||||
},
|
||||
counter: {
|
||||
fetch: fetchCounter,
|
||||
update: updateCounter,
|
||||
delete: deleteCounter,
|
||||
},
|
||||
timer: {
|
||||
fetch: fetchTimer,
|
||||
update: updateTimer,
|
||||
delete: deleteTimer,
|
||||
},
|
||||
};
|
||||
|
||||
@customElement("entity-settings-helper-tab")
|
||||
export class EntityRegistrySettingsHelper extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -198,7 +116,7 @@ export class EntityRegistrySettingsHelper extends LitElement {
|
||||
}
|
||||
|
||||
private async _getItem() {
|
||||
const items = await HELPERS[this.entry.platform].fetch(this.hass!);
|
||||
const items = await HELPERS_CRUD[this.entry.platform].fetch(this.hass!);
|
||||
this._item = items.find((item) => item.id === this.entry.unique_id) || null;
|
||||
}
|
||||
|
||||
@@ -206,7 +124,7 @@ export class EntityRegistrySettingsHelper extends LitElement {
|
||||
this._submitting = true;
|
||||
try {
|
||||
if (this._componentLoaded && this._item) {
|
||||
await HELPERS[this.entry.platform].update(
|
||||
await HELPERS_CRUD[this.entry.platform].update(
|
||||
this.hass!,
|
||||
this._item.id,
|
||||
this._item
|
||||
@@ -236,7 +154,10 @@ export class EntityRegistrySettingsHelper extends LitElement {
|
||||
|
||||
try {
|
||||
if (this._componentLoaded && this._item) {
|
||||
await HELPERS[this.entry.platform].delete(this.hass!, this._item.id);
|
||||
await HELPERS_CRUD[this.entry.platform].delete(
|
||||
this.hass!,
|
||||
this._item.id
|
||||
);
|
||||
} else {
|
||||
const stateObj = this.hass.states[this.entry.entity_id];
|
||||
if (!stateObj?.attributes.restored) {
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "@material/mwc-formfield/mwc-formfield";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import "../../../components/ha-area-picker";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-radio";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-textfield";
|
||||
import "../../../components/ha-radio";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
@@ -182,9 +182,12 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||
name="hiddendisabled"
|
||||
value="enabled"
|
||||
.checked=${!this._hiddenBy && !this._disabledBy}
|
||||
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
|
||||
this._device?.disabled_by ||
|
||||
(this._disabledBy && this._disabledBy !== "user")}
|
||||
.disabled=${this._device?.disabled_by ||
|
||||
(this._disabledBy &&
|
||||
!(
|
||||
this._disabledBy === "user" ||
|
||||
this._disabledBy === "integration"
|
||||
))}
|
||||
@change=${this._viewStatusChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
@@ -197,9 +200,12 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||
name="hiddendisabled"
|
||||
value="hidden"
|
||||
.checked=${this._hiddenBy !== null}
|
||||
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
|
||||
Boolean(this._device?.disabled_by) ||
|
||||
(this._disabledBy && this._disabledBy !== "user")}
|
||||
.disabled=${this._device?.disabled_by ||
|
||||
(this._disabledBy &&
|
||||
!(
|
||||
this._disabledBy === "user" ||
|
||||
this._disabledBy === "integration"
|
||||
))}
|
||||
@change=${this._viewStatusChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
@@ -212,9 +218,12 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||
name="hiddendisabled"
|
||||
value="disabled"
|
||||
.checked=${this._disabledBy !== null}
|
||||
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
|
||||
Boolean(this._device?.disabled_by) ||
|
||||
(this._disabledBy && this._disabledBy !== "user")}
|
||||
.disabled=${this._device?.disabled_by ||
|
||||
(this._disabledBy &&
|
||||
!(
|
||||
this._disabledBy === "user" ||
|
||||
this._disabledBy === "integration"
|
||||
))}
|
||||
@change=${this._viewStatusChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import "@material/mwc-formfield/mwc-formfield";
|
||||
import "../../../components/ha-radio";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-formfield/mwc-formfield";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
@@ -20,9 +19,15 @@ import "../../../components/ha-alert";
|
||||
import "../../../components/ha-area-picker";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-radio";
|
||||
import "../../../components/ha-select";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-textfield";
|
||||
import {
|
||||
ConfigEntry,
|
||||
deleteConfigEntry,
|
||||
getConfigEntries,
|
||||
} from "../../../data/config_entries";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
@@ -34,6 +39,7 @@ import {
|
||||
removeEntityRegistryEntry,
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../../data/entity_registry";
|
||||
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
@@ -45,18 +51,36 @@ import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detai
|
||||
|
||||
const OVERRIDE_DEVICE_CLASSES = {
|
||||
cover: [
|
||||
"awning",
|
||||
"blind",
|
||||
"curtain",
|
||||
"damper",
|
||||
"door",
|
||||
"garage",
|
||||
"gate",
|
||||
"shade",
|
||||
"shutter",
|
||||
"window",
|
||||
[
|
||||
"awning",
|
||||
"blind",
|
||||
"curtain",
|
||||
"damper",
|
||||
"door",
|
||||
"garage",
|
||||
"gate",
|
||||
"shade",
|
||||
"shutter",
|
||||
"window",
|
||||
],
|
||||
],
|
||||
binary_sensor: [
|
||||
["lock"], // Lock
|
||||
["window", "door", "garage_door", "opening"], // Door
|
||||
["battery", "battery_charging"], // Battery
|
||||
["cold", "gas", "heat"], // Climate
|
||||
["running", "motion", "moving", "occupancy", "presence", "vibration"], // Presence
|
||||
["power", "plug", "light"], // Power
|
||||
[
|
||||
"smoke",
|
||||
"safety",
|
||||
"sound",
|
||||
"problem",
|
||||
"tamper",
|
||||
"carbon_monoxide",
|
||||
"moisture",
|
||||
], // Alarm
|
||||
],
|
||||
binary_sensor: ["window", "door", "garage_door", "opening"],
|
||||
};
|
||||
|
||||
@customElement("entity-registry-settings")
|
||||
@@ -79,16 +103,20 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _hiddenBy!: string | null;
|
||||
|
||||
private _deviceLookup?: Record<string, DeviceRegistryEntry>;
|
||||
|
||||
@state() private _device?: DeviceRegistryEntry;
|
||||
|
||||
@state() private _helperConfigEntry?: ConfigEntry;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _submitting?: boolean;
|
||||
|
||||
private _origEntityId!: string;
|
||||
|
||||
private _deviceLookup?: Record<string, DeviceRegistryEntry>;
|
||||
|
||||
private _deviceClassOptions?: string[][];
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
|
||||
@@ -103,23 +131,55 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("entry")) {
|
||||
this._error = undefined;
|
||||
this._name = this.entry.name || "";
|
||||
this._icon = this.entry.icon || "";
|
||||
this._deviceClass =
|
||||
this.entry.device_class || this.entry.original_device_class;
|
||||
this._origEntityId = this.entry.entity_id;
|
||||
this._areaId = this.entry.area_id;
|
||||
this._entityId = this.entry.entity_id;
|
||||
this._disabledBy = this.entry.disabled_by;
|
||||
this._hiddenBy = this.entry.hidden_by;
|
||||
this._device =
|
||||
this.entry.device_id && this._deviceLookup
|
||||
? this._deviceLookup[this.entry.device_id]
|
||||
: undefined;
|
||||
protected firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
if (this.entry.config_entry_id) {
|
||||
getConfigEntries(this.hass, {
|
||||
type: "helper",
|
||||
domain: this.entry.platform,
|
||||
}).then((entries) => {
|
||||
this._helperConfigEntry = entries.find(
|
||||
(ent) => ent.entry_id === this.entry.config_entry_id
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues) {
|
||||
super.willUpdate(changedProperties);
|
||||
if (!changedProperties.has("entry")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._error = undefined;
|
||||
this._name = this.entry.name || "";
|
||||
this._icon = this.entry.icon || "";
|
||||
this._deviceClass =
|
||||
this.entry.device_class || this.entry.original_device_class;
|
||||
this._origEntityId = this.entry.entity_id;
|
||||
this._areaId = this.entry.area_id;
|
||||
this._entityId = this.entry.entity_id;
|
||||
this._disabledBy = this.entry.disabled_by;
|
||||
this._hiddenBy = this.entry.hidden_by;
|
||||
this._device =
|
||||
this.entry.device_id && this._deviceLookup
|
||||
? this._deviceLookup[this.entry.device_id]
|
||||
: undefined;
|
||||
|
||||
const domain = computeDomain(this.entry.entity_id);
|
||||
const deviceClasses: string[][] = OVERRIDE_DEVICE_CLASSES[domain];
|
||||
|
||||
if (!deviceClasses) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._deviceClassOptions = [[], []];
|
||||
for (const deviceClass of deviceClasses) {
|
||||
if (deviceClass.includes(this.entry.original_device_class!)) {
|
||||
this._deviceClassOptions[0] = deviceClass;
|
||||
} else {
|
||||
this._deviceClassOptions[1].push(...deviceClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,28 +235,39 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
: undefined}
|
||||
.disabled=${this._submitting}
|
||||
></ha-icon-picker>
|
||||
${OVERRIDE_DEVICE_CLASSES[domain]?.includes(this._deviceClass) ||
|
||||
(domain === "cover" && this.entry.original_device_class === null)
|
||||
? html`<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.device_class"
|
||||
)}
|
||||
.value=${this._deviceClass}
|
||||
naturalMenuWidth
|
||||
fixedMenuPosition
|
||||
@selected=${this._deviceClassChanged}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${OVERRIDE_DEVICE_CLASSES[domain].map(
|
||||
(deviceClass: string) => html`
|
||||
<mwc-list-item .value=${deviceClass}>
|
||||
${this.hass.localize(
|
||||
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}`
|
||||
)}
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>`
|
||||
${this._deviceClassOptions
|
||||
? html`
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.device_class"
|
||||
)}
|
||||
.value=${this._deviceClass}
|
||||
naturalMenuWidth
|
||||
fixedMenuPosition
|
||||
@selected=${this._deviceClassChanged}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${this._deviceClassOptions[0].map(
|
||||
(deviceClass: string) => html`
|
||||
<mwc-list-item .value=${deviceClass} test=${deviceClass}>
|
||||
${this.hass.localize(
|
||||
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}`
|
||||
)}
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
<li divider role="separator"></li>
|
||||
${this._deviceClassOptions[1].map(
|
||||
(deviceClass: string) => html`
|
||||
<mwc-list-item .value=${deviceClass} test=${deviceClass}>
|
||||
${this.hass.localize(
|
||||
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}`
|
||||
)}
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
`
|
||||
: ""}
|
||||
<ha-textfield
|
||||
error-message="Domain needs to stay the same"
|
||||
@@ -215,6 +286,21 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
@value-changed=${this._areaPicked}
|
||||
></ha-area-picker>`
|
||||
: ""}
|
||||
${this._helperConfigEntry
|
||||
? html`
|
||||
<div class="row">
|
||||
<mwc-button
|
||||
@click=${this._showOptionsFlow}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.configure_state"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<ha-expansion-panel
|
||||
.header=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.advanced"
|
||||
@@ -227,7 +313,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
)}:
|
||||
</div>
|
||||
<div class="secondary">
|
||||
${this._disabledBy && this._disabledBy !== "user"
|
||||
${this._disabledBy &&
|
||||
this._disabledBy !== "user" &&
|
||||
this._disabledBy !== "integration"
|
||||
? this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.enabled_cause",
|
||||
"cause",
|
||||
@@ -249,7 +337,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
.checked=${!this._hiddenBy && !this._disabledBy}
|
||||
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
|
||||
this._device?.disabled_by ||
|
||||
(this._disabledBy && this._disabledBy !== "user")}
|
||||
(this._disabledBy &&
|
||||
this._disabledBy !== "user" &&
|
||||
this._disabledBy !== "integration")}
|
||||
@change=${this._viewStatusChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
@@ -264,7 +354,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
.checked=${this._hiddenBy !== null}
|
||||
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
|
||||
Boolean(this._device?.disabled_by) ||
|
||||
(this._disabledBy && this._disabledBy !== "user")}
|
||||
(this._disabledBy &&
|
||||
this._disabledBy !== "user" &&
|
||||
this._disabledBy !== "integration")}
|
||||
@change=${this._viewStatusChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
@@ -279,7 +371,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
.checked=${this._disabledBy !== null}
|
||||
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
|
||||
Boolean(this._device?.disabled_by) ||
|
||||
(this._disabledBy && this._disabledBy !== "user")}
|
||||
(this._disabledBy &&
|
||||
this._disabledBy !== "user" &&
|
||||
this._disabledBy !== "integration")}
|
||||
@change=${this._viewStatusChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
@@ -341,7 +435,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
class="warning"
|
||||
@click=${this._confirmDeleteEntry}
|
||||
.disabled=${this._submitting ||
|
||||
!(stateObj && stateObj.attributes.restored)}
|
||||
(!this._helperConfigEntry && !stateObj?.attributes.restored)}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.entity_registry.editor.delete")}
|
||||
</mwc-button>
|
||||
@@ -471,13 +565,21 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
this._submitting = true;
|
||||
|
||||
try {
|
||||
await removeEntityRegistryEntry(this.hass!, this._origEntityId);
|
||||
if (this._helperConfigEntry) {
|
||||
await deleteConfigEntry(this.hass, this._helperConfigEntry.entry_id);
|
||||
} else {
|
||||
await removeEntityRegistryEntry(this.hass!, this._origEntityId);
|
||||
}
|
||||
fireEvent(this, "close-dialog");
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _showOptionsFlow() {
|
||||
showOptionsFlowDialog(this, this._helperConfigEntry!);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -532,6 +634,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
margin: 8px 0;
|
||||
width: 340px;
|
||||
}
|
||||
li[divider] {
|
||||
border-bottom-color: var(--divider-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -27,10 +27,6 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||
import { CloudStatus, fetchCloudStatus } from "../../data/cloud";
|
||||
import {
|
||||
fetchSupervisorAvailableUpdates,
|
||||
SupervisorAvailableUpdates,
|
||||
} from "../../data/supervisor/root";
|
||||
import "../../layouts/hass-loading-screen";
|
||||
import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page";
|
||||
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
||||
@@ -397,8 +393,6 @@ class HaPanelConfig extends HassRouterPage {
|
||||
|
||||
@state() private _cloudStatus?: CloudStatus;
|
||||
|
||||
@state() private _supervisorUpdates?: SupervisorAvailableUpdates[] | null;
|
||||
|
||||
private _listeners: Array<() => void> = [];
|
||||
|
||||
public connectedCallback() {
|
||||
@@ -433,19 +427,7 @@ class HaPanelConfig extends HassRouterPage {
|
||||
}
|
||||
});
|
||||
}
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
this._loadSupervisorUpdates();
|
||||
this.addEventListener("ha-refresh-supervisor", () => {
|
||||
this._loadSupervisorUpdates();
|
||||
});
|
||||
this.addEventListener("connection-status", (ev) => {
|
||||
if (ev.detail === "connected") {
|
||||
this._loadSupervisorUpdates();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._supervisorUpdates = null;
|
||||
}
|
||||
|
||||
this.addEventListener("ha-refresh-cloud-status", () =>
|
||||
this._updateCloudStatus()
|
||||
);
|
||||
@@ -476,7 +458,6 @@ class HaPanelConfig extends HassRouterPage {
|
||||
isWide,
|
||||
narrow: this.narrow,
|
||||
cloudStatus: this._cloudStatus,
|
||||
supervisorUpdates: this._supervisorUpdates,
|
||||
});
|
||||
} else {
|
||||
el.route = this.routeTail;
|
||||
@@ -485,7 +466,6 @@ class HaPanelConfig extends HassRouterPage {
|
||||
el.isWide = isWide;
|
||||
el.narrow = this.narrow;
|
||||
el.cloudStatus = this._cloudStatus;
|
||||
el.supervisorUpdates = this._supervisorUpdates;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,16 +483,6 @@ class HaPanelConfig extends HassRouterPage {
|
||||
setTimeout(() => this._updateCloudStatus(), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadSupervisorUpdates(): Promise<void> {
|
||||
try {
|
||||
this._supervisorUpdates = await fetchSupervisorAvailableUpdates(
|
||||
this.hass
|
||||
);
|
||||
} catch (err) {
|
||||
this._supervisorUpdates = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { Counter } from "../../../data/counter";
|
||||
import { InputBoolean } from "../../../data/input_boolean";
|
||||
import { InputButton } from "../../../data/input_button";
|
||||
import { InputDateTime } from "../../../data/input_datetime";
|
||||
import { InputNumber } from "../../../data/input_number";
|
||||
import { InputSelect } from "../../../data/input_select";
|
||||
import { InputText } from "../../../data/input_text";
|
||||
import { Timer } from "../../../data/timer";
|
||||
import type { Counter } from "../../../data/counter";
|
||||
import type { InputBoolean } from "../../../data/input_boolean";
|
||||
import type { InputButton } from "../../../data/input_button";
|
||||
import type { InputDateTime } from "../../../data/input_datetime";
|
||||
import type { InputNumber } from "../../../data/input_number";
|
||||
import type { InputSelect } from "../../../data/input_select";
|
||||
import type { InputText } from "../../../data/input_text";
|
||||
import type { Timer } from "../../../data/timer";
|
||||
|
||||
export const HELPER_DOMAINS = [
|
||||
"input_boolean",
|
||||
|
@@ -8,6 +8,8 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
|
||||
import { domainIcon } from "../../../common/entity/domain_icon";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import { getConfigFlowHandlers } from "../../../data/config_flow";
|
||||
import { createCounter } from "../../../data/counter";
|
||||
import { createInputBoolean } from "../../../data/input_boolean";
|
||||
import { createInputButton } from "../../../data/input_button";
|
||||
@@ -16,6 +18,7 @@ import { createInputNumber } from "../../../data/input_number";
|
||||
import { createInputSelect } from "../../../data/input_select";
|
||||
import { createInputText } from "../../../data/input_text";
|
||||
import { createTimer } from "../../../data/timer";
|
||||
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { Helper } from "./const";
|
||||
@@ -27,6 +30,8 @@ import "./forms/ha-input_number-form";
|
||||
import "./forms/ha-input_select-form";
|
||||
import "./forms/ha-input_text-form";
|
||||
import "./forms/ha-timer-form";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import type { ShowDialogHelperDetailParams } from "./show-dialog-helper-detail";
|
||||
|
||||
const HELPERS = {
|
||||
input_boolean: createInputBoolean,
|
||||
@@ -47,7 +52,7 @@ export class DialogHelperDetail extends LitElement {
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _platform?: string;
|
||||
@state() private _domain?: string;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@@ -55,102 +60,135 @@ export class DialogHelperDetail extends LitElement {
|
||||
|
||||
@query(".form") private _form?: HTMLDivElement;
|
||||
|
||||
public async showDialog(): Promise<void> {
|
||||
this._platform = undefined;
|
||||
@state() private _helperFlows?: string[];
|
||||
|
||||
private _params?: ShowDialogHelperDetailParams;
|
||||
|
||||
public async showDialog(params: ShowDialogHelperDetailParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._domain = undefined;
|
||||
this._item = undefined;
|
||||
this._opened = true;
|
||||
await this.updateComplete;
|
||||
Promise.all([
|
||||
getConfigFlowHandlers(this.hass, "helper"),
|
||||
// Ensure the titles are loaded before we render the flows.
|
||||
this.hass.loadBackendTranslation("title", undefined, true),
|
||||
]).then(([flows]) => {
|
||||
this._helperFlows = flows;
|
||||
});
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._opened = false;
|
||||
this._error = "";
|
||||
this._params = undefined;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
let content: TemplateResult;
|
||||
|
||||
if (this._domain) {
|
||||
content = html`
|
||||
<div class="form" @value-changed=${this._valueChanged}>
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
${dynamicElement(`ha-${this._domain}-form`, {
|
||||
hass: this.hass,
|
||||
item: this._item,
|
||||
new: true,
|
||||
})}
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._createItem}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.helpers.dialog.create")}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._goBack}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass!.localize("ui.common.back")}
|
||||
</mwc-button>
|
||||
`;
|
||||
} else if (this._helperFlows === undefined) {
|
||||
content = html`<ha-circular-progress active></ha-circular-progress>`;
|
||||
} else {
|
||||
const items: [string, string][] = [];
|
||||
|
||||
for (const helper of Object.keys(HELPERS)) {
|
||||
items.push([
|
||||
helper,
|
||||
this.hass.localize(`ui.panel.config.helpers.types.${helper}`) ||
|
||||
helper,
|
||||
]);
|
||||
}
|
||||
|
||||
for (const domain of this._helperFlows) {
|
||||
items.push([domain, domainToName(this.hass.localize, domain)]);
|
||||
}
|
||||
|
||||
items.sort((a, b) => a[1].localeCompare(b[1]));
|
||||
|
||||
content = html`
|
||||
${items.map(([domain, label]) => {
|
||||
// Only OG helpers need to be loaded prior adding one
|
||||
const isLoaded =
|
||||
!(domain in HELPERS) || isComponentLoaded(this.hass, domain);
|
||||
return html`
|
||||
<mwc-list-item
|
||||
.disabled=${!isLoaded}
|
||||
.domain=${domain}
|
||||
@click=${this._domainPicked}
|
||||
@keydown=${this._handleEnter}
|
||||
dialogInitialFocus
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${domainIcon(domain)}
|
||||
></ha-svg-icon>
|
||||
<span class="item-text"> ${label} </span>
|
||||
</mwc-list-item>
|
||||
${!isLoaded
|
||||
? html`
|
||||
<paper-tooltip animation-delay="0"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.platform_not_loaded",
|
||||
"platform",
|
||||
domain
|
||||
)}</paper-tooltip
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
})}
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass!.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._opened}
|
||||
@closed=${this.closeDialog}
|
||||
class=${classMap({ "button-left": !this._platform })}
|
||||
class=${classMap({ "button-left": !this._domain })}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${this._platform
|
||||
.heading=${this._domain
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.helpers.dialog.add_platform",
|
||||
"platform",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.helpers.types.${this._platform}`
|
||||
) || this._platform
|
||||
`ui.panel.config.helpers.types.${this._domain}`
|
||||
) || this._domain
|
||||
)
|
||||
: this.hass.localize("ui.panel.config.helpers.dialog.add_helper")}
|
||||
>
|
||||
${this._platform
|
||||
? html`
|
||||
<div class="form" @value-changed=${this._valueChanged}>
|
||||
${this._error
|
||||
? html` <div class="error">${this._error}</div> `
|
||||
: ""}
|
||||
${dynamicElement(`ha-${this._platform}-form`, {
|
||||
hass: this.hass,
|
||||
item: this._item,
|
||||
new: true,
|
||||
})}
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._createItem}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.helpers.dialog.create")}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._goBack}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass!.localize("ui.common.back")}
|
||||
</mwc-button>
|
||||
`
|
||||
: html`
|
||||
${Object.keys(HELPERS).map((platform: string) => {
|
||||
const isLoaded = isComponentLoaded(this.hass, platform);
|
||||
return html`
|
||||
<mwc-list-item
|
||||
.disabled=${!isLoaded}
|
||||
.platform=${platform}
|
||||
@click=${this._platformPicked}
|
||||
@keydown=${this._handleEnter}
|
||||
dialogInitialFocus
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${domainIcon(platform)}
|
||||
></ha-svg-icon>
|
||||
<span class="item-text">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.helpers.types.${platform}`
|
||||
) || platform}
|
||||
</span>
|
||||
</mwc-list-item>
|
||||
${!isLoaded
|
||||
? html`
|
||||
<paper-tooltip animation-delay="0"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.platform_not_loaded",
|
||||
"platform",
|
||||
platform
|
||||
)}</paper-tooltip
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
})}
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass!.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
`}
|
||||
${content}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
@@ -160,13 +198,13 @@ export class DialogHelperDetail extends LitElement {
|
||||
}
|
||||
|
||||
private async _createItem(): Promise<void> {
|
||||
if (!this._platform || !this._item) {
|
||||
if (!this._domain || !this._item) {
|
||||
return;
|
||||
}
|
||||
this._submitting = true;
|
||||
this._error = "";
|
||||
try {
|
||||
await HELPERS[this._platform](this.hass, this._item);
|
||||
await HELPERS[this._domain](this.hass, this._item);
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = err.message || "Unknown error";
|
||||
@@ -181,12 +219,22 @@ export class DialogHelperDetail extends LitElement {
|
||||
}
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this._platformPicked(ev);
|
||||
this._domainPicked(ev);
|
||||
}
|
||||
|
||||
private _platformPicked(ev: Event): void {
|
||||
this._platform = (ev.currentTarget! as any).platform;
|
||||
this._focusForm();
|
||||
private _domainPicked(ev: Event): void {
|
||||
const domain = (ev.currentTarget! as any).domain;
|
||||
|
||||
if (domain in HELPERS) {
|
||||
this._domain = domain;
|
||||
this._focusForm();
|
||||
} else {
|
||||
showConfigFlowDialog(this, {
|
||||
startFlowHandler: domain,
|
||||
dialogClosedCallback: this._params!.dialogClosedCallback,
|
||||
});
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private async _focusForm(): Promise<void> {
|
||||
@@ -195,7 +243,7 @@ export class DialogHelperDetail extends LitElement {
|
||||
}
|
||||
|
||||
private _goBack() {
|
||||
this._platform = undefined;
|
||||
this._domain = undefined;
|
||||
this._item = undefined;
|
||||
this._error = undefined;
|
||||
}
|
||||
|
@@ -1,28 +1,58 @@
|
||||
import { mdiPencilOff, mdiPlus } from "@mdi/js";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoize from "memoize-one";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { domainIcon } from "../../../common/entity/domain_icon";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../../data/entity_registry";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { HELPER_DOMAINS } from "./const";
|
||||
import { showHelperDetailDialog } from "./show-dialog-helper-detail";
|
||||
|
||||
// This groups items by a key but only returns last entry per key.
|
||||
const groupByOne = <T>(
|
||||
items: T[],
|
||||
keySelector: (item: T) => string
|
||||
): Record<string, T> => {
|
||||
const result: Record<string, T> = {};
|
||||
for (const item of items) {
|
||||
result[keySelector(item)] = item;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const getConfigEntry = (
|
||||
entityEntries: Record<string, EntityRegistryEntry>,
|
||||
configEntries: Record<string, ConfigEntry>,
|
||||
entityId: string
|
||||
) => {
|
||||
const configEntryId = entityEntries![entityId]?.config_entry_id;
|
||||
return configEntryId ? configEntries![configEntryId] : undefined;
|
||||
};
|
||||
|
||||
@customElement("ha-config-helpers")
|
||||
export class HaConfigHelpers extends LitElement {
|
||||
export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public isWide!: boolean;
|
||||
@@ -33,98 +63,122 @@ export class HaConfigHelpers extends LitElement {
|
||||
|
||||
@state() private _stateItems: HassEntity[] = [];
|
||||
|
||||
private _columns = memoize((narrow, _language): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer = {
|
||||
icon: {
|
||||
@state() private _entityEntries?: Record<string, EntityRegistryEntry>;
|
||||
|
||||
@state() private _configEntries?: Record<string, ConfigEntry>;
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer = {
|
||||
icon: {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.helpers.picker.headers.icon"),
|
||||
type: "icon",
|
||||
template: (icon, helper: any) =>
|
||||
icon
|
||||
? html` <ha-icon .icon=${icon}></ha-icon> `
|
||||
: html`<ha-svg-icon
|
||||
.path=${domainIcon(helper.type)}
|
||||
></ha-svg-icon>`,
|
||||
},
|
||||
name: {
|
||||
title: localize("ui.panel.config.helpers.picker.headers.name"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
direction: "asc",
|
||||
template: (name, item: any) =>
|
||||
html`
|
||||
${name}
|
||||
${narrow
|
||||
? html` <div class="secondary">${item.entity_id}</div> `
|
||||
: ""}
|
||||
`,
|
||||
},
|
||||
};
|
||||
if (!narrow) {
|
||||
columns.entity_id = {
|
||||
title: localize("ui.panel.config.helpers.picker.headers.entity_id"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "25%",
|
||||
};
|
||||
}
|
||||
columns.type = {
|
||||
title: localize("ui.panel.config.helpers.picker.headers.type"),
|
||||
sortable: true,
|
||||
width: "25%",
|
||||
filterable: true,
|
||||
template: (type, row) =>
|
||||
row.configEntry
|
||||
? domainToName(localize, type)
|
||||
: html`
|
||||
${localize(`ui.panel.config.helpers.types.${type}`) || type}
|
||||
`,
|
||||
};
|
||||
columns.editable = {
|
||||
title: "",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.helpers.picker.headers.icon"
|
||||
"ui.panel.config.helpers.picker.headers.editable"
|
||||
),
|
||||
type: "icon",
|
||||
template: (icon, helper: any) =>
|
||||
icon
|
||||
? html` <ha-icon .icon=${icon}></ha-icon> `
|
||||
: html`<ha-svg-icon
|
||||
.path=${domainIcon(helper.type)}
|
||||
></ha-svg-icon>`,
|
||||
},
|
||||
name: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.helpers.picker.headers.name"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
direction: "asc",
|
||||
template: (name, item: any) =>
|
||||
html`
|
||||
${name}
|
||||
${narrow
|
||||
? html` <div class="secondary">${item.entity_id}</div> `
|
||||
: ""}
|
||||
`,
|
||||
},
|
||||
};
|
||||
if (!narrow) {
|
||||
columns.entity_id = {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.helpers.picker.headers.entity_id"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "25%",
|
||||
};
|
||||
}
|
||||
columns.type = {
|
||||
title: this.hass.localize("ui.panel.config.helpers.picker.headers.type"),
|
||||
sortable: true,
|
||||
width: "25%",
|
||||
filterable: true,
|
||||
template: (type) =>
|
||||
html`
|
||||
${this.hass.localize(`ui.panel.config.helpers.types.${type}`) || type}
|
||||
template: (editable) => html`
|
||||
${!editable
|
||||
? html`
|
||||
<div
|
||||
tabindex="0"
|
||||
style="display:inline-block; position: relative;"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPencilOff}></ha-svg-icon>
|
||||
<paper-tooltip animation-delay="0" position="left">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.status.readonly"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`,
|
||||
};
|
||||
columns.editable = {
|
||||
title: "",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.helpers.picker.headers.editable"
|
||||
),
|
||||
type: "icon",
|
||||
template: (editable) => html`
|
||||
${!editable
|
||||
? html`
|
||||
<div
|
||||
tabindex="0"
|
||||
style="display:inline-block; position: relative;"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPencilOff}></ha-svg-icon>
|
||||
<paper-tooltip animation-delay="0" position="left">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.status.readonly"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`,
|
||||
};
|
||||
return columns;
|
||||
});
|
||||
};
|
||||
return columns;
|
||||
}
|
||||
);
|
||||
|
||||
private _getItems = memoize((stateItems: HassEntity[]) =>
|
||||
stateItems.map((entityState) => ({
|
||||
id: entityState.entity_id,
|
||||
icon: entityState.attributes.icon,
|
||||
name: entityState.attributes.friendly_name || "",
|
||||
entity_id: entityState.entity_id,
|
||||
editable: entityState.attributes.editable,
|
||||
type: computeStateDomain(entityState),
|
||||
}))
|
||||
private _getItems = memoizeOne(
|
||||
(
|
||||
stateItems: HassEntity[],
|
||||
entityEntries: Record<string, EntityRegistryEntry>,
|
||||
configEntries: Record<string, ConfigEntry>
|
||||
) =>
|
||||
stateItems.map((entityState) => {
|
||||
const configEntry = getConfigEntry(
|
||||
entityEntries,
|
||||
configEntries,
|
||||
entityState.entity_id
|
||||
);
|
||||
|
||||
return {
|
||||
id: entityState.entity_id,
|
||||
icon: entityState.attributes.icon,
|
||||
name: entityState.attributes.friendly_name || "",
|
||||
entity_id: entityState.entity_id,
|
||||
editable:
|
||||
configEntry !== undefined || entityState.attributes.editable,
|
||||
type: configEntry
|
||||
? configEntry.domain
|
||||
: computeStateDomain(entityState),
|
||||
configEntry,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || this._stateItems === undefined) {
|
||||
if (
|
||||
!this.hass ||
|
||||
this._stateItems === undefined ||
|
||||
this._entityEntries === undefined ||
|
||||
this._configEntries === undefined
|
||||
) {
|
||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
||||
}
|
||||
|
||||
@@ -135,8 +189,12 @@ export class HaConfigHelpers extends LitElement {
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
.columns=${this._columns(this.narrow, this.hass.language)}
|
||||
.data=${this._getItems(this._stateItems)}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
.data=${this._getItems(
|
||||
this._stateItems,
|
||||
this._entityEntries,
|
||||
this._configEntries
|
||||
)}
|
||||
@row-click=${this._openEditDialog}
|
||||
hasFab
|
||||
clickable
|
||||
@@ -160,32 +218,67 @@ export class HaConfigHelpers extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._getStates();
|
||||
this._getConfigEntries();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (oldHass && this._stateItems) {
|
||||
this._getStates(oldHass);
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this._entityEntries || !this._configEntries) {
|
||||
return;
|
||||
}
|
||||
|
||||
let changed =
|
||||
!this._stateItems ||
|
||||
changedProps.has("_entityEntries") ||
|
||||
changedProps.has("_configEntries");
|
||||
|
||||
if (!changed && changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
changed = !oldHass || oldHass.states !== this.hass.states;
|
||||
}
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const extraEntities = new Set<string>();
|
||||
|
||||
for (const entityEntry of Object.values(this._entityEntries)) {
|
||||
if (
|
||||
entityEntry.config_entry_id &&
|
||||
entityEntry.config_entry_id in this._configEntries
|
||||
) {
|
||||
extraEntities.add(entityEntry.entity_id);
|
||||
}
|
||||
}
|
||||
|
||||
const newStates = Object.values(this.hass!.states).filter(
|
||||
(entity) =>
|
||||
extraEntities.has(entity.entity_id) ||
|
||||
HELPER_DOMAINS.includes(computeStateDomain(entity))
|
||||
);
|
||||
|
||||
if (
|
||||
this._stateItems.length !== newStates.length ||
|
||||
!this._stateItems.every((val, idx) => newStates[idx] === val)
|
||||
) {
|
||||
this._stateItems = newStates;
|
||||
}
|
||||
}
|
||||
|
||||
private _getStates(oldHass?: HomeAssistant) {
|
||||
let changed = false;
|
||||
const tempStates = Object.values(this.hass!.states).filter((entity) => {
|
||||
if (!HELPER_DOMAINS.includes(computeStateDomain(entity))) {
|
||||
return false;
|
||||
}
|
||||
if (oldHass?.states[entity.entity_id] !== entity) {
|
||||
changed = true;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection!, (entries) => {
|
||||
this._entityEntries = groupByOne(entries, (entry) => entry.entity_id);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
if (changed || this._stateItems.length !== tempStates.length) {
|
||||
this._stateItems = tempStates;
|
||||
}
|
||||
private async _getConfigEntries() {
|
||||
this._configEntries = groupByOne(
|
||||
await getConfigEntries(this.hass, { type: "helper" }),
|
||||
(entry) => entry.entry_id
|
||||
);
|
||||
}
|
||||
|
||||
private async _openEditDialog(ev: CustomEvent): Promise<void> {
|
||||
@@ -196,6 +289,12 @@ export class HaConfigHelpers extends LitElement {
|
||||
}
|
||||
|
||||
private _createHelpler() {
|
||||
showHelperDetailDialog(this);
|
||||
showHelperDetailDialog(this, {
|
||||
dialogClosedCallback: (params) => {
|
||||
if (params.flowFinished) {
|
||||
this._getConfigEntries();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,11 +1,20 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { DataEntryFlowDialogParams } from "../../../dialogs/config-flow/show-dialog-data-entry-flow";
|
||||
|
||||
export const loadHelperDetailDialog = () => import("./dialog-helper-detail");
|
||||
|
||||
export const showHelperDetailDialog = (element: HTMLElement) => {
|
||||
export interface ShowDialogHelperDetailParams {
|
||||
// Only used for config entries
|
||||
dialogClosedCallback: DataEntryFlowDialogParams["dialogClosedCallback"];
|
||||
}
|
||||
|
||||
export const showHelperDetailDialog = (
|
||||
element: HTMLElement,
|
||||
params: ShowDialogHelperDetailParams
|
||||
) => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-helper-detail",
|
||||
dialogImport: loadHelperDetailDialog,
|
||||
dialogParams: {},
|
||||
dialogParams: params,
|
||||
});
|
||||
};
|
||||
|
@@ -521,24 +521,26 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _loadConfigEntries() {
|
||||
getConfigEntries(this.hass).then((configEntries) => {
|
||||
this._configEntries = configEntries
|
||||
.map(
|
||||
(entry: ConfigEntry): ConfigEntryExtended => ({
|
||||
...entry,
|
||||
localized_domain_name: domainToName(
|
||||
this.hass.localize,
|
||||
entry.domain
|
||||
),
|
||||
})
|
||||
)
|
||||
.sort((conf1, conf2) =>
|
||||
caseInsensitiveStringCompare(
|
||||
conf1.localized_domain_name + conf1.title,
|
||||
conf2.localized_domain_name + conf2.title
|
||||
getConfigEntries(this.hass, { type: "integration" }).then(
|
||||
(configEntries) => {
|
||||
this._configEntries = configEntries
|
||||
.map(
|
||||
(entry: ConfigEntry): ConfigEntryExtended => ({
|
||||
...entry,
|
||||
localized_domain_name: domainToName(
|
||||
this.hass.localize,
|
||||
entry.domain
|
||||
),
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
.sort((conf1, conf2) =>
|
||||
caseInsensitiveStringCompare(
|
||||
conf1.localized_domain_name + conf1.title,
|
||||
conf2.localized_domain_name + conf2.title
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async _scanUSBDevices() {
|
||||
@@ -656,7 +658,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
if (!domain) {
|
||||
return;
|
||||
}
|
||||
const handlers = await getConfigFlowHandlers(this.hass);
|
||||
const handlers = await getConfigFlowHandlers(this.hass, "integration");
|
||||
|
||||
if (!handlers.includes(domain)) {
|
||||
showAlertDialog(this, {
|
||||
|
@@ -111,7 +111,9 @@ class HaPanelDevMqtt extends LitElement {
|
||||
return;
|
||||
}
|
||||
const configEntryId = searchParams.get("config_entry") as string;
|
||||
const configEntries = await getConfigEntries(this.hass);
|
||||
const configEntries = await getConfigEntries(this.hass, {
|
||||
domain: "mqtt",
|
||||
});
|
||||
const configEntry = configEntries.find(
|
||||
(entry) => entry.entry_id === configEntryId
|
||||
);
|
||||
|
@@ -384,7 +384,9 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
if (!this.configEntryId) {
|
||||
return;
|
||||
}
|
||||
const configEntries = await getConfigEntries(this.hass);
|
||||
const configEntries = await getConfigEntries(this.hass, {
|
||||
domain: "zwave_js",
|
||||
});
|
||||
this._configEntry = configEntries.find(
|
||||
(entry) => entry.entry_id === this.configEntryId!
|
||||
);
|
||||
@@ -467,7 +469,9 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
if (!this.configEntryId) {
|
||||
return;
|
||||
}
|
||||
const configEntries = await getConfigEntries(this.hass);
|
||||
const configEntries = await getConfigEntries(this.hass, {
|
||||
domain: "zwave_js",
|
||||
});
|
||||
const configEntry = configEntries.find(
|
||||
(entry) => entry.entry_id === this.configEntryId
|
||||
);
|
||||
|
@@ -1,10 +1,12 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiSlopeUphill } from "@mdi/js";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/data-table/ha-data-table";
|
||||
import type { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
|
||||
import { subscribeEntityRegistry } from "../../../data/entity_registry";
|
||||
@@ -24,6 +26,7 @@ import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { showFixStatisticsUnitsChangedDialog } from "./show-dialog-statistics-fix-units-changed";
|
||||
import { showFixStatisticsUnsupportedUnitMetadataDialog } from "./show-dialog-statistics-fix-unsupported-unit-meta";
|
||||
import { showStatisticsAdjustSumDialog } from "./show-dialog-statistics-adjust-sum";
|
||||
|
||||
const FIX_ISSUES_ORDER = {
|
||||
no_state: 0,
|
||||
@@ -111,6 +114,30 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
: ""}`,
|
||||
width: "113px",
|
||||
},
|
||||
actions: {
|
||||
title: "",
|
||||
type: "overflow-menu",
|
||||
template: (
|
||||
_info,
|
||||
statistic: StatisticsMetaData
|
||||
) => html`<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.items=${[
|
||||
{
|
||||
path: mdiSlopeUphill,
|
||||
label: localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.adjust_sum"
|
||||
),
|
||||
action: () =>
|
||||
showStatisticsAdjustSumDialog(this, {
|
||||
statistic: statistic,
|
||||
}),
|
||||
},
|
||||
]}
|
||||
style="color: var(--secondary-text-color)"
|
||||
></ha-icon-overflow-menu>`,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
|
@@ -0,0 +1,166 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { LitElement, TemplateResult, html, CSSResultGroup } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import "../../../components/ha-dialog";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { haStyle, haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-radio";
|
||||
import "../../../components/ha-form/ha-form";
|
||||
import type { DialogStatisticsAdjustSumParams } from "./show-dialog-statistics-adjust-sum";
|
||||
import type {
|
||||
HaFormBaseSchema,
|
||||
HaFormSchema,
|
||||
} from "../../../components/ha-form/types";
|
||||
import { adjustStatisticsSum } from "../../../data/history";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { showToast } from "../../../util/toast";
|
||||
|
||||
let lastMoment: string | undefined;
|
||||
|
||||
@customElement("dialog-statistics-adjust-sum")
|
||||
export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: DialogStatisticsAdjustSumParams;
|
||||
|
||||
@state() private _data?: {
|
||||
moment: string;
|
||||
amount: number;
|
||||
};
|
||||
|
||||
@state() private _busy = false;
|
||||
|
||||
public showDialog(params: DialogStatisticsAdjustSumParams): void {
|
||||
this._params = params;
|
||||
this._busy = false;
|
||||
const now = new Date();
|
||||
this._data = {
|
||||
moment:
|
||||
lastMoment ||
|
||||
`${now.getFullYear()}-${
|
||||
now.getMonth() + 1
|
||||
}-${now.getDate()} ${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`,
|
||||
amount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
heading="Adjust sum for a specific time."
|
||||
>
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.schema=${this._getSchema(this._params.statistic)}
|
||||
.data=${this._data}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.disabled=${this._busy}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._fixIssue}
|
||||
dialogInitialFocus
|
||||
label="Adjust"
|
||||
></mwc-button>
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
dialogAction="cancel"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
></mwc-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _getSchema = memoizeOne((statistic): HaFormSchema[] => [
|
||||
{
|
||||
type: "constant",
|
||||
name: "name",
|
||||
value: statistic.name || statistic.statistic_id,
|
||||
},
|
||||
{
|
||||
name: "moment",
|
||||
required: true,
|
||||
selector: {
|
||||
datetime: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "amount",
|
||||
required: true,
|
||||
default: 0,
|
||||
selector: {
|
||||
number: {
|
||||
mode: "box",
|
||||
step: 0.1,
|
||||
unit_of_measurement: statistic.unit_of_measurement,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
private _computeLabel(value: HaFormBaseSchema) {
|
||||
switch (value.name) {
|
||||
case "name":
|
||||
return "Statistic";
|
||||
case "moment":
|
||||
return "Moment to adjust";
|
||||
case "amount":
|
||||
return "Amount";
|
||||
default:
|
||||
return value.name;
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChanged(ev) {
|
||||
this._data = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _fixIssue(): Promise<void> {
|
||||
this._busy = true;
|
||||
try {
|
||||
await adjustStatisticsSum(
|
||||
this.hass,
|
||||
this._params!.statistic.statistic_id,
|
||||
this._data!.moment,
|
||||
this._data!.amount
|
||||
);
|
||||
} catch (err: any) {
|
||||
this._busy = false;
|
||||
showAlertDialog(this, {
|
||||
text: `Error adjusting sum: ${err.message || err}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
showToast(this, {
|
||||
message: "Statistic sum adjusted",
|
||||
});
|
||||
lastMoment = this._data!.moment;
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [haStyle, haStyleDialog];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-statistics-adjust-sum": DialogStatisticsFixUnsupportedUnitMetadata;
|
||||
}
|
||||
}
|
@@ -11,7 +11,7 @@ import {
|
||||
} from "../../../data/history";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-radio";
|
||||
import { DialogStatisticsUnitsChangedParams } from "./show-dialog-statistics-fix-units-changed";
|
||||
import type { DialogStatisticsUnitsChangedParams } from "./show-dialog-statistics-fix-units-changed";
|
||||
|
||||
@customElement("dialog-statistics-fix-units-changed")
|
||||
export class DialogStatisticsFixUnitsChanged extends LitElement {
|
||||
|
@@ -8,7 +8,7 @@ import { HomeAssistant } from "../../../types";
|
||||
import { updateStatisticsMetadata } from "../../../data/history";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-radio";
|
||||
import { DialogStatisticsUnsupportedUnitMetaParams } from "./show-dialog-statistics-fix-unsupported-unit-meta";
|
||||
import type { DialogStatisticsUnsupportedUnitMetaParams } from "./show-dialog-statistics-fix-unsupported-unit-meta";
|
||||
|
||||
@customElement("dialog-statistics-fix-unsupported-unit-meta")
|
||||
export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
|
||||
|
@@ -0,0 +1,20 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { StatisticsMetaData } from "../../../data/history";
|
||||
|
||||
export const loadAdjustSumDialog = () =>
|
||||
import("./dialog-statistics-adjust-sum");
|
||||
|
||||
export interface DialogStatisticsAdjustSumParams {
|
||||
statistic: StatisticsMetaData;
|
||||
}
|
||||
|
||||
export const showStatisticsAdjustSumDialog = (
|
||||
element: HTMLElement,
|
||||
detailParams: DialogStatisticsAdjustSumParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-statistics-adjust-sum",
|
||||
dialogImport: loadAdjustSumDialog,
|
||||
dialogParams: detailParams,
|
||||
});
|
||||
};
|
@@ -116,7 +116,7 @@ export class HuiCardOptions extends LitElement {
|
||||
outline: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
:host:not(.panel) ::slotted(*) {
|
||||
:host(:not(.panel)) ::slotted(*) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,10 @@
|
||||
import { css } from "lit";
|
||||
|
||||
export const configElementStyle = css`
|
||||
.card-config {
|
||||
/* Cancels overlapping Margins for HAForm + Card Config options */
|
||||
overflow: auto;
|
||||
}
|
||||
ha-switch {
|
||||
padding: 16px 6px;
|
||||
}
|
||||
@@ -19,11 +23,11 @@ export const configElementStyle = css`
|
||||
.suffix {
|
||||
margin: 0 8px;
|
||||
}
|
||||
hui-theme-select-editor,
|
||||
hui-action-editor,
|
||||
ha-select,
|
||||
ha-textfield,
|
||||
ha-icon-picker {
|
||||
margin-top: 8px;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
@@ -99,6 +99,14 @@ export class HuiAlarmPanelCardEditor
|
||||
return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.name`);
|
||||
}
|
||||
|
||||
if (schema.name === "theme") {
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
}
|
||||
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.alarm-panel.${
|
||||
schema.name === "states" ? "available_states" : schema.name
|
||||
|
@@ -69,6 +69,12 @@ export class HuiAreaCardEditor
|
||||
|
||||
private _computeLabelCallback = (schema: HaFormSchema) => {
|
||||
switch (schema.name) {
|
||||
case "theme":
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
case "area":
|
||||
return this.hass!.localize("ui.panel.lovelace.editor.card.area.name");
|
||||
case "navigation_path":
|
||||
|
@@ -1,22 +1,22 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { assert, boolean, object, optional, string, assign } from "superstruct";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { assert, assign, boolean, object, optional, string } from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { domainIcon } from "../../../../common/entity/domain_icon";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../../../components/ha-form/types";
|
||||
import { ActionConfig } from "../../../../data/lovelace";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { ButtonCardConfig } from "../../cards/types";
|
||||
import "../../components/hui-action-editor";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import { actionConfigStruct } from "../structs/action-struct";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import type { EditorTarget } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { domainIcon } from "../../../../common/entity/domain_icon";
|
||||
import type { HaFormSchema } from "../../../../components/ha-form/types";
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
@@ -149,38 +149,36 @@ export class HuiButtonCardEditor
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
<div class="card-config">
|
||||
<div class="side-by-side">
|
||||
<hui-action-editor
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.tap_action"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})"
|
||||
.hass=${this.hass}
|
||||
.config=${this._tap_action}
|
||||
.actions=${actions}
|
||||
.configValue=${"tap_action"}
|
||||
.tooltipText=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.button.default_action_help"
|
||||
)}
|
||||
@value-changed=${this._actionChanged}
|
||||
></hui-action-editor>
|
||||
<hui-action-editor
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.hold_action"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})"
|
||||
.hass=${this.hass}
|
||||
.config=${this._hold_action}
|
||||
.actions=${actions}
|
||||
.configValue=${"hold_action"}
|
||||
.tooltipText=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.button.default_action_help"
|
||||
)}
|
||||
@value-changed=${this._actionChanged}
|
||||
></hui-action-editor>
|
||||
</div>
|
||||
<hui-action-editor
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.tap_action"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})"
|
||||
.hass=${this.hass}
|
||||
.config=${this._tap_action}
|
||||
.actions=${actions}
|
||||
.configValue=${"tap_action"}
|
||||
.tooltipText=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.button.default_action_help"
|
||||
)}
|
||||
@value-changed=${this._actionChanged}
|
||||
></hui-action-editor>
|
||||
<hui-action-editor
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.hold_action"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})"
|
||||
.hass=${this.hass}
|
||||
.config=${this._hold_action}
|
||||
.actions=${actions}
|
||||
.configValue=${"hold_action"}
|
||||
.tooltipText=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.button.default_action_help"
|
||||
)}
|
||||
@value-changed=${this._actionChanged}
|
||||
></hui-action-editor>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -202,6 +200,14 @@ export class HuiButtonCardEditor
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (schema.name === "theme") {
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
}
|
||||
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
|
@@ -121,6 +121,14 @@ export class HuiCalendarCardEditor
|
||||
return this.hass!.localize("ui.panel.lovelace.editor.card.generic.title");
|
||||
}
|
||||
|
||||
if (schema.name === "theme") {
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
}
|
||||
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.calendar.${schema.name}`
|
||||
);
|
||||
|
@@ -31,7 +31,7 @@ import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-switch";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { EntitiesCardConfig } from "../../cards/types";
|
||||
import "../../components/hui-theme-select-editor";
|
||||
import "../../../../components/ha-theme-picker";
|
||||
import { TIMESTAMP_RENDERING_FORMATS } from "../../components/types";
|
||||
import type { LovelaceRowConfig } from "../../entity-rows/types";
|
||||
import { headerFooterConfigStructs } from "../../header-footer/structs";
|
||||
@@ -265,12 +265,17 @@ export class HuiEntitiesCardEditor
|
||||
.configValue=${"title"}
|
||||
@input=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
<hui-theme-select-editor
|
||||
<ha-theme-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._theme}
|
||||
.label=${`${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`}
|
||||
.configValue=${"theme"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></hui-theme-select-editor>
|
||||
></ha-theme-picker>
|
||||
<div class="side-by-side">
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
|
@@ -112,6 +112,14 @@ export class HuiEntityCardEditor
|
||||
);
|
||||
}
|
||||
|
||||
if (schema.name === "theme") {
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
}
|
||||
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
|
@@ -172,6 +172,12 @@ export class HuiGaugeCardEditor
|
||||
return this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.gauge.needle_gauge"
|
||||
);
|
||||
case "theme":
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
}
|
||||
return (
|
||||
this.hass!.localize(
|
||||
|
@@ -117,11 +117,23 @@ export class HuiGlanceCardEditor
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: HaFormSchema) =>
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.glance.${schema.name}`
|
||||
) ||
|
||||
this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`);
|
||||
private _computeLabelCallback = (schema: HaFormSchema) => {
|
||||
if (schema.name === "theme") {
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
}
|
||||
return (
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.glance.${schema.name}`
|
||||
) ||
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -75,6 +75,14 @@ export class HuiHumidifierCardEditor
|
||||
);
|
||||
}
|
||||
|
||||
if (schema.name === "theme") {
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
}
|
||||
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
|
@@ -179,6 +179,14 @@ export class HuiLightCardEditor
|
||||
);
|
||||
}
|
||||
|
||||
if (schema.name === "theme") {
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
}
|
||||
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
|
@@ -96,11 +96,24 @@ export class HuiLogbookCardEditor
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: HaFormSchema) =>
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
) ||
|
||||
this.hass!.localize(`ui.panel.lovelace.editor.card.logbook.${schema.name}`);
|
||||
private _computeLabelCallback = (schema: HaFormSchema) => {
|
||||
if (schema.name === "theme") {
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
}
|
||||
|
||||
return (
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
) ||
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.logbook.${schema.name}`
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -58,13 +58,23 @@ export class HuiMarkdownCardEditor
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: HaFormSchema) =>
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
) ||
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.markdown.${schema.name}`
|
||||
private _computeLabelCallback = (schema: HaFormSchema) => {
|
||||
if (schema.name === "theme") {
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
}
|
||||
return (
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
) ||
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.markdown.${schema.name}`
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -5,7 +5,7 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/entity/ha-entity-picker";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { MediaControlCardConfig } from "../../cards/types";
|
||||
import "../../components/hui-theme-select-editor";
|
||||
import "../../../../components/ha-theme-picker";
|
||||
import { LovelaceCardEditor } from "../../types";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import { EditorTarget, EntitiesEditorEvent } from "../types";
|
||||
@@ -62,12 +62,17 @@ export class HuiMediaControlCardEditor
|
||||
@change=${this._valueChanged}
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>
|
||||
<hui-theme-select-editor
|
||||
<ha-theme-picker
|
||||
.label=${`${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`}
|
||||
.hass=${this.hass}
|
||||
.value=${this._theme}
|
||||
.configValue=${"theme"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></hui-theme-select-editor>
|
||||
></ha-theme-picker>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
@@ -1,17 +1,17 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { assert, object, optional, string, assign } from "superstruct";
|
||||
import { assert, assign, object, optional, string } from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { ActionConfig } from "../../../../data/lovelace";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { PictureCardConfig } from "../../cards/types";
|
||||
import "../../components/hui-action-editor";
|
||||
import "../../components/hui-theme-select-editor";
|
||||
import "../../../../components/ha-theme-picker";
|
||||
import { LovelaceCardEditor } from "../../types";
|
||||
import { actionConfigStruct } from "../structs/action-struct";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import { EditorTarget } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
@@ -72,38 +72,41 @@ export class HuiPictureCardEditor
|
||||
.configValue=${"image"}
|
||||
@input=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
<hui-theme-select-editor
|
||||
<ha-theme-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._theme}
|
||||
.label=${`${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`}
|
||||
.configValue=${"theme"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></hui-theme-select-editor>
|
||||
<div class="side-by-side">
|
||||
<hui-action-editor
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.tap_action"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})"
|
||||
.hass=${this.hass}
|
||||
.config=${this._tap_action}
|
||||
.actions=${actions}
|
||||
.configValue=${"tap_action"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></hui-action-editor>
|
||||
<hui-action-editor
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.hold_action"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})"
|
||||
.hass=${this.hass}
|
||||
.config=${this._hold_action}
|
||||
.actions=${actions}
|
||||
.configValue=${"hold_action"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></hui-action-editor>
|
||||
</div>
|
||||
></ha-theme-picker>
|
||||
<hui-action-editor
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.tap_action"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})"
|
||||
.hass=${this.hass}
|
||||
.config=${this._tap_action}
|
||||
.actions=${actions}
|
||||
.configValue=${"tap_action"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></hui-action-editor>
|
||||
<hui-action-editor
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.hold_action"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})"
|
||||
.hass=${this.hass}
|
||||
.config=${this._hold_action}
|
||||
.actions=${actions}
|
||||
.configValue=${"hold_action"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></hui-action-editor>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
@@ -108,32 +108,30 @@ export class HuiPictureEntityCardEditor
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
<div class="card-config">
|
||||
<div class="side-by-side">
|
||||
<hui-action-editor
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.tap_action"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})"
|
||||
.hass=${this.hass}
|
||||
.config=${this._tap_action}
|
||||
.actions=${actions}
|
||||
.configValue=${"tap_action"}
|
||||
@value-changed=${this._changed}
|
||||
></hui-action-editor>
|
||||
<hui-action-editor
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.hold_action"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})"
|
||||
.hass=${this.hass}
|
||||
.config=${this._hold_action}
|
||||
.actions=${actions}
|
||||
.configValue=${"hold_action"}
|
||||
@value-changed=${this._changed}
|
||||
></hui-action-editor>
|
||||
</div>
|
||||
<hui-action-editor
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.tap_action"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})"
|
||||
.hass=${this.hass}
|
||||
.config=${this._tap_action}
|
||||
.actions=${actions}
|
||||
.configValue=${"tap_action"}
|
||||
@value-changed=${this._changed}
|
||||
></hui-action-editor>
|
||||
<hui-action-editor
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.hold_action"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})"
|
||||
.hass=${this.hass}
|
||||
.config=${this._hold_action}
|
||||
.actions=${actions}
|
||||
.configValue=${"hold_action"}
|
||||
@value-changed=${this._changed}
|
||||
></hui-action-editor>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -172,6 +170,14 @@ export class HuiPictureEntityCardEditor
|
||||
);
|
||||
}
|
||||
|
||||
if (schema.name === "theme") {
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
}
|
||||
|
||||
return (
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user