Compare commits

...

29 Commits

Author SHA1 Message Date
Zack Barett
c7e9ee785d Merge pull request #12109 from home-assistant/number_selector_allow_0 2022-03-23 08:42:00 -05:00
Erik
079cc39a6e Fix selecting 0 with number selector 2022-03-23 14:24:55 +01:00
Marc Mueller
d6a1d5af79 Remove setup.py (#11593) 2022-03-23 09:22:12 +01:00
Matthias de Baat
c0dce08e19 Create user types page and rename the category (#12089)
Co-authored-by: Zack Barett <zackbarett@hey.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-03-23 03:51:35 +00:00
Zack Barett
a7a347ed05 Bumped version to 20220322.0 (#12102) 2022-03-22 17:08:30 -07:00
Zack Barett
2d9b50defc Fix Duration Selector Default (#12098)
* Fix Duration Default

* USe initial form data function
2022-03-22 23:33:16 +00:00
Paulus Schoutsen
840858b18c Add statistic adjust dialog (#12101)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-03-22 22:40:00 +00:00
Zack Barett
afd2e71f6c change from hidden to not shown (#12097) 2022-03-22 15:39:22 -07:00
Zack Barett
88af0aa788 Add entity include and exclude to selector (#12078)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-03-22 19:58:03 +00:00
Zack Barett
49124f6f09 Update When entity can change enabled or hidden (#12096) 2022-03-22 19:53:22 +00:00
Paulus Schoutsen
73f5580555 Add support for integration type (#12077) 2022-03-22 14:47:12 -05:00
Joakim Sørensen
bdde5268c6 Add support for update entities (#12059)
* Add support for update entities

* Apply suggestions from code review

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Add to gallery

* implement xx%

* Adjustments for skipped

* Add progress bar

* Add UPDATE_SUPPORT_INSTALL

* Allow skipping without install support

* Add version to service call if supported

* Adjust changelog link

* Use Installing

* adjustments

* Use unavailable

Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-03-22 10:23:54 -07:00
Zack Barett
15e972c158 Stack Action Inputs in the Button Editor (#12076)
* Stack Action Inputs in the Button Editor

* update style

* Update for other editors
2022-03-22 08:57:09 -07:00
Zack Barett
0fc4c24f5a Merge pull request #12087 from DuckyCrayfish/fix-toolbar-styles 2022-03-22 10:29:11 -05:00
Zack Barett
9eba50df0c Merge pull request #11651 from home-assistant/add-docs-icon-config-flow 2022-03-22 10:23:43 -05:00
Zack Barett
0e0e07437f Update src/dialogs/config-flow/dialog-data-entry-flow.ts 2022-03-22 10:08:43 -05:00
Joakim Sørensen
6ac51ede52 Change Netlify preview URL (#12095) 2022-03-22 16:00:43 +01:00
Pawel
ccf1fb573a Fix gas energy graph units if stats added by external source (#11892) 2022-03-21 21:15:28 -07:00
Nick Iacullo
fa537968c4 Update styles for hui-editor
Update the background-color and text-color of the app-toolbar in
hui-editor to match the styles of hui-root while in edit-mode.

Previously, these properties were set using undefined css variables that
could not be changed via themes (--dark-background-color and
--dark-text-color).
2022-03-21 10:28:24 +00:00
Marc Mueller
6bf2111a3c Upload release assets (#11566)
Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>
2022-03-21 08:23:05 +01:00
J. Nick Koston
ddf1cc0733 Fetch history with no_attributes for entities that do not need them (#12082) 2022-03-20 15:47:13 -10:00
Zack Barett
9c1d1cb6f6 Merge pull request #12069 from matthiasdebaat/patch-2 2022-03-18 07:32:14 -05:00
Matthias de Baat
470225abde Update logo.markdown 2022-03-18 11:26:19 +01:00
Matthias de Baat
ee230b86c1 Update gallery/src/pages/brand/logo.markdown
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-03-18 11:25:41 +01:00
Matthias de Baat
f927fc64a9 Update gallery/src/pages/brand/logo.markdown
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-03-18 11:25:34 +01:00
Matthias de Baat
3e0942b631 Add files via upload 2022-03-17 14:19:28 +01:00
Matthias de Baat
0261cea796 Create new Logo page 2022-03-17 14:14:13 +01:00
Paulus Schoutsen
35a41b3490 Use same help icon everywhere 2022-02-11 08:35:29 -08:00
Paulus Schoutsen
f59cb661cd Add a docs icon to the config flow dialog 2022-02-10 14:27:38 -08:00
68 changed files with 1733 additions and 560 deletions

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -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

View File

@@ -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",
},
];

View File

@@ -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>

View File

@@ -0,0 +1,34 @@
---
title: "Logo"
---
![Using our logo](/images/using-our-logo.png)
# 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)
![Logo](/images/logo.png)
## 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.
![Logo variants](/images/logo-variants.png)
## 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.
![Clearspace](/images/clearspace.png)

View File

@@ -0,0 +1,3 @@
---
title: Update
---

View 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;
}
}

View 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 arent demographic and dont 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.

View File

@@ -1,6 +1,6 @@
[metadata]
name = home-assistant-frontend
version = 20220317.0
version = 20220322.0
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0

View File

@@ -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()

View File

@@ -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",
];

View File

@@ -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 &&

View File

@@ -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) =>

View File

@@ -26,8 +26,11 @@ import {
mdiCheckCircleOutline,
mdiCloseCircleOutline,
mdiWeatherNight,
mdiPackage,
mdiPackageDown,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { updateIsInstalling, UpdateEntity } from "../../data/update";
/**
* Return the icon to be used for a domain.
*
@@ -133,6 +136,13 @@ export const domainIcon = (
return stateObj?.state === "above_horizon"
? FIXED_DOMAIN_ICONS[domain]
: mdiWeatherNight;
case "update":
return compareState === "on"
? updateIsInstalling(stateObj as UpdateEntity)
? mdiPackageDown
: mdiPackageUp
: mdiPackage;
}
if (domain in FIXED_DOMAIN_ICONS) {

View File

@@ -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}

View File

@@ -7,6 +7,7 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "../ha-combo-box";
@@ -77,6 +78,22 @@ export class HaEntityPicker extends LitElement {
@property({ type: Array, attribute: "include-unit-of-measurement" })
public includeUnitOfMeasurement?: string[];
/**
* List of allowed entities to show. Will ignore all other filters.
* @type {Array}
* @attr include-entities
*/
@property({ type: Array, attribute: "include-entities" })
public includeEntities?: string[];
/**
* List of entities to be excluded.
* @type {Array}
* @attr exclude-entities
*/
@property({ type: Array, attribute: "exclude-entities" })
public excludeEntities?: string[];
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Boolean }) public hideClearIcon = false;
@@ -109,7 +126,9 @@ export class HaEntityPicker extends LitElement {
excludeDomains: this["excludeDomains"],
entityFilter: this["entityFilter"],
includeDeviceClasses: this["includeDeviceClasses"],
includeUnitOfMeasurement: this["includeUnitOfMeasurement"]
includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
includeEntities: this["includeEntities"],
excludeEntities: this["excludeEntities"]
): HassEntityWithCachedName[] => {
let states: HassEntityWithCachedName[] = [];
@@ -139,6 +158,30 @@ export class HaEntityPicker extends LitElement {
];
}
if (includeEntities) {
entityIds = entityIds.filter((entityId) =>
this.includeEntities!.includes(entityId)
);
return entityIds
.map((key) => ({
...hass!.states[key],
friendly_name: computeStateName(hass!.states[key]) || key,
}))
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.friendly_name,
entityB.friendly_name
)
);
}
if (excludeEntities) {
entityIds = entityIds.filter(
(entityId) => !excludeEntities!.includes(entityId)
);
}
if (includeDomains) {
entityIds = entityIds.filter((eid) =>
includeDomains.includes(computeDomain(eid))
@@ -151,10 +194,17 @@ export class HaEntityPicker extends LitElement {
);
}
states = entityIds.sort().map((key) => ({
...hass!.states[key],
friendly_name: computeStateName(hass!.states[key]) || key,
}));
states = entityIds
.map((key) => ({
...hass!.states[key],
friendly_name: computeStateName(hass!.states[key]) || key,
}))
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.friendly_name,
entityB.friendly_name
)
);
if (includeDeviceClasses) {
states = states.filter(
@@ -231,7 +281,9 @@ export class HaEntityPicker extends LitElement {
this.excludeDomains,
this.entityFilter,
this.includeDeviceClasses,
this.includeUnitOfMeasurement
this.includeUnitOfMeasurement,
this.includeEntities,
this.excludeEntities
);
if (this._initedStates) {
(this.comboBox as any).filteredItems = this._states;

View File

@@ -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;

View File

@@ -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 {

View File

@@ -26,6 +26,7 @@ export class HaDateTimeSelector extends LitElement {
protected render() {
const values = this.value?.split(" ");
return html`
<ha-date-input
.label=${this.label}
@@ -37,7 +38,7 @@ export class HaDateTimeSelector extends LitElement {
</ha-date-input>
<ha-time-input
enable-second
.value=${values?.[1] || "00:00:00"}
.value=${values?.[1] || "0:00:00"}
.locale=${this.hass.locale}
.disabled=${this.disabled}
@value-changed=${this._valueChanged}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -6,8 +6,8 @@ import { subscribeEntityRegistry } from "../../data/entity_registry";
import { EntitySelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
import "../entity/ha-entity-picker";
import "../entity/ha-entities-picker";
import "../entity/ha-entity-picker";
@customElement("ha-selector-entity")
export class HaEntitySelector extends SubscribeMixin(LitElement) {
@@ -29,6 +29,8 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.includeEntities=${this.selector.entity.includeEntities}
.excludeEntities=${this.selector.entity.excludeEntities}
.entityFilter=${this._filterEntities}
.disabled=${this.disabled}
allow-custom-entity
@@ -41,6 +43,8 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
.hass=${this.hass}
.value=${this.value}
.entityFilter=${this._filterEntities}
.includeEntities=${this.selector.entity.includeEntities}
.excludeEntities=${this.selector.entity.excludeEntities}
></ha-entities-picker>
`;
}

View File

@@ -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}

View File

@@ -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
);
}

View File

@@ -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];

View File

@@ -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,

View File

@@ -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

View File

@@ -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
View 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,
},
};

View File

@@ -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,
});

View File

@@ -28,6 +28,8 @@ export interface EntitySelector {
domain?: string | string[];
device_class?: string;
multiple?: boolean;
includeEntities?: string[];
excludeEntities?: string[];
};
}

36
src/data/update.ts Normal file
View 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;

View File

@@ -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,33 @@ 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">
${this._step
? html`
<a
href=${documentationUrl(
this.hass,
`/integrations/${this._step.handler}`
)}
target="_blank"
rel="noreferrer noopener"
><ha-icon-button
.label=${this.hass.localize("ui.common.help")}
.path=${mdiHelpCircle}
?rtl=${computeRTL(this.hass)}
></ha-icon-button
></a>
`
: ""}
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.integrations.config_flow.dismiss"
)}
.path=${mdiClose}
dialogAction="close"
?rtl=${computeRTL(this.hass)}
></ha-icon-button>
</div>
${this._step === null
? this._handler
? html`<step-flow-pick-flow
@@ -472,16 +492,19 @@ class DataEntryFlowDialog extends LitElement {
ha-dialog {
--dialog-content-padding: 0;
}
ha-icon-button {
.dialog-actions {
padding: 16px;
position: absolute;
top: 0;
right: 0;
}
ha-icon-button[rtl] {
.dialog-actions[rtl] {
right: auto;
left: 0;
}
.dialog-actions > * {
color: var(--secondary-text-color);
}
`,
];
}

View File

@@ -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),
]);

View File

@@ -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");

View 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;
}
}

View File

@@ -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"),

View File

@@ -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(

View File

@@ -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;
}

View File

@@ -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),

View File

@@ -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() {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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,

View File

@@ -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",

View File

@@ -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;
}

View File

@@ -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();
}
},
});
}
}

View File

@@ -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,
});
};

View File

@@ -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, {

View File

@@ -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
);

View File

@@ -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
);

View File

@@ -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>`,
},
})
);

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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,
});
};

View File

@@ -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;
}
`;

View File

@@ -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>
`;
}

View File

@@ -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>
`;
}

View File

@@ -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>
`;
}

View File

@@ -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}

View File

@@ -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] {

View File

@@ -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": {
@@ -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": {