Compare commits

...

59 Commits

Author SHA1 Message Date
Paulus Schoutsen
8ac4a6d900 Bumped version to 20220521.0 2022-05-20 17:28:06 -07:00
Paulus Schoutsen
fae1bcf0e0 Fixes logbook (#12740) 2022-05-20 11:25:19 -07:00
Allen Porter
9a9eec40b2 Add an application credentials display name (#12720)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-05-19 21:27:43 -07:00
Bram Kragten
6ab19d66d5 Add option to compare energy graphs with previous period (#12723)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-05-20 04:20:18 +00:00
J. Nick Koston
a0a7ce014f Compute the icon based on the logbook state and not the current state (#12725)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-05-19 21:12:17 -07:00
Paulus Schoutsen
bfeb90780f Pass device ID to logbook if available (#12728) 2022-05-20 04:09:33 +00:00
J. Nick Koston
1f105b6c15 Get attributes from first state when using minimal responses (#12732) 2022-05-19 20:56:11 -07:00
Raman Gupta
5b7b0ea326 Use device_id instead of config entry id and node id for zwave_js (#12658)
* Use device_id instead of config entry id and node id for zwave_js

* Add additional cleanup from #12642

* Revert removal of multiple config entries check

* Update src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-actions-zwave_js.ts

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

* Update src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-05-19 10:23:16 -07:00
Raman Gupta
32a991989f Update zwave_js data collection URL (#12666) 2022-05-19 17:05:31 +02:00
Allen Porter
788f76ab9c Add error handling for application credentials removal (#12686) 2022-05-19 16:51:33 +02:00
Yosi Levy
f6411dce66 Select + target picker Rtl fixes (#12711) 2022-05-19 16:28:56 +02:00
Yosi Levy
6f19ea1d84 Various RTL fixes (#12721) 2022-05-19 16:25:30 +02:00
Michael Irigoyen
448609533f Update Material Design Icons to v6.7.96 (#12111) 2022-05-19 16:21:00 +02:00
J. Nick Koston
6c48ace41e Fix python to js timestamp conversions in logbook traces (#12677)
- The websocket version needs the time converted from
  where python stores the decimal
2022-05-18 12:36:08 -07:00
Paulus Schoutsen
c41e100c1c Bumped version to 20220518.0 2022-05-18 12:10:42 -07:00
RoboMagus
8216b522c2 Fix 'loading_log' string (#12712) 2022-05-18 12:09:31 -07:00
Paulus Schoutsen
82035d587a Import all date-fns from modules (#12717) 2022-05-18 12:09:25 -07:00
J. Nick Koston
2796c3570a Support requesting multiple integration manifests in a single request (#12706)
* Support requesting multiple integration manifests in a single request

* only fetch if there are some to actually fetch

* handle empty

* not truthy, wrong language

* Do not copy params

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-05-18 12:09:09 -07:00
J. Nick Koston
f4f51e1de5 Show the integration brand icon when there is no entity in logbook (#12713) 2022-05-18 12:01:09 -07:00
J. Nick Koston
af6b0d3266 Support requesting translations for multiple integrations in one request (#12704)
* Support requesting translations for multiple integrations in one request

- Requires https://github.com/home-assistant/core/pull/71979

* onboarding as well

* integrations -> integration

* fix cache

* short return if they are all loaded

* reduce

* reduce

* reduce
2022-05-18 11:37:47 -07:00
Paulus Schoutsen
7d1c77a38f Add support for OAuth2 callback via My (#12718) 2022-05-18 11:18:43 -07:00
J. Nick Koston
f807618f75 Convert history calls to use new websocket endpoint (#12662) 2022-05-18 10:20:38 -07:00
Steve Repsher
4cfb6713cb Delete focus targets for replaced dialogs (#12724) 2022-05-18 16:18:22 +00:00
Patrick ZAJDA
d32f84f28d Add missing labels in energy dashboard settings (#12722)
Signed-off-by: Patrick ZAJDA <patrick@zajda.fr>
2022-05-18 18:17:31 +02:00
Paulus Schoutsen
5fb1504211 Add logbook to area info page (#12715) 2022-05-17 12:20:49 -07:00
Paulus Schoutsen
c37e1f0c9d Add logbook to device info page (#12714) 2022-05-17 11:02:23 -07:00
Paulus Schoutsen
90c234ffad Refactor logbook data fetch logic into reusable class (#12701) 2022-05-17 08:53:22 -07:00
breakthestatic
dd3a3ec586 Add guard logic from PR home-assistant#12181 to input select row (#12703) 2022-05-17 10:25:32 +00:00
Zack Barett
6f67da09c0 Show manage cloud link to config (#12673) 2022-05-17 12:14:43 +02:00
Franck Nijhof
ba27c184f6 Add my support for Application Credentials (#12709) 2022-05-17 12:13:46 +02:00
Paulus Schoutsen
b37f97128a Fix float-end for LTR (#12707) 2022-05-17 08:20:19 +02:00
Bram Kragten
ee0de942f7 Bumped version to 20220516.0 2022-05-16 20:37:50 +02:00
Steve Repsher
ae2d48f2f4 Return focus after dialogs close (#11999) 2022-05-16 17:10:41 +02:00
Yosi Levy
1bd760b455 Rtl changes (#12693) 2022-05-16 15:57:14 +02:00
Joakim Sørensen
3d66a68791 Guard for missing backup integration (#12696) 2022-05-16 13:39:41 +02:00
J. Nick Koston
01a53439c4 Teach logbook about additional context data (#12667)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-05-15 21:25:55 -07:00
Sven
09ee8dbeb6 Update Lokalise URL (#12684) 2022-05-15 17:59:31 +02:00
Philip Allgaier
f36c91550d Add missing label to search icon (#12671) 2022-05-13 18:58:01 -04:00
Franck Nijhof
6be6c711d0 Fix strict error handling in Markdown card templates (#12661) 2022-05-13 13:17:56 +02:00
Allen Porter
72a36fb1cd Add calendar trigger offsets in automation editor (#12486)
* Add calendar trigger offsets in automation editor

* Use duration selector for offset

* Fix typing for offsets/duratons
2022-05-12 07:42:15 -05:00
J. Nick Koston
4c982b3323 Switch logbook calls to use the new websocket (#12665) 2022-05-11 22:28:18 -05:00
Yosi Levy
c9c3be71cc Merge pull request #12620 from yosilevy/RTL-no-host-context
Replace host-context with css properties - after session with Bram
2022-05-11 16:30:21 +03:00
Bram Kragten
f1b965dcc5 Update ha-fab.ts 2022-05-11 15:19:03 +02:00
Bram Kragten
a08a23a93d Use FabBase 2022-05-11 14:25:43 +02:00
Bram Kragten
2040a49458 Update var name 2022-05-11 14:21:02 +02:00
Bram Kragten
df94f4f907 Merge branch 'dev' into RTL-no-host-context 2022-05-11 14:18:26 +02:00
Bram Kragten
96d375cb84 Use / 2022-05-11 14:16:44 +02:00
Yosi Levy
7a9c2f56c5 Rtl menu fix (#12561)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-05-11 11:01:45 +02:00
J. Nick Koston
5ec7193e5c Show script traces in logbook (#12643) 2022-05-10 23:32:09 +02:00
Zack Barett
d89e4337f2 Hide Cloud URL - Add Copy Icon (#12655)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-05-10 19:37:31 +00:00
Zack Barett
2e192d5021 Update Translations to create helper (#12656) 2022-05-10 21:25:03 +02:00
Yosi Levy
7db28c0156 Update following review 2022-05-10 19:31:23 +03:00
Yosi Levy
f09c842981 Update src/state/translations-mixin.ts
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-05-10 18:25:28 +03:00
Yosi Levy
b295bbd706 RTL settings clickable list item fix (#12595) 2022-05-10 16:57:18 +02:00
Patrick ZAJDA
8d3132fefc Add label for Fix issue column header in statistics developer tools (#12597)
Signed-off-by: Patrick ZAJDA <patrick@zajda.fr>
2022-05-09 17:14:59 +02:00
Allen Porter
00c5d3dbbb Add configuration panel for Application Credentials (#12344)
Co-authored-by: Zack Barett <zackbarett@hey.com>
Co-authored-by: Zack <zackbarett@hey.com>
2022-05-09 17:03:59 +02:00
Zack Barett
ca37aff47d Move YAML to first tab of Developer Tools (#12589) 2022-05-09 08:07:17 -05:00
Joakim Sørensen
9ed069ef6a Get full core logs from core (#12639) 2022-05-09 08:07:01 -05:00
Yosi Levy
6c73ae5bf7 Replace host-context with css properties 2022-05-07 06:39:39 +03:00
128 changed files with 3603 additions and 1846 deletions

View File

@@ -1,4 +1,4 @@
import { format, startOfToday, startOfTomorrow } from "date-fns"; import { format, startOfToday, startOfTomorrow } from "date-fns/esm";
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"; } from "date-fns/esm";
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

@@ -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: "2021-03-25T04:36:51.240832+00:00", when: 1616647011.240832,
domain: "automation", domain: "automation",
}, },
{ {
when: "2021-03-25T04:36:51.249828+00:00", when: 1616647011.249828,
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: "2021-03-25T04:36:51.258947+00:00", when: 1616647011.258947,
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: "2021-03-25T04:36:51.261806+00:00", when: 1616647011.261806,
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: "2021-03-25T04:36:51.265246+00:00", when: 1616647011.265246,
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: "2021-03-14T06:07:01.768492+00:00", when: 1615702021.768492,
domain: "automation", domain: "automation",
}, },
{ {
when: "2021-03-14T06:07:01.872187+00:00", when: 1615702021.872187,
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: "2021-03-14T06:07:53.284505+00:00", when: 1615702073.284505,
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

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

View File

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

View File

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

@@ -2,67 +2,74 @@ 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, isNumericState } from "../number/format_number"; import { formatNumber, isNumericFromAttributes } from "../number/format_number";
import { LocalizeFunc } from "../translations/localize"; import { LocalizeFunc } from "../translations/localize";
import { computeStateDomain } from "./compute_state_domain"; import { supportsFeatureFromAttributes } from "./supports-feature";
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 => { ): string =>
const compareState = state !== undefined ? state : stateObj.state; computeStateDisplayFromEntityAttributes(
localize,
locale,
stateObj.entity_id,
stateObj.attributes,
state !== undefined ? state : stateObj.state
);
if (compareState === UNKNOWN || compareState === UNAVAILABLE) { export const computeStateDisplayFromEntityAttributes = (
return localize(`state.default.${compareState}`); localize: LocalizeFunc,
locale: FrontendLocaleData,
entityId: string,
attributes: any,
state: string
): string => {
if (state === UNKNOWN || state === UNAVAILABLE) {
return localize(`state.default.${state}`);
} }
// 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 (isNumericState(stateObj)) { if (isNumericFromAttributes(attributes)) {
// state is duration // state is duration
if ( if (
stateObj.attributes.device_class === "duration" && attributes.device_class === "duration" &&
stateObj.attributes.unit_of_measurement && attributes.unit_of_measurement &&
UNIT_TO_SECOND_CONVERT[stateObj.attributes.unit_of_measurement] UNIT_TO_SECOND_CONVERT[attributes.unit_of_measurement]
) { ) {
try { try {
return formatDuration( return formatDuration(state, attributes.unit_of_measurement);
compareState,
stateObj.attributes.unit_of_measurement
);
} catch (_err) { } catch (_err) {
// fallback to default // fallback to default
} }
} }
if (stateObj.attributes.device_class === "monetary") { if (attributes.device_class === "monetary") {
try { try {
return formatNumber(compareState, locale, { return formatNumber(state, locale, {
style: "currency", style: "currency",
currency: stateObj.attributes.unit_of_measurement, currency: attributes.unit_of_measurement,
minimumFractionDigits: 2, minimumFractionDigits: 2,
}); });
} catch (_err) { } catch (_err) {
// fallback to default // fallback to default
} }
} }
return `${formatNumber(compareState, locale)}${ return `${formatNumber(state, locale)}${
stateObj.attributes.unit_of_measurement attributes.unit_of_measurement ? " " + attributes.unit_of_measurement : ""
? " " + stateObj.attributes.unit_of_measurement
: ""
}`; }`;
} }
const domain = computeStateDomain(stateObj); const domain = computeDomain(entityId);
if (domain === "input_datetime") { if (domain === "input_datetime") {
if (state !== undefined) { if (state !== undefined) {
@@ -97,36 +104,32 @@ export const computeStateDisplay = (
} 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 (stateObj.attributes.has_date && stateObj.attributes.has_time) { if (attributes.has_date && attributes.has_time) {
date = new Date( date = new Date(
stateObj.attributes.year, attributes.year,
stateObj.attributes.month - 1, attributes.month - 1,
stateObj.attributes.day, attributes.day,
stateObj.attributes.hour, attributes.hour,
stateObj.attributes.minute attributes.minute
); );
return formatDateTime(date, locale); return formatDateTime(date, locale);
} }
if (stateObj.attributes.has_date) { if (attributes.has_date) {
date = new Date( date = new Date(attributes.year, attributes.month - 1, attributes.day);
stateObj.attributes.year,
stateObj.attributes.month - 1,
stateObj.attributes.day
);
return formatDate(date, locale); return formatDate(date, locale);
} }
if (stateObj.attributes.has_time) { if (attributes.has_time) {
date = new Date(); date = new Date();
date.setHours(stateObj.attributes.hour, stateObj.attributes.minute); date.setHours(attributes.hour, attributes.minute);
return formatTime(date, locale); return formatTime(date, locale);
} }
return stateObj.state; return state;
} }
} }
if (domain === "humidifier") { if (domain === "humidifier") {
if (compareState === "on" && stateObj.attributes.humidity) { if (state === "on" && attributes.humidity) {
return `${stateObj.attributes.humidity} %`; return `${attributes.humidity} %`;
} }
} }
@@ -136,7 +139,7 @@ export const computeStateDisplay = (
domain === "number" || domain === "number" ||
domain === "input_number" domain === "input_number"
) { ) {
return formatNumber(compareState, locale); return formatNumber(state, locale);
} }
// state of button is a timestamp // state of button is a timestamp
@@ -144,12 +147,12 @@ export const computeStateDisplay = (
domain === "button" || domain === "button" ||
domain === "input_button" || domain === "input_button" ||
domain === "scene" || domain === "scene" ||
(domain === "sensor" && stateObj.attributes.device_class === "timestamp") (domain === "sensor" && attributes.device_class === "timestamp")
) { ) {
try { try {
return formatDateTime(new Date(compareState), locale); return formatDateTime(new Date(state), locale);
} catch (_err) { } catch (_err) {
return compareState; return state;
} }
} }
@@ -160,30 +163,28 @@ export const computeStateDisplay = (
// 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 compareState === "on" return state === "on"
? updateIsInstalling(stateObj as UpdateEntity) ? updateIsInstallingFromAttributes(attributes)
? supportsFeature(stateObj, UPDATE_SUPPORT_PROGRESS) ? supportsFeatureFromAttributes(attributes, UPDATE_SUPPORT_PROGRESS)
? localize("ui.card.update.installing_with_progress", { ? localize("ui.card.update.installing_with_progress", {
progress: stateObj.attributes.in_progress, progress: attributes.in_progress,
}) })
: localize("ui.card.update.installing") : localize("ui.card.update.installing")
: stateObj.attributes.latest_version : attributes.latest_version
: stateObj.attributes.skipped_version === : attributes.skipped_version === attributes.latest_version
stateObj.attributes.latest_version ? attributes.latest_version ?? localize("state.default.unavailable")
? 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
(stateObj.attributes.device_class && (attributes.device_class &&
localize( localize(
`component.${domain}.state.${stateObj.attributes.device_class}.${compareState}` `component.${domain}.state.${attributes.device_class}.${state}`
)) || )) ||
// Return default translation // Return default translation
localize(`component.${domain}.state._.${compareState}`) || localize(`component.${domain}.state._.${state}`) ||
// We don't know! Return the raw state. // We don't know! Return the raw state.
compareState state
); );
}; };

View File

@@ -1,7 +1,13 @@
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 =>
stateObj.attributes.friendly_name === undefined computeStateNameFromEntityAttributes(stateObj.entity_id, stateObj.attributes);
? computeObjectId(stateObj.entity_id).replace(/_/g, " ")
: stateObj.attributes.friendly_name || "";

View File

@@ -46,6 +46,20 @@ 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) {
@@ -150,7 +164,5 @@ export const domainIcon = (
return FIXED_DOMAIN_ICONS[domain]; return FIXED_DOMAIN_ICONS[domain];
} }
// eslint-disable-next-line return undefined;
console.warn(`Unable to find icon for domain ${domain}`);
return DEFAULT_DOMAIN_ICON;
}; };

View File

@@ -3,6 +3,13 @@ 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
(stateObj.attributes.supported_features! & feature) !== 0; (attributes.supported_features! & feature) !== 0;

View File

@@ -7,8 +7,11 @@ 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 =>
!!stateObj.attributes.unit_of_measurement || isNumericFromAttributes(stateObj.attributes);
!!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;
return (...args: T): void => { const throttledFunc = (...args: T): void => {
const later = () => { const later = () => {
previous = leading === false ? 0 : Date.now(); previous = leading === false ? 0 : Date.now();
timeout = undefined; timeout = undefined;
@@ -35,4 +35,10 @@ 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"; } from "date-fns/esm";
import { import {
formatDate, formatDate,
formatDateMonth, formatDateMonth,

View File

@@ -269,8 +269,8 @@ export class HaDataTable extends LitElement {
@change=${this._handleHeaderRowCheckboxClick} @change=${this._handleHeaderRowCheckboxClick}
.indeterminate=${this._checkedRows.length && .indeterminate=${this._checkedRows.length &&
this._checkedRows.length !== this._checkableRowsCount} this._checkedRows.length !== this._checkableRowsCount}
.checked=${this._checkedRows.length === .checked=${this._checkedRows.length &&
this._checkableRowsCount} this._checkedRows.length === this._checkableRowsCount}
> >
</ha-checkbox> </ha-checkbox>
</div> </div>

View File

@@ -20,7 +20,7 @@ interface HassEntityWithCachedName extends HassEntity {
friendly_name: string; friendly_name: string;
} }
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; export type HaEntityPickerEntityFilterFunc = (entity: 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,17 +1,27 @@
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,
queryAssignedElements,
} 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; @property({ type: Number }) public x: number | null = null;
@property({ type: Number }) public y?: number; @property({ type: Number }) public y: number | null = null;
@property({ type: Boolean }) public multi = false; @property({ type: Boolean }) public multi = false;
@@ -23,6 +33,12 @@ export class HaButtonMenu extends LitElement {
@query("mwc-menu", true) private _menu?: Menu; @query("mwc-menu", true) private _menu?: Menu;
@queryAssignedElements({
slot: "trigger",
selector: "ha-icon-button, mwc-button",
})
private _triggerButton!: Array<HaIconButton | Button>;
public get items() { public get items() {
return this._menu?.items; return this._menu?.items;
} }
@@ -31,6 +47,14 @@ 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[0]?.focus();
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div @click=${this._handleClick}> <div @click=${this._handleClick}>
@@ -50,6 +74,21 @@ 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;

View File

@@ -47,10 +47,6 @@ export class HaClickableListItem extends ListItemBase {
padding-left: 0px; padding-left: 0px;
padding-right: 0px; padding-right: 0px;
} }
:host([rtl]) span {
margin-left: var(--mdc-list-item-graphic-margin, 20px) !important;
margin-right: 0px !important;
}
:host([graphic="avatar"]:not([twoLine])), :host([graphic="avatar"]:not([twoLine])),
:host([graphic="icon"]:not([twoLine])) { :host([graphic="icon"]:not([twoLine])) {
height: 48px; height: 48px;
@@ -64,6 +60,19 @@ 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;
} }
span.material-icons:first-of-type {
margin-inline-start: 0px !important;
margin-inline-end: var(
--mdc-list-item-graphic-margin,
16px
) !important;
direction: var(--direction);
}
span.material-icons:last-of-type {
margin-inline-start: auto !important;
margin-inline-end: 0px !important;
direction: var(--direction);
}
`, `,
]; ];
} }

View File

@@ -241,6 +241,9 @@ 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);
@@ -249,18 +252,9 @@ 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;
:host-context([style*="direction: rtl;"]) .toggle-button { direction: var(--direction);
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,6 +140,9 @@ 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 {
@@ -166,6 +169,9 @@ 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,12 +17,13 @@ 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);
} }
@@ -89,18 +90,18 @@ export class HaDialog extends DialogBase {
} }
.header_title { .header_title {
margin-right: 40px; margin-right: 40px;
margin-inline-end: 40px;
direction: var(--direction);
} }
[dir="rtl"].header_button { .header_button {
right: auto; inset-inline-start: initial;
left: 16px; inset-inline-end: 16px;
direction: var(--direction);
} }
[dir="rtl"].header_title { .dialog-actions {
margin-left: 40px; inset-inline-start: initial !important;
margin-right: 0px; inset-inline-end: 0px !important;
} direction: var(--direction);
:host-context([style*="direction: rtl;"]) .dialog-actions {
left: 0px !important;
right: auto !important;
} }
`, `,
]; ];

View File

@@ -133,6 +133,9 @@ 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,24 +1,25 @@
import { Fab } from "@material/mwc-fab"; import { FabBase } from "@material/mwc-fab/mwc-fab-base";
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 Fab { export class HaFab extends FabBase {
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 = Fab.styles.concat([ static override styles = [
styles,
css` css`
:host-context([style*="direction: rtl;"]) :host .mdc-fab--extended .mdc-fab__icon {
.mdc-fab--extended margin-inline-start: -8px;
.mdc-fab__icon { margin-inline-end: 12px;
margin-left: 12px !important; direction: var(--direction);
margin-right: calc(12px - 20px) !important;
} }
`, `,
]); ];
} }
declare global { declare global {

View File

@@ -175,24 +175,23 @@ 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;
:host-context([style*="direction: rtl;"]) inset-inline-end: 0px;
.mdc-text-field__icon--leading { direction: var(--direction);
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;
} }
:host-context([style*="direction: rtl;"]) .mdc-floating-label {
left: initial;
right: 16px;
}
:host-context([style*="direction: rtl;"])
.mdc-text-field--filled
.mdc-floating-label { .mdc-floating-label {
left: initial; inset-inline-start: 16px !important;
right: 48px; inset-inline-end: initial !important;
direction: var(--direction);
}
.mdc-text-field--filled .mdc-floating-label {
inset-inline-start: 48px !important;
inset-inline-end: initial !important;
direction: var(--direction);
} }
.dragged:before { .dragged:before {
position: var(--layout-fit_-_position); position: var(--layout-fit_-_position);

View File

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

View File

@@ -1,6 +1,7 @@
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 } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import "./ha-svg-icon"; import "./ha-svg-icon";
@customElement("ha-icon-button") @customElement("ha-icon-button")
@@ -15,6 +16,12 @@ export class HaIconButton extends LitElement {
@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,

View File

@@ -47,9 +47,18 @@ 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);
} }
:host-context([style*="direction: rtl;"]) .mdc-floating-label { .mdc-select--filled .mdc-floating-label {
right: 16px !important; inset-inline-start: 12px;
left: initial !important; inset-inline-end: initial;
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

@@ -569,6 +569,9 @@ 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;
@@ -578,6 +581,9 @@ 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;
@@ -616,10 +622,6 @@ 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

@@ -92,17 +92,18 @@ export class HaTextField extends TextFieldBase {
overflow: var(--text-field-overflow); overflow: var(--text-field-overflow);
} }
:host-context([style*="direction: rtl;"]) .mdc-floating-label { .mdc-floating-label {
right: 10px !important; inset-inline-start: 16px !important;
left: initial !important; inset-inline-end: initial !important;
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);
right: 48px !important; inset-inline-start: 48px !important;
left: initial !important; inset-inline-end: initial !important;
direction: var(--direction);
} }
`, `,
]; ];

View File

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

View File

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

View File

@@ -120,9 +120,10 @@ class MediaUploadButton extends LitElement {
vertical-align: middle; vertical-align: middle;
} }
:host-context([style*="direction: rtl;"]) ha-svg-icon[slot="icon"] { ha-svg-icon[slot="icon"] {
margin-left: 8px; margin-inline-start: 0px;
margin-right: 0px; margin-inline-end: 8px;
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"; import "../../panels/logbook/ha-logbook-renderer";
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 <ha-logbook-renderer
relative-time relative-time
.hass=${this.hass} .hass=${this.hass}
.entries=${this.logbookEntries} .entries=${this.logbookEntries}
.narrow=${this.narrow} .narrow=${this.narrow}
></ha-logbook> ></ha-logbook-renderer>
<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"; import "../../panels/logbook/ha-logbook-renderer";
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) >= startTime (entry) => new Date(entry.when * 1000) >= 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); const entryDate = new Date(entry.when * 1000);
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 <ha-logbook-renderer
relative-time relative-time
.hass=${this.hass} .hass=${this.hass}
.entries=${entries} .entries=${entries}
.narrow=${this.narrow} .narrow=${this.narrow}
></ha-logbook> ></ha-logbook-renderer>
<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); const entryDate = new Date(logbookEntry.when * 1000);
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) < timestamp new Date(this.logbookRenderer.curItem.when * 1000) < timestamp
) { ) {
this.logbookRenderer.maybeRenderItem(); this.logbookRenderer.maybeRenderItem();
} }

View File

@@ -0,0 +1,47 @@
import { HomeAssistant } from "../types";
export interface ApplicationCredentialsConfig {
domains: string[];
}
export interface ApplicationCredential {
id: string;
domain: string;
client_id: string;
client_secret: string;
name: string;
}
export const fetchApplicationCredentialsConfig = async (hass: HomeAssistant) =>
hass.callWS<ApplicationCredentialsConfig>({
type: "application_credentials/config",
});
export const fetchApplicationCredentials = async (hass: HomeAssistant) =>
hass.callWS<ApplicationCredential[]>({
type: "application_credentials/list",
});
export const createApplicationCredential = async (
hass: HomeAssistant,
domain: string,
clientId: string,
clientSecret: string,
name?: string
) =>
hass.callWS<ApplicationCredential>({
type: "application_credentials/create",
domain,
client_id: clientId,
client_secret: clientSecret,
name,
});
export const deleteApplicationCredential = async (
hass: HomeAssistant,
applicationCredentialsId: string
) =>
hass.callWS<void>({
type: "application_credentials/delete",
application_credentials_id: applicationCredentialsId,
});

View File

@@ -157,6 +157,7 @@ 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,
fetchRecent, HistoryStates,
HistoryResult, HistoryResult,
LineChartUnit, LineChartUnit,
TimelineEntity, TimelineEntity,
entityIdHistoryNeedsAttributes, entityIdHistoryNeedsAttributes,
fetchRecentWS,
} from "./history"; } from "./history";
export interface CacheConfig { export interface CacheConfig {
@@ -55,7 +55,7 @@ export const getRecent = (
} }
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId); const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
const prom = fetchRecent( const prom = fetchRecentWS(
hass, hass,
entityId, entityId,
startTime, startTime,
@@ -134,12 +134,12 @@ export const getRecentWithCache = (
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId); const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
const genProm = async () => { const genProm = async () => {
let fetchedHistory: HassEntity[][]; let fetchedHistory: HistoryStates;
try { try {
const results = await Promise.all([ const results = await Promise.all([
curCacheProm, curCacheProm,
fetchRecent( fetchRecentWS(
hass, hass,
entityId, entityId,
toFetchStartTime, toFetchStartTime,

View File

@@ -1,11 +1,14 @@
import { import {
addDays,
addHours, addHours,
addMilliseconds,
addMonths,
differenceInDays, differenceInDays,
endOfToday, endOfToday,
endOfYesterday, endOfYesterday,
startOfToday, startOfToday,
startOfYesterday, startOfYesterday,
} from "date-fns"; } from "date-fns/esm";
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";
@@ -14,9 +17,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)[] = [];
@@ -232,19 +235,24 @@ 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" }),
@@ -350,6 +358,8 @@ 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);
@@ -359,10 +369,34 @@ const getEnergyData = async (
startMinHour, startMinHour,
end, end,
statIDs, statIDs,
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour" period
); );
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(
@@ -373,6 +407,16 @@ 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) => {
@@ -388,15 +432,19 @@ const getEnergyData = async (
} }
}); });
const data = { const data: EnergyData = {
start, start,
end, end,
startCompare,
endCompare,
info, info,
prefs, prefs,
stats, stats,
statsCompare,
co2SignalConfigEntry, co2SignalConfigEntry,
co2SignalEntity, co2SignalEntity,
fossilEnergyConsumption, fossilEnergyConsumption,
fossilEnergyConsumptionCompare,
}; };
return data; return data;
@@ -405,9 +453,11 @@ 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;
@@ -478,7 +528,8 @@ export const getEnergyDataCollection = (
hass, hass,
collection.prefs, collection.prefs,
collection.start, collection.start,
collection.end collection.end,
collection.compare
); );
} }
) as EnergyCollection; ) as EnergyCollection;
@@ -534,6 +585,9 @@ export const getEnergyDataCollection = (
collection._updatePeriodTimeout = undefined; collection._updatePeriodTimeout = undefined;
} }
}; };
collection.setCompare = (compare: boolean) => {
collection.compare = compare;
};
return collection; return collection;
}; };

View File

@@ -1,8 +1,7 @@
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 { computeStateDisplay } from "../common/entity/compute_state_display"; import { computeStateDisplayFromEntityAttributes } from "../common/entity/compute_state_display";
import { computeStateDomain } from "../common/entity/compute_state_domain"; import { computeStateNameFromEntityAttributes } from "../common/entity/compute_state_name";
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";
@@ -27,7 +26,7 @@ const LINE_ATTRIBUTES_TO_KEEP = [
export interface LineChartState { export interface LineChartState {
state: string; state: string;
last_changed: string; last_changed: number;
attributes?: Record<string, any>; attributes?: Record<string, any>;
} }
@@ -47,7 +46,7 @@ export interface LineChartUnit {
export interface TimelineState { export interface TimelineState {
state_localize: string; state_localize: string;
state: string; state: string;
last_changed: string; last_changed: number;
} }
export interface TimelineEntity { export interface TimelineEntity {
@@ -141,6 +140,21 @@ 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
@@ -181,6 +195,27 @@ export const fetchRecent = (
return hass.callApi("GET", url); return hass.callApi("GET", url);
}; };
export const fetchRecentWS = (
hass: HomeAssistant,
entityId: string,
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],
});
export const fetchDate = ( export const fetchDate = (
hass: HomeAssistant, hass: HomeAssistant,
startTime: Date, startTime: Date,
@@ -198,6 +233,27 @@ 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.
@@ -212,46 +268,47 @@ const equalState = (obj1: LineChartState, obj2: LineChartState) =>
const processTimelineEntity = ( const processTimelineEntity = (
localize: LocalizeFunc, localize: LocalizeFunc,
language: FrontendLocaleData, language: FrontendLocaleData,
states: HassEntity[] entityId: string,
states: EntityHistoryState[]
): TimelineEntity => { ): TimelineEntity => {
const data: TimelineState[] = []; const data: TimelineState[] = [];
const last_element = states.length - 1; const first: EntityHistoryState = states[0];
for (const state of states) { for (const state of states) {
if (data.length > 0 && state.state === data[data.length - 1].state) { if (data.length > 0 && state.s === 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: computeStateDisplay(localize, state, language), state_localize: computeStateDisplayFromEntityAttributes(
state: state.state, localize,
last_changed: state.last_changed, language,
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: computeStateName(states[0]), name: computeStateNameFromEntityAttributes(entityId, states[0].a),
entity_id: states[0].entity_id, entity_id: entityId,
data, data,
}; };
}; };
const processLineChartEntities = ( const processLineChartEntities = (
unit, unit,
entities: HassEntity[][] entities: HistoryStates
): LineChartUnit => { ): LineChartUnit => {
const data: LineChartEntity[] = []; const data: LineChartEntity[] = [];
for (const states of entities) { Object.keys(entities).forEach((entityId) => {
const last: HassEntity = states[states.length - 1]; const states = entities[entityId];
const domain = computeStateDomain(last); const first: EntityHistoryState = states[0];
const domain = computeDomain(entityId);
const processedStates: LineChartState[] = []; const processedStates: LineChartState[] = [];
for (const state of states) { for (const state of states) {
@@ -259,18 +316,24 @@ const processLineChartEntities = (
if (DOMAINS_USE_LAST_UPDATED.includes(domain)) { if (DOMAINS_USE_LAST_UPDATED.includes(domain)) {
processedState = { processedState = {
state: state.state, state: state.s,
last_changed: state.last_updated, last_changed: state.lu * 1000,
attributes: {}, attributes: {},
}; };
for (const attr of LINE_ATTRIBUTES_TO_KEEP) { for (const attr of LINE_ATTRIBUTES_TO_KEEP) {
if (attr in state.attributes) { if (attr in state.a) {
processedState.attributes![attr] = state.attributes[attr]; processedState.attributes![attr] = state.a[attr];
} }
} }
} else { } else {
processedState = state; processedState = {
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 (
@@ -289,52 +352,53 @@ const processLineChartEntities = (
data.push({ data.push({
domain, domain,
name: computeStateName(last), name: computeStateNameFromEntityAttributes(entityId, first.a),
entity_id: last.entity_id, entity_id: entityId,
states: processedStates, states: processedStates,
}); });
} });
return { return {
unit, unit,
identifier: entities.map((states) => states[0].entity_id).join(""), identifier: Object.keys(entities).join(""),
data, data,
}; };
}; };
const stateUsesUnits = (state: HassEntity) => const stateUsesUnits = (state: HassEntity) =>
"unit_of_measurement" in state.attributes || attributesHaveUnits(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: HassEntity[][], stateHistory: HistoryStates,
localize: LocalizeFunc localize: LocalizeFunc
): HistoryResult => { ): HistoryResult => {
const lineChartDevices: { [unit: string]: HassEntity[][] } = {}; const lineChartDevices: { [unit: string]: HistoryStates } = {};
const timelineDevices: TimelineEntity[] = []; const timelineDevices: TimelineEntity[] = [];
if (!stateHistory) { if (!stateHistory) {
return { line: [], timeline: [] }; return { line: [], timeline: [] };
} }
Object.keys(stateHistory).forEach((entityId) => {
stateHistory.forEach((stateInfo) => { const stateInfo = stateHistory[entityId];
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.attributes && stateUsesUnits(state)); stateInfo.find((state) => state.a && attributesHaveUnits(state.a));
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.attributes.unit_of_measurement || " "; unit = stateWithUnitorStateClass.a.unit_of_measurement || " ";
} else { } else {
unit = { unit = {
climate: hass.config.unit_system.temperature, climate: hass.config.unit_system.temperature,
@@ -348,12 +412,15 @@ export const computeHistory = (
if (!unit) { if (!unit) {
timelineDevices.push( timelineDevices.push(
processTimelineEntity(localize, hass.locale, stateInfo) processTimelineEntity(localize, hass.locale, entityId, stateInfo)
); );
} else if (unit in lineChartDevices) { } else if (unit in lineChartDevices && entityId in lineChartDevices[unit]) {
lineChartDevices[unit].push(stateInfo); lineChartDevices[unit][entityId].push(...stateInfo);
} else { } else {
lineChartDevices[unit] = [stateInfo]; if (!(unit in lineChartDevices)) {
lineChartDevices[unit] = {};
}
lineChartDevices[unit][entityId] = stateInfo;
} }
}); });

View File

@@ -42,8 +42,18 @@ export const domainToName = (
manifest?: IntegrationManifest manifest?: IntegrationManifest
) => localize(`component.${domain}.title`) || manifest?.name || domain; ) => localize(`component.${domain}.title`) || manifest?.name || domain;
export const fetchIntegrationManifests = (hass: HomeAssistant) => export const fetchIntegrationManifests = (
hass.callWS<IntegrationManifest[]>({ type: "manifest/list" }); hass: HomeAssistant,
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

@@ -10,7 +10,8 @@ const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
export const CONTINUOUS_DOMAINS = ["proximity", "sensor"]; export const CONTINUOUS_DOMAINS = ["proximity", "sensor"];
export interface LogbookEntry { export interface LogbookEntry {
when: string; // Python timestamp. Do *1000 to get JS timestamp.
when: number;
name: string; name: string;
message?: string; message?: string;
entity_id?: string; entity_id?: string;
@@ -25,6 +26,7 @@ export interface LogbookEntry {
context_entity_id?: string; context_entity_id?: string;
context_entity_id_name?: string; context_entity_id_name?: string;
context_name?: string; context_name?: string;
context_message?: string;
state?: string; state?: string;
} }
@@ -46,7 +48,6 @@ export const getLogbookDataForContext = async (
startDate, startDate,
undefined, undefined,
undefined, undefined,
undefined,
contextId contextId
) )
); );
@@ -56,24 +57,28 @@ export const getLogbookData = async (
hass: HomeAssistant, hass: HomeAssistant,
startDate: string, startDate: string,
endDate: string, endDate: string,
entityId?: string, entityIds?: string[],
entity_matches_only?: boolean deviceIds?: string[]
): Promise<LogbookEntry[]> => { ): Promise<LogbookEntry[]> => {
const localize = await hass.loadBackendTranslation("device_class"); const localize = await hass.loadBackendTranslation("device_class");
return addLogbookMessage( return addLogbookMessage(
hass, hass,
localize, localize,
await getLogbookDataCache( // bypass cache if we have a device ID
deviceIds?.length
? await getLogbookDataFromServer(
hass, hass,
startDate, startDate,
endDate, endDate,
entityId, entityIds,
entity_matches_only undefined,
deviceIds
) )
: await getLogbookDataCache(hass, startDate, endDate, entityIds)
); );
}; };
export const addLogbookMessage = ( const addLogbookMessage = (
hass: HomeAssistant, hass: HomeAssistant,
localize: LocalizeFunc, localize: LocalizeFunc,
logbookData: LogbookEntry[] logbookData: LogbookEntry[]
@@ -93,78 +98,82 @@ export const addLogbookMessage = (
return logbookData; return logbookData;
}; };
export const getLogbookDataCache = async ( 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 = "*";
if (!entityId) { const entityIdKey = entityId ? entityId.toString() : ALL_ENTITIES;
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 (entityId in DATA_CACHE[cacheKey]) { if (entityIdKey in DATA_CACHE[cacheKey]) {
return DATA_CACHE[cacheKey][entityId]; return DATA_CACHE[cacheKey][entityIdKey];
} }
if (entityId !== ALL_ENTITIES && DATA_CACHE[cacheKey][ALL_ENTITIES]) { if (entityId && DATA_CACHE[cacheKey][ALL_ENTITIES]) {
const entities = await DATA_CACHE[cacheKey][ALL_ENTITIES]; const entities = await DATA_CACHE[cacheKey][ALL_ENTITIES];
return entities.filter((entity) => entity.entity_id === entityId); return entities.filter(
(entity) => entity.entity_id && entityId.includes(entity.entity_id)
);
} }
DATA_CACHE[cacheKey][entityId] = getLogbookDataFromServer( DATA_CACHE[cacheKey][entityIdKey] = getLogbookDataFromServer(
hass, hass,
startDate, startDate,
endDate, endDate,
entityId !== ALL_ENTITIES ? entityId : undefined, entityId
entity_matches_only );
).then((entries) => entries.reverse()); return DATA_CACHE[cacheKey][entityIdKey];
return DATA_CACHE[cacheKey][entityId];
}; };
const getLogbookDataFromServer = async ( const getLogbookDataFromServer = (
hass: HomeAssistant, hass: HomeAssistant,
startDate: string, startDate: string,
endDate?: string, endDate?: string,
entityId?: string, entityIds?: string[],
entitymatchesOnly?: boolean, contextId?: string,
contextId?: string deviceIds?: string[]
) => { ): Promise<LogbookEntry[]> => {
const params = new URLSearchParams(); // 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.resolve([]);
}
const params: any = {
type: "logbook/get_events",
start_time: startDate,
};
if (endDate) { if (endDate) {
params.append("end_time", endDate); params.end_time = endDate;
} }
if (entityId) { if (entityIds?.length) {
params.append("entity", entityId); params.entity_ids = entityIds;
} }
if (entitymatchesOnly) { if (deviceIds?.length) {
params.append("entity_matches_only", ""); params.device_ids = deviceIds;
} }
if (contextId) { if (contextId) {
params.append("context_id", contextId); params.context_id = contextId;
} }
return hass.callWS<LogbookEntry[]>(params);
return hass.callApi<LogbookEntry[]>(
"GET",
`logbook/${startDate}?${params.toString()}`
);
}; };
export const clearLogbookCache = (startDate: string, endDate: string) => { export const clearLogbookCache = (startDate: string, endDate: string) => {
DATA_CACHE[`${startDate}${endDate}`] = {}; DATA_CACHE[`${startDate}${endDate}`] = {};
}; };
export const getLogbookMessage = ( const getLogbookMessage = (
hass: HomeAssistant, hass: HomeAssistant,
localize: LocalizeFunc, localize: LocalizeFunc,
state: string, state: 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, integration?: string | 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,7 +7,10 @@ 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 { supportsFeature } from "../common/entity/supports-feature"; import {
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";
@@ -35,8 +38,13 @@ export interface UpdateEntity extends HassEntityBase {
} }
export const updateUsesProgress = (entity: UpdateEntity): boolean => export const updateUsesProgress = (entity: UpdateEntity): boolean =>
supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) && updateUsesProgressFromAttributes(entity.attributes);
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,
@@ -49,6 +57,11 @@ 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

@@ -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: number[]; nodes: ZWaveJSNodeStatus[];
} }
export interface ZWaveJSNodeStatus { export interface ZWaveJSNodeStatus {
@@ -200,8 +200,7 @@ export interface ZWaveJSNodeConfigParamMetadata {
export interface ZWaveJSSetConfigParamData { export interface ZWaveJSSetConfigParamData {
type: string; type: string;
entry_id: string; device_id: string;
node_id: number;
property: number; property: number;
property_key?: number; property_key?: number;
value: string | number; value: string | number;
@@ -427,49 +426,41 @@ export const unprovisionZwaveSmartStartNode = (
export const fetchZwaveNodeStatus = ( export const fetchZwaveNodeStatus = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, device_id: string
node_id: number
): Promise<ZWaveJSNodeStatus> => ): Promise<ZWaveJSNodeStatus> =>
hass.callWS({ hass.callWS({
type: "zwave_js/node_status", type: "zwave_js/node_status",
entry_id, device_id,
node_id,
}); });
export const fetchZwaveNodeMetadata = ( export const fetchZwaveNodeMetadata = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, device_id: string
node_id: number
): Promise<ZwaveJSNodeMetadata> => ): Promise<ZwaveJSNodeMetadata> =>
hass.callWS({ hass.callWS({
type: "zwave_js/node_metadata", type: "zwave_js/node_metadata",
entry_id, device_id,
node_id,
}); });
export const fetchZwaveNodeConfigParameters = ( export const fetchZwaveNodeConfigParameters = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, device_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",
entry_id, device_id,
node_id,
}); });
export const setZwaveNodeConfigParameter = ( export const setZwaveNodeConfigParameter = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, device_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",
entry_id, device_id,
node_id,
property, property,
value, value,
property_key, property_key,
@@ -479,42 +470,36 @@ export const setZwaveNodeConfigParameter = (
export const reinterviewZwaveNode = ( export const reinterviewZwaveNode = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, device_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",
entry_id, device_id,
node_id,
} }
); );
export const healZwaveNode = ( export const healZwaveNode = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, device_id: string
node_id: number
): Promise<boolean> => ): Promise<boolean> =>
hass.callWS({ hass.callWS({
type: "zwave_js/heal_node", type: "zwave_js/heal_node",
entry_id, device_id,
node_id,
}); });
export const removeFailedZwaveNode = ( export const removeFailedZwaveNode = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, device_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",
entry_id, device_id,
node_id,
} }
); );
@@ -538,16 +523,14 @@ export const stopHealZwaveNetwork = (
export const subscribeZwaveNodeReady = ( export const subscribeZwaveNodeReady = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, device_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",
entry_id, device_id,
node_id,
} }
); );

View File

@@ -518,10 +518,9 @@ class DataEntryFlowDialog extends LitElement {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
} inset-inline-start: initial;
:host-context([style*="direction: rtl;"]) .dialog-actions { inset-inline-end: 0px;
right: auto; direction: var(--direction);
left: 0;
} }
.dialog-actions > * { .dialog-actions > * {
color: var(--secondary-text-color); color: var(--secondary-text-color);

View File

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

View File

@@ -104,11 +104,8 @@ class StepFlowPickFlow extends LitElement {
margin: 16px 0; margin: 16px 0;
} }
h2 { h2 {
padding-right: 66px; padding-inline-end: 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,11 +311,8 @@ class StepFlowPickHandler extends LitElement {
border-bottom-color: var(--divider-color); border-bottom-color: var(--divider-color);
} }
h2 { h2 {
padding-right: 66px; padding-inline-end: 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,7 +3,11 @@ 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,6 +1,9 @@
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
@@ -40,7 +43,17 @@ export interface DialogState {
dialogParams?: unknown; dialogParams?: unknown;
} }
const LOADED = {}; interface LoadedDialogInfo {
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,
@@ -60,11 +73,25 @@ export const showDialog = async (
} }
return; return;
} }
LOADED[dialogTag] = dialogImport().then(() => { LOADED[dialogTag] = {
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) {
@@ -93,25 +120,29 @@ 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 = () => { export const replaceDialog = (dialogElement: HassDialog) => {
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: HassDialog = await LOADED[dialogTag]; const dialogElement = await LOADED[dialogTag].element;
if (dialogElement.closeDialog) { if (dialogElement.closeDialog) {
return dialogElement.closeDialog() !== false; return dialogElement.closeDialog() !== false;
} }
@@ -137,3 +168,33 @@ 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

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

View File

@@ -1,4 +1,4 @@
import { startOfYesterday } from "date-fns"; import { startOfYesterday } from "date-fns/esm";
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,17 +1,11 @@
import { startOfYesterday } from "date-fns"; import { startOfYesterday } from "date-fns/esm";
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 } 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 { haStyle } from "../../resources/styles"; import type { HomeAssistant } from "../../types";
import { HomeAssistant } from "../../types";
@customElement("ha-more-info-logbook") @customElement("ha-more-info-logbook")
export class MoreInfoLogbook extends LitElement { export class MoreInfoLogbook extends LitElement {
@@ -19,26 +13,14 @@ 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 _throttleGetLogbookEntries = throttle(() => { private _time = { recent: 86400 };
this._getLogBookData();
}, 10000); private _entityIdAsList = memoizeOne((entityId: string) => [entityId]);
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.entityId) { if (!isComponentLoaded(this.hass, "logbook") || !this.entityId) {
return html``; return html``;
} }
const stateObj = this.hass.states[this.entityId]; const stateObj = this.hass.states[this.entityId];
@@ -48,150 +30,34 @@ 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( >${this.hass.localize("ui.dialogs.more_info_control.show_more")}</a
"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 firstUpdated(): void { protected willUpdate(changedProps: PropertyValues): void {
this._fetchUserPromise = this._fetchUserNames(); super.willUpdate(changedProps);
}
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 {
@@ -200,13 +66,7 @@ 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;
} }
@@ -215,10 +75,6 @@ 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,12 +49,14 @@ 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) {
this.hass.loadBackendTranslation("config", flow.handler); integrations.add(flow.handler);
} }
} }
this.hass.loadBackendTranslation("config", Array.from(integrations));
}); });
} }

View File

@@ -336,6 +336,9 @@ 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,10 +194,13 @@ 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

@@ -0,0 +1,240 @@
import "@material/mwc-button";
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-circular-progress";
import "../../../components/ha-combo-box";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-textfield";
import {
fetchApplicationCredentialsConfig,
createApplicationCredential,
ApplicationCredential,
} from "../../../data/application_credential";
import { domainToName } from "../../../data/integration";
import { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { AddApplicationCredentialDialogParams } from "./show-dialog-add-application-credential";
interface Domain {
id: string;
name: string;
}
const rowRenderer: ComboBoxLitRenderer<Domain> = (item) => html`<mwc-list-item>
<span>${item.name}</span>
</mwc-list-item>`;
@customElement("dialog-add-application-credential")
export class DialogAddApplicationCredential extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _loading = false;
// Error message when can't talk to server etc
@state() private _error?: string;
@state() private _params?: AddApplicationCredentialDialogParams;
@state() private _domain?: string;
@state() private _name?: string;
@state() private _clientId?: string;
@state() private _clientSecret?: string;
@state() private _domains?: Domain[];
public showDialog(params: AddApplicationCredentialDialogParams) {
this._params = params;
this._domain = "";
this._name = "";
this._clientId = "";
this._clientSecret = "";
this._error = undefined;
this._loading = false;
this._fetchConfig();
}
private async _fetchConfig() {
const config = await fetchApplicationCredentialsConfig(this.hass);
this._domains = config.domains.map((domain) => ({
id: domain,
name: domainToName(this.hass.localize, domain),
}));
}
protected render(): TemplateResult {
if (!this._params || !this._domains) {
return html``;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this.hass.localize(
"ui.panel.config.application_credentials.editor.caption"
)
)}
>
<div>
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
<ha-combo-box
name="domain"
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.domain"
)}
.value=${this._domain}
.renderer=${rowRenderer}
.items=${this._domains}
item-id-path="id"
item-value-path="id"
item-label-path="name"
required
@value-changed=${this._handleDomainPicked}
></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
class="clientId"
name="clientId"
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_id"
)}
.value=${this._clientId}
required
@input=${this._handleValueChanged}
error-message=${this.hass.localize("ui.common.error_required")}
dialogInitialFocus
></ha-textfield>
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_secret"
)}
type="password"
name="clientSecret"
.value=${this._clientSecret}
required
@input=${this._handleValueChanged}
error-message=${this.hass.localize("ui.common.error_required")}
></ha-textfield>
</div>
${this._loading
? html`
<div slot="primaryAction" class="submit-spinner">
<ha-circular-progress active></ha-circular-progress>
</div>
`
: html`
<mwc-button
slot="primaryAction"
.disabled=${!this._domain ||
!this._clientId ||
!this._clientSecret}
@click=${this._createApplicationCredential}
>
${this.hass.localize(
"ui.panel.config.application_credentials.editor.create"
)}
</mwc-button>
`}
</ha-dialog>
`;
}
public closeDialog() {
this._params = undefined;
this._domains = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private async _handleDomainPicked(ev: PolymerChangedEvent<string>) {
const target = ev.target as any;
if (target.selectedItem) {
this._domain = target.selectedItem.id;
}
}
private _handleValueChanged(ev: CustomEvent) {
this._error = undefined;
const name = (ev.target as any).name;
const value = (ev.target as any).value;
this[`_${name}`] = value;
}
private async _createApplicationCredential(ev) {
ev.preventDefault();
if (!this._domain || !this._clientId || !this._clientSecret) {
return;
}
this._loading = true;
this._error = "";
let applicationCredential: ApplicationCredential;
try {
applicationCredential = await createApplicationCredential(
this.hass,
this._domain,
this._clientId,
this._clientSecret,
this._name
);
} catch (err: any) {
this._loading = false;
this._error = err.message;
return;
}
this._params!.applicationCredentialAddedCallback(applicationCredential);
this.closeDialog();
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 500px;
--dialog-z-index: 10;
}
.row {
display: flex;
padding: 8px 0;
}
ha-combo-box {
display: block;
margin-bottom: 24px;
}
ha-textfield {
display: block;
margin-bottom: 24px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-add-application-credential": DialogAddApplicationCredential;
}
}

View File

@@ -0,0 +1,283 @@
import { mdiDelete, mdiPlus } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { LocalizeFunc } from "../../../common/translations/localize";
import {
DataTableColumnContainer,
SelectionChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-icon";
import "../../../components/ha-fab";
import "../../../components/ha-help-tooltip";
import "../../../components/ha-svg-icon";
import {
ApplicationCredential,
deleteApplicationCredential,
fetchApplicationCredentials,
} from "../../../data/application_credential";
import { domainToName } from "../../../data/integration";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-tabs-subpage-data-table";
import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table";
import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import { showAddApplicationCredentialDialog } from "./show-dialog-add-application-credential";
@customElement("ha-config-application-credentials")
export class HaConfigApplicationCredentials extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() public _applicationCredentials: ApplicationCredential[] = [];
@property() public isWide!: boolean;
@property() public narrow!: boolean;
@property() public route!: Route;
@state() private _selected: string[] = [];
@query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable;
private _columns = memoizeOne(
(narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => {
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: {
title: localize(
"ui.panel.config.application_credentials.picker.headers.client_id"
),
width: "30%",
direction: "asc",
hidden: narrow,
template: (_, entry: ApplicationCredential) =>
html`${entry.client_id}`,
},
application: {
title: localize(
"ui.panel.config.application_credentials.picker.headers.application"
),
sortable: true,
width: "30%",
direction: "asc",
template: (_, entry) => html`${domainToName(localize, entry.domain)}`,
},
};
return columns;
}
);
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
this._loadTranslations();
this._fetchApplicationCredentials();
}
protected render() {
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
backPath="/config"
.tabs=${configSections.devices}
.columns=${this._columns(this.narrow, this.hass.localize)}
.data=${this._applicationCredentials}
hasFab
selectable
@selection-changed=${this._handleSelectionChanged}
>
${this._selected.length
? html`
<div
class=${classMap({
"header-toolbar": this.narrow,
"table-header": !this.narrow,
})}
slot="header"
>
<p class="selected-txt">
${this.hass.localize(
"ui.panel.config.application_credentials.picker.selected",
"number",
this._selected.length
)}
</p>
<div class="header-btns">
${!this.narrow
? html`
<mwc-button
@click=${this._removeSelected}
class="warning"
>${this.hass.localize(
"ui.panel.config.application_credentials.picker.remove_selected.button"
)}</mwc-button
>
`
: html`
<ha-icon-button
class="warning"
id="remove-btn"
@click=${this._removeSelected}
.path=${mdiDelete}
.label=${this.hass.localize("ui.common.remove")}
></ha-icon-button>
<ha-help-tooltip
.label=${this.hass.localize(
"ui.panel.config.application_credentials.picker.remove_selected.button"
)}
>
</ha-help-tooltip>
`}
</div>
</div>
`
: html``}
<ha-fab
slot="fab"
.label=${this.hass.localize(
"ui.panel.config.application_credentials.picker.add_application_credential"
)}
extended
@click=${this._addApplicationCredential}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</hass-tabs-subpage-data-table>
`;
}
private _handleSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selected = ev.detail.value;
}
private _removeSelected() {
showConfirmationDialog(this, {
title: this.hass.localize(
`ui.panel.config.application_credentials.picker.remove_selected.confirm_title`,
"number",
this._selected.length
),
text: this.hass.localize(
"ui.panel.config.application_credentials.picker.remove_selected.confirm_text"
),
confirmText: this.hass.localize("ui.common.remove"),
dismissText: this.hass.localize("ui.common.cancel"),
confirm: async () => {
try {
await Promise.all(
this._selected.map(async (applicationCredential) => {
await deleteApplicationCredential(
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._fetchApplicationCredentials();
},
});
}
private async _loadTranslations() {
await this.hass.loadBackendTranslation("title", undefined, true);
}
private async _fetchApplicationCredentials() {
this._applicationCredentials = await fetchApplicationCredentials(this.hass);
}
private _addApplicationCredential() {
showAddApplicationCredentialDialog(this, {
applicationCredentialAddedCallback: async (
applicationCredential: ApplicationCredential
) => {
if (applicationCredential) {
this._applicationCredentials = [
...this._applicationCredentials,
applicationCredential,
];
}
},
});
}
static get styles(): CSSResultGroup {
return css`
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
height: 56px;
background-color: var(--mdc-text-field-fill-color, whitesmoke);
border-bottom: 1px solid
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
box-sizing: border-box;
}
.header-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
color: var(--secondary-text-color);
position: relative;
top: -4px;
}
.selected-txt {
font-weight: bold;
padding-left: 16px;
}
.table-header .selected-txt {
margin-top: 20px;
}
.header-toolbar .selected-txt {
font-size: 16px;
}
.header-toolbar .header-btns {
margin-right: -12px;
}
.header-btns {
display: flex;
}
.header-btns > mwc-button,
.header-btns > ha-icon-button {
margin: 8px;
}
ha-button-menu {
margin-left: 8px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-application-credentials": HaConfigApplicationCredentials;
}
}

View File

@@ -0,0 +1,22 @@
import { fireEvent } from "../../../common/dom/fire_event";
import { ApplicationCredential } from "../../../data/application_credential";
export interface AddApplicationCredentialDialogParams {
applicationCredentialAddedCallback: (
applicationCredential: ApplicationCredential
) => void;
}
export const loadAddApplicationCredentialDialog = () =>
import("./dialog-add-application-credential");
export const showAddApplicationCredentialDialog = (
element: HTMLElement,
dialogParams: AddApplicationCredentialDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-add-application-credential",
dialogImport: loadAddApplicationCredentialDialog,
dialogParams,
});
};

View File

@@ -2,7 +2,10 @@ 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 { HassEntity } from "home-assistant-js-websocket/dist/types"; import {
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";
@@ -16,9 +19,11 @@ import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next"; import "../../../components/ha-icon-next";
import "../../logbook/ha-logbook";
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";
@@ -26,11 +31,13 @@ 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";
@@ -44,6 +51,7 @@ import {
loadAreaRegistryDetailDialog, loadAreaRegistryDetailDialog,
showAreaRegistryDetailDialog, showAreaRegistryDetailDialog,
} from "./show-dialog-area-registry-detail"; } from "./show-dialog-area-registry-detail";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
declare type NameAndEntity<EntityType extends HassEntity> = { declare type NameAndEntity<EntityType extends HassEntity> = {
name: string; name: string;
@@ -51,17 +59,11 @@ declare type NameAndEntity<EntityType extends HassEntity> = {
}; };
@customElement("ha-config-area-page") @customElement("ha-config-area-page")
class HaConfigAreaPage extends LitElement { class HaConfigAreaPage extends SubscribeMixin(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;
@@ -70,8 +72,16 @@ class HaConfigAreaPage extends 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,
@@ -86,7 +96,7 @@ class HaConfigAreaPage extends LitElement {
registryDevices: DeviceRegistryEntry[], registryDevices: DeviceRegistryEntry[],
registryEntities: EntityRegistryEntry[] registryEntities: EntityRegistryEntry[]
) => { ) => {
const devices = new Map(); const devices = new Map<string, DeviceRegistryEntry>();
for (const device of registryDevices) { for (const device of registryDevices) {
if (device.area_id === areaId) { if (device.area_id === areaId) {
@@ -102,7 +112,7 @@ class HaConfigAreaPage extends LitElement {
if (entity.area_id === areaId) { if (entity.area_id === areaId) {
entities.push(entity); entities.push(entity);
} }
} else if (devices.has(entity.device_id)) { } else if (entity.device_id && devices.has(entity.device_id)) {
indirectEntities.push(entity); indirectEntities.push(entity);
} }
} }
@@ -115,6 +125,20 @@ class HaConfigAreaPage extends 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();
@@ -127,8 +151,26 @@ class HaConfigAreaPage extends 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 {
const area = this._area(this.areaId, this.areas); if (!this._areas || !this._devices || !this._entities) {
return html``;
}
const area = this._area(this.areaId, this._areas);
if (!area) { if (!area) {
return html` return html`
@@ -139,11 +181,12 @@ class HaConfigAreaPage extends LitElement {
`; `;
} }
const { devices, entities } = this._memberships( const memberships = 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) {
@@ -359,8 +402,6 @@ class HaConfigAreaPage extends LitElement {
</ha-card> </ha-card>
` `
: ""} : ""}
</div>
<div class="column">
${isComponentLoaded(this.hass, "scene") ${isComponentLoaded(this.hass, "scene")
? html` ? html`
<ha-card <ha-card
@@ -442,6 +483,26 @@ class HaConfigAreaPage extends 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>
`; `;
@@ -699,6 +760,13 @@ class HaConfigAreaPage extends LitElement {
opacity: 0.5; opacity: 0.5;
border-radius: 50%; border-radius: 50%;
} }
ha-logbook {
height: 800px;
}
:host([narrow]) ha-logbook {
height: 400px;
overflow: auto;
}
`, `,
]; ];
} }

View File

@@ -1,6 +1,7 @@
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 } from "lit/decorators"; import { customElement, property, state } 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";
@@ -9,12 +10,20 @@ import "../../../components/ha-svg-icon";
import { import {
AreaRegistryEntry, AreaRegistryEntry,
createAreaRegistryEntry, createAreaRegistryEntry,
subscribeAreaRegistry,
} from "../../../data/area_registry"; } from "../../../data/area_registry";
import type { DeviceRegistryEntry } from "../../../data/device_registry"; import {
import type { EntityRegistryEntry } from "../../../data/entity_registry"; DeviceRegistryEntry,
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";
@@ -24,7 +33,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 LitElement { export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public isWide?: boolean; @property() public isWide?: boolean;
@@ -33,13 +42,13 @@ export class HaConfigAreasDashboard extends LitElement {
@property() public route!: Route; @property() public route!: Route;
@property() public areas!: AreaRegistryEntry[]; @state() private _areas!: AreaRegistryEntry[];
@property() public devices!: DeviceRegistryEntry[]; @state() private _devices!: DeviceRegistryEntry[];
@property() public entities!: EntityRegistryEntry[]; @state() private _entities!: EntityRegistryEntry[];
private _areas = memoizeOne( private _processAreas = memoizeOne(
( (
areas: AreaRegistryEntry[], areas: AreaRegistryEntry[],
devices: DeviceRegistryEntry[], devices: DeviceRegistryEntry[],
@@ -75,6 +84,20 @@ export class HaConfigAreasDashboard extends 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
@@ -92,7 +115,13 @@ export class HaConfigAreasDashboard extends LitElement {
@click=${this._showHelp} @click=${this._showHelp}
></ha-icon-button> ></ha-icon-button>
<div class="container"> <div class="container">
${this._areas(this.areas, this.devices, this.entities).map( ${!this._areas || !this._devices || !this._entities
? ""
: 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,20 +1,4 @@
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { customElement, property } from "lit/decorators";
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,
@@ -46,44 +30,6 @@ 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;
@@ -91,37 +37,11 @@ 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,17 +460,13 @@ 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: right; float: var(--float-end, 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,16 +288,13 @@ 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: right; float: var(--float-end, 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,16 +487,13 @@ 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: right; float: var(--float-end, 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,7 +5,9 @@ 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")
@@ -39,20 +41,57 @@ 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=${this.trigger} .data=${data}
.hass=${this.hass} .hass=${this.hass}
.computeLabel=${this._computeLabelCallback} .computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@@ -62,7 +101,14 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement {
private _valueChanged(ev: CustomEvent): void { private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation(); ev.stopPropagation();
const newTrigger = ev.detail.value; // Convert back to duration string representation
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,9 +1,11 @@
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 "../../../../components/ha-card"; import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-alert"; import "../../../../components/ha-alert";
import "../../../../components/ha-card";
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";
@@ -13,6 +15,7 @@ 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")
@@ -48,6 +51,11 @@ 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
@@ -85,8 +93,13 @@ export class CloudRemotePref extends LitElement {
class="break-word" class="break-word"
rel="noreferrer" rel="noreferrer"
> >
https://${remote_domain}</a ${hiddenURL}</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
@@ -133,6 +146,14 @@ 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 {
@@ -154,9 +175,6 @@ 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;
} }
@@ -178,6 +196,11 @@ 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,9 +189,11 @@ 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)
: await fetchBackupInfo(this.hass).then( : isComponentLoaded(this.hass, "backup")
? 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

@@ -11,8 +11,6 @@ import { customElement, property, state } from "lit/decorators";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry"; import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { import {
fetchZwaveNodeStatus, fetchZwaveNodeStatus,
getZwaveJsIdentifiersFromDevice,
ZWaveJSNodeIdentifiers,
ZWaveJSNodeStatus, ZWaveJSNodeStatus,
} from "../../../../../../data/zwave_js"; } from "../../../../../../data/zwave_js";
import { haStyle } from "../../../../../../resources/styles"; import { haStyle } from "../../../../../../resources/styles";
@@ -20,6 +18,7 @@ import { HomeAssistant } from "../../../../../../types";
import { showZWaveJSReinterviewNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-reinterview-node"; 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 { 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"; import { showZWaveJSRemoveFailedNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-remove-failed-node";
import { getConfigEntries } from "../../../../../../data/config_entries";
@customElement("ha-device-actions-zwave_js") @customElement("ha-device-actions-zwave_js")
export class HaDeviceActionsZWaveJS extends LitElement { export class HaDeviceActionsZWaveJS extends LitElement {
@@ -29,34 +28,37 @@ export class HaDeviceActionsZWaveJS extends LitElement {
@state() private _entryId?: string; @state() private _entryId?: string;
@state() private _nodeId?: number;
@state() private _node?: ZWaveJSNodeStatus; @state() private _node?: ZWaveJSNodeStatus;
protected updated(changedProperties: PropertyValues) { public willUpdate(changedProperties: PropertyValues) {
super.willUpdate(changedProperties);
if (changedProperties.has("device")) { 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(); this._fetchNodeDetails();
} }
} }
protected async _fetchNodeDetails() { protected async _fetchNodeDetails() {
if (!this._nodeId || !this._entryId) { if (!this.device) {
return; return;
} }
this._node = await fetchZwaveNodeStatus( this._node = undefined;
this.hass,
this._entryId, const configEntries = await getConfigEntries(this.hass, {
this._nodeId domain: "zwave_js",
});
const configEntry = configEntries.find((entry) =>
this.device.config_entries.includes(entry.entry_id)
); );
if (!configEntry) {
return;
}
this._entryId = configEntry.entry_id;
this._node = await fetchZwaveNodeStatus(this.hass, this.device.id);
} }
protected render(): TemplateResult { protected render(): TemplateResult {
@@ -96,33 +98,30 @@ export class HaDeviceActionsZWaveJS extends LitElement {
} }
private async _reinterviewClicked() { private async _reinterviewClicked() {
if (!this._nodeId || !this._entryId) { if (!this.device) {
return; return;
} }
showZWaveJSReinterviewNodeDialog(this, { showZWaveJSReinterviewNodeDialog(this, {
entry_id: this._entryId, device_id: this.device.id,
node_id: this._nodeId,
}); });
} }
private async _healNodeClicked() { private async _healNodeClicked() {
if (!this._nodeId || !this._entryId) { if (!this.device) {
return; return;
} }
showZWaveJSHealNodeDialog(this, { showZWaveJSHealNodeDialog(this, {
entry_id: this._entryId, entry_id: this._entryId!,
node_id: this._nodeId,
device: this.device, device: this.device,
}); });
} }
private async _removeFailedNode() { private async _removeFailedNode() {
if (!this._nodeId || !this._entryId) { if (!this.device) {
return; return;
} }
showZWaveJSRemoveFailedNodeDialog(this, { showZWaveJSRemoveFailedNodeDialog(this, {
entry_id: this._entryId, device_id: this.device.id,
node_id: this._nodeId,
}); });
} }

View File

@@ -14,10 +14,8 @@ import {
} from "../../../../../../data/config_entries"; } from "../../../../../../data/config_entries";
import { import {
fetchZwaveNodeStatus, fetchZwaveNodeStatus,
getZwaveJsIdentifiersFromDevice,
nodeStatus, nodeStatus,
ZWaveJSNodeStatus, ZWaveJSNodeStatus,
ZWaveJSNodeIdentifiers,
SecurityClass, SecurityClass,
} from "../../../../../../data/zwave_js"; } from "../../../../../../data/zwave_js";
import { haStyle } from "../../../../../../resources/styles"; import { haStyle } from "../../../../../../resources/styles";
@@ -29,57 +27,41 @@ export class HaDeviceInfoZWaveJS extends LitElement {
@property({ attribute: false }) public device!: DeviceRegistryEntry; @property({ attribute: false }) public device!: DeviceRegistryEntry;
@state() private _entryId?: string;
@state() private _configEntry?: ConfigEntry; @state() private _configEntry?: ConfigEntry;
@state() private _multipleConfigEntries = false; @state() private _multipleConfigEntries = false;
@state() private _nodeId?: number;
@state() private _node?: ZWaveJSNodeStatus; @state() private _node?: ZWaveJSNodeStatus;
protected updated(changedProperties: PropertyValues) { public willUpdate(changedProperties: PropertyValues) {
super.willUpdate(changedProperties);
if (changedProperties.has("device")) { 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(); this._fetchNodeDetails();
} }
} }
protected async _fetchNodeDetails() { protected async _fetchNodeDetails() {
if (!this._nodeId || !this._entryId) { if (!this.device) {
return; return;
} }
const configEntries = await getConfigEntries(this.hass, { const configEntries = await getConfigEntries(this.hass, {
domain: "zwave_js", domain: "zwave_js",
}); });
let zwaveJsConfEntries = 0;
for (const entry of configEntries) { this._multipleConfigEntries = configEntries.length > 1;
if (zwaveJsConfEntries) {
this._multipleConfigEntries = true; const configEntry = configEntries.find((entry) =>
} this.device.config_entries.includes(entry.entry_id)
if (entry.entry_id === this._entryId) { );
this._configEntry = entry;
} if (!configEntry) {
if (this._configEntry && this._multipleConfigEntries) { return;
break;
}
zwaveJsConfEntries++;
} }
this._node = await fetchZwaveNodeStatus( this._configEntry = configEntry;
this.hass,
this._entryId, this._node = await fetchZwaveNodeStatus(this.hass, this.device.id);
this._nodeId
);
} }
protected render(): TemplateResult { protected render(): TemplateResult {

View File

@@ -63,6 +63,7 @@ import {
loadDeviceRegistryDetailDialog, loadDeviceRegistryDetailDialog,
showDeviceRegistryDetailDialog, showDeviceRegistryDetailDialog,
} from "./device-registry-detail/show-dialog-device-registry-detail"; } from "./device-registry-detail/show-dialog-device-registry-detail";
import "../../logbook/ha-logbook";
export interface EntityRegistryStateEntry extends EntityRegistryEntry { export interface EntityRegistryStateEntry extends EntityRegistryEntry {
stateName?: string | null; stateName?: string | null;
@@ -99,6 +100,8 @@ export class HaConfigDevicePage extends LitElement {
@state() private _deleteButtons?: (TemplateResult | string)[]; @state() private _deleteButtons?: (TemplateResult | string)[];
private _logbookTime = { recent: 86400 };
private _device = memoizeOne( private _device = memoizeOne(
( (
deviceId: string, deviceId: string,
@@ -131,6 +134,13 @@ export class HaConfigDevicePage extends LitElement {
) )
); );
private _deviceIdInList = memoizeOne((deviceId: string) => [deviceId]);
private _entityIds = memoizeOne(
(entries: EntityRegistryStateEntry[]): string[] =>
entries.map((entry) => entry.entity_id)
);
private _entitiesByCategory = memoizeOne( private _entitiesByCategory = memoizeOne(
(entities: EntityRegistryEntry[]) => { (entities: EntityRegistryEntry[]) => {
const result = groupBy(entities, (entry) => const result = groupBy(entities, (entry) =>
@@ -574,6 +584,26 @@ export class HaConfigDevicePage extends LitElement {
` `
: "" : ""
)} )}
${
isComponentLoaded(this.hass, "logbook")
? html`
<ha-card outlined>
<h1 class="card-header">
${this.hass.localize("panel.logbook")}
</h1>
<ha-logbook
.hass=${this.hass}
.time=${this._logbookTime}
.entityIds=${this._entityIds(entities)}
.deviceIds=${this._deviceIdInList(this.deviceId)}
virtualize
narrow
no-icon
></ha-logbook>
</ha-card>
`
: ""
}
</div> </div>
<div class="column"> <div class="column">
${ ${
@@ -895,13 +925,12 @@ export class HaConfigDevicePage extends LitElement {
} }
private _renderIntegrationInfo( private _renderIntegrationInfo(
device, device: DeviceRegistryEntry,
integrations: ConfigEntry[], integrations: ConfigEntry[],
deviceInfo: TemplateResult[], deviceInfo: TemplateResult[],
deviceActions: (string | TemplateResult)[] deviceActions: (string | TemplateResult)[]
): TemplateResult[] { ) {
const domains = integrations.map((int) => int.domain); const domains = integrations.map((int) => int.domain);
const templates: TemplateResult[] = [];
if (domains.includes("mqtt")) { if (domains.includes("mqtt")) {
import( import(
"./device-detail/integration-elements/mqtt/ha-device-actions-mqtt" "./device-detail/integration-elements/mqtt/ha-device-actions-mqtt"
@@ -949,7 +978,6 @@ export class HaConfigDevicePage extends LitElement {
></ha-device-actions-zwave_js> ></ha-device-actions-zwave_js>
`); `);
} }
return templates;
} }
private async _showSettings() { private async _showSettings() {
@@ -1228,6 +1256,13 @@ export class HaConfigDevicePage extends LitElement {
.items { .items {
padding-bottom: 16px; padding-bottom: 16px;
} }
ha-logbook {
height: 400px;
}
:host([narrow]) ha-logbook {
height: 235px;
}
`, `,
]; ];
} }

View File

@@ -16,9 +16,9 @@ import {
} from "../../../components/data-table/ha-data-table"; } from "../../../components/data-table/ha-data-table";
import "../../../components/entity/ha-battery-icon"; import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-button-menu"; import "../../../components/ha-button-menu";
import "../../../components/ha-check-list-item";
import "../../../components/ha-fab"; import "../../../components/ha-fab";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-check-list-item";
import { AreaRegistryEntry } from "../../../data/area_registry"; import { AreaRegistryEntry } from "../../../data/area_registry";
import { ConfigEntry } from "../../../data/config_entries"; import { ConfigEntry } from "../../../data/config_entries";
import { import {
@@ -36,6 +36,7 @@ import "../../../layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import "../integrations/ha-integration-overflow-menu";
import { showZWaveJSAddNodeDialog } from "../integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node"; import { showZWaveJSAddNodeDialog } from "../integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node";
interface DeviceRowData extends DeviceRegistryEntry { interface DeviceRowData extends DeviceRegistryEntry {
@@ -408,6 +409,10 @@ export class HaConfigDeviceDashboard extends LitElement {
(filteredConfigEntry.domain === "zha" || (filteredConfigEntry.domain === "zha" ||
filteredConfigEntry.domain === "zwave_js")} filteredConfigEntry.domain === "zwave_js")}
> >
<ha-integration-overflow-menu
.hass=${this.hass}
slot="toolbar-icon"
></ha-integration-overflow-menu>
${!filteredConfigEntry ${!filteredConfigEntry
? "" ? ""
: filteredConfigEntry.domain === "zwave_js" : filteredConfigEntry.domain === "zwave_js"

View File

@@ -107,10 +107,16 @@ export class EnergyBatterySettings extends LitElement {
> >
</div> </div>
<ha-icon-button <ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.energy.battery.edit_battery_system"
)}
@click=${this._editSource} @click=${this._editSource}
.path=${mdiPencil} .path=${mdiPencil}
></ha-icon-button> ></ha-icon-button>
<ha-icon-button <ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.energy.battery.delete_battery_system"
)}
@click=${this._deleteSource} @click=${this._deleteSource}
.path=${mdiDelete} .path=${mdiDelete}
></ha-icon-button> ></ha-icon-button>

View File

@@ -94,10 +94,16 @@ export class EnergyGasSettings extends LitElement {
: source.stat_energy_from}</span : source.stat_energy_from}</span
> >
<ha-icon-button <ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.energy.gas.edit_gas_source"
)}
@click=${this._editSource} @click=${this._editSource}
.path=${mdiPencil} .path=${mdiPencil}
></ha-icon-button> ></ha-icon-button>
<ha-icon-button <ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.energy.gas.delete_gas_source"
)}
@click=${this._deleteSource} @click=${this._deleteSource}
.path=${mdiDelete} .path=${mdiDelete}
></ha-icon-button> ></ha-icon-button>

View File

@@ -132,10 +132,16 @@ export class EnergyGridSettings extends LitElement {
: flow.stat_energy_from}</span : flow.stat_energy_from}</span
> >
<ha-icon-button <ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.energy.grid.edit_consumption"
)}
@click=${this._editFromSource} @click=${this._editFromSource}
.path=${mdiPencil} .path=${mdiPencil}
></ha-icon-button> ></ha-icon-button>
<ha-icon-button <ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.energy.grid.delete_consumption"
)}
@click=${this._deleteFromSource} @click=${this._deleteFromSource}
.path=${mdiDelete} .path=${mdiDelete}
></ha-icon-button> ></ha-icon-button>
@@ -171,10 +177,16 @@ export class EnergyGridSettings extends LitElement {
: flow.stat_energy_to}</span : flow.stat_energy_to}</span
> >
<ha-icon-button <ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.energy.grid.edit_return"
)}
@click=${this._editToSource} @click=${this._editToSource}
.path=${mdiPencil} .path=${mdiPencil}
></ha-icon-button> ></ha-icon-button>
<ha-icon-button <ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.energy.grid.delete_return"
)}
@click=${this._deleteToSource} @click=${this._deleteToSource}
.path=${mdiDelete} .path=${mdiDelete}
></ha-icon-button> ></ha-icon-button>
@@ -212,6 +224,9 @@ export class EnergyGridSettings extends LitElement {
<ha-icon-button .path=${mdiPencil}></ha-icon-button> <ha-icon-button .path=${mdiPencil}></ha-icon-button>
</a> </a>
<ha-icon-button <ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.energy.grid.remove_co2_signal"
)}
@click=${this._removeCO2Sensor} @click=${this._removeCO2Sensor}
.path=${mdiDelete} .path=${mdiDelete}
></ha-icon-button> ></ha-icon-button>

View File

@@ -104,12 +104,18 @@ export class EnergySolarSettings extends LitElement {
${this.info ${this.info
? html` ? html`
<ha-icon-button <ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.energy.solar.edit_solar_production"
)}
@click=${this._editSource} @click=${this._editSource}
.path=${mdiPencil} .path=${mdiPencil}
></ha-icon-button> ></ha-icon-button>
` `
: ""} : ""}
<ha-icon-button <ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.energy.solar.delete_solar_production"
)}
@click=${this._deleteSource} @click=${this._deleteSource}
.path=${mdiDelete} .path=${mdiDelete}
></ha-icon-button> ></ha-icon-button>

View File

@@ -220,7 +220,7 @@ export class DialogEntityEditor extends LitElement {
} }
private _openMoreInfo(): void { private _openMoreInfo(): void {
replaceDialog(); replaceDialog(this);
fireEvent(this, "hass-more-info", { fireEvent(this, "hass-more-info", {
entityId: this._params!.entity_id, entityId: this._params!.entity_id,
}); });

View File

@@ -61,6 +61,7 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types"; import type { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import "../integrations/ha-integration-overflow-menu";
import { DialogEntityEditor } from "./dialog-entity-editor"; import { DialogEntityEditor } from "./dialog-entity-editor";
import { import {
loadEntityEditorDialog, loadEntityEditorDialog,
@@ -526,6 +527,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
id="entity_id" id="entity_id"
.hasFab=${includeZHAFab} .hasFab=${includeZHAFab}
> >
<ha-integration-overflow-menu
.hass=${this.hass}
slot="toolbar-icon"
></ha-integration-overflow-menu>
${this._selectedEntities.length ${this._selectedEntities.length
? html` ? html`
<div <div

View File

@@ -479,6 +479,11 @@ class HaPanelConfig extends HassRouterPage {
"./integrations/integration-panels/zwave_js/zwave_js-config-router" "./integrations/integration-panels/zwave_js/zwave_js-config-router"
), ),
}, },
application_credentials: {
tag: "ha-config-application-credentials",
load: () =>
import("./application_credentials/ha-config-application-credentials"),
},
}, },
}; };

View File

@@ -6,8 +6,8 @@ import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { dynamicElement } from "../../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
import "../../../components/ha-dialog";
import "../../../components/ha-circular-progress"; import "../../../components/ha-circular-progress";
import "../../../components/ha-dialog";
import { getConfigFlowHandlers } from "../../../data/config_flow"; import { getConfigFlowHandlers } from "../../../data/config_flow";
import { createCounter } from "../../../data/counter"; import { createCounter } from "../../../data/counter";
import { createInputBoolean } from "../../../data/input_boolean"; import { createInputBoolean } from "../../../data/input_boolean";
@@ -16,10 +16,12 @@ import { createInputDateTime } from "../../../data/input_datetime";
import { createInputNumber } from "../../../data/input_number"; import { createInputNumber } from "../../../data/input_number";
import { createInputSelect } from "../../../data/input_select"; import { createInputSelect } from "../../../data/input_select";
import { createInputText } from "../../../data/input_text"; import { createInputText } from "../../../data/input_text";
import { domainToName } from "../../../data/integration";
import { createTimer } from "../../../data/timer"; import { createTimer } from "../../../data/timer";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import { haStyleDialog } from "../../../resources/styles"; import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { Helper } from "./const"; import { Helper } from "./const";
import "./forms/ha-counter-form"; import "./forms/ha-counter-form";
import "./forms/ha-input_boolean-form"; import "./forms/ha-input_boolean-form";
@@ -29,9 +31,7 @@ import "./forms/ha-input_number-form";
import "./forms/ha-input_select-form"; import "./forms/ha-input_select-form";
import "./forms/ha-input_text-form"; import "./forms/ha-input_text-form";
import "./forms/ha-timer-form"; import "./forms/ha-timer-form";
import { domainToName } from "../../../data/integration";
import type { ShowDialogHelperDetailParams } from "./show-dialog-helper-detail"; import type { ShowDialogHelperDetailParams } from "./show-dialog-helper-detail";
import { brandsUrl } from "../../../util/brands-url";
const HELPERS = { const HELPERS = {
input_boolean: createInputBoolean, input_boolean: createInputBoolean,
@@ -187,13 +187,13 @@ export class DialogHelperDetail extends LitElement {
escapeKeyAction escapeKeyAction
.heading=${this._domain .heading=${this._domain
? this.hass.localize( ? this.hass.localize(
"ui.panel.config.helpers.dialog.add_platform", "ui.panel.config.helpers.dialog.create_platform",
"platform", "platform",
this.hass.localize( this.hass.localize(
`ui.panel.config.helpers.types.${this._domain}` `ui.panel.config.helpers.types.${this._domain}`
) || this._domain ) || this._domain
) )
: this.hass.localize("ui.panel.config.helpers.dialog.add_helper")} : this.hass.localize("ui.panel.config.helpers.dialog.create_helper")}
> >
${content} ${content}
</ha-dialog> </ha-dialog>

View File

@@ -35,6 +35,7 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor"; import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import "../integrations/ha-integration-overflow-menu";
import { HELPER_DOMAINS } from "./const"; import { HELPER_DOMAINS } from "./const";
import { showHelperDetailDialog } from "./show-dialog-helper-detail"; import { showHelperDetailDialog } from "./show-dialog-helper-detail";
@@ -210,10 +211,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
"ui.panel.config.helpers.picker.no_helpers" "ui.panel.config.helpers.picker.no_helpers"
)} )}
> >
<ha-integration-overflow-menu
.hass=${this.hass}
slot="toolbar-icon"
></ha-integration-overflow-menu>
<ha-fab <ha-fab
slot="fab" slot="fab"
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.helpers.picker.add_helper" "ui.panel.config.helpers.picker.create_helper"
)} )}
extended extended
@click=${this._createHelpler} @click=${this._createHelpler}

View File

@@ -13,21 +13,20 @@ import {
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";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import type { HASSDomEvent } from "../../../common/dom/fire_event"; import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate"; import { navigate } from "../../../common/navigate";
import "../../../components/search-input";
import { caseInsensitiveStringCompare } from "../../../common/string/compare"; import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import type { LocalizeFunc } from "../../../common/translations/localize"; import type { LocalizeFunc } from "../../../common/translations/localize";
import { extractSearchParam } from "../../../common/url/search-params"; import { extractSearchParam } from "../../../common/url/search-params";
import { nextRender } from "../../../common/util/render-status"; import { nextRender } from "../../../common/util/render-status";
import "../../../components/ha-button-menu"; import "../../../components/ha-button-menu";
import "../../../components/ha-check-list-item";
import "../../../components/ha-checkbox"; import "../../../components/ha-checkbox";
import "../../../components/ha-fab"; import "../../../components/ha-fab";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import "../../../components/ha-check-list-item"; import "../../../components/search-input";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries"; import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
import { import {
getConfigFlowHandlers, getConfigFlowHandlers,
@@ -40,13 +39,13 @@ import {
DeviceRegistryEntry, DeviceRegistryEntry,
subscribeDeviceRegistry, subscribeDeviceRegistry,
} from "../../../data/device_registry"; } from "../../../data/device_registry";
import { fetchDiagnosticHandlers } from "../../../data/diagnostics";
import { import {
EntityRegistryEntry, EntityRegistryEntry,
subscribeEntityRegistry, subscribeEntityRegistry,
} from "../../../data/entity_registry"; } from "../../../data/entity_registry";
import { import {
domainToName, domainToName,
fetchIntegrationManifest,
fetchIntegrationManifests, fetchIntegrationManifests,
IntegrationManifest, IntegrationManifest,
} from "../../../data/integration"; } from "../../../data/integration";
@@ -62,12 +61,12 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types"; import type { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import { HELPER_DOMAINS } from "../helpers/const";
import "./ha-config-flow-card"; import "./ha-config-flow-card";
import "./ha-ignored-config-entry-card"; import "./ha-ignored-config-entry-card";
import "./ha-integration-card"; import "./ha-integration-card";
import type { HaIntegrationCard } from "./ha-integration-card"; import type { HaIntegrationCard } from "./ha-integration-card";
import { fetchDiagnosticHandlers } from "../../../data/diagnostics"; import "./ha-integration-overflow-menu";
import { HELPER_DOMAINS } from "../helpers/const";
export interface ConfigEntryUpdatedEvent { export interface ConfigEntryUpdatedEvent {
entry: ConfigEntry; entry: ConfigEntry;
@@ -156,17 +155,20 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
this._deviceRegistryEntries = entries; this._deviceRegistryEntries = entries;
}), }),
subscribeConfigFlowInProgress(this.hass, async (flowsInProgress) => { subscribeConfigFlowInProgress(this.hass, async (flowsInProgress) => {
const translationsPromisses: Promise<LocalizeFunc>[] = []; const integrations: Set<string> = new Set();
const manifests: Set<string> = new Set();
flowsInProgress.forEach((flow) => { flowsInProgress.forEach((flow) => {
// To render title placeholders // To render title placeholders
if (flow.context.title_placeholders) { if (flow.context.title_placeholders) {
translationsPromisses.push( integrations.add(flow.handler);
this.hass.loadBackendTranslation("config", flow.handler)
);
} }
this._fetchManifest(flow.handler); manifests.add(flow.handler);
}); });
await Promise.all(translationsPromisses); await this.hass.loadBackendTranslation(
"config",
Array.from(integrations)
);
this._fetchIntegrationManifests(manifests);
await nextRender(); await nextRender();
this._configEntriesInProgress = flowsInProgress.map((flow) => ({ this._configEntriesInProgress = flowsInProgress.map((flow) => ({
...flow, ...flow,
@@ -302,9 +304,9 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
this._filter this._filter
); );
const filterMenu = html`<div const filterMenu = html`
slot=${ifDefined(this.narrow ? "toolbar-icon" : "suffix")} <div slot=${ifDefined(this.narrow ? "toolbar-icon" : "suffix")}>
> <div class="menu-badge-container">
${!this._showDisabled && this.narrow && disabledCount ${!this._showDisabled && this.narrow && disabledCount
? html`<span class="badge">${disabledCount}</span>` ? html`<span class="badge">${disabledCount}</span>`
: ""} : ""}
@@ -331,7 +333,17 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
)} )}
</ha-check-list-item> </ha-check-list-item>
</ha-button-menu> </ha-button-menu>
</div>`; </div>
${this.narrow
? html`
<ha-integration-overflow-menu
.hass=${this.hass}
slot="toolbar-icon"
></ha-integration-overflow-menu>
`
: ""}
</div>
`;
return html` return html`
<hass-tabs-subpage <hass-tabs-subpage
@@ -357,6 +369,10 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
${filterMenu} ${filterMenu}
` `
: html` : html`
<ha-integration-overflow-menu
.hass=${this.hass}
slot="toolbar-icon"
></ha-integration-overflow-menu>
<div class="search"> <div class="search">
<search-input <search-input
.hass=${this.hass} .hass=${this.hass}
@@ -551,8 +567,8 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
await scanUSBDevices(this.hass); await scanUSBDevices(this.hass);
} }
private async _fetchManifests() { private async _fetchManifests(integrations?: string[]) {
const fetched = await fetchIntegrationManifests(this.hass); const fetched = await fetchIntegrationManifests(this.hass, integrations);
// Make a copy so we can keep track of previously loaded manifests // Make a copy so we can keep track of previously loaded manifests
// for discovered flows (which are not part of these results) // for discovered flows (which are not part of these results)
const manifests = { ...this._manifests }; const manifests = { ...this._manifests };
@@ -560,23 +576,25 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
this._manifests = manifests; this._manifests = manifests;
} }
private async _fetchManifest(domain: string) { private async _fetchIntegrationManifests(integrations: Set<string>) {
if (domain in this._manifests) { const manifestsToFetch: string[] = [];
return; for (const integration of integrations) {
if (integration in this._manifests) {
continue;
} }
if (this._extraFetchedManifests) { if (this._extraFetchedManifests) {
if (this._extraFetchedManifests.has(domain)) { if (this._extraFetchedManifests.has(integration)) {
return; continue;
} }
} else { } else {
this._extraFetchedManifests = new Set(); this._extraFetchedManifests = new Set();
} }
this._extraFetchedManifests.add(domain); this._extraFetchedManifests.add(integration);
const manifest = await fetchIntegrationManifest(this.hass, domain); manifestsToFetch.push(integration);
this._manifests = { }
...this._manifests, if (manifestsToFetch.length) {
[domain]: manifest, await this._fetchManifests(manifestsToFetch);
}; }
} }
private _handleEntryRemoved(ev: HASSDomEvent<ConfigEntryRemovedEvent>) { private _handleEntryRemoved(ev: HASSDomEvent<ConfigEntryRemovedEvent>) {
@@ -797,10 +815,13 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
padding: 0px 4px; padding: 0px 4px;
color: var(--text-primary-color); color: var(--text-primary-color);
position: absolute; position: absolute;
right: 14px; right: 0px;
top: 8px; top: 4px;
font-size: 0.65em; font-size: 0.65em;
} }
.menu-badge-container {
position: relative;
}
ha-button-menu { ha-button-menu {
color: var(--primary-text-color); color: var(--primary-text-color);
} }

View File

@@ -135,17 +135,19 @@ export class HaIntegrationHeader extends LitElement {
.header { .header {
display: flex; display: flex;
position: relative; position: relative;
padding: 0 8px 8px 16px; padding-top: 0px;
padding-bottom: 8px;
padding-inline-start: 16px;
padding-inline-end: 8px;
direction: var(--direction);
} }
.header img { .header img {
margin-right: 16px;
margin-top: 16px; margin-top: 16px;
margin-inline-start: initial;
margin-inline-end: 16px;
width: 40px; width: 40px;
height: 40px; height: 40px;
} direction: var(--direction);
:host-context([style*="direction: rtl;"]) .header img {
margin-right: auto !important;
margin-left: 16px;
} }
.header .info { .header .info {
flex: 1; flex: 1;

View File

@@ -0,0 +1,45 @@
import { mdiDotsVertical } from "@mdi/js";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-button-menu";
import "../../../components/ha-clickable-list-item";
import "../../../components/ha-icon-button";
import type { HomeAssistant } from "../../../types";
@customElement("ha-integration-overflow-menu")
export class HaIntegrationOverflowMenu extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
protected render() {
return html`
<ha-button-menu activatable corner="BOTTOM_START">
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-clickable-list-item
@click=${this._entryClicked}
href="/config/application_credentials"
aria-label=${this.hass.localize(
"ui.panel.config.application_credentials.caption"
)}
>
${this.hass.localize(
"ui.panel.config.application_credentials.caption"
)}
</ha-clickable-list-item>
</ha-button-menu>
`;
}
private _entryClicked(ev) {
ev.currentTarget.blur();
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-integration-overflow-menu": HaIntegrationOverflowMenu;
}
}

View File

@@ -24,8 +24,6 @@ class DialogZWaveJSHealNode extends LitElement {
@state() private entry_id?: string; @state() private entry_id?: string;
@state() private node_id?: number;
@state() private device?: DeviceRegistryEntry; @state() private device?: DeviceRegistryEntry;
@state() private _status?: string; @state() private _status?: string;
@@ -35,14 +33,12 @@ class DialogZWaveJSHealNode extends LitElement {
public showDialog(params: ZWaveJSHealNodeDialogParams): void { public showDialog(params: ZWaveJSHealNodeDialogParams): void {
this.entry_id = params.entry_id; this.entry_id = params.entry_id;
this.device = params.device; this.device = params.device;
this.node_id = params.node_id;
this._fetchData(); this._fetchData();
} }
public closeDialog(): void { public closeDialog(): void {
this.entry_id = undefined; this.entry_id = undefined;
this._status = undefined; this._status = undefined;
this.node_id = undefined;
this.device = undefined; this.device = undefined;
this._error = undefined; this._error = undefined;
@@ -221,11 +217,7 @@ class DialogZWaveJSHealNode extends LitElement {
} }
this._status = "started"; this._status = "started";
try { try {
this._status = (await healZwaveNode( this._status = (await healZwaveNode(this.hass, this.device!.id))
this.hass,
this.entry_id!,
this.node_id!
))
? "finished" ? "finished"
: "failed"; : "failed";
} catch (err: any) { } catch (err: any) {

View File

@@ -15,9 +15,7 @@ import { ZWaveJSReinterviewNodeDialogParams } from "./show-dialog-zwave_js-reint
class DialogZWaveJSReinterviewNode extends LitElement { class DialogZWaveJSReinterviewNode extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@state() private entry_id?: string; @state() private device_id?: string;
@state() private node_id?: number;
@state() private _status?: string; @state() private _status?: string;
@@ -29,12 +27,11 @@ class DialogZWaveJSReinterviewNode extends LitElement {
params: ZWaveJSReinterviewNodeDialogParams params: ZWaveJSReinterviewNodeDialogParams
): Promise<void> { ): Promise<void> {
this._stages = undefined; this._stages = undefined;
this.entry_id = params.entry_id; this.device_id = params.device_id;
this.node_id = params.node_id;
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.entry_id) { if (!this.device_id) {
return html``; return html``;
} }
@@ -159,8 +156,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
} }
this._subscribed = reinterviewZwaveNode( this._subscribed = reinterviewZwaveNode(
this.hass, this.hass,
this.entry_id!, this.device_id!,
this.node_id!,
this._handleMessage.bind(this) this._handleMessage.bind(this)
); );
} }
@@ -194,8 +190,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
} }
public closeDialog(): void { public closeDialog(): void {
this.entry_id = undefined; this.device_id = undefined;
this.node_id = undefined;
this._status = undefined; this._status = undefined;
this._stages = undefined; this._stages = undefined;

View File

@@ -18,9 +18,7 @@ import { ZWaveJSRemoveFailedNodeDialogParams } from "./show-dialog-zwave_js-remo
class DialogZWaveJSRemoveFailedNode extends LitElement { class DialogZWaveJSRemoveFailedNode extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@state() private entry_id?: string; @state() private device_id?: string;
@state() private node_id?: number;
@state() private _status = ""; @state() private _status = "";
@@ -38,13 +36,12 @@ class DialogZWaveJSRemoveFailedNode extends LitElement {
public async showDialog( public async showDialog(
params: ZWaveJSRemoveFailedNodeDialogParams params: ZWaveJSRemoveFailedNodeDialogParams
): Promise<void> { ): Promise<void> {
this.entry_id = params.entry_id; this.device_id = params.device_id;
this.node_id = params.node_id;
} }
public closeDialog(): void { public closeDialog(): void {
this._unsubscribe(); this._unsubscribe();
this.entry_id = undefined; this.device_id = undefined;
this._status = ""; this._status = "";
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
@@ -56,7 +53,7 @@ class DialogZWaveJSRemoveFailedNode extends LitElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.entry_id || !this.node_id) { if (!this.device_id) {
return html``; return html``;
} }
@@ -166,8 +163,7 @@ class DialogZWaveJSRemoveFailedNode extends LitElement {
this._status = "started"; this._status = "started";
this._subscribed = removeFailedZwaveNode( this._subscribed = removeFailedZwaveNode(
this.hass, this.hass,
this.entry_id!, this.device_id!,
this.node_id!,
(message: any) => this._handleMessage(message) (message: any) => this._handleMessage(message)
).catch((error) => { ).catch((error) => {
this._status = "failed"; this._status = "failed";

View File

@@ -3,7 +3,6 @@ import { DeviceRegistryEntry } from "../../../../../data/device_registry";
export interface ZWaveJSHealNodeDialogParams { export interface ZWaveJSHealNodeDialogParams {
entry_id: string; entry_id: string;
node_id: number;
device: DeviceRegistryEntry; device: DeviceRegistryEntry;
} }

View File

@@ -1,8 +1,7 @@
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
export interface ZWaveJSReinterviewNodeDialogParams { export interface ZWaveJSReinterviewNodeDialogParams {
entry_id: string; device_id: string;
node_id: number;
} }
export const loadReinterviewNodeDialog = () => export const loadReinterviewNodeDialog = () =>

View File

@@ -1,8 +1,7 @@
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
export interface ZWaveJSRemoveFailedNodeDialogParams { export interface ZWaveJSRemoveFailedNodeDialogParams {
entry_id: string; device_id: string;
node_id: number;
} }
export const loadRemoveFailedNodeDialog = () => export const loadRemoveFailedNodeDialog = () =>

View File

@@ -17,7 +17,6 @@ import "../../../../../components/ha-svg-icon";
import { import {
fetchZwaveDataCollectionStatus, fetchZwaveDataCollectionStatus,
fetchZwaveNetworkStatus, fetchZwaveNetworkStatus,
fetchZwaveNodeStatus,
fetchZwaveProvisioningEntries, fetchZwaveProvisioningEntries,
InclusionState, InclusionState,
setZwaveDataCollectionPreference, setZwaveDataCollectionPreference,
@@ -25,7 +24,6 @@ import {
stopZwaveInclusion, stopZwaveInclusion,
ZWaveJSClient, ZWaveJSClient,
ZWaveJSNetwork, ZWaveJSNetwork,
ZWaveJSNodeStatus,
ZwaveJSProvisioningEntry, ZwaveJSProvisioningEntry,
} from "../../../../../data/zwave_js"; } from "../../../../../data/zwave_js";
import { import {
@@ -60,8 +58,6 @@ class ZWaveJSConfigDashboard extends LitElement {
@state() private _network?: ZWaveJSNetwork; @state() private _network?: ZWaveJSNetwork;
@state() private _nodes?: ZWaveJSNodeStatus[];
@state() private _provisioningEntries?: ZwaveJSProvisioningEntry[]; @state() private _provisioningEntries?: ZwaveJSProvisioningEntry[];
@state() private _status?: ZWaveJSClient["state"]; @state() private _status?: ZWaveJSClient["state"];
@@ -84,9 +80,8 @@ class ZWaveJSConfigDashboard extends LitElement {
if (ERROR_STATES.includes(this._configEntry.state)) { if (ERROR_STATES.includes(this._configEntry.state)) {
return this._renderErrorScreen(); return this._renderErrorScreen();
} }
const notReadyDevices = const notReadyDevices =
this._nodes?.filter((node) => !node.ready).length ?? 0; this._network?.controller.nodes.filter((node) => !node.ready).length ?? 0;
return html` return html`
<hass-tabs-subpage <hass-tabs-subpage
@@ -288,7 +283,7 @@ class ZWaveJSConfigDashboard extends LitElement {
data collected, can be found in the data collected, can be found in the
<a <a
target="_blank" target="_blank"
href="https://zwave-js.github.io/node-zwave-js/#/data-collection/data-collection?id=usage-statistics" href="https://zwave-js.github.io/node-zwave-js/#/data-collection/data-collection"
>Z-Wave JS data collection documentation</a >Z-Wave JS data collection documentation</a
>. >.
</p> </p>
@@ -414,18 +409,6 @@ class ZWaveJSConfigDashboard extends LitElement {
this._dataCollectionOptIn = this._dataCollectionOptIn =
dataCollectionStatus.opted_in === true || dataCollectionStatus.opted_in === true ||
dataCollectionStatus.enabled === true; dataCollectionStatus.enabled === true;
this._fetchNodeStatus();
}
private async _fetchNodeStatus() {
if (!this._network) {
return;
}
const nodeStatePromisses = this._network.controller.nodes.map((nodeId) =>
fetchZwaveNodeStatus(this.hass, this.configEntryId!, nodeId)
);
this._nodes = await Promise.all(nodeStatePromisses);
} }
private async _addNodeClicked() { private async _addNodeClicked() {

View File

@@ -61,19 +61,6 @@ const getDevice = memoizeOne(
entries?.find((device) => device.id === deviceId) entries?.find((device) => device.id === deviceId)
); );
const getNodeId = memoizeOne(
(device: DeviceRegistryEntry): number | undefined => {
const identifier = device.identifiers.find(
(ident) => ident[0] === "zwave_js"
);
if (!identifier) {
return undefined;
}
return parseInt(identifier[1].split("-")[1]);
}
);
@customElement("zwave_js-node-config") @customElement("zwave_js-node-config")
class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) { class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -382,12 +369,10 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
} }
private async _updateConfigParameter(target, value) { private async _updateConfigParameter(target, value) {
const nodeId = getNodeId(this._device!);
try { try {
const result = await setZwaveNodeConfigParameter( const result = await setZwaveNodeConfigParameter(
this.hass, this.hass,
this.configEntryId!, this._device!.id,
nodeId!,
target.property, target.property,
value, value,
target.propertyKey ? target.propertyKey : undefined target.propertyKey ? target.propertyKey : undefined
@@ -429,15 +414,9 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
return; return;
} }
const nodeId = getNodeId(device);
if (!nodeId) {
this._error = "device_not_found";
return;
}
[this._nodeMetadata, this._config] = await Promise.all([ [this._nodeMetadata, this._config] = await Promise.all([
fetchZwaveNodeMetadata(this.hass, this.configEntryId, nodeId!), fetchZwaveNodeMetadata(this.hass, device.id),
fetchZwaveNodeConfigParameters(this.hass, this.configEntryId, nodeId!), fetchZwaveNodeConfigParameters(this.hass, device.id),
]); ]);
} }

View File

@@ -16,7 +16,6 @@ import "../../../components/ha-ansi-to-html";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-select"; import "../../../components/ha-select";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
import { fetchErrorLog } from "../../../data/error_log"; import { fetchErrorLog } from "../../../data/error_log";
import { extractApiErrorMessage } from "../../../data/hassio/common"; import { extractApiErrorMessage } from "../../../data/hassio/common";
import { fetchHassioLogs } from "../../../data/hassio/supervisor"; import { fetchHassioLogs } from "../../../data/hassio/supervisor";
@@ -64,11 +63,7 @@ class ErrorLogCard extends LitElement {
: ""} : ""}
${!this._logHTML ${!this._logHTML
? html` ? html`
<mwc-button <mwc-button raised @click=${this._refreshLogs}>
raised
@click=${this._refreshLogs}
dir=${computeRTLDirection(this.hass)}
>
${this.hass.localize("ui.panel.config.logs.load_logs")} ${this.hass.localize("ui.panel.config.logs.load_logs")}
</mwc-button> </mwc-button>
` `
@@ -124,7 +119,7 @@ class ErrorLogCard extends LitElement {
this._logHTML = this.hass.localize("ui.panel.config.logs.loading_log"); this._logHTML = this.hass.localize("ui.panel.config.logs.loading_log");
let log: string; let log: string;
if (isComponentLoaded(this.hass, "hassio")) { if (this.provider !== "core" && isComponentLoaded(this.hass, "hassio")) {
try { try {
log = await fetchHassioLogs(this.hass, this.provider); log = await fetchHassioLogs(this.hass, this.provider);
if (this.filter) { if (this.filter) {
@@ -235,8 +230,8 @@ class ErrorLogCard extends LitElement {
color: var(--warning-color); color: var(--warning-color);
} }
:host-context([style*="direction: rtl;"]) mwc-button { mwc-button {
direction: rtl; direction: var(--direction);
} }
`; `;
} }

View File

@@ -18,7 +18,6 @@ import {
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { showSystemLogDetailDialog } from "./show-dialog-system-log-detail"; import { showSystemLogDetailDialog } from "./show-dialog-system-log-detail";
import { formatSystemLogTime } from "./util"; import { formatSystemLogTime } from "./util";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
@customElement("system-log-card") @customElement("system-log-card")
export class SystemLogCard extends LitElement { export class SystemLogCard extends LitElement {
@@ -86,7 +85,7 @@ export class SystemLogCard extends LitElement {
: html` : html`
${this._items.length === 0 ${this._items.length === 0
? html` ? html`
<div class="card-content"> <div class="card-content empty-content">
${this.hass.localize("ui.panel.config.logs.no_issues")} ${this.hass.localize("ui.panel.config.logs.no_issues")}
</div> </div>
` `
@@ -132,7 +131,7 @@ export class SystemLogCard extends LitElement {
` `
)} )}
<div class="card-actions" dir=${computeRTLDirection(this.hass)}> <div class="card-actions">
<ha-call-service-button <ha-call-service-button
.hass=${this.hass} .hass=${this.hass}
domain="system_log" domain="system_log"
@@ -206,8 +205,9 @@ export class SystemLogCard extends LitElement {
color: var(--warning-color); color: var(--warning-color);
} }
:host-context([style*="direction: rtl;"]) .card-actions { .card-actions,
direction: rtl; .empty-content {
direction: var(--direction);
} }
`; `;
} }

View File

@@ -154,7 +154,16 @@ class ConfigUrlForm extends LitElement {
${!this._showCustomExternalUrl && hasCloud ${!this._showCustomExternalUrl && hasCloud
? html` ? html`
${remoteEnabled ${remoteEnabled
? "" ? html`
<div class="row">
<div class="flex"></div>
<a href="/config/cloud"
>${this.hass.localize(
"ui.panel.config.url.manage_ha_cloud"
)}</a
>
</div>
`
: html` : html`
<ha-alert alert-type="error"> <ha-alert alert-type="error">
${this.hass.localize( ${this.hass.localize(

View File

@@ -14,7 +14,7 @@ class DeveloperToolsRouter extends HassRouterPage {
beforeRender: (page) => { beforeRender: (page) => {
if (!page || page === "not_found") { if (!page || page === "not_found") {
// If we can, we are going to restore the last visited page. // If we can, we are going to restore the last visited page.
return this._currentPage ? this._currentPage : "state"; return this._currentPage ? this._currentPage : "yaml";
} }
return undefined; return undefined;
}, },

View File

@@ -42,6 +42,9 @@ class PanelDeveloperTools extends LitElement {
.selected=${page} .selected=${page}
@iron-activate=${this.handlePageSelected} @iron-activate=${this.handlePageSelected}
> >
<paper-tab page-name="yaml">
${this.hass.localize("ui.panel.developer-tools.tabs.yaml.title")}
</paper-tab>
<paper-tab page-name="state"> <paper-tab page-name="state">
${this.hass.localize( ${this.hass.localize(
"ui.panel.developer-tools.tabs.states.title" "ui.panel.developer-tools.tabs.states.title"
@@ -67,9 +70,6 @@ class PanelDeveloperTools extends LitElement {
"ui.panel.developer-tools.tabs.statistics.title" "ui.panel.developer-tools.tabs.statistics.title"
)} )}
</paper-tab> </paper-tab>
<paper-tab page-name="yaml">
${this.hass.localize("ui.panel.developer-tools.tabs.yaml.title")}
</paper-tab>
</ha-tabs> </ha-tabs>
</app-header> </app-header>
<developer-tools-router <developer-tools-router

View File

@@ -1,4 +1,4 @@
import { addHours } from "date-fns"; import { addHours } from "date-fns/esm";
import "@material/mwc-button"; import "@material/mwc-button";
import { import {
mdiClipboardTextMultipleOutline, mdiClipboardTextMultipleOutline,

View File

@@ -103,6 +103,9 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
}, },
fix: { fix: {
title: "", title: "",
label: this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.fix"
),
template: (_, data: any) => template: (_, data: any) =>
html`${data.issues html`${data.issues
? html`<mwc-button @click=${this._fixIssue} .data=${data.issues}> ? html`<mwc-button @click=${this._fixIssue} .data=${data.issues}>

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