mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-24 16:49:27 +00:00
Compare commits
54 Commits
20220316.0
...
add-Use-UU
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1f6243145f | ||
![]() |
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 | ||
![]() |
9c1d1cb6f6 | ||
![]() |
470225abde | ||
![]() |
ee230b86c1 | ||
![]() |
f927fc64a9 | ||
![]() |
03677c33f7 | ||
![]() |
bc36a206da | ||
![]() |
af06ab1e2d | ||
![]() |
3e2135a485 | ||
![]() |
2e7f8fb46f | ||
![]() |
102568c4bd | ||
![]() |
4fcdae842e | ||
![]() |
ea19740f5a | ||
![]() |
3e0942b631 | ||
![]() |
0261cea796 | ||
![]() |
5247b2813f | ||
![]() |
8301ae262c | ||
![]() |
86dbf99ebe | ||
![]() |
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
|
||||
|
BIN
gallery/public/images/clearspace.png
Normal file
BIN
gallery/public/images/clearspace.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
BIN
gallery/public/images/logo-variants.png
Normal file
BIN
gallery/public/images/logo-variants.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
BIN
gallery/public/images/logo.png
Normal file
BIN
gallery/public/images/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
BIN
gallery/public/images/using-our-logo.png
Normal file
BIN
gallery/public/images/using-our-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
@@ -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>
|
||||
|
34
gallery/src/pages/brand/logo.markdown
Normal file
34
gallery/src/pages/brand/logo.markdown
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: "Logo"
|
||||
---
|
||||
|
||||

|
||||
|
||||
# Using our logo
|
||||
|
||||
As a community, we are proud of our logo. Follow these guidelines to ensure it always looks its best. Our logo follows Google's material design spec and uses the blue interface color.
|
||||
|
||||
[Download Logo](https://github.com/home-assistant/assets/tree/master/logo)
|
||||
|
||||

|
||||
|
||||
|
||||
## Using the icon
|
||||
|
||||
Our icon is a shorter and most used version of our logo. The icon can exist without the wordmark, the wordmark should never exist without the icon.
|
||||
|
||||

|
||||
|
||||
## Using the right variant
|
||||
|
||||
The pretty blue logo with a background shadow, pictured top left, is our primary logo. It should only be used with black, white, and non-duotone photography.
|
||||
|
||||
When needed you can use our logo without a shadow, as seen as the second variant.
|
||||
|
||||
The outlined logo should only be used on packaging.
|
||||
|
||||
## Exclusion zone
|
||||
|
||||
The logo needs some personal space. It's exclusion zone is equal to a quarter the height of the icon.
|
||||
|
||||

|
@@ -147,6 +147,7 @@ const SCHEMAS: {
|
||||
boolean: { name: "Boolean", selector: { boolean: {} } },
|
||||
time: { name: "Time", selector: { time: {} } },
|
||||
date: { name: "Date", selector: { date: {} } },
|
||||
datetime: { name: "Date Time", selector: { datetime: {} } },
|
||||
action: { name: "Action", selector: { action: {} } },
|
||||
text: {
|
||||
name: "Text",
|
||||
@@ -163,9 +164,24 @@ const SCHEMAS: {
|
||||
},
|
||||
},
|
||||
object: { name: "Object", selector: { object: {} } },
|
||||
select_radio: {
|
||||
name: "Select (Radio)",
|
||||
selector: { select: { options: ["Option 1", "Option 2"] } },
|
||||
},
|
||||
select: {
|
||||
name: "Select",
|
||||
selector: { select: { options: ["Option 1", "Option 2"] } },
|
||||
selector: {
|
||||
select: {
|
||||
options: [
|
||||
"Option 1",
|
||||
"Option 2",
|
||||
"Option 3",
|
||||
"Option 4",
|
||||
"Option 5",
|
||||
"Option 6",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
icon: { name: "Icon", selector: { icon: {} } },
|
||||
media: { name: "Media", selector: { media: {} } },
|
||||
|
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",
|
||||
@@ -108,7 +108,7 @@
|
||||
"fuse.js": "^6.0.0",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hls.js": "^1.1.5",
|
||||
"home-assistant-js-websocket": "^6.1.1",
|
||||
"home-assistant-js-websocket": "^7.0.1",
|
||||
"idb-keyval": "^5.1.3",
|
||||
"intl-messageformat": "^9.9.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = home-assistant-frontend
|
||||
version = 20220316.0
|
||||
version = 20220322.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) =>
|
||||
|
@@ -9,10 +9,10 @@ import {
|
||||
mdiCast,
|
||||
mdiCastConnected,
|
||||
mdiClock,
|
||||
mdiFlash,
|
||||
mdiGestureTapButton,
|
||||
mdiLanConnect,
|
||||
mdiLanDisconnect,
|
||||
mdiLightSwitch,
|
||||
mdiLock,
|
||||
mdiLockAlert,
|
||||
mdiLockClock,
|
||||
@@ -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.
|
||||
*
|
||||
@@ -108,7 +111,7 @@ export const domainIcon = (
|
||||
case "switch":
|
||||
return compareState === "on" ? mdiToggleSwitch : mdiToggleSwitchOff;
|
||||
default:
|
||||
return mdiFlash;
|
||||
return mdiLightSwitch;
|
||||
}
|
||||
|
||||
case "sensor": {
|
||||
@@ -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";
|
||||
@@ -17,6 +18,7 @@ import "./state-badge";
|
||||
|
||||
interface HassEntityWithCachedName extends HassEntity {
|
||||
friendly_name: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
||||
@@ -77,8 +79,27 @@ 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({ attribute: "item-value-path" }) public itemValuePath =
|
||||
"entity_id";
|
||||
|
||||
@property({ type: Boolean }) public hideClearIcon = false;
|
||||
|
||||
@state() private _opened = false;
|
||||
@@ -109,7 +130,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[] = [];
|
||||
|
||||
@@ -125,6 +148,7 @@ export class HaEntityPicker extends LitElement {
|
||||
state: "",
|
||||
last_changed: "",
|
||||
last_updated: "",
|
||||
id: "",
|
||||
context: { id: "", user_id: null, parent_id: null },
|
||||
friendly_name: this.hass!.localize(
|
||||
"ui.components.entity.entity-picker.no_entities"
|
||||
@@ -139,6 +163,35 @@ export class HaEntityPicker extends LitElement {
|
||||
];
|
||||
}
|
||||
|
||||
if (includeEntities) {
|
||||
entityIds = entityIds.filter((entityId) =>
|
||||
this.includeEntities!.includes(entityId)
|
||||
);
|
||||
|
||||
return entityIds
|
||||
.map((key) => {
|
||||
const stateObj = hass!.states[key];
|
||||
|
||||
return {
|
||||
...stateObj,
|
||||
friendly_name: computeStateName(stateObj) || key,
|
||||
id: stateObj.context.id,
|
||||
};
|
||||
})
|
||||
.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 +204,22 @@ export class HaEntityPicker extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
states = entityIds.sort().map((key) => ({
|
||||
...hass!.states[key],
|
||||
friendly_name: computeStateName(hass!.states[key]) || key,
|
||||
}));
|
||||
states = entityIds
|
||||
.map((key) => {
|
||||
const stateObj = hass!.states[key];
|
||||
|
||||
return {
|
||||
...stateObj,
|
||||
friendly_name: computeStateName(stateObj) || key,
|
||||
id: stateObj.context?.id,
|
||||
};
|
||||
})
|
||||
.sort((entityA, entityB) =>
|
||||
caseInsensitiveStringCompare(
|
||||
entityA.friendly_name,
|
||||
entityB.friendly_name
|
||||
)
|
||||
);
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
states = states.filter(
|
||||
@@ -193,6 +258,7 @@ export class HaEntityPicker extends LitElement {
|
||||
state: "",
|
||||
last_changed: "",
|
||||
last_updated: "",
|
||||
id: "",
|
||||
context: { id: "", user_id: null, parent_id: null },
|
||||
friendly_name: this.hass!.localize(
|
||||
"ui.components.entity.entity-picker.no_match"
|
||||
@@ -231,7 +297,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;
|
||||
@@ -243,8 +311,8 @@ export class HaEntityPicker extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-combo-box
|
||||
item-value-path="entity_id"
|
||||
item-label-path="friendly_name"
|
||||
.itemValuePath=${this.itemValuePath}
|
||||
.hass=${this.hass}
|
||||
.value=${this._value}
|
||||
.label=${this.label === undefined
|
||||
|
@@ -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,25 @@ export const computeInitialHaFormData = (
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
};
|
||||
} else if ("selector" in field) {
|
||||
const selector: Selector = field.selector;
|
||||
if ("boolean" in selector) {
|
||||
data[field.name] = false;
|
||||
} else if ("text" in selector) {
|
||||
data[field.name] = "";
|
||||
} else if ("number" in selector) {
|
||||
data[field.name] = "min" in selector.number ? 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
return data;
|
||||
|
@@ -1,16 +1,20 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { 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-radio";
|
||||
import type { HaRadio } from "../ha-radio";
|
||||
import "../ha-select";
|
||||
import type { HaSelect } from "../ha-select";
|
||||
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./types";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type {
|
||||
HaFormElement,
|
||||
HaFormSelectData,
|
||||
HaFormSelectSchema,
|
||||
} from "./types";
|
||||
import type { SelectSelector } from "../../data/selector";
|
||||
import "../ha-selector/ha-selector-select";
|
||||
|
||||
@customElement("ha-form-select")
|
||||
export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public schema!: HaFormSelectSchema;
|
||||
|
||||
@property() public data!: HaFormSelectData;
|
||||
@@ -19,60 +23,35 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("ha-select", true) private _input?: HTMLElement;
|
||||
|
||||
public focus() {
|
||||
if (this._input) {
|
||||
this._input.focus();
|
||||
}
|
||||
}
|
||||
private _selectSchema = memoizeOne(
|
||||
(options): SelectSelector => ({
|
||||
select: {
|
||||
options: options.map((option) => ({
|
||||
value: option[0],
|
||||
label: option[1],
|
||||
})),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this.schema.required && this.schema.options!.length < 6) {
|
||||
return html`
|
||||
<div>
|
||||
${this.label}
|
||||
${this.schema.options.map(
|
||||
([value, label]) => html`
|
||||
<mwc-formfield .label=${label}>
|
||||
<ha-radio
|
||||
.checked=${value === this.data}
|
||||
.value=${value}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._valueChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.label=${this.label}
|
||||
<ha-selector-select
|
||||
.hass=${this.hass}
|
||||
.schema=${this.schema}
|
||||
.value=${this.data}
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
@closed=${stopPropagation}
|
||||
@selected=${this._valueChanged}
|
||||
>
|
||||
${!this.schema.required
|
||||
? html`<mwc-list-item value=""></mwc-list-item>`
|
||||
: ""}
|
||||
${this.schema.options!.map(
|
||||
([value, label]) => html`
|
||||
<mwc-list-item .value=${value}>${label}</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
.required=${this.schema.required}
|
||||
.selector=${this._selectSchema(this.schema.options)}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-selector-select>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
let value: string | undefined = (ev.target as HaSelect | HaRadio).value;
|
||||
let value: string | undefined = ev.detail.value;
|
||||
|
||||
if (value === this.data) {
|
||||
return;
|
||||
@@ -86,15 +65,6 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-select,
|
||||
mwc-formfield {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -28,7 +28,11 @@ 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,12 +89,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 {
|
||||
|
74
src/components/ha-selector/ha-selector-datetime.ts
Normal file
74
src/components/ha-selector/ha-selector-datetime.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { DateTimeSelector } from "../../data/selector";
|
||||
import type { HaDateInput } from "../ha-date-input";
|
||||
import type { HaTimeInput } from "../ha-time-input";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../ha-date-input";
|
||||
import "../ha-time-input";
|
||||
|
||||
@customElement("ha-selector-datetime")
|
||||
export class HaDateTimeSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: DateTimeSelector;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@query("ha-date-input") private _dateInput!: HaDateInput;
|
||||
|
||||
@query("ha-time-input") private _timeInput!: HaTimeInput;
|
||||
|
||||
protected render() {
|
||||
const values = this.value?.split(" ");
|
||||
|
||||
return html`
|
||||
<ha-date-input
|
||||
.label=${this.label}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${this.disabled}
|
||||
.value=${values?.[0]}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-date-input>
|
||||
<ha-time-input
|
||||
enable-second
|
||||
.value=${values?.[1] || "0:00:00"}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-time-input>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: `${this._dateInput.value} ${this._timeInput.value}`,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
ha-date-input {
|
||||
min-width: 150px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-datetime": HaDateTimeSelector;
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
|
@@ -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,8 +29,11 @@ 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}
|
||||
.itemValuePath=${!this.selector.entity.use_uuid ? "entity_id" : "id"}
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>`;
|
||||
}
|
||||
@@ -41,6 +44,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>
|
||||
`;
|
||||
}
|
||||
|
@@ -46,7 +46,7 @@ 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}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
|
@@ -1,17 +1,19 @@
|
||||
import "@material/mwc-formfield/mwc-formfield";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { SelectOption, SelectSelector } from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import type { SelectOption, SelectSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-select";
|
||||
import "../ha-radio";
|
||||
|
||||
@customElement("ha-selector-select")
|
||||
export class HaSelectSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: SelectSelector;
|
||||
@property({ attribute: false }) public selector!: SelectSelector;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@@ -21,24 +23,51 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`<ha-select
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.label=${this.label}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
@closed=${stopPropagation}
|
||||
@selected=${this._valueChanged}
|
||||
>
|
||||
${this.selector.select.options.map((item: string | SelectOption) => {
|
||||
const value = typeof item === "object" ? item.value : item;
|
||||
const label = typeof item === "object" ? item.label : item;
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
return html`<mwc-list-item .value=${value}>${label}</mwc-list-item>`;
|
||||
})}
|
||||
</ha-select>`;
|
||||
protected render() {
|
||||
if (this.required && this.selector.select.options!.length < 6) {
|
||||
return html`
|
||||
<div>
|
||||
${this.label}
|
||||
${this.selector.select.options.map((item: string | SelectOption) => {
|
||||
const value = typeof item === "object" ? item.value : item;
|
||||
const label = typeof item === "object" ? item.label : item;
|
||||
|
||||
return html`
|
||||
<mwc-formfield .label=${label}>
|
||||
<ha-radio
|
||||
.checked=${value === this.value}
|
||||
.value=${value}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._valueChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.label=${this.label}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
@closed=${stopPropagation}
|
||||
@selected=${this._valueChanged}
|
||||
>
|
||||
${this.selector.select.options.map((item: string | SelectOption) => {
|
||||
const value = typeof item === "object" ? item.value : item;
|
||||
const label = typeof item === "object" ? item.label : item;
|
||||
|
||||
return html`<mwc-list-item .value=${value}>${label}</mwc-list-item>`;
|
||||
})}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev) {
|
||||
@@ -56,6 +85,9 @@ export class HaSelectSelector extends LitElement {
|
||||
ha-select {
|
||||
width: 100%;
|
||||
}
|
||||
mwc-formfield {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -10,6 +10,7 @@ import "./ha-selector-attribute";
|
||||
import "./ha-selector-boolean";
|
||||
import "./ha-selector-color-rgb";
|
||||
import "./ha-selector-date";
|
||||
import "./ha-selector-datetime";
|
||||
import "./ha-selector-device";
|
||||
import "./ha-selector-duration";
|
||||
import "./ha-selector-entity";
|
||||
|
@@ -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,67 +1,30 @@
|
||||
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;
|
||||
multiple?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AttributeSelector {
|
||||
attribute: {
|
||||
entity_id?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ColorRGBSelector {
|
||||
export interface ActionSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
color_rgb: {};
|
||||
}
|
||||
|
||||
export interface DateSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
date: {};
|
||||
}
|
||||
|
||||
export interface DeviceSelector {
|
||||
device: {
|
||||
integration?: string;
|
||||
manufacturer?: string;
|
||||
model?: string;
|
||||
entity?: {
|
||||
domain?: EntitySelector["entity"]["domain"];
|
||||
device_class?: EntitySelector["entity"]["device_class"];
|
||||
};
|
||||
multiple?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DurationSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
duration: {};
|
||||
action: {};
|
||||
}
|
||||
|
||||
export interface AddonSelector {
|
||||
@@ -86,18 +49,101 @@ export interface AreaSelector {
|
||||
};
|
||||
}
|
||||
|
||||
export interface TargetSelector {
|
||||
target: {
|
||||
export interface AttributeSelector {
|
||||
attribute: {
|
||||
entity_id?: string;
|
||||
};
|
||||
}
|
||||
|
||||
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: {};
|
||||
}
|
||||
|
||||
export interface DateTimeSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
datetime: {};
|
||||
}
|
||||
|
||||
export interface DeviceSelector {
|
||||
device: {
|
||||
integration?: string;
|
||||
manufacturer?: string;
|
||||
model?: string;
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DurationSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
duration: {};
|
||||
}
|
||||
|
||||
export interface EntitySelector {
|
||||
entity: {
|
||||
integration?: string;
|
||||
domain?: string | string[];
|
||||
device_class?: string;
|
||||
multiple?: boolean;
|
||||
use_uuid?: 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
|
||||
media: {};
|
||||
}
|
||||
|
||||
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 }[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -111,28 +157,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;
|
||||
@@ -154,58 +194,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,
|
||||
@@ -33,6 +33,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 +237,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 +494,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),
|
||||
]);
|
||||
|
||||
@@ -189,6 +189,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}`,
|
||||
|
@@ -83,6 +83,11 @@ export interface FlowConfig {
|
||||
|
||||
renderMenuHeader(hass: HomeAssistant, step: DataEntryFlowStepMenu): string;
|
||||
|
||||
renderMenuDescription(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepMenu
|
||||
): TemplateResult | "";
|
||||
|
||||
renderMenuOption(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepMenu,
|
||||
|
@@ -142,6 +142,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}`,
|
||||
|
@@ -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(
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -33,6 +33,7 @@ import {
|
||||
fetchDiagnosticHandler,
|
||||
getDeviceDiagnosticsDownloadUrl,
|
||||
getConfigEntryDiagnosticsDownloadUrl,
|
||||
DiagnosticInfo,
|
||||
} from "../../../data/diagnostics";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
@@ -219,22 +220,32 @@ export class HaConfigDevicePage extends LitElement {
|
||||
}
|
||||
|
||||
let links = await Promise.all(
|
||||
this._integrations(device, this.entries).map(async (entry) => {
|
||||
if (entry.state !== "loaded") {
|
||||
return false;
|
||||
}
|
||||
const info = await fetchDiagnosticHandler(this.hass, entry.domain);
|
||||
this._integrations(device, this.entries).map(
|
||||
async (entry): Promise<boolean | { link: string; domain: string }> => {
|
||||
if (entry.state !== "loaded") {
|
||||
return false;
|
||||
}
|
||||
let info: DiagnosticInfo;
|
||||
try {
|
||||
info = await fetchDiagnosticHandler(this.hass, entry.domain);
|
||||
} catch (err: any) {
|
||||
if (err.code === "not_found") {
|
||||
return false;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!info.handlers.device && !info.handlers.config_entry) {
|
||||
return false;
|
||||
if (!info.handlers.device && !info.handlers.config_entry) {
|
||||
return false;
|
||||
}
|
||||
return {
|
||||
link: info.handlers.device
|
||||
? getDeviceDiagnosticsDownloadUrl(entry.entry_id, this.deviceId)
|
||||
: getConfigEntryDiagnosticsDownloadUrl(entry.entry_id),
|
||||
domain: entry.domain,
|
||||
};
|
||||
}
|
||||
return {
|
||||
link: info.handlers.device
|
||||
? getDeviceDiagnosticsDownloadUrl(entry.entry_id, this.deviceId)
|
||||
: getConfigEntryDiagnosticsDownloadUrl(entry.entry_id),
|
||||
domain: entry.domain,
|
||||
};
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
links = links.filter(Boolean);
|
||||
|
@@ -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>
|
||||
|
@@ -42,6 +42,12 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
|
||||
import {
|
||||
ConfigEntry,
|
||||
deleteConfigEntry,
|
||||
getConfigEntries,
|
||||
} from "../../../data/config_entries";
|
||||
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
|
||||
|
||||
const OVERRIDE_DEVICE_CLASSES = {
|
||||
cover: [
|
||||
@@ -83,6 +89,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _device?: DeviceRegistryEntry;
|
||||
|
||||
@state() private _helperConfigEntry?: ConfigEntry;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _submitting?: boolean;
|
||||
@@ -103,6 +111,20 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
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 updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("entry")) {
|
||||
@@ -215,6 +237,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"
|
||||
@@ -341,7 +378,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 +508,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,
|
||||
|
@@ -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,
|
||||
});
|
||||
};
|
@@ -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;
|
||||
}
|
||||
@@ -25,5 +29,6 @@ export const configElementStyle = css`
|
||||
ha-textfield,
|
||||
ha-icon-picker {
|
||||
margin-top: 8px;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
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";
|
||||
@@ -9,9 +9,9 @@ import "../../components/hui-action-editor";
|
||||
import "../../components/hui-theme-select-editor";
|
||||
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,
|
||||
@@ -78,32 +78,30 @@ export class HuiPictureCardEditor
|
||||
.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>
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import "../../components/hui-action-editor";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { array, assert, assign, object, optional, string } from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../../../components/ha-form/types";
|
||||
import type { ActionConfig } from "../../../../data/lovelace";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { PictureGlanceCardConfig } from "../../cards/types";
|
||||
import "../../components/hui-action-editor";
|
||||
import "../../components/hui-entity-editor";
|
||||
import type { EntityConfig } from "../../entity-rows/types";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
@@ -96,28 +96,26 @@ export class HuiPictureGlanceCardEditor
|
||||
@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"
|
||||
)}
|
||||
.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"
|
||||
)}
|
||||
.hass=${this.hass}
|
||||
.config=${this._hold_action}
|
||||
.actions=${actions}
|
||||
.configValue=${"hold_action"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></hui-action-editor>
|
||||
</div>
|
||||
<hui-action-editor
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.tap_action"
|
||||
)}
|
||||
.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"
|
||||
)}
|
||||
.hass=${this.hass}
|
||||
.config=${this._hold_action}
|
||||
.actions=${actions}
|
||||
.configValue=${"hold_action"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></hui-action-editor>
|
||||
<hui-entity-editor
|
||||
.hass=${this.hass}
|
||||
.entities=${this._configEntities}
|
||||
|
@@ -148,8 +148,8 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
}
|
||||
|
||||
app-toolbar {
|
||||
background-color: var(--dark-background-color, #455a64);
|
||||
color: var(--dark-text-color);
|
||||
background-color: var(--app-header-edit-background-color, #455a64);
|
||||
color: var(--app-header-edit-text-color, #fff);
|
||||
}
|
||||
|
||||
mwc-button[disabled] {
|
||||
|
@@ -173,6 +173,9 @@ export class HaPickThemeRow extends LitElement {
|
||||
}
|
||||
|
||||
private _supportsModeSelection(themeName: string): boolean {
|
||||
if (!(themeName in this.hass.themes.themes)) {
|
||||
return false; // User's theme no longer exists
|
||||
}
|
||||
return "modes" in this.hass.themes.themes[themeName];
|
||||
}
|
||||
|
||||
|
@@ -228,6 +228,11 @@
|
||||
"service": {
|
||||
"run": "Run"
|
||||
},
|
||||
"update": {
|
||||
"installing": "Installing",
|
||||
"installing_with_progress": "Installing ({progress}%)",
|
||||
"up_to_date": "Up-to-date"
|
||||
},
|
||||
"timer": {
|
||||
"actions": {
|
||||
"start": "start",
|
||||
@@ -719,6 +724,14 @@
|
||||
"rising": "Rising",
|
||||
"setting": "Setting"
|
||||
},
|
||||
"update": {
|
||||
"current_version": "Current version",
|
||||
"latest_version": "Latest version",
|
||||
"release_announcement": "Read release announcement",
|
||||
"skip": "Skip",
|
||||
"install": "Install",
|
||||
"create_backup": "Create backup before updating"
|
||||
},
|
||||
"updater": {
|
||||
"title": "Update Instructions"
|
||||
},
|
||||
@@ -810,7 +823,8 @@
|
||||
"area": "Set entity area only",
|
||||
"area_note": "By default the entities of a device are in the same area as the device. If you change the area of this entity, it will no longer follow the area of the device.",
|
||||
"follow_device_area": "Follow device area",
|
||||
"change_device_area": "Change device area"
|
||||
"change_device_area": "Change device area",
|
||||
"configure_state": "Configure State"
|
||||
}
|
||||
},
|
||||
"helper_settings": {
|
||||
@@ -1065,9 +1079,9 @@
|
||||
"learn_more": "Learn more"
|
||||
},
|
||||
"updates": {
|
||||
"check_unavailable": {
|
||||
"no_update_entities": {
|
||||
"title": "Unable to check for updates",
|
||||
"description": "You need to run the Home Assistant operating system to be able to check and install updates from the Home Assistant user interface."
|
||||
"description": "You do not have any integrations that provide updates."
|
||||
},
|
||||
"check_updates": "Check for updates",
|
||||
"no_new_updates": "No new updates found",
|
||||
@@ -2391,7 +2405,7 @@
|
||||
"add_entities_lovelace": "Add to dashboard",
|
||||
"none": "This device has no entities",
|
||||
"show_less": "Show less",
|
||||
"hidden_entities": "+{count} {count, plural,\n one {hidden entity}\n other {hidden entities}\n}"
|
||||
"hidden_entities": "+{count} {count, plural,\n one {entity}\n other {entities}\n} not shown"
|
||||
},
|
||||
"confirm_rename_entity_ids": "Do you also want to rename the entity IDs of your entities?",
|
||||
"confirm_rename_entity_ids_warning": "This will not change any configuration (like automations, scripts, scenes, dashboards) that is currently using these entities! You will have to update them yourself to use the new entity IDs!",
|
||||
@@ -2416,7 +2430,7 @@
|
||||
"filter": {
|
||||
"filter": "Filter",
|
||||
"show_disabled": "Show disabled devices",
|
||||
"hidden_devices": "{number} hidden {number, plural,\n one {device}\n other {devices}\n}",
|
||||
"hidden_devices": "{number} {number, plural,\n one {device}\n other {devices}\n} not shown",
|
||||
"show_all": "Show all"
|
||||
}
|
||||
}
|
||||
@@ -2435,7 +2449,7 @@
|
||||
"show_disabled": "Show disabled entities",
|
||||
"show_unavailable": "Show unavailable entities",
|
||||
"show_readonly": "Show read-only entities",
|
||||
"hidden_entities": "{number} hidden {number, plural,\n one {entity}\n other {entities}\n}",
|
||||
"hidden_entities": "{number} {number, plural,\n one {entity}\n other {entities}\n} not shown",
|
||||
"show_all": "Show all"
|
||||
},
|
||||
"status": {
|
||||
|
30
yarn.lock
30
yarn.lock
@@ -2975,17 +2975,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mdi/js@npm:6.5.95":
|
||||
version: 6.5.95
|
||||
resolution: "@mdi/js@npm:6.5.95"
|
||||
checksum: b1db7713d216c119f584bf973514a2f9d8f2e671e91bf19ce8e56cfa7a9843c0a060328e794507ac31f2bded1032123294f39ff8e987ea5acb2719ab522ef146
|
||||
"@mdi/js@npm:6.6.95":
|
||||
version: 6.6.95
|
||||
resolution: "@mdi/js@npm:6.6.95"
|
||||
checksum: 4cf8c48156f0e9ff67e4394cd428158bd164b1a6b7ca1aa70fc6a6aee91cfede9eba56720eb7d13fa57315ac636e9519a62dedd3cd2a9708aa11f2e3624ddbff
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mdi/svg@npm:6.5.95":
|
||||
version: 6.5.95
|
||||
resolution: "@mdi/svg@npm:6.5.95"
|
||||
checksum: 2d45221d042d52d54c85eaf672a5f3697ed5201607fa38a6e235ee2e60d1c3c25d456a284f19ce47b5f06418cacfee29e8fecf6580b8c28538fd26044becaf1a
|
||||
"@mdi/svg@npm:6.6.95":
|
||||
version: 6.6.95
|
||||
resolution: "@mdi/svg@npm:6.6.95"
|
||||
checksum: 59b79db945847a3d981351418e0e7a457b831e09846fa751d44e80df8fb4cd19ef12bc889538ed2945d2638e522aa7ea5b1f97997e19dd68345f5d7bf5cad5e6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9048,8 +9048,8 @@ fsevents@^1.2.7:
|
||||
"@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
|
||||
"@open-wc/dev-server-hmr": ^0.0.2
|
||||
"@polymer/app-layout": ^3.1.0
|
||||
"@polymer/iron-flex-layout": ^3.0.1
|
||||
@@ -9128,7 +9128,7 @@ fsevents@^1.2.7:
|
||||
gulp-rename: ^2.0.0
|
||||
gulp-zopfli-green: ^3.0.1
|
||||
hls.js: ^1.1.5
|
||||
home-assistant-js-websocket: ^6.1.1
|
||||
home-assistant-js-websocket: ^7.0.1
|
||||
html-minifier: ^4.0.0
|
||||
husky: ^1.3.1
|
||||
idb-keyval: ^5.1.3
|
||||
@@ -9198,10 +9198,10 @@ fsevents@^1.2.7:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"home-assistant-js-websocket@npm:^6.1.1":
|
||||
version: 6.1.1
|
||||
resolution: "home-assistant-js-websocket@npm:6.1.1"
|
||||
checksum: b46dd44ac1b393ecfdda152743fe44dcd6534463d1df631c448a71e71166c04d4fea2a0827dbb2e55c85fcd8d9ccdea4f168713bf18e832c7f688b9b2fdc8d72
|
||||
"home-assistant-js-websocket@npm:^7.0.1":
|
||||
version: 7.0.1
|
||||
resolution: "home-assistant-js-websocket@npm:7.0.1"
|
||||
checksum: c9a87f11222571226adff43f022008d35df1f78799efae43e9a36f768eef10d21aed99886c905086c42c24d85d47c78e328c1be9593c117b397a18ee86b2fe64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
Reference in New Issue
Block a user