Compare commits

..

3 Commits

Author SHA1 Message Date
Zack
14de223ffc Update for other editors 2022-03-22 10:21:26 -05:00
Zack
1290336cc5 update style 2022-03-18 13:18:46 -05:00
Zack
abaf5dd0f2 Stack Action Inputs in the Button Editor 2022-03-18 13:15:55 -05:00
66 changed files with 670 additions and 1760 deletions

View File

@@ -10,18 +10,10 @@ env:
NODE_VERSION: 14 NODE_VERSION: 14
NODE_OPTIONS: --max_old_space_size=6144 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: jobs:
release: release:
name: Release name: Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write # Required to upload release assets
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v2 uses: actions/checkout@v2
@@ -55,13 +47,6 @@ jobs:
script/release script/release
- name: Upload release assets
uses: softprops/action-gh-release@v0.1.14
with:
files: |
dist/*.whl
dist/*.tar.gz
wheels-init: wheels-init:
name: Init wheels build name: Init wheels build
needs: release needs: release

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" createStatus "pending" "Building design preview" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID"
gulp build-gallery gulp build-gallery
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
createStatus "success" "Build complete" "$DEPLOY_PRIME_URL" createStatus "success" "Build complete" "$DEPLOY_URL"
else else
createStatus "error" "Build failed" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID" createStatus "error" "Build failed" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID"
fi fi

View File

@@ -42,11 +42,10 @@ module.exports = [
}, },
{ {
category: "user-test", category: "user-test",
header: "Users", header: "User Tests",
pages: ["user-types", "configuration-menu"],
}, },
{ {
category: "design.home-assistant.io", category: "design.home-assistant.io",
header: "About", header: "Design Documentation",
}, },
]; ];

View File

@@ -45,10 +45,6 @@ class HaGallery extends LitElement {
for (const page of group.pages!) { for (const page of group.pages!) {
const key = `${group.category}/${page}`; const key = `${group.category}/${page}`;
const active = this._page === key; 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; const title = PAGES[key].metadata.title || page;
links.push(html` links.push(html`
<a ?active=${active} href=${`#${group.category}/${page}`}>${title}</a> <a ?active=${active} href=${`#${group.category}/${page}`}>${title}</a>

View File

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

View File

@@ -1,140 +0,0 @@
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

@@ -1,17 +0,0 @@
---
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

@@ -45,6 +45,7 @@ import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-loading-screen"; import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage"; import "../../../src/layouts/hass-subpage";
import "../../../src/layouts/hass-tabs-subpage"; import "../../../src/layouts/hass-tabs-subpage";
import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates";
import { HomeAssistant, Route } from "../../../src/types"; import { HomeAssistant, Route } from "../../../src/types";
import { addonArchIsSupported, extractChangelog } from "../util/addon"; import { addonArchIsSupported, extractChangelog } from "../util/addon";
@@ -54,12 +55,6 @@ declare global {
} }
} }
const SUPERVISOR_UPDATE_NAMES = {
core: "Home Assistant Core",
os: "Home Assistant Operating System",
supervisor: "Home Assistant Supervisor",
};
type updateType = "os" | "supervisor" | "core" | "addon"; type updateType = "os" | "supervisor" | "core" | "addon";
const changelogUrl = ( const changelogUrl = (

View File

@@ -72,8 +72,8 @@
"@material/mwc-textfield": "0.25.3", "@material/mwc-textfield": "0.25.3",
"@material/mwc-top-app-bar-fixed": "^0.25.3", "@material/mwc-top-app-bar-fixed": "^0.25.3",
"@material/top-app-bar": "14.0.0-canary.261f2db59.0", "@material/top-app-bar": "14.0.0-canary.261f2db59.0",
"@mdi/js": "6.6.95", "@mdi/js": "6.5.95",
"@mdi/svg": "6.6.95", "@mdi/svg": "6.5.95",
"@polymer/app-layout": "^3.1.0", "@polymer/app-layout": "^3.1.0",
"@polymer/iron-flex-layout": "^3.0.1", "@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1", "@polymer/iron-icon": "^3.0.1",

View File

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

7
setup.py Normal file
View File

@@ -0,0 +1,7 @@
"""
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,7 +187,6 @@ export const DOMAINS_WITH_MORE_INFO = [
"scene", "scene",
"sun", "sun",
"timer", "timer",
"update",
"vacuum", "vacuum",
"water_heater", "water_heater",
"weather", "weather",
@@ -201,7 +200,6 @@ export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [
"input_text", "input_text",
"number", "number",
"scene", "scene",
"update",
"select", "select",
]; ];

View File

@@ -1,18 +1,12 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { FrontendLocaleData } from "../../data/translation"; import { FrontendLocaleData } from "../../data/translation";
import {
updateIsInstalling,
UpdateEntity,
UPDATE_SUPPORT_PROGRESS,
} from "../../data/update";
import { formatDate } from "../datetime/format_date"; import { formatDate } from "../datetime/format_date";
import { formatDateTime } from "../datetime/format_date_time"; import { formatDateTime } from "../datetime/format_date_time";
import { formatTime } from "../datetime/format_time"; import { formatTime } from "../datetime/format_time";
import { formatNumber, isNumericState } from "../number/format_number"; import { formatNumber, isNumericState } from "../number/format_number";
import { LocalizeFunc } from "../translations/localize"; import { LocalizeFunc } from "../translations/localize";
import { computeStateDomain } from "./compute_state_domain"; import { computeStateDomain } from "./compute_state_domain";
import { supportsFeature } from "./supports-feature";
export const computeStateDisplay = ( export const computeStateDisplay = (
localize: LocalizeFunc, localize: LocalizeFunc,
@@ -136,28 +130,6 @@ 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 (
// Return device class translation // Return device class translation
(stateObj.attributes.device_class && (stateObj.attributes.device_class &&

View File

@@ -1,4 +1,4 @@
import type { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { computeDomain } from "./compute_domain"; import { computeDomain } from "./compute_domain";
export const computeStateDomain = (stateObj: HassEntity) => export const computeStateDomain = (stateObj: HassEntity) =>

View File

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

View File

@@ -46,22 +46,6 @@ class HaEntitiesPickerLight extends LitElement {
@property({ type: Array, attribute: "include-unit-of-measurement" }) @property({ type: Array, attribute: "include-unit-of-measurement" })
public includeUnitOfMeasurement?: string[]; 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" }) @property({ attribute: "picked-entity-label" })
public pickedEntityLabel?: string; public pickedEntityLabel?: string;
@@ -85,8 +69,6 @@ class HaEntitiesPickerLight extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.includeDomains=${this.includeDomains} .includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains} .excludeDomains=${this.excludeDomains}
.includeEntities=${this.includeEntities}
.excludeEntities=${this.excludeEntities}
.includeDeviceClasses=${this.includeDeviceClasses} .includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement} .includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this._entityFilter} .entityFilter=${this._entityFilter}
@@ -102,8 +84,6 @@ class HaEntitiesPickerLight extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.includeDomains=${this.includeDomains} .includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains} .excludeDomains=${this.excludeDomains}
.includeEntities=${this.includeEntities}
.excludeEntities=${this.excludeEntities}
.includeDeviceClasses=${this.includeDeviceClasses} .includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement} .includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this._entityFilter} .entityFilter=${this._entityFilter}

View File

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

View File

@@ -1,5 +1,4 @@
import type { Selector } from "../../data/selector"; import { HaFormSchema } from "./types";
import type { HaFormSchema } from "./types";
export const computeInitialHaFormData = ( export const computeInitialHaFormData = (
schema: HaFormSchema[] schema: HaFormSchema[]
@@ -32,25 +31,6 @@ export const computeInitialHaFormData = (
minutes: 0, minutes: 0,
seconds: 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; return data;

View File

@@ -28,11 +28,7 @@ export class HaAreaSelector extends LitElement {
oldSelector !== this.selector && oldSelector !== this.selector &&
this.selector.area.device?.integration this.selector.area.device?.integration
) { ) {
getConfigEntries(this.hass, { this._loadConfigEntries();
domain: this.selector.area.device.integration,
}).then((entries) => {
this._configEntries = entries;
});
} }
} }
} }
@@ -89,6 +85,12 @@ export class HaAreaSelector extends LitElement {
} }
return true; return true;
}; };
private async _loadConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter(
(entry) => entry.domain === this.selector.area.device?.integration
);
}
} }
declare global { declare global {

View File

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

View File

@@ -25,11 +25,7 @@ export class HaDeviceSelector extends LitElement {
if (changedProperties.has("selector")) { if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector"); const oldSelector = changedProperties.get("selector");
if (oldSelector !== this.selector && this.selector.device?.integration) { if (oldSelector !== this.selector && this.selector.device?.integration) {
getConfigEntries(this.hass, { this._loadConfigEntries();
domain: this.selector.device.integration,
}).then((entries) => {
this._configEntries = entries;
});
} }
} }
} }
@@ -92,6 +88,12 @@ export class HaDeviceSelector extends LitElement {
} }
return true; return true;
}; };
private async _loadConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter(
(entry) => entry.domain === this.selector.device.integration
);
}
} }
declare global { declare global {

View File

@@ -1,8 +1,8 @@
import "../ha-duration-input";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import type { DurationSelector } from "../../data/selector"; import { DurationSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-duration-input";
@customElement("ha-selector-duration") @customElement("ha-selector-duration")
export class HaTimeDuration extends LitElement { export class HaTimeDuration extends LitElement {

View File

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

View File

@@ -46,7 +46,7 @@ export class HaNumberSelector extends LitElement {
class=${classMap({ single: this.selector.number.mode === "box" })} class=${classMap({ single: this.selector.number.mode === "box" })}
.min=${this.selector.number.min} .min=${this.selector.number.min}
.max=${this.selector.number.max} .max=${this.selector.number.max}
.value=${this.value ?? ""} .value=${this.value || ""}
.step=${this.selector.number.step ?? 1} .step=${this.selector.number.step ?? 1}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required} .required=${this.required}

View File

@@ -134,8 +134,9 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
private async _loadConfigEntries() { private async _loadConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter( this._configEntries = (await getConfigEntries(this.hass)).filter(
(entry) => (entry) =>
entry.domain === this.selector.target.device?.integration || entry.domain ===
entry.domain === this.selector.target.entity?.integration (this.selector.target.device?.integration ||
this.selector.target.entity?.integration)
); );
} }

View File

@@ -7,7 +7,6 @@ import {
HistoryResult, HistoryResult,
LineChartUnit, LineChartUnit,
TimelineEntity, TimelineEntity,
entityIdHistoryNeedsAttributes,
} from "./history"; } from "./history";
export interface CacheConfig { export interface CacheConfig {
@@ -54,17 +53,7 @@ export const getRecent = (
return cache.data; return cache.data;
} }
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId); const prom = fetchRecent(hass, entityId, startTime, endTime).then(
const prom = fetchRecent(
hass,
entityId,
startTime,
endTime,
false,
undefined,
true,
noAttributes
).then(
(stateHistory) => computeHistory(hass, stateHistory, localize), (stateHistory) => computeHistory(hass, stateHistory, localize),
(err) => { (err) => {
delete RECENT_CACHE[entityId]; delete RECENT_CACHE[entityId];
@@ -131,7 +120,6 @@ export const getRecentWithCache = (
} }
const curCacheProm = cache.prom; const curCacheProm = cache.prom;
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
const genProm = async () => { const genProm = async () => {
let fetchedHistory: HassEntity[][]; let fetchedHistory: HassEntity[][];
@@ -144,10 +132,7 @@ export const getRecentWithCache = (
entityId, entityId,
toFetchStartTime, toFetchStartTime,
endTime, endTime,
appendingToCache, appendingToCache
undefined,
true,
noAttributes
), ),
]); ]);
fetchedHistory = results[1]; fetchedHistory = results[1];

View File

@@ -34,24 +34,8 @@ export const ERROR_STATES: ConfigEntry["state"][] = [
"setup_retry", "setup_retry",
]; ];
export const getConfigEntries = ( export const getConfigEntries = (hass: HomeAssistant) =>
hass: HomeAssistant, hass.callApi<ConfigEntry[]>("GET", "config/config_entries/entry");
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 = ( export const updateConfigEntry = (
hass: HomeAssistant, hass: HomeAssistant,

View File

@@ -65,14 +65,8 @@ export const ignoreConfigFlow = (
export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) => export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) =>
hass.callApi("DELETE", `config/config_entries/flow/${flowId}`); hass.callApi("DELETE", `config/config_entries/flow/${flowId}`);
export const getConfigFlowHandlers = ( export const getConfigFlowHandlers = (hass: HomeAssistant) =>
hass: HomeAssistant, hass.callApi<string[]>("GET", "config/config_entries/flow_handlers");
type?: "helper" | "integration"
) =>
hass.callApi<string[]>(
"GET",
`config/config_entries/flow_handlers${type ? `?type=${type}` : ""}`
);
export const fetchConfigFlowInProgress = ( export const fetchConfigFlowInProgress = (
conn: Connection conn: Connection

View File

@@ -12,12 +12,7 @@ import { subscribeOne } from "../common/util/subscribe-one";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { ConfigEntry, getConfigEntries } from "./config_entries"; import { ConfigEntry, getConfigEntries } from "./config_entries";
import { subscribeEntityRegistry } from "./entity_registry"; import { subscribeEntityRegistry } from "./entity_registry";
import { import { fetchStatistics, Statistics } from "./history";
fetchStatistics,
Statistics,
StatisticsMetaData,
getStatisticMetadata,
} from "./history";
const energyCollectionKeys: (string | undefined)[] = []; const energyCollectionKeys: (string | undefined)[] = [];
@@ -141,7 +136,6 @@ export interface GasSourceTypeEnergyPreference {
entity_energy_from: string | null; entity_energy_from: string | null;
entity_energy_price: string | null; entity_energy_price: string | null;
number_energy_price: number | null; number_energy_price: number | null;
unit_of_measurement?: string | null;
} }
type EnergySource = type EnergySource =
@@ -247,14 +241,14 @@ const getEnergyData = async (
end?: Date end?: Date
): Promise<EnergyData> => { ): Promise<EnergyData> => {
const [configEntries, entityRegistryEntries, info] = await Promise.all([ const [configEntries, entityRegistryEntries, info] = await Promise.all([
getConfigEntries(hass, { domain: "co2signal" }), getConfigEntries(hass),
subscribeOne(hass.connection, subscribeEntityRegistry), subscribeOne(hass.connection, subscribeEntityRegistry),
getEnergyInfo(hass), getEnergyInfo(hass),
]); ]);
const co2SignalConfigEntry = configEntries.length const co2SignalConfigEntry = configEntries.find(
? configEntries[0] (entry) => entry.domain === "co2signal"
: undefined; );
let co2SignalEntity: string | undefined; let co2SignalEntity: string | undefined;
@@ -277,15 +271,6 @@ const getEnergyData = async (
const consumptionStatIDs: string[] = []; const consumptionStatIDs: string[] = [];
const statIDs: 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) { for (const source of prefs.energy_sources) {
if (source.type === "solar") { if (source.type === "solar") {
@@ -295,20 +280,6 @@ const getEnergyData = async (
if (source.type === "gas") { if (source.type === "gas") {
statIDs.push(source.stat_energy_from); 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) { if (source.stat_cost) {
statIDs.push(source.stat_cost); statIDs.push(source.stat_cost);
} }
@@ -588,9 +559,6 @@ export const getEnergyGasUnit = (
? "kWh" ? "kWh"
: entity.attributes.unit_of_measurement; : entity.attributes.unit_of_measurement;
} }
if (source.unit_of_measurement) {
return source.unit_of_measurement;
}
} }
return undefined; return undefined;
}; };

View File

@@ -1,71 +0,0 @@
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,5 +1,4 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDisplay } from "../common/entity/compute_state_display"; import { computeStateDisplay } from "../common/entity/compute_state_display";
import { computeStateDomain } from "../common/entity/compute_state_domain"; import { computeStateDomain } from "../common/entity/compute_state_domain";
import { computeStateName } from "../common/entity/compute_state_name"; import { computeStateName } from "../common/entity/compute_state_name";
@@ -8,13 +7,6 @@ import { HomeAssistant } from "../types";
import { FrontendLocaleData } from "./translation"; import { FrontendLocaleData } from "./translation";
const DOMAINS_USE_LAST_UPDATED = ["climate", "humidifier", "water_heater"]; 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 = [ const LINE_ATTRIBUTES_TO_KEEP = [
"temperature", "temperature",
"current_temperature", "current_temperature",
@@ -84,8 +76,6 @@ export interface StatisticsMetaData {
statistic_id: string; statistic_id: string;
source: string; source: string;
name?: string | null; name?: string | null;
has_sum: boolean;
has_mean: boolean;
} }
export type StatisticsValidationResult = export type StatisticsValidationResult =
@@ -141,13 +131,6 @@ export interface StatisticsValidationResults {
[statisticId: string]: StatisticsValidationResult[]; [statisticId: string]: StatisticsValidationResult[];
} }
export const entityIdHistoryNeedsAttributes = (
hass: HomeAssistant,
entityId: string
) =>
!hass.states[entityId] ||
NEED_ATTRIBUTE_DOMAINS.includes(computeDomain(entityId));
export const fetchRecent = ( export const fetchRecent = (
hass: HomeAssistant, hass: HomeAssistant,
entityId: string, entityId: string,
@@ -155,8 +138,7 @@ export const fetchRecent = (
endTime: Date, endTime: Date,
skipInitialState = false, skipInitialState = false,
significantChangesOnly?: boolean, significantChangesOnly?: boolean,
minimalResponse = true, minimalResponse = true
noAttributes?: boolean
): Promise<HassEntity[][]> => { ): Promise<HassEntity[][]> => {
let url = "history/period"; let url = "history/period";
if (startTime) { if (startTime) {
@@ -175,9 +157,7 @@ export const fetchRecent = (
if (minimalResponse) { if (minimalResponse) {
url += "&minimal_response"; url += "&minimal_response";
} }
if (noAttributes) {
url += "&no_attributes";
}
return hass.callApi("GET", url); return hass.callApi("GET", url);
}; };
@@ -191,10 +171,6 @@ export const fetchDate = (
"GET", "GET",
`history/period/${startTime.toISOString()}?end_time=${endTime.toISOString()}&minimal_response${ `history/period/${startTime.toISOString()}?end_time=${endTime.toISOString()}&minimal_response${
entityId ? `&filter_entity_id=${entityId}` : `` entityId ? `&filter_entity_id=${entityId}` : ``
}${
entityId && !entityIdHistoryNeedsAttributes(hass, entityId)
? `&no_attributes`
: ``
}` }`
); );
@@ -302,10 +278,6 @@ const processLineChartEntities = (
}; };
}; };
const stateUsesUnits = (state: HassEntity) =>
"unit_of_measurement" in state.attributes ||
"state_class" in state.attributes;
export const computeHistory = ( export const computeHistory = (
hass: HomeAssistant, hass: HomeAssistant,
stateHistory: HassEntity[][], stateHistory: HassEntity[][],
@@ -322,18 +294,16 @@ export const computeHistory = (
return; return;
} }
const entityId = stateInfo[0].entity_id; const stateWithUnitorStateClass = stateInfo.find(
const currentState = (state) =>
entityId in hass.states ? hass.states[entityId] : undefined; state.attributes &&
const stateWithUnitorStateClass = ("unit_of_measurement" in state.attributes ||
!currentState && "state_class" in state.attributes)
stateInfo.find((state) => state.attributes && stateUsesUnits(state)); );
let unit: string | undefined; let unit: string | undefined;
if (currentState && stateUsesUnits(currentState)) { if (stateWithUnitorStateClass) {
unit = currentState.attributes.unit_of_measurement || " ";
} else if (stateWithUnitorStateClass) {
unit = stateWithUnitorStateClass.attributes.unit_of_measurement || " "; unit = stateWithUnitorStateClass.attributes.unit_of_measurement || " ";
} else { } else {
unit = { unit = {
@@ -343,7 +313,7 @@ export const computeHistory = (
input_number: "#", input_number: "#",
number: "#", number: "#",
water_heater: hass.config.unit_system.temperature, water_heater: hass.config.unit_system.temperature,
}[computeDomain(entityId)]; }[computeStateDomain(stateInfo[0])];
} }
if (!unit) { if (!unit) {
@@ -375,15 +345,6 @@ export const getStatisticIds = (
statistic_type, statistic_type,
}); });
export const getStatisticMetadata = (
hass: HomeAssistant,
statistic_ids?: string[]
) =>
hass.callWS<StatisticsMetaData[]>({
type: "recorder/get_statistics_metadata",
statistic_ids,
});
export const fetchStatistics = ( export const fetchStatistics = (
hass: HomeAssistant, hass: HomeAssistant,
startTime: Date, startTime: Date,
@@ -467,16 +428,3 @@ export const statisticsHaveType = (
stats: StatisticValue[], stats: StatisticValue[],
type: StatisticType type: StatisticType
) => stats.some((stat) => stat[type] !== null); ) => 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,8 +28,6 @@ export interface EntitySelector {
domain?: string | string[]; domain?: string | string[];
device_class?: string; device_class?: string;
multiple?: boolean; multiple?: boolean;
includeEntities?: string[];
excludeEntities?: string[];
}; };
} }

View File

@@ -0,0 +1,58 @@
import { HomeAssistant } from "../../types";
interface SupervisorBaseAvailableUpdates {
panel_path?: string;
update_type?: string;
version_latest?: string;
}
interface SupervisorAddonAvailableUpdates
extends SupervisorBaseAvailableUpdates {
update_type?: "addon";
icon?: string;
name?: string;
}
interface SupervisorCoreAvailableUpdates
extends SupervisorBaseAvailableUpdates {
update_type?: "core";
}
interface SupervisorOsAvailableUpdates extends SupervisorBaseAvailableUpdates {
update_type?: "os";
}
interface SupervisorSupervisorAvailableUpdates
extends SupervisorBaseAvailableUpdates {
update_type?: "supervisor";
}
export type SupervisorAvailableUpdates =
| SupervisorAddonAvailableUpdates
| SupervisorCoreAvailableUpdates
| SupervisorOsAvailableUpdates
| SupervisorSupervisorAvailableUpdates;
export interface SupervisorAvailableUpdatesResponse {
available_updates: SupervisorAvailableUpdates[];
}
export const fetchSupervisorAvailableUpdates = async (
hass: HomeAssistant
): Promise<SupervisorAvailableUpdates[]> =>
(
await hass.callWS<SupervisorAvailableUpdatesResponse>({
type: "supervisor/api",
endpoint: "/available_updates",
method: "get",
})
).available_updates;
export const refreshSupervisorAvailableUpdates = async (
hass: HomeAssistant
): Promise<void> =>
hass.callWS<void>({
type: "supervisor/api",
endpoint: "/refresh_updates",
method: "post",
});

View File

@@ -1,36 +0,0 @@
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 "@material/mwc-button";
import { mdiClose, mdiHelpCircle } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { import {
css, css,
@@ -33,7 +33,6 @@ import {
} from "../../data/device_registry"; } from "../../data/device_registry";
import { haStyleDialog } from "../../resources/styles"; import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import { showAlertDialog } from "../generic/show-dialog-box"; import { showAlertDialog } from "../generic/show-dialog-box";
import { import {
DataEntryFlowDialogParams, DataEntryFlowDialogParams,
@@ -237,33 +236,14 @@ class DataEntryFlowDialog extends LitElement {
// to reset the element. // to reset the element.
"" ""
: html` : html`
<div class="dialog-actions"> <ha-icon-button
${this._step .label=${this.hass.localize(
? html` "ui.panel.config.integrations.config_flow.dismiss"
<a )}
href=${documentationUrl( .path=${mdiClose}
this.hass, dialogAction="close"
`/integrations/${this._step.handler}` ?rtl=${computeRTL(this.hass)}
)} ></ha-icon-button>
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._step === null
? this._handler ? this._handler
? html`<step-flow-pick-flow ? html`<step-flow-pick-flow
@@ -492,19 +472,16 @@ class DataEntryFlowDialog extends LitElement {
ha-dialog { ha-dialog {
--dialog-content-padding: 0; --dialog-content-padding: 0;
} }
.dialog-actions { ha-icon-button {
padding: 16px; padding: 16px;
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
} }
.dialog-actions[rtl] { ha-icon-button[rtl] {
right: auto; right: auto;
left: 0; left: 0;
} }
.dialog-actions > * {
color: var(--secondary-text-color);
}
`, `,
]; ];
} }

View File

@@ -24,7 +24,7 @@ export const showConfigFlowDialog = (
loadDevicesAndAreas: true, loadDevicesAndAreas: true,
getFlowHandlers: async (hass) => { getFlowHandlers: async (hass) => {
const [handlers] = await Promise.all([ const [handlers] = await Promise.all([
getConfigFlowHandlers(hass, "integration"), getConfigFlowHandlers(hass),
hass.loadBackendTranslation("title", undefined, true), hass.loadBackendTranslation("title", undefined, true),
]); ]);

View File

@@ -216,16 +216,15 @@ class StepFlowPickHandler extends LitElement {
if (handler.is_add) { if (handler.is_add) {
if (handler.slug === "zwave_js") { if (handler.slug === "zwave_js") {
const entries = await getConfigEntries(this.hass, { const entries = await getConfigEntries(this.hass);
domain: "zwave_js", const entry = entries.find((ent) => ent.domain === "zwave_js");
});
if (!entries.length) { if (!entry) {
return; return;
} }
showZWaveJSAddNodeDialog(this, { showZWaveJSAddNodeDialog(this, {
entry_id: entries[0].entry_id, entry_id: entry.entry_id,
}); });
} else if (handler.slug === "zha") { } else if (handler.slug === "zha") {
navigate("/config/zha/add"); navigate("/config/zha/add");

View File

@@ -1,212 +0,0 @@
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,7 +25,6 @@ const LAZY_LOADED_MORE_INFO_CONTROL = {
script: () => import("./controls/more-info-script"), script: () => import("./controls/more-info-script"),
sun: () => import("./controls/more-info-sun"), sun: () => import("./controls/more-info-sun"),
timer: () => import("./controls/more-info-timer"), timer: () => import("./controls/more-info-timer"),
update: () => import("./controls/more-info-update"),
vacuum: () => import("./controls/more-info-vacuum"), vacuum: () => import("./controls/more-info-vacuum"),
water_heater: () => import("./controls/more-info-water_heater"), water_heater: () => import("./controls/more-info-water_heater"),
weather: () => import("./controls/more-info-weather"), weather: () => import("./controls/more-info-weather"),

View File

@@ -169,8 +169,8 @@ class OnboardingIntegrations extends LitElement {
} }
private async _loadConfigEntries() { private async _loadConfigEntries() {
const entries = await getConfigEntries(this.hass!, { type: "integration" }); const entries = await getConfigEntries(this.hass!);
// We filter out the config entries that are automatically created during onboarding. // We filter out the config entry for the local weather and rpi_power.
// It is one that we create automatically and it will confuse the user // It is one that we create automatically and it will confuse the user
// if it starts showing up during onboarding. // if it starts showing up during onboarding.
this._entries = entries.filter( this._entries = entries.filter(

View File

@@ -59,9 +59,7 @@ class HaConfigAutomation extends HassRouterPage {
private _getAutomations = memoizeOne( private _getAutomations = memoizeOne(
(states: HassEntities): AutomationEntity[] => (states: HassEntities): AutomationEntity[] =>
Object.values(states).filter( Object.values(states).filter(
(entity) => (entity) => computeStateDomain(entity) === "automation"
computeStateDomain(entity) === "automation" &&
!entity.attributes.restored
) as AutomationEntity[] ) as AutomationEntity[]
); );
@@ -89,7 +87,7 @@ class HaConfigAutomation extends HassRouterPage {
(!changedProps || changedProps.has("route")) && (!changedProps || changedProps.has("route")) &&
this._currentPage !== "dashboard" this._currentPage !== "dashboard"
) { ) {
const automationId = decodeURIComponent(this.routeTail.path.substr(1)); const automationId = this.routeTail.path.substr(1);
pageEl.automationId = automationId === "new" ? null : automationId; pageEl.automationId = automationId === "new" ? null : automationId;
} }
} }

View File

@@ -1,5 +1,3 @@
import type { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { import {
mdiCloudLock, mdiCloudLock,
mdiDotsVertical, mdiDotsVertical,
@@ -7,9 +5,10 @@ import {
mdiMagnify, mdiMagnify,
mdiNewBox, mdiNewBox,
} from "@mdi/js"; } from "@mdi/js";
import "@material/mwc-list/mwc-list-item";
import type { ActionDetail } from "@material/mwc-list";
import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
import type { HassEntities } from "home-assistant-js-websocket";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -19,29 +18,30 @@ import {
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import "../../../components/ha-button-menu";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next"; import "../../../components/ha-icon-next";
import "../../../components/ha-icon-button";
import "../../../components/ha-menu-button"; import "../../../components/ha-menu-button";
import "../../../components/ha-button-menu";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { CloudStatus } from "../../../data/cloud"; import { CloudStatus } from "../../../data/cloud";
import { updateCanInstall, UpdateEntity } from "../../../data/update"; import {
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; refreshSupervisorAvailableUpdates,
SupervisorAvailableUpdates,
} from "../../../data/supervisor/root";
import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar"; import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar";
import "../../../layouts/ha-app-layout"; import "../../../layouts/ha-app-layout";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast";
import "../ha-config-section"; import "../ha-config-section";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import "./ha-config-navigation"; import "./ha-config-navigation";
import "./ha-config-updates"; import "./ha-config-updates";
import { fireEvent } from "../../../common/dom/fire_event";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { showToast } from "../../../util/toast";
import { documentationUrl } from "../../../util/documentation-url";
const randomTip = (hass: HomeAssistant) => { const randomTip = (hass: HomeAssistant) => {
const weighted: string[] = []; const weighted: string[] = [];
@@ -113,6 +113,9 @@ class HaConfigDashboard extends LitElement {
@property() public cloudStatus?: CloudStatus; @property() public cloudStatus?: CloudStatus;
// null means not available
@property() public supervisorUpdates?: SupervisorAvailableUpdates[] | null;
@property() public showAdvanced!: boolean; @property() public showAdvanced!: boolean;
@state() private _tip?: string; @state() private _tip?: string;
@@ -120,9 +123,6 @@ class HaConfigDashboard extends LitElement {
private _notifyUpdates = false; private _notifyUpdates = false;
protected render(): TemplateResult { protected render(): TemplateResult {
const canInstallUpdates = this._filterUpdateEntitiesWithInstall(
this.hass.states
);
return html` return html`
<ha-app-layout> <ha-app-layout>
<app-header fixed slot="header"> <app-header fixed slot="header">
@@ -160,47 +160,50 @@ class HaConfigDashboard extends LitElement {
.isWide=${this.isWide} .isWide=${this.isWide}
full-width full-width
> >
${canInstallUpdates.length ${this.supervisorUpdates === undefined
? html`<ha-card> ? // Hide everything until updates loaded
<ha-config-updates html``
.hass=${this.hass} : html`${this.supervisorUpdates?.length
.narrow=${this.narrow} ? html`<ha-card>
.updateEntities=${canInstallUpdates} <ha-config-updates
></ha-config-updates> .hass=${this.hass}
</ha-card>` .narrow=${this.narrow}
: ""} .supervisorUpdates=${this.supervisorUpdates}
<ha-card> ></ha-config-updates>
${this.narrow && canInstallUpdates.length </ha-card>`
? html`<div class="title"> : ""}
${this.hass.localize("panel.config")} <ha-card>
</div>` ${this.narrow && this.supervisorUpdates?.length
: ""} ? html`<div class="title">
${this.cloudStatus && isComponentLoaded(this.hass, "cloud") ${this.hass.localize("panel.config")}
? html` </div>`
: ""}
${this.cloudStatus && isComponentLoaded(this.hass, "cloud")
? html`
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.showAdvanced=${this.showAdvanced}
.pages=${[
{
component: "cloud",
path: "/config/cloud",
name: "Home Assistant Cloud",
info: this.cloudStatus,
iconPath: mdiCloudLock,
iconColor: "#3B808E",
},
]}
></ha-config-navigation>
`
: ""}
<ha-config-navigation <ha-config-navigation
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.showAdvanced=${this.showAdvanced} .showAdvanced=${this.showAdvanced}
.pages=${[ .pages=${configSections.dashboard}
{
component: "cloud",
path: "/config/cloud",
name: "Home Assistant Cloud",
info: this.cloudStatus,
iconPath: mdiCloudLock,
iconColor: "#3B808E",
},
]}
></ha-config-navigation> ></ha-config-navigation>
` </ha-card>`}
: ""}
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.showAdvanced=${this.showAdvanced}
.pages=${configSections.dashboard}
></ha-config-navigation>
</ha-card>
<div class="tips"> <div class="tips">
<ha-svg-icon .path=${mdiLightbulbOutline}></ha-svg-icon> <ha-svg-icon .path=${mdiLightbulbOutline}></ha-svg-icon>
<span class="tip-word">Tip!</span> <span class="tip-word">Tip!</span>
@@ -218,11 +221,11 @@ class HaConfigDashboard extends LitElement {
this._tip = randomTip(this.hass); this._tip = randomTip(this.hass);
} }
if (!changedProps.has("hass") || !this._notifyUpdates) { if (!changedProps.has("supervisorUpdates") || !this._notifyUpdates) {
return; return;
} }
this._notifyUpdates = false; this._notifyUpdates = false;
if (this._filterUpdateEntitiesWithInstall(this.hass.states).length) { if (this.supervisorUpdates?.length) {
showToast(this, { showToast(this, {
message: this.hass.localize( message: this.hass.localize(
"ui.panel.config.updates.updates_refreshed" "ui.panel.config.updates.updates_refreshed"
@@ -235,44 +238,6 @@ class HaConfigDashboard extends LitElement {
} }
} }
private _filterUpdateEntities = memoizeOne((entities: HassEntities) =>
(
Object.values(entities).filter(
(entity) => computeStateDomain(entity) === "update"
) as UpdateEntity[]
).sort((a, b) => {
if (a.attributes.title === "Home Assistant Core") {
return -3;
}
if (b.attributes.title === "Home Assistant Core") {
return 3;
}
if (a.attributes.title === "Home Assistant Operating System") {
return -2;
}
if (b.attributes.title === "Home Assistant Operating System") {
return 2;
}
if (a.attributes.title === "Home Assistant Supervisor") {
return -1;
}
if (b.attributes.title === "Home Assistant Supervisor") {
return 1;
}
return caseInsensitiveStringCompare(
a.attributes.title || a.attributes.friendly_name || "",
b.attributes.title || b.attributes.friendly_name || ""
);
})
);
private _filterUpdateEntitiesWithInstall = memoizeOne(
(entities: HassEntities) =>
this._filterUpdateEntities(entities).filter((entity) =>
updateCanInstall(entity)
)
);
private _showQuickBar(): void { private _showQuickBar(): void {
showQuickBar(this, { showQuickBar(this, {
commandMode: true, commandMode: true,
@@ -281,24 +246,20 @@ class HaConfigDashboard extends LitElement {
} }
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) { private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
const _entities = this._filterUpdateEntities(this.hass.states).map(
(entity) => entity.entity_id
);
switch (ev.detail.index) { switch (ev.detail.index) {
case 0: case 0:
if (_entities.length) { if (isComponentLoaded(this.hass, "hassio")) {
this._notifyUpdates = true; this._notifyUpdates = true;
await this.hass.callService("homeassistant", "update_entity", { await refreshSupervisorAvailableUpdates(this.hass);
entity_id: _entities, fireEvent(this, "ha-refresh-supervisor");
});
return; return;
} }
showAlertDialog(this, { showAlertDialog(this, {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.updates.no_update_entities.title" "ui.panel.config.updates.check_unavailable.title"
), ),
text: this.hass.localize( text: this.hass.localize(
"ui.panel.config.updates.no_update_entities.description" "ui.panel.config.updates.check_unavailable.description"
), ),
warning: true, warning: true,
}); });

View File

@@ -1,14 +1,21 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { mdiPackageVariant } from "@mdi/js";
import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/entity/state-badge";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-icon-next"; import "../../../components/ha-logo-svg";
import type { UpdateEntity } from "../../../data/update"; import "../../../components/ha-svg-icon";
import { SupervisorAvailableUpdates } from "../../../data/supervisor/root";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import "../../../components/ha-icon-next";
export const SUPERVISOR_UPDATE_NAMES = {
core: "Home Assistant Core",
os: "Home Assistant Operating System",
supervisor: "Home Assistant Supervisor",
};
@customElement("ha-config-updates") @customElement("ha-config-updates")
class HaConfigUpdates extends LitElement { class HaConfigUpdates extends LitElement {
@@ -17,60 +24,62 @@ class HaConfigUpdates extends LitElement {
@property({ type: Boolean }) public narrow!: boolean; @property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) @property({ attribute: false })
public updateEntities?: UpdateEntity[]; public supervisorUpdates?: SupervisorAvailableUpdates[] | null;
@state() private _showAll = false; @state() private _showAll = false;
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.updateEntities?.length) { if (!this.supervisorUpdates?.length) {
return html``; return html``;
} }
const updates = const updates =
this._showAll || this.updateEntities.length <= 3 this._showAll || this.supervisorUpdates.length <= 3
? this.updateEntities ? this.supervisorUpdates
: this.updateEntities.slice(0, 2); : this.supervisorUpdates.slice(0, 2);
return html` return html`
<div class="title"> <div class="title">
${this.hass.localize("ui.panel.config.updates.title", { ${this.hass.localize("ui.panel.config.updates.title", {
count: this.updateEntities.length, count: this.supervisorUpdates.length,
})} })}
</div> </div>
${updates.map( ${updates.map(
(entity) => html` (update) => html`
<paper-icon-item <a href="/hassio${update.panel_path}">
@click=${this._openMoreInfo} <paper-icon-item>
.entity_id=${entity.entity_id} <span slot="item-icon" class="icon">
> ${update.update_type === "addon"
<span slot="item-icon" class="icon"> ? update.icon
<state-badge ? html`<img src="/api/hassio${update.icon}" />`
.title=${entity.attributes.title || : html`<ha-svg-icon
entity.attributes.friendly_name} .path=${mdiPackageVariant}
.stateObj=${entity} ></ha-svg-icon>`
slot="item-icon" : html`<ha-logo-svg></ha-logo-svg>`}
></state-badge> </span>
</span> <paper-item-body two-line>
<paper-item-body two-line> ${update.update_type === "addon"
${entity.attributes.title || entity.attributes.friendly_name} ? update.name
<div secondary> : SUPERVISOR_UPDATE_NAMES[update.update_type!]}
${this.hass.localize( <div secondary>
"ui.panel.config.updates.version_available", ${this.hass.localize(
{ "ui.panel.config.updates.version_available",
version_available: entity.attributes.latest_version, {
} version_available: update.version_latest,
)} }
</div> )}
</paper-item-body> </div>
${!this.narrow ? html`<ha-icon-next></ha-icon-next>` : ""} </paper-item-body>
</paper-icon-item> ${!this.narrow ? html`<ha-icon-next></ha-icon-next>` : ""}
</paper-icon-item>
</a>
` `
)} )}
${!this._showAll && this.updateEntities.length >= 4 ${!this._showAll && this.supervisorUpdates.length >= 4
? html` ? html`
<button class="show-more" @click=${this._showAllClicked}> <button class="show-more" @click=${this._showAllClicked}>
${this.hass.localize("ui.panel.config.updates.more_updates", { ${this.hass.localize("ui.panel.config.updates.more_updates", {
count: this.updateEntities!.length - updates.length, count: this.supervisorUpdates!.length - updates.length,
})} })}
</button> </button>
` `
@@ -78,12 +87,6 @@ class HaConfigUpdates extends LitElement {
`; `;
} }
private _openMoreInfo(ev: MouseEvent): void {
fireEvent(this, "hass-more-info", {
entityId: (ev.currentTarget as any).entity_id,
});
}
private _showAllClicked() { private _showAllClicked() {
this._showAll = true; this._showAll = true;
} }
@@ -96,11 +99,25 @@ class HaConfigUpdates extends LitElement {
padding: 16px; padding: 16px;
padding-bottom: 0; padding-bottom: 0;
} }
a {
text-decoration: none;
color: var(--primary-text-color);
}
.icon { .icon {
display: inline-flex; display: inline-flex;
height: 100%; height: 100%;
align-items: center; align-items: center;
} }
img,
ha-svg-icon,
ha-logo-svg {
--mdc-icon-size: 32px;
max-height: 32px;
width: 32px;
}
ha-logo-svg {
color: var(--secondary-text-color);
}
ha-icon-next { ha-icon-next {
color: var(--secondary-text-color); color: var(--secondary-text-color);
height: 24px; height: 24px;
@@ -122,9 +139,6 @@ class HaConfigUpdates extends LitElement {
outline: none; outline: none;
text-decoration: underline; text-decoration: underline;
} }
paper-icon-item {
cursor: pointer;
}
`, `,
]; ];
} }

View File

@@ -58,11 +58,12 @@ export class HaDeviceInfoZWaveJS extends LitElement {
return; return;
} }
const configEntries = await getConfigEntries(this.hass, { const configEntries = await getConfigEntries(this.hass);
domain: "zwave_js",
});
let zwaveJsConfEntries = 0; let zwaveJsConfEntries = 0;
for (const entry of configEntries) { for (const entry of configEntries) {
if (entry.domain !== "zwave_js") {
continue;
}
if (zwaveJsConfEntries) { if (zwaveJsConfEntries) {
this._multipleConfigEntries = true; this._multipleConfigEntries = true;
} }

View File

@@ -121,7 +121,6 @@ export class EnergyGasSettings extends LitElement {
showEnergySettingsGasDialog(this, { showEnergySettingsGasDialog(this, {
unit: getEnergyGasUnitCategory(this.hass, this.preferences), unit: getEnergyGasUnitCategory(this.hass, this.preferences),
saveCallback: async (source) => { saveCallback: async (source) => {
delete source.unit_of_measurement;
await this._savePreferences({ await this._savePreferences({
...this.preferences, ...this.preferences,
energy_sources: this.preferences.energy_sources.concat(source), energy_sources: this.preferences.energy_sources.concat(source),

View File

@@ -54,7 +54,7 @@ export class EnergyGridSettings extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
public validationResult?: EnergyPreferencesValidation; public validationResult?: EnergyPreferencesValidation;
@state() private _co2ConfigEntry?: ConfigEntry; @state() private _configEntries?: ConfigEntry[];
protected firstUpdated() { protected firstUpdated() {
this._fetchCO2SignalConfigEntries(); this._fetchCO2SignalConfigEntries();
@@ -195,28 +195,28 @@ export class EnergyGridSettings extends LitElement {
"ui.panel.config.energy.grid.grid_carbon_footprint" "ui.panel.config.energy.grid.grid_carbon_footprint"
)} )}
</h3> </h3>
${this._co2ConfigEntry ${this._configEntries?.map(
? html`<div class="row" .entry=${this._co2ConfigEntry}> (entry) => html`<div class="row" .entry=${entry}>
<img <img
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
src=${brandsUrl({ src=${brandsUrl({
domain: "co2signal", domain: "co2signal",
type: "icon", type: "icon",
darkOptimized: this.hass.themes?.darkMode, darkOptimized: this.hass.themes?.darkMode,
})} })}
/> />
<span class="content">${this._co2ConfigEntry.title}</span> <span class="content">${entry.title}</span>
<a <a href=${`/config/integrations#config_entry=${entry.entry_id}`}>
href=${`/config/integrations#config_entry=${this._co2ConfigEntry.entry_id}`} <ha-icon-button .path=${mdiPencil}></ha-icon-button>
> </a>
<ha-icon-button .path=${mdiPencil}></ha-icon-button> <ha-icon-button
</a> @click=${this._removeCO2Sensor}
<ha-icon-button .path=${mdiDelete}
@click=${this._removeCO2Sensor} ></ha-icon-button>
.path=${mdiDelete} </div>`
></ha-icon-button> )}
</div>` ${this._configEntries?.length === 0
: html` ? html`
<div class="row border-bottom"> <div class="row border-bottom">
<img <img
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
@@ -232,15 +232,17 @@ export class EnergyGridSettings extends LitElement {
)} )}
</mwc-button> </mwc-button>
</div> </div>
`} `
: ""}
</div> </div>
</ha-card> </ha-card>
`; `;
} }
private async _fetchCO2SignalConfigEntries() { private async _fetchCO2SignalConfigEntries() {
const entries = await getConfigEntries(this.hass, { domain: "co2signal" }); this._configEntries = (await getConfigEntries(this.hass)).filter(
this._co2ConfigEntry = entries.length ? entries[0] : undefined; (entry) => entry.domain === "co2signal"
);
} }
private _addCO2Sensor() { private _addCO2Sensor() {

View File

@@ -176,17 +176,9 @@ export class DialogEnergySolarSettings
private async _fetchSolarForecastConfigEntries() { private async _fetchSolarForecastConfigEntries() {
const domains = this._params!.info.solar_forecast_domains; const domains = this._params!.info.solar_forecast_domains;
this._configEntries = this._configEntries = (await getConfigEntries(this.hass)).filter((entry) =>
domains.length === 0 domains.includes(entry.domain)
? [] );
: 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) { private _handleForecastChanged(ev: CustomEvent) {

View File

@@ -10,11 +10,50 @@ import { customElement, property, state, query } from "lit/decorators";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import {
deleteCounter,
fetchCounter,
updateCounter,
} from "../../../../../data/counter";
import { import {
ExtEntityRegistryEntry, ExtEntityRegistryEntry,
removeEntityRegistryEntry, removeEntityRegistryEntry,
} from "../../../../../data/entity_registry"; } from "../../../../../data/entity_registry";
import { HELPERS_CRUD } from "../../../../../data/helpers_crud"; 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 { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
@@ -30,6 +69,49 @@ import "../../../helpers/forms/ha-timer-form";
import "../../entity-registry-basic-editor"; import "../../entity-registry-basic-editor";
import type { HaEntityRegistryBasicEditor } from "../../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") @customElement("entity-settings-helper-tab")
export class EntityRegistrySettingsHelper extends LitElement { export class EntityRegistrySettingsHelper extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -116,7 +198,7 @@ export class EntityRegistrySettingsHelper extends LitElement {
} }
private async _getItem() { private async _getItem() {
const items = await HELPERS_CRUD[this.entry.platform].fetch(this.hass!); const items = await HELPERS[this.entry.platform].fetch(this.hass!);
this._item = items.find((item) => item.id === this.entry.unique_id) || null; this._item = items.find((item) => item.id === this.entry.unique_id) || null;
} }
@@ -124,7 +206,7 @@ export class EntityRegistrySettingsHelper extends LitElement {
this._submitting = true; this._submitting = true;
try { try {
if (this._componentLoaded && this._item) { if (this._componentLoaded && this._item) {
await HELPERS_CRUD[this.entry.platform].update( await HELPERS[this.entry.platform].update(
this.hass!, this.hass!,
this._item.id, this._item.id,
this._item this._item
@@ -154,10 +236,7 @@ export class EntityRegistrySettingsHelper extends LitElement {
try { try {
if (this._componentLoaded && this._item) { if (this._componentLoaded && this._item) {
await HELPERS_CRUD[this.entry.platform].delete( await HELPERS[this.entry.platform].delete(this.hass!, this._item.id);
this.hass!,
this._item.id
);
} else { } else {
const stateObj = this.hass.states[this.entry.entity_id]; const stateObj = this.hass.states[this.entry.entity_id];
if (!stateObj?.attributes.restored) { if (!stateObj?.attributes.restored) {

View File

@@ -1,13 +1,13 @@
import "../../../components/ha-expansion-panel";
import "@material/mwc-formfield/mwc-formfield"; import "@material/mwc-formfield/mwc-formfield";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import "../../../components/ha-area-picker"; import "../../../components/ha-area-picker";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-radio";
import "../../../components/ha-switch"; import "../../../components/ha-switch";
import "../../../components/ha-textfield"; import "../../../components/ha-textfield";
import "../../../components/ha-radio";
import { import {
DeviceRegistryEntry, DeviceRegistryEntry,
subscribeDeviceRegistry, subscribeDeviceRegistry,
@@ -182,12 +182,9 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
name="hiddendisabled" name="hiddendisabled"
value="enabled" value="enabled"
.checked=${!this._hiddenBy && !this._disabledBy} .checked=${!this._hiddenBy && !this._disabledBy}
.disabled=${this._device?.disabled_by || .disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
(this._disabledBy && this._device?.disabled_by ||
!( (this._disabledBy && this._disabledBy !== "user")}
this._disabledBy === "user" ||
this._disabledBy === "integration"
))}
@change=${this._viewStatusChanged} @change=${this._viewStatusChanged}
></ha-radio> ></ha-radio>
</mwc-formfield> </mwc-formfield>
@@ -200,12 +197,9 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
name="hiddendisabled" name="hiddendisabled"
value="hidden" value="hidden"
.checked=${this._hiddenBy !== null} .checked=${this._hiddenBy !== null}
.disabled=${this._device?.disabled_by || .disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
(this._disabledBy && Boolean(this._device?.disabled_by) ||
!( (this._disabledBy && this._disabledBy !== "user")}
this._disabledBy === "user" ||
this._disabledBy === "integration"
))}
@change=${this._viewStatusChanged} @change=${this._viewStatusChanged}
></ha-radio> ></ha-radio>
</mwc-formfield> </mwc-formfield>
@@ -218,12 +212,9 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
name="hiddendisabled" name="hiddendisabled"
value="disabled" value="disabled"
.checked=${this._disabledBy !== null} .checked=${this._disabledBy !== null}
.disabled=${this._device?.disabled_by || .disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
(this._disabledBy && Boolean(this._device?.disabled_by) ||
!( (this._disabledBy && this._disabledBy !== "user")}
this._disabledBy === "user" ||
this._disabledBy === "integration"
))}
@change=${this._viewStatusChanged} @change=${this._viewStatusChanged}
></ha-radio> ></ha-radio>
</mwc-formfield> </mwc-formfield>

View File

@@ -42,12 +42,6 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail"; 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 = { const OVERRIDE_DEVICE_CLASSES = {
cover: [ cover: [
@@ -89,8 +83,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@state() private _device?: DeviceRegistryEntry; @state() private _device?: DeviceRegistryEntry;
@state() private _helperConfigEntry?: ConfigEntry;
@state() private _error?: string; @state() private _error?: string;
@state() private _submitting?: boolean; @state() private _submitting?: boolean;
@@ -111,20 +103,6 @@ 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) { protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties); super.updated(changedProperties);
if (changedProperties.has("entry")) { if (changedProperties.has("entry")) {
@@ -237,21 +215,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@value-changed=${this._areaPicked} @value-changed=${this._areaPicked}
></ha-area-picker>` ></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 <ha-expansion-panel
.header=${this.hass.localize( .header=${this.hass.localize(
"ui.dialogs.entity_registry.editor.advanced" "ui.dialogs.entity_registry.editor.advanced"
@@ -378,7 +341,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
class="warning" class="warning"
@click=${this._confirmDeleteEntry} @click=${this._confirmDeleteEntry}
.disabled=${this._submitting || .disabled=${this._submitting ||
(!this._helperConfigEntry && !stateObj.attributes.restored)} !(stateObj && stateObj.attributes.restored)}
> >
${this.hass.localize("ui.dialogs.entity_registry.editor.delete")} ${this.hass.localize("ui.dialogs.entity_registry.editor.delete")}
</mwc-button> </mwc-button>
@@ -508,21 +471,13 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
this._submitting = true; this._submitting = true;
try { try {
if (this._helperConfigEntry) { await removeEntityRegistryEntry(this.hass!, this._origEntityId);
await deleteConfigEntry(this.hass, this._helperConfigEntry.entry_id);
} else {
await removeEntityRegistryEntry(this.hass!, this._origEntityId);
}
fireEvent(this, "close-dialog"); fireEvent(this, "close-dialog");
} finally { } finally {
this._submitting = false; this._submitting = false;
} }
} }
private async _showOptionsFlow() {
showOptionsFlowDialog(this, this._helperConfigEntry!);
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,

View File

@@ -27,6 +27,10 @@ import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { listenMediaQuery } from "../../common/dom/media_query"; import { listenMediaQuery } from "../../common/dom/media_query";
import { CloudStatus, fetchCloudStatus } from "../../data/cloud"; import { CloudStatus, fetchCloudStatus } from "../../data/cloud";
import {
fetchSupervisorAvailableUpdates,
SupervisorAvailableUpdates,
} from "../../data/supervisor/root";
import "../../layouts/hass-loading-screen"; import "../../layouts/hass-loading-screen";
import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page"; import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page";
import { PageNavigation } from "../../layouts/hass-tabs-subpage"; import { PageNavigation } from "../../layouts/hass-tabs-subpage";
@@ -393,6 +397,8 @@ class HaPanelConfig extends HassRouterPage {
@state() private _cloudStatus?: CloudStatus; @state() private _cloudStatus?: CloudStatus;
@state() private _supervisorUpdates?: SupervisorAvailableUpdates[] | null;
private _listeners: Array<() => void> = []; private _listeners: Array<() => void> = [];
public connectedCallback() { public connectedCallback() {
@@ -427,7 +433,19 @@ class HaPanelConfig extends HassRouterPage {
} }
}); });
} }
if (isComponentLoaded(this.hass, "hassio")) {
this._loadSupervisorUpdates();
this.addEventListener("ha-refresh-supervisor", () => {
this._loadSupervisorUpdates();
});
this.addEventListener("connection-status", (ev) => {
if (ev.detail === "connected") {
this._loadSupervisorUpdates();
}
});
} else {
this._supervisorUpdates = null;
}
this.addEventListener("ha-refresh-cloud-status", () => this.addEventListener("ha-refresh-cloud-status", () =>
this._updateCloudStatus() this._updateCloudStatus()
); );
@@ -458,6 +476,7 @@ class HaPanelConfig extends HassRouterPage {
isWide, isWide,
narrow: this.narrow, narrow: this.narrow,
cloudStatus: this._cloudStatus, cloudStatus: this._cloudStatus,
supervisorUpdates: this._supervisorUpdates,
}); });
} else { } else {
el.route = this.routeTail; el.route = this.routeTail;
@@ -466,6 +485,7 @@ class HaPanelConfig extends HassRouterPage {
el.isWide = isWide; el.isWide = isWide;
el.narrow = this.narrow; el.narrow = this.narrow;
el.cloudStatus = this._cloudStatus; el.cloudStatus = this._cloudStatus;
el.supervisorUpdates = this._supervisorUpdates;
} }
} }
@@ -483,6 +503,16 @@ class HaPanelConfig extends HassRouterPage {
setTimeout(() => this._updateCloudStatus(), 5000); setTimeout(() => this._updateCloudStatus(), 5000);
} }
} }
private async _loadSupervisorUpdates(): Promise<void> {
try {
this._supervisorUpdates = await fetchSupervisorAvailableUpdates(
this.hass
);
} catch (err) {
this._supervisorUpdates = null;
}
}
} }
declare global { declare global {

View File

@@ -1,11 +1,11 @@
import type { Counter } from "../../../data/counter"; import { Counter } from "../../../data/counter";
import type { InputBoolean } from "../../../data/input_boolean"; import { InputBoolean } from "../../../data/input_boolean";
import type { InputButton } from "../../../data/input_button"; import { InputButton } from "../../../data/input_button";
import type { InputDateTime } from "../../../data/input_datetime"; import { InputDateTime } from "../../../data/input_datetime";
import type { InputNumber } from "../../../data/input_number"; import { InputNumber } from "../../../data/input_number";
import type { InputSelect } from "../../../data/input_select"; import { InputSelect } from "../../../data/input_select";
import type { InputText } from "../../../data/input_text"; import { InputText } from "../../../data/input_text";
import type { Timer } from "../../../data/timer"; import { Timer } from "../../../data/timer";
export const HELPER_DOMAINS = [ export const HELPER_DOMAINS = [
"input_boolean", "input_boolean",

View File

@@ -8,8 +8,6 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { dynamicElement } from "../../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
import { domainIcon } from "../../../common/entity/domain_icon"; import { domainIcon } from "../../../common/entity/domain_icon";
import "../../../components/ha-dialog"; import "../../../components/ha-dialog";
import "../../../components/ha-circular-progress";
import { getConfigFlowHandlers } from "../../../data/config_flow";
import { createCounter } from "../../../data/counter"; import { createCounter } from "../../../data/counter";
import { createInputBoolean } from "../../../data/input_boolean"; import { createInputBoolean } from "../../../data/input_boolean";
import { createInputButton } from "../../../data/input_button"; import { createInputButton } from "../../../data/input_button";
@@ -18,7 +16,6 @@ import { createInputNumber } from "../../../data/input_number";
import { createInputSelect } from "../../../data/input_select"; import { createInputSelect } from "../../../data/input_select";
import { createInputText } from "../../../data/input_text"; import { createInputText } from "../../../data/input_text";
import { createTimer } from "../../../data/timer"; import { createTimer } from "../../../data/timer";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import { haStyleDialog } from "../../../resources/styles"; import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { Helper } from "./const"; import { Helper } from "./const";
@@ -30,8 +27,6 @@ import "./forms/ha-input_number-form";
import "./forms/ha-input_select-form"; import "./forms/ha-input_select-form";
import "./forms/ha-input_text-form"; import "./forms/ha-input_text-form";
import "./forms/ha-timer-form"; import "./forms/ha-timer-form";
import { domainToName } from "../../../data/integration";
import type { ShowDialogHelperDetailParams } from "./show-dialog-helper-detail";
const HELPERS = { const HELPERS = {
input_boolean: createInputBoolean, input_boolean: createInputBoolean,
@@ -52,7 +47,7 @@ export class DialogHelperDetail extends LitElement {
@state() private _opened = false; @state() private _opened = false;
@state() private _domain?: string; @state() private _platform?: string;
@state() private _error?: string; @state() private _error?: string;
@@ -60,135 +55,102 @@ export class DialogHelperDetail extends LitElement {
@query(".form") private _form?: HTMLDivElement; @query(".form") private _form?: HTMLDivElement;
@state() private _helperFlows?: string[]; public async showDialog(): Promise<void> {
this._platform = undefined;
private _params?: ShowDialogHelperDetailParams;
public async showDialog(params: ShowDialogHelperDetailParams): Promise<void> {
this._params = params;
this._domain = undefined;
this._item = undefined; this._item = undefined;
this._opened = true; this._opened = true;
await this.updateComplete; 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 { public closeDialog(): void {
this._opened = false; this._opened = false;
this._error = ""; this._error = "";
this._params = undefined;
} }
protected render(): TemplateResult { 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` return html`
<ha-dialog <ha-dialog
.open=${this._opened} .open=${this._opened}
@closed=${this.closeDialog} @closed=${this.closeDialog}
class=${classMap({ "button-left": !this._domain })} class=${classMap({ "button-left": !this._platform })}
scrimClickAction scrimClickAction
escapeKeyAction escapeKeyAction
.heading=${this._domain .heading=${this._platform
? this.hass.localize( ? this.hass.localize(
"ui.panel.config.helpers.dialog.add_platform", "ui.panel.config.helpers.dialog.add_platform",
"platform", "platform",
this.hass.localize( this.hass.localize(
`ui.panel.config.helpers.types.${this._domain}` `ui.panel.config.helpers.types.${this._platform}`
) || this._domain ) || this._platform
) )
: this.hass.localize("ui.panel.config.helpers.dialog.add_helper")} : this.hass.localize("ui.panel.config.helpers.dialog.add_helper")}
> >
${content} ${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>
`}
</ha-dialog> </ha-dialog>
`; `;
} }
@@ -198,13 +160,13 @@ export class DialogHelperDetail extends LitElement {
} }
private async _createItem(): Promise<void> { private async _createItem(): Promise<void> {
if (!this._domain || !this._item) { if (!this._platform || !this._item) {
return; return;
} }
this._submitting = true; this._submitting = true;
this._error = ""; this._error = "";
try { try {
await HELPERS[this._domain](this.hass, this._item); await HELPERS[this._platform](this.hass, this._item);
this.closeDialog(); this.closeDialog();
} catch (err: any) { } catch (err: any) {
this._error = err.message || "Unknown error"; this._error = err.message || "Unknown error";
@@ -219,22 +181,12 @@ export class DialogHelperDetail extends LitElement {
} }
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
this._domainPicked(ev); this._platformPicked(ev);
} }
private _domainPicked(ev: Event): void { private _platformPicked(ev: Event): void {
const domain = (ev.currentTarget! as any).domain; this._platform = (ev.currentTarget! as any).platform;
this._focusForm();
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> { private async _focusForm(): Promise<void> {
@@ -243,7 +195,7 @@ export class DialogHelperDetail extends LitElement {
} }
private _goBack() { private _goBack() {
this._domain = undefined; this._platform = undefined;
this._item = undefined; this._item = undefined;
this._error = undefined; this._error = undefined;
} }

View File

@@ -1,58 +1,28 @@
import { mdiPencilOff, mdiPlus } from "@mdi/js"; import { mdiPencilOff, mdiPlus } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoize from "memoize-one";
import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { domainIcon } from "../../../common/entity/domain_icon"; import { domainIcon } from "../../../common/entity/domain_icon";
import { LocalizeFunc } from "../../../common/translations/localize";
import { import {
DataTableColumnContainer, DataTableColumnContainer,
RowClickedEvent, RowClickedEvent,
} from "../../../components/data-table/ha-data-table"; } from "../../../components/data-table/ha-data-table";
import "../../../components/ha-fab"; import "../../../components/ha-fab";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-icon"; import "../../../components/ha-icon";
import "../../../components/ha-svg-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-loading-screen";
import "../../../layouts/hass-tabs-subpage-data-table"; import "../../../layouts/hass-tabs-subpage-data-table";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor"; import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import { HELPER_DOMAINS } from "./const"; import { HELPER_DOMAINS } from "./const";
import { showHelperDetailDialog } from "./show-dialog-helper-detail"; 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") @customElement("ha-config-helpers")
export class HaConfigHelpers extends SubscribeMixin(LitElement) { export class HaConfigHelpers extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public isWide!: boolean; @property() public isWide!: boolean;
@@ -63,122 +33,98 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
@state() private _stateItems: HassEntity[] = []; @state() private _stateItems: HassEntity[] = [];
@state() private _entityEntries?: Record<string, EntityRegistryEntry>; private _columns = memoize((narrow, _language): DataTableColumnContainer => {
const columns: DataTableColumnContainer = {
@state() private _configEntries?: Record<string, ConfigEntry>; icon: {
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: "", title: "",
label: this.hass.localize( label: this.hass.localize(
"ui.panel.config.helpers.picker.headers.editable" "ui.panel.config.helpers.picker.headers.icon"
), ),
type: "icon", type: "icon",
template: (editable) => html` template: (icon, helper: any) =>
${!editable icon
? html` ? html` <ha-icon .icon=${icon}></ha-icon> `
<div : html`<ha-svg-icon
tabindex="0" .path=${domainIcon(helper.type)}
style="display:inline-block; position: relative;" ></ha-svg-icon>`,
> },
<ha-svg-icon .path=${mdiPencilOff}></ha-svg-icon> name: {
<paper-tooltip animation-delay="0" position="left"> title: this.hass.localize(
${this.hass.localize( "ui.panel.config.helpers.picker.headers.name"
"ui.panel.config.entities.picker.status.readonly" ),
)} sortable: true,
</paper-tooltip> filterable: true,
</div> 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%",
}; };
return columns;
} }
); 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}
`,
};
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;
});
private _getItems = memoizeOne( private _getItems = memoize((stateItems: HassEntity[]) =>
( stateItems.map((entityState) => ({
stateItems: HassEntity[], id: entityState.entity_id,
entityEntries: Record<string, EntityRegistryEntry>, icon: entityState.attributes.icon,
configEntries: Record<string, ConfigEntry> name: entityState.attributes.friendly_name || "",
) => entity_id: entityState.entity_id,
stateItems.map((entityState) => { editable: entityState.attributes.editable,
const configEntry = getConfigEntry( type: computeStateDomain(entityState),
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 { protected render(): TemplateResult {
if ( if (!this.hass || this._stateItems === undefined) {
!this.hass ||
this._stateItems === undefined ||
this._entityEntries === undefined ||
this._configEntries === undefined
) {
return html` <hass-loading-screen></hass-loading-screen> `; return html` <hass-loading-screen></hass-loading-screen> `;
} }
@@ -189,12 +135,8 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
back-path="/config" back-path="/config"
.route=${this.route} .route=${this.route}
.tabs=${configSections.automations} .tabs=${configSections.automations}
.columns=${this._columns(this.narrow, this.hass.localize)} .columns=${this._columns(this.narrow, this.hass.language)}
.data=${this._getItems( .data=${this._getItems(this._stateItems)}
this._stateItems,
this._entityEntries,
this._configEntries
)}
@row-click=${this._openEditDialog} @row-click=${this._openEditDialog}
hasFab hasFab
clickable clickable
@@ -218,67 +160,32 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this._getConfigEntries(); this._getStates();
} }
protected willUpdate(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
super.willUpdate(changedProps); super.updated(changedProps);
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!this._entityEntries || !this._configEntries) { if (oldHass && this._stateItems) {
return; this._getStates(oldHass);
} }
}
let changed = private _getStates(oldHass?: HomeAssistant) {
!this._stateItems || let changed = false;
changedProps.has("_entityEntries") || const tempStates = Object.values(this.hass!.states).filter((entity) => {
changedProps.has("_configEntries"); if (!HELPER_DOMAINS.includes(computeStateDomain(entity))) {
return false;
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);
} }
if (oldHass?.states[entity.entity_id] !== entity) {
changed = true;
}
return true;
});
if (changed || this._stateItems.length !== tempStates.length) {
this._stateItems = tempStates;
} }
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;
}
}
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entries) => {
this._entityEntries = groupByOne(entries, (entry) => entry.entity_id);
}),
];
}
private async _getConfigEntries() {
this._configEntries = groupByOne(
await getConfigEntries(this.hass, { type: "helper" }),
(entry) => entry.entry_id
);
} }
private async _openEditDialog(ev: CustomEvent): Promise<void> { private async _openEditDialog(ev: CustomEvent): Promise<void> {
@@ -289,12 +196,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
} }
private _createHelpler() { private _createHelpler() {
showHelperDetailDialog(this, { showHelperDetailDialog(this);
dialogClosedCallback: (params) => {
if (params.flowFinished) {
this._getConfigEntries();
}
},
});
} }
} }

View File

@@ -1,20 +1,11 @@
import { fireEvent } from "../../../common/dom/fire_event"; 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 loadHelperDetailDialog = () => import("./dialog-helper-detail");
export interface ShowDialogHelperDetailParams { export const showHelperDetailDialog = (element: HTMLElement) => {
// Only used for config entries
dialogClosedCallback: DataEntryFlowDialogParams["dialogClosedCallback"];
}
export const showHelperDetailDialog = (
element: HTMLElement,
params: ShowDialogHelperDetailParams
) => {
fireEvent(element, "show-dialog", { fireEvent(element, "show-dialog", {
dialogTag: "dialog-helper-detail", dialogTag: "dialog-helper-detail",
dialogImport: loadHelperDetailDialog, dialogImport: loadHelperDetailDialog,
dialogParams: params, dialogParams: {},
}); });
}; };

View File

@@ -521,26 +521,24 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
} }
private _loadConfigEntries() { private _loadConfigEntries() {
getConfigEntries(this.hass, { type: "integration" }).then( getConfigEntries(this.hass).then((configEntries) => {
(configEntries) => { this._configEntries = configEntries
this._configEntries = configEntries .map(
.map( (entry: ConfigEntry): ConfigEntryExtended => ({
(entry: ConfigEntry): ConfigEntryExtended => ({ ...entry,
...entry, localized_domain_name: domainToName(
localized_domain_name: domainToName( this.hass.localize,
this.hass.localize, entry.domain
entry.domain ),
), })
}) )
.sort((conf1, conf2) =>
caseInsensitiveStringCompare(
conf1.localized_domain_name + conf1.title,
conf2.localized_domain_name + conf2.title
) )
.sort((conf1, conf2) => );
caseInsensitiveStringCompare( });
conf1.localized_domain_name + conf1.title,
conf2.localized_domain_name + conf2.title
)
);
}
);
} }
private async _scanUSBDevices() { private async _scanUSBDevices() {
@@ -658,7 +656,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
if (!domain) { if (!domain) {
return; return;
} }
const handlers = await getConfigFlowHandlers(this.hass, "integration"); const handlers = await getConfigFlowHandlers(this.hass);
if (!handlers.includes(domain)) { if (!handlers.includes(domain)) {
showAlertDialog(this, { showAlertDialog(this, {

View File

@@ -111,9 +111,7 @@ class HaPanelDevMqtt extends LitElement {
return; return;
} }
const configEntryId = searchParams.get("config_entry") as string; 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( const configEntry = configEntries.find(
(entry) => entry.entry_id === configEntryId (entry) => entry.entry_id === configEntryId
); );

View File

@@ -384,9 +384,7 @@ class ZWaveJSConfigDashboard extends LitElement {
if (!this.configEntryId) { if (!this.configEntryId) {
return; return;
} }
const configEntries = await getConfigEntries(this.hass, { const configEntries = await getConfigEntries(this.hass);
domain: "zwave_js",
});
this._configEntry = configEntries.find( this._configEntry = configEntries.find(
(entry) => entry.entry_id === this.configEntryId! (entry) => entry.entry_id === this.configEntryId!
); );
@@ -469,9 +467,7 @@ class ZWaveJSConfigDashboard extends LitElement {
if (!this.configEntryId) { if (!this.configEntryId) {
return; return;
} }
const configEntries = await getConfigEntries(this.hass, { const configEntries = await getConfigEntries(this.hass);
domain: "zwave_js",
});
const configEntry = configEntries.find( const configEntry = configEntries.find(
(entry) => entry.entry_id === this.configEntryId (entry) => entry.entry_id === this.configEntryId
); );

View File

@@ -1,12 +1,10 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { mdiSlopeUphill } from "@mdi/js";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/data-table/ha-data-table"; import "../../../components/data-table/ha-data-table";
import type { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; import type { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
import { subscribeEntityRegistry } from "../../../data/entity_registry"; import { subscribeEntityRegistry } from "../../../data/entity_registry";
@@ -26,7 +24,6 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { showFixStatisticsUnitsChangedDialog } from "./show-dialog-statistics-fix-units-changed"; import { showFixStatisticsUnitsChangedDialog } from "./show-dialog-statistics-fix-units-changed";
import { showFixStatisticsUnsupportedUnitMetadataDialog } from "./show-dialog-statistics-fix-unsupported-unit-meta"; import { showFixStatisticsUnsupportedUnitMetadataDialog } from "./show-dialog-statistics-fix-unsupported-unit-meta";
import { showStatisticsAdjustSumDialog } from "./show-dialog-statistics-adjust-sum";
const FIX_ISSUES_ORDER = { const FIX_ISSUES_ORDER = {
no_state: 0, no_state: 0,
@@ -114,30 +111,6 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
: ""}`, : ""}`,
width: "113px", width: "113px",
}, },
actions: {
title: "",
type: "overflow-menu",
template: (_info, statistic: StatisticsMetaData) =>
statistic.has_sum
? 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>`
: html``,
},
}) })
); );
@@ -212,8 +185,6 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
source: "", source: "",
state: this.hass.states[statisticId], state: this.hass.states[statisticId],
issues: issues[statisticId], issues: issues[statisticId],
has_sum: false,
has_mean: false,
}); });
} }
}); });

View File

@@ -1,166 +0,0 @@
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"; } from "../../../data/history";
import "../../../components/ha-formfield"; import "../../../components/ha-formfield";
import "../../../components/ha-radio"; import "../../../components/ha-radio";
import type { DialogStatisticsUnitsChangedParams } from "./show-dialog-statistics-fix-units-changed"; import { DialogStatisticsUnitsChangedParams } from "./show-dialog-statistics-fix-units-changed";
@customElement("dialog-statistics-fix-units-changed") @customElement("dialog-statistics-fix-units-changed")
export class DialogStatisticsFixUnitsChanged extends LitElement { export class DialogStatisticsFixUnitsChanged extends LitElement {

View File

@@ -8,7 +8,7 @@ import { HomeAssistant } from "../../../types";
import { updateStatisticsMetadata } from "../../../data/history"; import { updateStatisticsMetadata } from "../../../data/history";
import "../../../components/ha-formfield"; import "../../../components/ha-formfield";
import "../../../components/ha-radio"; import "../../../components/ha-radio";
import type { DialogStatisticsUnsupportedUnitMetaParams } from "./show-dialog-statistics-fix-unsupported-unit-meta"; import { DialogStatisticsUnsupportedUnitMetaParams } from "./show-dialog-statistics-fix-unsupported-unit-meta";
@customElement("dialog-statistics-fix-unsupported-unit-meta") @customElement("dialog-statistics-fix-unsupported-unit-meta")
export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement { export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {

View File

@@ -1,20 +0,0 @@
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

@@ -148,8 +148,8 @@ class LovelaceFullConfigEditor extends LitElement {
} }
app-toolbar { app-toolbar {
background-color: var(--app-header-edit-background-color, #455a64); background-color: var(--dark-background-color, #455a64);
color: var(--app-header-edit-text-color, #fff); color: var(--dark-text-color);
} }
mwc-button[disabled] { mwc-button[disabled] {

View File

@@ -228,11 +228,6 @@
"service": { "service": {
"run": "Run" "run": "Run"
}, },
"update": {
"installing": "Installing",
"installing_with_progress": "Installing ({progress}%)",
"up_to_date": "Up-to-date"
},
"timer": { "timer": {
"actions": { "actions": {
"start": "start", "start": "start",
@@ -724,14 +719,6 @@
"rising": "Rising", "rising": "Rising",
"setting": "Setting" "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": { "updater": {
"title": "Update Instructions" "title": "Update Instructions"
}, },
@@ -823,8 +810,7 @@
"area": "Set entity area only", "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.", "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", "follow_device_area": "Follow device area",
"change_device_area": "Change device area", "change_device_area": "Change device area"
"configure_state": "Configure State"
} }
}, },
"helper_settings": { "helper_settings": {
@@ -1079,9 +1065,9 @@
"learn_more": "Learn more" "learn_more": "Learn more"
}, },
"updates": { "updates": {
"no_update_entities": { "check_unavailable": {
"title": "Unable to check for updates", "title": "Unable to check for updates",
"description": "You do not have any integrations that provide updates." "description": "You need to run the Home Assistant operating system to be able to check and install updates from the Home Assistant user interface."
}, },
"check_updates": "Check for updates", "check_updates": "Check for updates",
"no_new_updates": "No new updates found", "no_new_updates": "No new updates found",
@@ -2405,7 +2391,7 @@
"add_entities_lovelace": "Add to dashboard", "add_entities_lovelace": "Add to dashboard",
"none": "This device has no entities", "none": "This device has no entities",
"show_less": "Show less", "show_less": "Show less",
"hidden_entities": "+{count} {count, plural,\n one {entity}\n other {entities}\n} not shown" "hidden_entities": "+{count} {count, plural,\n one {hidden entity}\n other {hidden entities}\n}"
}, },
"confirm_rename_entity_ids": "Do you also want to rename the entity IDs of your entities?", "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!", "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!",
@@ -2430,7 +2416,7 @@
"filter": { "filter": {
"filter": "Filter", "filter": "Filter",
"show_disabled": "Show disabled devices", "show_disabled": "Show disabled devices",
"hidden_devices": "{number} {number, plural,\n one {device}\n other {devices}\n} not shown", "hidden_devices": "{number} hidden {number, plural,\n one {device}\n other {devices}\n}",
"show_all": "Show all" "show_all": "Show all"
} }
} }
@@ -2449,7 +2435,7 @@
"show_disabled": "Show disabled entities", "show_disabled": "Show disabled entities",
"show_unavailable": "Show unavailable entities", "show_unavailable": "Show unavailable entities",
"show_readonly": "Show read-only entities", "show_readonly": "Show read-only entities",
"hidden_entities": "{number} {number, plural,\n one {entity}\n other {entities}\n} not shown", "hidden_entities": "{number} hidden {number, plural,\n one {entity}\n other {entities}\n}",
"show_all": "Show all" "show_all": "Show all"
}, },
"status": { "status": {

View File

@@ -2975,17 +2975,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mdi/js@npm:6.6.95": "@mdi/js@npm:6.5.95":
version: 6.6.95 version: 6.5.95
resolution: "@mdi/js@npm:6.6.95" resolution: "@mdi/js@npm:6.5.95"
checksum: 4cf8c48156f0e9ff67e4394cd428158bd164b1a6b7ca1aa70fc6a6aee91cfede9eba56720eb7d13fa57315ac636e9519a62dedd3cd2a9708aa11f2e3624ddbff checksum: b1db7713d216c119f584bf973514a2f9d8f2e671e91bf19ce8e56cfa7a9843c0a060328e794507ac31f2bded1032123294f39ff8e987ea5acb2719ab522ef146
languageName: node languageName: node
linkType: hard linkType: hard
"@mdi/svg@npm:6.6.95": "@mdi/svg@npm:6.5.95":
version: 6.6.95 version: 6.5.95
resolution: "@mdi/svg@npm:6.6.95" resolution: "@mdi/svg@npm:6.5.95"
checksum: 59b79db945847a3d981351418e0e7a457b831e09846fa751d44e80df8fb4cd19ef12bc889538ed2945d2638e522aa7ea5b1f97997e19dd68345f5d7bf5cad5e6 checksum: 2d45221d042d52d54c85eaf672a5f3697ed5201607fa38a6e235ee2e60d1c3c25d456a284f19ce47b5f06418cacfee29e8fecf6580b8c28538fd26044becaf1a
languageName: node languageName: node
linkType: hard linkType: hard
@@ -9048,8 +9048,8 @@ fsevents@^1.2.7:
"@material/mwc-textfield": 0.25.3 "@material/mwc-textfield": 0.25.3
"@material/mwc-top-app-bar-fixed": ^0.25.3 "@material/mwc-top-app-bar-fixed": ^0.25.3
"@material/top-app-bar": 14.0.0-canary.261f2db59.0 "@material/top-app-bar": 14.0.0-canary.261f2db59.0
"@mdi/js": 6.6.95 "@mdi/js": 6.5.95
"@mdi/svg": 6.6.95 "@mdi/svg": 6.5.95
"@open-wc/dev-server-hmr": ^0.0.2 "@open-wc/dev-server-hmr": ^0.0.2
"@polymer/app-layout": ^3.1.0 "@polymer/app-layout": ^3.1.0
"@polymer/iron-flex-layout": ^3.0.1 "@polymer/iron-flex-layout": ^3.0.1