Compare commits

..

1 Commits

Author SHA1 Message Date
Zack
723e6c46b4 Move Editing of Dashboard to config page 2022-05-10 10:35:31 -05:00
177 changed files with 3085 additions and 5326 deletions

View File

@@ -51,7 +51,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
<!-- <!--
Provide details about the versions you are using, which helps us reproducing Provide details about the versions you are using, which helps us reproducing
and finding the issue quicker. Version information is found in the and finding the issue quicker. Version information is found in the
Home Assistant frontend: Settings -> About. Home Assistant frontend: Configuration -> Info.
Browser version and operating system is important! Please try to replicate Browser version and operating system is important! Please try to replicate
your issue in a different browser and be sure to include your findings. your issue in a different browser and be sure to include your findings.

View File

@@ -64,7 +64,7 @@ body:
label: What version of Home Assistant Core has the issue? label: What version of Home Assistant Core has the issue?
placeholder: core- placeholder: core-
description: > description: >
Can be found in: [Settings -> About](https://my.home-assistant.io/redirect/info/). Can be found in the Configuration panel -> Info.
- type: input - type: input
attributes: attributes:
label: What was the last working version of Home Assistant Core? label: What was the last working version of Home Assistant Core?

View File

@@ -26,8 +26,8 @@ module.exports = {
}, },
version() { version() {
const version = fs const version = fs
.readFileSync(path.resolve(paths.polymer_dir, "pyproject.toml"), "utf8") .readFileSync(path.resolve(paths.polymer_dir, "setup.cfg"), "utf8")
.match(/version\W+=\W"(\d{8}\.\d)"/); .match(/version\W+=\W(\d{8}\.\d)/);
if (!version) { if (!version) {
throw Error("Version not found"); throw Error("Version not found");
} }

View File

@@ -194,7 +194,7 @@ export const demoLovelaceJimpower: DemoConfig["lovelace"] = () => ({
type: "state-icon", type: "state-icon",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "group.downstairs_lights", entity_id: "group.downstairs_lights",
}, },
service: "homeassistant.toggle", service: "homeassistant.toggle",

View File

@@ -377,7 +377,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
name: "AC bed", name: "AC bed",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "script.air_cleaner_quiet", entity_id: "script.air_cleaner_quiet",
}, },
service: "script.turn_on", service: "script.turn_on",
@@ -390,7 +390,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
name: "AC bed", name: "AC bed",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "script.air_cleaner_auto", entity_id: "script.air_cleaner_auto",
}, },
service: "script.turn_on", service: "script.turn_on",
@@ -403,7 +403,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
name: "AC bed", name: "AC bed",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "script.air_cleaner_turbo", entity_id: "script.air_cleaner_turbo",
}, },
service: "script.turn_on", service: "script.turn_on",
@@ -416,7 +416,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
name: "AC", name: "AC",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "script.ac_off", entity_id: "script.ac_off",
}, },
service: "script.turn_on", service: "script.turn_on",
@@ -429,7 +429,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
name: "AC", name: "AC",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "script.ac_on", entity_id: "script.ac_on",
}, },
service: "script.turn_on", service: "script.turn_on",
@@ -629,7 +629,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
entity: "scene.morning_lights", entity: "scene.morning_lights",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "scene.morning_lights", entity_id: "scene.morning_lights",
}, },
service: "scene.turn_on", service: "scene.turn_on",
@@ -641,7 +641,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
entity: "scene.movie_time", entity: "scene.movie_time",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "scene.movie_time", entity_id: "scene.movie_time",
}, },
service: "scene.turn_on", service: "scene.turn_on",
@@ -702,7 +702,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
entity: "light.downstairs_lights", entity: "light.downstairs_lights",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "light.downstairs_lights", entity_id: "light.downstairs_lights",
}, },
service: "light.toggle", service: "light.toggle",
@@ -714,7 +714,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
entity: "light.upstairs_lights", entity: "light.upstairs_lights",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "light.upstairs_lights", entity_id: "light.upstairs_lights",
}, },
service: "light.toggle", service: "light.toggle",

View File

@@ -1,4 +1,4 @@
import { format, startOfToday, startOfTomorrow } from "date-fns/esm"; import { format, startOfToday, startOfTomorrow } from "date-fns";
import { EnergySolarForecasts } from "../../../src/data/energy"; import { EnergySolarForecasts } from "../../../src/data/energy";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";

View File

@@ -4,7 +4,7 @@ import {
addMonths, addMonths,
differenceInHours, differenceInHours,
endOfDay, endOfDay,
} from "date-fns/esm"; } from "date-fns";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { StatisticValue } from "../../../src/data/history"; import { StatisticValue } from "../../../src/data/history";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";

View File

@@ -119,7 +119,7 @@ export const basicTrace: DemoTrace = {
params: { params: {
domain: "input_boolean", domain: "input_boolean",
service: "toggle", service: "toggle",
data: {}, service_data: {},
target: { target: {
entity_id: ["input_boolean.toggle_4"], entity_id: ["input_boolean.toggle_4"],
}, },
@@ -164,7 +164,7 @@ export const basicTrace: DemoTrace = {
params: { params: {
domain: "input_boolean", domain: "input_boolean",
service: "toggle", service: "toggle",
data: {}, service_data: {},
target: { target: {
entity_id: ["input_boolean.toggle_2"], entity_id: ["input_boolean.toggle_2"],
}, },
@@ -182,7 +182,7 @@ export const basicTrace: DemoTrace = {
params: { params: {
domain: "input_boolean", domain: "input_boolean",
service: "toggle", service: "toggle",
data: {}, service_data: {},
target: { target: {
entity_id: ["input_boolean.toggle_3"], entity_id: ["input_boolean.toggle_3"],
}, },
@@ -200,7 +200,7 @@ export const basicTrace: DemoTrace = {
params: { params: {
domain: "input_boolean", domain: "input_boolean",
service: "toggle", service: "toggle",
data: {}, service_data: {},
target: { target: {
entity_id: ["input_boolean.toggle_4"], entity_id: ["input_boolean.toggle_4"],
}, },
@@ -298,11 +298,11 @@ export const basicTrace: DemoTrace = {
source: "state of input_boolean.toggle_1", source: "state of input_boolean.toggle_1",
entity_id: "automation.toggle_toggles", entity_id: "automation.toggle_toggles",
context_id: "6cfcae368e7b3686fad6c59e83ae76c9", context_id: "6cfcae368e7b3686fad6c59e83ae76c9",
when: 1616647011.240832, when: "2021-03-25T04:36:51.240832+00:00",
domain: "automation", domain: "automation",
}, },
{ {
when: 1616647011.249828, when: "2021-03-25T04:36:51.249828+00:00",
name: "Toggle 4", name: "Toggle 4",
state: "on", state: "on",
entity_id: "input_boolean.toggle_4", entity_id: "input_boolean.toggle_4",
@@ -313,7 +313,7 @@ export const basicTrace: DemoTrace = {
context_name: "Ensure Party mode", context_name: "Ensure Party mode",
}, },
{ {
when: 1616647011.258947, when: "2021-03-25T04:36:51.258947+00:00",
name: "Toggle 2", name: "Toggle 2",
state: "on", state: "on",
entity_id: "input_boolean.toggle_2", entity_id: "input_boolean.toggle_2",
@@ -324,7 +324,7 @@ export const basicTrace: DemoTrace = {
context_name: "Ensure Party mode", context_name: "Ensure Party mode",
}, },
{ {
when: 1616647011.261806, when: "2021-03-25T04:36:51.261806+00:00",
name: "Toggle 3", name: "Toggle 3",
state: "off", state: "off",
entity_id: "input_boolean.toggle_3", entity_id: "input_boolean.toggle_3",
@@ -335,7 +335,7 @@ export const basicTrace: DemoTrace = {
context_name: "Ensure Party mode", context_name: "Ensure Party mode",
}, },
{ {
when: 1616647011.265246, when: "2021-03-25T04:36:51.265246+00:00",
name: "Toggle 4", name: "Toggle 4",
state: "off", state: "off",
entity_id: "input_boolean.toggle_4", entity_id: "input_boolean.toggle_4",

View File

@@ -185,11 +185,11 @@ export const motionLightTrace: DemoTrace = {
"has been triggered by state of binary_sensor.pauluss_macbook_pro_camera_in_use", "has been triggered by state of binary_sensor.pauluss_macbook_pro_camera_in_use",
source: "state of binary_sensor.pauluss_macbook_pro_camera_in_use", source: "state of binary_sensor.pauluss_macbook_pro_camera_in_use",
entity_id: "automation.auto_elgato", entity_id: "automation.auto_elgato",
when: 1615702021.768492, when: "2021-03-14T06:07:01.768492+00:00",
domain: "automation", domain: "automation",
}, },
{ {
when: 1615702021.872187, when: "2021-03-14T06:07:01.872187+00:00",
name: "Elgato Key Light Air", name: "Elgato Key Light Air",
state: "on", state: "on",
entity_id: "light.elgato_key_light_air", entity_id: "light.elgato_key_light_air",
@@ -200,7 +200,7 @@ export const motionLightTrace: DemoTrace = {
context_name: "Auto Elgato", context_name: "Auto Elgato",
}, },
{ {
when: 1615702073.284505, when: "2021-03-14T06:07:53.284505+00:00",
name: "Elgato Key Light Air", name: "Elgato Key Light Air",
state: "off", state: "off",
entity_id: "light.elgato_key_light_air", entity_id: "light.elgato_key_light_air",

View File

@@ -249,7 +249,7 @@ const CONFIGS = [
name: Bed light name: Bed light
action_name: Toggle light action_name: Toggle light
service: light.toggle service: light.toggle
data: service_data:
entity_id: light.bed_light entity_id: light.bed_light
- type: section - type: section
label: Links label: Links

View File

@@ -199,7 +199,7 @@ const CONFIGS = [
tap_action: tap_action:
action: call-service action: call-service
service: light.turn_on service: light.turn_on
data: service_data:
entity_id: light.ceiling_lights entity_id: light.ceiling_lights
- entity: sun.sun - entity: sun.sun
name: Regular name: Regular

View File

@@ -40,7 +40,7 @@ const CONFIGS = [
left: 90% left: 90%
padding: 0px padding: 0px
service: light.turn_off service: light.turn_off
data: service_data:
entity_id: group.all_lights entity_id: group.all_lights
- type: icon - type: icon
icon: mdi:cctv icon: mdi:cctv
@@ -88,7 +88,7 @@ const CONFIGS = [
left: 90% left: 90%
padding: 0px padding: 0px
service: light.turn_off service: light.turn_off
data: service_data:
entity_id: group.all_lights entity_id: group.all_lights
- type: icon - type: icon
icon: mdi:cctv icon: mdi:cctv

View File

@@ -17,10 +17,7 @@ import {
HassioAddonDetails, HassioAddonDetails,
} from "../../../src/data/hassio/addon"; } from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { import { setSupervisorOption } from "../../../src/data/hassio/supervisor";
fetchHassioSupervisorInfo,
setSupervisorOption,
} from "../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-error-screen"; import "../../../src/layouts/hass-error-screen";
@@ -172,10 +169,9 @@ class HassioAddonDashboard extends LitElement {
if (this.route.path === "") { if (this.route.path === "") {
const requestedAddon = extractSearchParam("addon"); const requestedAddon = extractSearchParam("addon");
const requestedAddonRepository = extractSearchParam("repository_url"); const requestedAddonRepository = extractSearchParam("repository_url");
if (requestedAddonRepository) {
const supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
if ( if (
!supervisorInfo.addons_repositories.find( requestedAddonRepository &&
!this.supervisor.supervisor.addons_repositories.find(
(repo) => repo === requestedAddonRepository (repo) => repo === requestedAddonRepository
) )
) { ) {
@@ -199,7 +195,7 @@ class HassioAddonDashboard extends LitElement {
try { try {
await setSupervisorOption(this.hass, { await setSupervisorOption(this.hass, {
addons_repositories: [ addons_repositories: [
...supervisorInfo.addons_repositories, ...this.supervisor.supervisor.addons_repositories,
requestedAddonRepository, requestedAddonRepository,
], ],
}); });
@@ -207,7 +203,6 @@ class HassioAddonDashboard extends LitElement {
this._error = extractApiErrorMessage(err); this._error = extractApiErrorMessage(err);
} }
} }
}
if (requestedAddon) { if (requestedAddon) {
const addonsInfo = await fetchHassioAddonsInfo(this.hass); const addonsInfo = await fetchHassioAddonsInfo(this.hass);

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.7.96", "@mdi/js": "6.6.95",
"@mdi/svg": "6.7.96", "@mdi/svg": "6.6.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,30 +1,3 @@
[build-system] [build-system]
requires = ["setuptools~=62.3", "wheel~=0.37.1"] requires = ["setuptools~=60.5", "wheel~=0.37.1"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20220525.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"
authors = [
{name = "The Home Assistant Authors", email = "hello@home-assistant.io"}
]
requires-python = ">=3.4.0"
[project.urls]
"Homepage" = "https://github.com/home-assistant/frontend"
[tool.setuptools]
platforms = ["any"]
zip-safe = false
include-package-data = true
[tool.setuptools.packages.find]
include = ["hass_frontend*"]
[tool.mypy]
python_version = 3.4
show_error_codes = true
strict = true

View File

@@ -50,14 +50,14 @@ async function main(args) {
return; return;
} }
const setup = fs.readFileSync("pyproject.toml", "utf8"); const setup = fs.readFileSync("setup.cfg", "utf8");
const version = setup.match(/version\W+=\W"(\d{8}\.\d)"/)[1]; const version = setup.match(/\d{8}\.\d+/)[0];
const newVersion = method(version); const newVersion = method(version);
console.log("Current version:", version); console.log("Current version:", version);
console.log("New version:", newVersion); console.log("New version:", newVersion);
fs.writeFileSync("pyproject.toml", setup.replace(version, newVersion), "utf-8"); fs.writeFileSync("setup.cfg", setup.replace(version, newVersion), "utf-8");
if (!commit) { if (!commit) {
return; return;

26
setup.cfg Normal file
View File

@@ -0,0 +1,26 @@
[metadata]
name = home-assistant-frontend
version = 20220504.0
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0
platforms = any
description = The Home Assistant frontend
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/home-assistant/frontend
[options]
packages = find:
zip_safe = False
include_package_data = True
python_requires = >= 3.4.0
[options.packages.find]
include =
hass_frontend*
[mypy]
python_version = 3.4
show_error_codes = True
strict = True

View File

@@ -1,41 +0,0 @@
const DEFAULT_OWN = true;
// Finds the closest ancestor of an element that has a specific optionally owned property,
// traversing slot and shadow root boundaries until the body element is reached
export const closestWithProperty = (
element: Element | null,
property: string | symbol,
own = DEFAULT_OWN
) => {
if (!element || element === document.body) return null;
element = element.assignedSlot ?? element;
if (element.parentElement) {
element = element.parentElement;
} else {
const root = element.getRootNode();
element = root instanceof ShadowRoot ? root.host : null;
}
if (
own
? Object.prototype.hasOwnProperty.call(element, property)
: element && property in element
)
return element;
return closestWithProperty(element, property, own);
};
// Finds the set of all such ancestors and includes starting element as first in the set
export const ancestorsWithProperty = (
element: Element | null,
property: string | symbol,
own = DEFAULT_OWN
) => {
const ancestors: Set<Element> = new Set();
while (element) {
ancestors.add(element);
element = closestWithProperty(element, property, own);
}
return ancestors;
};

View File

@@ -1,11 +1,6 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE_STATES } from "../../data/entity";
export const computeActiveState = (stateObj: HassEntity): string => { export const computeActiveState = (stateObj: HassEntity): string => {
if (UNAVAILABLE_STATES.includes(stateObj.state)) {
return stateObj.state;
}
const domain = stateObj.entity_id.split(".")[0]; const domain = stateObj.entity_id.split(".")[0];
let state = stateObj.state; let state = stateObj.state;

View File

@@ -2,74 +2,67 @@ 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 { import {
updateIsInstalling,
UpdateEntity,
UPDATE_SUPPORT_PROGRESS, UPDATE_SUPPORT_PROGRESS,
updateIsInstallingFromAttributes,
} from "../../data/update"; } 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, isNumericFromAttributes } from "../number/format_number"; import { formatNumber, isNumericState } from "../number/format_number";
import { LocalizeFunc } from "../translations/localize"; import { LocalizeFunc } from "../translations/localize";
import { supportsFeatureFromAttributes } from "./supports-feature"; import { computeStateDomain } from "./compute_state_domain";
import { supportsFeature } from "./supports-feature";
import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration"; import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration";
import { computeDomain } from "./compute_domain";
export const computeStateDisplay = ( export const computeStateDisplay = (
localize: LocalizeFunc, localize: LocalizeFunc,
stateObj: HassEntity, stateObj: HassEntity,
locale: FrontendLocaleData, locale: FrontendLocaleData,
state?: string state?: string
): string =>
computeStateDisplayFromEntityAttributes(
localize,
locale,
stateObj.entity_id,
stateObj.attributes,
state !== undefined ? state : stateObj.state
);
export const computeStateDisplayFromEntityAttributes = (
localize: LocalizeFunc,
locale: FrontendLocaleData,
entityId: string,
attributes: any,
state: string
): string => { ): string => {
if (state === UNKNOWN || state === UNAVAILABLE) { const compareState = state !== undefined ? state : stateObj.state;
return localize(`state.default.${state}`);
if (compareState === UNKNOWN || compareState === UNAVAILABLE) {
return localize(`state.default.${compareState}`);
} }
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber` // Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
if (isNumericFromAttributes(attributes)) { if (isNumericState(stateObj)) {
// state is duration // state is duration
if ( if (
attributes.device_class === "duration" && stateObj.attributes.device_class === "duration" &&
attributes.unit_of_measurement && stateObj.attributes.unit_of_measurement &&
UNIT_TO_SECOND_CONVERT[attributes.unit_of_measurement] UNIT_TO_SECOND_CONVERT[stateObj.attributes.unit_of_measurement]
) { ) {
try { try {
return formatDuration(state, attributes.unit_of_measurement); return formatDuration(
compareState,
stateObj.attributes.unit_of_measurement
);
} catch (_err) { } catch (_err) {
// fallback to default // fallback to default
} }
} }
if (attributes.device_class === "monetary") { if (stateObj.attributes.device_class === "monetary") {
try { try {
return formatNumber(state, locale, { return formatNumber(compareState, locale, {
style: "currency", style: "currency",
currency: attributes.unit_of_measurement, currency: stateObj.attributes.unit_of_measurement,
minimumFractionDigits: 2, minimumFractionDigits: 2,
}); });
} catch (_err) { } catch (_err) {
// fallback to default // fallback to default
} }
} }
return `${formatNumber(state, locale)}${ return `${formatNumber(compareState, locale)}${
attributes.unit_of_measurement ? " " + attributes.unit_of_measurement : "" stateObj.attributes.unit_of_measurement
? " " + stateObj.attributes.unit_of_measurement
: ""
}`; }`;
} }
const domain = computeDomain(entityId); const domain = computeStateDomain(stateObj);
if (domain === "input_datetime") { if (domain === "input_datetime") {
if (state !== undefined) { if (state !== undefined) {
@@ -104,32 +97,36 @@ export const computeStateDisplayFromEntityAttributes = (
} else { } else {
// If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format. // If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format.
let date: Date; let date: Date;
if (attributes.has_date && attributes.has_time) { if (stateObj.attributes.has_date && stateObj.attributes.has_time) {
date = new Date( date = new Date(
attributes.year, stateObj.attributes.year,
attributes.month - 1, stateObj.attributes.month - 1,
attributes.day, stateObj.attributes.day,
attributes.hour, stateObj.attributes.hour,
attributes.minute stateObj.attributes.minute
); );
return formatDateTime(date, locale); return formatDateTime(date, locale);
} }
if (attributes.has_date) { if (stateObj.attributes.has_date) {
date = new Date(attributes.year, attributes.month - 1, attributes.day); date = new Date(
stateObj.attributes.year,
stateObj.attributes.month - 1,
stateObj.attributes.day
);
return formatDate(date, locale); return formatDate(date, locale);
} }
if (attributes.has_time) { if (stateObj.attributes.has_time) {
date = new Date(); date = new Date();
date.setHours(attributes.hour, attributes.minute); date.setHours(stateObj.attributes.hour, stateObj.attributes.minute);
return formatTime(date, locale); return formatTime(date, locale);
} }
return state; return stateObj.state;
} }
} }
if (domain === "humidifier") { if (domain === "humidifier") {
if (state === "on" && attributes.humidity) { if (compareState === "on" && stateObj.attributes.humidity) {
return `${attributes.humidity} %`; return `${stateObj.attributes.humidity} %`;
} }
} }
@@ -139,7 +136,7 @@ export const computeStateDisplayFromEntityAttributes = (
domain === "number" || domain === "number" ||
domain === "input_number" domain === "input_number"
) { ) {
return formatNumber(state, locale); return formatNumber(compareState, locale);
} }
// state of button is a timestamp // state of button is a timestamp
@@ -147,12 +144,12 @@ export const computeStateDisplayFromEntityAttributes = (
domain === "button" || domain === "button" ||
domain === "input_button" || domain === "input_button" ||
domain === "scene" || domain === "scene" ||
(domain === "sensor" && attributes.device_class === "timestamp") (domain === "sensor" && stateObj.attributes.device_class === "timestamp")
) { ) {
try { try {
return formatDateTime(new Date(state), locale); return formatDateTime(new Date(compareState), locale);
} catch (_err) { } catch (_err) {
return state; return compareState;
} }
} }
@@ -163,28 +160,30 @@ export const computeStateDisplayFromEntityAttributes = (
// When the latest version is skipped, show the latest 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, show "Up-to-date"
// When update is not available and there is no latest_version show "Unavailable" // When update is not available and there is no latest_version show "Unavailable"
return state === "on" return compareState === "on"
? updateIsInstallingFromAttributes(attributes) ? updateIsInstalling(stateObj as UpdateEntity)
? supportsFeatureFromAttributes(attributes, UPDATE_SUPPORT_PROGRESS) ? supportsFeature(stateObj, UPDATE_SUPPORT_PROGRESS)
? localize("ui.card.update.installing_with_progress", { ? localize("ui.card.update.installing_with_progress", {
progress: attributes.in_progress, progress: stateObj.attributes.in_progress,
}) })
: localize("ui.card.update.installing") : localize("ui.card.update.installing")
: attributes.latest_version : stateObj.attributes.latest_version
: attributes.skipped_version === attributes.latest_version : stateObj.attributes.skipped_version ===
? attributes.latest_version ?? localize("state.default.unavailable") stateObj.attributes.latest_version
? stateObj.attributes.latest_version ??
localize("state.default.unavailable")
: localize("ui.card.update.up_to_date"); : localize("ui.card.update.up_to_date");
} }
return ( return (
// Return device class translation // Return device class translation
(attributes.device_class && (stateObj.attributes.device_class &&
localize( localize(
`component.${domain}.state.${attributes.device_class}.${state}` `component.${domain}.state.${stateObj.attributes.device_class}.${compareState}`
)) || )) ||
// Return default translation // Return default translation
localize(`component.${domain}.state._.${state}`) || localize(`component.${domain}.state._.${compareState}`) ||
// We don't know! Return the raw state. // We don't know! Return the raw state.
state compareState
); );
}; };

View File

@@ -1,13 +1,7 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { computeObjectId } from "./compute_object_id"; import { computeObjectId } from "./compute_object_id";
export const computeStateNameFromEntityAttributes = (
entityId: string,
attributes: { [key: string]: any }
): string =>
attributes.friendly_name === undefined
? computeObjectId(entityId).replace(/_/g, " ")
: attributes.friendly_name || "";
export const computeStateName = (stateObj: HassEntity): string => export const computeStateName = (stateObj: HassEntity): string =>
computeStateNameFromEntityAttributes(stateObj.entity_id, stateObj.attributes); stateObj.attributes.friendly_name === undefined
? computeObjectId(stateObj.entity_id).replace(/_/g, " ")
: stateObj.attributes.friendly_name || "";

View File

@@ -29,8 +29,7 @@ import {
mdiWeatherNight, mdiWeatherNight,
} from "@mdi/js"; } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { UpdateEntity, updateIsInstalling } from "../../data/update"; import { updateIsInstalling, UpdateEntity } from "../../data/update";
import { weatherIcon } from "../../data/weather";
/** /**
* Return the icon to be used for a domain. * Return the icon to be used for a domain.
* *
@@ -47,20 +46,6 @@ export const domainIcon = (
stateObj?: HassEntity, stateObj?: HassEntity,
state?: string state?: string
): string => { ): string => {
const icon = domainIconWithoutDefault(domain, stateObj, state);
if (icon) {
return icon;
}
// eslint-disable-next-line
console.warn(`Unable to find icon for domain ${domain}`);
return DEFAULT_DOMAIN_ICON;
};
export const domainIconWithoutDefault = (
domain: string,
stateObj?: HassEntity,
state?: string
): string | undefined => {
const compareState = state !== undefined ? state : stateObj?.state; const compareState = state !== undefined ? state : stateObj?.state;
switch (domain) { switch (domain) {
@@ -102,15 +87,6 @@ export const domainIconWithoutDefault = (
? mdiCheckCircleOutline ? mdiCheckCircleOutline
: mdiCloseCircleOutline; : mdiCloseCircleOutline;
case "input_datetime":
if (!stateObj?.attributes.has_date) {
return mdiClock;
}
if (!stateObj.attributes.has_time) {
return mdiCalendar;
}
break;
case "lock": case "lock":
switch (compareState) { switch (compareState) {
case "unlocked": case "unlocked":
@@ -148,6 +124,15 @@ export const domainIconWithoutDefault = (
break; break;
} }
case "input_datetime":
if (!stateObj?.attributes.has_date) {
return mdiClock;
}
if (!stateObj.attributes.has_time) {
return mdiCalendar;
}
break;
case "sun": case "sun":
return stateObj?.state === "above_horizon" return stateObj?.state === "above_horizon"
? FIXED_DOMAIN_ICONS[domain] ? FIXED_DOMAIN_ICONS[domain]
@@ -159,14 +144,13 @@ export const domainIconWithoutDefault = (
? mdiPackageDown ? mdiPackageDown
: mdiPackageUp : mdiPackageUp
: mdiPackage; : mdiPackage;
case "weather":
return weatherIcon(stateObj?.state);
} }
if (domain in FIXED_DOMAIN_ICONS) { if (domain in FIXED_DOMAIN_ICONS) {
return FIXED_DOMAIN_ICONS[domain]; return FIXED_DOMAIN_ICONS[domain];
} }
return undefined; // eslint-disable-next-line
console.warn(`Unable to find icon for domain ${domain}`);
return DEFAULT_DOMAIN_ICON;
}; };

View File

@@ -3,13 +3,6 @@ import { HassEntity } from "home-assistant-js-websocket";
export const supportsFeature = ( export const supportsFeature = (
stateObj: HassEntity, stateObj: HassEntity,
feature: number feature: number
): boolean => supportsFeatureFromAttributes(stateObj.attributes, feature);
export const supportsFeatureFromAttributes = (
attributes: {
[key: string]: any;
},
feature: number
): boolean => ): boolean =>
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
(attributes.supported_features! & feature) !== 0; (stateObj.attributes.supported_features! & feature) !== 0;

View File

@@ -7,11 +7,8 @@ import { round } from "./round";
* @param stateObj The entity state object * @param stateObj The entity state object
*/ */
export const isNumericState = (stateObj: HassEntity): boolean => export const isNumericState = (stateObj: HassEntity): boolean =>
isNumericFromAttributes(stateObj.attributes); !!stateObj.attributes.unit_of_measurement ||
!!stateObj.attributes.state_class;
export const isNumericFromAttributes = (attributes: {
[key: string]: any;
}): boolean => !!attributes.unit_of_measurement || !!attributes.state_class;
export const numberFormatToLocale = ( export const numberFormatToLocale = (
localeOptions: FrontendLocaleData localeOptions: FrontendLocaleData

View File

@@ -13,7 +13,7 @@ export const throttle = <T extends any[]>(
) => { ) => {
let timeout: number | undefined; let timeout: number | undefined;
let previous = 0; let previous = 0;
const throttledFunc = (...args: T): void => { return (...args: T): void => {
const later = () => { const later = () => {
previous = leading === false ? 0 : Date.now(); previous = leading === false ? 0 : Date.now();
timeout = undefined; timeout = undefined;
@@ -35,10 +35,4 @@ export const throttle = <T extends any[]>(
timeout = window.setTimeout(later, remaining); timeout = window.setTimeout(later, remaining);
} }
}; };
throttledFunc.cancel = () => {
clearTimeout(timeout);
timeout = undefined;
previous = 0;
};
return throttledFunc;
}; };

View File

@@ -34,7 +34,7 @@ import {
endOfMonth, endOfMonth,
endOfQuarter, endOfQuarter,
endOfYear, endOfYear,
} from "date-fns/esm"; } from "date-fns";
import { import {
formatDate, formatDate,
formatDateMonth, formatDateMonth,

View File

@@ -20,7 +20,7 @@ interface HassEntityWithCachedName extends HassEntity {
friendly_name: string; friendly_name: string;
} }
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean; export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
// eslint-disable-next-line lit/prefer-static-styles // eslint-disable-next-line lit/prefer-static-styles
const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) => const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) =>

View File

@@ -1,22 +1,17 @@
import type { Button } from "@material/mwc-button";
import "@material/mwc-menu"; import "@material/mwc-menu";
import type { Corner, Menu, MenuCorner } from "@material/mwc-menu"; import type { Corner, Menu, MenuCorner } from "@material/mwc-menu";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
import type { HaIconButton } from "./ha-icon-button";
@customElement("ha-button-menu") @customElement("ha-button-menu")
export class HaButtonMenu extends LitElement { export class HaButtonMenu extends LitElement {
protected readonly [FOCUS_TARGET];
@property() public corner: Corner = "TOP_START"; @property() public corner: Corner = "TOP_START";
@property() public menuCorner: MenuCorner = "START"; @property() public menuCorner: MenuCorner = "START";
@property({ type: Number }) public x: number | null = null; @property({ type: Number }) public x?: number;
@property({ type: Number }) public y: number | null = null; @property({ type: Number }) public y?: number;
@property({ type: Boolean }) public multi = false; @property({ type: Boolean }) public multi = false;
@@ -36,18 +31,10 @@ export class HaButtonMenu extends LitElement {
return this._menu?.selected; return this._menu?.selected;
} }
public override focus() {
if (this._menu?.open) {
this._menu.focusItemAtIndex(0);
} else {
this._triggerButton?.focus();
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div @click=${this._handleClick}> <div @click=${this._handleClick}>
<slot name="trigger" @slotchange=${this._setTriggerAria}></slot> <slot name="trigger"></slot>
</div> </div>
<mwc-menu <mwc-menu
.corner=${this.corner} .corner=${this.corner}
@@ -63,21 +50,6 @@ export class HaButtonMenu extends LitElement {
`; `;
} }
protected firstUpdated(changedProps): void {
super.firstUpdated(changedProps);
if (document.dir === "rtl") {
this.updateComplete.then(() => {
this.querySelectorAll("mwc-list-item").forEach((item) => {
const style = document.createElement("style");
style.innerHTML =
"span.material-icons:first-of-type { margin-left: var(--mdc-list-item-graphic-margin, 32px) !important; margin-right: 0px !important;}";
item!.shadowRoot!.appendChild(style);
});
});
}
}
private _handleClick(): void { private _handleClick(): void {
if (this.disabled) { if (this.disabled) {
return; return;
@@ -86,18 +58,6 @@ export class HaButtonMenu extends LitElement {
this._menu!.show(); this._menu!.show();
} }
private get _triggerButton() {
return this.querySelector(
'ha-icon-button[slot="trigger"], mwc-button[slot="trigger"]'
) as HaIconButton | Button | null;
}
private _setTriggerAria() {
if (this._triggerButton) {
this._triggerButton.ariaHasPopup = "menu";
}
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host { :host {

View File

@@ -66,13 +66,9 @@ export class HaChip extends LitElement {
line-height: 14px; line-height: 14px;
color: var(--ha-chip-icon-color, var(--ha-chip-text-color)); color: var(--ha-chip-icon-color, var(--ha-chip-text-color));
} }
.mdc-chip.mdc-chip--selected .mdc-chip__checkmark,
.mdc-chip.no-text .mdc-chip.no-text
.mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) { .mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) {
margin-right: -4px; margin-right: -4px;
margin-inline-start: -4px;
margin-inline-end: 4px;
direction: var(--direction);
} }
span[role="gridcell"] { span[role="gridcell"] {

View File

@@ -60,18 +60,15 @@ export class HaClickableListItem extends ListItemBase {
padding-right: var(--mdc-list-side-padding, 20px); padding-right: var(--mdc-list-side-padding, 20px);
overflow: hidden; overflow: hidden;
} }
:host-context([style*="direction: rtl;"])
span.material-icons:first-of-type { span.material-icons:first-of-type {
margin-inline-start: 0px !important; margin-left: var(--mdc-list-item-graphic-margin, 16px) !important;
margin-inline-end: var( margin-right: 0px !important;
--mdc-list-item-graphic-margin,
16px
) !important;
direction: var(--direction);
} }
:host-context([style*="direction: rtl;"])
span.material-icons:last-of-type { span.material-icons:last-of-type {
margin-inline-start: auto !important; margin-left: 0px !important;
margin-inline-end: 0px !important; margin-right: auto !important;
direction: var(--direction);
} }
`, `,
]; ];

View File

@@ -3,7 +3,6 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { formatNumber } from "../common/number/format_number"; import { formatNumber } from "../common/number/format_number";
import { CLIMATE_PRESET_NONE } from "../data/climate"; import { CLIMATE_PRESET_NONE } from "../data/climate";
import { UNAVAILABLE_STATES } from "../data/entity";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
@customElement("ha-climate-state") @customElement("ha-climate-state")
@@ -16,7 +15,7 @@ class HaClimateState extends LitElement {
const currentStatus = this._computeCurrentStatus(); const currentStatus = this._computeCurrentStatus();
return html`<div class="target"> return html`<div class="target">
${!UNAVAILABLE_STATES.includes(this.stateObj.state) ${this.stateObj.state !== "unknown"
? html`<span class="state-label"> ? html`<span class="state-label">
${this._localizeState()} ${this._localizeState()}
${this.stateObj.attributes.preset_mode && ${this.stateObj.attributes.preset_mode &&
@@ -26,12 +25,12 @@ class HaClimateState extends LitElement {
`state_attributes.climate.preset_mode.${this.stateObj.attributes.preset_mode}` `state_attributes.climate.preset_mode.${this.stateObj.attributes.preset_mode}`
) || this.stateObj.attributes.preset_mode}` ) || this.stateObj.attributes.preset_mode}`
: ""} : ""}
</span> </span>`
<div class="unit">${this._computeTarget()}</div>` : ""}
: this._localizeState()} <div class="unit">${this._computeTarget()}</div>
</div> </div>
${currentStatus && !UNAVAILABLE_STATES.includes(this.stateObj.state) ${currentStatus
? html`<div class="current"> ? html`<div class="current">
${this.hass.localize("ui.card.climate.currently")}: ${this.hass.localize("ui.card.climate.currently")}:
<div class="unit">${currentStatus}</div> <div class="unit">${currentStatus}</div>
@@ -109,10 +108,6 @@ class HaClimateState extends LitElement {
} }
private _localizeState(): string { private _localizeState(): string {
if (UNAVAILABLE_STATES.includes(this.stateObj.state)) {
return this.hass.localize(`state.default.${this.stateObj.state}`);
}
const stateString = this.hass.localize( const stateString = this.hass.localize(
`component.climate.state._.${this.stateObj.state}` `component.climate.state._.${this.stateObj.state}`
); );

View File

@@ -241,9 +241,6 @@ export class HaComboBox extends LitElement {
.toggle-button { .toggle-button {
right: 12px; right: 12px;
top: -10px; top: -10px;
inset-inline-start: initial;
inset-inline-end: 12px;
direction: var(--direction);
} }
:host([opened]) .toggle-button { :host([opened]) .toggle-button {
color: var(--primary-color); color: var(--primary-color);
@@ -252,9 +249,18 @@ export class HaComboBox extends LitElement {
--mdc-icon-size: 20px; --mdc-icon-size: 20px;
top: -7px; top: -7px;
right: 36px; right: 36px;
inset-inline-start: initial; }
inset-inline-end: 36px;
direction: var(--direction); :host-context([style*="direction: rtl;"]) .toggle-button {
left: 12px;
right: auto;
top: -10px;
}
:host-context([style*="direction: rtl;"]) .clear-button {
--mdc-icon-size: 20px;
top: -7px;
left: 36px;
right: auto;
} }
`; `;
} }

View File

@@ -140,9 +140,6 @@ export class HaDateRangePicker extends LitElement {
return css` return css`
ha-svg-icon { ha-svg-icon {
margin-right: 8px; margin-right: 8px;
margin-inline-end: 8px;
margin-inline-start: initial;
direction: var(--direction);
} }
.date-range-inputs { .date-range-inputs {
@@ -169,9 +166,6 @@ export class HaDateRangePicker extends LitElement {
ha-textfield:last-child { ha-textfield:last-child {
margin-left: 8px; margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
direction: var(--direction);
} }
@media only screen and (max-width: 800px) { @media only screen and (max-width: 800px) {

View File

@@ -3,8 +3,8 @@ import { styles } from "@material/mwc-dialog/mwc-dialog.css";
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import { css, html, TemplateResult } from "lit"; import { css, html, TemplateResult } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { computeRTLDirection } from "../common/util/compute_rtl";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
import "./ha-icon-button"; import "./ha-icon-button";
export const createCloseHeading = ( export const createCloseHeading = (
@@ -17,13 +17,12 @@ export const createCloseHeading = (
.path=${mdiClose} .path=${mdiClose}
dialogAction="close" dialogAction="close"
class="header_button" class="header_button"
dir=${computeRTLDirection(hass)}
></ha-icon-button> ></ha-icon-button>
`; `;
@customElement("ha-dialog") @customElement("ha-dialog")
export class HaDialog extends DialogBase { export class HaDialog extends DialogBase {
protected readonly [FOCUS_TARGET];
public scrollToPos(x: number, y: number) { public scrollToPos(x: number, y: number) {
this.contentElement?.scrollTo(x, y); this.contentElement?.scrollTo(x, y);
} }
@@ -90,18 +89,18 @@ export class HaDialog extends DialogBase {
} }
.header_title { .header_title {
margin-right: 40px; margin-right: 40px;
margin-inline-end: 40px;
direction: var(--direction);
} }
.header_button { [dir="rtl"].header_button {
inset-inline-start: initial; right: auto;
inset-inline-end: 16px; left: 16px;
direction: var(--direction);
} }
.dialog-actions { [dir="rtl"].header_title {
inset-inline-start: initial !important; margin-left: 40px;
inset-inline-end: 0px !important; margin-right: 0px;
direction: var(--direction); }
:host-context([style*="direction: rtl;"]) .dialog-actions {
left: 0px !important;
right: auto !important;
} }
`, `,
]; ];

View File

@@ -133,9 +133,6 @@ class HaExpansionPanel extends LitElement {
.summary-icon { .summary-icon {
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1); transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
margin-left: auto; margin-left: auto;
margin-inline-start: auto;
margin-inline-end: initial;
direction: var(--direction);
} }
.summary-icon.expanded { .summary-icon.expanded {

View File

@@ -1,25 +1,24 @@
import { FabBase } from "@material/mwc-fab/mwc-fab-base"; import { Fab } from "@material/mwc-fab";
import { styles } from "@material/mwc-fab/mwc-fab.css";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { css } from "lit"; import { css } from "lit";
@customElement("ha-fab") @customElement("ha-fab")
export class HaFab extends FabBase { export class HaFab extends Fab {
protected firstUpdated(changedProperties) { protected firstUpdated(changedProperties) {
super.firstUpdated(changedProperties); super.firstUpdated(changedProperties);
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)"); this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
} }
static override styles = [ static override styles = Fab.styles.concat([
styles,
css` css`
:host .mdc-fab--extended .mdc-fab__icon { :host-context([style*="direction: rtl;"])
margin-inline-start: -8px; .mdc-fab--extended
margin-inline-end: 12px; .mdc-fab__icon {
direction: var(--direction); margin-left: 12px !important;
margin-right: calc(12px - 20px) !important;
} }
`, `,
]; ]);
} }
declare global { declare global {

View File

@@ -175,23 +175,24 @@ export class HaFileUpload extends LitElement {
} }
.mdc-text-field__icon--leading { .mdc-text-field__icon--leading {
margin-bottom: 12px; margin-bottom: 12px;
inset-inline-start: initial; }
inset-inline-end: 0px; :host-context([style*="direction: rtl;"])
direction: var(--direction); .mdc-text-field__icon--leading {
margin-right: 0px;
} }
.mdc-text-field--filled .mdc-floating-label--float-above { .mdc-text-field--filled .mdc-floating-label--float-above {
transform: scale(0.75); transform: scale(0.75);
top: 8px; top: 8px;
} }
.mdc-floating-label { :host-context([style*="direction: rtl;"]) .mdc-floating-label {
inset-inline-start: 16px !important; left: initial;
inset-inline-end: initial !important; right: 16px;
direction: var(--direction);
} }
.mdc-text-field--filled .mdc-floating-label { :host-context([style*="direction: rtl;"])
inset-inline-start: 48px !important; .mdc-text-field--filled
inset-inline-end: initial !important; .mdc-floating-label {
direction: var(--direction); left: initial;
right: 48px;
} }
.dragged:before { .dragged:before {
position: var(--layout-fit_-_position); position: var(--layout-fit_-_position);

View File

@@ -133,10 +133,9 @@ export class HaFormString extends LitElement implements HaFormElement {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
ha-icon-button { :host-context([style*="direction: rtl;"]) ha-icon-button {
inset-inline-start: initial; right: auto;
inset-inline-end: 12px; left: 12px;
direction: var(--direction);
} }
`; `;
} }

View File

@@ -1,8 +1,6 @@
import "@material/mwc-icon-button"; import "@material/mwc-icon-button";
import type { IconButton } from "@material/mwc-icon-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import "./ha-svg-icon"; import "./ha-svg-icon";
@customElement("ha-icon-button") @customElement("ha-icon-button")
@@ -13,32 +11,21 @@ export class HaIconButton extends LitElement {
@property({ type: String }) path?: string; @property({ type: String }) path?: string;
// Label that is used for ARIA support and as tooltip // Label that is used for ARIA support and as tooltip
@property({ type: String }) label?: string; @property({ type: String }) label = "";
// These should always be set as properties, not attributes,
// so that only the <button> element gets the attribute
@property({ type: String, attribute: "aria-haspopup" })
override ariaHasPopup!: IconButton["ariaHasPopup"];
@property({ type: Boolean }) hideTitle = false; @property({ type: Boolean }) hideTitle = false;
@query("mwc-icon-button", true) private _button?: IconButton;
public override focus() {
this._button?.focus();
}
static shadowRootOptions: ShadowRootInit = { static shadowRootOptions: ShadowRootInit = {
mode: "open", mode: "open",
delegatesFocus: true, delegatesFocus: true,
}; };
protected render(): TemplateResult { protected render(): TemplateResult {
// Note: `ariaLabel` required despite the `mwc-icon-button` docs saying `label` should be enough
return html` return html`
<mwc-icon-button <mwc-icon-button
aria-label=${ifDefined(this.label)} .ariaLabel=${this.label}
title=${ifDefined(this.hideTitle ? undefined : this.label)} .title=${this.hideTitle ? "" : this.label}
aria-haspopup=${ifDefined(this.ariaHasPopup)}
.disabled=${this.disabled} .disabled=${this.disabled}
> >
${this.path ${this.path

View File

@@ -47,18 +47,9 @@ export class HaSelect extends SelectBase {
.mdc-select__anchor { .mdc-select__anchor {
width: var(--ha-select-min-width, 200px); width: var(--ha-select-min-width, 200px);
} }
.mdc-select--filled .mdc-floating-label { :host-context([style*="direction: rtl;"]) .mdc-floating-label {
inset-inline-start: 12px; right: 16px !important;
inset-inline-end: initial; left: initial !important;
direction: var(--direction);
}
.mdc-select .mdc-select__anchor {
padding-inline-start: 12px;
padding-inline-end: 0px;
direction: var(--direction);
}
.mdc-select__anchor .mdc-floating-label--float-above {
transform-origin: var(--float-start);
} }
`, `,
]; ];

View File

@@ -1,24 +1,15 @@
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
import { DeviceRegistryEntry } from "../../data/device_registry"; import { DeviceRegistryEntry } from "../../data/device_registry";
import { import { EntityRegistryEntry } from "../../data/entity_registry";
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import {
EntitySources,
fetchEntitySourcesWithCache,
} from "../../data/entity_sources";
import { AreaSelector } from "../../data/selector"; import { AreaSelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-area-picker"; import "../ha-area-picker";
import "../ha-areas-picker"; import "../ha-areas-picker";
@customElement("ha-selector-area") @customElement("ha-selector-area")
export class HaAreaSelector extends SubscribeMixin(LitElement) { export class HaAreaSelector extends LitElement {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@property() public selector!: AreaSelector; @property() public selector!: AreaSelector;
@@ -29,44 +20,29 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
@property() public helper?: string; @property() public helper?: string;
@state() private _entitySources?: EntitySources; @state() public _configEntries?: ConfigEntry[];
@state() private _entities?: EntityRegistryEntry[];
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true; @property({ type: Boolean }) public required = true;
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities.filter((entity) => entity.device_id !== null);
}),
];
}
protected updated(changedProperties) { protected updated(changedProperties) {
if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector");
if ( if (
changedProperties.has("selector") && oldSelector !== this.selector &&
(this.selector.area.device?.integration || this.selector.area.device?.integration
this.selector.area.entity?.integration) &&
!this._entitySources
) { ) {
fetchEntitySourcesWithCache(this.hass).then((sources) => { getConfigEntries(this.hass, {
this._entitySources = sources; domain: this.selector.area.device.integration,
}).then((entries) => {
this._configEntries = entries;
}); });
} }
} }
}
protected render() { protected render() {
if (
(this.selector.area.device?.integration ||
this.selector.area.entity?.integration) &&
!this._entitySources
) {
return html``;
}
if (!this.selector.area.multiple) { if (!this.selector.area.multiple) {
return html` return html`
<ha-area-picker <ha-area-picker
@@ -111,62 +87,39 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
} }
private _filterEntities = (entity: EntityRegistryEntry): boolean => { private _filterEntities = (entity: EntityRegistryEntry): boolean => {
const filterIntegration = this.selector.area.entity?.integration; if (this.selector.area.entity?.integration) {
if ( if (entity.platform !== this.selector.area.entity.integration) {
filterIntegration &&
this._entitySources?.[entity.entity_id]?.domain !== filterIntegration
) {
return false; return false;
} }
}
return true; return true;
}; };
private _filterDevices = (device: DeviceRegistryEntry): boolean => { private _filterDevices = (device: DeviceRegistryEntry): boolean => {
if (!this.selector.area.device) { if (
return true; this.selector.area.device?.manufacturer &&
} device.manufacturer !== this.selector.area.device.manufacturer
) {
const {
manufacturer: filterManufacturer,
model: filterModel,
integration: filterIntegration,
} = this.selector.area.device;
if (filterManufacturer && device.manufacturer !== filterManufacturer) {
return false; return false;
} }
if (filterModel && device.model !== filterModel) { if (
this.selector.area.device?.model &&
device.model !== this.selector.area.device.model
) {
return false; return false;
} }
if (filterIntegration && this._entitySources && this._entities) { if (this.selector.area.device?.integration) {
const deviceIntegrations = this._deviceIntegrations( if (
this._entitySources, this._configEntries &&
this._entities !this._configEntries.some((entry) =>
); device.config_entries.includes(entry.entry_id)
if (!deviceIntegrations?.[device.id]?.includes(filterIntegration)) { )
) {
return false; return false;
} }
} }
return true; return true;
}; };
private _deviceIntegrations = memoizeOne(
(entitySources: EntitySources, entities: EntityRegistryEntry[]) => {
const deviceIntegrations: Record<string, string[]> = {};
for (const entity of entities) {
const source = entitySources[entity.entity_id];
if (!source?.domain) {
continue;
}
if (!deviceIntegrations[entity.device_id!]) {
deviceIntegrations[entity.device_id!] = [];
}
deviceIntegrations[entity.device_id!].push(source.domain);
}
return deviceIntegrations;
}
);
} }
declare global { declare global {

View File

@@ -1,33 +1,18 @@
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
import { ConfigEntry } from "../../data/config_entries";
import type { DeviceRegistryEntry } from "../../data/device_registry"; import type { DeviceRegistryEntry } from "../../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import {
EntitySources,
fetchEntitySourcesWithCache,
} from "../../data/entity_sources";
import type { DeviceSelector } from "../../data/selector"; import type { DeviceSelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../device/ha-device-picker"; import "../device/ha-device-picker";
import "../device/ha-devices-picker"; import "../device/ha-devices-picker";
@customElement("ha-selector-device") @customElement("ha-selector-device")
export class HaDeviceSelector extends SubscribeMixin(LitElement) { export class HaDeviceSelector extends LitElement {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@property() public selector!: DeviceSelector; @property() public selector!: DeviceSelector;
@state() private _entitySources?: EntitySources;
@state() private _entities?: EntityRegistryEntry[];
@property() public value?: any; @property() public value?: any;
@property() public label?: string; @property() public label?: string;
@@ -40,32 +25,20 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public required = true; @property({ type: Boolean }) public required = true;
public hassSubscribe(): UnsubscribeFunc[] { protected updated(changedProperties) {
return [ if (changedProperties.has("selector")) {
subscribeEntityRegistry(this.hass.connection!, (entities) => { const oldSelector = changedProperties.get("selector");
this._entities = entities.filter((entity) => entity.device_id !== null); if (oldSelector !== this.selector && this.selector.device?.integration) {
}), getConfigEntries(this.hass, {
]; domain: this.selector.device.integration,
} }).then((entries) => {
this._configEntries = entries;
protected updated(changedProperties): void {
super.updated(changedProperties);
if (
changedProperties.has("selector") &&
this.selector.device.integration &&
!this._entitySources
) {
fetchEntitySourcesWithCache(this.hass).then((sources) => {
this._entitySources = sources;
}); });
} }
} }
}
protected render() { protected render() {
if (this.selector.device.integration && !this._entitySources) {
return html``;
}
if (!this.selector.device.multiple) { if (!this.selector.device.multiple) {
return html` return html`
<ha-device-picker <ha-device-picker
@@ -107,48 +80,30 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
} }
private _filterDevices = (device: DeviceRegistryEntry): boolean => { private _filterDevices = (device: DeviceRegistryEntry): boolean => {
const { if (
manufacturer: filterManufacturer, this.selector.device?.manufacturer &&
model: filterModel, device.manufacturer !== this.selector.device.manufacturer
integration: filterIntegration, ) {
} = this.selector.device;
if (filterManufacturer && device.manufacturer !== filterManufacturer) {
return false; return false;
} }
if (filterModel && device.model !== filterModel) { if (
this.selector.device?.model &&
device.model !== this.selector.device.model
) {
return false; return false;
} }
if (filterIntegration && this._entitySources && this._entities) { if (this.selector.device?.integration) {
const deviceIntegrations = this._deviceIntegrations( if (
this._entitySources, this._configEntries &&
this._entities !this._configEntries.some((entry) =>
); device.config_entries.includes(entry.entry_id)
if (!deviceIntegrations?.[device.id]?.includes(filterIntegration)) { )
) {
return false; return false;
} }
} }
return true; return true;
}; };
private _deviceIntegrations = memoizeOne(
(entitySources: EntitySources, entities: EntityRegistryEntry[]) => {
const deviceIntegrations: Record<string, string[]> = {};
for (const entity of entities) {
const source = entitySources[entity.entity_id];
if (!source?.domain) {
continue;
}
if (!deviceIntegrations[entity.device_id!]) {
deviceIntegrations[entity.device_id!] = [];
}
deviceIntegrations[entity.device_id!].push(source.domain);
}
return deviceIntegrations;
}
);
} }
declare global { declare global {

View File

@@ -287,7 +287,9 @@ export class HaServiceControl extends LitElement {
${shouldRenderServiceDataYaml ${shouldRenderServiceDataYaml
? html`<ha-yaml-editor ? html`<ha-yaml-editor
.hass=${this.hass} .hass=${this.hass}
.label=${this.hass.localize("ui.components.service-control.data")} .label=${this.hass.localize(
"ui.components.service-control.service_data"
)}
.name=${"data"} .name=${"data"}
.defaultValue=${this._value?.data} .defaultValue=${this._value?.data}
@value-changed=${this._dataChanged} @value-changed=${this._dataChanged}

View File

@@ -569,9 +569,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
height: 16px; height: 16px;
--mdc-icon-size: 14px; --mdc-icon-size: 14px;
color: var(--secondary-text-color); color: var(--secondary-text-color);
margin-inline-start: 4px !important;
margin-inline-end: -4px !important;
direction: var(--direction);
} }
.mdc-chip__icon--leading { .mdc-chip__icon--leading {
display: flex; display: flex;
@@ -581,9 +578,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
border-radius: 50%; border-radius: 50%;
padding: 6px; padding: 6px;
margin-left: -14px !important; margin-left: -14px !important;
margin-inline-start: -14px !important;
margin-inline-end: 4px !important;
direction: var(--direction);
} }
.expand-btn { .expand-btn {
margin-right: 0; margin-right: 0;
@@ -622,6 +616,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
opacity: var(--light-disabled-opacity); opacity: var(--light-disabled-opacity);
pointer-events: none; pointer-events: none;
} }
:host-context([style*="direction: rtl;"]) .mdc-chip__icon {
margin-right: -14px !important;
margin-left: 4px !important;
}
`; `;
} }
} }

View File

@@ -57,9 +57,6 @@ export class HaTextField extends TextFieldBase {
.mdc-text-field__affix--suffix { .mdc-text-field__affix--suffix {
padding-left: var(--text-field-suffix-padding-left, 12px); padding-left: var(--text-field-suffix-padding-left, 12px);
padding-right: var(--text-field-suffix-padding-right, 0px); padding-right: var(--text-field-suffix-padding-right, 0px);
padding-inline-start: var(--text-field-suffix-padding-left, 12px);
padding-inline-end: var(--text-field-suffix-padding-right, 0px);
direction: var(--direction);
} }
.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field:not(.mdc-text-field--disabled)
@@ -95,19 +92,17 @@ export class HaTextField extends TextFieldBase {
overflow: var(--text-field-overflow); overflow: var(--text-field-overflow);
} }
.mdc-floating-label { :host-context([style*="direction: rtl;"]) .mdc-floating-label {
inset-inline-start: 16px !important; right: 10px !important;
inset-inline-end: initial !important; left: initial !important;
transform-origin: var(--float-start);
direction: var(--direction);
} }
:host-context([style*="direction: rtl;"])
.mdc-text-field--with-leading-icon.mdc-text-field--filled .mdc-text-field--with-leading-icon.mdc-text-field--filled
.mdc-floating-label { .mdc-floating-label {
max-width: calc(100% - 48px); max-width: calc(100% - 48px);
inset-inline-start: 48px !important; right: 48px !important;
inset-inline-end: initial !important; left: initial !important;
direction: var(--direction);
} }
`, `,
]; ];

View File

@@ -314,10 +314,9 @@ class DialogMediaManage extends LitElement {
vertical-align: middle; vertical-align: middle;
} }
ha-svg-icon[slot="icon"] { :host-context([style*="direction: rtl;"]) ha-svg-icon[slot="icon"] {
margin-inline-start: 0px !important; margin-left: 8px !important;
margin-inline-end: 8px !important; margin-right: 0px !important;
direction: var(--direction);
} }
.refresh { .refresh {

View File

@@ -60,10 +60,9 @@ class MediaManageButton extends LitElement {
vertical-align: middle; vertical-align: middle;
} }
ha-svg-icon[slot="icon"] { :host-context([style*="direction: rtl;"]) ha-svg-icon[slot="icon"] {
margin-inline-start: 0px; margin-left: 8px;
margin-inline-end: 8px; margin-right: 0px;
direction: var(--direction);
} }
`; `;
} }

View File

@@ -120,10 +120,9 @@ class MediaUploadButton extends LitElement {
vertical-align: middle; vertical-align: middle;
} }
ha-svg-icon[slot="icon"] { :host-context([style*="direction: rtl;"]) ha-svg-icon[slot="icon"] {
margin-inline-start: 0px; margin-left: 8px;
margin-inline-end: 8px; margin-right: 0px;
direction: var(--direction);
} }
`; `;
} }

View File

@@ -3,7 +3,7 @@ import { customElement, property } from "lit/decorators";
import { LogbookEntry } from "../../data/logbook"; import { LogbookEntry } from "../../data/logbook";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "./hat-logbook-note"; import "./hat-logbook-note";
import "../../panels/logbook/ha-logbook-renderer"; import "../../panels/logbook/ha-logbook";
import { TraceExtended } from "../../data/trace"; import { TraceExtended } from "../../data/trace";
@customElement("ha-trace-logbook") @customElement("ha-trace-logbook")
@@ -19,12 +19,12 @@ export class HaTraceLogbook extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return this.logbookEntries.length return this.logbookEntries.length
? html` ? html`
<ha-logbook-renderer <ha-logbook
relative-time relative-time
.hass=${this.hass} .hass=${this.hass}
.entries=${this.logbookEntries} .entries=${this.logbookEntries}
.narrow=${this.narrow} .narrow=${this.narrow}
></ha-logbook-renderer> ></ha-logbook>
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note> <hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
` `
: html`<div class="padded-box"> : html`<div class="padded-box">

View File

@@ -13,7 +13,7 @@ import {
getDataFromPath, getDataFromPath,
TraceExtended, TraceExtended,
} from "../../data/trace"; } from "../../data/trace";
import "../../panels/logbook/ha-logbook-renderer"; import "../../panels/logbook/ha-logbook";
import { traceTabStyles } from "./trace-tab-styles"; import { traceTabStyles } from "./trace-tab-styles";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import type { NodeInfo } from "./hat-script-graph"; import type { NodeInfo } from "./hat-script-graph";
@@ -194,7 +194,7 @@ export class HaTracePathDetails extends LitElement {
// it's the last entry. Find all logbook entries after start. // it's the last entry. Find all logbook entries after start.
const startTime = new Date(startTrace[0].timestamp); const startTime = new Date(startTrace[0].timestamp);
const idx = this.logbookEntries.findIndex( const idx = this.logbookEntries.findIndex(
(entry) => new Date(entry.when * 1000) >= startTime (entry) => new Date(entry.when) >= startTime
); );
if (idx === -1) { if (idx === -1) {
entries = []; entries = [];
@@ -210,7 +210,7 @@ export class HaTracePathDetails extends LitElement {
entries = []; entries = [];
for (const entry of this.logbookEntries || []) { for (const entry of this.logbookEntries || []) {
const entryDate = new Date(entry.when * 1000); const entryDate = new Date(entry.when);
if (entryDate >= startTime) { if (entryDate >= startTime) {
if (entryDate < endTime) { if (entryDate < endTime) {
entries.push(entry); entries.push(entry);
@@ -224,12 +224,12 @@ export class HaTracePathDetails extends LitElement {
return entries.length return entries.length
? html` ? html`
<ha-logbook-renderer <ha-logbook
relative-time relative-time
.hass=${this.hass} .hass=${this.hass}
.entries=${entries} .entries=${entries}
.narrow=${this.narrow} .narrow=${this.narrow}
></ha-logbook-renderer> ></ha-logbook>
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note> <hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
` `
: html`<div class="padded-box"> : html`<div class="padded-box">

View File

@@ -116,7 +116,7 @@ class LogbookRenderer {
maybeRenderItem() { maybeRenderItem() {
const logbookEntry = this.curItem; const logbookEntry = this.curItem;
this.curIndex++; this.curIndex++;
const entryDate = new Date(logbookEntry.when * 1000); const entryDate = new Date(logbookEntry.when);
if (this.pendingItems.length === 0) { if (this.pendingItems.length === 0) {
this.pendingItems.push([entryDate, logbookEntry]); this.pendingItems.push([entryDate, logbookEntry]);
@@ -248,7 +248,7 @@ class ActionRenderer {
// Render all logbook items that are in front of this item. // Render all logbook items that are in front of this item.
while ( while (
this.logbookRenderer.hasNext && this.logbookRenderer.hasNext &&
new Date(this.logbookRenderer.curItem.when * 1000) < timestamp new Date(this.logbookRenderer.curItem.when) < timestamp
) { ) {
this.logbookRenderer.maybeRenderItem(); this.logbookRenderer.maybeRenderItem();
} }

View File

@@ -9,7 +9,6 @@ export interface ApplicationCredential {
domain: string; domain: string;
client_id: string; client_id: string;
client_secret: string; client_secret: string;
name: string;
} }
export const fetchApplicationCredentialsConfig = async (hass: HomeAssistant) => export const fetchApplicationCredentialsConfig = async (hass: HomeAssistant) =>
@@ -26,15 +25,13 @@ export const createApplicationCredential = async (
hass: HomeAssistant, hass: HomeAssistant,
domain: string, domain: string,
clientId: string, clientId: string,
clientSecret: string, clientSecret: string
name?: string
) => ) =>
hass.callWS<ApplicationCredential>({ hass.callWS<ApplicationCredential>({
type: "application_credentials/create", type: "application_credentials/create",
domain, domain,
client_id: clientId, client_id: clientId,
client_secret: clientSecret, client_secret: clientSecret,
name,
}); });
export const deleteApplicationCredential = async ( export const deleteApplicationCredential = async (

View File

@@ -157,7 +157,6 @@ export interface CalendarTrigger extends BaseTrigger {
platform: "calendar"; platform: "calendar";
event: "start" | "end"; event: "start" | "end";
entity_id: string; entity_id: string;
offset: string;
} }
export type Trigger = export type Trigger =

View File

@@ -1,13 +1,13 @@
import { HassEntity } from "home-assistant-js-websocket";
import { LocalizeFunc } from "../common/translations/localize"; import { LocalizeFunc } from "../common/translations/localize";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { import {
computeHistory, computeHistory,
HistoryStates, fetchRecent,
HistoryResult, HistoryResult,
LineChartUnit, LineChartUnit,
TimelineEntity, TimelineEntity,
entityIdHistoryNeedsAttributes, entityIdHistoryNeedsAttributes,
fetchRecentWS,
} from "./history"; } from "./history";
export interface CacheConfig { export interface CacheConfig {
@@ -34,7 +34,7 @@ const RECENT_THRESHOLD = 60000; // 1 minute
const RECENT_CACHE: { [cacheKey: string]: RecentCacheResults } = {}; const RECENT_CACHE: { [cacheKey: string]: RecentCacheResults } = {};
const stateHistoryCache: { [cacheKey: string]: CachedResults } = {}; const stateHistoryCache: { [cacheKey: string]: CachedResults } = {};
// Cached type 1 function. Without cache config. // Cached type 1 unction. Without cache config.
export const getRecent = ( export const getRecent = (
hass: HomeAssistant, hass: HomeAssistant,
entityId: string, entityId: string,
@@ -55,7 +55,7 @@ export const getRecent = (
} }
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId); const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
const prom = fetchRecentWS( const prom = fetchRecent(
hass, hass,
entityId, entityId,
startTime, startTime,
@@ -103,14 +103,13 @@ export const getRecentWithCache = (
language: string language: string
) => { ) => {
const cacheKey = cacheConfig.cacheKey; const cacheKey = cacheConfig.cacheKey;
const fullCacheKey = cacheKey + `_${cacheConfig.hoursToShow}`;
const endTime = new Date(); const endTime = new Date();
const startTime = new Date(endTime); const startTime = new Date(endTime);
startTime.setHours(startTime.getHours() - cacheConfig.hoursToShow); startTime.setHours(startTime.getHours() - cacheConfig.hoursToShow);
let toFetchStartTime = startTime; let toFetchStartTime = startTime;
let appendingToCache = false; let appendingToCache = false;
let cache = stateHistoryCache[fullCacheKey]; let cache = stateHistoryCache[cacheKey + `_${cacheConfig.hoursToShow}`];
if ( if (
cache && cache &&
toFetchStartTime >= cache.startTime && toFetchStartTime >= cache.startTime &&
@@ -124,7 +123,7 @@ export const getRecentWithCache = (
return cache.prom; return cache.prom;
} }
} else { } else {
cache = stateHistoryCache[fullCacheKey] = getEmptyCache( cache = stateHistoryCache[cacheKey] = getEmptyCache(
language, language,
startTime, startTime,
endTime endTime
@@ -135,12 +134,12 @@ export const getRecentWithCache = (
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId); const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
const genProm = async () => { const genProm = async () => {
let fetchedHistory: HistoryStates; let fetchedHistory: HassEntity[][];
try { try {
const results = await Promise.all([ const results = await Promise.all([
curCacheProm, curCacheProm,
fetchRecentWS( fetchRecent(
hass, hass,
entityId, entityId,
toFetchStartTime, toFetchStartTime,
@@ -153,7 +152,7 @@ export const getRecentWithCache = (
]); ]);
fetchedHistory = results[1]; fetchedHistory = results[1];
} catch (err: any) { } catch (err: any) {
delete stateHistoryCache[fullCacheKey]; delete stateHistoryCache[cacheKey];
throw err; throw err;
} }
const stateHistory = computeHistory(hass, fetchedHistory, localize); const stateHistory = computeHistory(hass, fetchedHistory, localize);

View File

@@ -1,14 +1,11 @@
import { import {
addDays,
addHours, addHours,
addMilliseconds,
addMonths,
differenceInDays, differenceInDays,
endOfToday, endOfToday,
endOfYesterday, endOfYesterday,
startOfToday, startOfToday,
startOfYesterday, startOfYesterday,
} from "date-fns/esm"; } from "date-fns";
import { Collection, getCollection } from "home-assistant-js-websocket"; import { Collection, getCollection } from "home-assistant-js-websocket";
import { groupBy } from "../common/util/group-by"; import { groupBy } from "../common/util/group-by";
import { subscribeOne } from "../common/util/subscribe-one"; import { subscribeOne } from "../common/util/subscribe-one";
@@ -17,9 +14,9 @@ import { ConfigEntry, getConfigEntries } from "./config_entries";
import { subscribeEntityRegistry } from "./entity_registry"; import { subscribeEntityRegistry } from "./entity_registry";
import { import {
fetchStatistics, fetchStatistics,
getStatisticMetadata,
Statistics, Statistics,
StatisticsMetaData, StatisticsMetaData,
getStatisticMetadata,
} from "./history"; } from "./history";
const energyCollectionKeys: (string | undefined)[] = []; const energyCollectionKeys: (string | undefined)[] = [];
@@ -235,24 +232,19 @@ export const energySourcesByType = (prefs: EnergyPreferences) =>
export interface EnergyData { export interface EnergyData {
start: Date; start: Date;
end?: Date; end?: Date;
startCompare?: Date;
endCompare?: Date;
prefs: EnergyPreferences; prefs: EnergyPreferences;
info: EnergyInfo; info: EnergyInfo;
stats: Statistics; stats: Statistics;
statsCompare: Statistics;
co2SignalConfigEntry?: ConfigEntry; co2SignalConfigEntry?: ConfigEntry;
co2SignalEntity?: string; co2SignalEntity?: string;
fossilEnergyConsumption?: FossilEnergyConsumption; fossilEnergyConsumption?: FossilEnergyConsumption;
fossilEnergyConsumptionCompare?: FossilEnergyConsumption;
} }
const getEnergyData = async ( const getEnergyData = async (
hass: HomeAssistant, hass: HomeAssistant,
prefs: EnergyPreferences, prefs: EnergyPreferences,
start: Date, start: Date,
end?: Date, end?: Date
compare?: boolean
): Promise<EnergyData> => { ): Promise<EnergyData> => {
const [configEntries, entityRegistryEntries, info] = await Promise.all([ const [configEntries, entityRegistryEntries, info] = await Promise.all([
getConfigEntries(hass, { domain: "co2signal" }), getConfigEntries(hass, { domain: "co2signal" }),
@@ -358,8 +350,6 @@ const getEnergyData = async (
} }
const dayDifference = differenceInDays(end || new Date(), start); const dayDifference = differenceInDays(end || new Date(), start);
const period =
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour";
// Subtract 1 hour from start to get starting point data // Subtract 1 hour from start to get starting point data
const startMinHour = addHours(start, -1); const startMinHour = addHours(start, -1);
@@ -369,34 +359,10 @@ const getEnergyData = async (
startMinHour, startMinHour,
end, end,
statIDs, statIDs,
period dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
); );
let statsCompare;
let startCompare;
let endCompare;
if (compare) {
if (dayDifference > 27 && dayDifference < 32) {
// When comparing a month, we want to start at the begining of the month
startCompare = addMonths(start, -1);
} else {
startCompare = addDays(start, (dayDifference + 1) * -1);
}
const compareStartMinHour = addHours(startCompare, -1);
endCompare = addMilliseconds(start, -1);
statsCompare = await fetchStatistics(
hass!,
compareStartMinHour,
endCompare,
statIDs,
period
);
}
let fossilEnergyConsumption: FossilEnergyConsumption | undefined; let fossilEnergyConsumption: FossilEnergyConsumption | undefined;
let fossilEnergyConsumptionCompare: FossilEnergyConsumption | undefined;
if (co2SignalEntity !== undefined) { if (co2SignalEntity !== undefined) {
fossilEnergyConsumption = await getFossilEnergyConsumption( fossilEnergyConsumption = await getFossilEnergyConsumption(
@@ -407,16 +373,6 @@ const getEnergyData = async (
end, end,
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour" dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
); );
if (compare) {
fossilEnergyConsumptionCompare = await getFossilEnergyConsumption(
hass!,
startCompare,
consumptionStatIDs,
co2SignalEntity,
endCompare,
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
);
}
} }
Object.values(stats).forEach((stat) => { Object.values(stats).forEach((stat) => {
@@ -432,19 +388,15 @@ const getEnergyData = async (
} }
}); });
const data: EnergyData = { const data = {
start, start,
end, end,
startCompare,
endCompare,
info, info,
prefs, prefs,
stats, stats,
statsCompare,
co2SignalConfigEntry, co2SignalConfigEntry,
co2SignalEntity, co2SignalEntity,
fossilEnergyConsumption, fossilEnergyConsumption,
fossilEnergyConsumptionCompare,
}; };
return data; return data;
@@ -453,11 +405,9 @@ const getEnergyData = async (
export interface EnergyCollection extends Collection<EnergyData> { export interface EnergyCollection extends Collection<EnergyData> {
start: Date; start: Date;
end?: Date; end?: Date;
compare?: boolean;
prefs?: EnergyPreferences; prefs?: EnergyPreferences;
clearPrefs(): void; clearPrefs(): void;
setPeriod(newStart: Date, newEnd?: Date): void; setPeriod(newStart: Date, newEnd?: Date): void;
setCompare(compare: boolean): void;
_refreshTimeout?: number; _refreshTimeout?: number;
_updatePeriodTimeout?: number; _updatePeriodTimeout?: number;
_active: number; _active: number;
@@ -528,8 +478,7 @@ export const getEnergyDataCollection = (
hass, hass,
collection.prefs, collection.prefs,
collection.start, collection.start,
collection.end, collection.end
collection.compare
); );
} }
) as EnergyCollection; ) as EnergyCollection;
@@ -585,9 +534,6 @@ export const getEnergyDataCollection = (
collection._updatePeriodTimeout = undefined; collection._updatePeriodTimeout = undefined;
} }
}; };
collection.setCompare = (compare: boolean) => {
collection.compare = compare;
};
return collection; return collection;
}; };

View File

@@ -20,20 +20,3 @@ export const BOARD_NAMES: Record<string, string> = {
"intel-nuc": "Intel NUC", "intel-nuc": "Intel NUC",
yellow: "Home Assistant Yellow", yellow: "Home Assistant Yellow",
}; };
export interface HardwareInfo {
hardware: HardwareInfoEntry[];
}
export interface HardwareInfoEntry {
board: HardwareInfoBoardInfo;
name: string;
url?: string;
}
export interface HardwareInfoBoardInfo {
manufacturer: string;
model?: string;
revision?: string;
hassio_board_id?: string;
}

View File

@@ -1,7 +1,8 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain"; import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDisplayFromEntityAttributes } from "../common/entity/compute_state_display"; import { computeStateDisplay } from "../common/entity/compute_state_display";
import { computeStateNameFromEntityAttributes } from "../common/entity/compute_state_name"; import { computeStateDomain } from "../common/entity/compute_state_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import { LocalizeFunc } from "../common/translations/localize"; import { LocalizeFunc } from "../common/translations/localize";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { FrontendLocaleData } from "./translation"; import { FrontendLocaleData } from "./translation";
@@ -26,7 +27,7 @@ const LINE_ATTRIBUTES_TO_KEEP = [
export interface LineChartState { export interface LineChartState {
state: string; state: string;
last_changed: number; last_changed: string;
attributes?: Record<string, any>; attributes?: Record<string, any>;
} }
@@ -46,7 +47,7 @@ export interface LineChartUnit {
export interface TimelineState { export interface TimelineState {
state_localize: string; state_localize: string;
state: string; state: string;
last_changed: number; last_changed: string;
} }
export interface TimelineEntity { export interface TimelineEntity {
@@ -140,21 +141,6 @@ export interface StatisticsValidationResults {
[statisticId: string]: StatisticsValidationResult[]; [statisticId: string]: StatisticsValidationResult[];
} }
export interface HistoryStates {
[entityId: string]: EntityHistoryState[];
}
interface EntityHistoryState {
/** state */
s: string;
/** attributes */
a: { [key: string]: any };
/** last_changed; if set, also applies to lu */
lc: number;
/** last_updated */
lu: number;
}
export const entityIdHistoryNeedsAttributes = ( export const entityIdHistoryNeedsAttributes = (
hass: HomeAssistant, hass: HomeAssistant,
entityId: string entityId: string
@@ -195,27 +181,6 @@ export const fetchRecent = (
return hass.callApi("GET", url); return hass.callApi("GET", url);
}; };
export const fetchRecentWS = (
hass: HomeAssistant,
entityId: string, // This may be CSV
startTime: Date,
endTime: Date,
skipInitialState = false,
significantChangesOnly?: boolean,
minimalResponse = true,
noAttributes?: boolean
) =>
hass.callWS<HistoryStates>({
type: "history/history_during_period",
start_time: startTime.toISOString(),
end_time: endTime.toISOString(),
significant_changes_only: significantChangesOnly || false,
include_start_time_state: !skipInitialState,
minimal_response: minimalResponse,
no_attributes: noAttributes || false,
entity_ids: entityId.split(","),
});
export const fetchDate = ( export const fetchDate = (
hass: HomeAssistant, hass: HomeAssistant,
startTime: Date, startTime: Date,
@@ -233,27 +198,6 @@ export const fetchDate = (
}` }`
); );
export const fetchDateWS = (
hass: HomeAssistant,
startTime: Date,
endTime: Date,
entityId?: string
) => {
const params = {
type: "history/history_during_period",
start_time: startTime.toISOString(),
end_time: endTime.toISOString(),
minimal_response: true,
no_attributes: !!(
entityId && !entityIdHistoryNeedsAttributes(hass, entityId)
),
};
if (entityId) {
return hass.callWS<HistoryStates>({ ...params, entity_ids: [entityId] });
}
return hass.callWS<HistoryStates>(params);
};
const equalState = (obj1: LineChartState, obj2: LineChartState) => const equalState = (obj1: LineChartState, obj2: LineChartState) =>
obj1.state === obj2.state && obj1.state === obj2.state &&
// Only compare attributes if both states have an attributes object. // Only compare attributes if both states have an attributes object.
@@ -268,47 +212,46 @@ const equalState = (obj1: LineChartState, obj2: LineChartState) =>
const processTimelineEntity = ( const processTimelineEntity = (
localize: LocalizeFunc, localize: LocalizeFunc,
language: FrontendLocaleData, language: FrontendLocaleData,
entityId: string, states: HassEntity[]
states: EntityHistoryState[]
): TimelineEntity => { ): TimelineEntity => {
const data: TimelineState[] = []; const data: TimelineState[] = [];
const first: EntityHistoryState = states[0]; const last_element = states.length - 1;
for (const state of states) { for (const state of states) {
if (data.length > 0 && state.s === data[data.length - 1].state) { if (data.length > 0 && state.state === data[data.length - 1].state) {
continue; continue;
} }
// Copy the data from the last element as its the newest
// and is only needed to localize the data
if (!state.entity_id) {
state.attributes = states[last_element].attributes;
state.entity_id = states[last_element].entity_id;
}
data.push({ data.push({
state_localize: computeStateDisplayFromEntityAttributes( state_localize: computeStateDisplay(localize, state, language),
localize, state: state.state,
language, last_changed: state.last_changed,
entityId,
state.a || first.a,
state.s
),
state: state.s,
// lc (last_changed) may be omitted if its the same
// as lu (last_updated).
last_changed: (state.lc ? state.lc : state.lu) * 1000,
}); });
} }
return { return {
name: computeStateNameFromEntityAttributes(entityId, states[0].a), name: computeStateName(states[0]),
entity_id: entityId, entity_id: states[0].entity_id,
data, data,
}; };
}; };
const processLineChartEntities = ( const processLineChartEntities = (
unit, unit,
entities: HistoryStates entities: HassEntity[][]
): LineChartUnit => { ): LineChartUnit => {
const data: LineChartEntity[] = []; const data: LineChartEntity[] = [];
Object.keys(entities).forEach((entityId) => { for (const states of entities) {
const states = entities[entityId]; const last: HassEntity = states[states.length - 1];
const first: EntityHistoryState = states[0]; const domain = computeStateDomain(last);
const domain = computeDomain(entityId);
const processedStates: LineChartState[] = []; const processedStates: LineChartState[] = [];
for (const state of states) { for (const state of states) {
@@ -316,24 +259,18 @@ const processLineChartEntities = (
if (DOMAINS_USE_LAST_UPDATED.includes(domain)) { if (DOMAINS_USE_LAST_UPDATED.includes(domain)) {
processedState = { processedState = {
state: state.s, state: state.state,
last_changed: state.lu * 1000, last_changed: state.last_updated,
attributes: {}, attributes: {},
}; };
for (const attr of LINE_ATTRIBUTES_TO_KEEP) { for (const attr of LINE_ATTRIBUTES_TO_KEEP) {
if (attr in state.a) { if (attr in state.attributes) {
processedState.attributes![attr] = state.a[attr]; processedState.attributes![attr] = state.attributes[attr];
} }
} }
} else { } else {
processedState = { processedState = state;
state: state.s,
// lc (last_changed) may be omitted if its the same
// as lu (last_updated).
last_changed: (state.lc ? state.lc : state.lu) * 1000,
attributes: {},
};
} }
if ( if (
@@ -352,53 +289,52 @@ const processLineChartEntities = (
data.push({ data.push({
domain, domain,
name: computeStateNameFromEntityAttributes(entityId, first.a), name: computeStateName(last),
entity_id: entityId, entity_id: last.entity_id,
states: processedStates, states: processedStates,
}); });
}); }
return { return {
unit, unit,
identifier: Object.keys(entities).join(""), identifier: entities.map((states) => states[0].entity_id).join(""),
data, data,
}; };
}; };
const stateUsesUnits = (state: HassEntity) => const stateUsesUnits = (state: HassEntity) =>
attributesHaveUnits(state.attributes); "unit_of_measurement" in state.attributes ||
"state_class" in state.attributes;
const attributesHaveUnits = (attributes: { [key: string]: any }) =>
"unit_of_measurement" in attributes || "state_class" in attributes;
export const computeHistory = ( export const computeHistory = (
hass: HomeAssistant, hass: HomeAssistant,
stateHistory: HistoryStates, stateHistory: HassEntity[][],
localize: LocalizeFunc localize: LocalizeFunc
): HistoryResult => { ): HistoryResult => {
const lineChartDevices: { [unit: string]: HistoryStates } = {}; const lineChartDevices: { [unit: string]: HassEntity[][] } = {};
const timelineDevices: TimelineEntity[] = []; const timelineDevices: TimelineEntity[] = [];
if (!stateHistory) { if (!stateHistory) {
return { line: [], timeline: [] }; return { line: [], timeline: [] };
} }
Object.keys(stateHistory).forEach((entityId) => {
const stateInfo = stateHistory[entityId]; stateHistory.forEach((stateInfo) => {
if (stateInfo.length === 0) { if (stateInfo.length === 0) {
return; return;
} }
const entityId = stateInfo[0].entity_id;
const currentState = const currentState =
entityId in hass.states ? hass.states[entityId] : undefined; entityId in hass.states ? hass.states[entityId] : undefined;
const stateWithUnitorStateClass = const stateWithUnitorStateClass =
!currentState && !currentState &&
stateInfo.find((state) => state.a && attributesHaveUnits(state.a)); stateInfo.find((state) => state.attributes && stateUsesUnits(state));
let unit: string | undefined; let unit: string | undefined;
if (currentState && stateUsesUnits(currentState)) { if (currentState && stateUsesUnits(currentState)) {
unit = currentState.attributes.unit_of_measurement || " "; unit = currentState.attributes.unit_of_measurement || " ";
} else if (stateWithUnitorStateClass) { } else if (stateWithUnitorStateClass) {
unit = stateWithUnitorStateClass.a.unit_of_measurement || " "; unit = stateWithUnitorStateClass.attributes.unit_of_measurement || " ";
} else { } else {
unit = { unit = {
climate: hass.config.unit_system.temperature, climate: hass.config.unit_system.temperature,
@@ -412,15 +348,12 @@ export const computeHistory = (
if (!unit) { if (!unit) {
timelineDevices.push( timelineDevices.push(
processTimelineEntity(localize, hass.locale, entityId, stateInfo) processTimelineEntity(localize, hass.locale, stateInfo)
); );
} else if (unit in lineChartDevices && entityId in lineChartDevices[unit]) { } else if (unit in lineChartDevices) {
lineChartDevices[unit][entityId].push(...stateInfo); lineChartDevices[unit].push(stateInfo);
} else { } else {
if (!(unit in lineChartDevices)) { lineChartDevices[unit] = [stateInfo];
lineChartDevices[unit] = {};
}
lineChartDevices[unit][entityId] = stateInfo;
} }
}); });

View File

@@ -42,18 +42,8 @@ export const domainToName = (
manifest?: IntegrationManifest manifest?: IntegrationManifest
) => localize(`component.${domain}.title`) || manifest?.name || domain; ) => localize(`component.${domain}.title`) || manifest?.name || domain;
export const fetchIntegrationManifests = ( export const fetchIntegrationManifests = (hass: HomeAssistant) =>
hass: HomeAssistant, hass.callWS<IntegrationManifest[]>({ type: "manifest/list" });
integrations?: string[]
) => {
const params: any = {
type: "manifest/list",
};
if (integrations) {
params.integrations = integrations;
}
return hass.callWS<IntegrationManifest[]>(params);
};
export const fetchIntegrationManifest = ( export const fetchIntegrationManifest = (
hass: HomeAssistant, hass: HomeAssistant,

View File

@@ -1,9 +1,5 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { import { BINARY_STATE_OFF, BINARY_STATE_ON } from "../common/const";
BINARY_STATE_OFF,
BINARY_STATE_ON,
DOMAINS_WITH_DYNAMIC_PICTURE,
} from "../common/const";
import { computeDomain } from "../common/entity/compute_domain"; import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDisplay } from "../common/entity/compute_state_display"; import { computeStateDisplay } from "../common/entity/compute_state_display";
import { LocalizeFunc } from "../common/translations/localize"; import { LocalizeFunc } from "../common/translations/localize";
@@ -13,51 +9,25 @@ import { UNAVAILABLE_STATES } from "./entity";
const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages"; const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
export const CONTINUOUS_DOMAINS = ["proximity", "sensor"]; export const CONTINUOUS_DOMAINS = ["proximity", "sensor"];
export interface LogbookStreamMessage {
events: LogbookEntry[];
start_time?: number; // Start time of this historical chunk
end_time?: number; // End time of this historical chunk
partial?: boolean; // Indiciates more historical chunks are coming
}
export interface LogbookEntry { export interface LogbookEntry {
// Base data when: string;
when: number; // Python timestamp. Do *1000 to get JS timestamp.
name: string; name: string;
message?: string; message?: string;
entity_id?: string; entity_id?: string;
icon?: string; icon?: string;
source?: string; // The trigger source source?: string;
domain?: string; domain?: string;
state?: string; // The state of the entity
// Context data
context_id?: string; context_id?: string;
context_user_id?: string; context_user_id?: string;
context_event_type?: string; context_event_type?: string;
context_domain?: string; context_domain?: string;
context_service?: string; // Service calls only context_service?: string;
context_entity_id?: string; context_entity_id?: string;
context_entity_id_name?: string; // Legacy, not longer sent context_entity_id_name?: string;
context_name?: string; context_name?: string;
context_state?: string; // The state of the entity state?: string;
context_source?: string; // The trigger source
context_message?: string;
} }
//
// Localization mapping for all the triggers in core
// in homeassistant.components.homeassistant.triggers
//
const triggerPhrases = {
"numeric state of": "triggered_by_numeric_state_of", // number state trigger
"state of": "triggered_by_state_of", // state trigger
event: "triggered_by_event", // event trigger
time: "triggered_by_time", // time trigger
"time pattern": "triggered_by_time_pattern", // time trigger
"Home Assistant stopping": "triggered_by_homeassistant_stopping", // stop event
"Home Assistant starting": "triggered_by_homeassistant_starting", // start event
};
const DATA_CACHE: { const DATA_CACHE: {
[cacheKey: string]: { [entityId: string]: Promise<LogbookEntry[]> }; [cacheKey: string]: { [entityId: string]: Promise<LogbookEntry[]> };
} = {}; } = {};
@@ -67,13 +37,18 @@ export const getLogbookDataForContext = async (
startDate: string, startDate: string,
contextId?: string contextId?: string
): Promise<LogbookEntry[]> => { ): Promise<LogbookEntry[]> => {
await hass.loadBackendTranslation("device_class"); const localize = await hass.loadBackendTranslation("device_class");
return getLogbookDataFromServer( return addLogbookMessage(
hass,
localize,
await getLogbookDataFromServer(
hass, hass,
startDate, startDate,
undefined, undefined,
undefined, undefined,
undefined,
contextId contextId
)
); );
}; };
@@ -81,123 +56,107 @@ export const getLogbookData = async (
hass: HomeAssistant, hass: HomeAssistant,
startDate: string, startDate: string,
endDate: string, endDate: string,
entityIds?: string[], entityId?: string,
deviceIds?: string[] entity_matches_only?: boolean
): Promise<LogbookEntry[]> => { ): Promise<LogbookEntry[]> => {
await hass.loadBackendTranslation("device_class"); const localize = await hass.loadBackendTranslation("device_class");
return deviceIds?.length return addLogbookMessage(
? getLogbookDataFromServer( hass,
localize,
await getLogbookDataCache(
hass, hass,
startDate, startDate,
endDate, endDate,
entityIds, entityId,
undefined, entity_matches_only
deviceIds
) )
: getLogbookDataCache(hass, startDate, endDate, entityIds); );
}; };
const getLogbookDataCache = async ( export const addLogbookMessage = (
hass: HomeAssistant,
localize: LocalizeFunc,
logbookData: LogbookEntry[]
): LogbookEntry[] => {
for (const entry of logbookData) {
const stateObj = hass!.states[entry.entity_id!];
if (entry.state && stateObj) {
entry.message = getLogbookMessage(
hass,
localize,
entry.state,
stateObj,
computeDomain(entry.entity_id!)
);
}
}
return logbookData;
};
export const getLogbookDataCache = async (
hass: HomeAssistant, hass: HomeAssistant,
startDate: string, startDate: string,
endDate: string, endDate: string,
entityId?: string[] entityId?: string,
entity_matches_only?: boolean
) => { ) => {
const ALL_ENTITIES = "*"; const ALL_ENTITIES = "*";
const entityIdKey = entityId ? entityId.toString() : ALL_ENTITIES; if (!entityId) {
entityId = ALL_ENTITIES;
}
const cacheKey = `${startDate}${endDate}`; const cacheKey = `${startDate}${endDate}`;
if (!DATA_CACHE[cacheKey]) { if (!DATA_CACHE[cacheKey]) {
DATA_CACHE[cacheKey] = {}; DATA_CACHE[cacheKey] = {};
} }
if (entityIdKey in DATA_CACHE[cacheKey]) { if (entityId in DATA_CACHE[cacheKey]) {
return DATA_CACHE[cacheKey][entityIdKey]; return DATA_CACHE[cacheKey][entityId];
} }
if (entityId && DATA_CACHE[cacheKey][ALL_ENTITIES]) { if (entityId !== ALL_ENTITIES && DATA_CACHE[cacheKey][ALL_ENTITIES]) {
const entities = await DATA_CACHE[cacheKey][ALL_ENTITIES]; const entities = await DATA_CACHE[cacheKey][ALL_ENTITIES];
return entities.filter( return entities.filter((entity) => entity.entity_id === entityId);
(entity) => entity.entity_id && entityId.includes(entity.entity_id)
);
} }
DATA_CACHE[cacheKey][entityIdKey] = getLogbookDataFromServer( DATA_CACHE[cacheKey][entityId] = getLogbookDataFromServer(
hass, hass,
startDate, startDate,
endDate, endDate,
entityId entityId !== ALL_ENTITIES ? entityId : undefined,
); entity_matches_only
return DATA_CACHE[cacheKey][entityIdKey]; ).then((entries) => entries.reverse());
return DATA_CACHE[cacheKey][entityId];
}; };
const getLogbookDataFromServer = ( const getLogbookDataFromServer = async (
hass: HomeAssistant, hass: HomeAssistant,
startDate: string, startDate: string,
endDate?: string, endDate?: string,
entityIds?: string[], entityId?: string,
contextId?: string, entitymatchesOnly?: boolean,
deviceIds?: string[] contextId?: string
): Promise<LogbookEntry[]> => { ) => {
// If all specified filters are empty lists, we can return an empty list. const params = new URLSearchParams();
if (
(entityIds || deviceIds) &&
(!entityIds || entityIds.length === 0) &&
(!deviceIds || deviceIds.length === 0)
) {
return Promise.resolve([]);
}
const params: any = {
type: "logbook/get_events",
start_time: startDate,
};
if (endDate) { if (endDate) {
params.end_time = endDate; params.append("end_time", endDate);
} }
if (entityIds?.length) { if (entityId) {
params.entity_ids = entityIds; params.append("entity", entityId);
} }
if (deviceIds?.length) { if (entitymatchesOnly) {
params.device_ids = deviceIds; params.append("entity_matches_only", "");
} }
if (contextId) { if (contextId) {
params.context_id = contextId; params.append("context_id", contextId);
} }
return hass.callWS<LogbookEntry[]>(params);
};
export const subscribeLogbook = ( return hass.callApi<LogbookEntry[]>(
hass: HomeAssistant, "GET",
callbackFunction: (message: LogbookStreamMessage) => void, `logbook/${startDate}?${params.toString()}`
startDate: string,
endDate: string,
entityIds?: string[],
deviceIds?: string[]
): Promise<UnsubscribeFunc> => {
// If all specified filters are empty lists, we can return an empty list.
if (
(entityIds || deviceIds) &&
(!entityIds || entityIds.length === 0) &&
(!deviceIds || deviceIds.length === 0)
) {
return Promise.reject("No entities or devices");
}
const params: any = {
type: "logbook/event_stream",
start_time: startDate,
end_time: endDate,
};
if (entityIds?.length) {
params.entity_ids = entityIds;
}
if (deviceIds?.length) {
params.device_ids = deviceIds;
}
return hass.connection.subscribeMessage<LogbookStreamMessage>(
(message) => callbackFunction(message),
params
); );
}; };
@@ -205,49 +164,7 @@ export const clearLogbookCache = (startDate: string, endDate: string) => {
DATA_CACHE[`${startDate}${endDate}`] = {}; DATA_CACHE[`${startDate}${endDate}`] = {};
}; };
export const createHistoricState = ( export const getLogbookMessage = (
currentStateObj: HassEntity,
state?: string
): HassEntity => <HassEntity>(<unknown>{
entity_id: currentStateObj.entity_id,
state: state,
attributes: {
// Rebuild the historical state by copying static attributes only
device_class: currentStateObj?.attributes.device_class,
source_type: currentStateObj?.attributes.source_type,
has_date: currentStateObj?.attributes.has_date,
has_time: currentStateObj?.attributes.has_time,
// We do not want to use dynamic entity pictures (e.g., from media player) for the log book rendering,
// as they would present a false state in the log (played media right now vs actual historic data).
entity_picture_local: DOMAINS_WITH_DYNAMIC_PICTURE.has(
computeDomain(currentStateObj.entity_id)
)
? undefined
: currentStateObj?.attributes.entity_picture_local,
entity_picture: DOMAINS_WITH_DYNAMIC_PICTURE.has(
computeDomain(currentStateObj.entity_id)
)
? undefined
: currentStateObj?.attributes.entity_picture,
},
});
export const localizeTriggerSource = (
localize: LocalizeFunc,
source: string
) => {
for (const triggerPhrase in triggerPhrases) {
if (source.startsWith(triggerPhrase)) {
return source.replace(
triggerPhrase,
`${localize(`ui.components.logbook.${triggerPhrases[triggerPhrase]}`)}`
);
}
}
return source;
};
export const localizeStateMessage = (
hass: HomeAssistant, hass: HomeAssistant,
localize: LocalizeFunc, localize: LocalizeFunc,
state: string, state: string,

View File

@@ -131,9 +131,9 @@ export interface CallServiceActionConfig extends BaseActionConfig {
action: "call-service"; action: "call-service";
service: string; service: string;
target?: HassServiceTarget; target?: HassServiceTarget;
// "service_data" is kept for backwards compatibility. Replaced by "data". service_data?: {
service_data?: Record<string, unknown>; [key: string]: any;
data?: Record<string, unknown>; };
} }
export interface NavigateActionConfig extends BaseActionConfig { export interface NavigateActionConfig extends BaseActionConfig {

View File

@@ -47,17 +47,12 @@ export interface SceneConfig {
name: string; name: string;
icon?: string; icon?: string;
entities: SceneEntities; entities: SceneEntities;
metadata?: SceneMetaData;
} }
export interface SceneEntities { export interface SceneEntities {
[entityId: string]: string | { state: string; [key: string]: any }; [entityId: string]: string | { state: string; [key: string]: any };
} }
export interface SceneMetaData {
[entityId: string]: { entity_only?: boolean | undefined };
}
export const activateScene = ( export const activateScene = (
hass: HomeAssistant, hass: HomeAssistant,
entityId: string entityId: string

View File

@@ -52,7 +52,7 @@ export const getHassTranslations = async (
hass: HomeAssistant, hass: HomeAssistant,
language: string, language: string,
category: TranslationCategory, category: TranslationCategory,
integration?: string | string[], integration?: string,
config_flow?: boolean config_flow?: boolean
): Promise<Record<string, unknown>> => { ): Promise<Record<string, unknown>> => {
const result = await hass.callWS<{ resources: Record<string, unknown> }>({ const result = await hass.callWS<{ resources: Record<string, unknown> }>({

View File

@@ -7,10 +7,7 @@ import type {
import { BINARY_STATE_ON } from "../common/const"; import { BINARY_STATE_ON } from "../common/const";
import { computeDomain } from "../common/entity/compute_domain"; import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDomain } from "../common/entity/compute_state_domain"; import { computeStateDomain } from "../common/entity/compute_state_domain";
import { import { supportsFeature } from "../common/entity/supports-feature";
supportsFeature,
supportsFeatureFromAttributes,
} from "../common/entity/supports-feature";
import { caseInsensitiveStringCompare } from "../common/string/compare"; import { caseInsensitiveStringCompare } from "../common/string/compare";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
@@ -38,13 +35,8 @@ export interface UpdateEntity extends HassEntityBase {
} }
export const updateUsesProgress = (entity: UpdateEntity): boolean => export const updateUsesProgress = (entity: UpdateEntity): boolean =>
updateUsesProgressFromAttributes(entity.attributes); supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) &&
typeof entity.attributes.in_progress === "number";
export const updateUsesProgressFromAttributes = (attributes: {
[key: string]: any;
}): boolean =>
supportsFeatureFromAttributes(attributes, UPDATE_SUPPORT_PROGRESS) &&
typeof attributes.in_progress === "number";
export const updateCanInstall = ( export const updateCanInstall = (
entity: UpdateEntity, entity: UpdateEntity,
@@ -57,11 +49,6 @@ export const updateCanInstall = (
export const updateIsInstalling = (entity: UpdateEntity): boolean => export const updateIsInstalling = (entity: UpdateEntity): boolean =>
updateUsesProgress(entity) || !!entity.attributes.in_progress; updateUsesProgress(entity) || !!entity.attributes.in_progress;
export const updateIsInstallingFromAttributes = (attributes: {
[key: string]: any;
}): boolean =>
updateUsesProgressFromAttributes(attributes) || !!attributes.in_progress;
export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) => export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
hass.callWS<string | null>({ hass.callWS<string | null>({
type: "update/release_notes", type: "update/release_notes",

View File

@@ -2,21 +2,9 @@ import {
mdiAlertCircleOutline, mdiAlertCircleOutline,
mdiGauge, mdiGauge,
mdiWaterPercent, mdiWaterPercent,
mdiWeatherCloudy,
mdiWeatherFog, mdiWeatherFog,
mdiWeatherHail,
mdiWeatherLightning,
mdiWeatherLightningRainy,
mdiWeatherNight,
mdiWeatherNightPartlyCloudy,
mdiWeatherPartlyCloudy,
mdiWeatherPouring,
mdiWeatherRainy, mdiWeatherRainy,
mdiWeatherSnowy,
mdiWeatherSnowyRainy,
mdiWeatherSunny,
mdiWeatherWindy, mdiWeatherWindy,
mdiWeatherWindyVariant,
} from "@mdi/js"; } from "@mdi/js";
import { import {
HassEntityAttributeBase, HassEntityAttributeBase,
@@ -69,21 +57,7 @@ export const weatherSVGs = new Set<string>([
]); ]);
export const weatherIcons = { export const weatherIcons = {
"clear-night": mdiWeatherNight,
cloudy: mdiWeatherCloudy,
exceptional: mdiAlertCircleOutline, exceptional: mdiAlertCircleOutline,
fog: mdiWeatherFog,
hail: mdiWeatherHail,
lightning: mdiWeatherLightning,
"lightning-rainy": mdiWeatherLightningRainy,
partlycloudy: mdiWeatherPartlyCloudy,
pouring: mdiWeatherPouring,
rainy: mdiWeatherRainy,
snowy: mdiWeatherSnowy,
"snowy-rainy": mdiWeatherSnowyRainy,
sunny: mdiWeatherSunny,
windy: mdiWeatherWindy,
"windy-variant": mdiWeatherWindyVariant,
}; };
export const weatherAttrIcons = { export const weatherAttrIcons = {
@@ -463,13 +437,6 @@ export const getWeatherStateIcon = (
return undefined; return undefined;
}; };
export const weatherIcon = (state?: string, nightTime?: boolean): string =>
!state
? undefined
: nightTime && state === "partlycloudy"
? mdiWeatherNightPartlyCloudy
: weatherIcons[state];
const DAY_IN_MILLISECONDS = 86400000; const DAY_IN_MILLISECONDS = 86400000;
export const isForecastHourly = ( export const isForecastHourly = (

View File

@@ -145,7 +145,7 @@ export interface ZWaveJSController {
supports_timers: boolean; supports_timers: boolean;
is_heal_network_active: boolean; is_heal_network_active: boolean;
inclusion_state: InclusionState; inclusion_state: InclusionState;
nodes: ZWaveJSNodeStatus[]; nodes: number[];
} }
export interface ZWaveJSNodeStatus { export interface ZWaveJSNodeStatus {
@@ -167,9 +167,6 @@ export interface ZwaveJSNodeMetadata {
wakeup: string; wakeup: string;
reset: string; reset: string;
device_database_url: string; device_database_url: string;
}
export interface ZwaveJSNodeComments {
comments: ZWaveJSNodeComment[]; comments: ZWaveJSNodeComment[];
} }
@@ -203,7 +200,8 @@ export interface ZWaveJSNodeConfigParamMetadata {
export interface ZWaveJSSetConfigParamData { export interface ZWaveJSSetConfigParamData {
type: string; type: string;
device_id: string; entry_id: string;
node_id: number;
property: number; property: number;
property_key?: number; property_key?: number;
value: string | number; value: string | number;
@@ -230,20 +228,6 @@ export interface ZWaveJSHealNetworkStatusMessage {
heal_node_status: { [key: number]: string }; heal_node_status: { [key: number]: string };
} }
export interface ZWaveJSControllerStatisticsUpdatedMessage {
event: "statistics updated";
source: "controller";
messages_tx: number;
messages_rx: number;
messages_dropped_tx: number;
messages_dropped_rx: number;
nak: number;
can: number;
timeout_ack: number;
timeout_response: number;
timeout_callback: number;
}
export interface ZWaveJSRemovedNode { export interface ZWaveJSRemovedNode {
node_id: number; node_id: number;
manufacturer: string; manufacturer: string;
@@ -301,23 +285,12 @@ export const migrateZwave = (
export const fetchZwaveNetworkStatus = ( export const fetchZwaveNetworkStatus = (
hass: HomeAssistant, hass: HomeAssistant,
device_or_entry_id: { entry_id: string
device_id?: string; ): Promise<ZWaveJSNetwork> =>
entry_id?: string; hass.callWS({
}
): Promise<ZWaveJSNetwork> => {
if (device_or_entry_id.device_id && device_or_entry_id.entry_id) {
throw new Error("Only one of device or entry ID should be supplied.");
}
if (!device_or_entry_id.device_id && !device_or_entry_id.entry_id) {
throw new Error("Either device or entry ID should be supplied.");
}
return hass.callWS({
type: "zwave_js/network_status", type: "zwave_js/network_status",
device_id: device_or_entry_id.device_id, entry_id,
entry_id: device_or_entry_id.entry_id,
}); });
};
export const fetchZwaveDataCollectionStatus = ( export const fetchZwaveDataCollectionStatus = (
hass: HomeAssistant, hass: HomeAssistant,
@@ -454,50 +427,49 @@ export const unprovisionZwaveSmartStartNode = (
export const fetchZwaveNodeStatus = ( export const fetchZwaveNodeStatus = (
hass: HomeAssistant, hass: HomeAssistant,
device_id: string entry_id: string,
node_id: number
): Promise<ZWaveJSNodeStatus> => ): Promise<ZWaveJSNodeStatus> =>
hass.callWS({ hass.callWS({
type: "zwave_js/node_status", type: "zwave_js/node_status",
device_id, entry_id,
node_id,
}); });
export const fetchZwaveNodeMetadata = ( export const fetchZwaveNodeMetadata = (
hass: HomeAssistant, hass: HomeAssistant,
device_id: string entry_id: string,
node_id: number
): Promise<ZwaveJSNodeMetadata> => ): Promise<ZwaveJSNodeMetadata> =>
hass.callWS({ hass.callWS({
type: "zwave_js/node_metadata", type: "zwave_js/node_metadata",
device_id, entry_id,
}); node_id,
export const fetchZwaveNodeComments = (
hass: HomeAssistant,
device_id: string
): Promise<ZwaveJSNodeComments> =>
hass.callWS({
type: "zwave_js/node_comments",
device_id,
}); });
export const fetchZwaveNodeConfigParameters = ( export const fetchZwaveNodeConfigParameters = (
hass: HomeAssistant, hass: HomeAssistant,
device_id: string entry_id: string,
node_id: number
): Promise<ZWaveJSNodeConfigParams> => ): Promise<ZWaveJSNodeConfigParams> =>
hass.callWS({ hass.callWS({
type: "zwave_js/get_config_parameters", type: "zwave_js/get_config_parameters",
device_id, entry_id,
node_id,
}); });
export const setZwaveNodeConfigParameter = ( export const setZwaveNodeConfigParameter = (
hass: HomeAssistant, hass: HomeAssistant,
device_id: string, entry_id: string,
node_id: number,
property: number, property: number,
value: number, value: number,
property_key?: number property_key?: number
): Promise<ZWaveJSSetConfigParamResult> => { ): Promise<ZWaveJSSetConfigParamResult> => {
const data: ZWaveJSSetConfigParamData = { const data: ZWaveJSSetConfigParamData = {
type: "zwave_js/set_config_parameter", type: "zwave_js/set_config_parameter",
device_id, entry_id,
node_id,
property, property,
value, value,
property_key, property_key,
@@ -507,36 +479,42 @@ export const setZwaveNodeConfigParameter = (
export const reinterviewZwaveNode = ( export const reinterviewZwaveNode = (
hass: HomeAssistant, hass: HomeAssistant,
device_id: string, entry_id: string,
node_id: number,
callbackFunction: (message: ZWaveJSRefreshNodeStatusMessage) => void callbackFunction: (message: ZWaveJSRefreshNodeStatusMessage) => void
): Promise<UnsubscribeFunc> => ): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage( hass.connection.subscribeMessage(
(message: any) => callbackFunction(message), (message: any) => callbackFunction(message),
{ {
type: "zwave_js/refresh_node_info", type: "zwave_js/refresh_node_info",
device_id, entry_id,
node_id,
} }
); );
export const healZwaveNode = ( export const healZwaveNode = (
hass: HomeAssistant, hass: HomeAssistant,
device_id: string entry_id: string,
node_id: number
): Promise<boolean> => ): Promise<boolean> =>
hass.callWS({ hass.callWS({
type: "zwave_js/heal_node", type: "zwave_js/heal_node",
device_id, entry_id,
node_id,
}); });
export const removeFailedZwaveNode = ( export const removeFailedZwaveNode = (
hass: HomeAssistant, hass: HomeAssistant,
device_id: string, entry_id: string,
node_id: number,
callbackFunction: (message: any) => void callbackFunction: (message: any) => void
): Promise<UnsubscribeFunc> => ): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage( hass.connection.subscribeMessage(
(message: any) => callbackFunction(message), (message: any) => callbackFunction(message),
{ {
type: "zwave_js/remove_failed_node", type: "zwave_js/remove_failed_node",
device_id, entry_id,
node_id,
} }
); );
@@ -560,14 +538,16 @@ export const stopHealZwaveNetwork = (
export const subscribeZwaveNodeReady = ( export const subscribeZwaveNodeReady = (
hass: HomeAssistant, hass: HomeAssistant,
device_id: string, entry_id: string,
node_id: number,
callbackFunction: (message) => void callbackFunction: (message) => void
): Promise<UnsubscribeFunc> => ): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage( hass.connection.subscribeMessage(
(message: any) => callbackFunction(message), (message: any) => callbackFunction(message),
{ {
type: "zwave_js/node_ready", type: "zwave_js/node_ready",
device_id, entry_id,
node_id,
} }
); );
@@ -584,19 +564,6 @@ export const subscribeHealZwaveNetworkProgress = (
} }
); );
export const subscribeZwaveControllerStatistics = (
hass: HomeAssistant,
entry_id: string,
callbackFunction: (message: ZWaveJSControllerStatisticsUpdatedMessage) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage(
(message: any) => callbackFunction(message),
{
type: "zwave_js/subscribe_controller_statistics",
entry_id,
}
);
export const getZwaveJsIdentifiersFromDevice = ( export const getZwaveJsIdentifiersFromDevice = (
device: DeviceRegistryEntry device: DeviceRegistryEntry
): ZWaveJSNodeIdentifiers | undefined => { ): ZWaveJSNodeIdentifiers | undefined => {

View File

@@ -309,7 +309,7 @@ class DataEntryFlowDialog extends LitElement {
: this._step.type === "abort" : this._step.type === "abort"
? html` ? html`
<step-flow-abort <step-flow-abort
.params=${this._params} .flowConfig=${this._params.flowConfig}
.step=${this._step} .step=${this._step}
.hass=${this.hass} .hass=${this.hass}
.domain=${this._step.handler} .domain=${this._step.handler}
@@ -518,9 +518,10 @@ class DataEntryFlowDialog extends LitElement {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
inset-inline-start: initial; }
inset-inline-end: 0px; :host-context([style*="direction: rtl;"]) .dialog-actions {
direction: var(--direction); right: auto;
left: 0;
} }
.dialog-actions > * { .dialog-actions > * {
color: var(--secondary-text-color); color: var(--secondary-text-color);

View File

@@ -131,7 +131,6 @@ export interface DataEntryFlowDialogParams {
}) => void; }) => void;
flowConfig: FlowConfig; flowConfig: FlowConfig;
showAdvanced?: boolean; showAdvanced?: boolean;
dialogParentElement?: HTMLElement;
} }
export const loadDataEntryFlowDialog = () => import("./dialog-data-entry-flow"); export const loadDataEntryFlowDialog = () => import("./dialog-data-entry-flow");
@@ -147,7 +146,6 @@ export const showFlowDialog = (
dialogParams: { dialogParams: {
...dialogParams, ...dialogParams,
flowConfig, flowConfig,
dialogParentElement: element,
}, },
}); });
}; };

View File

@@ -1,25 +1,15 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
CSSResultGroup,
html,
LitElement,
TemplateResult,
PropertyValues,
} from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { DataEntryFlowStepAbort } from "../../data/data_entry_flow"; import { DataEntryFlowStepAbort } from "../../data/data_entry_flow";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { showAddApplicationCredentialDialog } from "../../panels/config/application_credentials/show-dialog-add-application-credential"; import { FlowConfig } from "./show-dialog-data-entry-flow";
import { configFlowContentStyles } from "./styles"; import { configFlowContentStyles } from "./styles";
import { showConfirmationDialog } from "../generic/show-dialog-box";
import { domainToName } from "../../data/integration";
import { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow";
import { showConfigFlowDialog } from "./show-dialog-config-flow";
@customElement("step-flow-abort") @customElement("step-flow-abort")
class StepFlowAbort extends LitElement { class StepFlowAbort extends LitElement {
@property({ attribute: false }) public params!: DataEntryFlowDialogParams; @property({ attribute: false }) public flowConfig!: FlowConfig;
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -27,21 +17,11 @@ class StepFlowAbort extends LitElement {
@property({ attribute: false }) public domain!: string; @property({ attribute: false }) public domain!: string;
protected firstUpdated(changed: PropertyValues) {
super.firstUpdated(changed);
if (this.step.reason === "missing_credentials") {
this._handleMissingCreds();
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (this.step.reason === "missing_credentials") {
return html``;
}
return html` return html`
<h2>${this.hass.localize(`component.${this.domain}.title`)}</h2> <h2>${this.hass.localize(`component.${this.domain}.title`)}</h2>
<div class="content"> <div class="content">
${this.params.flowConfig.renderAbortDescription(this.hass, this.step)} ${this.flowConfig.renderAbortDescription(this.hass, this.step)}
</div> </div>
<div class="buttons"> <div class="buttons">
<mwc-button @click=${this._flowDone} <mwc-button @click=${this._flowDone}
@@ -53,32 +33,6 @@ class StepFlowAbort extends LitElement {
`; `;
} }
private async _handleMissingCreds() {
const confirm = await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.missing_credentials",
{
integration: domainToName(this.hass.localize, this.domain),
}
),
});
this._flowDone();
if (!confirm) {
return;
}
// Prompt to enter credentials and restart integration setup
showAddApplicationCredentialDialog(this.params.dialogParentElement!, {
selectedDomain: this.domain,
applicationCredentialAddedCallback: () => {
showConfigFlowDialog(this.params.dialogParentElement!, {
dialogClosedCallback: this.params.dialogClosedCallback,
startFlowHandler: this.domain,
showAdvanced: this.hass.userData?.showAdvanced,
});
},
});
}
private _flowDone(): void { private _flowDone(): void {
fireEvent(this, "flow-update", { step: undefined }); fireEvent(this, "flow-update", { step: undefined });
} }

View File

@@ -192,8 +192,11 @@ class StepFlowForm extends LitElement {
} }
h2 { h2 {
word-break: break-word; word-break: break-word;
padding-inline-end: 72px; padding-right: 72px;
direction: var(--direction); }
:host-context([style*="direction: rtl;"]) h2 {
padding-right: auto !important;
padding-left: 72px !important;
} }
`, `,
]; ];

View File

@@ -104,8 +104,11 @@ class StepFlowPickFlow extends LitElement {
margin: 16px 0; margin: 16px 0;
} }
h2 { h2 {
padding-inline-end: 66px; padding-right: 66px;
direction: var(--direction); }
:host-context([style*="direction: rtl;"]) h2 {
padding-right: auto !important;
padding-left: 66px !important;
} }
@media all and (max-height: 900px) { @media all and (max-height: 900px) {
div { div {

View File

@@ -311,8 +311,11 @@ class StepFlowPickHandler extends LitElement {
border-bottom-color: var(--divider-color); border-bottom-color: var(--divider-color);
} }
h2 { h2 {
padding-inline-end: 66px; padding-right: 66px;
direction: var(--direction); }
:host-context([style*="direction: rtl;"]) h2 {
padding-right: auto !important;
padding-left: 66px !important;
} }
@media all and (max-height: 900px) { @media all and (max-height: 900px) {
mwc-list { mwc-list {

View File

@@ -3,11 +3,7 @@ import { css } from "lit";
export const configFlowContentStyles = css` export const configFlowContentStyles = css`
h2 { h2 {
margin: 24px 38px 0 0; margin: 24px 38px 0 0;
margin-inline-start: 0px;
margin-inline-end: 38px;
padding: 0 24px; padding: 0 24px;
padding-inline-start: 24px;
padding-inline-end: 24px;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
font-family: var( font-family: var(

View File

@@ -1,9 +1,6 @@
import { HASSDomEvent, ValidHassDomEvent } from "../common/dom/fire_event"; import { HASSDomEvent, ValidHassDomEvent } from "../common/dom/fire_event";
import { mainWindow } from "../common/dom/get_main_window"; import { mainWindow } from "../common/dom/get_main_window";
import { ProvideHassElement } from "../mixins/provide-hass-lit-mixin"; import { ProvideHassElement } from "../mixins/provide-hass-lit-mixin";
import { ancestorsWithProperty } from "../common/dom/ancestors-with-property";
import { deepActiveElement } from "../common/dom/deep-active-element";
import { nextRender } from "../common/util/render-status";
declare global { declare global {
// for fire event // for fire event
@@ -43,17 +40,7 @@ export interface DialogState {
dialogParams?: unknown; dialogParams?: unknown;
} }
interface LoadedDialogInfo { const LOADED = {};
element: Promise<HassDialog>;
closedFocusTargets?: Set<Element>;
}
interface LoadedDialogsDict {
[tag: string]: LoadedDialogInfo;
}
const LOADED: LoadedDialogsDict = {};
export const FOCUS_TARGET = Symbol.for("HA focus target");
export const showDialog = async ( export const showDialog = async (
element: HTMLElement & ProvideHassElement, element: HTMLElement & ProvideHassElement,
@@ -73,25 +60,11 @@ export const showDialog = async (
} }
return; return;
} }
LOADED[dialogTag] = { LOADED[dialogTag] = dialogImport().then(() => {
element: dialogImport().then(() => {
const dialogEl = document.createElement(dialogTag) as HassDialog; const dialogEl = document.createElement(dialogTag) as HassDialog;
element.provideHass(dialogEl); element.provideHass(dialogEl);
return dialogEl; return dialogEl;
}), });
};
}
// Get the focus targets after the dialog closes, but keep the original if dialog is being replaced
if (mainWindow.history.state?.replaced) {
LOADED[dialogTag].closedFocusTargets =
LOADED[mainWindow.history.state.dialog].closedFocusTargets;
delete LOADED[mainWindow.history.state.dialog].closedFocusTargets;
} else {
LOADED[dialogTag].closedFocusTargets = ancestorsWithProperty(
deepActiveElement(),
FOCUS_TARGET
);
} }
if (addHistory) { if (addHistory) {
@@ -120,29 +93,25 @@ export const showDialog = async (
); );
} }
} }
const dialogElement = await LOADED[dialogTag];
const dialogElement = await LOADED[dialogTag].element;
dialogElement.addEventListener("dialog-closed", _handleClosedFocus);
// Append it again so it's the last element in the root, // Append it again so it's the last element in the root,
// so it's guaranteed to be on top of the other elements // so it's guaranteed to be on top of the other elements
root.appendChild(dialogElement); root.appendChild(dialogElement);
dialogElement.showDialog(dialogParams); dialogElement.showDialog(dialogParams);
}; };
export const replaceDialog = (dialogElement: HassDialog) => { export const replaceDialog = () => {
mainWindow.history.replaceState( mainWindow.history.replaceState(
{ ...mainWindow.history.state, replaced: true }, { ...mainWindow.history.state, replaced: true },
"" ""
); );
dialogElement.removeEventListener("dialog-closed", _handleClosedFocus);
}; };
export const closeDialog = async (dialogTag: string): Promise<boolean> => { export const closeDialog = async (dialogTag: string): Promise<boolean> => {
if (!(dialogTag in LOADED)) { if (!(dialogTag in LOADED)) {
return true; return true;
} }
const dialogElement = await LOADED[dialogTag].element; const dialogElement: HassDialog = await LOADED[dialogTag];
if (dialogElement.closeDialog) { if (dialogElement.closeDialog) {
return dialogElement.closeDialog() !== false; return dialogElement.closeDialog() !== false;
} }
@@ -168,33 +137,3 @@ export const makeDialogManager = (
} }
); );
}; };
const _handleClosedFocus = async (ev: HASSDomEvent<DialogClosedParams>) => {
const closedFocusTargets = LOADED[ev.detail.dialog].closedFocusTargets;
delete LOADED[ev.detail.dialog].closedFocusTargets;
if (!closedFocusTargets) return;
// Undo whatever the browser focused to provide easy checking
let focusedElement = deepActiveElement();
if (focusedElement instanceof HTMLElement) focusedElement.blur();
// Make sure backdrop is fully updated before trying (especially needed for underlay dialogs)
await nextRender();
// Try all targets in order and stop when one works
for (const focusTarget of closedFocusTargets) {
if (focusTarget instanceof HTMLElement) {
focusTarget.focus();
focusedElement = deepActiveElement();
if (focusedElement && focusedElement !== document.body) return;
}
}
if (__DEV__) {
// eslint-disable-next-line no-console
console.warn(
"Failed to focus any targets after closing dialog: %o",
closedFocusTargets
);
}
};

View File

@@ -1,14 +1,35 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-camera-stream"; import "../../../components/ha-camera-stream";
import { CameraEntity } from "../../../data/camera"; import type { HaCheckbox } from "../../../components/ha-checkbox";
import "../../../components/ha-checkbox";
import {
CameraEntity,
CameraPreferences,
CAMERA_SUPPORT_STREAM,
fetchCameraPrefs,
STREAM_TYPE_HLS,
updateCameraPrefs,
} from "../../../data/camera";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import "../../../components/ha-formfield";
class MoreInfoCamera extends LitElement { class MoreInfoCamera extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: CameraEntity; @property({ attribute: false }) public stateObj?: CameraEntity;
@state() private _cameraPrefs?: CameraPreferences;
@state() private _attached = false; @state() private _attached = false;
public connectedCallback() { public connectedCallback() {
@@ -33,13 +54,83 @@ class MoreInfoCamera extends LitElement {
allow-exoplayer allow-exoplayer
controls controls
></ha-camera-stream> ></ha-camera-stream>
${this._cameraPrefs
? html`
<ha-formfield label="Preload stream">
<ha-checkbox
.checked=${this._cameraPrefs.preload_stream}
@change=${this._handleCheckboxChanged}
>
</ha-checkbox>
</ha-formfield>
`
: undefined}
`; `;
} }
protected updated(changedProps: PropertyValues) {
if (!changedProps.has("stateObj")) {
return;
}
const oldState = changedProps.get("stateObj") as this["stateObj"];
const oldEntityId = oldState ? oldState.entity_id : undefined;
const curEntityId = this.stateObj ? this.stateObj.entity_id : undefined;
// Same entity, ignore.
if (curEntityId === oldEntityId) {
return;
}
if (
curEntityId &&
isComponentLoaded(this.hass!, "stream") &&
supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM) &&
// The stream component for HLS streams supports a server-side pre-load
// option that client initiated WebRTC streams do not
this.stateObj!.attributes.frontend_stream_type === STREAM_TYPE_HLS
) {
// Fetch in background while we set up the video.
this._fetchCameraPrefs();
}
}
private async _fetchCameraPrefs() {
this._cameraPrefs = await fetchCameraPrefs(
this.hass!,
this.stateObj!.entity_id
);
}
private async _handleCheckboxChanged(ev) {
const checkbox = ev.currentTarget as HaCheckbox;
try {
this._cameraPrefs = await updateCameraPrefs(
this.hass!,
this.stateObj!.entity_id,
{
preload_stream: checkbox.checked!,
}
);
} catch (err: any) {
alert(err.message);
checkbox.checked = !checkbox.checked;
}
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host { :host {
display: block; display: block;
position: relative;
}
ha-formfield {
position: absolute;
top: 0;
right: 0;
background-color: var(--secondary-background-color);
padding-right: 16px;
border-bottom-left-radius: 4px;
} }
`; `;
} }

View File

@@ -1,9 +1,23 @@
import { import {
mdiAlertCircleOutline,
mdiEye, mdiEye,
mdiGauge, mdiGauge,
mdiThermometer, mdiThermometer,
mdiWaterPercent, mdiWaterPercent,
mdiWeatherCloudy,
mdiWeatherFog,
mdiWeatherHail,
mdiWeatherLightning,
mdiWeatherLightningRainy,
mdiWeatherNight,
mdiWeatherPartlyCloudy,
mdiWeatherPouring,
mdiWeatherRainy,
mdiWeatherSnowy,
mdiWeatherSnowyRainy,
mdiWeatherSunny,
mdiWeatherWindy, mdiWeatherWindy,
mdiWeatherWindyVariant,
} from "@mdi/js"; } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { import {
@@ -23,10 +37,27 @@ import {
getWeatherUnit, getWeatherUnit,
getWind, getWind,
isForecastHourly, isForecastHourly,
weatherIcons,
} from "../../../data/weather"; } from "../../../data/weather";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
const weatherIcons = {
"clear-night": mdiWeatherNight,
cloudy: mdiWeatherCloudy,
exceptional: mdiAlertCircleOutline,
fog: mdiWeatherFog,
hail: mdiWeatherHail,
lightning: mdiWeatherLightning,
"lightning-rainy": mdiWeatherLightningRainy,
partlycloudy: mdiWeatherPartlyCloudy,
pouring: mdiWeatherPouring,
rainy: mdiWeatherRainy,
snowy: mdiWeatherSnowy,
"snowy-rainy": mdiWeatherSnowyRainy,
sunny: mdiWeatherSunny,
windy: mdiWeatherWindy,
"windy-variant": mdiWeatherWindyVariant,
};
@customElement("more-info-weather") @customElement("more-info-weather")
class MoreInfoWeather extends LitElement { class MoreInfoWeather extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -204,7 +235,6 @@ class MoreInfoWeather extends LitElement {
return css` return css`
ha-svg-icon { ha-svg-icon {
color: var(--paper-item-icon-color); color: var(--paper-item-icon-color);
margin-left: 8px;
} }
.section { .section {
margin: 16px 0 8px 0; margin: 16px 0 8px 0;

View File

@@ -295,7 +295,7 @@ export class MoreInfoDialog extends LitElement {
} }
private _gotoSettings() { private _gotoSettings() {
replaceDialog(this); replaceDialog();
showEntityEditorDialog(this, { showEntityEditorDialog(this, {
entity_id: this._entityId!, entity_id: this._entityId!,
}); });

View File

@@ -1,4 +1,4 @@
import { startOfYesterday } from "date-fns/esm"; import { startOfYesterday } from "date-fns";
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 { isComponentLoaded } from "../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../common/config/is_component_loaded";

View File

@@ -1,11 +1,17 @@
import { startOfYesterday } from "date-fns/esm"; import { startOfYesterday } from "date-fns";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property } 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 { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { throttle } from "../../common/util/throttle";
import "../../components/ha-circular-progress";
import { getLogbookData, LogbookEntry } from "../../data/logbook";
import { loadTraceContexts, TraceContexts } from "../../data/trace";
import { fetchUsers } from "../../data/user";
import "../../panels/logbook/ha-logbook"; import "../../panels/logbook/ha-logbook";
import type { HomeAssistant } from "../../types"; import { haStyle } from "../../resources/styles";
import { HomeAssistant } from "../../types";
@customElement("ha-more-info-logbook") @customElement("ha-more-info-logbook")
export class MoreInfoLogbook extends LitElement { export class MoreInfoLogbook extends LitElement {
@@ -13,14 +19,26 @@ export class MoreInfoLogbook extends LitElement {
@property() public entityId!: string; @property() public entityId!: string;
@state() private _logbookEntries?: LogbookEntry[];
@state() private _traceContexts?: TraceContexts;
@state() private _userIdToName = {};
private _lastLogbookDate?: Date;
private _fetchUserPromise?: Promise<void>;
private _error?: string;
private _showMoreHref = ""; private _showMoreHref = "";
private _time = { recent: 86400 }; private _throttleGetLogbookEntries = throttle(() => {
this._getLogBookData();
private _entityIdAsList = memoizeOne((entityId: string) => [entityId]); }, 10000);
protected render(): TemplateResult { protected render(): TemplateResult {
if (!isComponentLoaded(this.hass, "logbook") || !this.entityId) { if (!this.entityId) {
return html``; return html``;
} }
const stateObj = this.hass.states[this.entityId]; const stateObj = this.hass.states[this.entityId];
@@ -30,34 +48,150 @@ export class MoreInfoLogbook extends LitElement {
} }
return html` return html`
${isComponentLoaded(this.hass, "logbook")
? this._error
? html`<div class="no-entries">
${`${this.hass.localize(
"ui.components.logbook.retrieval_error"
)}: ${this._error}`}
</div>`
: !this._logbookEntries
? html`
<ha-circular-progress
active
alt=${this.hass.localize("ui.common.loading")}
></ha-circular-progress>
`
: this._logbookEntries.length
? html`
<div class="header"> <div class="header">
<div class="title"> <div class="title">
${this.hass.localize("ui.dialogs.more_info_control.logbook")} ${this.hass.localize("ui.dialogs.more_info_control.logbook")}
</div> </div>
<a href=${this._showMoreHref} @click=${this._close} <a href=${this._showMoreHref} @click=${this._close}
>${this.hass.localize("ui.dialogs.more_info_control.show_more")}</a >${this.hass.localize(
"ui.dialogs.more_info_control.show_more"
)}</a
> >
</div> </div>
<ha-logbook <ha-logbook
.hass=${this.hass}
.time=${this._time}
.entityIds=${this._entityIdAsList(this.entityId)}
narrow narrow
no-icon no-icon
no-name no-name
relative-time relative-time
.hass=${this.hass}
.entries=${this._logbookEntries}
.traceContexts=${this._traceContexts}
.userIdToName=${this._userIdToName}
></ha-logbook> ></ha-logbook>
`
: html`<div class="no-entries">
${this.hass.localize("ui.components.logbook.entries_not_found")}
</div>`
: ""}
`; `;
} }
protected willUpdate(changedProps: PropertyValues): void { protected firstUpdated(): void {
super.willUpdate(changedProps); this._fetchUserPromise = this._fetchUserNames();
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (changedProps.has("entityId")) {
this._lastLogbookDate = undefined;
this._logbookEntries = undefined;
if (!this.entityId) {
return;
}
if (changedProps.has("entityId") && this.entityId) {
this._showMoreHref = `/logbook?entity_id=${ this._showMoreHref = `/logbook?entity_id=${
this.entityId this.entityId
}&start_date=${startOfYesterday().toISOString()}`; }&start_date=${startOfYesterday().toISOString()}`;
this._throttleGetLogbookEntries();
return;
} }
if (!this.entityId || !changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (
oldHass &&
this.hass.states[this.entityId] !== oldHass?.states[this.entityId]
) {
// wait for commit of data (we only account for the default setting of 1 sec)
setTimeout(this._throttleGetLogbookEntries, 1000);
}
}
private async _getLogBookData() {
if (!isComponentLoaded(this.hass, "logbook")) {
return;
}
const lastDate =
this._lastLogbookDate ||
new Date(new Date().getTime() - 24 * 60 * 60 * 1000);
const now = new Date();
let newEntries;
let traceContexts;
try {
[newEntries, traceContexts] = await Promise.all([
getLogbookData(
this.hass,
lastDate.toISOString(),
now.toISOString(),
this.entityId,
true
),
this.hass.user?.is_admin ? loadTraceContexts(this.hass) : {},
this._fetchUserPromise,
]);
} catch (err: any) {
this._error = err.message;
}
this._logbookEntries = this._logbookEntries
? [...newEntries, ...this._logbookEntries]
: newEntries;
this._lastLogbookDate = now;
this._traceContexts = traceContexts;
}
private async _fetchUserNames() {
const userIdToName = {};
// Start loading users
const userProm = this.hass.user?.is_admin && fetchUsers(this.hass);
// Process persons
Object.values(this.hass.states).forEach((entity) => {
if (
entity.attributes.user_id &&
computeStateDomain(entity) === "person"
) {
this._userIdToName[entity.attributes.user_id] =
entity.attributes.friendly_name;
}
});
// Process users
if (userProm) {
const users = await userProm;
for (const user of users) {
if (!(user.id in userIdToName)) {
userIdToName[user.id] = user.name;
}
}
}
this._userIdToName = userIdToName;
} }
private _close(): void { private _close(): void {
@@ -66,7 +200,13 @@ export class MoreInfoLogbook extends LitElement {
static get styles() { static get styles() {
return [ return [
haStyle,
css` css`
.no-entries {
text-align: center;
padding: 16px;
color: var(--secondary-text-color);
}
ha-logbook { ha-logbook {
--logbook-max-height: 250px; --logbook-max-height: 250px;
} }
@@ -75,6 +215,10 @@ export class MoreInfoLogbook extends LitElement {
--logbook-max-height: unset; --logbook-max-height: unset;
} }
} }
ha-circular-progress {
display: flex;
justify-content: center;
}
.header { .header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@@ -49,14 +49,12 @@ class OnboardingIntegrations extends LitElement {
this.hass.loadBackendTranslation("title", undefined, true); this.hass.loadBackendTranslation("title", undefined, true);
this._unsubEvents = subscribeConfigFlowInProgress(this.hass, (flows) => { this._unsubEvents = subscribeConfigFlowInProgress(this.hass, (flows) => {
this._discovered = flows; this._discovered = flows;
const integrations: Set<string> = new Set();
for (const flow of flows) { for (const flow of flows) {
// To render title placeholders // To render title placeholders
if (flow.context.title_placeholders) { if (flow.context.title_placeholders) {
integrations.add(flow.handler); this.hass.loadBackendTranslation("config", flow.handler);
} }
} }
this.hass.loadBackendTranslation("config", Array.from(integrations));
}); });
} }

View File

@@ -336,9 +336,6 @@ export class HAFullCalendar extends LitElement {
.today { .today {
margin-right: 20px; margin-right: 20px;
margin-inline-end: 20px;
margin-inline-start: initial;
direction: var(--direction);
} }
.prev, .prev,

View File

@@ -194,13 +194,10 @@ class PanelCalendar extends LitElement {
.calendar-list { .calendar-list {
padding-right: 16px; padding-right: 16px;
padding-inline-end: 16px;
padding-inline-start: initial;
min-width: 170px; min-width: 170px;
flex: 0 0 15%; flex: 0 0 15%;
overflow: hidden; overflow: hidden;
--mdc-theme-text-primary-on-background: var(--primary-text-color); --mdc-theme-text-primary-on-background: var(--primary-text-color);
direction: var(--direction);
} }
.calendar-list > div { .calendar-list > div {

View File

@@ -41,8 +41,6 @@ export class DialogAddApplicationCredential extends LitElement {
@state() private _domain?: string; @state() private _domain?: string;
@state() private _name?: string;
@state() private _clientId?: string; @state() private _clientId?: string;
@state() private _clientSecret?: string; @state() private _clientSecret?: string;
@@ -51,9 +49,7 @@ export class DialogAddApplicationCredential extends LitElement {
public showDialog(params: AddApplicationCredentialDialogParams) { public showDialog(params: AddApplicationCredentialDialogParams) {
this._params = params; this._params = params;
this._domain = this._domain = "";
params.selectedDomain !== undefined ? params.selectedDomain : "";
this._name = "";
this._clientId = ""; this._clientId = "";
this._clientSecret = ""; this._clientSecret = "";
this._error = undefined; this._error = undefined;
@@ -76,7 +72,7 @@ export class DialogAddApplicationCredential extends LitElement {
return html` return html`
<ha-dialog <ha-dialog
open open
@closed=${this._abortDialog} @closed=${this.closeDialog}
scrimClickAction scrimClickAction
escapeKeyAction escapeKeyAction
.heading=${createCloseHeading( .heading=${createCloseHeading(
@@ -91,7 +87,6 @@ export class DialogAddApplicationCredential extends LitElement {
<ha-combo-box <ha-combo-box
name="domain" name="domain"
.hass=${this.hass} .hass=${this.hass}
.disabled=${!!this._params.selectedDomain}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.domain" "ui.panel.config.application_credentials.editor.domain"
)} )}
@@ -104,18 +99,6 @@ export class DialogAddApplicationCredential extends LitElement {
required required
@value-changed=${this._handleDomainPicked} @value-changed=${this._handleDomainPicked}
></ha-combo-box> ></ha-combo-box>
<ha-textfield
class="name"
name="name"
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.name"
)}
.value=${this._name}
required
@input=${this._handleValueChanged}
error-message=${this.hass.localize("ui.common.error_required")}
dialogInitialFocus
></ha-textfield>
<ha-textfield <ha-textfield
class="clientId" class="clientId"
name="clientId" name="clientId"
@@ -183,13 +166,6 @@ export class DialogAddApplicationCredential extends LitElement {
this[`_${name}`] = value; this[`_${name}`] = value;
} }
private _abortDialog() {
if (this._params && this._params.dialogAbortedCallback) {
this._params.dialogAbortedCallback();
}
this.closeDialog();
}
private async _createApplicationCredential(ev) { private async _createApplicationCredential(ev) {
ev.preventDefault(); ev.preventDefault();
if (!this._domain || !this._clientId || !this._clientSecret) { if (!this._domain || !this._clientId || !this._clientSecret) {
@@ -205,8 +181,7 @@ export class DialogAddApplicationCredential extends LitElement {
this.hass, this.hass,
this._domain, this._domain,
this._clientId, this._clientId,
this._clientSecret, this._clientSecret
this._name
); );
} catch (err: any) { } catch (err: any) {
this._loading = false; this._loading = false;

View File

@@ -19,10 +19,7 @@ import {
fetchApplicationCredentials, fetchApplicationCredentials,
} from "../../../data/application_credential"; } from "../../../data/application_credential";
import { domainToName } from "../../../data/integration"; import { domainToName } from "../../../data/integration";
import { import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-tabs-subpage-data-table"; import "../../../layouts/hass-tabs-subpage-data-table";
import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table"; import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
@@ -49,22 +46,13 @@ export class HaConfigApplicationCredentials extends LitElement {
private _columns = memoizeOne( private _columns = memoizeOne(
(narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => { (narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => {
const columns: DataTableColumnContainer<ApplicationCredential> = { const columns: DataTableColumnContainer<ApplicationCredential> = {
name: {
title: localize(
"ui.panel.config.application_credentials.picker.headers.name"
),
width: "40%",
direction: "asc",
grows: true,
template: (_, entry: ApplicationCredential) => html`${entry.name}`,
},
clientId: { clientId: {
title: localize( title: localize(
"ui.panel.config.application_credentials.picker.headers.client_id" "ui.panel.config.application_credentials.picker.headers.client_id"
), ),
width: "30%", width: "25%",
direction: "asc", direction: "asc",
hidden: narrow, grows: true,
template: (_, entry: ApplicationCredential) => template: (_, entry: ApplicationCredential) =>
html`${entry.client_id}`, html`${entry.client_id}`,
}, },
@@ -73,8 +61,9 @@ export class HaConfigApplicationCredentials extends LitElement {
"ui.panel.config.application_credentials.picker.headers.application" "ui.panel.config.application_credentials.picker.headers.application"
), ),
sortable: true, sortable: true,
width: "30%", width: "20%",
direction: "asc", direction: "asc",
hidden: narrow,
template: (_, entry) => html`${domainToName(localize, entry.domain)}`, template: (_, entry) => html`${domainToName(localize, entry.domain)}`,
}, },
}; };
@@ -182,24 +171,11 @@ export class HaConfigApplicationCredentials extends LitElement {
confirmText: this.hass.localize("ui.common.remove"), confirmText: this.hass.localize("ui.common.remove"),
dismissText: this.hass.localize("ui.common.cancel"), dismissText: this.hass.localize("ui.common.cancel"),
confirm: async () => { confirm: async () => {
try {
await Promise.all( await Promise.all(
this._selected.map(async (applicationCredential) => { this._selected.map(async (applicationCredential) => {
await deleteApplicationCredential( await deleteApplicationCredential(this.hass, applicationCredential);
this.hass,
applicationCredential
);
}) })
); );
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.application_credentials.picker.remove_selected.error_title"
),
text: err.message,
});
return;
}
this._dataTable.clearSelection(); this._dataTable.clearSelection();
this._fetchApplicationCredentials(); this._fetchApplicationCredentials();
}, },
@@ -252,8 +228,6 @@ export class HaConfigApplicationCredentials extends LitElement {
.selected-txt { .selected-txt {
font-weight: bold; font-weight: bold;
padding-left: 16px; padding-left: 16px;
padding-inline-start: 16px;
direction: var(--direction);
} }
.table-header .selected-txt { .table-header .selected-txt {
margin-top: 20px; margin-top: 20px;

View File

@@ -5,8 +5,6 @@ export interface AddApplicationCredentialDialogParams {
applicationCredentialAddedCallback: ( applicationCredentialAddedCallback: (
applicationCredential: ApplicationCredential applicationCredential: ApplicationCredential
) => void; ) => void;
dialogAbortedCallback?: () => void;
selectedDomain?: string;
} }
export const loadAddApplicationCredentialDialog = () => export const loadAddApplicationCredentialDialog = () =>

View File

@@ -2,10 +2,7 @@ import "@material/mwc-button";
import { mdiImagePlus, mdiPencil } from "@mdi/js"; import { mdiImagePlus, mdiPencil } from "@mdi/js";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
import { import { HassEntity } from "home-assistant-js-websocket/dist/types";
HassEntity,
UnsubscribeFunc,
} from "home-assistant-js-websocket/dist/types";
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 { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
@@ -22,7 +19,6 @@ import "../../../components/ha-icon-next";
import { import {
AreaRegistryEntry, AreaRegistryEntry,
deleteAreaRegistryEntry, deleteAreaRegistryEntry,
subscribeAreaRegistry,
updateAreaRegistryEntry, updateAreaRegistryEntry,
} from "../../../data/area_registry"; } from "../../../data/area_registry";
import { AutomationEntity } from "../../../data/automation"; import { AutomationEntity } from "../../../data/automation";
@@ -30,22 +26,18 @@ import {
computeDeviceName, computeDeviceName,
DeviceRegistryEntry, DeviceRegistryEntry,
sortDeviceRegistryByName, sortDeviceRegistryByName,
subscribeDeviceRegistry,
} from "../../../data/device_registry"; } from "../../../data/device_registry";
import { import {
computeEntityRegistryName, computeEntityRegistryName,
EntityRegistryEntry, EntityRegistryEntry,
sortEntityRegistryByName, sortEntityRegistryByName,
subscribeEntityRegistry,
} from "../../../data/entity_registry"; } from "../../../data/entity_registry";
import { SceneEntity } from "../../../data/scene"; import { SceneEntity } from "../../../data/scene";
import { ScriptEntity } from "../../../data/script"; import { ScriptEntity } from "../../../data/script";
import { findRelated, RelatedResult } from "../../../data/search"; import { findRelated, RelatedResult } from "../../../data/search";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
import "../../logbook/ha-logbook";
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 { import {
@@ -59,11 +51,17 @@ declare type NameAndEntity<EntityType extends HassEntity> = {
}; };
@customElement("ha-config-area-page") @customElement("ha-config-area-page")
class HaConfigAreaPage extends SubscribeMixin(LitElement) { class HaConfigAreaPage extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public areaId!: string; @property() public areaId!: string;
@property() public areas!: AreaRegistryEntry[];
@property() public devices!: DeviceRegistryEntry[];
@property() public entities!: EntityRegistryEntry[];
@property({ type: Boolean, reflect: true }) public narrow!: boolean; @property({ type: Boolean, reflect: true }) public narrow!: boolean;
@property() public isWide!: boolean; @property() public isWide!: boolean;
@@ -72,16 +70,8 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
@property() public route!: Route; @property() public route!: Route;
@state() public _areas!: AreaRegistryEntry[];
@state() public _devices!: DeviceRegistryEntry[];
@state() public _entities!: EntityRegistryEntry[];
@state() private _related?: RelatedResult; @state() private _related?: RelatedResult;
private _logbookTime = { recent: 86400 };
private _area = memoizeOne( private _area = memoizeOne(
( (
areaId: string, areaId: string,
@@ -96,7 +86,7 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
registryDevices: DeviceRegistryEntry[], registryDevices: DeviceRegistryEntry[],
registryEntities: EntityRegistryEntry[] registryEntities: EntityRegistryEntry[]
) => { ) => {
const devices = new Map<string, DeviceRegistryEntry>(); const devices = new Map();
for (const device of registryDevices) { for (const device of registryDevices) {
if (device.area_id === areaId) { if (device.area_id === areaId) {
@@ -112,7 +102,7 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
if (entity.area_id === areaId) { if (entity.area_id === areaId) {
entities.push(entity); entities.push(entity);
} }
} else if (entity.device_id && devices.has(entity.device_id)) { } else if (devices.has(entity.device_id)) {
indirectEntities.push(entity); indirectEntities.push(entity);
} }
} }
@@ -125,20 +115,6 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
} }
); );
private _allDeviceIds = memoizeOne((devices: DeviceRegistryEntry[]) =>
devices.map((device) => device.id)
);
private _allEntities = memoizeOne(
(memberships: {
entities: EntityRegistryEntry[];
indirectEntities: EntityRegistryEntry[];
}) =>
memberships.entities
.map((entry) => entry.entity_id)
.concat(memberships.indirectEntities.map((entry) => entry.entity_id))
);
protected firstUpdated(changedProps) { protected firstUpdated(changedProps) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
loadAreaRegistryDetailDialog(); loadAreaRegistryDetailDialog();
@@ -151,26 +127,8 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
} }
} }
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeAreaRegistry(this.hass.connection, (areas) => {
this._areas = areas;
}),
subscribeDeviceRegistry(this.hass.connection, (entries) => {
this._devices = entries;
}),
subscribeEntityRegistry(this.hass.connection, (entries) => {
this._entities = entries;
}),
];
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._areas || !this._devices || !this._entities) { const area = this._area(this.areaId, this.areas);
return html``;
}
const area = this._area(this.areaId, this._areas);
if (!area) { if (!area) {
return html` return html`
@@ -181,12 +139,11 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
`; `;
} }
const memberships = this._memberships( const { devices, entities } = this._memberships(
this.areaId, this.areaId,
this._devices, this.devices,
this._entities this.entities
); );
const { devices, entities } = memberships;
// Pre-compute the entity and device names, so we can sort by them // Pre-compute the entity and device names, so we can sort by them
if (devices) { if (devices) {
@@ -402,6 +359,8 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
</ha-card> </ha-card>
` `
: ""} : ""}
</div>
<div class="column">
${isComponentLoaded(this.hass, "scene") ${isComponentLoaded(this.hass, "scene")
? html` ? html`
<ha-card <ha-card
@@ -483,26 +442,6 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
` `
: ""} : ""}
</div> </div>
<div class="column">
${isComponentLoaded(this.hass, "logbook")
? html`
<ha-card
outlined
.header=${this.hass.localize("panel.logbook")}
>
<ha-logbook
.hass=${this.hass}
.time=${this._logbookTime}
.entityIds=${this._allEntities(memberships)}
.deviceIds=${this._allDeviceIds(memberships.devices)}
virtualize
narrow
no-icon
></ha-logbook>
</ha-card>
`
: ""}
</div>
</div> </div>
</hass-tabs-subpage> </hass-tabs-subpage>
`; `;
@@ -760,13 +699,6 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
opacity: 0.5; opacity: 0.5;
border-radius: 50%; border-radius: 50%;
} }
ha-logbook {
height: 400px;
}
:host([narrow]) ha-logbook {
height: 235px;
overflow: auto;
}
`, `,
]; ];
} }

View File

@@ -1,7 +1,6 @@
import { mdiHelpCircle, mdiPlus } from "@mdi/js"; import { mdiHelpCircle, mdiPlus } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
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 } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import "../../../components/ha-fab"; import "../../../components/ha-fab";
@@ -10,20 +9,12 @@ import "../../../components/ha-svg-icon";
import { import {
AreaRegistryEntry, AreaRegistryEntry,
createAreaRegistryEntry, createAreaRegistryEntry,
subscribeAreaRegistry,
} from "../../../data/area_registry"; } from "../../../data/area_registry";
import { import type { DeviceRegistryEntry } from "../../../data/device_registry";
DeviceRegistryEntry, import type { EntityRegistryEntry } from "../../../data/entity_registry";
subscribeDeviceRegistry,
} from "../../../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../data/entity_registry";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-loading-screen"; import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage"; import "../../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
import "../ha-config-section"; import "../ha-config-section";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
@@ -33,7 +24,7 @@ import {
} from "./show-dialog-area-registry-detail"; } from "./show-dialog-area-registry-detail";
@customElement("ha-config-areas-dashboard") @customElement("ha-config-areas-dashboard")
export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { export class HaConfigAreasDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public isWide?: boolean; @property() public isWide?: boolean;
@@ -42,13 +33,13 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
@property() public route!: Route; @property() public route!: Route;
@state() private _areas!: AreaRegistryEntry[]; @property() public areas!: AreaRegistryEntry[];
@state() private _devices!: DeviceRegistryEntry[]; @property() public devices!: DeviceRegistryEntry[];
@state() private _entities!: EntityRegistryEntry[]; @property() public entities!: EntityRegistryEntry[];
private _processAreas = memoizeOne( private _areas = memoizeOne(
( (
areas: AreaRegistryEntry[], areas: AreaRegistryEntry[],
devices: DeviceRegistryEntry[], devices: DeviceRegistryEntry[],
@@ -84,20 +75,6 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
}) })
); );
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeAreaRegistry(this.hass.connection, (areas) => {
this._areas = areas;
}),
subscribeDeviceRegistry(this.hass.connection, (entries) => {
this._devices = entries;
}),
subscribeEntityRegistry(this.hass.connection, (entries) => {
this._entities = entries;
}),
];
}
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<hass-tabs-subpage <hass-tabs-subpage
@@ -115,13 +92,7 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
@click=${this._showHelp} @click=${this._showHelp}
></ha-icon-button> ></ha-icon-button>
<div class="container"> <div class="container">
${!this._areas || !this._devices || !this._entities ${this._areas(this.areas, this.devices, this.entities).map(
? ""
: this._processAreas(
this._areas,
this._devices,
this._entities
).map(
(area) => (area) =>
html`<a href=${`/config/areas/area/${area.area_id}`} html`<a href=${`/config/areas/area/${area.area_id}`}
><ha-card outlined> ><ha-card outlined>

View File

@@ -1,4 +1,20 @@
import { customElement, property } from "lit/decorators"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { stringCompare } from "../../../common/string/compare";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../../data/area_registry";
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../data/entity_registry";
import { import {
HassRouterPage, HassRouterPage,
RouterOptions, RouterOptions,
@@ -30,6 +46,44 @@ class HaConfigAreas extends HassRouterPage {
}, },
}; };
@state() private _configEntries: ConfigEntry[] = [];
@state()
private _deviceRegistryEntries: DeviceRegistryEntry[] = [];
@state()
private _entityRegistryEntries: EntityRegistryEntry[] = [];
@state() private _areas: AreaRegistryEntry[] = [];
private _unsubs?: UnsubscribeFunc[];
public connectedCallback() {
super.connectedCallback();
if (!this.hass) {
return;
}
this._loadData();
}
public disconnectedCallback() {
super.disconnectedCallback();
if (this._unsubs) {
while (this._unsubs.length) {
this._unsubs.pop()!();
}
this._unsubs = undefined;
}
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (!this._unsubs && changedProps.has("hass")) {
this._loadData();
}
}
protected updatePageEl(pageEl) { protected updatePageEl(pageEl) {
pageEl.hass = this.hass; pageEl.hass = this.hass;
@@ -37,11 +91,37 @@ class HaConfigAreas extends HassRouterPage {
pageEl.areaId = this.routeTail.path.substr(1); pageEl.areaId = this.routeTail.path.substr(1);
} }
pageEl.entries = this._configEntries;
pageEl.devices = this._deviceRegistryEntries;
pageEl.entities = this._entityRegistryEntries;
pageEl.areas = this._areas;
pageEl.narrow = this.narrow; pageEl.narrow = this.narrow;
pageEl.isWide = this.isWide; pageEl.isWide = this.isWide;
pageEl.showAdvanced = this.showAdvanced; pageEl.showAdvanced = this.showAdvanced;
pageEl.route = this.routeTail; pageEl.route = this.routeTail;
} }
private _loadData() {
getConfigEntries(this.hass).then((configEntries) => {
this._configEntries = configEntries.sort((conf1, conf2) =>
stringCompare(conf1.title, conf2.title)
);
});
if (this._unsubs) {
return;
}
this._unsubs = [
subscribeAreaRegistry(this.hass.connection, (areas) => {
this._areas = areas;
}),
subscribeDeviceRegistry(this.hass.connection, (entries) => {
this._deviceRegistryEntries = entries;
}),
subscribeEntityRegistry(this.hass.connection, (entries) => {
this._entityRegistryEntries = entries;
}),
];
}
} }
declare global { declare global {

View File

@@ -460,13 +460,17 @@ export default class HaAutomationActionRow extends LitElement {
border-top-left-radius: var(--ha-card-border-radius); border-top-left-radius: var(--ha-card-border-radius);
} }
.card-menu { .card-menu {
float: var(--float-end, right); float: right;
z-index: 3; z-index: 3;
margin: 4px; margin: 4px;
--mdc-theme-text-primary-on-background: var(--primary-text-color); --mdc-theme-text-primary-on-background: var(--primary-text-color);
display: flex; display: flex;
align-items: center; align-items: center;
} }
:host-context([style*="direction: rtl;"]) .card-menu {
right: initial;
left: 16px;
}
mwc-list-item[disabled] { mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color); --mdc-theme-text-primary-on-background: var(--disabled-text-color);
} }

View File

@@ -288,13 +288,16 @@ export default class HaAutomationConditionRow extends LitElement {
border-top-left-radius: var(--ha-card-border-radius); border-top-left-radius: var(--ha-card-border-radius);
} }
.card-menu { .card-menu {
float: var(--float-end, right); float: right;
z-index: 3; z-index: 3;
margin: 4px; margin: 4px;
--mdc-theme-text-primary-on-background: var(--primary-text-color); --mdc-theme-text-primary-on-background: var(--primary-text-color);
display: flex; display: flex;
align-items: center; align-items: center;
} }
:host-context([style*="direction: rtl;"]) .card-menu {
float: left;
}
mwc-list-item[disabled] { mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color); --mdc-theme-text-primary-on-background: var(--disabled-text-color);
} }

View File

@@ -487,13 +487,16 @@ export default class HaAutomationTriggerRow extends LitElement {
border-top-left-radius: var(--ha-card-border-radius); border-top-left-radius: var(--ha-card-border-radius);
} }
.card-menu { .card-menu {
float: var(--float-end, right); float: right;
z-index: 3; z-index: 3;
margin: 4px; margin: 4px;
--mdc-theme-text-primary-on-background: var(--primary-text-color); --mdc-theme-text-primary-on-background: var(--primary-text-color);
display: flex; display: flex;
align-items: center; align-items: center;
} }
:host-context([style*="direction: rtl;"]) .card-menu {
float: left;
}
.triggered { .triggered {
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;

View File

@@ -5,9 +5,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
import type { CalendarTrigger } from "../../../../../data/automation"; import type { CalendarTrigger } from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
import type { TriggerElement } from "../ha-automation-trigger-row"; import type { TriggerElement } from "../ha-automation-trigger-row";
import type { HaDurationData } from "../../../../../components/ha-duration-input";
import type { HaFormSchema } from "../../../../../components/ha-form/types"; import type { HaFormSchema } from "../../../../../components/ha-form/types";
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
import type { LocalizeFunc } from "../../../../../common/translations/localize"; import type { LocalizeFunc } from "../../../../../common/translations/localize";
@customElement("ha-automation-trigger-calendar") @customElement("ha-automation-trigger-calendar")
@@ -41,57 +39,20 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement {
], ],
], ],
}, },
{ name: "offset", selector: { duration: {} } },
{
name: "offset_type",
type: "select",
required: true,
options: [
[
"before",
localize(
"ui.panel.config.automation.editor.triggers.type.calendar.before"
),
],
[
"after",
localize(
"ui.panel.config.automation.editor.triggers.type.calendar.after"
),
],
],
},
]); ]);
public static get defaultConfig() { public static get defaultConfig() {
return { return {
event: "start" as CalendarTrigger["event"], event: "start" as CalendarTrigger["event"],
offset: 0,
}; };
} }
protected render() { protected render() {
const schema = this._schema(this.hass.localize); const schema = this._schema(this.hass.localize);
// Convert from string representation to ha form duration representation
const trigger_offset = this.trigger.offset;
const duration: HaDurationData = createDurationData(trigger_offset)!;
let offset_type = "after";
if (
(typeof trigger_offset === "object" && duration!.hours! < 0) ||
(typeof trigger_offset === "string" && trigger_offset.startsWith("-"))
) {
duration.hours = Math.abs(duration.hours!);
offset_type = "before";
}
const data = {
...this.trigger,
offset: duration,
offset_type: offset_type,
};
return html` return html`
<ha-form <ha-form
.schema=${schema} .schema=${schema}
.data=${data} .data=${this.trigger}
.hass=${this.hass} .hass=${this.hass}
.computeLabel=${this._computeLabelCallback} .computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@@ -101,14 +62,7 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement {
private _valueChanged(ev: CustomEvent): void { private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation(); ev.stopPropagation();
// Convert back to duration string representation const newTrigger = ev.detail.value;
const duration = ev.detail.value.offset;
const offsetType = ev.detail.value.offset_type === "before" ? "-" : "";
const newTrigger = {
...ev.detail.value,
offset: `${offsetType}${duration.hours}:${duration.minutes}:${duration.seconds}`,
};
delete newTrigger.offset_type;
fireEvent(this, "value-changed", { value: newTrigger }); fireEvent(this, "value-changed", { value: newTrigger });
} }

View File

@@ -1,11 +1,9 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { mdiContentCopy } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-alert";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-alert";
import "../../../../components/ha-switch"; import "../../../../components/ha-switch";
// eslint-disable-next-line // eslint-disable-next-line
import type { HaSwitch } from "../../../../components/ha-switch"; import type { HaSwitch } from "../../../../components/ha-switch";
@@ -15,7 +13,6 @@ import {
disconnectCloudRemote, disconnectCloudRemote,
} from "../../../../data/cloud"; } from "../../../../data/cloud";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast";
import { showCloudCertificateDialog } from "../dialog-cloud-certificate/show-dialog-cloud-certificate"; import { showCloudCertificateDialog } from "../dialog-cloud-certificate/show-dialog-cloud-certificate";
@customElement("cloud-remote-pref") @customElement("cloud-remote-pref")
@@ -51,11 +48,6 @@ export class CloudRemotePref extends LitElement {
`; `;
} }
const urlParts = remote_domain!.split(".");
const hiddenURL = `https://${urlParts[0].substring(0, 5)}***.${
urlParts[1]
}.${urlParts[2]}.${urlParts[3]}`;
return html` return html`
<ha-card <ha-card
outlined outlined
@@ -93,13 +85,8 @@ export class CloudRemotePref extends LitElement {
class="break-word" class="break-word"
rel="noreferrer" rel="noreferrer"
> >
${hiddenURL}</a https://${remote_domain}</a
>. >.
<ha-svg-icon
.url=${`https://${remote_domain}`}
.path=${mdiContentCopy}
@click=${this._copyURL}
></ha-svg-icon>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<a <a
@@ -146,14 +133,6 @@ export class CloudRemotePref extends LitElement {
} }
} }
private async _copyURL(ev): Promise<void> {
const url = ev.currentTarget.url;
await copyToClipboard(url);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
.preparing { .preparing {
@@ -175,6 +154,9 @@ export class CloudRemotePref extends LitElement {
font-weight: bold; font-weight: bold;
margin-bottom: 1em; margin-bottom: 1em;
} }
.warning ha-svg-icon {
color: var(--warning-color);
}
.break-word { .break-word {
overflow-wrap: break-word; overflow-wrap: break-word;
} }
@@ -196,11 +178,6 @@ export class CloudRemotePref extends LitElement {
.spacer { .spacer {
flex-grow: 1; flex-grow: 1;
} }
ha-svg-icon {
--mdc-icon-size: 18px;
color: var(--secondary-text-color);
cursor: pointer;
}
`; `;
} }
} }

View File

@@ -189,11 +189,9 @@ class HaConfigSystemNavigation extends LitElement {
private async _fetchBackupInfo(isHassioLoaded: boolean) { private async _fetchBackupInfo(isHassioLoaded: boolean) {
const backups: BackupContent[] | HassioBackup[] = isHassioLoaded const backups: BackupContent[] | HassioBackup[] = isHassioLoaded
? await fetchHassioBackups(this.hass) ? await fetchHassioBackups(this.hass)
: isComponentLoaded(this.hass, "backup") : await fetchBackupInfo(this.hass).then(
? await fetchBackupInfo(this.hass).then(
(backupData) => backupData.backups (backupData) => backupData.backups
) );
: [];
if (backups.length > 0) { if (backups.length > 0) {
this._latestBackupDate = (backups as any[]).reduce((a, b) => this._latestBackupDate = (backups as any[]).reduce((a, b) =>

View File

@@ -1,13 +0,0 @@
import type { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import type { DeviceAction } from "../../../ha-config-device-page";
import { showMQTTDeviceDebugInfoDialog } from "./show-dialog-mqtt-device-debug-info";
export const getMQTTDeviceActions = (
el: HTMLElement,
device: DeviceRegistryEntry
): DeviceAction[] => [
{
label: "MQTT Info",
action: async () => showMQTTDeviceDebugInfoDialog(el, { device }),
},
];

View File

@@ -0,0 +1,36 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { haStyle } from "../../../../../../resources/styles";
import { HomeAssistant } from "../../../../../../types";
import { showMQTTDeviceDebugInfoDialog } from "./show-dialog-mqtt-device-debug-info";
@customElement("ha-device-actions-mqtt")
export class HaDeviceActionsMqtt extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public device!: DeviceRegistryEntry;
protected render(): TemplateResult {
return html`
<mwc-button @click=${this._showDebugInfo}> MQTT Info </mwc-button>
`;
}
private async _showDebugInfo(): Promise<void> {
const device = this.device;
await showMQTTDeviceDebugInfoDialog(this, { device });
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
display: flex;
justify-content: space-between;
}
`,
];
}
}

View File

@@ -1,108 +0,0 @@
import { navigate } from "../../../../../../common/navigate";
import type { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { fetchZHADevice } from "../../../../../../data/zha";
import { showConfirmationDialog } from "../../../../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../../../../types";
import { showZHAClusterDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-cluster";
import { showZHADeviceChildrenDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-children";
import { showZHADeviceZigbeeInfoDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info";
import { showZHAReconfigureDeviceDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-reconfigure-device";
import type { DeviceAction } from "../../../ha-config-device-page";
export const getZHADeviceActions = async (
el: HTMLElement,
hass: HomeAssistant,
device: DeviceRegistryEntry
): Promise<DeviceAction[]> => {
const zigbeeConnection = device.connections.find(
(conn) => conn[0] === "zigbee"
);
if (!zigbeeConnection) {
return [];
}
const zhaDevice = await fetchZHADevice(hass, zigbeeConnection[1]);
if (!zhaDevice) {
return [];
}
const actions: DeviceAction[] = [];
if (zhaDevice.device_type !== "Coordinator") {
actions.push({
label: hass.localize("ui.dialogs.zha_device_info.buttons.reconfigure"),
action: () => showZHAReconfigureDeviceDialog(el, { device: zhaDevice }),
});
}
if (
zhaDevice.power_source === "Mains" &&
(zhaDevice.device_type === "Router" ||
zhaDevice.device_type === "Coordinator")
) {
actions.push(
...[
{
label: hass.localize("ui.dialogs.zha_device_info.buttons.add"),
action: () => navigate(`/config/zha/add/${zhaDevice!.ieee}`),
},
{
label: hass.localize(
"ui.dialogs.zha_device_info.buttons.device_children"
),
action: () => showZHADeviceChildrenDialog(el, { device: zhaDevice! }),
},
]
);
}
if (zhaDevice.device_type !== "Coordinator") {
actions.push(
...[
{
label: hass.localize(
"ui.dialogs.zha_device_info.buttons.zigbee_information"
),
action: () =>
showZHADeviceZigbeeInfoDialog(el, { device: zhaDevice }),
},
{
label: hass.localize("ui.dialogs.zha_device_info.buttons.clusters"),
action: () => showZHAClusterDialog(el, { device: zhaDevice }),
},
{
label: hass.localize(
"ui.dialogs.zha_device_info.buttons.view_in_visualization"
),
action: () =>
navigate(`/config/zha/visualization/${zhaDevice!.device_reg_id}`),
},
{
label: hass.localize("ui.dialogs.zha_device_info.buttons.remove"),
classes: "warning",
action: async () => {
const confirmed = await showConfirmationDialog(el, {
text: hass.localize(
"ui.dialogs.zha_device_info.confirmations.remove"
),
});
if (!confirmed) {
return;
}
await hass.callService("zha", "remove", {
ieee: zhaDevice.ieee,
});
history.back();
},
},
]
);
}
return actions;
};

View File

@@ -0,0 +1,155 @@
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { navigate } from "../../../../../../common/navigate";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { fetchZHADevice, ZHADevice } from "../../../../../../data/zha";
import { showConfirmationDialog } from "../../../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../../../resources/styles";
import { HomeAssistant } from "../../../../../../types";
import { showZHAClusterDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-cluster";
import { showZHADeviceChildrenDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-children";
import { showZHADeviceZigbeeInfoDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info";
import { showZHAReconfigureDeviceDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-reconfigure-device";
@customElement("ha-device-actions-zha")
export class HaDeviceActionsZha extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public device!: DeviceRegistryEntry;
@state() private _zhaDevice?: ZHADevice;
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("device")) {
const zigbeeConnection = this.device.connections.find(
(conn) => conn[0] === "zigbee"
);
if (!zigbeeConnection) {
return;
}
fetchZHADevice(this.hass, zigbeeConnection[1]).then((device) => {
this._zhaDevice = device;
});
}
}
protected render(): TemplateResult {
if (!this._zhaDevice) {
return html``;
}
return html`
${this._zhaDevice.device_type !== "Coordinator"
? html`
<mwc-button @click=${this._onReconfigureNodeClick}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.reconfigure"
)}
</mwc-button>
`
: ""}
${this._zhaDevice.power_source === "Mains" &&
(this._zhaDevice.device_type === "Router" ||
this._zhaDevice.device_type === "Coordinator")
? html`
<mwc-button @click=${this._onAddDevicesClick}>
${this.hass!.localize("ui.dialogs.zha_device_info.buttons.add")}
</mwc-button>
<mwc-button @click=${this._handleDeviceChildrenClicked}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.device_children"
)}
</mwc-button>
`
: ""}
${this._zhaDevice.device_type !== "Coordinator"
? html`
<mwc-button @click=${this._handleZigbeeInfoClicked}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.zigbee_information"
)}
</mwc-button>
<mwc-button @click=${this._showClustersDialog}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.clusters"
)}
</mwc-button>
<mwc-button @click=${this._onViewInVisualizationClick}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.view_in_visualization"
)}
</mwc-button>
<mwc-button class="warning" @click=${this._removeDevice}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.remove"
)}
</mwc-button>
`
: ""}
`;
}
private async _showClustersDialog(): Promise<void> {
await showZHAClusterDialog(this, { device: this._zhaDevice! });
}
private async _onReconfigureNodeClick(): Promise<void> {
if (!this.hass) {
return;
}
showZHAReconfigureDeviceDialog(this, { device: this._zhaDevice! });
}
private _onAddDevicesClick() {
navigate(`/config/zha/add/${this._zhaDevice!.ieee}`);
}
private _onViewInVisualizationClick() {
navigate(`/config/zha/visualization/${this._zhaDevice!.device_reg_id}`);
}
private async _handleZigbeeInfoClicked() {
showZHADeviceZigbeeInfoDialog(this, { device: this._zhaDevice! });
}
private async _handleDeviceChildrenClicked() {
showZHADeviceChildrenDialog(this, { device: this._zhaDevice! });
}
private async _removeDevice() {
const confirmed = await showConfirmationDialog(this, {
text: this.hass.localize(
"ui.dialogs.zha_device_info.confirmations.remove"
),
});
if (!confirmed) {
return;
}
await this.hass.callService("zha", "remove", {
ieee: this._zhaDevice!.ieee,
});
history.back();
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
display: flex;
flex-direction: column;
align-items: flex-start;
}
`,
];
}
}

View File

@@ -7,7 +7,6 @@ import {
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "../../../../../../components/ha-expansion-panel";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry"; import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { fetchZHADevice, ZHADevice } from "../../../../../../data/zha"; import { fetchZHADevice, ZHADevice } from "../../../../../../data/zha";
import { haStyle } from "../../../../../../resources/styles"; import { haStyle } from "../../../../../../resources/styles";
@@ -41,7 +40,7 @@ export class HaDeviceActionsZha extends LitElement {
return html``; return html``;
} }
return html` return html`
<ha-expansion-panel header="Zigbee info"> <h4>Zigbee info</h4>
<div>IEEE: ${this._zhaDevice.ieee}</div> <div>IEEE: ${this._zhaDevice.ieee}</div>
<div>Nwk: ${formatAsPaddedHex(this._zhaDevice.nwk)}</div> <div>Nwk: ${formatAsPaddedHex(this._zhaDevice.nwk)}</div>
<div>Device Type: ${this._zhaDevice.device_type}</div> <div>Device Type: ${this._zhaDevice.device_type}</div>
@@ -73,7 +72,6 @@ export class HaDeviceActionsZha extends LitElement {
</div> </div>
` `
: ""} : ""}
</ha-expansion-panel>
`; `;
} }
@@ -88,11 +86,6 @@ export class HaDeviceActionsZha extends LitElement {
word-break: break-all; word-break: break-all;
margin-top: 2px; margin-top: 2px;
} }
ha-expansion-panel {
--expansion-panel-summary-padding: 0;
--expansion-panel-content-padding: 0;
padding-top: 4px;
}
`, `,
]; ];
} }

View File

@@ -1,68 +0,0 @@
import { getConfigEntries } from "../../../../../../data/config_entries";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { fetchZwaveNodeStatus } from "../../../../../../data/zwave_js";
import type { HomeAssistant } from "../../../../../../types";
import { showZWaveJSHealNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-heal-node";
import { showZWaveJSReinterviewNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-reinterview-node";
import { showZWaveJSRemoveFailedNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-remove-failed-node";
import type { DeviceAction } from "../../../ha-config-device-page";
export const getZwaveDeviceActions = async (
el: HTMLElement,
hass: HomeAssistant,
device: DeviceRegistryEntry
): Promise<DeviceAction[]> => {
const configEntries = await getConfigEntries(hass, {
domain: "zwave_js",
});
const configEntry = configEntries.find((entry) =>
device.config_entries.includes(entry.entry_id)
);
if (!configEntry) {
return [];
}
const entryId = configEntry.entry_id;
const node = await fetchZwaveNodeStatus(hass, device.id);
if (!node || node.is_controller_node) {
return [];
}
return [
{
label: hass.localize(
"ui.panel.config.zwave_js.device_info.device_config"
),
href: `/config/zwave_js/node_config/${device.id}?config_entry=${entryId}`,
},
{
label: hass.localize(
"ui.panel.config.zwave_js.device_info.reinterview_device"
),
action: () =>
showZWaveJSReinterviewNodeDialog(el, {
device_id: device.id,
}),
},
{
label: hass.localize("ui.panel.config.zwave_js.device_info.heal_node"),
action: () =>
showZWaveJSHealNodeDialog(el, {
device: device,
}),
},
{
label: hass.localize(
"ui.panel.config.zwave_js.device_info.remove_failed"
),
action: () =>
showZWaveJSRemoveFailedNodeDialog(el, {
device_id: device.id,
}),
},
];
};

View File

@@ -0,0 +1,139 @@
import "@material/mwc-button/mwc-button";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import {
fetchZwaveNodeStatus,
getZwaveJsIdentifiersFromDevice,
ZWaveJSNodeIdentifiers,
ZWaveJSNodeStatus,
} from "../../../../../../data/zwave_js";
import { haStyle } from "../../../../../../resources/styles";
import { HomeAssistant } from "../../../../../../types";
import { showZWaveJSReinterviewNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-reinterview-node";
import { showZWaveJSHealNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-heal-node";
import { showZWaveJSRemoveFailedNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-remove-failed-node";
@customElement("ha-device-actions-zwave_js")
export class HaDeviceActionsZWaveJS extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public device!: DeviceRegistryEntry;
@state() private _entryId?: string;
@state() private _nodeId?: number;
@state() private _node?: ZWaveJSNodeStatus;
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("device")) {
const identifiers: ZWaveJSNodeIdentifiers | undefined =
getZwaveJsIdentifiersFromDevice(this.device);
if (!identifiers) {
return;
}
this._nodeId = identifiers.node_id;
this._entryId = this.device.config_entries[0];
this._fetchNodeDetails();
}
}
protected async _fetchNodeDetails() {
if (!this._nodeId || !this._entryId) {
return;
}
this._node = await fetchZwaveNodeStatus(
this.hass,
this._entryId,
this._nodeId
);
}
protected render(): TemplateResult {
if (!this._node) {
return html``;
}
return html`
${!this._node.is_controller_node
? html`
<a
.href=${`/config/zwave_js/node_config/${this.device.id}?config_entry=${this._entryId}`}
>
<mwc-button>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.device_config"
)}
</mwc-button>
</a>
<mwc-button @click=${this._reinterviewClicked}>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.reinterview_device"
)}
</mwc-button>
<mwc-button @click=${this._healNodeClicked}>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.heal_node"
)}
</mwc-button>
<mwc-button @click=${this._removeFailedNode}>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.remove_failed"
)}
</mwc-button>
`
: ""}
`;
}
private async _reinterviewClicked() {
if (!this._nodeId || !this._entryId) {
return;
}
showZWaveJSReinterviewNodeDialog(this, {
entry_id: this._entryId,
node_id: this._nodeId,
});
}
private async _healNodeClicked() {
if (!this._nodeId || !this._entryId) {
return;
}
showZWaveJSHealNodeDialog(this, {
entry_id: this._entryId,
node_id: this._nodeId,
device: this.device,
});
}
private async _removeFailedNode() {
if (!this._nodeId || !this._entryId) {
return;
}
showZWaveJSRemoveFailedNodeDialog(this, {
entry_id: this._entryId,
node_id: this._nodeId,
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
a {
text-decoration: none;
}
`,
];
}
}

Some files were not shown because too many files have changed in this diff Show More