mirror of
https://github.com/home-assistant/frontend.git
synced 2025-04-25 05:47:20 +00:00
20220526.0 (#12807)
* Fix setting _externalAccess (#12584) * Fix enter key support for generic dialog box (#12600) * Revert #10991 (#12618) * Replace host-context with css properties * Remove "Lovelace" from Github issue templates (#12614) * Remove "Lovelace" from Github issue templates * Changes from review * Get full core logs from core (#12639) * Move YAML to first tab of Developer Tools (#12589) * Add configuration panel for Application Credentials (#12344) Co-authored-by: Zack Barett <zackbarett@hey.com> Co-authored-by: Zack <zackbarett@hey.com> * Add label for Fix issue column header in statistics developer tools (#12597) Signed-off-by: Patrick ZAJDA <patrick@zajda.fr> * RTL settings clickable list item fix (#12595) * Update src/state/translations-mixin.ts Co-authored-by: Bram Kragten <mail@bramkragten.nl> * Update following review * Update Translations to create helper (#12656) * Hide Cloud URL - Add Copy Icon (#12655) Co-authored-by: Bram Kragten <mail@bramkragten.nl> * Show script traces in logbook (#12643) * Rtl menu fix (#12561) Co-authored-by: Bram Kragten <mail@bramkragten.nl> * Use / * Update var name * Use FabBase * Update ha-fab.ts * Switch logbook calls to use the new websocket (#12665) * 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 * Fix strict error handling in Markdown card templates (#12661) * Add missing label to search icon (#12671) * Update Lokalise URL (#12684) * Teach logbook about additional context data (#12667) Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io> * Guard for missing backup integration (#12696) * Rtl changes (#12693) * Return focus after dialogs close (#11999) * Bumped version to 20220516.0 * Fix float-end for LTR (#12707) * Add my support for Application Credentials (#12709) * Show manage cloud link to config (#12673) * Add guard logic from PR home-assistant#12181 to input select row (#12703) * Refactor logbook data fetch logic into reusable class (#12701) * Add logbook to device info page (#12714) * Add logbook to area info page (#12715) * Add missing labels in energy dashboard settings (#12722) Signed-off-by: Patrick ZAJDA <patrick@zajda.fr> * Delete focus targets for replaced dialogs (#12724) * Convert history calls to use new websocket endpoint (#12662) * Add support for OAuth2 callback via My (#12718) * 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 * Show the integration brand icon when there is no entity in logbook (#12713) * 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> * Import all date-fns from modules (#12717) * Fix 'loading_log' string (#12712) * Bumped version to 20220518.0 * Fix python to js timestamp conversions in logbook traces (#12677) - The websocket version needs the time converted from where python stores the decimal * Update Material Design Icons to v6.7.96 (#12111) * Various RTL fixes (#12721) * Select + target picker Rtl fixes (#12711) * Add error handling for application credentials removal (#12686) * Update zwave_js data collection URL (#12666) * 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> * Get attributes from first state when using minimal responses (#12732) * Pass device ID to logbook if available (#12728) * Compute the icon based on the logbook state and not the current state (#12725) Co-authored-by: Bram Kragten <mail@bramkragten.nl> * Add option to compare energy graphs with previous period (#12723) Co-authored-by: Paulus Schoutsen <balloob@gmail.com> * Add an application credentials display name (#12720) Co-authored-by: Bram Kragten <mail@bramkragten.nl> Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io> * Fixes logbook (#12740) * Bumped version to 20220521.0 * Stop closed event when selecting datadisk (#12749) * Use logbook livestream when requesting a time window that includes the future (#12744) * Fetch supervisor info directly (#12751) * Remove kernel and agent versions from about page (#12750) * Move `preload_stream` setting to entity settings (#12730) Co-authored-by: Zack <zackbarett@hey.com> * Use new localized context state and source in logbook (#12742) * Use new logbook streaming websocket api for cases where we need end_time (#12753) * Open Application Credentials from integration configuration flow (#12708) * Make entities and devices independent in the scene editor (#11046) Co-authored-by: Zack Barett <zackbarett@hey.com> Co-authored-by: Erik <erik@montnemery.com> Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io> Co-authored-by: Paulus Schoutsen <balloob@gmail.com> * Allow setting `device_class` "outlet" again through entity settings (#12669) * Allow setting `device_class` "outlet" again through UI * Fixes * Null check deviceClass and adjust used translation * Bumped version to 20220523.0 (#12756) * Adjust logbook stream consumer to handle new metadata (#12755) * Adjust path to version info in issue template (#12760) * Add My HA link to about page to Github issue template (#12761) * RTL updates (#12745) * Add compare to energy sources table (#12762) * Fix (#12764) * Change service_data to just data (#12628) * Simplify OAuth2 authorize callback URL (#12765) * Hide hidden media player entities in media panel (#12766) * Add integration filter to Device Selector (#12680) Co-authored-by: Paulus Schoutsen <balloob@gmail.com> * Integration filter for Area Selector (#12682) Co-authored-by: Paulus Schoutsen <balloob@gmail.com> * Bump Version to 20220524.0 (#12769) * Move zwave_js node comments from device config to info page (#12625) Co-authored-by: Zack Barett <zackbarett@hey.com> Co-authored-by: Bram Kragten <mail@bramkragten.nl> * Various RTL fixes * Add controller statistics to zwave_js config dashboard (#12668) * Move Logbook and make device page better (#12763) Co-authored-by: Bram Kragten <mail@bramkragten.nl> * Move metadata to pyproject.toml (#12770) * Add aria-haspopup to button menus (#12758) Co-authored-by: Zack Barett <zackbarett@hey.com> * Hardware MVP (#12773) * Fix "unavailable" handling for climate state rendering (#12778) * Ensure state is vertically centered in more-info (#12780) * Update zwave_js/network_status WS API (#12735) * Bumped version to 20220525.0 (#12779) * Use dynamic weather domain icon + icon alignment fix weather more-info (#12781) * Fix typo in credentials removal dialog (#12784) * Handle history api being passed entity ids as CSV (#12787) * Fix history cache when there is cacheConfig (#12788) * Use Hardware Integration for System Menu (#12789) * Fix Media Player More info cramped controls (#12790) * Fix Switch as X unable to change to a new type (#12797) * Fix Zwave Alerts on device page (#12785) Co-authored-by: Bram Kragten <mail@bramkragten.nl> * Set Error if entity is unavailable (#12791) * Dynamically determine the correct action config struct (#12798) * Remove import * Hide Cloud information a bit more (#12802) * Add dynamic header/footer config determination and update struct (#12795) * Bump HAWS to 7.1.0 (#12804) * Update Narrow Order on Device Page (#12801) Co-authored-by: Bram Kragten <mail@bramkragten.nl> * Fix combo box inside dialog (#12805) * Bumped version to 20220526.0 Co-authored-by: Joakim Sørensen <ludeeus@ludeeus.dev> Co-authored-by: Steve Repsher <steverep@users.noreply.github.com> Co-authored-by: Zack Barett <zackbarett@hey.com> Co-authored-by: Yosi Levy <yosilevy@gmail.com> Co-authored-by: Philip Allgaier <mail@spacegaier.de> Co-authored-by: Allen Porter <allen@thebends.org> Co-authored-by: Patrick ZAJDA <patrick@zajda.fr> Co-authored-by: Yosi Levy <37745463+yosilevy@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: Franck Nijhof <git@frenck.dev> Co-authored-by: Sven <85389871+wrt54g@users.noreply.github.com> Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io> Co-authored-by: Paulus Schoutsen <balloob@gmail.com> Co-authored-by: breakthestatic <breakthestatic@gmail.com> Co-authored-by: RoboMagus <68224306+RoboMagus@users.noreply.github.com> Co-authored-by: Michael Irigoyen <michael@irigoyen.dev> Co-authored-by: Raman Gupta <7243222+raman325@users.noreply.github.com> Co-authored-by: David F. Mulcahey <david.mulcahey@me.com> Co-authored-by: Erik <erik@montnemery.com> Co-authored-by: Thomas Lovén <thomasloven@gmail.com> Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
This commit is contained in:
commit
d810cae194
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -51,7 +51,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
||||
<!--
|
||||
Provide details about the versions you are using, which helps us reproducing
|
||||
and finding the issue quicker. Version information is found in the
|
||||
Home Assistant frontend: Configuration -> Info.
|
||||
Home Assistant frontend: Settings -> About.
|
||||
|
||||
Browser version and operating system is important! Please try to replicate
|
||||
your issue in a different browser and be sure to include your findings.
|
||||
|
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Report a bug with the UI, Frontend or Lovelace
|
||||
name: Report a bug with the UI / Dashboards
|
||||
description: Report an issue related to the Home Assistant frontend.
|
||||
labels: bug
|
||||
body:
|
||||
@ -9,7 +9,7 @@ body:
|
||||
|
||||
If you have a feature or enhancement request for the frontend, please [start an discussion][fr] instead of creating an issue.
|
||||
|
||||
**Please not not report issues for custom Lovelace cards.**
|
||||
**Please not not report issues for custom cards.**
|
||||
|
||||
[fr]: https://github.com/home-assistant/frontend/discussions
|
||||
[releases]: https://github.com/home-assistant/home-assistant/releases
|
||||
@ -64,7 +64,7 @@ body:
|
||||
label: What version of Home Assistant Core has the issue?
|
||||
placeholder: core-
|
||||
description: >
|
||||
Can be found in the Configuration panel -> Info.
|
||||
Can be found in: [Settings -> About](https://my.home-assistant.io/redirect/info/).
|
||||
- type: input
|
||||
attributes:
|
||||
label: What was the last working version of Home Assistant Core?
|
||||
|
8
.github/ISSUE_TEMPLATE/config.yml
vendored
8
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,17 +1,17 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Request a feature for the UI, Frontend or Lovelace
|
||||
- name: Request a feature for the UI / Dashboards
|
||||
url: https://github.com/home-assistant/frontend/discussions/category_choices
|
||||
about: Request an new feature for the Home Assistant frontend.
|
||||
- name: Report a bug that is NOT related to the UI, Frontend or Lovelace
|
||||
- name: Report a bug that is NOT related to the UI / Dashboards
|
||||
url: https://github.com/home-assistant/core/issues
|
||||
about: This is the issue tracker for our frontend. Please report other issues with the backend repository.
|
||||
about: This is the issue tracker for our frontend. Please report other issues in the backend ("core") repository.
|
||||
- name: Report incorrect or missing information on our website
|
||||
url: https://github.com/home-assistant/home-assistant.io/issues
|
||||
about: Our documentation has its own issue tracker. Please report issues with the website there.
|
||||
- name: I have a question or need support
|
||||
url: https://www.home-assistant.io/help
|
||||
about: We use GitHub for tracking bugs, check our website for resources on getting help.
|
||||
about: We use GitHub for tracking bugs. Check our website for resources on getting help.
|
||||
- name: I'm unsure where to go
|
||||
url: https://www.home-assistant.io/join-chat
|
||||
about: If you are unsure where to go, then joining our chat is recommended; Just ask!
|
||||
|
@ -26,8 +26,8 @@ module.exports = {
|
||||
},
|
||||
version() {
|
||||
const version = fs
|
||||
.readFileSync(path.resolve(paths.polymer_dir, "setup.cfg"), "utf8")
|
||||
.match(/version\W+=\W(\d{8}\.\d)/);
|
||||
.readFileSync(path.resolve(paths.polymer_dir, "pyproject.toml"), "utf8")
|
||||
.match(/version\W+=\W"(\d{8}\.\d)"/);
|
||||
if (!version) {
|
||||
throw Error("Version not found");
|
||||
}
|
||||
|
@ -194,7 +194,7 @@ export const demoLovelaceJimpower: DemoConfig["lovelace"] = () => ({
|
||||
type: "state-icon",
|
||||
tap_action: {
|
||||
action: "call-service",
|
||||
service_data: {
|
||||
data: {
|
||||
entity_id: "group.downstairs_lights",
|
||||
},
|
||||
service: "homeassistant.toggle",
|
||||
|
@ -377,7 +377,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
name: "AC bed",
|
||||
tap_action: {
|
||||
action: "call-service",
|
||||
service_data: {
|
||||
data: {
|
||||
entity_id: "script.air_cleaner_quiet",
|
||||
},
|
||||
service: "script.turn_on",
|
||||
@ -390,7 +390,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
name: "AC bed",
|
||||
tap_action: {
|
||||
action: "call-service",
|
||||
service_data: {
|
||||
data: {
|
||||
entity_id: "script.air_cleaner_auto",
|
||||
},
|
||||
service: "script.turn_on",
|
||||
@ -403,7 +403,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
name: "AC bed",
|
||||
tap_action: {
|
||||
action: "call-service",
|
||||
service_data: {
|
||||
data: {
|
||||
entity_id: "script.air_cleaner_turbo",
|
||||
},
|
||||
service: "script.turn_on",
|
||||
@ -416,7 +416,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
name: "AC",
|
||||
tap_action: {
|
||||
action: "call-service",
|
||||
service_data: {
|
||||
data: {
|
||||
entity_id: "script.ac_off",
|
||||
},
|
||||
service: "script.turn_on",
|
||||
@ -429,7 +429,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
name: "AC",
|
||||
tap_action: {
|
||||
action: "call-service",
|
||||
service_data: {
|
||||
data: {
|
||||
entity_id: "script.ac_on",
|
||||
},
|
||||
service: "script.turn_on",
|
||||
@ -629,7 +629,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
entity: "scene.morning_lights",
|
||||
tap_action: {
|
||||
action: "call-service",
|
||||
service_data: {
|
||||
data: {
|
||||
entity_id: "scene.morning_lights",
|
||||
},
|
||||
service: "scene.turn_on",
|
||||
@ -641,7 +641,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
entity: "scene.movie_time",
|
||||
tap_action: {
|
||||
action: "call-service",
|
||||
service_data: {
|
||||
data: {
|
||||
entity_id: "scene.movie_time",
|
||||
},
|
||||
service: "scene.turn_on",
|
||||
@ -702,7 +702,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
entity: "light.downstairs_lights",
|
||||
tap_action: {
|
||||
action: "call-service",
|
||||
service_data: {
|
||||
data: {
|
||||
entity_id: "light.downstairs_lights",
|
||||
},
|
||||
service: "light.toggle",
|
||||
@ -714,7 +714,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
entity: "light.upstairs_lights",
|
||||
tap_action: {
|
||||
action: "call-service",
|
||||
service_data: {
|
||||
data: {
|
||||
entity_id: "light.upstairs_lights",
|
||||
},
|
||||
service: "light.toggle",
|
||||
|
@ -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 { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
addMonths,
|
||||
differenceInHours,
|
||||
endOfDay,
|
||||
} from "date-fns";
|
||||
} from "date-fns/esm";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { StatisticValue } from "../../../src/data/history";
|
||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
@ -119,7 +119,7 @@ export const basicTrace: DemoTrace = {
|
||||
params: {
|
||||
domain: "input_boolean",
|
||||
service: "toggle",
|
||||
service_data: {},
|
||||
data: {},
|
||||
target: {
|
||||
entity_id: ["input_boolean.toggle_4"],
|
||||
},
|
||||
@ -164,7 +164,7 @@ export const basicTrace: DemoTrace = {
|
||||
params: {
|
||||
domain: "input_boolean",
|
||||
service: "toggle",
|
||||
service_data: {},
|
||||
data: {},
|
||||
target: {
|
||||
entity_id: ["input_boolean.toggle_2"],
|
||||
},
|
||||
@ -182,7 +182,7 @@ export const basicTrace: DemoTrace = {
|
||||
params: {
|
||||
domain: "input_boolean",
|
||||
service: "toggle",
|
||||
service_data: {},
|
||||
data: {},
|
||||
target: {
|
||||
entity_id: ["input_boolean.toggle_3"],
|
||||
},
|
||||
@ -200,7 +200,7 @@ export const basicTrace: DemoTrace = {
|
||||
params: {
|
||||
domain: "input_boolean",
|
||||
service: "toggle",
|
||||
service_data: {},
|
||||
data: {},
|
||||
target: {
|
||||
entity_id: ["input_boolean.toggle_4"],
|
||||
},
|
||||
@ -298,11 +298,11 @@ export const basicTrace: DemoTrace = {
|
||||
source: "state of input_boolean.toggle_1",
|
||||
entity_id: "automation.toggle_toggles",
|
||||
context_id: "6cfcae368e7b3686fad6c59e83ae76c9",
|
||||
when: "2021-03-25T04:36:51.240832+00:00",
|
||||
when: 1616647011.240832,
|
||||
domain: "automation",
|
||||
},
|
||||
{
|
||||
when: "2021-03-25T04:36:51.249828+00:00",
|
||||
when: 1616647011.249828,
|
||||
name: "Toggle 4",
|
||||
state: "on",
|
||||
entity_id: "input_boolean.toggle_4",
|
||||
@ -313,7 +313,7 @@ export const basicTrace: DemoTrace = {
|
||||
context_name: "Ensure Party mode",
|
||||
},
|
||||
{
|
||||
when: "2021-03-25T04:36:51.258947+00:00",
|
||||
when: 1616647011.258947,
|
||||
name: "Toggle 2",
|
||||
state: "on",
|
||||
entity_id: "input_boolean.toggle_2",
|
||||
@ -324,7 +324,7 @@ export const basicTrace: DemoTrace = {
|
||||
context_name: "Ensure Party mode",
|
||||
},
|
||||
{
|
||||
when: "2021-03-25T04:36:51.261806+00:00",
|
||||
when: 1616647011.261806,
|
||||
name: "Toggle 3",
|
||||
state: "off",
|
||||
entity_id: "input_boolean.toggle_3",
|
||||
@ -335,7 +335,7 @@ export const basicTrace: DemoTrace = {
|
||||
context_name: "Ensure Party mode",
|
||||
},
|
||||
{
|
||||
when: "2021-03-25T04:36:51.265246+00:00",
|
||||
when: 1616647011.265246,
|
||||
name: "Toggle 4",
|
||||
state: "off",
|
||||
entity_id: "input_boolean.toggle_4",
|
||||
|
@ -185,11 +185,11 @@ export const motionLightTrace: DemoTrace = {
|
||||
"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",
|
||||
entity_id: "automation.auto_elgato",
|
||||
when: "2021-03-14T06:07:01.768492+00:00",
|
||||
when: 1615702021.768492,
|
||||
domain: "automation",
|
||||
},
|
||||
{
|
||||
when: "2021-03-14T06:07:01.872187+00:00",
|
||||
when: 1615702021.872187,
|
||||
name: "Elgato Key Light Air",
|
||||
state: "on",
|
||||
entity_id: "light.elgato_key_light_air",
|
||||
@ -200,7 +200,7 @@ export const motionLightTrace: DemoTrace = {
|
||||
context_name: "Auto Elgato",
|
||||
},
|
||||
{
|
||||
when: "2021-03-14T06:07:53.284505+00:00",
|
||||
when: 1615702073.284505,
|
||||
name: "Elgato Key Light Air",
|
||||
state: "off",
|
||||
entity_id: "light.elgato_key_light_air",
|
||||
|
@ -249,7 +249,7 @@ const CONFIGS = [
|
||||
name: Bed light
|
||||
action_name: Toggle light
|
||||
service: light.toggle
|
||||
service_data:
|
||||
data:
|
||||
entity_id: light.bed_light
|
||||
- type: section
|
||||
label: Links
|
||||
|
@ -199,7 +199,7 @@ const CONFIGS = [
|
||||
tap_action:
|
||||
action: call-service
|
||||
service: light.turn_on
|
||||
service_data:
|
||||
data:
|
||||
entity_id: light.ceiling_lights
|
||||
- entity: sun.sun
|
||||
name: Regular
|
||||
|
@ -40,7 +40,7 @@ const CONFIGS = [
|
||||
left: 90%
|
||||
padding: 0px
|
||||
service: light.turn_off
|
||||
service_data:
|
||||
data:
|
||||
entity_id: group.all_lights
|
||||
- type: icon
|
||||
icon: mdi:cctv
|
||||
@ -88,7 +88,7 @@ const CONFIGS = [
|
||||
left: 90%
|
||||
padding: 0px
|
||||
service: light.turn_off
|
||||
service_data:
|
||||
data:
|
||||
entity_id: group.all_lights
|
||||
- type: icon
|
||||
icon: mdi:cctv
|
||||
|
@ -17,7 +17,10 @@ import {
|
||||
HassioAddonDetails,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import { setSupervisorOption } from "../../../src/data/hassio/supervisor";
|
||||
import {
|
||||
fetchHassioSupervisorInfo,
|
||||
setSupervisorOption,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import "../../../src/layouts/hass-error-screen";
|
||||
@ -169,38 +172,40 @@ class HassioAddonDashboard extends LitElement {
|
||||
if (this.route.path === "") {
|
||||
const requestedAddon = extractSearchParam("addon");
|
||||
const requestedAddonRepository = extractSearchParam("repository_url");
|
||||
if (
|
||||
requestedAddonRepository &&
|
||||
!this.supervisor.supervisor.addons_repositories.find(
|
||||
(repo) => repo === requestedAddonRepository
|
||||
)
|
||||
) {
|
||||
if (requestedAddonRepository) {
|
||||
const supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize("my.add_addon_repository_title"),
|
||||
text: this.supervisor.localize(
|
||||
"my.add_addon_repository_description",
|
||||
{ addon: requestedAddon, repository: requestedAddonRepository }
|
||||
),
|
||||
confirmText: this.supervisor.localize("common.add"),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
}))
|
||||
!supervisorInfo.addons_repositories.find(
|
||||
(repo) => repo === requestedAddonRepository
|
||||
)
|
||||
) {
|
||||
this._error = this.supervisor.localize(
|
||||
"my.error_repository_not_found"
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize("my.add_addon_repository_title"),
|
||||
text: this.supervisor.localize(
|
||||
"my.add_addon_repository_description",
|
||||
{ addon: requestedAddon, repository: requestedAddonRepository }
|
||||
),
|
||||
confirmText: this.supervisor.localize("common.add"),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
}))
|
||||
) {
|
||||
this._error = this.supervisor.localize(
|
||||
"my.error_repository_not_found"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await setSupervisorOption(this.hass, {
|
||||
addons_repositories: [
|
||||
...this.supervisor.supervisor.addons_repositories,
|
||||
requestedAddonRepository,
|
||||
],
|
||||
});
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
try {
|
||||
await setSupervisorOption(this.hass, {
|
||||
addons_repositories: [
|
||||
...supervisorInfo.addons_repositories,
|
||||
requestedAddonRepository,
|
||||
],
|
||||
});
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
10
package.json
10
package.json
@ -72,8 +72,8 @@
|
||||
"@material/mwc-textfield": "0.25.3",
|
||||
"@material/mwc-top-app-bar-fixed": "^0.25.3",
|
||||
"@material/top-app-bar": "14.0.0-canary.261f2db59.0",
|
||||
"@mdi/js": "6.6.95",
|
||||
"@mdi/svg": "6.6.95",
|
||||
"@mdi/js": "6.7.96",
|
||||
"@mdi/svg": "6.7.96",
|
||||
"@polymer/app-layout": "^3.1.0",
|
||||
"@polymer/iron-flex-layout": "^3.0.1",
|
||||
"@polymer/iron-icon": "^3.0.1",
|
||||
@ -89,8 +89,8 @@
|
||||
"@polymer/paper-tooltip": "^3.0.1",
|
||||
"@polymer/polymer": "3.4.1",
|
||||
"@thomasloven/round-slider": "0.5.4",
|
||||
"@vaadin/combo-box": "^22.0.4",
|
||||
"@vaadin/vaadin-themable-mixin": "^22.0.4",
|
||||
"@vaadin/combo-box": "^23.0.10",
|
||||
"@vaadin/vaadin-themable-mixin": "^23.0.10",
|
||||
"@vibrant/color": "^3.2.1-alpha.1",
|
||||
"@vibrant/core": "^3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
|
||||
@ -108,7 +108,7 @@
|
||||
"fuse.js": "^6.0.0",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hls.js": "^1.1.5",
|
||||
"home-assistant-js-websocket": "^7.0.3",
|
||||
"home-assistant-js-websocket": "^7.1.0",
|
||||
"idb-keyval": "^5.1.3",
|
||||
"intl-messageformat": "^9.9.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
|
@ -1,3 +1,30 @@
|
||||
[build-system]
|
||||
requires = ["setuptools~=60.5", "wheel~=0.37.1"]
|
||||
requires = ["setuptools~=62.3", "wheel~=0.37.1"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20220526.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
{name = "The Home Assistant Authors", email = "hello@home-assistant.io"}
|
||||
]
|
||||
requires-python = ">=3.4.0"
|
||||
|
||||
[project.urls]
|
||||
"Homepage" = "https://github.com/home-assistant/frontend"
|
||||
|
||||
[tool.setuptools]
|
||||
platforms = ["any"]
|
||||
zip-safe = false
|
||||
include-package-data = true
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["hass_frontend*"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = 3.4
|
||||
show_error_codes = true
|
||||
strict = true
|
||||
|
@ -50,14 +50,14 @@ async function main(args) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setup = fs.readFileSync("setup.cfg", "utf8");
|
||||
const version = setup.match(/\d{8}\.\d+/)[0];
|
||||
const setup = fs.readFileSync("pyproject.toml", "utf8");
|
||||
const version = setup.match(/version\W+=\W"(\d{8}\.\d)"/)[1];
|
||||
const newVersion = method(version);
|
||||
|
||||
console.log("Current version:", version);
|
||||
console.log("New version:", newVersion);
|
||||
|
||||
fs.writeFileSync("setup.cfg", setup.replace(version, newVersion), "utf-8");
|
||||
fs.writeFileSync("pyproject.toml", setup.replace(version, newVersion), "utf-8");
|
||||
|
||||
if (!commit) {
|
||||
return;
|
||||
|
26
setup.cfg
26
setup.cfg
@ -1,26 +0,0 @@
|
||||
[metadata]
|
||||
name = home-assistant-frontend
|
||||
version = 20220504.1
|
||||
author = The Home Assistant Authors
|
||||
author_email = hello@home-assistant.io
|
||||
license = Apache-2.0
|
||||
platforms = any
|
||||
description = The Home Assistant frontend
|
||||
long_description = file: README.md
|
||||
long_description_content_type = text/markdown
|
||||
url = https://github.com/home-assistant/frontend
|
||||
|
||||
[options]
|
||||
packages = find:
|
||||
zip_safe = False
|
||||
include_package_data = True
|
||||
python_requires = >= 3.4.0
|
||||
|
||||
[options.packages.find]
|
||||
include =
|
||||
hass_frontend*
|
||||
|
||||
[mypy]
|
||||
python_version = 3.4
|
||||
show_error_codes = True
|
||||
strict = True
|
41
src/common/dom/ancestors-with-property.ts
Normal file
41
src/common/dom/ancestors-with-property.ts
Normal 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;
|
||||
};
|
@ -1,6 +1,11 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { UNAVAILABLE_STATES } from "../../data/entity";
|
||||
|
||||
export const computeActiveState = (stateObj: HassEntity): string => {
|
||||
if (UNAVAILABLE_STATES.includes(stateObj.state)) {
|
||||
return stateObj.state;
|
||||
}
|
||||
|
||||
const domain = stateObj.entity_id.split(".")[0];
|
||||
let state = stateObj.state;
|
||||
|
||||
|
@ -2,67 +2,74 @@ import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import {
|
||||
updateIsInstalling,
|
||||
UpdateEntity,
|
||||
UPDATE_SUPPORT_PROGRESS,
|
||||
updateIsInstallingFromAttributes,
|
||||
} from "../../data/update";
|
||||
import { formatDate } from "../datetime/format_date";
|
||||
import { formatDateTime } from "../datetime/format_date_time";
|
||||
import { formatTime } from "../datetime/format_time";
|
||||
import { formatNumber, isNumericState } from "../number/format_number";
|
||||
import { formatNumber, isNumericFromAttributes } from "../number/format_number";
|
||||
import { LocalizeFunc } from "../translations/localize";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
import { supportsFeature } from "./supports-feature";
|
||||
import { supportsFeatureFromAttributes } from "./supports-feature";
|
||||
import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
|
||||
export const computeStateDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity,
|
||||
locale: FrontendLocaleData,
|
||||
state?: string
|
||||
): string => {
|
||||
const compareState = state !== undefined ? state : stateObj.state;
|
||||
): string =>
|
||||
computeStateDisplayFromEntityAttributes(
|
||||
localize,
|
||||
locale,
|
||||
stateObj.entity_id,
|
||||
stateObj.attributes,
|
||||
state !== undefined ? state : stateObj.state
|
||||
);
|
||||
|
||||
if (compareState === UNKNOWN || compareState === UNAVAILABLE) {
|
||||
return localize(`state.default.${compareState}`);
|
||||
export const computeStateDisplayFromEntityAttributes = (
|
||||
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`
|
||||
if (isNumericState(stateObj)) {
|
||||
if (isNumericFromAttributes(attributes)) {
|
||||
// state is duration
|
||||
if (
|
||||
stateObj.attributes.device_class === "duration" &&
|
||||
stateObj.attributes.unit_of_measurement &&
|
||||
UNIT_TO_SECOND_CONVERT[stateObj.attributes.unit_of_measurement]
|
||||
attributes.device_class === "duration" &&
|
||||
attributes.unit_of_measurement &&
|
||||
UNIT_TO_SECOND_CONVERT[attributes.unit_of_measurement]
|
||||
) {
|
||||
try {
|
||||
return formatDuration(
|
||||
compareState,
|
||||
stateObj.attributes.unit_of_measurement
|
||||
);
|
||||
return formatDuration(state, attributes.unit_of_measurement);
|
||||
} catch (_err) {
|
||||
// fallback to default
|
||||
}
|
||||
}
|
||||
if (stateObj.attributes.device_class === "monetary") {
|
||||
if (attributes.device_class === "monetary") {
|
||||
try {
|
||||
return formatNumber(compareState, locale, {
|
||||
return formatNumber(state, locale, {
|
||||
style: "currency",
|
||||
currency: stateObj.attributes.unit_of_measurement,
|
||||
currency: attributes.unit_of_measurement,
|
||||
minimumFractionDigits: 2,
|
||||
});
|
||||
} catch (_err) {
|
||||
// fallback to default
|
||||
}
|
||||
}
|
||||
return `${formatNumber(compareState, locale)}${
|
||||
stateObj.attributes.unit_of_measurement
|
||||
? " " + stateObj.attributes.unit_of_measurement
|
||||
: ""
|
||||
return `${formatNumber(state, locale)}${
|
||||
attributes.unit_of_measurement ? " " + attributes.unit_of_measurement : ""
|
||||
}`;
|
||||
}
|
||||
|
||||
const domain = computeStateDomain(stateObj);
|
||||
const domain = computeDomain(entityId);
|
||||
|
||||
if (domain === "input_datetime") {
|
||||
if (state !== undefined) {
|
||||
@ -97,36 +104,32 @@ export const computeStateDisplay = (
|
||||
} else {
|
||||
// If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format.
|
||||
let date: Date;
|
||||
if (stateObj.attributes.has_date && stateObj.attributes.has_time) {
|
||||
if (attributes.has_date && attributes.has_time) {
|
||||
date = new Date(
|
||||
stateObj.attributes.year,
|
||||
stateObj.attributes.month - 1,
|
||||
stateObj.attributes.day,
|
||||
stateObj.attributes.hour,
|
||||
stateObj.attributes.minute
|
||||
attributes.year,
|
||||
attributes.month - 1,
|
||||
attributes.day,
|
||||
attributes.hour,
|
||||
attributes.minute
|
||||
);
|
||||
return formatDateTime(date, locale);
|
||||
}
|
||||
if (stateObj.attributes.has_date) {
|
||||
date = new Date(
|
||||
stateObj.attributes.year,
|
||||
stateObj.attributes.month - 1,
|
||||
stateObj.attributes.day
|
||||
);
|
||||
if (attributes.has_date) {
|
||||
date = new Date(attributes.year, attributes.month - 1, attributes.day);
|
||||
return formatDate(date, locale);
|
||||
}
|
||||
if (stateObj.attributes.has_time) {
|
||||
if (attributes.has_time) {
|
||||
date = new Date();
|
||||
date.setHours(stateObj.attributes.hour, stateObj.attributes.minute);
|
||||
date.setHours(attributes.hour, attributes.minute);
|
||||
return formatTime(date, locale);
|
||||
}
|
||||
return stateObj.state;
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
if (domain === "humidifier") {
|
||||
if (compareState === "on" && stateObj.attributes.humidity) {
|
||||
return `${stateObj.attributes.humidity} %`;
|
||||
if (state === "on" && attributes.humidity) {
|
||||
return `${attributes.humidity} %`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,7 +139,7 @@ export const computeStateDisplay = (
|
||||
domain === "number" ||
|
||||
domain === "input_number"
|
||||
) {
|
||||
return formatNumber(compareState, locale);
|
||||
return formatNumber(state, locale);
|
||||
}
|
||||
|
||||
// state of button is a timestamp
|
||||
@ -144,12 +147,12 @@ export const computeStateDisplay = (
|
||||
domain === "button" ||
|
||||
domain === "input_button" ||
|
||||
domain === "scene" ||
|
||||
(domain === "sensor" && stateObj.attributes.device_class === "timestamp")
|
||||
(domain === "sensor" && attributes.device_class === "timestamp")
|
||||
) {
|
||||
try {
|
||||
return formatDateTime(new Date(compareState), locale);
|
||||
return formatDateTime(new Date(state), locale);
|
||||
} catch (_err) {
|
||||
return compareState;
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
@ -160,30 +163,28 @@ export const computeStateDisplay = (
|
||||
// When the latest version is skipped, show the latest version
|
||||
// When update is not available, show "Up-to-date"
|
||||
// When update is not available and there is no latest_version show "Unavailable"
|
||||
return compareState === "on"
|
||||
? updateIsInstalling(stateObj as UpdateEntity)
|
||||
? supportsFeature(stateObj, UPDATE_SUPPORT_PROGRESS)
|
||||
return state === "on"
|
||||
? updateIsInstallingFromAttributes(attributes)
|
||||
? supportsFeatureFromAttributes(attributes, UPDATE_SUPPORT_PROGRESS)
|
||||
? localize("ui.card.update.installing_with_progress", {
|
||||
progress: stateObj.attributes.in_progress,
|
||||
progress: attributes.in_progress,
|
||||
})
|
||||
: localize("ui.card.update.installing")
|
||||
: stateObj.attributes.latest_version
|
||||
: stateObj.attributes.skipped_version ===
|
||||
stateObj.attributes.latest_version
|
||||
? stateObj.attributes.latest_version ??
|
||||
localize("state.default.unavailable")
|
||||
: attributes.latest_version
|
||||
: attributes.skipped_version === attributes.latest_version
|
||||
? attributes.latest_version ?? localize("state.default.unavailable")
|
||||
: localize("ui.card.update.up_to_date");
|
||||
}
|
||||
|
||||
return (
|
||||
// Return device class translation
|
||||
(stateObj.attributes.device_class &&
|
||||
(attributes.device_class &&
|
||||
localize(
|
||||
`component.${domain}.state.${stateObj.attributes.device_class}.${compareState}`
|
||||
`component.${domain}.state.${attributes.device_class}.${state}`
|
||||
)) ||
|
||||
// Return default translation
|
||||
localize(`component.${domain}.state._.${compareState}`) ||
|
||||
localize(`component.${domain}.state._.${state}`) ||
|
||||
// We don't know! Return the raw state.
|
||||
compareState
|
||||
state
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,13 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
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 =>
|
||||
stateObj.attributes.friendly_name === undefined
|
||||
? computeObjectId(stateObj.entity_id).replace(/_/g, " ")
|
||||
: stateObj.attributes.friendly_name || "";
|
||||
computeStateNameFromEntityAttributes(stateObj.entity_id, stateObj.attributes);
|
||||
|
@ -29,7 +29,8 @@ import {
|
||||
mdiWeatherNight,
|
||||
} from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { updateIsInstalling, UpdateEntity } from "../../data/update";
|
||||
import { UpdateEntity, updateIsInstalling } from "../../data/update";
|
||||
import { weatherIcon } from "../../data/weather";
|
||||
/**
|
||||
* Return the icon to be used for a domain.
|
||||
*
|
||||
@ -46,6 +47,20 @@ export const domainIcon = (
|
||||
stateObj?: HassEntity,
|
||||
state?: 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;
|
||||
|
||||
switch (domain) {
|
||||
@ -87,6 +102,15 @@ export const domainIcon = (
|
||||
? mdiCheckCircleOutline
|
||||
: mdiCloseCircleOutline;
|
||||
|
||||
case "input_datetime":
|
||||
if (!stateObj?.attributes.has_date) {
|
||||
return mdiClock;
|
||||
}
|
||||
if (!stateObj.attributes.has_time) {
|
||||
return mdiCalendar;
|
||||
}
|
||||
break;
|
||||
|
||||
case "lock":
|
||||
switch (compareState) {
|
||||
case "unlocked":
|
||||
@ -124,15 +148,6 @@ export const domainIcon = (
|
||||
break;
|
||||
}
|
||||
|
||||
case "input_datetime":
|
||||
if (!stateObj?.attributes.has_date) {
|
||||
return mdiClock;
|
||||
}
|
||||
if (!stateObj.attributes.has_time) {
|
||||
return mdiCalendar;
|
||||
}
|
||||
break;
|
||||
|
||||
case "sun":
|
||||
return stateObj?.state === "above_horizon"
|
||||
? FIXED_DOMAIN_ICONS[domain]
|
||||
@ -144,13 +159,14 @@ export const domainIcon = (
|
||||
? mdiPackageDown
|
||||
: mdiPackageUp
|
||||
: mdiPackage;
|
||||
|
||||
case "weather":
|
||||
return weatherIcon(stateObj?.state);
|
||||
}
|
||||
|
||||
if (domain in FIXED_DOMAIN_ICONS) {
|
||||
return FIXED_DOMAIN_ICONS[domain];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
console.warn(`Unable to find icon for domain ${domain}`);
|
||||
return DEFAULT_DOMAIN_ICON;
|
||||
return undefined;
|
||||
};
|
||||
|
@ -3,6 +3,13 @@ import { HassEntity } from "home-assistant-js-websocket";
|
||||
export const supportsFeature = (
|
||||
stateObj: HassEntity,
|
||||
feature: number
|
||||
): boolean => supportsFeatureFromAttributes(stateObj.attributes, feature);
|
||||
|
||||
export const supportsFeatureFromAttributes = (
|
||||
attributes: {
|
||||
[key: string]: any;
|
||||
},
|
||||
feature: number
|
||||
): boolean =>
|
||||
// eslint-disable-next-line no-bitwise
|
||||
(stateObj.attributes.supported_features! & feature) !== 0;
|
||||
(attributes.supported_features! & feature) !== 0;
|
||||
|
@ -7,8 +7,11 @@ import { round } from "./round";
|
||||
* @param stateObj The entity state object
|
||||
*/
|
||||
export const isNumericState = (stateObj: HassEntity): boolean =>
|
||||
!!stateObj.attributes.unit_of_measurement ||
|
||||
!!stateObj.attributes.state_class;
|
||||
isNumericFromAttributes(stateObj.attributes);
|
||||
|
||||
export const isNumericFromAttributes = (attributes: {
|
||||
[key: string]: any;
|
||||
}): boolean => !!attributes.unit_of_measurement || !!attributes.state_class;
|
||||
|
||||
export const numberFormatToLocale = (
|
||||
localeOptions: FrontendLocaleData
|
||||
|
@ -13,7 +13,7 @@ export const throttle = <T extends any[]>(
|
||||
) => {
|
||||
let timeout: number | undefined;
|
||||
let previous = 0;
|
||||
return (...args: T): void => {
|
||||
const throttledFunc = (...args: T): void => {
|
||||
const later = () => {
|
||||
previous = leading === false ? 0 : Date.now();
|
||||
timeout = undefined;
|
||||
@ -35,4 +35,10 @@ export const throttle = <T extends any[]>(
|
||||
timeout = window.setTimeout(later, remaining);
|
||||
}
|
||||
};
|
||||
throttledFunc.cancel = () => {
|
||||
clearTimeout(timeout);
|
||||
timeout = undefined;
|
||||
previous = 0;
|
||||
};
|
||||
return throttledFunc;
|
||||
};
|
||||
|
@ -34,7 +34,7 @@ import {
|
||||
endOfMonth,
|
||||
endOfQuarter,
|
||||
endOfYear,
|
||||
} from "date-fns";
|
||||
} from "date-fns/esm";
|
||||
import {
|
||||
formatDate,
|
||||
formatDateMonth,
|
||||
|
@ -269,8 +269,8 @@ export class HaDataTable extends LitElement {
|
||||
@change=${this._handleHeaderRowCheckboxClick}
|
||||
.indeterminate=${this._checkedRows.length &&
|
||||
this._checkedRows.length !== this._checkableRowsCount}
|
||||
.checked=${this._checkedRows.length ===
|
||||
this._checkableRowsCount}
|
||||
.checked=${this._checkedRows.length &&
|
||||
this._checkedRows.length === this._checkableRowsCount}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</div>
|
||||
|
@ -20,7 +20,7 @@ interface HassEntityWithCachedName extends HassEntity {
|
||||
friendly_name: string;
|
||||
}
|
||||
|
||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
||||
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
|
||||
|
||||
// eslint-disable-next-line lit/prefer-static-styles
|
||||
const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) =>
|
||||
@ -31,6 +31,7 @@ const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) =>
|
||||
<span>${item.friendly_name}</span>
|
||||
<span slot="secondary">${item.entity_id}</span>
|
||||
</mwc-list-item>`;
|
||||
|
||||
@customElement("ha-entity-picker")
|
||||
export class HaEntityPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
@ -1,17 +1,22 @@
|
||||
import type { Button } from "@material/mwc-button";
|
||||
import "@material/mwc-menu";
|
||||
import type { Corner, Menu, MenuCorner } from "@material/mwc-menu";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
|
||||
import type { HaIconButton } from "./ha-icon-button";
|
||||
|
||||
@customElement("ha-button-menu")
|
||||
export class HaButtonMenu extends LitElement {
|
||||
protected readonly [FOCUS_TARGET];
|
||||
|
||||
@property() public corner: Corner = "TOP_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;
|
||||
|
||||
@ -31,10 +36,18 @@ export class HaButtonMenu extends LitElement {
|
||||
return this._menu?.selected;
|
||||
}
|
||||
|
||||
public override focus() {
|
||||
if (this._menu?.open) {
|
||||
this._menu.focusItemAtIndex(0);
|
||||
} else {
|
||||
this._triggerButton?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div @click=${this._handleClick}>
|
||||
<slot name="trigger"></slot>
|
||||
<slot name="trigger" @slotchange=${this._setTriggerAria}></slot>
|
||||
</div>
|
||||
<mwc-menu
|
||||
.corner=${this.corner}
|
||||
@ -50,6 +63,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 {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
@ -58,6 +86,18 @@ export class HaButtonMenu extends LitElement {
|
||||
this._menu!.show();
|
||||
}
|
||||
|
||||
private get _triggerButton() {
|
||||
return this.querySelector(
|
||||
'ha-icon-button[slot="trigger"], mwc-button[slot="trigger"]'
|
||||
) as HaIconButton | Button | null;
|
||||
}
|
||||
|
||||
private _setTriggerAria() {
|
||||
if (this._triggerButton) {
|
||||
this._triggerButton.ariaHasPopup = "menu";
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
|
@ -66,9 +66,13 @@ export class HaChip extends LitElement {
|
||||
line-height: 14px;
|
||||
color: var(--ha-chip-icon-color, var(--ha-chip-text-color));
|
||||
}
|
||||
.mdc-chip.mdc-chip--selected .mdc-chip__checkmark,
|
||||
.mdc-chip.no-text
|
||||
.mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) {
|
||||
margin-right: -4px;
|
||||
margin-inline-start: -4px;
|
||||
margin-inline-end: 4px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
span[role="gridcell"] {
|
||||
|
@ -47,10 +47,6 @@ export class HaClickableListItem extends ListItemBase {
|
||||
padding-left: 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="icon"]:not([twoLine])) {
|
||||
height: 48px;
|
||||
@ -64,6 +60,19 @@ export class HaClickableListItem extends ListItemBase {
|
||||
padding-right: var(--mdc-list-side-padding, 20px);
|
||||
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);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
import { CLIMATE_PRESET_NONE } from "../data/climate";
|
||||
import { UNAVAILABLE_STATES } from "../data/entity";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-climate-state")
|
||||
@ -15,22 +16,22 @@ class HaClimateState extends LitElement {
|
||||
const currentStatus = this._computeCurrentStatus();
|
||||
|
||||
return html`<div class="target">
|
||||
${this.stateObj.state !== "unknown"
|
||||
${!UNAVAILABLE_STATES.includes(this.stateObj.state)
|
||||
? html`<span class="state-label">
|
||||
${this._localizeState()}
|
||||
${this.stateObj.attributes.preset_mode &&
|
||||
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
|
||||
? html`-
|
||||
${this.hass.localize(
|
||||
`state_attributes.climate.preset_mode.${this.stateObj.attributes.preset_mode}`
|
||||
) || this.stateObj.attributes.preset_mode}`
|
||||
: ""}
|
||||
</span>`
|
||||
: ""}
|
||||
<div class="unit">${this._computeTarget()}</div>
|
||||
${this._localizeState()}
|
||||
${this.stateObj.attributes.preset_mode &&
|
||||
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
|
||||
? html`-
|
||||
${this.hass.localize(
|
||||
`state_attributes.climate.preset_mode.${this.stateObj.attributes.preset_mode}`
|
||||
) || this.stateObj.attributes.preset_mode}`
|
||||
: ""}
|
||||
</span>
|
||||
<div class="unit">${this._computeTarget()}</div>`
|
||||
: this._localizeState()}
|
||||
</div>
|
||||
|
||||
${currentStatus
|
||||
${currentStatus && !UNAVAILABLE_STATES.includes(this.stateObj.state)
|
||||
? html`<div class="current">
|
||||
${this.hass.localize("ui.card.climate.currently")}:
|
||||
<div class="unit">${currentStatus}</div>
|
||||
@ -108,6 +109,10 @@ class HaClimateState extends LitElement {
|
||||
}
|
||||
|
||||
private _localizeState(): string {
|
||||
if (UNAVAILABLE_STATES.includes(this.stateObj.state)) {
|
||||
return this.hass.localize(`state.default.${this.stateObj.state}`);
|
||||
}
|
||||
|
||||
const stateString = this.hass.localize(
|
||||
`component.climate.state._.${this.stateObj.state}`
|
||||
);
|
||||
|
@ -96,6 +96,8 @@ export class HaComboBox extends LitElement {
|
||||
|
||||
@query("vaadin-combo-box-light", true) private _comboBox!: ComboBoxLight;
|
||||
|
||||
private _overlayMutationObserver?: MutationObserver;
|
||||
|
||||
public open() {
|
||||
this.updateComplete.then(() => {
|
||||
this._comboBox?.open();
|
||||
@ -108,6 +110,14 @@ export class HaComboBox extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this._overlayMutationObserver) {
|
||||
this._overlayMutationObserver.disconnect();
|
||||
this._overlayMutationObserver = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public get selectedItem() {
|
||||
return this._comboBox.selectedItem;
|
||||
}
|
||||
@ -194,12 +204,57 @@ export class HaComboBox extends LitElement {
|
||||
}
|
||||
|
||||
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
|
||||
const opened = ev.detail.value;
|
||||
// delay this so we can handle click event before setting _opened
|
||||
setTimeout(() => {
|
||||
this._opened = ev.detail.value;
|
||||
this._opened = opened;
|
||||
}, 0);
|
||||
// @ts-ignore
|
||||
fireEvent(this, ev.type, ev.detail);
|
||||
|
||||
if (
|
||||
opened &&
|
||||
"MutationObserver" in window &&
|
||||
!this._overlayMutationObserver
|
||||
) {
|
||||
const overlay = document.querySelector<HTMLElement>(
|
||||
"vaadin-combo-box-overlay"
|
||||
);
|
||||
|
||||
if (!overlay) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._overlayMutationObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (
|
||||
mutation.type === "attributes" &&
|
||||
mutation.attributeName === "inert" &&
|
||||
// @ts-expect-error
|
||||
overlay.inert === true
|
||||
) {
|
||||
// @ts-expect-error
|
||||
overlay.inert = false;
|
||||
this._overlayMutationObserver?.disconnect();
|
||||
this._overlayMutationObserver = undefined;
|
||||
} else if (mutation.type === "childList") {
|
||||
mutation.removedNodes.forEach((node) => {
|
||||
if (node.nodeName === "VAADIN-COMBO-BOX-OVERLAY") {
|
||||
this._overlayMutationObserver?.disconnect();
|
||||
this._overlayMutationObserver = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this._overlayMutationObserver.observe(overlay, {
|
||||
attributes: true,
|
||||
});
|
||||
this._overlayMutationObserver.observe(document.body, {
|
||||
childList: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _filterChanged(ev: PolymerChangedEvent<string>) {
|
||||
@ -241,6 +296,9 @@ export class HaComboBox extends LitElement {
|
||||
.toggle-button {
|
||||
right: 12px;
|
||||
top: -10px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 12px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
:host([opened]) .toggle-button {
|
||||
color: var(--primary-color);
|
||||
@ -249,18 +307,9 @@ export class HaComboBox extends LitElement {
|
||||
--mdc-icon-size: 20px;
|
||||
top: -7px;
|
||||
right: 36px;
|
||||
}
|
||||
|
||||
:host-context([style*="direction: rtl;"]) .toggle-button {
|
||||
left: 12px;
|
||||
right: auto;
|
||||
top: -10px;
|
||||
}
|
||||
:host-context([style*="direction: rtl;"]) .clear-button {
|
||||
--mdc-icon-size: 20px;
|
||||
top: -7px;
|
||||
left: 36px;
|
||||
right: auto;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 36px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -140,6 +140,9 @@ export class HaDateRangePicker extends LitElement {
|
||||
return css`
|
||||
ha-svg-icon {
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 8px;
|
||||
margin-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.date-range-inputs {
|
||||
@ -166,6 +169,9 @@ export class HaDateRangePicker extends LitElement {
|
||||
|
||||
ha-textfield:last-child {
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
|
@ -3,8 +3,8 @@ import { styles } from "@material/mwc-dialog/mwc-dialog.css";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { computeRTLDirection } from "../common/util/compute_rtl";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
|
||||
import "./ha-icon-button";
|
||||
|
||||
export const createCloseHeading = (
|
||||
@ -17,12 +17,13 @@ export const createCloseHeading = (
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
class="header_button"
|
||||
dir=${computeRTLDirection(hass)}
|
||||
></ha-icon-button>
|
||||
`;
|
||||
|
||||
@customElement("ha-dialog")
|
||||
export class HaDialog extends DialogBase {
|
||||
protected readonly [FOCUS_TARGET];
|
||||
|
||||
public scrollToPos(x: number, y: number) {
|
||||
this.contentElement?.scrollTo(x, y);
|
||||
}
|
||||
@ -89,18 +90,18 @@ export class HaDialog extends DialogBase {
|
||||
}
|
||||
.header_title {
|
||||
margin-right: 40px;
|
||||
margin-inline-end: 40px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
[dir="rtl"].header_button {
|
||||
right: auto;
|
||||
left: 16px;
|
||||
.header_button {
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 16px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
[dir="rtl"].header_title {
|
||||
margin-left: 40px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
:host-context([style*="direction: rtl;"]) .dialog-actions {
|
||||
left: 0px !important;
|
||||
right: auto !important;
|
||||
.dialog-actions {
|
||||
inset-inline-start: initial !important;
|
||||
inset-inline-end: 0px !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@ -133,6 +133,9 @@ class HaExpansionPanel extends LitElement {
|
||||
.summary-icon {
|
||||
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
margin-left: auto;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.summary-icon.expanded {
|
||||
|
@ -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 { css } from "lit";
|
||||
|
||||
@customElement("ha-fab")
|
||||
export class HaFab extends Fab {
|
||||
export class HaFab extends FabBase {
|
||||
protected firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
|
||||
}
|
||||
|
||||
static override styles = Fab.styles.concat([
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host-context([style*="direction: rtl;"])
|
||||
.mdc-fab--extended
|
||||
.mdc-fab__icon {
|
||||
margin-left: 12px !important;
|
||||
margin-right: calc(12px - 20px) !important;
|
||||
:host .mdc-fab--extended .mdc-fab__icon {
|
||||
margin-inline-start: -8px;
|
||||
margin-inline-end: 12px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`,
|
||||
]);
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -175,24 +175,23 @@ export class HaFileUpload extends LitElement {
|
||||
}
|
||||
.mdc-text-field__icon--leading {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
:host-context([style*="direction: rtl;"])
|
||||
.mdc-text-field__icon--leading {
|
||||
margin-right: 0px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 0px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-text-field--filled .mdc-floating-label--float-above {
|
||||
transform: scale(0.75);
|
||||
top: 8px;
|
||||
}
|
||||
:host-context([style*="direction: rtl;"]) .mdc-floating-label {
|
||||
left: initial;
|
||||
right: 16px;
|
||||
.mdc-floating-label {
|
||||
inset-inline-start: 16px !important;
|
||||
inset-inline-end: initial !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
:host-context([style*="direction: rtl;"])
|
||||
.mdc-text-field--filled
|
||||
.mdc-floating-label {
|
||||
left: initial;
|
||||
right: 48px;
|
||||
.mdc-text-field--filled .mdc-floating-label {
|
||||
inset-inline-start: 48px !important;
|
||||
inset-inline-end: initial !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.dragged:before {
|
||||
position: var(--layout-fit_-_position);
|
||||
|
@ -133,9 +133,10 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
:host-context([style*="direction: rtl;"]) ha-icon-button {
|
||||
right: auto;
|
||||
left: 12px;
|
||||
ha-icon-button {
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 12px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -28,10 +28,15 @@ export class HaFormfield extends FormfieldBase {
|
||||
css`
|
||||
:host(:not([alignEnd])) ::slotted(ha-switch) {
|
||||
margin-right: 10px;
|
||||
margin-inline-end: 10px;
|
||||
margin-inline-start: inline;
|
||||
}
|
||||
:host([dir="rtl"]:not([alignEnd])) ::slotted(ha-switch) {
|
||||
margin-left: 10px;
|
||||
margin-right: auto;
|
||||
.mdc-form-field > label {
|
||||
direction: var(--direction);
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: auto;
|
||||
padding-inline-start: 4px;
|
||||
padding-inline-end: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@ -1,6 +1,8 @@
|
||||
import "@material/mwc-icon-button";
|
||||
import type { IconButton } from "@material/mwc-icon-button";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-icon-button")
|
||||
@ -11,21 +13,32 @@ export class HaIconButton extends LitElement {
|
||||
@property({ type: String }) path?: string;
|
||||
|
||||
// Label that is used for ARIA support and as tooltip
|
||||
@property({ type: String }) label = "";
|
||||
@property({ type: String }) label?: string;
|
||||
|
||||
// These should always be set as properties, not attributes,
|
||||
// so that only the <button> element gets the attribute
|
||||
@property({ type: String, attribute: "aria-haspopup" })
|
||||
override ariaHasPopup!: IconButton["ariaHasPopup"];
|
||||
|
||||
@property({ type: Boolean }) hideTitle = false;
|
||||
|
||||
@query("mwc-icon-button", true) private _button?: IconButton;
|
||||
|
||||
public override focus() {
|
||||
this._button?.focus();
|
||||
}
|
||||
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
mode: "open",
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
// Note: `ariaLabel` required despite the `mwc-icon-button` docs saying `label` should be enough
|
||||
return html`
|
||||
<mwc-icon-button
|
||||
.ariaLabel=${this.label}
|
||||
.title=${this.hideTitle ? "" : this.label}
|
||||
aria-label=${ifDefined(this.label)}
|
||||
title=${ifDefined(this.hideTitle ? undefined : this.label)}
|
||||
aria-haspopup=${ifDefined(this.ariaHasPopup)}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.path
|
||||
|
@ -47,9 +47,18 @@ export class HaSelect extends SelectBase {
|
||||
.mdc-select__anchor {
|
||||
width: var(--ha-select-min-width, 200px);
|
||||
}
|
||||
:host-context([style*="direction: rtl;"]) .mdc-floating-label {
|
||||
right: 16px !important;
|
||||
left: initial !important;
|
||||
.mdc-select--filled .mdc-floating-label {
|
||||
inset-inline-start: 12px;
|
||||
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);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@ -1,15 +1,24 @@
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../data/entity_registry";
|
||||
import {
|
||||
EntitySources,
|
||||
fetchEntitySourcesWithCache,
|
||||
} from "../../data/entity_sources";
|
||||
import { AreaSelector } from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-area-picker";
|
||||
import "../ha-areas-picker";
|
||||
|
||||
@customElement("ha-selector-area")
|
||||
export class HaAreaSelector extends LitElement {
|
||||
export class HaAreaSelector extends SubscribeMixin(LitElement) {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: AreaSelector;
|
||||
@ -20,29 +29,44 @@ export class HaAreaSelector extends LitElement {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@state() public _configEntries?: ConfigEntry[];
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@state() private _entities?: EntityRegistryEntry[];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
this._entities = entities.filter((entity) => entity.device_id !== null);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changedProperties) {
|
||||
if (changedProperties.has("selector")) {
|
||||
const oldSelector = changedProperties.get("selector");
|
||||
if (
|
||||
oldSelector !== this.selector &&
|
||||
this.selector.area.device?.integration
|
||||
) {
|
||||
getConfigEntries(this.hass, {
|
||||
domain: this.selector.area.device.integration,
|
||||
}).then((entries) => {
|
||||
this._configEntries = entries;
|
||||
});
|
||||
}
|
||||
if (
|
||||
changedProperties.has("selector") &&
|
||||
(this.selector.area.device?.integration ||
|
||||
this.selector.area.entity?.integration) &&
|
||||
!this._entitySources
|
||||
) {
|
||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||
this._entitySources = sources;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (
|
||||
(this.selector.area.device?.integration ||
|
||||
this.selector.area.entity?.integration) &&
|
||||
!this._entitySources
|
||||
) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
if (!this.selector.area.multiple) {
|
||||
return html`
|
||||
<ha-area-picker
|
||||
@ -87,39 +111,62 @@ export class HaAreaSelector extends LitElement {
|
||||
}
|
||||
|
||||
private _filterEntities = (entity: EntityRegistryEntry): boolean => {
|
||||
if (this.selector.area.entity?.integration) {
|
||||
if (entity.platform !== this.selector.area.entity.integration) {
|
||||
const filterIntegration = this.selector.area.entity?.integration;
|
||||
if (
|
||||
filterIntegration &&
|
||||
this._entitySources?.[entity.entity_id]?.domain !== filterIntegration
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||
if (!this.selector.area.device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const {
|
||||
manufacturer: filterManufacturer,
|
||||
model: filterModel,
|
||||
integration: filterIntegration,
|
||||
} = this.selector.area.device;
|
||||
|
||||
if (filterManufacturer && device.manufacturer !== filterManufacturer) {
|
||||
return false;
|
||||
}
|
||||
if (filterModel && device.model !== filterModel) {
|
||||
return false;
|
||||
}
|
||||
if (filterIntegration && this._entitySources && this._entities) {
|
||||
const deviceIntegrations = this._deviceIntegrations(
|
||||
this._entitySources,
|
||||
this._entities
|
||||
);
|
||||
if (!deviceIntegrations?.[device.id]?.includes(filterIntegration)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||
if (
|
||||
this.selector.area.device?.manufacturer &&
|
||||
device.manufacturer !== this.selector.area.device.manufacturer
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
this.selector.area.device?.model &&
|
||||
device.model !== this.selector.area.device.model
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (this.selector.area.device?.integration) {
|
||||
if (
|
||||
this._configEntries &&
|
||||
!this._configEntries.some((entry) =>
|
||||
device.config_entries.includes(entry.entry_id)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
private _deviceIntegrations = memoizeOne(
|
||||
(entitySources: EntitySources, entities: EntityRegistryEntry[]) => {
|
||||
const deviceIntegrations: Record<string, string[]> = {};
|
||||
|
||||
for (const entity of entities) {
|
||||
const source = entitySources[entity.entity_id];
|
||||
if (!source?.domain) {
|
||||
continue;
|
||||
}
|
||||
if (!deviceIntegrations[entity.device_id!]) {
|
||||
deviceIntegrations[entity.device_id!] = [];
|
||||
}
|
||||
deviceIntegrations[entity.device_id!].push(source.domain);
|
||||
}
|
||||
return deviceIntegrations;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -1,18 +1,33 @@
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ConfigEntry } from "../../data/config_entries";
|
||||
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../data/entity_registry";
|
||||
import {
|
||||
EntitySources,
|
||||
fetchEntitySourcesWithCache,
|
||||
} from "../../data/entity_sources";
|
||||
import type { DeviceSelector } from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../device/ha-device-picker";
|
||||
import "../device/ha-devices-picker";
|
||||
|
||||
@customElement("ha-selector-device")
|
||||
export class HaDeviceSelector extends LitElement {
|
||||
export class HaDeviceSelector extends SubscribeMixin(LitElement) {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: DeviceSelector;
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@state() private _entities?: EntityRegistryEntry[];
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@property() public label?: string;
|
||||
@ -25,20 +40,32 @@ export class HaDeviceSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
protected updated(changedProperties) {
|
||||
if (changedProperties.has("selector")) {
|
||||
const oldSelector = changedProperties.get("selector");
|
||||
if (oldSelector !== this.selector && this.selector.device?.integration) {
|
||||
getConfigEntries(this.hass, {
|
||||
domain: this.selector.device.integration,
|
||||
}).then((entries) => {
|
||||
this._configEntries = entries;
|
||||
});
|
||||
}
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
this._entities = entities.filter((entity) => entity.device_id !== null);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changedProperties): void {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
changedProperties.has("selector") &&
|
||||
this.selector.device.integration &&
|
||||
!this._entitySources
|
||||
) {
|
||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||
this._entitySources = sources;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this.selector.device.integration && !this._entitySources) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
if (!this.selector.device.multiple) {
|
||||
return html`
|
||||
<ha-device-picker
|
||||
@ -80,30 +107,48 @@ export class HaDeviceSelector extends LitElement {
|
||||
}
|
||||
|
||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||
if (
|
||||
this.selector.device?.manufacturer &&
|
||||
device.manufacturer !== this.selector.device.manufacturer
|
||||
) {
|
||||
const {
|
||||
manufacturer: filterManufacturer,
|
||||
model: filterModel,
|
||||
integration: filterIntegration,
|
||||
} = this.selector.device;
|
||||
|
||||
if (filterManufacturer && device.manufacturer !== filterManufacturer) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
this.selector.device?.model &&
|
||||
device.model !== this.selector.device.model
|
||||
) {
|
||||
if (filterModel && device.model !== filterModel) {
|
||||
return false;
|
||||
}
|
||||
if (this.selector.device?.integration) {
|
||||
if (
|
||||
this._configEntries &&
|
||||
!this._configEntries.some((entry) =>
|
||||
device.config_entries.includes(entry.entry_id)
|
||||
)
|
||||
) {
|
||||
if (filterIntegration && this._entitySources && this._entities) {
|
||||
const deviceIntegrations = this._deviceIntegrations(
|
||||
this._entitySources,
|
||||
this._entities
|
||||
);
|
||||
if (!deviceIntegrations?.[device.id]?.includes(filterIntegration)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
private _deviceIntegrations = memoizeOne(
|
||||
(entitySources: EntitySources, entities: EntityRegistryEntry[]) => {
|
||||
const deviceIntegrations: Record<string, string[]> = {};
|
||||
|
||||
for (const entity of entities) {
|
||||
const source = entitySources[entity.entity_id];
|
||||
if (!source?.domain) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!deviceIntegrations[entity.device_id!]) {
|
||||
deviceIntegrations[entity.device_id!] = [];
|
||||
}
|
||||
deviceIntegrations[entity.device_id!].push(source.domain);
|
||||
}
|
||||
return deviceIntegrations;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import "@material/mwc-formfield/mwc-formfield";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
@ -47,14 +46,14 @@ export class HaSelectSelector extends LitElement {
|
||||
${this.label}
|
||||
${options.map(
|
||||
(item: SelectOption) => html`
|
||||
<mwc-formfield .label=${item.label}>
|
||||
<ha-formfield .label=${item.label}>
|
||||
<ha-radio
|
||||
.checked=${item.value === this.value}
|
||||
.value=${item.value}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._valueChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
</ha-formfield>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
|
@ -287,9 +287,7 @@ export class HaServiceControl extends LitElement {
|
||||
${shouldRenderServiceDataYaml
|
||||
? html`<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.service-control.service_data"
|
||||
)}
|
||||
.label=${this.hass.localize("ui.components.service-control.data")}
|
||||
.name=${"data"}
|
||||
.defaultValue=${this._value?.data}
|
||||
@value-changed=${this._dataChanged}
|
||||
|
@ -569,6 +569,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
height: 16px;
|
||||
--mdc-icon-size: 14px;
|
||||
color: var(--secondary-text-color);
|
||||
margin-inline-start: 4px !important;
|
||||
margin-inline-end: -4px !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-chip__icon--leading {
|
||||
display: flex;
|
||||
@ -578,6 +581,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
border-radius: 50%;
|
||||
padding: 6px;
|
||||
margin-left: -14px !important;
|
||||
margin-inline-start: -14px !important;
|
||||
margin-inline-end: 4px !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.expand-btn {
|
||||
margin-right: 0;
|
||||
@ -616,10 +622,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
opacity: var(--light-disabled-opacity);
|
||||
pointer-events: none;
|
||||
}
|
||||
:host-context([style*="direction: rtl;"]) .mdc-chip__icon {
|
||||
margin-right: -14px !important;
|
||||
margin-left: 4px !important;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +57,9 @@ export class HaTextField extends TextFieldBase {
|
||||
.mdc-text-field__affix--suffix {
|
||||
padding-left: var(--text-field-suffix-padding-left, 12px);
|
||||
padding-right: var(--text-field-suffix-padding-right, 0px);
|
||||
padding-inline-start: var(--text-field-suffix-padding-left, 12px);
|
||||
padding-inline-end: var(--text-field-suffix-padding-right, 0px);
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.mdc-text-field:not(.mdc-text-field--disabled)
|
||||
@ -92,17 +95,19 @@ export class HaTextField extends TextFieldBase {
|
||||
overflow: var(--text-field-overflow);
|
||||
}
|
||||
|
||||
:host-context([style*="direction: rtl;"]) .mdc-floating-label {
|
||||
right: 10px !important;
|
||||
left: initial !important;
|
||||
.mdc-floating-label {
|
||||
inset-inline-start: 16px !important;
|
||||
inset-inline-end: initial !important;
|
||||
transform-origin: var(--float-start);
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
:host-context([style*="direction: rtl;"])
|
||||
.mdc-text-field--with-leading-icon.mdc-text-field--filled
|
||||
.mdc-text-field--with-leading-icon.mdc-text-field--filled
|
||||
.mdc-floating-label {
|
||||
max-width: calc(100% - 48px);
|
||||
right: 48px !important;
|
||||
left: initial !important;
|
||||
inset-inline-start: 48px !important;
|
||||
inset-inline-end: initial !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@ -314,9 +314,10 @@ class DialogMediaManage extends LitElement {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
:host-context([style*="direction: rtl;"]) ha-svg-icon[slot="icon"] {
|
||||
margin-left: 8px !important;
|
||||
margin-right: 0px !important;
|
||||
ha-svg-icon[slot="icon"] {
|
||||
margin-inline-start: 0px !important;
|
||||
margin-inline-end: 8px !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.refresh {
|
||||
|
@ -60,9 +60,10 @@ class MediaManageButton extends LitElement {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
:host-context([style*="direction: rtl;"]) ha-svg-icon[slot="icon"] {
|
||||
margin-left: 8px;
|
||||
margin-right: 0px;
|
||||
ha-svg-icon[slot="icon"] {
|
||||
margin-inline-start: 0px;
|
||||
margin-inline-end: 8px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { getSignedPath } from "../../data/auth";
|
||||
import { UNAVAILABLE_STATES } from "../../data/entity";
|
||||
import type { MediaPlayerItem } from "../../data/media-player";
|
||||
import {
|
||||
browseMediaPlayer,
|
||||
@ -45,6 +46,7 @@ import type { HomeAssistant } from "../../types";
|
||||
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import "../entity/ha-entity-picker";
|
||||
import "../ha-alert";
|
||||
import "../ha-button-menu";
|
||||
import "../ha-card";
|
||||
import "../ha-circular-progress";
|
||||
@ -246,6 +248,16 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
],
|
||||
replace: true,
|
||||
});
|
||||
} else if (
|
||||
err.code === "entity_not_found" &&
|
||||
UNAVAILABLE_STATES.includes(this.hass.states[this.entityId]?.state)
|
||||
) {
|
||||
this._setError({
|
||||
message: this.hass.localize(
|
||||
`ui.components.media-browser.media_player_unavailable`
|
||||
),
|
||||
code: "entity_not_found",
|
||||
});
|
||||
} else {
|
||||
this._setError(err);
|
||||
}
|
||||
@ -305,7 +317,11 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
if (this._error) {
|
||||
return html`
|
||||
<div class="container">${this._renderError(this._error)}</div>
|
||||
<div class="container">
|
||||
<ha-alert alert-type="error">
|
||||
${this._renderError(this._error)}
|
||||
</ha-alert>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -420,7 +436,9 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
this._error
|
||||
? html`
|
||||
<div class="container">
|
||||
${this._renderError(this._error)}
|
||||
<ha-alert alert-type="error">
|
||||
${this._renderError(this._error)}
|
||||
</ha-alert>
|
||||
</div>
|
||||
`
|
||||
: isTTSMediaSource(currentItem.media_content_id)
|
||||
|
@ -120,9 +120,10 @@ class MediaUploadButton extends LitElement {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
:host-context([style*="direction: rtl;"]) ha-svg-icon[slot="icon"] {
|
||||
margin-left: 8px;
|
||||
margin-right: 0px;
|
||||
ha-svg-icon[slot="icon"] {
|
||||
margin-inline-start: 0px;
|
||||
margin-inline-end: 8px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { customElement, property } from "lit/decorators";
|
||||
import { LogbookEntry } from "../../data/logbook";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "./hat-logbook-note";
|
||||
import "../../panels/logbook/ha-logbook";
|
||||
import "../../panels/logbook/ha-logbook-renderer";
|
||||
import { TraceExtended } from "../../data/trace";
|
||||
|
||||
@customElement("ha-trace-logbook")
|
||||
@ -19,12 +19,12 @@ export class HaTraceLogbook extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return this.logbookEntries.length
|
||||
? html`
|
||||
<ha-logbook
|
||||
<ha-logbook-renderer
|
||||
relative-time
|
||||
.hass=${this.hass}
|
||||
.entries=${this.logbookEntries}
|
||||
.narrow=${this.narrow}
|
||||
></ha-logbook>
|
||||
></ha-logbook-renderer>
|
||||
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
|
||||
`
|
||||
: html`<div class="padded-box">
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
getDataFromPath,
|
||||
TraceExtended,
|
||||
} from "../../data/trace";
|
||||
import "../../panels/logbook/ha-logbook";
|
||||
import "../../panels/logbook/ha-logbook-renderer";
|
||||
import { traceTabStyles } from "./trace-tab-styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
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.
|
||||
const startTime = new Date(startTrace[0].timestamp);
|
||||
const idx = this.logbookEntries.findIndex(
|
||||
(entry) => new Date(entry.when) >= startTime
|
||||
(entry) => new Date(entry.when * 1000) >= startTime
|
||||
);
|
||||
if (idx === -1) {
|
||||
entries = [];
|
||||
@ -210,7 +210,7 @@ export class HaTracePathDetails extends LitElement {
|
||||
entries = [];
|
||||
|
||||
for (const entry of this.logbookEntries || []) {
|
||||
const entryDate = new Date(entry.when);
|
||||
const entryDate = new Date(entry.when * 1000);
|
||||
if (entryDate >= startTime) {
|
||||
if (entryDate < endTime) {
|
||||
entries.push(entry);
|
||||
@ -224,12 +224,12 @@ export class HaTracePathDetails extends LitElement {
|
||||
|
||||
return entries.length
|
||||
? html`
|
||||
<ha-logbook
|
||||
<ha-logbook-renderer
|
||||
relative-time
|
||||
.hass=${this.hass}
|
||||
.entries=${entries}
|
||||
.narrow=${this.narrow}
|
||||
></ha-logbook>
|
||||
></ha-logbook-renderer>
|
||||
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
|
||||
`
|
||||
: html`<div class="padded-box">
|
||||
|
@ -116,7 +116,7 @@ class LogbookRenderer {
|
||||
maybeRenderItem() {
|
||||
const logbookEntry = this.curItem;
|
||||
this.curIndex++;
|
||||
const entryDate = new Date(logbookEntry.when);
|
||||
const entryDate = new Date(logbookEntry.when * 1000);
|
||||
|
||||
if (this.pendingItems.length === 0) {
|
||||
this.pendingItems.push([entryDate, logbookEntry]);
|
||||
@ -248,7 +248,7 @@ class ActionRenderer {
|
||||
// Render all logbook items that are in front of this item.
|
||||
while (
|
||||
this.logbookRenderer.hasNext &&
|
||||
new Date(this.logbookRenderer.curItem.when) < timestamp
|
||||
new Date(this.logbookRenderer.curItem.when * 1000) < timestamp
|
||||
) {
|
||||
this.logbookRenderer.maybeRenderItem();
|
||||
}
|
||||
|
47
src/data/application_credential.ts
Normal file
47
src/data/application_credential.ts
Normal 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,
|
||||
});
|
@ -157,6 +157,7 @@ export interface CalendarTrigger extends BaseTrigger {
|
||||
platform: "calendar";
|
||||
event: "start" | "end";
|
||||
entity_id: string;
|
||||
offset: string;
|
||||
}
|
||||
|
||||
export type Trigger =
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import { HomeAssistant } from "../types";
|
||||
import {
|
||||
computeHistory,
|
||||
fetchRecent,
|
||||
HistoryStates,
|
||||
HistoryResult,
|
||||
LineChartUnit,
|
||||
TimelineEntity,
|
||||
entityIdHistoryNeedsAttributes,
|
||||
fetchRecentWS,
|
||||
} from "./history";
|
||||
|
||||
export interface CacheConfig {
|
||||
@ -34,7 +34,7 @@ const RECENT_THRESHOLD = 60000; // 1 minute
|
||||
const RECENT_CACHE: { [cacheKey: string]: RecentCacheResults } = {};
|
||||
const stateHistoryCache: { [cacheKey: string]: CachedResults } = {};
|
||||
|
||||
// Cached type 1 unction. Without cache config.
|
||||
// Cached type 1 function. Without cache config.
|
||||
export const getRecent = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
@ -55,7 +55,7 @@ export const getRecent = (
|
||||
}
|
||||
|
||||
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
|
||||
const prom = fetchRecent(
|
||||
const prom = fetchRecentWS(
|
||||
hass,
|
||||
entityId,
|
||||
startTime,
|
||||
@ -103,13 +103,14 @@ export const getRecentWithCache = (
|
||||
language: string
|
||||
) => {
|
||||
const cacheKey = cacheConfig.cacheKey;
|
||||
const fullCacheKey = cacheKey + `_${cacheConfig.hoursToShow}`;
|
||||
const endTime = new Date();
|
||||
const startTime = new Date(endTime);
|
||||
startTime.setHours(startTime.getHours() - cacheConfig.hoursToShow);
|
||||
let toFetchStartTime = startTime;
|
||||
let appendingToCache = false;
|
||||
|
||||
let cache = stateHistoryCache[cacheKey + `_${cacheConfig.hoursToShow}`];
|
||||
let cache = stateHistoryCache[fullCacheKey];
|
||||
if (
|
||||
cache &&
|
||||
toFetchStartTime >= cache.startTime &&
|
||||
@ -123,7 +124,7 @@ export const getRecentWithCache = (
|
||||
return cache.prom;
|
||||
}
|
||||
} else {
|
||||
cache = stateHistoryCache[cacheKey] = getEmptyCache(
|
||||
cache = stateHistoryCache[fullCacheKey] = getEmptyCache(
|
||||
language,
|
||||
startTime,
|
||||
endTime
|
||||
@ -134,12 +135,12 @@ export const getRecentWithCache = (
|
||||
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
|
||||
|
||||
const genProm = async () => {
|
||||
let fetchedHistory: HassEntity[][];
|
||||
let fetchedHistory: HistoryStates;
|
||||
|
||||
try {
|
||||
const results = await Promise.all([
|
||||
curCacheProm,
|
||||
fetchRecent(
|
||||
fetchRecentWS(
|
||||
hass,
|
||||
entityId,
|
||||
toFetchStartTime,
|
||||
@ -152,7 +153,7 @@ export const getRecentWithCache = (
|
||||
]);
|
||||
fetchedHistory = results[1];
|
||||
} catch (err: any) {
|
||||
delete stateHistoryCache[cacheKey];
|
||||
delete stateHistoryCache[fullCacheKey];
|
||||
throw err;
|
||||
}
|
||||
const stateHistory = computeHistory(hass, fetchedHistory, localize);
|
||||
|
@ -1,11 +1,14 @@
|
||||
import {
|
||||
addDays,
|
||||
addHours,
|
||||
addMilliseconds,
|
||||
addMonths,
|
||||
differenceInDays,
|
||||
endOfToday,
|
||||
endOfYesterday,
|
||||
startOfToday,
|
||||
startOfYesterday,
|
||||
} from "date-fns";
|
||||
} from "date-fns/esm";
|
||||
import { Collection, getCollection } from "home-assistant-js-websocket";
|
||||
import { groupBy } from "../common/util/group-by";
|
||||
import { subscribeOne } from "../common/util/subscribe-one";
|
||||
@ -14,9 +17,9 @@ import { ConfigEntry, getConfigEntries } from "./config_entries";
|
||||
import { subscribeEntityRegistry } from "./entity_registry";
|
||||
import {
|
||||
fetchStatistics,
|
||||
getStatisticMetadata,
|
||||
Statistics,
|
||||
StatisticsMetaData,
|
||||
getStatisticMetadata,
|
||||
} from "./history";
|
||||
|
||||
const energyCollectionKeys: (string | undefined)[] = [];
|
||||
@ -232,19 +235,24 @@ export const energySourcesByType = (prefs: EnergyPreferences) =>
|
||||
export interface EnergyData {
|
||||
start: Date;
|
||||
end?: Date;
|
||||
startCompare?: Date;
|
||||
endCompare?: Date;
|
||||
prefs: EnergyPreferences;
|
||||
info: EnergyInfo;
|
||||
stats: Statistics;
|
||||
statsCompare: Statistics;
|
||||
co2SignalConfigEntry?: ConfigEntry;
|
||||
co2SignalEntity?: string;
|
||||
fossilEnergyConsumption?: FossilEnergyConsumption;
|
||||
fossilEnergyConsumptionCompare?: FossilEnergyConsumption;
|
||||
}
|
||||
|
||||
const getEnergyData = async (
|
||||
hass: HomeAssistant,
|
||||
prefs: EnergyPreferences,
|
||||
start: Date,
|
||||
end?: Date
|
||||
end?: Date,
|
||||
compare?: boolean
|
||||
): Promise<EnergyData> => {
|
||||
const [configEntries, entityRegistryEntries, info] = await Promise.all([
|
||||
getConfigEntries(hass, { domain: "co2signal" }),
|
||||
@ -350,6 +358,8 @@ const getEnergyData = async (
|
||||
}
|
||||
|
||||
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
|
||||
const startMinHour = addHours(start, -1);
|
||||
@ -359,10 +369,34 @@ const getEnergyData = async (
|
||||
startMinHour,
|
||||
end,
|
||||
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 fossilEnergyConsumptionCompare: FossilEnergyConsumption | undefined;
|
||||
|
||||
if (co2SignalEntity !== undefined) {
|
||||
fossilEnergyConsumption = await getFossilEnergyConsumption(
|
||||
@ -373,6 +407,16 @@ const getEnergyData = async (
|
||||
end,
|
||||
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) => {
|
||||
@ -388,15 +432,19 @@ const getEnergyData = async (
|
||||
}
|
||||
});
|
||||
|
||||
const data = {
|
||||
const data: EnergyData = {
|
||||
start,
|
||||
end,
|
||||
startCompare,
|
||||
endCompare,
|
||||
info,
|
||||
prefs,
|
||||
stats,
|
||||
statsCompare,
|
||||
co2SignalConfigEntry,
|
||||
co2SignalEntity,
|
||||
fossilEnergyConsumption,
|
||||
fossilEnergyConsumptionCompare,
|
||||
};
|
||||
|
||||
return data;
|
||||
@ -405,9 +453,11 @@ const getEnergyData = async (
|
||||
export interface EnergyCollection extends Collection<EnergyData> {
|
||||
start: Date;
|
||||
end?: Date;
|
||||
compare?: boolean;
|
||||
prefs?: EnergyPreferences;
|
||||
clearPrefs(): void;
|
||||
setPeriod(newStart: Date, newEnd?: Date): void;
|
||||
setCompare(compare: boolean): void;
|
||||
_refreshTimeout?: number;
|
||||
_updatePeriodTimeout?: number;
|
||||
_active: number;
|
||||
@ -478,7 +528,8 @@ export const getEnergyDataCollection = (
|
||||
hass,
|
||||
collection.prefs,
|
||||
collection.start,
|
||||
collection.end
|
||||
collection.end,
|
||||
collection.compare
|
||||
);
|
||||
}
|
||||
) as EnergyCollection;
|
||||
@ -534,6 +585,9 @@ export const getEnergyDataCollection = (
|
||||
collection._updatePeriodTimeout = undefined;
|
||||
}
|
||||
};
|
||||
collection.setCompare = (compare: boolean) => {
|
||||
collection.compare = compare;
|
||||
};
|
||||
return collection;
|
||||
};
|
||||
|
||||
|
@ -20,3 +20,20 @@ export const BOARD_NAMES: Record<string, string> = {
|
||||
"intel-nuc": "Intel NUC",
|
||||
yellow: "Home Assistant Yellow",
|
||||
};
|
||||
|
||||
export interface HardwareInfo {
|
||||
hardware: HardwareInfoEntry[];
|
||||
}
|
||||
|
||||
export interface HardwareInfoEntry {
|
||||
board: HardwareInfoBoardInfo;
|
||||
name: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface HardwareInfoBoardInfo {
|
||||
manufacturer: string;
|
||||
model?: string;
|
||||
revision?: string;
|
||||
hassio_board_id?: string;
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { computeStateDisplayFromEntityAttributes } from "../common/entity/compute_state_display";
|
||||
import { computeStateNameFromEntityAttributes } from "../common/entity/compute_state_name";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { FrontendLocaleData } from "./translation";
|
||||
@ -27,7 +26,7 @@ const LINE_ATTRIBUTES_TO_KEEP = [
|
||||
|
||||
export interface LineChartState {
|
||||
state: string;
|
||||
last_changed: string;
|
||||
last_changed: number;
|
||||
attributes?: Record<string, any>;
|
||||
}
|
||||
|
||||
@ -47,7 +46,7 @@ export interface LineChartUnit {
|
||||
export interface TimelineState {
|
||||
state_localize: string;
|
||||
state: string;
|
||||
last_changed: string;
|
||||
last_changed: number;
|
||||
}
|
||||
|
||||
export interface TimelineEntity {
|
||||
@ -141,6 +140,21 @@ export interface StatisticsValidationResults {
|
||||
[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 = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
@ -181,6 +195,27 @@ export const fetchRecent = (
|
||||
return hass.callApi("GET", url);
|
||||
};
|
||||
|
||||
export const fetchRecentWS = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string, // This may be CSV
|
||||
startTime: Date,
|
||||
endTime: Date,
|
||||
skipInitialState = false,
|
||||
significantChangesOnly?: boolean,
|
||||
minimalResponse = true,
|
||||
noAttributes?: boolean
|
||||
) =>
|
||||
hass.callWS<HistoryStates>({
|
||||
type: "history/history_during_period",
|
||||
start_time: startTime.toISOString(),
|
||||
end_time: endTime.toISOString(),
|
||||
significant_changes_only: significantChangesOnly || false,
|
||||
include_start_time_state: !skipInitialState,
|
||||
minimal_response: minimalResponse,
|
||||
no_attributes: noAttributes || false,
|
||||
entity_ids: entityId.split(","),
|
||||
});
|
||||
|
||||
export const fetchDate = (
|
||||
hass: HomeAssistant,
|
||||
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) =>
|
||||
obj1.state === obj2.state &&
|
||||
// Only compare attributes if both states have an attributes object.
|
||||
@ -212,46 +268,47 @@ const equalState = (obj1: LineChartState, obj2: LineChartState) =>
|
||||
const processTimelineEntity = (
|
||||
localize: LocalizeFunc,
|
||||
language: FrontendLocaleData,
|
||||
states: HassEntity[]
|
||||
entityId: string,
|
||||
states: EntityHistoryState[]
|
||||
): TimelineEntity => {
|
||||
const data: TimelineState[] = [];
|
||||
const last_element = states.length - 1;
|
||||
|
||||
const first: EntityHistoryState = states[0];
|
||||
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;
|
||||
}
|
||||
|
||||
// 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({
|
||||
state_localize: computeStateDisplay(localize, state, language),
|
||||
state: state.state,
|
||||
last_changed: state.last_changed,
|
||||
state_localize: computeStateDisplayFromEntityAttributes(
|
||||
localize,
|
||||
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 {
|
||||
name: computeStateName(states[0]),
|
||||
entity_id: states[0].entity_id,
|
||||
name: computeStateNameFromEntityAttributes(entityId, states[0].a),
|
||||
entity_id: entityId,
|
||||
data,
|
||||
};
|
||||
};
|
||||
|
||||
const processLineChartEntities = (
|
||||
unit,
|
||||
entities: HassEntity[][]
|
||||
entities: HistoryStates
|
||||
): LineChartUnit => {
|
||||
const data: LineChartEntity[] = [];
|
||||
|
||||
for (const states of entities) {
|
||||
const last: HassEntity = states[states.length - 1];
|
||||
const domain = computeStateDomain(last);
|
||||
Object.keys(entities).forEach((entityId) => {
|
||||
const states = entities[entityId];
|
||||
const first: EntityHistoryState = states[0];
|
||||
const domain = computeDomain(entityId);
|
||||
const processedStates: LineChartState[] = [];
|
||||
|
||||
for (const state of states) {
|
||||
@ -259,18 +316,24 @@ const processLineChartEntities = (
|
||||
|
||||
if (DOMAINS_USE_LAST_UPDATED.includes(domain)) {
|
||||
processedState = {
|
||||
state: state.state,
|
||||
last_changed: state.last_updated,
|
||||
state: state.s,
|
||||
last_changed: state.lu * 1000,
|
||||
attributes: {},
|
||||
};
|
||||
|
||||
for (const attr of LINE_ATTRIBUTES_TO_KEEP) {
|
||||
if (attr in state.attributes) {
|
||||
processedState.attributes![attr] = state.attributes[attr];
|
||||
if (attr in state.a) {
|
||||
processedState.attributes![attr] = state.a[attr];
|
||||
}
|
||||
}
|
||||
} 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 (
|
||||
@ -289,52 +352,53 @@ const processLineChartEntities = (
|
||||
|
||||
data.push({
|
||||
domain,
|
||||
name: computeStateName(last),
|
||||
entity_id: last.entity_id,
|
||||
name: computeStateNameFromEntityAttributes(entityId, first.a),
|
||||
entity_id: entityId,
|
||||
states: processedStates,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
unit,
|
||||
identifier: entities.map((states) => states[0].entity_id).join(""),
|
||||
identifier: Object.keys(entities).join(""),
|
||||
data,
|
||||
};
|
||||
};
|
||||
|
||||
const stateUsesUnits = (state: HassEntity) =>
|
||||
"unit_of_measurement" in state.attributes ||
|
||||
"state_class" in state.attributes;
|
||||
attributesHaveUnits(state.attributes);
|
||||
|
||||
const attributesHaveUnits = (attributes: { [key: string]: any }) =>
|
||||
"unit_of_measurement" in attributes || "state_class" in attributes;
|
||||
|
||||
export const computeHistory = (
|
||||
hass: HomeAssistant,
|
||||
stateHistory: HassEntity[][],
|
||||
stateHistory: HistoryStates,
|
||||
localize: LocalizeFunc
|
||||
): HistoryResult => {
|
||||
const lineChartDevices: { [unit: string]: HassEntity[][] } = {};
|
||||
const lineChartDevices: { [unit: string]: HistoryStates } = {};
|
||||
const timelineDevices: TimelineEntity[] = [];
|
||||
if (!stateHistory) {
|
||||
return { line: [], timeline: [] };
|
||||
}
|
||||
|
||||
stateHistory.forEach((stateInfo) => {
|
||||
Object.keys(stateHistory).forEach((entityId) => {
|
||||
const stateInfo = stateHistory[entityId];
|
||||
if (stateInfo.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entityId = stateInfo[0].entity_id;
|
||||
const currentState =
|
||||
entityId in hass.states ? hass.states[entityId] : undefined;
|
||||
const stateWithUnitorStateClass =
|
||||
!currentState &&
|
||||
stateInfo.find((state) => state.attributes && stateUsesUnits(state));
|
||||
stateInfo.find((state) => state.a && attributesHaveUnits(state.a));
|
||||
|
||||
let unit: string | undefined;
|
||||
|
||||
if (currentState && stateUsesUnits(currentState)) {
|
||||
unit = currentState.attributes.unit_of_measurement || " ";
|
||||
} else if (stateWithUnitorStateClass) {
|
||||
unit = stateWithUnitorStateClass.attributes.unit_of_measurement || " ";
|
||||
unit = stateWithUnitorStateClass.a.unit_of_measurement || " ";
|
||||
} else {
|
||||
unit = {
|
||||
climate: hass.config.unit_system.temperature,
|
||||
@ -348,12 +412,15 @@ export const computeHistory = (
|
||||
|
||||
if (!unit) {
|
||||
timelineDevices.push(
|
||||
processTimelineEntity(localize, hass.locale, stateInfo)
|
||||
processTimelineEntity(localize, hass.locale, entityId, stateInfo)
|
||||
);
|
||||
} else if (unit in lineChartDevices) {
|
||||
lineChartDevices[unit].push(stateInfo);
|
||||
} else if (unit in lineChartDevices && entityId in lineChartDevices[unit]) {
|
||||
lineChartDevices[unit][entityId].push(...stateInfo);
|
||||
} else {
|
||||
lineChartDevices[unit] = [stateInfo];
|
||||
if (!(unit in lineChartDevices)) {
|
||||
lineChartDevices[unit] = {};
|
||||
}
|
||||
lineChartDevices[unit][entityId] = stateInfo;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -42,8 +42,18 @@ export const domainToName = (
|
||||
manifest?: IntegrationManifest
|
||||
) => localize(`component.${domain}.title`) || manifest?.name || domain;
|
||||
|
||||
export const fetchIntegrationManifests = (hass: HomeAssistant) =>
|
||||
hass.callWS<IntegrationManifest[]>({ type: "manifest/list" });
|
||||
export const fetchIntegrationManifests = (
|
||||
hass: HomeAssistant,
|
||||
integrations?: string[]
|
||||
) => {
|
||||
const params: any = {
|
||||
type: "manifest/list",
|
||||
};
|
||||
if (integrations) {
|
||||
params.integrations = integrations;
|
||||
}
|
||||
return hass.callWS<IntegrationManifest[]>(params);
|
||||
};
|
||||
|
||||
export const fetchIntegrationManifest = (
|
||||
hass: HomeAssistant,
|
||||
|
@ -1,5 +1,9 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { BINARY_STATE_OFF, BINARY_STATE_ON } from "../common/const";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
BINARY_STATE_OFF,
|
||||
BINARY_STATE_ON,
|
||||
DOMAINS_WITH_DYNAMIC_PICTURE,
|
||||
} from "../common/const";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
@ -9,25 +13,51 @@ import { UNAVAILABLE_STATES } from "./entity";
|
||||
const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
|
||||
export const CONTINUOUS_DOMAINS = ["proximity", "sensor"];
|
||||
|
||||
export interface LogbookStreamMessage {
|
||||
events: LogbookEntry[];
|
||||
start_time?: number; // Start time of this historical chunk
|
||||
end_time?: number; // End time of this historical chunk
|
||||
partial?: boolean; // Indiciates more historical chunks are coming
|
||||
}
|
||||
|
||||
export interface LogbookEntry {
|
||||
when: string;
|
||||
// Base data
|
||||
when: number; // Python timestamp. Do *1000 to get JS timestamp.
|
||||
name: string;
|
||||
message?: string;
|
||||
entity_id?: string;
|
||||
icon?: string;
|
||||
source?: string;
|
||||
source?: string; // The trigger source
|
||||
domain?: string;
|
||||
state?: string; // The state of the entity
|
||||
// Context data
|
||||
context_id?: string;
|
||||
context_user_id?: string;
|
||||
context_event_type?: string;
|
||||
context_domain?: string;
|
||||
context_service?: string;
|
||||
context_service?: string; // Service calls only
|
||||
context_entity_id?: string;
|
||||
context_entity_id_name?: string;
|
||||
context_entity_id_name?: string; // Legacy, not longer sent
|
||||
context_name?: string;
|
||||
state?: string;
|
||||
context_state?: string; // The state of the entity
|
||||
context_source?: string; // The trigger source
|
||||
context_message?: string;
|
||||
}
|
||||
|
||||
//
|
||||
// Localization mapping for all the triggers in core
|
||||
// in homeassistant.components.homeassistant.triggers
|
||||
//
|
||||
const triggerPhrases = {
|
||||
"numeric state of": "triggered_by_numeric_state_of", // number state trigger
|
||||
"state of": "triggered_by_state_of", // state trigger
|
||||
event: "triggered_by_event", // event trigger
|
||||
time: "triggered_by_time", // time trigger
|
||||
"time pattern": "triggered_by_time_pattern", // time trigger
|
||||
"Home Assistant stopping": "triggered_by_homeassistant_stopping", // stop event
|
||||
"Home Assistant starting": "triggered_by_homeassistant_starting", // start event
|
||||
};
|
||||
|
||||
const DATA_CACHE: {
|
||||
[cacheKey: string]: { [entityId: string]: Promise<LogbookEntry[]> };
|
||||
} = {};
|
||||
@ -37,18 +67,13 @@ export const getLogbookDataForContext = async (
|
||||
startDate: string,
|
||||
contextId?: string
|
||||
): Promise<LogbookEntry[]> => {
|
||||
const localize = await hass.loadBackendTranslation("device_class");
|
||||
return addLogbookMessage(
|
||||
await hass.loadBackendTranslation("device_class");
|
||||
return getLogbookDataFromServer(
|
||||
hass,
|
||||
localize,
|
||||
await getLogbookDataFromServer(
|
||||
hass,
|
||||
startDate,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
contextId
|
||||
)
|
||||
startDate,
|
||||
undefined,
|
||||
undefined,
|
||||
contextId
|
||||
);
|
||||
};
|
||||
|
||||
@ -56,107 +81,123 @@ export const getLogbookData = async (
|
||||
hass: HomeAssistant,
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
entityId?: string,
|
||||
entity_matches_only?: boolean
|
||||
entityIds?: string[],
|
||||
deviceIds?: string[]
|
||||
): Promise<LogbookEntry[]> => {
|
||||
const localize = await hass.loadBackendTranslation("device_class");
|
||||
return addLogbookMessage(
|
||||
hass,
|
||||
localize,
|
||||
await getLogbookDataCache(
|
||||
hass,
|
||||
startDate,
|
||||
endDate,
|
||||
entityId,
|
||||
entity_matches_only
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const addLogbookMessage = (
|
||||
hass: HomeAssistant,
|
||||
localize: LocalizeFunc,
|
||||
logbookData: LogbookEntry[]
|
||||
): LogbookEntry[] => {
|
||||
for (const entry of logbookData) {
|
||||
const stateObj = hass!.states[entry.entity_id!];
|
||||
if (entry.state && stateObj) {
|
||||
entry.message = getLogbookMessage(
|
||||
await hass.loadBackendTranslation("device_class");
|
||||
return deviceIds?.length
|
||||
? getLogbookDataFromServer(
|
||||
hass,
|
||||
localize,
|
||||
entry.state,
|
||||
stateObj,
|
||||
computeDomain(entry.entity_id!)
|
||||
);
|
||||
}
|
||||
}
|
||||
return logbookData;
|
||||
startDate,
|
||||
endDate,
|
||||
entityIds,
|
||||
undefined,
|
||||
deviceIds
|
||||
)
|
||||
: getLogbookDataCache(hass, startDate, endDate, entityIds);
|
||||
};
|
||||
|
||||
export const getLogbookDataCache = async (
|
||||
const getLogbookDataCache = async (
|
||||
hass: HomeAssistant,
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
entityId?: string,
|
||||
entity_matches_only?: boolean
|
||||
entityId?: string[]
|
||||
) => {
|
||||
const ALL_ENTITIES = "*";
|
||||
|
||||
if (!entityId) {
|
||||
entityId = ALL_ENTITIES;
|
||||
}
|
||||
|
||||
const entityIdKey = entityId ? entityId.toString() : ALL_ENTITIES;
|
||||
const cacheKey = `${startDate}${endDate}`;
|
||||
|
||||
if (!DATA_CACHE[cacheKey]) {
|
||||
DATA_CACHE[cacheKey] = {};
|
||||
}
|
||||
|
||||
if (entityId in DATA_CACHE[cacheKey]) {
|
||||
return DATA_CACHE[cacheKey][entityId];
|
||||
if (entityIdKey in DATA_CACHE[cacheKey]) {
|
||||
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];
|
||||
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,
|
||||
startDate,
|
||||
endDate,
|
||||
entityId !== ALL_ENTITIES ? entityId : undefined,
|
||||
entity_matches_only
|
||||
).then((entries) => entries.reverse());
|
||||
return DATA_CACHE[cacheKey][entityId];
|
||||
entityId
|
||||
);
|
||||
return DATA_CACHE[cacheKey][entityIdKey];
|
||||
};
|
||||
|
||||
const getLogbookDataFromServer = async (
|
||||
const getLogbookDataFromServer = (
|
||||
hass: HomeAssistant,
|
||||
startDate: string,
|
||||
endDate?: string,
|
||||
entityId?: string,
|
||||
entitymatchesOnly?: boolean,
|
||||
contextId?: string
|
||||
) => {
|
||||
const params = new URLSearchParams();
|
||||
entityIds?: string[],
|
||||
contextId?: string,
|
||||
deviceIds?: string[]
|
||||
): Promise<LogbookEntry[]> => {
|
||||
// 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) {
|
||||
params.append("end_time", endDate);
|
||||
params.end_time = endDate;
|
||||
}
|
||||
if (entityId) {
|
||||
params.append("entity", entityId);
|
||||
if (entityIds?.length) {
|
||||
params.entity_ids = entityIds;
|
||||
}
|
||||
if (entitymatchesOnly) {
|
||||
params.append("entity_matches_only", "");
|
||||
if (deviceIds?.length) {
|
||||
params.device_ids = deviceIds;
|
||||
}
|
||||
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 subscribeLogbook = (
|
||||
hass: HomeAssistant,
|
||||
callbackFunction: (message: LogbookStreamMessage) => void,
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
entityIds?: string[],
|
||||
deviceIds?: string[]
|
||||
): Promise<UnsubscribeFunc> => {
|
||||
// If all specified filters are empty lists, we can return an empty list.
|
||||
if (
|
||||
(entityIds || deviceIds) &&
|
||||
(!entityIds || entityIds.length === 0) &&
|
||||
(!deviceIds || deviceIds.length === 0)
|
||||
) {
|
||||
return Promise.reject("No entities or devices");
|
||||
}
|
||||
const params: any = {
|
||||
type: "logbook/event_stream",
|
||||
start_time: startDate,
|
||||
end_time: endDate,
|
||||
};
|
||||
if (entityIds?.length) {
|
||||
params.entity_ids = entityIds;
|
||||
}
|
||||
if (deviceIds?.length) {
|
||||
params.device_ids = deviceIds;
|
||||
}
|
||||
return hass.connection.subscribeMessage<LogbookStreamMessage>(
|
||||
(message) => callbackFunction(message),
|
||||
params
|
||||
);
|
||||
};
|
||||
|
||||
@ -164,7 +205,49 @@ export const clearLogbookCache = (startDate: string, endDate: string) => {
|
||||
DATA_CACHE[`${startDate}${endDate}`] = {};
|
||||
};
|
||||
|
||||
export const getLogbookMessage = (
|
||||
export const createHistoricState = (
|
||||
currentStateObj: HassEntity,
|
||||
state?: string
|
||||
): HassEntity => <HassEntity>(<unknown>{
|
||||
entity_id: currentStateObj.entity_id,
|
||||
state: state,
|
||||
attributes: {
|
||||
// Rebuild the historical state by copying static attributes only
|
||||
device_class: currentStateObj?.attributes.device_class,
|
||||
source_type: currentStateObj?.attributes.source_type,
|
||||
has_date: currentStateObj?.attributes.has_date,
|
||||
has_time: currentStateObj?.attributes.has_time,
|
||||
// We do not want to use dynamic entity pictures (e.g., from media player) for the log book rendering,
|
||||
// as they would present a false state in the log (played media right now vs actual historic data).
|
||||
entity_picture_local: DOMAINS_WITH_DYNAMIC_PICTURE.has(
|
||||
computeDomain(currentStateObj.entity_id)
|
||||
)
|
||||
? undefined
|
||||
: currentStateObj?.attributes.entity_picture_local,
|
||||
entity_picture: DOMAINS_WITH_DYNAMIC_PICTURE.has(
|
||||
computeDomain(currentStateObj.entity_id)
|
||||
)
|
||||
? undefined
|
||||
: currentStateObj?.attributes.entity_picture,
|
||||
},
|
||||
});
|
||||
|
||||
export const localizeTriggerSource = (
|
||||
localize: LocalizeFunc,
|
||||
source: string
|
||||
) => {
|
||||
for (const triggerPhrase in triggerPhrases) {
|
||||
if (source.startsWith(triggerPhrase)) {
|
||||
return source.replace(
|
||||
triggerPhrase,
|
||||
`${localize(`ui.components.logbook.${triggerPhrases[triggerPhrase]}`)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
return source;
|
||||
};
|
||||
|
||||
export const localizeStateMessage = (
|
||||
hass: HomeAssistant,
|
||||
localize: LocalizeFunc,
|
||||
state: string,
|
||||
|
@ -131,9 +131,9 @@ export interface CallServiceActionConfig extends BaseActionConfig {
|
||||
action: "call-service";
|
||||
service: string;
|
||||
target?: HassServiceTarget;
|
||||
service_data?: {
|
||||
[key: string]: any;
|
||||
};
|
||||
// "service_data" is kept for backwards compatibility. Replaced by "data".
|
||||
service_data?: Record<string, unknown>;
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface NavigateActionConfig extends BaseActionConfig {
|
||||
@ -159,6 +159,7 @@ export interface CustomActionConfig extends BaseActionConfig {
|
||||
}
|
||||
|
||||
export interface BaseActionConfig {
|
||||
action: string;
|
||||
confirmation?: ConfirmationRestrictionConfig;
|
||||
}
|
||||
|
||||
|
@ -47,12 +47,17 @@ export interface SceneConfig {
|
||||
name: string;
|
||||
icon?: string;
|
||||
entities: SceneEntities;
|
||||
metadata?: SceneMetaData;
|
||||
}
|
||||
|
||||
export interface SceneEntities {
|
||||
[entityId: string]: string | { state: string; [key: string]: any };
|
||||
}
|
||||
|
||||
export interface SceneMetaData {
|
||||
[entityId: string]: { entity_only?: boolean | undefined };
|
||||
}
|
||||
|
||||
export const activateScene = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
|
@ -52,7 +52,7 @@ export const getHassTranslations = async (
|
||||
hass: HomeAssistant,
|
||||
language: string,
|
||||
category: TranslationCategory,
|
||||
integration?: string,
|
||||
integration?: string | string[],
|
||||
config_flow?: boolean
|
||||
): Promise<Record<string, unknown>> => {
|
||||
const result = await hass.callWS<{ resources: Record<string, unknown> }>({
|
||||
|
@ -7,7 +7,10 @@ import type {
|
||||
import { BINARY_STATE_ON } from "../common/const";
|
||||
import { computeDomain } from "../common/entity/compute_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 { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { HomeAssistant } from "../types";
|
||||
@ -35,8 +38,13 @@ export interface UpdateEntity extends HassEntityBase {
|
||||
}
|
||||
|
||||
export const updateUsesProgress = (entity: UpdateEntity): boolean =>
|
||||
supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) &&
|
||||
typeof entity.attributes.in_progress === "number";
|
||||
updateUsesProgressFromAttributes(entity.attributes);
|
||||
|
||||
export const updateUsesProgressFromAttributes = (attributes: {
|
||||
[key: string]: any;
|
||||
}): boolean =>
|
||||
supportsFeatureFromAttributes(attributes, UPDATE_SUPPORT_PROGRESS) &&
|
||||
typeof attributes.in_progress === "number";
|
||||
|
||||
export const updateCanInstall = (
|
||||
entity: UpdateEntity,
|
||||
@ -49,6 +57,11 @@ export const updateCanInstall = (
|
||||
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
|
||||
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) =>
|
||||
hass.callWS<string | null>({
|
||||
type: "update/release_notes",
|
||||
|
@ -2,9 +2,21 @@ import {
|
||||
mdiAlertCircleOutline,
|
||||
mdiGauge,
|
||||
mdiWaterPercent,
|
||||
mdiWeatherCloudy,
|
||||
mdiWeatherFog,
|
||||
mdiWeatherHail,
|
||||
mdiWeatherLightning,
|
||||
mdiWeatherLightningRainy,
|
||||
mdiWeatherNight,
|
||||
mdiWeatherNightPartlyCloudy,
|
||||
mdiWeatherPartlyCloudy,
|
||||
mdiWeatherPouring,
|
||||
mdiWeatherRainy,
|
||||
mdiWeatherSnowy,
|
||||
mdiWeatherSnowyRainy,
|
||||
mdiWeatherSunny,
|
||||
mdiWeatherWindy,
|
||||
mdiWeatherWindyVariant,
|
||||
} from "@mdi/js";
|
||||
import {
|
||||
HassEntityAttributeBase,
|
||||
@ -57,7 +69,21 @@ export const weatherSVGs = new Set<string>([
|
||||
]);
|
||||
|
||||
export const weatherIcons = {
|
||||
"clear-night": mdiWeatherNight,
|
||||
cloudy: mdiWeatherCloudy,
|
||||
exceptional: mdiAlertCircleOutline,
|
||||
fog: mdiWeatherFog,
|
||||
hail: mdiWeatherHail,
|
||||
lightning: mdiWeatherLightning,
|
||||
"lightning-rainy": mdiWeatherLightningRainy,
|
||||
partlycloudy: mdiWeatherPartlyCloudy,
|
||||
pouring: mdiWeatherPouring,
|
||||
rainy: mdiWeatherRainy,
|
||||
snowy: mdiWeatherSnowy,
|
||||
"snowy-rainy": mdiWeatherSnowyRainy,
|
||||
sunny: mdiWeatherSunny,
|
||||
windy: mdiWeatherWindy,
|
||||
"windy-variant": mdiWeatherWindyVariant,
|
||||
};
|
||||
|
||||
export const weatherAttrIcons = {
|
||||
@ -437,6 +463,13 @@ export const getWeatherStateIcon = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const weatherIcon = (state?: string, nightTime?: boolean): string =>
|
||||
!state
|
||||
? undefined
|
||||
: nightTime && state === "partlycloudy"
|
||||
? mdiWeatherNightPartlyCloudy
|
||||
: weatherIcons[state];
|
||||
|
||||
const DAY_IN_MILLISECONDS = 86400000;
|
||||
|
||||
export const isForecastHourly = (
|
||||
|
@ -145,7 +145,7 @@ export interface ZWaveJSController {
|
||||
supports_timers: boolean;
|
||||
is_heal_network_active: boolean;
|
||||
inclusion_state: InclusionState;
|
||||
nodes: number[];
|
||||
nodes: ZWaveJSNodeStatus[];
|
||||
}
|
||||
|
||||
export interface ZWaveJSNodeStatus {
|
||||
@ -167,6 +167,9 @@ export interface ZwaveJSNodeMetadata {
|
||||
wakeup: string;
|
||||
reset: string;
|
||||
device_database_url: string;
|
||||
}
|
||||
|
||||
export interface ZwaveJSNodeComments {
|
||||
comments: ZWaveJSNodeComment[];
|
||||
}
|
||||
|
||||
@ -200,8 +203,7 @@ export interface ZWaveJSNodeConfigParamMetadata {
|
||||
|
||||
export interface ZWaveJSSetConfigParamData {
|
||||
type: string;
|
||||
entry_id: string;
|
||||
node_id: number;
|
||||
device_id: string;
|
||||
property: number;
|
||||
property_key?: number;
|
||||
value: string | number;
|
||||
@ -228,6 +230,20 @@ export interface ZWaveJSHealNetworkStatusMessage {
|
||||
heal_node_status: { [key: number]: string };
|
||||
}
|
||||
|
||||
export interface ZWaveJSControllerStatisticsUpdatedMessage {
|
||||
event: "statistics updated";
|
||||
source: "controller";
|
||||
messages_tx: number;
|
||||
messages_rx: number;
|
||||
messages_dropped_tx: number;
|
||||
messages_dropped_rx: number;
|
||||
nak: number;
|
||||
can: number;
|
||||
timeout_ack: number;
|
||||
timeout_response: number;
|
||||
timeout_callback: number;
|
||||
}
|
||||
|
||||
export interface ZWaveJSRemovedNode {
|
||||
node_id: number;
|
||||
manufacturer: string;
|
||||
@ -285,12 +301,23 @@ export const migrateZwave = (
|
||||
|
||||
export const fetchZwaveNetworkStatus = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string
|
||||
): Promise<ZWaveJSNetwork> =>
|
||||
hass.callWS({
|
||||
device_or_entry_id: {
|
||||
device_id?: string;
|
||||
entry_id?: string;
|
||||
}
|
||||
): Promise<ZWaveJSNetwork> => {
|
||||
if (device_or_entry_id.device_id && device_or_entry_id.entry_id) {
|
||||
throw new Error("Only one of device or entry ID should be supplied.");
|
||||
}
|
||||
if (!device_or_entry_id.device_id && !device_or_entry_id.entry_id) {
|
||||
throw new Error("Either device or entry ID should be supplied.");
|
||||
}
|
||||
return hass.callWS({
|
||||
type: "zwave_js/network_status",
|
||||
entry_id,
|
||||
device_id: device_or_entry_id.device_id,
|
||||
entry_id: device_or_entry_id.entry_id,
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchZwaveDataCollectionStatus = (
|
||||
hass: HomeAssistant,
|
||||
@ -427,49 +454,50 @@ export const unprovisionZwaveSmartStartNode = (
|
||||
|
||||
export const fetchZwaveNodeStatus = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number
|
||||
device_id: string
|
||||
): Promise<ZWaveJSNodeStatus> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/node_status",
|
||||
entry_id,
|
||||
node_id,
|
||||
device_id,
|
||||
});
|
||||
|
||||
export const fetchZwaveNodeMetadata = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number
|
||||
device_id: string
|
||||
): Promise<ZwaveJSNodeMetadata> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/node_metadata",
|
||||
entry_id,
|
||||
node_id,
|
||||
device_id,
|
||||
});
|
||||
|
||||
export const fetchZwaveNodeComments = (
|
||||
hass: HomeAssistant,
|
||||
device_id: string
|
||||
): Promise<ZwaveJSNodeComments> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/node_comments",
|
||||
device_id,
|
||||
});
|
||||
|
||||
export const fetchZwaveNodeConfigParameters = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number
|
||||
device_id: string
|
||||
): Promise<ZWaveJSNodeConfigParams> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/get_config_parameters",
|
||||
entry_id,
|
||||
node_id,
|
||||
device_id,
|
||||
});
|
||||
|
||||
export const setZwaveNodeConfigParameter = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number,
|
||||
device_id: string,
|
||||
property: number,
|
||||
value: number,
|
||||
property_key?: number
|
||||
): Promise<ZWaveJSSetConfigParamResult> => {
|
||||
const data: ZWaveJSSetConfigParamData = {
|
||||
type: "zwave_js/set_config_parameter",
|
||||
entry_id,
|
||||
node_id,
|
||||
device_id,
|
||||
property,
|
||||
value,
|
||||
property_key,
|
||||
@ -479,42 +507,36 @@ export const setZwaveNodeConfigParameter = (
|
||||
|
||||
export const reinterviewZwaveNode = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number,
|
||||
device_id: string,
|
||||
callbackFunction: (message: ZWaveJSRefreshNodeStatusMessage) => void
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
hass.connection.subscribeMessage(
|
||||
(message: any) => callbackFunction(message),
|
||||
{
|
||||
type: "zwave_js/refresh_node_info",
|
||||
entry_id,
|
||||
node_id,
|
||||
device_id,
|
||||
}
|
||||
);
|
||||
|
||||
export const healZwaveNode = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number
|
||||
device_id: string
|
||||
): Promise<boolean> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/heal_node",
|
||||
entry_id,
|
||||
node_id,
|
||||
device_id,
|
||||
});
|
||||
|
||||
export const removeFailedZwaveNode = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number,
|
||||
device_id: string,
|
||||
callbackFunction: (message: any) => void
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
hass.connection.subscribeMessage(
|
||||
(message: any) => callbackFunction(message),
|
||||
{
|
||||
type: "zwave_js/remove_failed_node",
|
||||
entry_id,
|
||||
node_id,
|
||||
device_id,
|
||||
}
|
||||
);
|
||||
|
||||
@ -538,16 +560,14 @@ export const stopHealZwaveNetwork = (
|
||||
|
||||
export const subscribeZwaveNodeReady = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number,
|
||||
device_id: string,
|
||||
callbackFunction: (message) => void
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
hass.connection.subscribeMessage(
|
||||
(message: any) => callbackFunction(message),
|
||||
{
|
||||
type: "zwave_js/node_ready",
|
||||
entry_id,
|
||||
node_id,
|
||||
device_id,
|
||||
}
|
||||
);
|
||||
|
||||
@ -564,6 +584,19 @@ export const subscribeHealZwaveNetworkProgress = (
|
||||
}
|
||||
);
|
||||
|
||||
export const subscribeZwaveControllerStatistics = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
callbackFunction: (message: ZWaveJSControllerStatisticsUpdatedMessage) => void
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
hass.connection.subscribeMessage(
|
||||
(message: any) => callbackFunction(message),
|
||||
{
|
||||
type: "zwave_js/subscribe_controller_statistics",
|
||||
entry_id,
|
||||
}
|
||||
);
|
||||
|
||||
export const getZwaveJsIdentifiersFromDevice = (
|
||||
device: DeviceRegistryEntry
|
||||
): ZWaveJSNodeIdentifiers | undefined => {
|
||||
|
@ -309,7 +309,7 @@ class DataEntryFlowDialog extends LitElement {
|
||||
: this._step.type === "abort"
|
||||
? html`
|
||||
<step-flow-abort
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.params=${this._params}
|
||||
.step=${this._step}
|
||||
.hass=${this.hass}
|
||||
.domain=${this._step.handler}
|
||||
@ -518,10 +518,9 @@ class DataEntryFlowDialog extends LitElement {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
:host-context([style*="direction: rtl;"]) .dialog-actions {
|
||||
right: auto;
|
||||
left: 0;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 0px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.dialog-actions > * {
|
||||
color: var(--secondary-text-color);
|
||||
|
@ -131,6 +131,7 @@ export interface DataEntryFlowDialogParams {
|
||||
}) => void;
|
||||
flowConfig: FlowConfig;
|
||||
showAdvanced?: boolean;
|
||||
dialogParentElement?: HTMLElement;
|
||||
}
|
||||
|
||||
export const loadDataEntryFlowDialog = () => import("./dialog-data-entry-flow");
|
||||
@ -146,6 +147,7 @@ export const showFlowDialog = (
|
||||
dialogParams: {
|
||||
...dialogParams,
|
||||
flowConfig,
|
||||
dialogParentElement: element,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,15 +1,25 @@
|
||||
import "@material/mwc-button";
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { DataEntryFlowStepAbort } from "../../data/data_entry_flow";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { FlowConfig } from "./show-dialog-data-entry-flow";
|
||||
import { showAddApplicationCredentialDialog } from "../../panels/config/application_credentials/show-dialog-add-application-credential";
|
||||
import { configFlowContentStyles } from "./styles";
|
||||
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow";
|
||||
import { showConfigFlowDialog } from "./show-dialog-config-flow";
|
||||
|
||||
@customElement("step-flow-abort")
|
||||
class StepFlowAbort extends LitElement {
|
||||
@property({ attribute: false }) public flowConfig!: FlowConfig;
|
||||
@property({ attribute: false }) public params!: DataEntryFlowDialogParams;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@ -17,11 +27,21 @@ class StepFlowAbort extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public domain!: string;
|
||||
|
||||
protected firstUpdated(changed: PropertyValues) {
|
||||
super.firstUpdated(changed);
|
||||
if (this.step.reason === "missing_credentials") {
|
||||
this._handleMissingCreds();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this.step.reason === "missing_credentials") {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<h2>${this.hass.localize(`component.${this.domain}.title`)}</h2>
|
||||
<div class="content">
|
||||
${this.flowConfig.renderAbortDescription(this.hass, this.step)}
|
||||
${this.params.flowConfig.renderAbortDescription(this.hass, this.step)}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<mwc-button @click=${this._flowDone}
|
||||
@ -33,6 +53,32 @@ class StepFlowAbort extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _handleMissingCreds() {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.missing_credentials",
|
||||
{
|
||||
integration: domainToName(this.hass.localize, this.domain),
|
||||
}
|
||||
),
|
||||
});
|
||||
this._flowDone();
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
// Prompt to enter credentials and restart integration setup
|
||||
showAddApplicationCredentialDialog(this.params.dialogParentElement!, {
|
||||
selectedDomain: this.domain,
|
||||
applicationCredentialAddedCallback: () => {
|
||||
showConfigFlowDialog(this.params.dialogParentElement!, {
|
||||
dialogClosedCallback: this.params.dialogClosedCallback,
|
||||
startFlowHandler: this.domain,
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _flowDone(): void {
|
||||
fireEvent(this, "flow-update", { step: undefined });
|
||||
}
|
||||
|
@ -192,11 +192,8 @@ class StepFlowForm extends LitElement {
|
||||
}
|
||||
h2 {
|
||||
word-break: break-word;
|
||||
padding-right: 72px;
|
||||
}
|
||||
:host-context([style*="direction: rtl;"]) h2 {
|
||||
padding-right: auto !important;
|
||||
padding-left: 72px !important;
|
||||
padding-inline-end: 72px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@ -104,11 +104,8 @@ class StepFlowPickFlow extends LitElement {
|
||||
margin: 16px 0;
|
||||
}
|
||||
h2 {
|
||||
padding-right: 66px;
|
||||
}
|
||||
:host-context([style*="direction: rtl;"]) h2 {
|
||||
padding-right: auto !important;
|
||||
padding-left: 66px !important;
|
||||
padding-inline-end: 66px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
@media all and (max-height: 900px) {
|
||||
div {
|
||||
|
@ -311,11 +311,8 @@ class StepFlowPickHandler extends LitElement {
|
||||
border-bottom-color: var(--divider-color);
|
||||
}
|
||||
h2 {
|
||||
padding-right: 66px;
|
||||
}
|
||||
:host-context([style*="direction: rtl;"]) h2 {
|
||||
padding-right: auto !important;
|
||||
padding-left: 66px !important;
|
||||
padding-inline-end: 66px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
@media all and (max-height: 900px) {
|
||||
mwc-list {
|
||||
|
@ -3,7 +3,11 @@ import { css } from "lit";
|
||||
export const configFlowContentStyles = css`
|
||||
h2 {
|
||||
margin: 24px 38px 0 0;
|
||||
margin-inline-start: 0px;
|
||||
margin-inline-end: 38px;
|
||||
padding: 0 24px;
|
||||
padding-inline-start: 24px;
|
||||
padding-inline-end: 24px;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-family: var(
|
||||
|
@ -1,12 +1,13 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiAlertOutline } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/ha-switch";
|
||||
import "../../components/ha-textfield";
|
||||
import { HaTextField } from "../../components/ha-textfield";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { DialogBoxParams } from "./show-dialog-box";
|
||||
@ -17,13 +18,10 @@ class DialogBox extends LitElement {
|
||||
|
||||
@state() private _params?: DialogBoxParams;
|
||||
|
||||
@state() private _value?: string;
|
||||
@query("ha-textfield") private _textField?: HaTextField;
|
||||
|
||||
public async showDialog(params: DialogBoxParams): Promise<void> {
|
||||
this._params = params;
|
||||
if (params.prompt) {
|
||||
this._value = params.defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
@ -75,9 +73,7 @@ class DialogBox extends LitElement {
|
||||
? html`
|
||||
<ha-textfield
|
||||
dialogInitialFocus
|
||||
.value=${this._value || ""}
|
||||
@keyup=${this._handleKeyUp}
|
||||
@input=${this._valueChanged}
|
||||
value=${ifDefined(this._params.defaultValue)}
|
||||
.label=${this._params.inputLabel
|
||||
? this._params.inputLabel
|
||||
: ""}
|
||||
@ -109,10 +105,6 @@ class DialogBox extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev) {
|
||||
this._value = ev.target.value;
|
||||
}
|
||||
|
||||
private _dismiss(): void {
|
||||
if (this._params?.cancel) {
|
||||
this._params.cancel();
|
||||
@ -120,15 +112,9 @@ class DialogBox extends LitElement {
|
||||
this._close();
|
||||
}
|
||||
|
||||
private _handleKeyUp(ev: KeyboardEvent) {
|
||||
if (ev.keyCode === 13) {
|
||||
this._confirm();
|
||||
}
|
||||
}
|
||||
|
||||
private _confirm(): void {
|
||||
if (this._params!.confirm) {
|
||||
this._params!.confirm(this._value);
|
||||
this._params!.confirm(this._textField?.value);
|
||||
}
|
||||
this._close();
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { HASSDomEvent, ValidHassDomEvent } from "../common/dom/fire_event";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
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 {
|
||||
// for fire event
|
||||
@ -40,7 +43,17 @@ export interface DialogState {
|
||||
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 (
|
||||
element: HTMLElement & ProvideHassElement,
|
||||
@ -60,11 +73,25 @@ export const showDialog = async (
|
||||
}
|
||||
return;
|
||||
}
|
||||
LOADED[dialogTag] = dialogImport().then(() => {
|
||||
const dialogEl = document.createElement(dialogTag) as HassDialog;
|
||||
element.provideHass(dialogEl);
|
||||
return dialogEl;
|
||||
});
|
||||
LOADED[dialogTag] = {
|
||||
element: dialogImport().then(() => {
|
||||
const dialogEl = document.createElement(dialogTag) as HassDialog;
|
||||
element.provideHass(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) {
|
||||
@ -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,
|
||||
// so it's guaranteed to be on top of the other elements
|
||||
root.appendChild(dialogElement);
|
||||
dialogElement.showDialog(dialogParams);
|
||||
};
|
||||
|
||||
export const replaceDialog = () => {
|
||||
export const replaceDialog = (dialogElement: HassDialog) => {
|
||||
mainWindow.history.replaceState(
|
||||
{ ...mainWindow.history.state, replaced: true },
|
||||
""
|
||||
);
|
||||
dialogElement.removeEventListener("dialog-closed", _handleClosedFocus);
|
||||
};
|
||||
|
||||
export const closeDialog = async (dialogTag: string): Promise<boolean> => {
|
||||
if (!(dialogTag in LOADED)) {
|
||||
return true;
|
||||
}
|
||||
const dialogElement: HassDialog = await LOADED[dialogTag];
|
||||
const dialogElement = await LOADED[dialogTag].element;
|
||||
if (dialogElement.closeDialog) {
|
||||
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
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -1,35 +1,14 @@
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-camera-stream";
|
||||
import type { HaCheckbox } from "../../../components/ha-checkbox";
|
||||
import "../../../components/ha-checkbox";
|
||||
import {
|
||||
CameraEntity,
|
||||
CameraPreferences,
|
||||
CAMERA_SUPPORT_STREAM,
|
||||
fetchCameraPrefs,
|
||||
STREAM_TYPE_HLS,
|
||||
updateCameraPrefs,
|
||||
} from "../../../data/camera";
|
||||
import { CameraEntity } from "../../../data/camera";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../../../components/ha-formfield";
|
||||
|
||||
class MoreInfoCamera extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: CameraEntity;
|
||||
|
||||
@state() private _cameraPrefs?: CameraPreferences;
|
||||
|
||||
@state() private _attached = false;
|
||||
|
||||
public connectedCallback() {
|
||||
@ -54,83 +33,13 @@ class MoreInfoCamera extends LitElement {
|
||||
allow-exoplayer
|
||||
controls
|
||||
></ha-camera-stream>
|
||||
${this._cameraPrefs
|
||||
? html`
|
||||
<ha-formfield label="Preload stream">
|
||||
<ha-checkbox
|
||||
.checked=${this._cameraPrefs.preload_stream}
|
||||
@change=${this._handleCheckboxChanged}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>
|
||||
`
|
||||
: undefined}
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (!changedProps.has("stateObj")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldState = changedProps.get("stateObj") as this["stateObj"];
|
||||
const oldEntityId = oldState ? oldState.entity_id : undefined;
|
||||
const curEntityId = this.stateObj ? this.stateObj.entity_id : undefined;
|
||||
|
||||
// Same entity, ignore.
|
||||
if (curEntityId === oldEntityId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
curEntityId &&
|
||||
isComponentLoaded(this.hass!, "stream") &&
|
||||
supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM) &&
|
||||
// The stream component for HLS streams supports a server-side pre-load
|
||||
// option that client initiated WebRTC streams do not
|
||||
this.stateObj!.attributes.frontend_stream_type === STREAM_TYPE_HLS
|
||||
) {
|
||||
// Fetch in background while we set up the video.
|
||||
this._fetchCameraPrefs();
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchCameraPrefs() {
|
||||
this._cameraPrefs = await fetchCameraPrefs(
|
||||
this.hass!,
|
||||
this.stateObj!.entity_id
|
||||
);
|
||||
}
|
||||
|
||||
private async _handleCheckboxChanged(ev) {
|
||||
const checkbox = ev.currentTarget as HaCheckbox;
|
||||
try {
|
||||
this._cameraPrefs = await updateCameraPrefs(
|
||||
this.hass!,
|
||||
this.stateObj!.entity_id,
|
||||
{
|
||||
preload_stream: checkbox.checked!,
|
||||
}
|
||||
);
|
||||
} catch (err: any) {
|
||||
alert(err.message);
|
||||
checkbox.checked = !checkbox.checked;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
ha-formfield {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background-color: var(--secondary-background-color);
|
||||
padding-right: 16px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -211,6 +211,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
--mdc-theme-primary: currentColor;
|
||||
}
|
||||
|
@ -1,23 +1,9 @@
|
||||
import {
|
||||
mdiAlertCircleOutline,
|
||||
mdiEye,
|
||||
mdiGauge,
|
||||
mdiThermometer,
|
||||
mdiWaterPercent,
|
||||
mdiWeatherCloudy,
|
||||
mdiWeatherFog,
|
||||
mdiWeatherHail,
|
||||
mdiWeatherLightning,
|
||||
mdiWeatherLightningRainy,
|
||||
mdiWeatherNight,
|
||||
mdiWeatherPartlyCloudy,
|
||||
mdiWeatherPouring,
|
||||
mdiWeatherRainy,
|
||||
mdiWeatherSnowy,
|
||||
mdiWeatherSnowyRainy,
|
||||
mdiWeatherSunny,
|
||||
mdiWeatherWindy,
|
||||
mdiWeatherWindyVariant,
|
||||
} from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
@ -37,27 +23,10 @@ import {
|
||||
getWeatherUnit,
|
||||
getWind,
|
||||
isForecastHourly,
|
||||
weatherIcons,
|
||||
} from "../../../data/weather";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
const weatherIcons = {
|
||||
"clear-night": mdiWeatherNight,
|
||||
cloudy: mdiWeatherCloudy,
|
||||
exceptional: mdiAlertCircleOutline,
|
||||
fog: mdiWeatherFog,
|
||||
hail: mdiWeatherHail,
|
||||
lightning: mdiWeatherLightning,
|
||||
"lightning-rainy": mdiWeatherLightningRainy,
|
||||
partlycloudy: mdiWeatherPartlyCloudy,
|
||||
pouring: mdiWeatherPouring,
|
||||
rainy: mdiWeatherRainy,
|
||||
snowy: mdiWeatherSnowy,
|
||||
"snowy-rainy": mdiWeatherSnowyRainy,
|
||||
sunny: mdiWeatherSunny,
|
||||
windy: mdiWeatherWindy,
|
||||
"windy-variant": mdiWeatherWindyVariant,
|
||||
};
|
||||
|
||||
@customElement("more-info-weather")
|
||||
class MoreInfoWeather extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@ -235,6 +204,7 @@ class MoreInfoWeather extends LitElement {
|
||||
return css`
|
||||
ha-svg-icon {
|
||||
color: var(--paper-item-icon-color);
|
||||
margin-left: 8px;
|
||||
}
|
||||
.section {
|
||||
margin: 16px 0 8px 0;
|
||||
|
@ -295,7 +295,7 @@ export class MoreInfoDialog extends LitElement {
|
||||
}
|
||||
|
||||
private _gotoSettings() {
|
||||
replaceDialog();
|
||||
replaceDialog(this);
|
||||
showEntityEditorDialog(this, {
|
||||
entity_id: this._entityId!,
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { startOfYesterday } from "date-fns";
|
||||
import { startOfYesterday } from "date-fns/esm";
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
|
@ -1,17 +1,11 @@
|
||||
import { startOfYesterday } from "date-fns";
|
||||
import { startOfYesterday } from "date-fns/esm";
|
||||
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 { 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 { haStyle } from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-more-info-logbook")
|
||||
export class MoreInfoLogbook extends LitElement {
|
||||
@ -19,26 +13,14 @@ export class MoreInfoLogbook extends LitElement {
|
||||
|
||||
@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 _throttleGetLogbookEntries = throttle(() => {
|
||||
this._getLogBookData();
|
||||
}, 10000);
|
||||
private _time = { recent: 86400 };
|
||||
|
||||
private _entityIdAsList = memoizeOne((entityId: string) => [entityId]);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.entityId) {
|
||||
if (!isComponentLoaded(this.hass, "logbook") || !this.entityId) {
|
||||
return html``;
|
||||
}
|
||||
const stateObj = this.hass.states[this.entityId];
|
||||
@ -48,150 +30,34 @@ export class MoreInfoLogbook extends LitElement {
|
||||
}
|
||||
|
||||
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="title">
|
||||
${this.hass.localize("ui.dialogs.more_info_control.logbook")}
|
||||
</div>
|
||||
<a href=${this._showMoreHref} @click=${this._close}
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.show_more"
|
||||
)}</a
|
||||
>
|
||||
</div>
|
||||
<ha-logbook
|
||||
narrow
|
||||
no-icon
|
||||
no-name
|
||||
relative-time
|
||||
.hass=${this.hass}
|
||||
.entries=${this._logbookEntries}
|
||||
.traceContexts=${this._traceContexts}
|
||||
.userIdToName=${this._userIdToName}
|
||||
></ha-logbook>
|
||||
`
|
||||
: html`<div class="no-entries">
|
||||
${this.hass.localize("ui.components.logbook.entries_not_found")}
|
||||
</div>`
|
||||
: ""}
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
${this.hass.localize("ui.dialogs.more_info_control.logbook")}
|
||||
</div>
|
||||
<a href=${this._showMoreHref} @click=${this._close}
|
||||
>${this.hass.localize("ui.dialogs.more_info_control.show_more")}</a
|
||||
>
|
||||
</div>
|
||||
<ha-logbook
|
||||
.hass=${this.hass}
|
||||
.time=${this._time}
|
||||
.entityIds=${this._entityIdAsList(this.entityId)}
|
||||
narrow
|
||||
no-icon
|
||||
no-name
|
||||
relative-time
|
||||
></ha-logbook>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._fetchUserPromise = this._fetchUserNames();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (changedProps.has("entityId")) {
|
||||
this._lastLogbookDate = undefined;
|
||||
this._logbookEntries = undefined;
|
||||
|
||||
if (!this.entityId) {
|
||||
return;
|
||||
}
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (changedProps.has("entityId") && this.entityId) {
|
||||
this._showMoreHref = `/logbook?entity_id=${
|
||||
this.entityId
|
||||
}&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 {
|
||||
@ -200,13 +66,7 @@ export class MoreInfoLogbook extends LitElement {
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.no-entries {
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-logbook {
|
||||
--logbook-max-height: 250px;
|
||||
}
|
||||
@ -215,10 +75,6 @@ export class MoreInfoLogbook extends LitElement {
|
||||
--logbook-max-height: unset;
|
||||
}
|
||||
}
|
||||
ha-circular-progress {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -49,12 +49,14 @@ class OnboardingIntegrations extends LitElement {
|
||||
this.hass.loadBackendTranslation("title", undefined, true);
|
||||
this._unsubEvents = subscribeConfigFlowInProgress(this.hass, (flows) => {
|
||||
this._discovered = flows;
|
||||
const integrations: Set<string> = new Set();
|
||||
for (const flow of flows) {
|
||||
// To render title placeholders
|
||||
if (flow.context.title_placeholders) {
|
||||
this.hass.loadBackendTranslation("config", flow.handler);
|
||||
integrations.add(flow.handler);
|
||||
}
|
||||
}
|
||||
this.hass.loadBackendTranslation("config", Array.from(integrations));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -336,6 +336,9 @@ export class HAFullCalendar extends LitElement {
|
||||
|
||||
.today {
|
||||
margin-right: 20px;
|
||||
margin-inline-end: 20px;
|
||||
margin-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.prev,
|
||||
|
@ -194,10 +194,13 @@ class PanelCalendar extends LitElement {
|
||||
|
||||
.calendar-list {
|
||||
padding-right: 16px;
|
||||
padding-inline-end: 16px;
|
||||
padding-inline-start: initial;
|
||||
min-width: 170px;
|
||||
flex: 0 0 15%;
|
||||
overflow: hidden;
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.calendar-list > div {
|
||||
|
@ -0,0 +1,246 @@
|
||||
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 { 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 =
|
||||
params.selectedDomain !== undefined ? params.selectedDomain : "";
|
||||
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._abortDialog}
|
||||
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}
|
||||
.disabled=${!!this._params.selectedDomain}
|
||||
.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: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._domain = ev.detail.value;
|
||||
}
|
||||
|
||||
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 _abortDialog() {
|
||||
if (this._params && this._params.dialogAbortedCallback) {
|
||||
this._params.dialogAbortedCallback();
|
||||
}
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,285 @@
|
||||
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;
|
||||
padding-inline-start: 16px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { ApplicationCredential } from "../../../data/application_credential";
|
||||
|
||||
export interface AddApplicationCredentialDialogParams {
|
||||
applicationCredentialAddedCallback: (
|
||||
applicationCredential: ApplicationCredential
|
||||
) => void;
|
||||
dialogAbortedCallback?: () => void;
|
||||
selectedDomain?: string;
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
};
|
@ -2,7 +2,10 @@ import "@material/mwc-button";
|
||||
import { mdiImagePlus, mdiPencil } from "@mdi/js";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
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 { customElement, property, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
@ -19,6 +22,7 @@ import "../../../components/ha-icon-next";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
deleteAreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
updateAreaRegistryEntry,
|
||||
} from "../../../data/area_registry";
|
||||
import { AutomationEntity } from "../../../data/automation";
|
||||
@ -26,18 +30,22 @@ import {
|
||||
computeDeviceName,
|
||||
DeviceRegistryEntry,
|
||||
sortDeviceRegistryByName,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../../data/device_registry";
|
||||
import {
|
||||
computeEntityRegistryName,
|
||||
EntityRegistryEntry,
|
||||
sortEntityRegistryByName,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../../data/entity_registry";
|
||||
import { SceneEntity } from "../../../data/scene";
|
||||
import { ScriptEntity } from "../../../data/script";
|
||||
import { findRelated, RelatedResult } from "../../../data/search";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import "../../logbook/ha-logbook";
|
||||
import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import {
|
||||
@ -51,17 +59,11 @@ declare type NameAndEntity<EntityType extends HassEntity> = {
|
||||
};
|
||||
|
||||
@customElement("ha-config-area-page")
|
||||
class HaConfigAreaPage extends LitElement {
|
||||
class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@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() public isWide!: boolean;
|
||||
@ -70,8 +72,16 @@ class HaConfigAreaPage extends LitElement {
|
||||
|
||||
@property() public route!: Route;
|
||||
|
||||
@state() public _areas!: AreaRegistryEntry[];
|
||||
|
||||
@state() public _devices!: DeviceRegistryEntry[];
|
||||
|
||||
@state() public _entities!: EntityRegistryEntry[];
|
||||
|
||||
@state() private _related?: RelatedResult;
|
||||
|
||||
private _logbookTime = { recent: 86400 };
|
||||
|
||||
private _area = memoizeOne(
|
||||
(
|
||||
areaId: string,
|
||||
@ -86,7 +96,7 @@ class HaConfigAreaPage extends LitElement {
|
||||
registryDevices: DeviceRegistryEntry[],
|
||||
registryEntities: EntityRegistryEntry[]
|
||||
) => {
|
||||
const devices = new Map();
|
||||
const devices = new Map<string, DeviceRegistryEntry>();
|
||||
|
||||
for (const device of registryDevices) {
|
||||
if (device.area_id === areaId) {
|
||||
@ -102,7 +112,7 @@ class HaConfigAreaPage extends LitElement {
|
||||
if (entity.area_id === areaId) {
|
||||
entities.push(entity);
|
||||
}
|
||||
} else if (devices.has(entity.device_id)) {
|
||||
} else if (entity.device_id && devices.has(entity.device_id)) {
|
||||
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) {
|
||||
super.firstUpdated(changedProps);
|
||||
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 {
|
||||
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) {
|
||||
return html`
|
||||
@ -139,11 +181,12 @@ class HaConfigAreaPage extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
const { devices, entities } = this._memberships(
|
||||
const memberships = this._memberships(
|
||||
this.areaId,
|
||||
this.devices,
|
||||
this.entities
|
||||
this._devices,
|
||||
this._entities
|
||||
);
|
||||
const { devices, entities } = memberships;
|
||||
|
||||
// Pre-compute the entity and device names, so we can sort by them
|
||||
if (devices) {
|
||||
@ -359,8 +402,6 @@ class HaConfigAreaPage extends LitElement {
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="column">
|
||||
${isComponentLoaded(this.hass, "scene")
|
||||
? html`
|
||||
<ha-card
|
||||
@ -442,6 +483,26 @@ class HaConfigAreaPage extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
</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>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
@ -699,6 +760,13 @@ class HaConfigAreaPage extends LitElement {
|
||||
opacity: 0.5;
|
||||
border-radius: 50%;
|
||||
}
|
||||
ha-logbook {
|
||||
height: 400px;
|
||||
}
|
||||
:host([narrow]) ha-logbook {
|
||||
height: 235px;
|
||||
overflow: auto;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { mdiHelpCircle, mdiPlus } from "@mdi/js";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
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 memoizeOne from "memoize-one";
|
||||
import "../../../components/ha-fab";
|
||||
@ -9,12 +10,20 @@ import "../../../components/ha-svg-icon";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
createAreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
} from "../../../data/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../../data/entity_registry";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import "../ha-config-section";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
@ -24,7 +33,7 @@ import {
|
||||
} from "./show-dialog-area-registry-detail";
|
||||
|
||||
@customElement("ha-config-areas-dashboard")
|
||||
export class HaConfigAreasDashboard extends LitElement {
|
||||
export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public isWide?: boolean;
|
||||
@ -33,13 +42,13 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
|
||||
@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[],
|
||||
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 {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
@ -92,56 +115,62 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
@click=${this._showHelp}
|
||||
></ha-icon-button>
|
||||
<div class="container">
|
||||
${this._areas(this.areas, this.devices, this.entities).map(
|
||||
(area) =>
|
||||
html`<a href=${`/config/areas/area/${area.area_id}`}
|
||||
><ha-card outlined>
|
||||
<div
|
||||
style=${styleMap({
|
||||
backgroundImage: area.picture
|
||||
? `url(${area.picture})`
|
||||
: undefined,
|
||||
})}
|
||||
class="picture ${!area.picture ? "placeholder" : ""}"
|
||||
></div>
|
||||
<h1 class="card-header">${area.name}</h1>
|
||||
<div class="card-content">
|
||||
<div>
|
||||
${area.devices
|
||||
? html`
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.devices",
|
||||
"count",
|
||||
area.devices
|
||||
)}${area.services ? "," : ""}
|
||||
`
|
||||
: ""}
|
||||
${area.services
|
||||
? html`
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.services",
|
||||
"count",
|
||||
area.services
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
${(area.devices || area.services) && area.entities
|
||||
? this.hass.localize("ui.common.and")
|
||||
: ""}
|
||||
${area.entities
|
||||
? html`
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.entities",
|
||||
"count",
|
||||
area.entities
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card></a
|
||||
>`
|
||||
)}
|
||||
${!this._areas || !this._devices || !this._entities
|
||||
? ""
|
||||
: this._processAreas(
|
||||
this._areas,
|
||||
this._devices,
|
||||
this._entities
|
||||
).map(
|
||||
(area) =>
|
||||
html`<a href=${`/config/areas/area/${area.area_id}`}
|
||||
><ha-card outlined>
|
||||
<div
|
||||
style=${styleMap({
|
||||
backgroundImage: area.picture
|
||||
? `url(${area.picture})`
|
||||
: undefined,
|
||||
})}
|
||||
class="picture ${!area.picture ? "placeholder" : ""}"
|
||||
></div>
|
||||
<h1 class="card-header">${area.name}</h1>
|
||||
<div class="card-content">
|
||||
<div>
|
||||
${area.devices
|
||||
? html`
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.devices",
|
||||
"count",
|
||||
area.devices
|
||||
)}${area.services ? "," : ""}
|
||||
`
|
||||
: ""}
|
||||
${area.services
|
||||
? html`
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.services",
|
||||
"count",
|
||||
area.services
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
${(area.devices || area.services) && area.entities
|
||||
? this.hass.localize("ui.common.and")
|
||||
: ""}
|
||||
${area.entities
|
||||
? html`
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.entities",
|
||||
"count",
|
||||
area.entities
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card></a
|
||||
>`
|
||||
)}
|
||||
</div>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
|
@ -1,20 +1,4 @@
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
} from "../../../data/area_registry";
|
||||
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../../data/entity_registry";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import {
|
||||
HassRouterPage,
|
||||
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) {
|
||||
pageEl.hass = this.hass;
|
||||
|
||||
@ -91,37 +37,11 @@ class HaConfigAreas extends HassRouterPage {
|
||||
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.isWide = this.isWide;
|
||||
pageEl.showAdvanced = this.showAdvanced;
|
||||
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 {
|
||||
|
@ -460,17 +460,13 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
border-top-left-radius: var(--ha-card-border-radius);
|
||||
}
|
||||
.card-menu {
|
||||
float: right;
|
||||
float: var(--float-end, right);
|
||||
z-index: 3;
|
||||
margin: 4px;
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
:host-context([style*="direction: rtl;"]) .card-menu {
|
||||
right: initial;
|
||||
left: 16px;
|
||||
}
|
||||
mwc-list-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
|
@ -288,16 +288,13 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
border-top-left-radius: var(--ha-card-border-radius);
|
||||
}
|
||||
.card-menu {
|
||||
float: right;
|
||||
float: var(--float-end, right);
|
||||
z-index: 3;
|
||||
margin: 4px;
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
:host-context([style*="direction: rtl;"]) .card-menu {
|
||||
float: left;
|
||||
}
|
||||
mwc-list-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
|
@ -487,16 +487,13 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
border-top-left-radius: var(--ha-card-border-radius);
|
||||
}
|
||||
.card-menu {
|
||||
float: right;
|
||||
float: var(--float-end, right);
|
||||
z-index: 3;
|
||||
margin: 4px;
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
:host-context([style*="direction: rtl;"]) .card-menu {
|
||||
float: left;
|
||||
}
|
||||
.triggered {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
|
@ -5,7 +5,9 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import type { CalendarTrigger } from "../../../../../data/automation";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
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 { createDurationData } from "../../../../../common/datetime/create_duration_data";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
|
||||
@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() {
|
||||
return {
|
||||
event: "start" as CalendarTrigger["event"],
|
||||
offset: 0,
|
||||
};
|
||||
}
|
||||
|
||||
protected render() {
|
||||
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`
|
||||
<ha-form
|
||||
.schema=${schema}
|
||||
.data=${this.trigger}
|
||||
.data=${data}
|
||||
.hass=${this.hass}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
@ -62,7 +101,14 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement {
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
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 });
|
||||
}
|
||||
|
||||
|
@ -88,7 +88,10 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
|
||||
>
|
||||
<div class="account-row">
|
||||
<paper-item-body two-line>
|
||||
${this.cloudStatus.email}
|
||||
${this.cloudStatus.email.replace(
|
||||
/(\w{3})[\w.-]+@([\w.]+\w)/,
|
||||
"$1***@$2"
|
||||
)}
|
||||
<div secondary class="wrap">
|
||||
${this._subscription
|
||||
? this._subscription.human_description.replace(
|
||||
|
@ -1,9 +1,11 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiContentCopy } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
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-card";
|
||||
import "../../../../components/ha-switch";
|
||||
// eslint-disable-next-line
|
||||
import type { HaSwitch } from "../../../../components/ha-switch";
|
||||
@ -13,6 +15,7 @@ import {
|
||||
disconnectCloudRemote,
|
||||
} from "../../../../data/cloud";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import { showCloudCertificateDialog } from "../dialog-cloud-certificate/show-dialog-cloud-certificate";
|
||||
|
||||
@customElement("cloud-remote-pref")
|
||||
@ -84,9 +87,15 @@ export class CloudRemotePref extends LitElement {
|
||||
target="_blank"
|
||||
class="break-word"
|
||||
rel="noreferrer"
|
||||
>
|
||||
https://${remote_domain}</a
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.nabu_casa_url"
|
||||
)}</a
|
||||
>.
|
||||
<ha-svg-icon
|
||||
.url=${`https://${remote_domain}`}
|
||||
.path=${mdiContentCopy}
|
||||
@click=${this._copyURL}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a
|
||||
@ -133,6 +142,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 {
|
||||
return css`
|
||||
.preparing {
|
||||
@ -154,9 +171,6 @@ export class CloudRemotePref extends LitElement {
|
||||
font-weight: bold;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.warning ha-svg-icon {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
.break-word {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
@ -178,6 +192,11 @@ export class CloudRemotePref extends LitElement {
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
ha-svg-icon {
|
||||
--mdc-icon-size: 18px;
|
||||
color: var(--secondary-text-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user