diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 880e043c7f..5bbb751eb2 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 90 days stale policy - uses: actions/stale@v5.1.1 + uses: actions/stale@v6.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 90 diff --git a/demo/src/ha-demo.ts b/demo/src/ha-demo.ts index b583245e5a..b80c4d71de 100644 --- a/demo/src/ha-demo.ts +++ b/demo/src/ha-demo.ts @@ -68,6 +68,7 @@ class HaDemo extends HomeAssistantAppEl { hidden_by: null, entity_category: null, has_entity_name: false, + unique_id: "co2_intensity", }, { config_entry_id: "co2signal", @@ -82,6 +83,7 @@ class HaDemo extends HomeAssistantAppEl { hidden_by: null, entity_category: null, has_entity_name: false, + unique_id: "grid_fossil_fuel_percentage", }, ]); diff --git a/demo/src/stubs/energy.ts b/demo/src/stubs/energy.ts index 00e7ef7c3f..80f10b968c 100644 --- a/demo/src/stubs/energy.ts +++ b/demo/src/stubs/energy.ts @@ -11,14 +11,12 @@ export const mockEnergy = (hass: MockHomeAssistant) => { { stat_energy_from: "sensor.energy_consumption_tarif_1", stat_cost: "sensor.energy_consumption_tarif_1_cost", - entity_energy_from: "sensor.energy_consumption_tarif_1", entity_energy_price: null, number_energy_price: null, }, { stat_energy_from: "sensor.energy_consumption_tarif_2", stat_cost: "sensor.energy_consumption_tarif_2_cost", - entity_energy_from: "sensor.energy_consumption_tarif_2", entity_energy_price: null, number_energy_price: null, }, @@ -27,14 +25,12 @@ export const mockEnergy = (hass: MockHomeAssistant) => { { stat_energy_to: "sensor.energy_production_tarif_1", stat_compensation: "sensor.energy_production_tarif_1_compensation", - entity_energy_to: "sensor.energy_production_tarif_1", entity_energy_price: null, number_energy_price: null, }, { stat_energy_to: "sensor.energy_production_tarif_2", stat_compensation: "sensor.energy_production_tarif_2_compensation", - entity_energy_to: "sensor.energy_production_tarif_2", entity_energy_price: null, number_energy_price: null, }, @@ -55,7 +51,6 @@ export const mockEnergy = (hass: MockHomeAssistant) => { type: "gas", stat_energy_from: "sensor.energy_gas", stat_cost: "sensor.energy_gas_cost", - entity_energy_from: "sensor.energy_gas", entity_energy_price: null, number_energy_price: null, }, diff --git a/demo/src/stubs/history.ts b/demo/src/stubs/history.ts index 2e2c507ccc..86f3445956 100644 --- a/demo/src/stubs/history.ts +++ b/demo/src/stubs/history.ts @@ -6,7 +6,7 @@ import { endOfDay, } from "date-fns/esm"; import { HassEntity } from "home-assistant-js-websocket"; -import { StatisticValue } from "../../../src/data/history"; +import { StatisticValue } from "../../../src/data/recorder"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; interface HistoryQueryParams { diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index dcc099b636..b794d15671 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -195,6 +195,48 @@ const SCHEMAS: { }, }, }, + select_disabled_list: { + name: "Select disabled option", + selector: { + select: { + options: [ + { label: "Option 1", value: "Option 1" }, + { label: "Option 2", value: "Option 2" }, + { label: "Option 3", value: "Option 3", disabled: true }, + ], + mode: "list", + }, + }, + }, + select_disabled_multiple: { + name: "Select disabled option", + selector: { + select: { + multiple: true, + options: [ + { label: "Option 1", value: "Option 1" }, + { label: "Option 2", value: "Option 2" }, + { label: "Option 3", value: "Option 3", disabled: true }, + ], + mode: "list", + }, + }, + }, + select_disabled: { + name: "Select disabled option", + selector: { + select: { + options: [ + { label: "Option 1", value: "Option 1" }, + { label: "Option 2", value: "Option 2" }, + { label: "Option 3", value: "Option 3", disabled: true }, + { label: "Option 4", value: "Option 4", disabled: true }, + { label: "Option 5", value: "Option 5", disabled: true }, + { label: "Option 6", value: "Option 6" }, + ], + }, + }, + }, select_custom: { name: "Select (Custom)", selector: { diff --git a/gallery/src/pages/misc/integration-card.ts b/gallery/src/pages/misc/integration-card.ts index 7d3fc2eda4..5b6b064803 100644 --- a/gallery/src/pages/misc/integration-card.ts +++ b/gallery/src/pages/misc/integration-card.ts @@ -196,6 +196,7 @@ const createEntityRegistryEntries = ( icon: null, platform: "updater", has_entity_name: false, + unique_id: "updater", }, ]; diff --git a/gallery/src/pages/more-info/cover.ts b/gallery/src/pages/more-info/cover.ts index 6afa1a6ff9..65f9ba4a25 100644 --- a/gallery/src/pages/more-info/cover.ts +++ b/gallery/src/pages/more-info/cover.ts @@ -1,16 +1,7 @@ import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { customElement, property, query } from "lit/decorators"; import "../../../../src/components/ha-card"; -import { - SUPPORT_OPEN, - SUPPORT_STOP, - SUPPORT_CLOSE, - SUPPORT_SET_POSITION, - SUPPORT_OPEN_TILT, - SUPPORT_STOP_TILT, - SUPPORT_CLOSE_TILT, - SUPPORT_SET_TILT_POSITION, -} from "../../../../src/data/cover"; +import { CoverEntityFeature } from "../../../../src/data/cover"; import "../../../../src/dialogs/more-info/more-info-content"; import { getEntity } from "../../../../src/fake_data/entity"; import { @@ -22,113 +13,127 @@ import "../../components/demo-more-infos"; const ENTITIES = [ getEntity("cover", "position_buttons", "on", { friendly_name: "Position Buttons", - supported_features: SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE, + supported_features: + CoverEntityFeature.OPEN + + CoverEntityFeature.STOP + + CoverEntityFeature.CLOSE, }), getEntity("cover", "position_slider_half", "on", { friendly_name: "Position Half-Open", supported_features: - SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION, + CoverEntityFeature.OPEN + + CoverEntityFeature.STOP + + CoverEntityFeature.CLOSE + + CoverEntityFeature.SET_POSITION, current_position: 50, }), getEntity("cover", "position_slider_open", "on", { friendly_name: "Position Open", supported_features: - SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION, + CoverEntityFeature.OPEN + + CoverEntityFeature.STOP + + CoverEntityFeature.CLOSE + + CoverEntityFeature.SET_POSITION, current_position: 100, }), getEntity("cover", "position_slider_closed", "on", { friendly_name: "Position Closed", supported_features: - SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION, + CoverEntityFeature.OPEN + + CoverEntityFeature.STOP + + CoverEntityFeature.CLOSE + + CoverEntityFeature.SET_POSITION, current_position: 0, }), getEntity("cover", "tilt_buttons", "on", { friendly_name: "Tilt Buttons", supported_features: - SUPPORT_OPEN_TILT + SUPPORT_STOP_TILT + SUPPORT_CLOSE_TILT, + CoverEntityFeature.OPEN_TILT + + CoverEntityFeature.STOP_TILT + + CoverEntityFeature.CLOSE_TILT, }), getEntity("cover", "tilt_slider_half", "on", { friendly_name: "Tilt Half-Open", supported_features: - SUPPORT_OPEN_TILT + - SUPPORT_STOP_TILT + - SUPPORT_CLOSE_TILT + - SUPPORT_SET_TILT_POSITION, + CoverEntityFeature.OPEN_TILT + + CoverEntityFeature.STOP_TILT + + CoverEntityFeature.CLOSE_TILT + + CoverEntityFeature.SET_TILT_POSITION, current_tilt_position: 50, }), getEntity("cover", "tilt_slider_open", "on", { friendly_name: "Tilt Open", supported_features: - SUPPORT_OPEN_TILT + - SUPPORT_STOP_TILT + - SUPPORT_CLOSE_TILT + - SUPPORT_SET_TILT_POSITION, + CoverEntityFeature.OPEN_TILT + + CoverEntityFeature.STOP_TILT + + CoverEntityFeature.CLOSE_TILT + + CoverEntityFeature.SET_TILT_POSITION, current_tilt_position: 100, }), getEntity("cover", "tilt_slider_closed", "on", { friendly_name: "Tilt Closed", supported_features: - SUPPORT_OPEN_TILT + - SUPPORT_STOP_TILT + - SUPPORT_CLOSE_TILT + - SUPPORT_SET_TILT_POSITION, + CoverEntityFeature.OPEN_TILT + + CoverEntityFeature.STOP_TILT + + CoverEntityFeature.CLOSE_TILT + + CoverEntityFeature.SET_TILT_POSITION, current_tilt_position: 0, }), getEntity("cover", "position_slider_tilt_slider", "on", { friendly_name: "Both Sliders", supported_features: - SUPPORT_OPEN + - SUPPORT_STOP + - SUPPORT_CLOSE + - SUPPORT_SET_POSITION + - SUPPORT_OPEN_TILT + - SUPPORT_STOP_TILT + - SUPPORT_CLOSE_TILT + - SUPPORT_SET_TILT_POSITION, + CoverEntityFeature.OPEN + + CoverEntityFeature.STOP + + CoverEntityFeature.CLOSE + + CoverEntityFeature.SET_POSITION + + CoverEntityFeature.OPEN_TILT + + CoverEntityFeature.STOP_TILT + + CoverEntityFeature.CLOSE_TILT + + CoverEntityFeature.SET_TILT_POSITION, current_position: 30, current_tilt_position: 70, }), getEntity("cover", "position_tilt_slider", "on", { friendly_name: "Position & Tilt Slider", supported_features: - SUPPORT_OPEN + - SUPPORT_STOP + - SUPPORT_CLOSE + - SUPPORT_OPEN_TILT + - SUPPORT_STOP_TILT + - SUPPORT_CLOSE_TILT + - SUPPORT_SET_TILT_POSITION, + CoverEntityFeature.OPEN + + CoverEntityFeature.STOP + + CoverEntityFeature.CLOSE + + CoverEntityFeature.OPEN_TILT + + CoverEntityFeature.STOP_TILT + + CoverEntityFeature.CLOSE_TILT + + CoverEntityFeature.SET_TILT_POSITION, current_tilt_position: 70, }), getEntity("cover", "position_slider_tilt", "on", { friendly_name: "Position Slider & Tilt", supported_features: - SUPPORT_OPEN + - SUPPORT_STOP + - SUPPORT_CLOSE + - SUPPORT_SET_POSITION + - SUPPORT_OPEN_TILT + - SUPPORT_STOP_TILT + - SUPPORT_CLOSE_TILT, + CoverEntityFeature.OPEN + + CoverEntityFeature.STOP + + CoverEntityFeature.CLOSE + + CoverEntityFeature.SET_POSITION + + CoverEntityFeature.OPEN_TILT + + CoverEntityFeature.STOP_TILT + + CoverEntityFeature.CLOSE_TILT, current_position: 30, }), getEntity("cover", "position_slider_only_tilt_slider", "on", { friendly_name: "Position Slider Only & Tilt Buttons", supported_features: - SUPPORT_SET_POSITION + - SUPPORT_OPEN_TILT + - SUPPORT_STOP_TILT + - SUPPORT_CLOSE_TILT, + CoverEntityFeature.SET_POSITION + + CoverEntityFeature.OPEN_TILT + + CoverEntityFeature.STOP_TILT + + CoverEntityFeature.CLOSE_TILT, current_position: 30, }), getEntity("cover", "position_slider_only_tilt", "on", { friendly_name: "Position Slider Only & Tilt", supported_features: - SUPPORT_SET_POSITION + - SUPPORT_OPEN_TILT + - SUPPORT_STOP_TILT + - SUPPORT_CLOSE_TILT + - SUPPORT_SET_TILT_POSITION, + CoverEntityFeature.SET_POSITION + + CoverEntityFeature.OPEN_TILT + + CoverEntityFeature.STOP_TILT + + CoverEntityFeature.CLOSE_TILT + + CoverEntityFeature.SET_TILT_POSITION, current_position: 30, current_tilt_position: 70, }), diff --git a/gallery/src/pages/more-info/input-number.markdown b/gallery/src/pages/more-info/input-number.markdown new file mode 100644 index 0000000000..20babab09b --- /dev/null +++ b/gallery/src/pages/more-info/input-number.markdown @@ -0,0 +1,3 @@ +--- +title: Input Number +--- diff --git a/gallery/src/pages/more-info/input-number.ts b/gallery/src/pages/more-info/input-number.ts new file mode 100644 index 0000000000..e070b52e8b --- /dev/null +++ b/gallery/src/pages/more-info/input-number.ts @@ -0,0 +1,60 @@ +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, property, query } from "lit/decorators"; +import "../../../../src/components/ha-card"; +import "../../../../src/dialogs/more-info/more-info-content"; +import { getEntity } from "../../../../src/fake_data/entity"; +import { + MockHomeAssistant, + provideHass, +} from "../../../../src/fake_data/provide_hass"; +import "../../components/demo-more-infos"; + +const ENTITIES = [ + getEntity("input_number", "box1", 0, { + friendly_name: "Box1", + min: 0, + max: 100, + step: 1, + initial: 0, + mode: "box", + unit_of_measurement: "items", + }), + getEntity("input_number", "slider1", 0, { + friendly_name: "Slider1", + min: 0, + max: 100, + step: 1, + initial: 0, + mode: "slider", + unit_of_measurement: "items", + }), +]; + +@customElement("demo-more-info-input-number") +class DemoMoreInfoInputNumber extends LitElement { + @property() public hass!: MockHomeAssistant; + + @query("demo-more-infos") private _demoRoot!: HTMLElement; + + protected render(): TemplateResult { + return html` + ent.entityId)} + > + `; + } + + protected firstUpdated(changedProperties: PropertyValues) { + super.firstUpdated(changedProperties); + const hass = provideHass(this._demoRoot); + hass.updateTranslations(null, "en"); + hass.addEntities(ENTITIES); + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-more-info-input-number": DemoMoreInfoInputNumber; + } +} diff --git a/gallery/src/pages/more-info/light.ts b/gallery/src/pages/more-info/light.ts index 999a4908ad..336aeb8ca8 100644 --- a/gallery/src/pages/more-info/light.ts +++ b/gallery/src/pages/more-info/light.ts @@ -1,12 +1,7 @@ import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { customElement, property, query } from "lit/decorators"; import "../../../../src/components/ha-card"; -import { - LightColorModes, - SUPPORT_EFFECT, - SUPPORT_FLASH, - SUPPORT_TRANSITION, -} from "../../../../src/data/light"; +import { LightColorMode, LightEntityFeature } from "../../../../src/data/light"; import "../../../../src/dialogs/more-info/more-info-content"; import { getEntity } from "../../../../src/fake_data/entity"; import { @@ -22,8 +17,8 @@ const ENTITIES = [ getEntity("light", "kitchen_light", "on", { friendly_name: "Brightness Light", brightness: 200, - supported_color_modes: [LightColorModes.BRIGHTNESS], - color_mode: LightColorModes.BRIGHTNESS, + supported_color_modes: [LightColorMode.BRIGHTNESS], + color_mode: LightColorMode.BRIGHTNESS, }), getEntity("light", "color_temperature_light", "on", { friendly_name: "White Color Temperature Light", @@ -32,10 +27,10 @@ const ENTITIES = [ min_mireds: 30, max_mireds: 150, supported_color_modes: [ - LightColorModes.BRIGHTNESS, - LightColorModes.COLOR_TEMP, + LightColorMode.BRIGHTNESS, + LightColorMode.COLOR_TEMP, ], - color_mode: LightColorModes.COLOR_TEMP, + color_mode: LightColorMode.COLOR_TEMP, }), getEntity("light", "color_hs_light", "on", { friendly_name: "Color HS Light", @@ -44,13 +39,16 @@ const ENTITIES = [ rgb_color: [30, 100, 255], min_mireds: 30, max_mireds: 150, - supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION, + supported_features: + LightEntityFeature.EFFECT + + LightEntityFeature.FLASH + + LightEntityFeature.TRANSITION, supported_color_modes: [ - LightColorModes.BRIGHTNESS, - LightColorModes.COLOR_TEMP, - LightColorModes.HS, + LightColorMode.BRIGHTNESS, + LightColorMode.COLOR_TEMP, + LightColorMode.HS, ], - color_mode: LightColorModes.HS, + color_mode: LightColorMode.HS, effect_list: ["random", "colorloop"], }), getEntity("light", "color_rgb_ct_light", "on", { @@ -59,22 +57,28 @@ const ENTITIES = [ color_temp: 75, min_mireds: 30, max_mireds: 150, - supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION, + supported_features: + LightEntityFeature.EFFECT + + LightEntityFeature.FLASH + + LightEntityFeature.TRANSITION, supported_color_modes: [ - LightColorModes.BRIGHTNESS, - LightColorModes.COLOR_TEMP, - LightColorModes.RGB, + LightColorMode.BRIGHTNESS, + LightColorMode.COLOR_TEMP, + LightColorMode.RGB, ], - color_mode: LightColorModes.COLOR_TEMP, + color_mode: LightColorMode.COLOR_TEMP, effect_list: ["random", "colorloop"], }), getEntity("light", "color_RGB_light", "on", { friendly_name: "Color Effects Light", brightness: 255, rgb_color: [30, 100, 255], - supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION, - supported_color_modes: [LightColorModes.BRIGHTNESS, LightColorModes.RGB], - color_mode: LightColorModes.RGB, + supported_features: + LightEntityFeature.EFFECT + + LightEntityFeature.FLASH + + LightEntityFeature.TRANSITION, + supported_color_modes: [LightColorMode.BRIGHTNESS, LightColorMode.RGB], + color_mode: LightColorMode.RGB, effect_list: ["random", "colorloop"], }), getEntity("light", "color_rgbw_light", "on", { @@ -83,13 +87,16 @@ const ENTITIES = [ rgbw_color: [30, 100, 255, 125], min_mireds: 30, max_mireds: 150, - supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION, + supported_features: + LightEntityFeature.EFFECT + + LightEntityFeature.FLASH + + LightEntityFeature.TRANSITION, supported_color_modes: [ - LightColorModes.BRIGHTNESS, - LightColorModes.COLOR_TEMP, - LightColorModes.RGBW, + LightColorMode.BRIGHTNESS, + LightColorMode.COLOR_TEMP, + LightColorMode.RGBW, ], - color_mode: LightColorModes.RGBW, + color_mode: LightColorMode.RGBW, effect_list: ["random", "colorloop"], }), getEntity("light", "color_rgbww_light", "on", { @@ -98,13 +105,16 @@ const ENTITIES = [ rgbww_color: [30, 100, 255, 125, 10], min_mireds: 30, max_mireds: 150, - supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION, + supported_features: + LightEntityFeature.EFFECT + + LightEntityFeature.FLASH + + LightEntityFeature.TRANSITION, supported_color_modes: [ - LightColorModes.BRIGHTNESS, - LightColorModes.COLOR_TEMP, - LightColorModes.RGBWW, + LightColorMode.BRIGHTNESS, + LightColorMode.COLOR_TEMP, + LightColorMode.RGBWW, ], - color_mode: LightColorModes.RGBWW, + color_mode: LightColorMode.RGBWW, effect_list: ["random", "colorloop"], }), getEntity("light", "color_xy_light", "on", { @@ -114,13 +124,16 @@ const ENTITIES = [ rgb_color: [30, 100, 255], min_mireds: 30, max_mireds: 150, - supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION, + supported_features: + LightEntityFeature.EFFECT + + LightEntityFeature.FLASH + + LightEntityFeature.TRANSITION, supported_color_modes: [ - LightColorModes.BRIGHTNESS, - LightColorModes.COLOR_TEMP, - LightColorModes.XY, + LightColorMode.BRIGHTNESS, + LightColorMode.COLOR_TEMP, + LightColorMode.XY, ], - color_mode: LightColorModes.XY, + color_mode: LightColorMode.XY, effect_list: ["random", "colorloop"], }), ]; diff --git a/hassio/src/addon-view/info/hassio-addon-info.ts b/hassio/src/addon-view/info/hassio-addon-info.ts index 3753b8a42d..3cc3d23010 100644 --- a/hassio/src/addon-view/info/hassio-addon-info.ts +++ b/hassio/src/addon-view/info/hassio-addon-info.ts @@ -1024,10 +1024,13 @@ class HassioAddonInfo extends LitElement { button.progress = true; const confirmed = await showConfirmationDialog(this, { - title: this.addon.name, - text: "Are you sure you want to uninstall this add-on?", - confirmText: "uninstall add-on", - dismissText: "no", + title: this.supervisor.localize("dialog.uninstall_addon.title", { + name: this.addon.name, + }), + text: this.supervisor.localize("dialog.uninstall_addon.text"), + confirmText: this.supervisor.localize("dialog.uninstall_addon.uninstall"), + dismissText: this.supervisor.localize("common.cancel"), + destructive: true, }); if (!confirmed) { diff --git a/hassio/src/dialogs/suggestAddonRestart.ts b/hassio/src/dialogs/suggestAddonRestart.ts index 66e95bf08b..56a7bb8a0e 100644 --- a/hassio/src/dialogs/suggestAddonRestart.ts +++ b/hassio/src/dialogs/suggestAddonRestart.ts @@ -18,9 +18,11 @@ export const suggestAddonRestart = async ( addon: HassioAddonDetails ): Promise => { const confirmed = await showConfirmationDialog(element, { - title: supervisor.localize("common.restart_name", "name", addon.name), + title: supervisor.localize("dialog.restart_addon.title", { + name: addon.name, + }), text: supervisor.localize("dialog.restart_addon.text"), - confirmText: supervisor.localize("dialog.restart_addon.confirm_text"), + confirmText: supervisor.localize("dialog.restart_addon.restart"), dismissText: supervisor.localize("common.cancel"), }); if (confirmed) { @@ -28,11 +30,9 @@ export const suggestAddonRestart = async ( await restartHassioAddon(hass, addon.slug); } catch (err: any) { showAlertDialog(element, { - title: supervisor.localize( - "common.failed_to_restart_name", - "name", - addon.name - ), + title: supervisor.localize("common.failed_to_restart_name", { + name: addon.name, + }), text: extractApiErrorMessage(err), }); } diff --git a/hassio/src/system/hassio-supervisor-info.ts b/hassio/src/system/hassio-supervisor-info.ts index e1acfc74f1..1dbca65417 100644 --- a/hassio/src/system/hassio-supervisor-info.ts +++ b/hassio/src/system/hassio-supervisor-info.ts @@ -23,6 +23,7 @@ import { showAlertDialog, showConfirmationDialog, } from "../../../src/dialogs/generic/show-dialog-box"; +import { showJoinBetaDialog } from "../../../src/panels/config/core/updates/show-dialog-join-beta"; import { UNHEALTHY_REASON_URL, UNSUPPORTED_REASON_URL, @@ -230,36 +231,27 @@ class HassioSupervisorInfo extends LitElement { button.progress = true; if (this.supervisor.supervisor.channel === "stable") { - const confirmed = await showConfirmationDialog(this, { - title: this.supervisor.localize("system.supervisor.warning"), - text: html`${this.supervisor.localize("system.supervisor.beta_warning")} -
- ${this.supervisor.localize("system.supervisor.beta_backup")} -

- ${this.supervisor.localize("system.supervisor.beta_release_items")} - -
- ${this.supervisor.localize("system.supervisor.beta_join_confirm")}`, - confirmText: this.supervisor.localize( - "system.supervisor.join_beta_action" - ), - dismissText: this.supervisor.localize("common.cancel"), + showJoinBetaDialog(this, { + join: async () => { + await this._setChannel("beta"); + button.progress = false; + }, + cancel: () => { + button.progress = false; + }, }); - - if (!confirmed) { - button.progress = false; - return; - } + } else { + await this._setChannel("stable"); + button.progress = false; } + } + private async _setChannel( + channel: SupervisorOptions["channel"] + ): Promise { try { const data: Partial = { - channel: - this.supervisor.supervisor.channel === "stable" ? "beta" : "stable", + channel, }; await setSupervisorOption(this.hass, data); await this._reloadSupervisor(); @@ -270,8 +262,6 @@ class HassioSupervisorInfo extends LitElement { ), text: extractApiErrorMessage(err), }); - } finally { - button.progress = false; } } diff --git a/package.json b/package.json index d27157841f..987792155e 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "deep-freeze": "^0.0.1", "fuse.js": "^6.0.0", "google-timezones-json": "^1.0.2", - "hls.js": "^1.2.1", + "hls.js": "^1.2.3", "home-assistant-js-websocket": "^8.0.0", "idb-keyval": "^5.1.3", "intl-messageformat": "^9.9.1", diff --git a/pyproject.toml b/pyproject.toml index ba3b991ccb..392614fdd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20220907.2" +version = "20220928.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" diff --git a/script/core b/script/core index 8ac6da6284..93b7021d57 100755 --- a/script/core +++ b/script/core @@ -46,6 +46,14 @@ frontend: # development_repo: ${WD}" >> "${WD}/config/configuration.yaml" fi + if [ ! -z "${CODESPACES}" ]; then + echo " +http: + use_x_forwarded_for: true + trusted_proxies: + - 127.0.0.1 +" >> "${WD}/config/configuration.yaml" + fi fi hass -c "${WD}/config" diff --git a/src/common/const.ts b/src/common/const.ts index 19899e3033..e4e383e49b 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -6,6 +6,7 @@ import { mdiAlert, mdiAngleAcute, mdiAppleSafari, + mdiArrowLeftRight, mdiBell, mdiBookmark, mdiBrightness5, @@ -25,7 +26,6 @@ import { mdiFlower, mdiFormatListBulleted, mdiFormTextbox, - mdiGasCylinder, mdiGauge, mdiGestureTapButton, mdiGoogleAssistant, @@ -37,6 +37,8 @@ import { mdiLightningBolt, mdiMailbox, mdiMapMarkerRadius, + mdiMeterGas, + mdiMicrophoneMessage, mdiMolecule, mdiMoleculeCo, mdiMoleculeCo2, @@ -47,13 +49,14 @@ import { mdiRobotVacuum, mdiScriptText, mdiSineWave, - mdiMicrophoneMessage, + mdiSpeedometer, mdiThermometer, mdiThermostat, mdiTimerOutline, mdiVideo, mdiWaterPercent, mdiWeatherCloudy, + mdiWeight, mdiWhiteBalanceSunny, mdiWifi, } from "@mdi/js"; @@ -121,9 +124,10 @@ export const FIXED_DEVICE_CLASS_ICONS = { carbon_monoxide: mdiMoleculeCo, current: mdiCurrentAc, date: mdiCalendar, + distance: mdiArrowLeftRight, energy: mdiLightningBolt, frequency: mdiSineWave, - gas: mdiGasCylinder, + gas: mdiMeterGas, humidity: mdiWaterPercent, illuminance: mdiBrightness5, moisture: mdiWaterPercent, @@ -140,11 +144,14 @@ export const FIXED_DEVICE_CLASS_ICONS = { pressure: mdiGauge, reactive_power: mdiFlash, signal_strength: mdiWifi, + speed: mdiSpeedometer, sulphur_dioxide: mdiMolecule, temperature: mdiThermometer, timestamp: mdiClock, volatile_organic_compounds: mdiMolecule, voltage: mdiSineWave, + // volume: TBD, => no well matching icon found + weight: mdiWeight, }; /** Domains that have a state card. */ diff --git a/src/common/entity/feature_class_names.ts b/src/common/entity/feature_class_names.ts index aaa9220c28..7898e9d872 100644 --- a/src/common/entity/feature_class_names.ts +++ b/src/common/entity/feature_class_names.ts @@ -1,10 +1,14 @@ import { HassEntity } from "home-assistant-js-websocket"; import { supportsFeature } from "./supports-feature"; +export type FeatureClassNames = Partial< + Record +>; + // Expects classNames to be an object mapping feature-bit -> className export const featureClassNames = ( stateObj: HassEntity, - classNames: { [feature: number]: string } + classNames: FeatureClassNames ) => { if (!stateObj || !stateObj.attributes.supported_features) { return ""; diff --git a/src/common/entity/get_states.ts b/src/common/entity/get_states.ts index f79cc1a4d0..7c59eb2faa 100644 --- a/src/common/entity/get_states.ts +++ b/src/common/entity/get_states.ts @@ -37,6 +37,7 @@ const FIXED_DOMAIN_STATES = { siren: ["on", "off"], sun: ["above_horizon", "below_horizon"], switch: ["on", "off"], + timer: ["active", "idle", "paused"], update: ["on", "off"], vacuum: ["cleaning", "docked", "error", "idle", "paused", "returning"], weather: [ diff --git a/src/common/integrations/protocolIntegrationPicked.ts b/src/common/integrations/protocolIntegrationPicked.ts index ff62cacbd9..4af63a34b1 100644 --- a/src/common/integrations/protocolIntegrationPicked.ts +++ b/src/common/integrations/protocolIntegrationPicked.ts @@ -1,11 +1,11 @@ import { html } from "lit"; import { getConfigEntries } from "../../data/config_entries"; +import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow"; import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box"; import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node"; import type { HomeAssistant } from "../../types"; import { documentationUrl } from "../../util/documentation-url"; import { isComponentLoaded } from "../config/is_component_loaded"; -import { fireEvent } from "../dom/fire_event"; import { navigate } from "../navigate"; export const protocolIntegrationPicked = async ( @@ -39,8 +39,8 @@ export const protocolIntegrationPicked = async ( "ui.panel.config.integrations.config_flow.proceed" ), confirm: () => { - fireEvent(element, "handler-picked", { - handler: "zwave_js", + showConfigFlowDialog(element, { + startFlowHandler: "zwave_js", }); }, }); @@ -75,8 +75,8 @@ export const protocolIntegrationPicked = async ( "ui.panel.config.integrations.config_flow.proceed" ), confirm: () => { - fireEvent(element, "handler-picked", { - handler: "zha", + showConfigFlowDialog(element, { + startFlowHandler: "zha", }); }, }); diff --git a/src/common/string/title-case.ts b/src/common/string/title-case.ts new file mode 100644 index 0000000000..f089f6520c --- /dev/null +++ b/src/common/string/title-case.ts @@ -0,0 +1,4 @@ +export const titleCase = (s) => + s.replace(/^_*(.)|_+(.)/g, (_s, c, d) => + c ? c.toUpperCase() : " " + d.toUpperCase() + ); diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index ece8671a8c..ea566c9fc7 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -13,6 +13,7 @@ import { TemplateResult, } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { getGraphColorByIndex } from "../../common/color/colors"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { @@ -20,31 +21,37 @@ import { numberFormatToLocale, } from "../../common/number/format_number"; import { - getStatisticIds, getStatisticLabel, + getStatisticMetadata, Statistics, statisticsHaveType, - StatisticsMetaData, StatisticType, -} from "../../data/history"; +} from "../../data/recorder"; import type { HomeAssistant } from "../../types"; import "./ha-chart-base"; +export type ExtendedStatisticType = StatisticType | "state"; + +export const statTypeMap: Record = { + mean: "mean", + min: "min", + max: "max", + sum: "sum", + state: "sum", +}; @customElement("statistics-chart") class StatisticsChart extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public statisticsData!: Statistics; - @property({ type: Array }) public statisticIds?: StatisticsMetaData[]; - @property() public names: boolean | Record = false; @property() public unit?: string; @property({ attribute: false }) public endTime?: Date; - @property({ type: Array }) public statTypes: Array = [ + @property({ type: Array }) public statTypes: Array = [ "sum", "min", "mean", @@ -191,18 +198,28 @@ class StatisticsChart extends LitElement { }; } - private async _getStatisticIds() { - this.statisticIds = await getStatisticIds(this.hass); - } + private _getStatisticsMetaData = memoizeOne( + async (statisticIds: string[] | undefined) => { + const statsMetadataArray = await getStatisticMetadata( + this.hass, + statisticIds + ); + const statisticsMetaData = {}; + statsMetadataArray.forEach((x) => { + statisticsMetaData[x.statistic_id] = x; + }); + return statisticsMetaData; + } + ); private async _generateData() { if (!this.statisticsData) { return; } - if (!this.statisticIds) { - await this._getStatisticIds(); - } + const statisticsMetaData = await this._getStatisticsMetaData( + Object.keys(this.statisticsData) + ); let colorIndex = 0; const statisticsData = Object.values(this.statisticsData); @@ -233,9 +250,7 @@ class StatisticsChart extends LitElement { const names = this.names || {}; statisticsData.forEach((stats) => { const firstStat = stats[0]; - const meta = this.statisticIds!.find( - (stat) => stat.statistic_id === firstStat.statistic_id - ); + const meta = statisticsMetaData?.[firstStat.statistic_id]; let name = names[firstStat.statistic_id]; if (!name) { name = getStatisticLabel(this.hass, firstStat.statistic_id, meta); @@ -301,7 +316,7 @@ class StatisticsChart extends LitElement { : this.statTypes; sortedTypes.forEach((type) => { - if (statisticsHaveType(stats, type)) { + if (statisticsHaveType(stats, statTypeMap[type])) { const band = drawBands && (type === "min" || type === "max"); statTypes.push(type); statDataSets.push({ @@ -329,7 +344,6 @@ class StatisticsChart extends LitElement { let prevDate: Date | null = null; // Process chart data. - let initVal: number | null = null; let prevSum: number | null = null; stats.forEach((stat) => { const date = new Date(stat.start); @@ -341,11 +355,11 @@ class StatisticsChart extends LitElement { statTypes.forEach((type) => { let val: number | null; if (type === "sum") { - if (initVal === null) { - initVal = val = stat.state || 0; + if (prevSum === null) { + val = 0; prevSum = stat.sum; } else { - val = initVal + ((stat.sum || 0) - prevSum!); + val = (stat.sum || 0) - prevSum; } } else { val = stat[type]; diff --git a/src/components/entity/ha-statistic-picker.ts b/src/components/entity/ha-statistic-picker.ts index a2533c7bb9..ebf129efaa 100644 --- a/src/components/entity/ha-statistic-picker.ts +++ b/src/components/entity/ha-statistic-picker.ts @@ -3,10 +3,11 @@ import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; +import { ensureArray } from "../../common/ensure-array"; import { fireEvent } from "../../common/dom/fire_event"; import { computeStateName } from "../../common/entity/compute_state_name"; import { stringCompare } from "../../common/string/compare"; -import { getStatisticIds, StatisticsMetaData } from "../../data/history"; +import { getStatisticIds, StatisticsMetaData } from "../../data/recorder"; import { PolymerChangedEvent } from "../../polymer-types"; import { HomeAssistant } from "../../types"; import { documentationUrl } from "../../util/documentation-url"; @@ -39,22 +40,20 @@ export class HaStatisticPicker extends LitElement { type: Array, attribute: "include-statistics-unit-of-measurement", }) - public includeStatisticsUnitOfMeasurement?: string[]; + public includeStatisticsUnitOfMeasurement?: string | string[]; /** * Show only statistics displayed with these units of measurements. - * @type {Array} * @attr include-display-unit-of-measurement */ - @property({ type: Array, attribute: "include-display-unit-of-measurement" }) - public includeDisplayUnitOfMeasurement?: string[]; + @property({ attribute: "include-display-unit-of-measurement" }) + public includeDisplayUnitOfMeasurement?: string | string[]; /** * Show only statistics with these device classes. - * @type {Array} * @attr include-device-classes */ - @property({ type: Array, attribute: "include-device-classes" }) + @property({ attribute: "include-device-classes" }) public includeDeviceClasses?: string[]; /** @@ -97,8 +96,8 @@ export class HaStatisticPicker extends LitElement { private _getStatistics = memoizeOne( ( statisticIds: StatisticsMetaData[], - includeStatisticsUnitOfMeasurement?: string[], - includeDisplayUnitOfMeasurement?: string[], + includeStatisticsUnitOfMeasurement?: string | string[], + includeDisplayUnitOfMeasurement?: string | string[], includeDeviceClasses?: string[], entitiesOnly?: boolean ): Array<{ id: string; name: string; state?: HassEntity }> => { @@ -114,17 +113,15 @@ export class HaStatisticPicker extends LitElement { } if (includeStatisticsUnitOfMeasurement) { + const includeUnits = ensureArray(includeStatisticsUnitOfMeasurement); statisticIds = statisticIds.filter((meta) => - includeStatisticsUnitOfMeasurement.includes( - meta.statistics_unit_of_measurement - ) + includeUnits.includes(meta.statistics_unit_of_measurement) ); } if (includeDisplayUnitOfMeasurement) { + const includeUnits = ensureArray(includeDisplayUnitOfMeasurement); statisticIds = statisticIds.filter((meta) => - includeDisplayUnitOfMeasurement.includes( - meta.display_unit_of_measurement - ) + includeUnits.includes(meta.display_unit_of_measurement) ); } diff --git a/src/components/entity/ha-statistics-picker.ts b/src/components/entity/ha-statistics-picker.ts index aa25e45803..ef450d4ae2 100644 --- a/src/components/entity/ha-statistics-picker.ts +++ b/src/components/entity/ha-statistics-picker.ts @@ -22,11 +22,52 @@ class HaStatisticsPicker extends LitElement { @property({ attribute: "pick-statistic-label" }) public pickStatisticLabel?: string; + /** + * Show only statistics natively stored with these units of measurements. + * @attr include-statistics-unit-of-measurement + */ + @property({ + attribute: "include-statistics-unit-of-measurement", + }) + public includeStatisticsUnitOfMeasurement?: string[] | string; + + /** + * Show only statistics displayed with these units of measurements. + * @attr include-display-unit-of-measurement + */ + @property({ attribute: "include-display-unit-of-measurement" }) + public includeDisplayUnitOfMeasurement?: string[] | string; + + /** + * Ignore filtering of statistics type and units when only a single statistic is selected. + * @type {boolean} + * @attr ignore-restrictions-on-first-statistic + */ + @property({ + type: Boolean, + attribute: "ignore-restrictions-on-first-statistic", + }) + public ignoreRestrictionsOnFirstStatistic = false; + protected render(): TemplateResult { if (!this.hass) { return html``; } + const ignoreRestriction = + this.ignoreRestrictionsOnFirstStatistic && + this._currentStatistics.length <= 1; + + const includeDisplayUnitCurrent = ignoreRestriction + ? undefined + : this.includeDisplayUnitOfMeasurement; + const includeStatisticsUnitCurrent = ignoreRestriction + ? undefined + : this.includeStatisticsUnitOfMeasurement; + const includeStatisticTypesCurrent = ignoreRestriction + ? undefined + : this.statisticTypes; + return html` ${this._currentStatistics.map( (statisticId) => html` @@ -34,8 +75,10 @@ class HaStatisticsPicker extends LitElement { nextRender()).then(this._resizeExoPlayer); this._videoEl.style.visibility = "hidden"; - await this.hass!.auth.external!.sendMessage({ + await this.hass!.auth.external!.fireMessage({ type: "exoplayer/play_hls", payload: { url: new URL(url, window.location.href).toString(), diff --git a/src/components/ha-navigation-picker.ts b/src/components/ha-navigation-picker.ts new file mode 100644 index 0000000000..84c1084b30 --- /dev/null +++ b/src/components/ha-navigation-picker.ts @@ -0,0 +1,221 @@ +import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; +import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import { fireEvent } from "../common/dom/fire_event"; +import { titleCase } from "../common/string/title-case"; +import { + fetchConfig, + LovelaceConfig, + LovelaceViewConfig, +} from "../data/lovelace"; +import { PolymerChangedEvent } from "../polymer-types"; +import { HomeAssistant, PanelInfo } from "../types"; +import "./ha-combo-box"; +import type { HaComboBox } from "./ha-combo-box"; +import "./ha-icon"; + +type NavigationItem = { + path: string; + icon: string; + title: string; +}; + +const DEFAULT_ITEMS: NavigationItem[] = [{ path: "", icon: "", title: "" }]; + +// eslint-disable-next-line lit/prefer-static-styles +const rowRenderer: ComboBoxLitRenderer = (item) => html` + + + ${item.title || item.path} + ${item.path} + +`; + +const createViewNavigationItem = ( + prefix: string, + view: LovelaceViewConfig, + index: number +) => ({ + path: `/${prefix}/${view.path ?? index}`, + icon: view.icon ?? "mdi:view-compact", + title: view.title ?? (view.path ? titleCase(view.path) : `${index}`), +}); + +const createPanelNavigationItem = (hass: HomeAssistant, panel: PanelInfo) => ({ + path: `/${panel.url_path}`, + icon: panel.icon ?? "mdi:view-dashboard", + title: + panel.url_path === hass.defaultPanel + ? hass.localize("panel.states") + : hass.localize(`panel.${panel.title}`) || + panel.title || + (panel.url_path ? titleCase(panel.url_path) : ""), +}); + +@customElement("ha-navigation-picker") +export class HaNavigationPicker extends LitElement { + @property() public hass?: HomeAssistant; + + @property() public label?: string; + + @property() public value?: string; + + @property() public helper?: string; + + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean }) public required = false; + + @state() private _opened = false; + + private navigationItemsLoaded = false; + + private navigationItems: NavigationItem[] = DEFAULT_ITEMS; + + @query("ha-combo-box", true) private comboBox!: HaComboBox; + + protected render(): TemplateResult { + return html` + + + `; + } + + private async _openedChanged(ev: PolymerChangedEvent) { + this._opened = ev.detail.value; + if (this._opened && !this.navigationItemsLoaded) { + this._loadNavigationItems(); + } + } + + private async _loadNavigationItems() { + this.navigationItemsLoaded = true; + + const panels = Object.entries(this.hass!.panels).map(([id, panel]) => ({ + id, + ...panel, + })); + const lovelacePanels = panels.filter( + (panel) => panel.component_name === "lovelace" + ); + + const viewConfigs = await Promise.all( + lovelacePanels.map((panel) => + fetchConfig( + this.hass!.connection, + // path should be null to fetch default lovelace panel + panel.url_path === "lovelace" ? null : panel.url_path, + true + ) + .then((config) => [panel.id, config] as [string, LovelaceConfig]) + .catch((_) => [panel.id, undefined] as [string, undefined]) + ) + ); + + const panelViewConfig = new Map(viewConfigs); + + this.navigationItems = []; + + for (const panel of panels) { + this.navigationItems.push(createPanelNavigationItem(this.hass!, panel)); + + const config = panelViewConfig.get(panel.id); + + if (!config) continue; + + config.views.forEach((view, index) => + this.navigationItems.push( + createViewNavigationItem(panel.url_path, view, index) + ) + ); + } + + this.comboBox.filteredItems = this.navigationItems; + } + + protected shouldUpdate(changedProps: PropertyValues) { + return !this._opened || changedProps.has("_opened"); + } + + private _valueChanged(ev: PolymerChangedEvent) { + ev.stopPropagation(); + this._setValue(ev.detail.value); + } + + private _setValue(value: string) { + this.value = value; + fireEvent( + this, + "value-changed", + { value: this._value }, + { + bubbles: false, + composed: false, + } + ); + } + + private _filterChanged(ev: CustomEvent): void { + const filterString = ev.detail.value.toLowerCase(); + const characterCount = filterString.length; + if (characterCount >= 2) { + const filteredItems: NavigationItem[] = []; + + this.navigationItems.forEach((item) => { + if ( + item.path.toLowerCase().includes(filterString) || + item.title.toLowerCase().includes(filterString) + ) { + filteredItems.push(item); + } + }); + + if (filteredItems.length > 0) { + this.comboBox.filteredItems = filteredItems; + } else { + this.comboBox.filteredItems = []; + } + } else { + this.comboBox.filteredItems = this.navigationItems; + } + } + + private get _value() { + return this.value || ""; + } + + static get styles() { + return css` + ha-icon, + ha-svg-icon { + color: var(--primary-text-color); + position: relative; + bottom: 0px; + } + *[slot="prefix"] { + margin-right: 8px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-navigation-picker": HaNavigationPicker; + } +} diff --git a/src/components/ha-selector/ha-selector-navigation.ts b/src/components/ha-selector/ha-selector-navigation.ts new file mode 100644 index 0000000000..e275c47d3a --- /dev/null +++ b/src/components/ha-selector/ha-selector-navigation.ts @@ -0,0 +1,47 @@ +import { html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../../common/dom/fire_event"; +import { NavigationSelector } from "../../data/selector"; +import { HomeAssistant } from "../../types"; +import "../ha-navigation-picker"; + +@customElement("ha-selector-navigation") +export class HaNavigationSelector extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public selector!: NavigationSelector; + + @property() public value?: string; + + @property() public label?: string; + + @property() public helper?: string; + + @property({ type: Boolean, reflect: true }) public disabled = false; + + @property({ type: Boolean }) public required = true; + + protected render() { + return html` + + `; + } + + private _valueChanged(ev: CustomEvent) { + fireEvent(this, "value-changed", { value: ev.detail.value }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-navigation": HaNavigationSelector; + } +} diff --git a/src/components/ha-selector/ha-selector-select.ts b/src/components/ha-selector/ha-selector-select.ts index 71869b0e68..c454d339cd 100644 --- a/src/components/ha-selector/ha-selector-select.ts +++ b/src/components/ha-selector/ha-selector-select.ts @@ -13,6 +13,7 @@ import type { HaComboBox } from "../ha-combo-box"; import "../ha-formfield"; import "../ha-radio"; import "../ha-select"; +import "../ha-input-helper-text"; @customElement("ha-selector-select") export class HaSelectSelector extends LitElement { @@ -40,7 +41,7 @@ export class HaSelectSelector extends LitElement { ); if (!this.selector.select.custom_value && this._mode === "list") { - if (!this.selector.select.multiple || this.required) { + if (!this.selector.select.multiple) { return html`
${this.label} @@ -50,7 +51,7 @@ export class HaSelectSelector extends LitElement { @@ -63,13 +64,14 @@ export class HaSelectSelector extends LitElement { return html`
- ${this.label}${options.map( + ${this.label} + ${options.map( (item: SelectOption) => html` @@ -112,7 +114,9 @@ export class HaSelectSelector extends LitElement { .disabled=${this.disabled} .required=${this.required && !value.length} .value=${this._filter} - .items=${options.filter((item) => !this.value?.includes(item.value))} + .items=${options.filter( + (option) => !option.disabled && !value?.includes(option.value) + )} @filter-changed=${this._filterChanged} @value-changed=${this._comboBoxValueChanged} > @@ -136,7 +140,7 @@ export class HaSelectSelector extends LitElement { .helper=${this.helper} .disabled=${this.disabled} .required=${this.required} - .items=${options} + .items=${options.filter((item) => !item.disabled)} .value=${this.value} @filter-changed=${this._filterChanged} @value-changed=${this._comboBoxValueChanged} @@ -157,7 +161,9 @@ export class HaSelectSelector extends LitElement { > ${options.map( (item: SelectOption) => html` - ${item.label} + ${item.label} ` )} @@ -285,6 +291,9 @@ export class HaSelectSelector extends LitElement { ha-formfield { display: block; } + mwc-list-item[disabled] { + --mdc-theme-text-primary-on-background: var(--disabled-text-color); + } `; } diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 40f22e1a13..bc177836f5 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -16,6 +16,7 @@ import "./ha-selector-device"; import "./ha-selector-duration"; import "./ha-selector-entity"; import "./ha-selector-file"; +import "./ha-selector-navigation"; import "./ha-selector-number"; import "./ha-selector-object"; import "./ha-selector-select"; diff --git a/src/components/ha-textfield.ts b/src/components/ha-textfield.ts index 15f92ad470..5377e71e7f 100644 --- a/src/components/ha-textfield.ts +++ b/src/components/ha-textfield.ts @@ -82,6 +82,13 @@ export class HaTextField extends TextFieldBase { direction: var(--direction); } + .mdc-floating-label:not(.mdc-floating-label--float-above) { + text-overflow: ellipsis; + width: inherit; + padding-right: 30px; + box-sizing: border-box; + } + input { text-align: var(--text-field-text-align, start); } diff --git a/src/components/trace/trace-tab-styles.ts b/src/components/trace/trace-tab-styles.ts index 40878d304f..4f7146df7a 100644 --- a/src/components/trace/trace-tab-styles.ts +++ b/src/components/trace/trace-tab-styles.ts @@ -28,7 +28,7 @@ export const traceTabStyles = css` } .tabs > *.active { - border-bottom-color: var(--accent-color); + border-bottom-color: var(--primary-color); } .tabs > *:focus, diff --git a/src/data/application_credential.ts b/src/data/application_credential.ts index 5dee1bd355..65f9d25a7d 100644 --- a/src/data/application_credential.ts +++ b/src/data/application_credential.ts @@ -8,6 +8,10 @@ export interface ApplicationCredentialsConfig { integrations: Record; } +export interface ApplicationCredentialsConfigEntry { + application_credentials_id?: string; +} + export interface ApplicationCredential { id: string; domain: string; @@ -21,6 +25,15 @@ export const fetchApplicationCredentialsConfig = async (hass: HomeAssistant) => type: "application_credentials/config", }); +export const fetchApplicationCredentialsConfigEntry = async ( + hass: HomeAssistant, + configEntryId: string +) => + hass.callWS({ + type: "application_credentials/config_entry", + config_entry_id: configEntryId, + }); + export const fetchApplicationCredentials = async (hass: HomeAssistant) => hass.callWS({ type: "application_credentials/list", diff --git a/src/data/automation_i18n.ts b/src/data/automation_i18n.ts index e81e6c992a..8b17c894ed 100644 --- a/src/data/automation_i18n.ts +++ b/src/data/automation_i18n.ts @@ -189,7 +189,7 @@ export const describeTrigger = ( // Time Trigger if (trigger.platform === "time" && trigger.at) { const at = trigger.at.includes(".") - ? hass.states[trigger.at] || trigger.at + ? `entity ${computeStateName(hass.states[trigger.at]) || trigger.at}` : trigger.at; return `When the time is equal to ${at}`; diff --git a/src/data/camera.ts b/src/data/camera.ts index cca3671377..26d928f754 100644 --- a/src/data/camera.ts +++ b/src/data/camera.ts @@ -6,6 +6,7 @@ import { timeCacheEntityPromiseFunc } from "../common/util/time-cache-entity-pro import { HomeAssistant } from "../types"; import { getSignedPath } from "./auth"; +export const CAMERA_ORIENTATIONS = [1, 2, 3, 4, 6, 8]; export const CAMERA_SUPPORT_ON_OFF = 1; export const CAMERA_SUPPORT_STREAM = 2; @@ -26,6 +27,7 @@ export interface CameraEntity extends HassEntityBase { export interface CameraPreferences { preload_stream: boolean; + orientation: number; } export interface CameraThumbnail { @@ -109,11 +111,13 @@ export const fetchCameraPrefs = (hass: HomeAssistant, entityId: string) => entity_id: entityId, }); +type ValueOf = T[number]; export const updateCameraPrefs = ( hass: HomeAssistant, entityId: string, prefs: { preload_stream?: boolean; + orientation?: ValueOf; } ) => hass.callWS({ diff --git a/src/data/config_entries.ts b/src/data/config_entries.ts index 5a3757b8e4..42124f53d6 100644 --- a/src/data/config_entries.ts +++ b/src/data/config_entries.ts @@ -1,3 +1,4 @@ +import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { HomeAssistant } from "../types"; export interface ConfigEntry { @@ -44,6 +45,29 @@ export const RECOVERABLE_STATES: ConfigEntry["state"][] = [ "setup_retry", ]; +export interface ConfigEntryUpdate { + // null means no update as is the current state + type: null | "added" | "removed" | "updated"; + entry: ConfigEntry; +} + +export const subscribeConfigEntries = ( + hass: HomeAssistant, + callbackFunction: (message: ConfigEntryUpdate[]) => void, + filters?: { type?: "helper" | "integration"; domain?: string } +): Promise => { + const params: any = { + type: "config_entries/subscribe", + }; + if (filters && filters.type) { + params.type_filter = filters.type; + } + return hass.connection.subscribeMessage( + (message) => callbackFunction(message), + params + ); +}; + export const getConfigEntries = ( hass: HomeAssistant, filters?: { type?: "helper" | "integration"; domain?: string } diff --git a/src/data/cover.ts b/src/data/cover.ts index 3ca55b6bf8..7a7bab7ad2 100644 --- a/src/data/cover.ts +++ b/src/data/cover.ts @@ -4,46 +4,16 @@ import { } from "home-assistant-js-websocket"; import { supportsFeature } from "../common/entity/supports-feature"; -export const SUPPORT_OPEN = 1; -export const SUPPORT_CLOSE = 2; -export const SUPPORT_SET_POSITION = 4; -export const SUPPORT_STOP = 8; -export const SUPPORT_OPEN_TILT = 16; -export const SUPPORT_CLOSE_TILT = 32; -export const SUPPORT_STOP_TILT = 64; -export const SUPPORT_SET_TILT_POSITION = 128; - -export const FEATURE_CLASS_NAMES = { - 4: "has-set_position", - 16: "has-open_tilt", - 32: "has-close_tilt", - 64: "has-stop_tilt", - 128: "has-set_tilt_position", -}; - -export const supportsOpen = (stateObj) => - supportsFeature(stateObj, SUPPORT_OPEN); - -export const supportsClose = (stateObj) => - supportsFeature(stateObj, SUPPORT_CLOSE); - -export const supportsSetPosition = (stateObj) => - supportsFeature(stateObj, SUPPORT_SET_POSITION); - -export const supportsStop = (stateObj) => - supportsFeature(stateObj, SUPPORT_STOP); - -export const supportsOpenTilt = (stateObj) => - supportsFeature(stateObj, SUPPORT_OPEN_TILT); - -export const supportsCloseTilt = (stateObj) => - supportsFeature(stateObj, SUPPORT_CLOSE_TILT); - -export const supportsStopTilt = (stateObj) => - supportsFeature(stateObj, SUPPORT_STOP_TILT); - -export const supportsSetTiltPosition = (stateObj) => - supportsFeature(stateObj, SUPPORT_SET_TILT_POSITION); +export const enum CoverEntityFeature { + OPEN = 1, + CLOSE = 2, + SET_POSITION = 4, + STOP = 8, + OPEN_TILT = 16, + CLOSE_TILT = 32, + STOP_TILT = 64, + SET_TILT_POSITION = 128, +} export function isFullyOpen(stateObj: CoverEntity) { if (stateObj.attributes.current_position !== undefined) { @@ -77,17 +47,19 @@ export function isClosing(stateObj: CoverEntity) { export function isTiltOnly(stateObj: CoverEntity) { const supportsCover = - supportsOpen(stateObj) || supportsClose(stateObj) || supportsStop(stateObj); + supportsFeature(stateObj, CoverEntityFeature.OPEN) || + supportsFeature(stateObj, CoverEntityFeature.CLOSE) || + supportsFeature(stateObj, CoverEntityFeature.STOP); const supportsTilt = - supportsOpenTilt(stateObj) || - supportsCloseTilt(stateObj) || - supportsStopTilt(stateObj); + supportsFeature(stateObj, CoverEntityFeature.OPEN_TILT) || + supportsFeature(stateObj, CoverEntityFeature.CLOSE_TILT) || + supportsFeature(stateObj, CoverEntityFeature.STOP_TILT); return supportsTilt && !supportsCover; } interface CoverEntityAttributes extends HassEntityAttributeBase { - current_position: number; - current_tilt_position: number; + current_position?: number; + current_tilt_position?: number; } export interface CoverEntity extends HassEntityBase { diff --git a/src/data/energy.ts b/src/data/energy.ts index f9a2c3d862..8420419978 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -20,7 +20,8 @@ import { getStatisticMetadata, Statistics, StatisticsMetaData, -} from "./history"; + StatisticsUnitConfiguration, +} from "./recorder"; const energyCollectionKeys: (string | undefined)[] = []; @@ -28,7 +29,6 @@ export const emptyFlowFromGridSourceEnergyPreference = (): FlowFromGridSourceEnergyPreference => ({ stat_energy_from: "", stat_cost: null, - entity_energy_from: null, entity_energy_price: null, number_energy_price: null, }); @@ -37,7 +37,6 @@ export const emptyFlowToGridSourceEnergyPreference = (): FlowToGridSourceEnergyPreference => ({ stat_energy_to: "", stat_compensation: null, - entity_energy_to: null, entity_energy_price: null, number_energy_price: null, }); @@ -67,7 +66,6 @@ export const emptyGasEnergyPreference = (): GasSourceTypeEnergyPreference => ({ type: "gas", stat_energy_from: "", stat_cost: null, - entity_energy_from: null, entity_energy_price: null, number_energy_price: null, }); @@ -92,7 +90,6 @@ export interface FlowFromGridSourceEnergyPreference { stat_cost: string | null; // Can be used to generate costs if stat_cost omitted - entity_energy_from: string | null; entity_energy_price: string | null; number_energy_price: number | null; } @@ -104,8 +101,7 @@ export interface FlowToGridSourceEnergyPreference { // $ meter stat_compensation: string | null; - // Can be used to generate costs if stat_cost omitted - entity_energy_to: string | null; + // Can be used to generate costs if stat_compensation omitted entity_energy_price: string | null; number_energy_price: number | null; } @@ -141,7 +137,6 @@ export interface GasSourceTypeEnergyPreference { stat_cost: string | null; // Can be used to generate costs if stat_cost omitted - entity_energy_from: string | null; entity_energy_price: string | null; number_energy_price: number | null; unit_of_measurement?: string | null; @@ -358,12 +353,19 @@ const getEnergyData = async ( // Subtract 1 hour from start to get starting point data const startMinHour = addHours(start, -1); + const lengthUnit = hass.config.unit_system.length || ""; + const units: StatisticsUnitConfiguration = { + energy: "kWh", + volume: lengthUnit === "km" ? "m³" : "ft³", + }; + const stats = await fetchStatistics( hass!, startMinHour, end, statIDs, - period + period, + units ); let statsCompare; @@ -385,7 +387,8 @@ const getEnergyData = async ( compareStartMinHour, endCompare, statIDs, - period + period, + units ); } @@ -621,7 +624,7 @@ export const getEnergyGasUnitCategory = ( const statisticIdWithMeta = statisticsMetaData[source.stat_energy_from]; if (statisticIdWithMeta) { return ENERGY_GAS_VOLUME_UNITS.includes( - statisticIdWithMeta.display_unit_of_measurement + statisticIdWithMeta.statistics_unit_of_measurement ) ? "volume" : "energy"; diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index 78da19f6f2..3802b26dca 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -20,10 +20,10 @@ export interface EntityRegistryEntry { entity_category: "config" | "diagnostic" | null; has_entity_name: boolean; original_name?: string; + unique_id: string; } export interface ExtEntityRegistryEntry extends EntityRegistryEntry { - unique_id: string; capabilities: Record; original_icon?: string; device_class?: string; @@ -61,7 +61,7 @@ export interface EntityRegistryEntryUpdateParams { hidden_by: string | null; new_entity_id?: string; options_domain?: string; - options?: SensorEntityOptions | WeatherEntityOptions; + options?: SensorEntityOptions | NumberEntityOptions | WeatherEntityOptions; } export const findBatteryEntity = ( diff --git a/src/data/history.ts b/src/data/history.ts index 8337dead32..3f8777588a 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -1,10 +1,7 @@ import { HassEntities, HassEntity } from "home-assistant-js-websocket"; import { computeDomain } from "../common/entity/compute_domain"; import { computeStateDisplayFromEntityAttributes } from "../common/entity/compute_state_display"; -import { - computeStateName, - computeStateNameFromEntityAttributes, -} from "../common/entity/compute_state_name"; +import { computeStateNameFromEntityAttributes } from "../common/entity/compute_state_name"; import { LocalizeFunc } from "../common/translations/localize"; import { HomeAssistant } from "../types"; import { FrontendLocaleData } from "./translation"; @@ -63,87 +60,6 @@ export interface HistoryResult { timeline: TimelineEntity[]; } -export type StatisticType = "sum" | "min" | "max" | "mean"; - -export interface Statistics { - [statisticId: string]: StatisticValue[]; -} - -export interface StatisticValue { - statistic_id: string; - start: string; - end: string; - last_reset: string | null; - max: number | null; - mean: number | null; - min: number | null; - sum: number | null; - state: number | null; -} - -export interface StatisticsMetaData { - display_unit_of_measurement: string; - statistics_unit_of_measurement: string; - statistic_id: string; - source: string; - name?: string | null; - has_sum: boolean; - has_mean: boolean; -} - -export type StatisticsValidationResult = - | StatisticsValidationResultNoState - | StatisticsValidationResultEntityNotRecorded - | StatisticsValidationResultEntityNoLongerRecorded - | StatisticsValidationResultUnsupportedStateClass - | StatisticsValidationResultUnitsChanged - | StatisticsValidationResultUnsupportedUnitMetadata - | StatisticsValidationResultUnsupportedUnitState; - -export interface StatisticsValidationResultNoState { - type: "no_state"; - data: { statistic_id: string }; -} - -export interface StatisticsValidationResultEntityNoLongerRecorded { - type: "entity_no_longer_recorded"; - data: { statistic_id: string }; -} - -export interface StatisticsValidationResultEntityNotRecorded { - type: "entity_not_recorded"; - data: { statistic_id: string }; -} - -export interface StatisticsValidationResultUnsupportedStateClass { - type: "unsupported_state_class"; - data: { statistic_id: string; state_class: string }; -} - -export interface StatisticsValidationResultUnitsChanged { - type: "units_changed"; - data: { statistic_id: string; state_unit: string; metadata_unit: string }; -} - -export interface StatisticsValidationResultUnsupportedUnitMetadata { - type: "unsupported_unit_metadata"; - data: { - statistic_id: string; - device_class: string; - metadata_unit: string; - supported_unit: string; - }; -} - -export interface StatisticsValidationResultUnsupportedUnitState { - type: "unsupported_unit_state"; - data: { statistic_id: string; device_class: string; metadata_unit: string }; -} - -export interface StatisticsValidationResults { - [statisticId: string]: StatisticsValidationResult[]; -} - export interface HistoryStates { [entityId: string]: EntityHistoryState[]; } @@ -449,132 +365,3 @@ export const computeHistory = ( return { line: unitStates, timeline: timelineDevices }; }; - -// Statistics - -export const getStatisticIds = ( - hass: HomeAssistant, - statistic_type?: "mean" | "sum" -) => - hass.callWS({ - type: "history/list_statistic_ids", - statistic_type, - }); - -export const getStatisticMetadata = ( - hass: HomeAssistant, - statistic_ids?: string[] -) => - hass.callWS({ - type: "recorder/get_statistics_metadata", - statistic_ids, - }); - -export const fetchStatistics = ( - hass: HomeAssistant, - startTime: Date, - endTime?: Date, - statistic_ids?: string[], - period: "5minute" | "hour" | "day" | "month" = "hour" -) => - hass.callWS({ - type: "history/statistics_during_period", - start_time: startTime.toISOString(), - end_time: endTime?.toISOString(), - statistic_ids, - period, - }); - -export const validateStatistics = (hass: HomeAssistant) => - hass.callWS({ - type: "recorder/validate_statistics", - }); - -export const updateStatisticsMetadata = ( - hass: HomeAssistant, - statistic_id: string, - unit_of_measurement: string | null -) => - hass.callWS({ - type: "recorder/update_statistics_metadata", - statistic_id, - unit_of_measurement, - }); - -export const clearStatistics = (hass: HomeAssistant, statistic_ids: string[]) => - hass.callWS({ - type: "recorder/clear_statistics", - statistic_ids, - }); - -export const calculateStatisticSumGrowth = ( - values: StatisticValue[] -): number | null => { - if (!values || values.length < 2) { - return null; - } - const endSum = values[values.length - 1].sum; - if (endSum === null) { - return null; - } - const startSum = values[0].sum; - if (startSum === null) { - return endSum; - } - return endSum - startSum; -}; - -export const calculateStatisticsSumGrowth = ( - data: Statistics, - stats: string[] -): number | null => { - let totalGrowth: number | null = null; - - for (const stat of stats) { - if (!(stat in data)) { - continue; - } - const statGrowth = calculateStatisticSumGrowth(data[stat]); - - if (statGrowth === null) { - continue; - } - if (totalGrowth === null) { - totalGrowth = statGrowth; - } else { - totalGrowth += statGrowth; - } - } - - return totalGrowth; -}; - -export const statisticsHaveType = ( - stats: StatisticValue[], - type: StatisticType -) => stats.some((stat) => stat[type] !== null); - -export const adjustStatisticsSum = ( - hass: HomeAssistant, - statistic_id: string, - start_time: string, - adjustment: number -): Promise => - hass.callWS({ - type: "recorder/adjust_sum_statistics", - statistic_id, - start_time, - adjustment, - }); - -export const getStatisticLabel = ( - hass: HomeAssistant, - statisticsId: string, - statisticsMetaData: StatisticsMetaData | undefined -): string => { - const entity = hass.states[statisticsId]; - if (entity) { - return computeStateName(entity); - } - return statisticsMetaData?.name || statisticsId; -}; diff --git a/src/data/integrations.ts b/src/data/integrations.ts new file mode 100644 index 0000000000..70c799b154 --- /dev/null +++ b/src/data/integrations.ts @@ -0,0 +1,37 @@ +import { HomeAssistant } from "../types"; + +export type IotStandards = "z-wave" | "zigbee" | "homekit" | "matter"; + +export interface Integration { + name?: string; + config_flow?: boolean; + integrations?: Integrations; + iot_standards?: IotStandards[]; + is_built_in?: boolean; + iot_class?: string; +} + +export interface Integrations { + [domain: string]: Integration; +} + +export interface IntegrationDescriptions { + core: { + integration: Integrations; + hardware: Integrations; + helper: Integrations; + translated_name: string[]; + }; + custom: { + integration: Integrations; + hardware: Integrations; + helper: Integrations; + }; +} + +export const getIntegrationDescriptions = ( + hass: HomeAssistant +): Promise => + hass.callWS({ + type: "integration/descriptions", + }); diff --git a/src/data/light.ts b/src/data/light.ts index 93ae0c1d75..a171568451 100644 --- a/src/data/light.ts +++ b/src/data/light.ts @@ -3,76 +3,83 @@ import { HassEntityBase, } from "home-assistant-js-websocket"; -export const enum LightColorModes { +export const enum LightEntityFeature { + EFFECT = 4, + FLASH = 8, + TRANSITION = 32, +} + +export const enum LightColorMode { UNKNOWN = "unknown", ONOFF = "onoff", BRIGHTNESS = "brightness", COLOR_TEMP = "color_temp", - WHITE = "white", HS = "hs", XY = "xy", RGB = "rgb", RGBW = "rgbw", RGBWW = "rgbww", + WHITE = "white", } const modesSupportingColor = [ - LightColorModes.HS, - LightColorModes.XY, - LightColorModes.RGB, - LightColorModes.RGBW, - LightColorModes.RGBWW, + LightColorMode.HS, + LightColorMode.XY, + LightColorMode.RGB, + LightColorMode.RGBW, + LightColorMode.RGBWW, ]; -const modesSupportingDimming = [ +const modesSupportingBrightness = [ ...modesSupportingColor, - LightColorModes.COLOR_TEMP, - LightColorModes.BRIGHTNESS, + LightColorMode.COLOR_TEMP, + LightColorMode.BRIGHTNESS, + LightColorMode.WHITE, ]; -export const SUPPORT_EFFECT = 4; -export const SUPPORT_FLASH = 8; -export const SUPPORT_TRANSITION = 32; - export const lightSupportsColorMode = ( entity: LightEntity, - mode: LightColorModes -) => entity.attributes.supported_color_modes?.includes(mode); + mode: LightColorMode +) => entity.attributes.supported_color_modes?.includes(mode) || false; export const lightIsInColorMode = (entity: LightEntity) => - modesSupportingColor.includes(entity.attributes.color_mode); + (entity.attributes.color_mode && + modesSupportingColor.includes(entity.attributes.color_mode)) || + false; export const lightSupportsColor = (entity: LightEntity) => entity.attributes.supported_color_modes?.some((mode) => modesSupportingColor.includes(mode) ); -export const lightSupportsDimming = (entity: LightEntity) => +export const lightSupportsBrightness = (entity: LightEntity) => entity.attributes.supported_color_modes?.some((mode) => - modesSupportingDimming.includes(mode) - ); + modesSupportingBrightness.includes(mode) + ) || false; -export const getLightCurrentModeRgbColor = (entity: LightEntity): number[] => - entity.attributes.color_mode === LightColorModes.RGBWW +export const getLightCurrentModeRgbColor = ( + entity: LightEntity +): number[] | undefined => + entity.attributes.color_mode === LightColorMode.RGBWW ? entity.attributes.rgbww_color - : entity.attributes.color_mode === LightColorModes.RGBW + : entity.attributes.color_mode === LightColorMode.RGBW ? entity.attributes.rgbw_color : entity.attributes.rgb_color; interface LightEntityAttributes extends HassEntityAttributeBase { - min_mireds: number; - max_mireds: number; - friendly_name: string; - brightness: number; - hs_color: [number, number]; - rgb_color: [number, number, number]; - rgbw_color: [number, number, number, number]; - rgbww_color: [number, number, number, number, number]; - color_temp: number; + min_mireds?: number; + max_mireds?: number; + brightness?: number; + xy_color?: [number, number]; + hs_color?: [number, number]; + color_temp?: number; + rgb_color?: [number, number, number]; + rgbw_color?: [number, number, number, number]; + rgbww_color?: [number, number, number, number, number]; effect?: string; - effect_list: string[] | null; - supported_color_modes: LightColorModes[]; - color_mode: LightColorModes; + effect_list?: string[] | null; + supported_color_modes?: LightColorMode[]; + color_mode?: LightColorMode; } export interface LightEntity extends HassEntityBase { diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts index 28d1c4886f..432cb6167b 100644 --- a/src/data/lovelace.ts +++ b/src/data/lovelace.ts @@ -93,6 +93,8 @@ export interface LovelaceViewConfig { panel?: boolean; background?: string; visible?: boolean | ShowViewConfig[]; + subview?: boolean; + back_path?: string; } export interface LovelaceViewElement extends HTMLElement { diff --git a/src/data/recorder.ts b/src/data/recorder.ts new file mode 100644 index 0000000000..42f9ad5342 --- /dev/null +++ b/src/data/recorder.ts @@ -0,0 +1,247 @@ +import { computeStateName } from "../common/entity/compute_state_name"; +import { HomeAssistant } from "../types"; + +export type StatisticType = "state" | "sum" | "min" | "max" | "mean"; + +export interface Statistics { + [statisticId: string]: StatisticValue[]; +} + +export interface StatisticValue { + statistic_id: string; + start: string; + end: string; + last_reset: string | null; + max: number | null; + mean: number | null; + min: number | null; + sum: number | null; + state: number | null; +} + +export interface StatisticsMetaData { + display_unit_of_measurement: string; + statistics_unit_of_measurement: string; + statistic_id: string; + source: string; + name?: string | null; + has_sum: boolean; + has_mean: boolean; +} + +export type StatisticsValidationResult = + | StatisticsValidationResultNoState + | StatisticsValidationResultEntityNotRecorded + | StatisticsValidationResultEntityNoLongerRecorded + | StatisticsValidationResultUnsupportedStateClass + | StatisticsValidationResultUnitsChanged + | StatisticsValidationResultUnsupportedUnitMetadata + | StatisticsValidationResultUnsupportedUnitState; + +export interface StatisticsValidationResultNoState { + type: "no_state"; + data: { statistic_id: string }; +} + +export interface StatisticsValidationResultEntityNoLongerRecorded { + type: "entity_no_longer_recorded"; + data: { statistic_id: string }; +} + +export interface StatisticsValidationResultEntityNotRecorded { + type: "entity_not_recorded"; + data: { statistic_id: string }; +} + +export interface StatisticsValidationResultUnsupportedStateClass { + type: "unsupported_state_class"; + data: { statistic_id: string; state_class: string }; +} + +export interface StatisticsValidationResultUnitsChanged { + type: "units_changed"; + data: { statistic_id: string; state_unit: string; metadata_unit: string }; +} + +export interface StatisticsValidationResultUnsupportedUnitMetadata { + type: "unsupported_unit_metadata"; + data: { + statistic_id: string; + device_class: string; + metadata_unit: string; + supported_unit: string; + }; +} + +export interface StatisticsUnitConfiguration { + energy?: "Wh" | "kWh" | "MWh"; + power?: "W" | "kW"; + pressure?: + | "Pa" + | "hPa" + | "kPa" + | "bar" + | "cbar" + | "mbar" + | "inHg" + | "psi" + | "mmHg"; + temperature?: "°C" | "°F" | "K"; + volume?: "ft³" | "m³"; +} + +export interface StatisticsValidationResultUnsupportedUnitState { + type: "unsupported_unit_state"; + data: { statistic_id: string; device_class: string; metadata_unit: string }; +} + +export interface StatisticsValidationResults { + [statisticId: string]: StatisticsValidationResult[]; +} + +export const getStatisticIds = ( + hass: HomeAssistant, + statistic_type?: "mean" | "sum" +) => + hass.callWS({ + type: "recorder/list_statistic_ids", + statistic_type, + }); + +export const getStatisticMetadata = ( + hass: HomeAssistant, + statistic_ids?: string[] +) => + hass.callWS({ + type: "recorder/get_statistics_metadata", + statistic_ids, + }); + +export const fetchStatistics = ( + hass: HomeAssistant, + startTime: Date, + endTime?: Date, + statistic_ids?: string[], + period: "5minute" | "hour" | "day" | "month" = "hour", + units?: StatisticsUnitConfiguration +) => + hass.callWS({ + type: "recorder/statistics_during_period", + start_time: startTime.toISOString(), + end_time: endTime?.toISOString(), + statistic_ids, + period, + units, + }); + +export const validateStatistics = (hass: HomeAssistant) => + hass.callWS({ + type: "recorder/validate_statistics", + }); + +export const updateStatisticsMetadata = ( + hass: HomeAssistant, + statistic_id: string, + unit_of_measurement: string | null +) => + hass.callWS({ + type: "recorder/update_statistics_metadata", + statistic_id, + unit_of_measurement, + }); + +export const clearStatistics = (hass: HomeAssistant, statistic_ids: string[]) => + hass.callWS({ + type: "recorder/clear_statistics", + statistic_ids, + }); + +export const calculateStatisticSumGrowth = ( + values: StatisticValue[] +): number | null => { + if (!values || values.length < 2) { + return null; + } + const endSum = values[values.length - 1].sum; + if (endSum === null) { + return null; + } + const startSum = values[0].sum; + if (startSum === null) { + return endSum; + } + return endSum - startSum; +}; + +export const calculateStatisticsSumGrowth = ( + data: Statistics, + stats: string[] +): number | null => { + let totalGrowth: number | null = null; + + for (const stat of stats) { + if (!(stat in data)) { + continue; + } + const statGrowth = calculateStatisticSumGrowth(data[stat]); + + if (statGrowth === null) { + continue; + } + if (totalGrowth === null) { + totalGrowth = statGrowth; + } else { + totalGrowth += statGrowth; + } + } + + return totalGrowth; +}; + +export const statisticsHaveType = ( + stats: StatisticValue[], + type: StatisticType +) => stats.some((stat) => stat[type] !== null); + +const mean_stat_types: readonly StatisticType[] = ["mean", "min", "max"]; +const sum_stat_types: readonly StatisticType[] = ["sum"]; + +export const statisticsMetaHasType = ( + metadata: StatisticsMetaData, + type: StatisticType +) => { + if (mean_stat_types.includes(type) && metadata.has_mean) { + return true; + } + if (sum_stat_types.includes(type) && metadata.has_sum) { + return true; + } + return false; +}; + +export const adjustStatisticsSum = ( + hass: HomeAssistant, + statistic_id: string, + start_time: string, + adjustment: number, + display_unit: string +): Promise => + hass.callWS({ + type: "recorder/adjust_sum_statistics", + statistic_id, + start_time, + adjustment, + display_unit, + }); + +export const getStatisticLabel = ( + hass: HomeAssistant, + statisticsId: string, + statisticsMetaData: StatisticsMetaData | undefined +): string => { + const entity = hass.states[statisticsId]; + if (entity) { + return computeStateName(entity); + } + return statisticsMetaData?.name || statisticsId; +}; diff --git a/src/data/scene.ts b/src/data/scene.ts index f15e7c2562..e8f27d6c26 100644 --- a/src/data/scene.ts +++ b/src/data/scene.ts @@ -14,8 +14,11 @@ export const SCENE_IGNORED_DOMAINS = [ "input_button", "persistent_notification", "person", + "scene", + "schedule", "sensor", "sun", + "update", "weather", "zone", ]; diff --git a/src/data/script.ts b/src/data/script.ts index 1f925cffe3..ec13344714 100644 --- a/src/data/script.ts +++ b/src/data/script.ts @@ -15,7 +15,6 @@ import { Describe, boolean, } from "superstruct"; -import { computeObjectId } from "../common/entity/compute_object_id"; import { navigate } from "../common/navigate"; import { HomeAssistant } from "../types"; import { @@ -278,9 +277,9 @@ export type ActionType = keyof ActionTypes; export const triggerScript = ( hass: HomeAssistant, - entityId: string, + scriptId: string, variables?: Record -) => hass.callService("script", computeObjectId(entityId), variables); +) => hass.callService("script", scriptId, variables); export const canRun = (state: ScriptEntity) => { if (state.state === "off") { diff --git a/src/data/selector.ts b/src/data/selector.ts index 1620f1c306..c82a8e3a06 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -21,6 +21,7 @@ export type Selector = | IconSelector | LocationSelector | MediaSelector + | NavigationSelector | NumberSelector | ObjectSelector | SelectSelector @@ -171,6 +172,11 @@ export interface MediaSelectorValue { }; } +export interface NavigationSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + navigation: {}; +} + export interface NumberSelector { number: { min?: number; @@ -189,6 +195,7 @@ export interface ObjectSelector { export interface SelectOption { value: string; label: string; + disabled?: boolean; } export interface SelectSelector { diff --git a/src/data/supported_brands.ts b/src/data/supported_brands.ts index 8f0afcf884..62691ed275 100644 --- a/src/data/supported_brands.ts +++ b/src/data/supported_brands.ts @@ -1,6 +1,13 @@ -import { SupportedBrandObj } from "../dialogs/config-flow/step-flow-pick-handler"; import type { HomeAssistant } from "../types"; +export interface SupportedBrandObj { + name: string; + slug: string; + is_add?: boolean; + is_helper?: boolean; + supported_flows: string[]; +} + export type SupportedBrandHandler = Record; export const getSupportedBrands = (hass: HomeAssistant) => diff --git a/src/data/zha.ts b/src/data/zha.ts index fb0e2b0f31..55bf16d7c4 100644 --- a/src/data/zha.ts +++ b/src/data/zha.ts @@ -309,7 +309,7 @@ export const fetchCommandsForCluster = ( cluster_type: clusterType, }); -export const fetchClustersForZhaNode = ( +export const fetchClustersForZhaDevice = ( hass: HomeAssistant, ieeeAddress: string ): Promise => diff --git a/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts b/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts index aaced030d6..8329685a6e 100644 --- a/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts +++ b/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts @@ -170,7 +170,6 @@ class DialogConfigEntrySystemOptions extends LitElement { ), }); } - this._params!.entryUpdated(result.config_entry); this.closeDialog(); } catch (err: any) { this._error = err.message || "Unknown error"; diff --git a/src/dialogs/config-entry-system-options/show-dialog-config-entry-system-options.ts b/src/dialogs/config-entry-system-options/show-dialog-config-entry-system-options.ts index 77415e02cd..0ea3b9d966 100644 --- a/src/dialogs/config-entry-system-options/show-dialog-config-entry-system-options.ts +++ b/src/dialogs/config-entry-system-options/show-dialog-config-entry-system-options.ts @@ -5,7 +5,6 @@ import { IntegrationManifest } from "../../data/integration"; export interface ConfigEntrySystemOptionsDialogParams { entry: ConfigEntry; manifest?: IntegrationManifest; - entryUpdated(entry: ConfigEntry): void; } export const loadConfigEntrySystemOptionsDialog = () => diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 7654a0b506..3c148b7172 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -18,9 +18,7 @@ import { AreaRegistryEntry, subscribeAreaRegistry, } from "../../data/area_registry"; -import { fetchConfigFlowInProgress } from "../../data/config_flow"; import { - DataEntryFlowProgress, DataEntryFlowStep, subscribeDataEntryFlowProgressed, } from "../../data/data_entry_flow"; @@ -28,14 +26,12 @@ import { DeviceRegistryEntry, subscribeDeviceRegistry, } from "../../data/device_registry"; -import { fetchIntegrationManifest } from "../../data/integration"; import { haStyleDialog } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; import { documentationUrl } from "../../util/documentation-url"; import { showAlertDialog } from "../generic/show-dialog-box"; import { DataEntryFlowDialogParams, - FlowHandlers, LoadingReason, } from "./show-dialog-data-entry-flow"; import "./step-flow-abort"; @@ -44,8 +40,6 @@ import "./step-flow-external"; import "./step-flow-form"; import "./step-flow-loading"; import "./step-flow-menu"; -import "./step-flow-pick-flow"; -import "./step-flow-pick-handler"; import "./step-flow-progress"; let instance = 0; @@ -86,12 +80,8 @@ class DataEntryFlowDialog extends LitElement { @state() private _areas?: AreaRegistryEntry[]; - @state() private _handlers?: FlowHandlers; - @state() private _handler?: string; - @state() private _flowsInProgress?: DataEntryFlowProgress[]; - private _unsubAreas?: UnsubscribeFunc; private _unsubDevices?: UnsubscribeFunc; @@ -102,15 +92,39 @@ class DataEntryFlowDialog extends LitElement { this._params = params; this._instance = instance++; - if (params.startFlowHandler) { - this._checkFlowsInProgress(params.startFlowHandler); - return; - } + const curInstance = this._instance; + let step: DataEntryFlowStep; - if (params.continueFlowId) { + if (params.startFlowHandler) { + this._loading = "loading_flow"; + this._handler = params.startFlowHandler; + try { + step = await this._params!.flowConfig.createFlow( + this.hass, + params.startFlowHandler + ); + } catch (err: any) { + this.closeDialog(); + let message = err.message || err.body || "Unknown error"; + if (typeof message !== "string") { + message = JSON.stringify(message); + } + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.integrations.config_flow.error" + ), + text: `${this.hass.localize( + "ui.panel.config.integrations.config_flow.could_not_load" + )}: ${message}`, + }); + return; + } + // Happens if second showDialog called + if (curInstance !== this._instance) { + return; + } + } else if (params.continueFlowId) { this._loading = "loading_flow"; - const curInstance = this._instance; - let step: DataEntryFlowStep; try { step = await params.flowConfig.fetchFlow( this.hass, @@ -132,32 +146,17 @@ class DataEntryFlowDialog extends LitElement { }); return; } - - // Happens if second showDialog called - if (curInstance !== this._instance) { - return; - } - - this._processStep(step); - this._loading = undefined; + } else { return; } - // Create a new config flow. Show picker - if (!params.flowConfig.getFlowHandlers) { - throw new Error("No getFlowHandlers defined in flow config"); + // Happens if second showDialog called + if (curInstance !== this._instance) { + return; } - this._step = null; - // We only load the handlers once - if (this._handlers === undefined) { - this._loading = "loading_handlers"; - try { - this._handlers = await params.flowConfig.getFlowHandlers(this.hass); - } finally { - this._loading = undefined; - } - } + this._processStep(step); + this._loading = undefined; } public closeDialog() { @@ -185,7 +184,6 @@ class DataEntryFlowDialog extends LitElement { this._step = undefined; this._params = undefined; this._devices = undefined; - this._flowsInProgress = undefined; this._handler = undefined; if (this._unsubAreas) { this._unsubAreas(); @@ -218,15 +216,12 @@ class DataEntryFlowDialog extends LitElement { hideActions >
- ${this._loading || - (this._step === null && - this._handlers === undefined && - this._handler === undefined) + ${this._loading || this._step === null ? html` @@ -273,24 +268,7 @@ class DataEntryFlowDialog extends LitElement { dialogAction="close" >
- ${this._step === null - ? this._handler - ? html`` - : // Show handler picker - html` - - ` - : this._step.type === "form" + ${this._step.type === "form" ? html` flow.handler === handler); - - if (!flowsInProgress.length) { - // No flows in progress, create a new flow - this._loading = "loading_flow"; - let step: DataEntryFlowStep; - try { - step = await this._params!.flowConfig.createFlow(this.hass, handler); - } catch (err: any) { - this.closeDialog(); - const message = - err?.status_code === 404 - ? this.hass.localize( - "ui.panel.config.integrations.config_flow.no_config_flow" - ) - : `${this.hass.localize( - "ui.panel.config.integrations.config_flow.could_not_load" - )}: ${err?.body?.message || err?.message}`; - - showAlertDialog(this, { - title: this.hass.localize( - "ui.panel.config.integrations.config_flow.error" - ), - text: message, - }); - return; - } finally { - this._handler = undefined; - } - this._processStep(step); - if (this._params!.manifest === undefined) { - try { - this._params!.manifest = await fetchIntegrationManifest( - this.hass, - this._params?.domain || step.handler - ); - } catch (_) { - // No manifest - this._params!.manifest = null; - } - } - } else { - this._step = null; - this._flowsInProgress = flowsInProgress; - } - this._loading = undefined; - } - - private _handlerPicked(ev) { - this._checkFlowsInProgress(ev.detail.handler); - } - private async _processStep( step: DataEntryFlowStep | undefined | Promise ): Promise { diff --git a/src/dialogs/config-flow/show-dialog-config-flow.ts b/src/dialogs/config-flow/show-dialog-config-flow.ts index c1d56c6d73..84d332492c 100644 --- a/src/dialogs/config-flow/show-dialog-config-flow.ts +++ b/src/dialogs/config-flow/show-dialog-config-flow.ts @@ -3,11 +3,9 @@ import { createConfigFlow, deleteConfigFlow, fetchConfigFlow, - getConfigFlowHandlers, handleConfigFlowStep, } from "../../data/config_flow"; import { domainToName } from "../../data/integration"; -import { getSupportedBrands } from "../../data/supported_brands"; import { DataEntryFlowDialogParams, loadDataEntryFlowDialog, @@ -22,16 +20,6 @@ export const showConfigFlowDialog = ( ): void => showFlowDialog(element, dialogParams, { loadDevicesAndAreas: true, - getFlowHandlers: async (hass) => { - const [integrations, helpers, supportedBrands] = await Promise.all([ - getConfigFlowHandlers(hass, "integration"), - getConfigFlowHandlers(hass, "helper"), - getSupportedBrands(hass), - hass.loadBackendTranslation("title", undefined, true), - ]); - - return { integrations, helpers, supportedBrands }; - }, createFlow: async (hass, handler) => { const [step] = await Promise.all([ createConfigFlow(hass, handler), diff --git a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts index 1c5f407c69..5a6239cb3b 100644 --- a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts @@ -22,8 +22,6 @@ export interface FlowHandlers { export interface FlowConfig { loadDevicesAndAreas: boolean; - getFlowHandlers?: (hass: HomeAssistant) => Promise; - createFlow(hass: HomeAssistant, handler: string): Promise; fetchFlow(hass: HomeAssistant, flowId: string): Promise; diff --git a/src/dialogs/config-flow/step-flow-abort.ts b/src/dialogs/config-flow/step-flow-abort.ts index e1909d0bfe..08b8a46df1 100644 --- a/src/dialogs/config-flow/step-flow-abort.ts +++ b/src/dialogs/config-flow/step-flow-abort.ts @@ -12,10 +12,10 @@ import { DataEntryFlowStepAbort } from "../../data/data_entry_flow"; import { HomeAssistant } from "../../types"; 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"; +import { domainToName } from "../../data/integration"; +import { showConfirmationDialog } from "../generic/show-dialog-box"; @customElement("step-flow-abort") class StepFlowAbort extends LitElement { @@ -56,11 +56,16 @@ class StepFlowAbort extends LitElement { private async _handleMissingCreds() { const confirm = await showConfirmationDialog(this, { title: this.hass.localize( + "ui.panel.config.integrations.config_flow.missing_credentials_title" + ), + text: this.hass.localize( "ui.panel.config.integrations.config_flow.missing_credentials", { integration: domainToName(this.hass.localize, this.domain), } ), + confirmText: this.hass.localize("ui.common.yes"), + dismissText: this.hass.localize("ui.common.no"), }); this._flowDone(); if (!confirm) { diff --git a/src/dialogs/config-flow/step-flow-pick-flow.ts b/src/dialogs/config-flow/step-flow-pick-flow.ts deleted file mode 100644 index fd24243a0b..0000000000 --- a/src/dialogs/config-flow/step-flow-pick-flow.ts +++ /dev/null @@ -1,130 +0,0 @@ -import "@polymer/paper-item"; -import "@polymer/paper-item/paper-icon-item"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-item/paper-item-body"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property } from "lit/decorators"; -import { fireEvent } from "../../common/dom/fire_event"; -import "../../components/ha-icon-next"; -import { localizeConfigFlowTitle } from "../../data/config_flow"; -import { DataEntryFlowProgress } from "../../data/data_entry_flow"; -import { domainToName } from "../../data/integration"; -import { HomeAssistant } from "../../types"; -import { brandsUrl } from "../../util/brands-url"; -import { FlowConfig } from "./show-dialog-data-entry-flow"; -import { configFlowContentStyles } from "./styles"; - -@customElement("step-flow-pick-flow") -class StepFlowPickFlow extends LitElement { - public flowConfig!: FlowConfig; - - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) - public flowsInProgress!: DataEntryFlowProgress[]; - - @property() public handler!: string; - - protected render(): TemplateResult { - return html` -

- ${this.hass.localize( - "ui.panel.config.integrations.config_flow.pick_flow_step.title" - )} -

- -
- ${this.flowsInProgress.map( - (flow) => html` - - - - ${localizeConfigFlowTitle(this.hass.localize, flow)} - - - ` - )} - - - ${this.hass.localize( - "ui.panel.config.integrations.config_flow.pick_flow_step.new_flow", - "integration", - domainToName(this.hass.localize, this.handler) - )} - - - -
- `; - } - - private _startNewFlowPicked(ev) { - this._startFlow(ev.currentTarget.handler); - } - - private _startFlow(handler: string) { - fireEvent(this, "flow-update", { - stepPromise: this.flowConfig.createFlow(this.hass, handler), - }); - } - - private _flowInProgressPicked(ev) { - const flow: DataEntryFlowProgress = ev.currentTarget.flow; - fireEvent(this, "flow-update", { - stepPromise: this.flowConfig.fetchFlow(this.hass, flow.flow_id), - }); - } - - static get styles(): CSSResultGroup { - return [ - configFlowContentStyles, - css` - img { - width: 40px; - height: 40px; - } - ha-icon-next { - margin-right: 8px; - } - div { - overflow: auto; - max-height: 600px; - margin: 16px 0; - } - h2 { - padding-inline-end: 66px; - direction: var(--direction); - } - @media all and (max-height: 900px) { - div { - max-height: calc(100vh - 134px); - } - } - paper-icon-item, - paper-item { - cursor: pointer; - margin-bottom: 4px; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "step-flow-pick-flow": StepFlowPickFlow; - } -} diff --git a/src/dialogs/config-flow/step-flow-pick-handler.ts b/src/dialogs/config-flow/step-flow-pick-handler.ts deleted file mode 100644 index 21fa545da3..0000000000 --- a/src/dialogs/config-flow/step-flow-pick-handler.ts +++ /dev/null @@ -1,372 +0,0 @@ -import "@material/mwc-list/mwc-list"; -import "@material/mwc-list/mwc-list-item"; -import Fuse from "fuse.js"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { styleMap } from "lit/directives/style-map"; -import memoizeOne from "memoize-one"; -import { isComponentLoaded } from "../../common/config/is_component_loaded"; -import { fireEvent } from "../../common/dom/fire_event"; -import { protocolIntegrationPicked } from "../../common/integrations/protocolIntegrationPicked"; -import { navigate } from "../../common/navigate"; -import { caseInsensitiveStringCompare } from "../../common/string/compare"; -import { LocalizeFunc } from "../../common/translations/localize"; -import "../../components/ha-icon-next"; -import "../../components/search-input"; -import { domainToName } from "../../data/integration"; -import { haStyleScrollbar } from "../../resources/styles"; -import { HomeAssistant } from "../../types"; -import { brandsUrl } from "../../util/brands-url"; -import { documentationUrl } from "../../util/documentation-url"; -import { showConfirmationDialog } from "../generic/show-dialog-box"; -import { FlowHandlers } from "./show-dialog-data-entry-flow"; -import { configFlowContentStyles } from "./styles"; - -interface HandlerObj { - name: string; - slug: string; - is_add?: boolean; - is_helper?: boolean; -} - -export interface SupportedBrandObj extends HandlerObj { - supported_flows: string[]; -} - -declare global { - // for fire event - interface HASSDomEvents { - "handler-picked": { - handler: string; - }; - } -} - -@customElement("step-flow-pick-handler") -class StepFlowPickHandler extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public handlers!: FlowHandlers; - - @property() public initialFilter?: string; - - @state() private _filter?: string; - - private _width?: number; - - private _height?: number; - - private _filterHandlers = memoizeOne( - ( - h: FlowHandlers, - filter?: string, - _localize?: LocalizeFunc - ): [(HandlerObj | SupportedBrandObj)[], HandlerObj[]] => { - const integrations: (HandlerObj | SupportedBrandObj)[] = - h.integrations.map((handler) => ({ - name: domainToName(this.hass.localize, handler), - slug: handler, - })); - - for (const [domain, domainBrands] of Object.entries(h.supportedBrands)) { - for (const [slug, name] of Object.entries(domainBrands)) { - integrations.push({ - slug, - name, - supported_flows: [domain], - }); - } - } - - if (filter) { - const options: Fuse.IFuseOptions = { - keys: ["name", "slug"], - isCaseSensitive: false, - minMatchCharLength: 2, - threshold: 0.2, - }; - const helpers: HandlerObj[] = h.helpers.map((handler) => ({ - name: domainToName(this.hass.localize, handler), - slug: handler, - is_helper: true, - })); - return [ - new Fuse(integrations, options) - .search(filter) - .map((result) => result.item), - new Fuse(helpers, options) - .search(filter) - .map((result) => result.item), - ]; - } - return [ - integrations.sort((a, b) => - caseInsensitiveStringCompare(a.name, b.name) - ), - [], - ]; - } - ); - - protected render(): TemplateResult { - const [integrations, helpers] = this._getHandlers(); - - const addDeviceRows: HandlerObj[] = ["zha", "zwave_js"] - .filter((domain) => isComponentLoaded(this.hass, domain)) - .map((domain) => ({ - name: this.hass.localize( - `ui.panel.config.integrations.add_${domain}_device` - ), - slug: domain, - is_add: true, - })) - .sort((a, b) => caseInsensitiveStringCompare(a.name, b.name)); - - return html` -

${this.hass.localize("ui.panel.config.integrations.new")}

- - - ${addDeviceRows.length - ? html` - ${addDeviceRows.map((handler) => this._renderRow(handler))} - - ` - : ""} - ${integrations.length - ? integrations.map((handler) => this._renderRow(handler)) - : html` -

- ${this.hass.localize( - "ui.panel.config.integrations.note_about_integrations" - )}
- ${this.hass.localize( - "ui.panel.config.integrations.note_about_website_reference" - )}${this.hass.localize( - "ui.panel.config.integrations.home_assistant_website" - )}. -

- `} - ${helpers.length - ? html` - - ${helpers.map((handler) => this._renderRow(handler))} - ` - : ""} -
- `; - } - - private _renderRow(handler: HandlerObj) { - return html` - - - ${handler.name} ${handler.is_helper ? " (helper)" : ""} - ${handler.is_add ? "" : html``} - - `; - } - - public willUpdate(changedProps: PropertyValues): void { - super.willUpdate(changedProps); - if (this._filter === undefined && this.initialFilter !== undefined) { - this._filter = this.initialFilter; - } - if (this.initialFilter !== undefined && this._filter === "") { - this.initialFilter = undefined; - this._filter = ""; - this._width = undefined; - this._height = undefined; - } else if ( - this.hasUpdated && - changedProps.has("_filter") && - (!this._width || !this._height) - ) { - // Store the width and height so that when we search, box doesn't jump - const boundingRect = - this.shadowRoot!.querySelector("mwc-list")!.getBoundingClientRect(); - this._width = boundingRect.width; - this._height = boundingRect.height; - } - } - - protected firstUpdated(changedProps) { - super.firstUpdated(changedProps); - setTimeout( - () => this.shadowRoot!.querySelector("search-input")!.focus(), - 0 - ); - } - - private _getHandlers() { - return this._filterHandlers( - this.handlers, - this._filter, - this.hass.localize - ); - } - - private async _filterChanged(e) { - this._filter = e.detail.value; - } - - private async _handlerPicked(ev) { - const handler: HandlerObj | SupportedBrandObj = ev.currentTarget.handler; - - if (handler.is_add) { - this._handleAddPicked(handler.slug); - return; - } - - if (handler.is_helper) { - navigate(`/config/helpers/add?domain=${handler.slug}`); - // This closes dialog. - fireEvent(this, "flow-update"); - return; - } - - if ("supported_flows" in handler) { - const slug = handler.supported_flows[0]; - - showConfirmationDialog(this, { - text: this.hass.localize( - "ui.panel.config.integrations.config_flow.supported_brand_flow", - { - supported_brand: handler.name, - flow_domain_name: domainToName(this.hass.localize, slug), - } - ), - confirm: () => { - if (["zha", "zwave_js"].includes(slug)) { - this._handleAddPicked(slug); - return; - } - - fireEvent(this, "handler-picked", { - handler: slug, - }); - }, - }); - - return; - } - - fireEvent(this, "handler-picked", { - handler: handler.slug, - }); - } - - private async _handleAddPicked(slug: string): Promise { - await protocolIntegrationPicked(this, this.hass, slug); - // This closes dialog. - fireEvent(this, "flow-update"); - } - - private _maybeSubmit(ev: KeyboardEvent) { - if (ev.key !== "Enter") { - return; - } - - const handlers = this._getHandlers(); - - if (handlers.length > 0) { - fireEvent(this, "handler-picked", { - handler: handlers[0][0].slug, - }); - } - } - - static get styles(): CSSResultGroup { - return [ - configFlowContentStyles, - haStyleScrollbar, - css` - img { - width: 40px; - height: 40px; - } - search-input { - display: block; - margin: 16px 16px 0; - } - ha-icon-next { - margin-right: 8px; - } - mwc-list { - overflow: auto; - max-height: 600px; - } - .divider { - border-bottom-color: var(--divider-color); - } - h2 { - padding-inline-end: 66px; - direction: var(--direction); - } - @media all and (max-height: 900px) { - mwc-list { - max-height: calc(100vh - 134px); - } - } - p { - text-align: center; - padding: 16px; - margin: 0; - } - p > a { - color: var(--primary-color); - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "step-flow-pick-handler": StepFlowPickHandler; - } -} diff --git a/src/dialogs/generic/dialog-box.ts b/src/dialogs/generic/dialog-box.ts index 27da086a0b..33e5edcbad 100644 --- a/src/dialogs/generic/dialog-box.ts +++ b/src/dialogs/generic/dialog-box.ts @@ -2,6 +2,7 @@ import "@material/mwc-button/mwc-button"; import { mdiAlertOutline } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, query, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; import { fireEvent } from "../../common/dom/fire_event"; import "../../components/ha-dialog"; @@ -96,6 +97,9 @@ class DialogBox extends LitElement { @click=${this._confirm} ?dialogInitialFocus=${!this._params.prompt} slot="primaryAction" + class=${classMap({ + destructive: this._params.destructive || false, + })} > ${this._params.confirmText ? this._params.confirmText @@ -153,6 +157,9 @@ class DialogBox extends LitElement { .secondary { color: var(--secondary-text-color); } + .destructive { + --mdc-theme-primary: var(--error-color); + } ha-dialog { --mdc-dialog-heading-ink-color: var(--primary-text-color); --mdc-dialog-content-ink-color: var(--primary-text-color); diff --git a/src/dialogs/generic/show-dialog-box.ts b/src/dialogs/generic/show-dialog-box.ts index 3180a381f4..6abc0e1dda 100644 --- a/src/dialogs/generic/show-dialog-box.ts +++ b/src/dialogs/generic/show-dialog-box.ts @@ -16,6 +16,7 @@ export interface ConfirmationDialogParams extends BaseDialogBoxParams { dismissText?: string; confirm?: () => void; cancel?: () => void; + destructive?: boolean; } export interface PromptDialogParams extends BaseDialogBoxParams { diff --git a/src/dialogs/more-info/controls/more-info-climate.ts b/src/dialogs/more-info/controls/more-info-climate.ts index 2a66080055..03b94c3827 100644 --- a/src/dialogs/more-info/controls/more-info-climate.ts +++ b/src/dialogs/more-info/controls/more-info-climate.ts @@ -377,12 +377,14 @@ class MoreInfoClimate extends LitElement { private _handlePresetmodeChanged(ev) { const newVal = ev.target.value || null; - this._callServiceHelper( - this.stateObj!.attributes.preset_mode, - newVal, - "set_preset_mode", - { preset_mode: newVal } - ); + if (newVal) { + this._callServiceHelper( + this.stateObj!.attributes.preset_mode, + newVal, + "set_preset_mode", + { preset_mode: newVal } + ); + } } private async _callServiceHelper( diff --git a/src/dialogs/more-info/controls/more-info-cover.ts b/src/dialogs/more-info/controls/more-info-cover.ts index 61faf09af2..6f6125578b 100644 --- a/src/dialogs/more-info/controls/more-info-cover.ts +++ b/src/dialogs/more-info/controls/more-info-cover.ts @@ -1,19 +1,29 @@ import { css, CSSResult, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { attributeClassNames } from "../../../common/entity/attribute_class_names"; -import { featureClassNames } from "../../../common/entity/feature_class_names"; +import { + FeatureClassNames, + featureClassNames, +} from "../../../common/entity/feature_class_names"; +import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-attributes"; import "../../../components/ha-cover-tilt-controls"; import "../../../components/ha-labeled-slider"; import { CoverEntity, - FEATURE_CLASS_NAMES, + CoverEntityFeature, isTiltOnly, - supportsSetPosition, - supportsSetTiltPosition, } from "../../../data/cover"; import { HomeAssistant } from "../../../types"; +export const FEATURE_CLASS_NAMES: FeatureClassNames = { + [CoverEntityFeature.SET_POSITION]: "has-set_position", + [CoverEntityFeature.OPEN_TILT]: "has-open_tilt", + [CoverEntityFeature.CLOSE_TILT]: "has-close_tilt", + [CoverEntityFeature.STOP_TILT]: "has-stop_tilt", + [CoverEntityFeature.SET_TILT_POSITION]: "has-set_tilt_position", +}; + @customElement("more-info-cover") class MoreInfoCover extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -34,13 +44,16 @@ class MoreInfoCover extends LitElement { .caption=${this.hass.localize("ui.card.cover.position")} pin="" .value=${this.stateObj.attributes.current_position} - .disabled=${!supportsSetPosition(this.stateObj)} + .disabled=${!supportsFeature( + this.stateObj, + CoverEntityFeature.SET_POSITION + )} @change=${this._coverPositionSliderChanged} >
- ${supportsSetTiltPosition(this.stateObj) + ${supportsFeature(this.stateObj, CoverEntityFeature.SET_TILT_POSITION) ? // Either render the labeled slider and put the tilt buttons into its slot // or (if tilt position is not supported and therefore no slider is shown) // render a title
(same style as for a labeled slider) and directly put diff --git a/src/dialogs/more-info/controls/more-info-light.ts b/src/dialogs/more-info/controls/more-info-light.ts index 29fb2644d8..be85eb7921 100644 --- a/src/dialogs/more-info/controls/more-info-light.ts +++ b/src/dialogs/more-info/controls/more-info-light.ts @@ -20,13 +20,13 @@ import "../../../components/ha-labeled-slider"; import "../../../components/ha-select"; import { getLightCurrentModeRgbColor, - LightColorModes, + LightColorMode, LightEntity, + LightEntityFeature, lightIsInColorMode, lightSupportsColor, lightSupportsColorMode, - lightSupportsDimming, - SUPPORT_EFFECT, + lightSupportsBrightness, } from "../../../data/light"; import type { HomeAssistant } from "../../../types"; @@ -56,7 +56,7 @@ class MoreInfoLight extends LitElement { @state() private _colorPickerColor?: [number, number, number]; - @state() private _mode?: "color" | LightColorModes; + @state() private _mode?: "color" | LightColorMode; protected render(): TemplateResult { if (!this.hass || !this.stateObj) { @@ -65,29 +65,29 @@ class MoreInfoLight extends LitElement { const supportsTemp = lightSupportsColorMode( this.stateObj, - LightColorModes.COLOR_TEMP + LightColorMode.COLOR_TEMP ); const supportsWhite = lightSupportsColorMode( this.stateObj, - LightColorModes.WHITE + LightColorMode.WHITE ); const supportsRgbww = lightSupportsColorMode( this.stateObj, - LightColorModes.RGBWW + LightColorMode.RGBWW ); const supportsRgbw = !supportsRgbww && - lightSupportsColorMode(this.stateObj, LightColorModes.RGBW); + lightSupportsColorMode(this.stateObj, LightColorMode.RGBW); const supportsColor = supportsRgbww || supportsRgbw || lightSupportsColor(this.stateObj); return html`
- ${lightSupportsDimming(this.stateObj) + ${lightSupportsBrightness(this.stateObj) ? html` @@ -260,31 +260,31 @@ class MoreInfoLight extends LitElement { let brightnessAdjust = 100; this._brightnessAdjusted = undefined; if ( - stateObj.attributes.color_mode === LightColorModes.RGB && - !lightSupportsColorMode(stateObj, LightColorModes.RGBWW) && - !lightSupportsColorMode(stateObj, LightColorModes.RGBW) + stateObj.attributes.color_mode === LightColorMode.RGB && + !lightSupportsColorMode(stateObj, LightColorMode.RGBWW) && + !lightSupportsColorMode(stateObj, LightColorMode.RGBW) ) { - const maxVal = Math.max(...stateObj.attributes.rgb_color); + const maxVal = Math.max(...stateObj.attributes.rgb_color!); if (maxVal < 255) { this._brightnessAdjusted = maxVal; brightnessAdjust = (this._brightnessAdjusted / 255) * 100; } } this._brightnessSliderValue = Math.round( - (stateObj.attributes.brightness * brightnessAdjust) / 255 + ((stateObj.attributes.brightness || 0) * brightnessAdjust) / 255 ); this._ctSliderValue = stateObj.attributes.color_temp; this._wvSliderValue = - stateObj.attributes.color_mode === LightColorModes.RGBW - ? Math.round((stateObj.attributes.rgbw_color[3] * 100) / 255) + stateObj.attributes.color_mode === LightColorMode.RGBW + ? Math.round((stateObj.attributes.rgbw_color![3] * 100) / 255) : undefined; this._cwSliderValue = - stateObj.attributes.color_mode === LightColorModes.RGBWW - ? Math.round((stateObj.attributes.rgbww_color[3] * 100) / 255) + stateObj.attributes.color_mode === LightColorMode.RGBWW + ? Math.round((stateObj.attributes.rgbww_color![3] * 100) / 255) : undefined; this._wwSliderValue = - stateObj.attributes.color_mode === LightColorModes.RGBWW - ? Math.round((stateObj.attributes.rgbww_color[4] * 100) / 255) + stateObj.attributes.color_mode === LightColorMode.RGBWW + ? Math.round((stateObj.attributes.rgbww_color![4] * 100) / 255) : undefined; const currentRgbColor = getLightCurrentModeRgbColor(stateObj); @@ -307,10 +307,10 @@ class MoreInfoLight extends LitElement { (supportsTemp: boolean, supportsWhite: boolean) => { const modes = [{ label: "Color", value: "color" }]; if (supportsTemp) { - modes.push({ label: "Temperature", value: LightColorModes.COLOR_TEMP }); + modes.push({ label: "Temperature", value: LightColorMode.COLOR_TEMP }); } if (supportsWhite) { - modes.push({ label: "White", value: LightColorModes.WHITE }); + modes.push({ label: "White", value: LightColorMode.WHITE }); } return modes; } @@ -342,7 +342,7 @@ class MoreInfoLight extends LitElement { this._brightnessSliderValue = bri; - if (this._mode === LightColorModes.WHITE) { + if (this._mode === LightColorMode.WHITE) { this.hass.callService("light", "turn_on", { entity_id: this.stateObj!.entity_id, white: Math.min(255, Math.round((bri * 255) / 100)), @@ -486,7 +486,7 @@ class MoreInfoLight extends LitElement { } private _setRgbWColor(rgbColor: [number, number, number]) { - if (lightSupportsColorMode(this.stateObj!, LightColorModes.RGBWW)) { + if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGBWW)) { const rgbww_color: [number, number, number, number, number] = this .stateObj!.attributes.rgbww_color ? [...this.stateObj!.attributes.rgbww_color] @@ -495,7 +495,7 @@ class MoreInfoLight extends LitElement { entity_id: this.stateObj!.entity_id, rgbww_color: rgbColor.concat(rgbww_color.slice(3)), }); - } else if (lightSupportsColorMode(this.stateObj!, LightColorModes.RGBW)) { + } else if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGBW)) { const rgbw_color: [number, number, number, number] = this.stateObj! .attributes.rgbw_color ? [...this.stateObj!.attributes.rgbw_color] @@ -524,8 +524,8 @@ class MoreInfoLight extends LitElement { ]; if ( - lightSupportsColorMode(this.stateObj!, LightColorModes.RGBWW) || - lightSupportsColorMode(this.stateObj!, LightColorModes.RGBW) + lightSupportsColorMode(this.stateObj!, LightColorMode.RGBWW) || + lightSupportsColorMode(this.stateObj!, LightColorMode.RGBW) ) { this._setRgbWColor( this._colorBrightnessSliderValue @@ -535,7 +535,7 @@ class MoreInfoLight extends LitElement { ) : [ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b] ); - } else if (lightSupportsColorMode(this.stateObj!, LightColorModes.RGB)) { + } else if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGB)) { const rgb_color: [number, number, number] = [ ev.detail.rgb.r, ev.detail.rgb.g, diff --git a/src/dialogs/more-info/ha-more-info-dialog.ts b/src/dialogs/more-info/ha-more-info-dialog.ts index f07a3c3863..d47a02fbad 100644 --- a/src/dialogs/more-info/ha-more-info-dialog.ts +++ b/src/dialogs/more-info/ha-more-info-dialog.ts @@ -90,7 +90,7 @@ export class MoreInfoDialog extends LitElement { const stateObj = this.hass.states[entityId]; const domain = computeDomain(entityId); - const name = stateObj ? computeStateName(stateObj) : entityId; + const name = (stateObj && computeStateName(stateObj)) || entityId; const tabs = this._getTabs(entityId, this.hass.user!.is_admin); return html` diff --git a/src/external_app/external_app_entrypoint.ts b/src/external_app/external_app_entrypoint.ts index 46d5a39dee..0f059a11ae 100644 --- a/src/external_app/external_app_entrypoint.ts +++ b/src/external_app/external_app_entrypoint.ts @@ -7,7 +7,7 @@ This is the entry point for providing external app stuff from app entrypoint. import { fireEvent } from "../common/dom/fire_event"; import { HomeAssistantMain } from "../layouts/home-assistant-main"; -import type { EMExternalMessageCommands } from "./external_messaging"; +import type { EMIncomingMessageCommands } from "./external_messaging"; export const attachExternalToApp = (hassMainEl: HomeAssistantMain) => { window.addEventListener("haptic", (ev) => @@ -24,7 +24,7 @@ export const attachExternalToApp = (hassMainEl: HomeAssistantMain) => { const handleExternalMessage = ( hassMainEl: HomeAssistantMain, - msg: EMExternalMessageCommands + msg: EMIncomingMessageCommands ): boolean => { const bus = hassMainEl.hass.auth.external!; diff --git a/src/external_app/external_messaging.ts b/src/external_app/external_messaging.ts index fdab1ab2eb..2bf8fa34fb 100644 --- a/src/external_app/external_messaging.ts +++ b/src/external_app/external_messaging.ts @@ -8,7 +8,6 @@ interface CommandInFlight { export interface EMMessage { id?: number; type: string; - payload?: unknown; } interface EMError { @@ -30,34 +29,120 @@ interface EMMessageResultError { error: EMError; } -interface EMExternalMessageRestart { +interface EMOutgoingMessageConfigGet extends EMMessage { + type: "config/get"; +} + +interface EMOutgoingMessageMatterCommission extends EMMessage { + type: "matter/commission"; +} + +type EMOutgoingMessageWithAnswer = { + "config/get": { + request: EMOutgoingMessageConfigGet; + response: ExternalConfig; + }; + "matter/commission": { + request: EMOutgoingMessageMatterCommission; + response: { + code: string; + }; + }; +}; + +interface EMOutgoingMessageExoplayerPlayHLS extends EMMessage { + type: "exoplayer/play_hls"; + payload: { + url: string; + muted: boolean; + }; +} +interface EMOutgoingMessageExoplayerResize extends EMMessage { + type: "exoplayer/resize"; + payload: { + left: number; + top: number; + right: number; + bottom: number; + }; +} + +interface EMOutgoingMessageExoplayerStop extends EMMessage { + type: "exoplayer/stop"; +} + +interface EMOutgoingMessageThemeUpdate extends EMMessage { + type: "theme-update"; +} + +interface EMOutgoingMessageHaptic extends EMMessage { + type: "haptic"; + payload: { hapticType: string }; +} + +interface EMOutgoingMessageConnectionStatus extends EMMessage { + type: "connection-status"; + payload: { event: string }; +} + +interface EMOutgoingMessageAppConfiguration extends EMMessage { + type: "config_screen/show"; +} + +interface EMOutgoingMessageTagWrite extends EMMessage { + type: "tag/write"; + payload: { + name: string | null; + tag: string; + }; +} + +interface EMOutgoingMessageSidebarShow extends EMMessage { + type: "sidebar/show"; +} + +type EMOutgoingMessageWithoutAnswer = + | EMOutgoingMessageHaptic + | EMOutgoingMessageConnectionStatus + | EMOutgoingMessageAppConfiguration + | EMOutgoingMessageTagWrite + | EMOutgoingMessageSidebarShow + | EMOutgoingMessageExoplayerPlayHLS + | EMOutgoingMessageExoplayerResize + | EMOutgoingMessageExoplayerStop + | EMOutgoingMessageThemeUpdate + | EMMessageResultSuccess + | EMMessageResultError; + +interface EMIncomingMessageRestart { id: number; type: "command"; command: "restart"; } -interface EMExternMessageShowNotifications { +interface EMIncomingMessageShowNotifications { id: number; type: "command"; command: "notifications/show"; } -export type EMExternalMessageCommands = - | EMExternalMessageRestart - | EMExternMessageShowNotifications; +export type EMIncomingMessageCommands = + | EMIncomingMessageRestart + | EMIncomingMessageShowNotifications; -type ExternalMessage = +type EMIncomingMessage = | EMMessageResultSuccess | EMMessageResultError - | EMExternalMessageCommands; + | EMIncomingMessageCommands; -type ExternalMessageHandler = (msg: EMExternalMessageCommands) => boolean; +type EMIncomingMessageHandler = (msg: EMIncomingMessageCommands) => boolean; export interface ExternalConfig { hasSettingsScreen: boolean; hasSidebar: boolean; canWriteTag: boolean; hasExoPlayer: boolean; + canCommissionMatter: boolean; } export class ExternalMessaging { @@ -67,7 +152,7 @@ export class ExternalMessaging { public msgId = 0; - private _commandHandler?: ExternalMessageHandler; + private _commandHandler?: EMIncomingMessageHandler; public async attach() { window[CALLBACK_EXTERNAL_BUS] = (msg) => this.receiveMessage(msg); @@ -77,12 +162,12 @@ export class ExternalMessaging { payload: { event: ev.detail }, }) ); - this.config = await this.sendMessage({ + this.config = await this.sendMessage<"config/get">({ type: "config/get", }); } - public addCommandHandler(handler: ExternalMessageHandler) { + public addCommandHandler(handler: EMIncomingMessageHandler) { this._commandHandler = handler; } @@ -90,31 +175,33 @@ export class ExternalMessaging { * Send message to external app that expects a response. * @param msg message to send */ - public sendMessage(msg: EMMessage): Promise { + public sendMessage( + msg: EMOutgoingMessageWithAnswer[T]["request"] + ): Promise { const msgId = ++this.msgId; msg.id = msgId; - this.fireMessage(msg); + this._sendExternal(msg); - return new Promise((resolve, reject) => { - this.commands[msgId] = { resolve, reject }; - }); + return new Promise( + (resolve, reject) => { + this.commands[msgId] = { resolve, reject }; + } + ); } /** * Send message to external app without expecting a response. * @param msg message to send */ - public fireMessage( - msg: EMMessage | EMMessageResultSuccess | EMMessageResultError - ) { + public fireMessage(msg: EMOutgoingMessageWithoutAnswer) { if (!msg.id) { msg.id = ++this.msgId; } this._sendExternal(msg); } - public receiveMessage(msg: ExternalMessage) { + public receiveMessage(msg: EMIncomingMessage) { if (__DEV__) { // eslint-disable-next-line no-console console.log("Receiving message from external app", msg); diff --git a/src/panels/config/application_credentials/dialog-add-application-credential.ts b/src/panels/config/application_credentials/dialog-add-application-credential.ts index e01ded0e87..f1fc6ecf87 100644 --- a/src/panels/config/application_credentials/dialog-add-application-credential.ts +++ b/src/panels/config/application_credentials/dialog-add-application-credential.ts @@ -1,8 +1,9 @@ import "@material/mwc-button"; import "@material/mwc-list/mwc-list-item"; +import { mdiOpenInNew } from "@mdi/js"; +import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-circular-progress"; import "../../../components/ha-combo-box"; @@ -10,14 +11,15 @@ import { createCloseHeading } from "../../../components/ha-dialog"; import "../../../components/ha-markdown"; import "../../../components/ha-textfield"; import { - fetchApplicationCredentialsConfig, - createApplicationCredential, - ApplicationCredentialsConfig, ApplicationCredential, + ApplicationCredentialsConfig, + createApplicationCredential, + fetchApplicationCredentialsConfig, } from "../../../data/application_credential"; import { domainToName } from "../../../data/integration"; import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; +import { documentationUrl } from "../../../util/documentation-url"; import { AddApplicationCredentialDialogParams } from "./show-dialog-add-application-credential"; interface Domain { @@ -98,6 +100,25 @@ export class DialogAddApplicationCredential extends LitElement { >
${this._error ? html`
${this._error}
` : ""} +

+ ${this.hass.localize( + "ui.panel.config.application_credentials.editor.description" + )} +
+ + ${this.hass!.localize( + "ui.panel.config.application_credentials.editor.view_documentation" + )} + + +

${this._loading @@ -163,15 +192,18 @@ export class DialogAddApplicationCredential extends LitElement {
` : html` + + ${this.hass.localize("ui.common.cancel")} + ${this.hass.localize( - "ui.panel.config.application_credentials.editor.create" + "ui.panel.config.application_credentials.editor.add" )} `} @@ -213,7 +245,7 @@ export class DialogAddApplicationCredential extends LitElement { this.closeDialog(); } - private async _createApplicationCredential(ev) { + private async _addApplicationCredential(ev) { ev.preventDefault(); if (!this._domain || !this._clientId || !this._clientSecret) { return; @@ -260,6 +292,12 @@ export class DialogAddApplicationCredential extends LitElement { display: block; margin-bottom: 24px; } + a { + text-decoration: none; + } + a ha-svg-icon { + --mdc-icon-size: 16px; + } `, ]; } diff --git a/src/panels/config/automation/action/ha-automation-action-row.ts b/src/panels/config/automation/action/ha-automation-action-row.ts index c742eefc01..0d89543050 100644 --- a/src/panels/config/automation/action/ha-automation-action-row.ts +++ b/src/panels/config/automation/action/ha-automation-action-row.ts @@ -404,11 +404,15 @@ export default class HaAutomationActionRow extends LitElement { private _onDelete() { showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.automation.editor.actions.delete_confirm_title" + ), text: this.hass.localize( - "ui.panel.config.automation.editor.actions.delete_confirm" + "ui.panel.config.automation.editor.actions.delete_confirm_text" ), dismissText: this.hass.localize("ui.common.cancel"), confirmText: this.hass.localize("ui.common.delete"), + destructive: true, confirm: () => { fireEvent(this, "value-changed", { value: null }); }, diff --git a/src/panels/config/automation/condition/ha-automation-condition-row.ts b/src/panels/config/automation/condition/ha-automation-condition-row.ts index dcc93b8015..386e158d01 100644 --- a/src/panels/config/automation/condition/ha-automation-condition-row.ts +++ b/src/panels/config/automation/condition/ha-automation-condition-row.ts @@ -314,11 +314,15 @@ export default class HaAutomationConditionRow extends LitElement { private _onDelete() { showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.automation.editor.conditions.delete_confirm_title" + ), text: this.hass.localize( - "ui.panel.config.automation.editor.conditions.delete_confirm" + "ui.panel.config.automation.editor.conditions.delete_confirm_text" ), dismissText: this.hass.localize("ui.common.cancel"), confirmText: this.hass.localize("ui.common.delete"), + destructive: true, confirm: () => { fireEvent(this, "value-changed", { value: null }); }, diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index 14f34c7f6b..e7c7236fd3 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -31,6 +31,7 @@ import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../../../common/dom/fire_event"; import { navigate } from "../../../common/navigate"; import { copyToClipboard } from "../../../common/util/copy-clipboard"; +import { afterNextRender } from "../../../common/util/render-status"; import "../../../components/ha-button-menu"; import "../../../components/ha-card"; import "../../../components/ha-fab"; @@ -540,11 +541,15 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { private async confirmUnsavedChanged(): Promise { if (this._dirty) { return showConfirmationDialog(this, { + title: this.hass!.localize( + "ui.panel.config.automation.editor.unsaved_confirm_title" + ), text: this.hass!.localize( - "ui.panel.config.automation.editor.unsaved_confirm" + "ui.panel.config.automation.editor.unsaved_confirm_text" ), confirmText: this.hass!.localize("ui.common.leave"), dismissText: this.hass!.localize("ui.common.stay"), + destructive: true, }); } return true; @@ -553,7 +558,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { private _backTapped = async () => { const result = await this.confirmUnsavedChanged(); if (result) { - history.back(); + afterNextRender(() => history.back()); } }; @@ -570,10 +575,15 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { private async _deleteConfirm() { showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.automation.picker.delete_confirm_title" + ), text: this.hass.localize( - "ui.panel.config.automation.picker.delete_confirm" + "ui.panel.config.automation.picker.delete_confirm_text", + { name: this._config?.alias } ), confirmText: this.hass!.localize("ui.common.delete"), + destructive: true, dismissText: this.hass!.localize("ui.common.cancel"), confirm: () => this._delete(), }); diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index dc395aefaf..3fe409f34b 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -341,12 +341,17 @@ class HaAutomationPicker extends LitElement { private async _deleteConfirm(automation) { showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.automation.picker.delete_confirm_title" + ), text: this.hass.localize( - "ui.panel.config.automation.picker.delete_confirm" + "ui.panel.config.automation.picker.delete_confirm_text", + { name: automation.name } ), confirmText: this.hass!.localize("ui.common.delete"), dismissText: this.hass!.localize("ui.common.cancel"), confirm: () => this._delete(automation), + destructive: true, }); } diff --git a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts index 2674664af9..8d71152164 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts @@ -430,11 +430,15 @@ export default class HaAutomationTriggerRow extends LitElement { private _onDelete() { showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.automation.editor.triggers.delete_confirm_title" + ), text: this.hass.localize( - "ui.panel.config.automation.editor.triggers.delete_confirm" + "ui.panel.config.automation.editor.triggers.delete_confirm_text" ), dismissText: this.hass.localize("ui.common.cancel"), confirmText: this.hass.localize("ui.common.delete"), + destructive: true, confirm: () => { fireEvent(this, "value-changed", { value: null }); }, diff --git a/src/panels/config/blueprint/dialog-import-blueprint.ts b/src/panels/config/blueprint/dialog-import-blueprint.ts index ef43378c16..1b97d52e8d 100644 --- a/src/panels/config/blueprint/dialog-import-blueprint.ts +++ b/src/panels/config/blueprint/dialog-import-blueprint.ts @@ -1,4 +1,5 @@ import "@material/mwc-button"; +import { mdiOpenInNew } from "@mdi/js"; import { css, html, LitElement, TemplateResult } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; @@ -105,50 +106,61 @@ class DialogImportBlueprint extends LitElement { >
${this._result.raw_data}
` - : html`${this.hass.localize( - "ui.panel.config.blueprint.add.import_introduction_link", - "community_link", - html`${this.hass.localize( - "ui.panel.config.blueprint.add.community_forums" - )}` - )} + ${this.hass.localize( + "ui.panel.config.blueprint.add.import_introduction" + )} +

+ + ${this.hass.localize( + "ui.panel.config.blueprint.add.community_forums" + )} + + + `} + >
+ `}
+ + ${this.hass.localize("ui.common.cancel")} + ${!this._result - ? html` - ${this._importing - ? html`` - : ""} - ${this.hass.localize("ui.panel.config.blueprint.add.import_btn")} - ` - : html` - ${this.hass.localize("ui.common.cancel")} + ${this._importing + ? html`` + : ""} + ${this.hass.localize( + "ui.panel.config.blueprint.add.import_btn" + )} + ` + : html` ` : ""} ${this.hass.localize("ui.panel.config.blueprint.add.save_btn")} - `} + + `} `; } @@ -215,9 +228,19 @@ class DialogImportBlueprint extends LitElement { static styles = [ haStyleDialog, css` + p { + margin-top: 0; + margin-bottom: 8px; + } ha-textfield { display: block; - margin-top: 8px; + margin-top: 24px; + } + a { + text-decoration: none; + } + a ha-svg-icon { + --mdc-icon-size: 16px; } `, ]; diff --git a/src/panels/config/blueprint/ha-blueprint-overview.ts b/src/panels/config/blueprint/ha-blueprint-overview.ts index 3df0c44e41..3a395312af 100644 --- a/src/panels/config/blueprint/ha-blueprint-overview.ts +++ b/src/panels/config/blueprint/ha-blueprint-overview.ts @@ -329,11 +329,15 @@ class HaBlueprintOverview extends LitElement { if ( !(await showConfirmationDialog(this, { title: this.hass.localize( - "ui.panel.config.blueprint.overview.confirm_delete_header" + "ui.panel.config.blueprint.overview.confirm_delete_title" ), text: this.hass.localize( - "ui.panel.config.blueprint.overview.confirm_delete_text" + "ui.panel.config.blueprint.overview.confirm_delete_text", + { name: blueprint.name } ), + confirmText: this.hass!.localize("ui.common.delete"), + dismissText: this.hass!.localize("ui.common.cancel"), + destructive: true, })) ) { return; diff --git a/src/panels/config/cloud/account/cloud-alexa-pref.ts b/src/panels/config/cloud/account/cloud-alexa-pref.ts index aaae98dd20..ad0ae9a406 100644 --- a/src/panels/config/cloud/account/cloud-alexa-pref.ts +++ b/src/panels/config/cloud/account/cloud-alexa-pref.ts @@ -1,11 +1,13 @@ import "@material/mwc-button"; +import { mdiHelpCircle } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { property, state } from "lit/decorators"; +import { property } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-alert"; import "../../../../components/ha-card"; +import "../../../../components/ha-settings-row"; import "../../../../components/ha-switch"; import type { HaSwitch } from "../../../../components/ha-switch"; -import { syncCloudAlexaEntities } from "../../../../data/alexa"; import { CloudStatusLoggedIn, updateCloudPref } from "../../../../data/cloud"; import type { HomeAssistant } from "../../../../types"; @@ -14,8 +16,6 @@ export class CloudAlexaPref extends LitElement { @property() public cloudStatus?: CloudStatusLoggedIn; - @state() private _syncing = false; - protected render(): TemplateResult { if (!this.cloudStatus) { return html``; @@ -31,7 +31,20 @@ export class CloudAlexaPref extends LitElement { "ui.panel.config.cloud.account.alexa.title" )} > -
+
+ + + ` : html` -
-

+ + ${this.hass!.localize( "ui.panel.config.cloud.account.alexa.enable_state_reporting" )} -

-
- -
-
-

- ${this.hass!.localize( - "ui.panel.config.cloud.account.alexa.info_state_reporting" - )} -

+ + + ${this.hass!.localize( + "ui.panel.config.cloud.account.alexa.info_state_reporting" + )} + + + `}
- ${alexa_registered - ? html` - - ${this.hass!.localize( - "ui.panel.config.cloud.account.alexa.sync_entities" - )} - - ` - : ""} -
${this.hass!.localize( @@ -127,21 +125,6 @@ export class CloudAlexaPref extends LitElement { `; } - private async _handleSync() { - this._syncing = true; - try { - await syncCloudAlexaEntities(this.hass!); - } catch (err: any) { - alert( - `${this.hass!.localize( - "ui.panel.config.cloud.account.alexa.sync_entities_error" - )} ${err.body.message}` - ); - } finally { - this._syncing = false; - } - } - private async _enabledToggleChanged(ev) { const toggle = ev.target as HaSwitch; try { @@ -180,40 +163,33 @@ export class CloudAlexaPref extends LitElement { a { color: var(--primary-color); } - .switch { + ha-settings-row { + padding: 0; + } + .header-actions { position: absolute; right: 24px; top: 24px; + display: flex; + flex-direction: row; } - :host([dir="rtl"]) .switch { + :host([dir="rtl"]) .header-actions { right: auto; left: 24px; } + .header-actions .icon-link { + margin-top: -16px; + margin-inline-end: 8px; + margin-right: 8px; + direction: var(--direction); + color: var(--secondary-text-color); + } .card-actions { display: flex; } .card-actions a { text-decoration: none; } - .spacer { - flex-grow: 1; - } - .state-reporting { - display: flex; - margin-top: 1.5em; - } - .state-reporting + p { - margin-top: 0.5em; - } - .state-reporting h3 { - flex-grow: 1; - margin: 0; - } - .state-reporting-switch { - margin-top: 0.25em; - margin-right: 7px; - margin-left: 0.5em; - } `; } } diff --git a/src/panels/config/cloud/account/cloud-google-pref.ts b/src/panels/config/cloud/account/cloud-google-pref.ts index 16e831e448..bef63407fa 100644 --- a/src/panels/config/cloud/account/cloud-google-pref.ts +++ b/src/panels/config/cloud/account/cloud-google-pref.ts @@ -1,15 +1,15 @@ import "@material/mwc-button"; +import { mdiHelpCircle } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { property, state } from "lit/decorators"; +import { property } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/ha-alert"; import "../../../../components/ha-card"; +import "../../../../components/ha-settings-row"; import type { HaSwitch } from "../../../../components/ha-switch"; import "../../../../components/ha-textfield"; import type { HaTextField } from "../../../../components/ha-textfield"; import { CloudStatusLoggedIn, updateCloudPref } from "../../../../data/cloud"; -import { syncCloudGoogleEntities } from "../../../../data/google_assistant"; -import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box"; import type { HomeAssistant } from "../../../../types"; import { showSaveSuccessToast } from "../../../../util/toast-saved-success"; @@ -18,8 +18,6 @@ export class CloudGooglePref extends LitElement { @property({ attribute: false }) public cloudStatus?: CloudStatusLoggedIn; - @state() private _syncing = false; - protected render(): TemplateResult { if (!this.cloudStatus) { return html``; @@ -36,11 +34,23 @@ export class CloudGooglePref extends LitElement { "ui.panel.config.cloud.account.google.title" )} > -
+
@@ -110,61 +120,50 @@ export class CloudGooglePref extends LitElement { ` : ""} -
-

- ${this.hass.localize( + + + ${this.hass!.localize( "ui.panel.config.cloud.account.google.enable_state_reporting" )} -

-
- -
-
-

- ${this.hass.localize( - "ui.panel.config.cloud.account.google.info_state_reporting" - )} -

-
-

+ + + ${this.hass!.localize( + "ui.panel.config.cloud.account.google.info_state_reporting" + )} + + + + + + ${this.hass.localize( "ui.panel.config.cloud.account.google.security_devices" )} -

- ${this.hass.localize( - "ui.panel.config.cloud.account.google.enter_pin_info" + + + ${this.hass.localize( + "ui.panel.config.cloud.account.google.enter_pin_info" + )} + + + + -
+ .placeholder=${this.hass.localize( + "ui.panel.config.cloud.account.google.enter_pin_hint" + )} + .value=${google_secure_devices_pin || ""} + @change=${this._pinChanged} + > `}
- ${google_registered - ? html` - - ${this.hass.localize( - "ui.panel.config.cloud.account.google.sync_entities" - )} - - ` - : ""} -
${this.hass.localize( @@ -177,32 +176,7 @@ export class CloudGooglePref extends LitElement { `; } - private async _handleSync() { - this._syncing = true; - try { - await syncCloudGoogleEntities(this.hass!); - } catch (err: any) { - showAlertDialog(this, { - title: this.hass.localize( - `ui.panel.config.cloud.account.google.${ - err.status_code === 404 - ? "not_configured_title" - : "sync_failed_title" - }` - ), - text: this.hass.localize( - `ui.panel.config.cloud.account.google.${ - err.status_code === 404 ? "not_configured_text" : "sync_failed_text" - }` - ), - }); - fireEvent(this, "ha-refresh-cloud-status"); - } finally { - this._syncing = false; - } - } - - private async _enableToggleChanged(ev) { + private async _enabledToggleChanged(ev) { const toggle = ev.target as HaSwitch; try { await updateCloudPref(this.hass, { [toggle.id]: toggle.checked! }); @@ -252,15 +226,27 @@ export class CloudGooglePref extends LitElement { a { color: var(--primary-color); } - .switch { + .header-actions { position: absolute; right: 24px; top: 24px; + display: flex; + flex-direction: row; } - :host([dir="rtl"]) .switch { + :host([dir="rtl"]) .header-actions { right: auto; left: 24px; } + .header-actions .icon-link { + margin-top: -16px; + margin-inline-end: 8px; + margin-right: 8px; + direction: var(--direction); + color: var(--secondary-text-color); + } + ha-settings-row { + padding: 0; + } ha-textfield { width: 250px; display: block; @@ -275,32 +261,6 @@ export class CloudGooglePref extends LitElement { .warning { color: var(--error-color); } - .secure_devices { - padding-top: 8px; - } - .spacer { - flex-grow: 1; - } - - .state-reporting { - display: flex; - margin-top: 1.5em; - } - .state-reporting + p { - margin-top: 0.5em; - } - h3 { - margin: 0 0 8px 0; - } - .state-reporting h3 { - flex-grow: 1; - margin: 0; - } - .state-reporting-switch { - margin-top: 0.25em; - margin-right: 7px; - margin-left: 0.5em; - } `; } } diff --git a/src/panels/config/cloud/account/cloud-remote-pref.ts b/src/panels/config/cloud/account/cloud-remote-pref.ts index d195b49890..bc27ede2f6 100644 --- a/src/panels/config/cloud/account/cloud-remote-pref.ts +++ b/src/panels/config/cloud/account/cloud-remote-pref.ts @@ -1,5 +1,5 @@ import "@material/mwc-button"; -import { mdiContentCopy } from "@mdi/js"; +import { mdiContentCopy, mdiHelpCircle } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; @@ -58,12 +58,26 @@ export class CloudRemotePref extends LitElement { "ui.panel.config.cloud.account.remote.title" )} > -
+ +
${!remote_connected && remote_enabled ? html` @@ -98,18 +112,6 @@ export class CloudRemotePref extends LitElement { >
- - ${this.hass.localize( - "ui.panel.config.cloud.account.remote.link_learn_how_it_works" - )} - -
${this.hass.localize( "ui.panel.config.cloud.account.remote.certificate_info" @@ -158,15 +160,24 @@ export class CloudRemotePref extends LitElement { a { color: var(--primary-color); } - .switch { + .header-actions { position: absolute; right: 24px; top: 24px; + display: flex; + flex-direction: row; } - :host([dir="rtl"]) .switch { + :host([dir="rtl"]) .header-actions { right: auto; left: 24px; } + .header-actions .icon-link { + margin-top: -16px; + margin-inline-end: 8px; + margin-right: 8px; + direction: var(--direction); + color: var(--secondary-text-color); + } .warning { font-weight: bold; margin-bottom: 1em; @@ -179,19 +190,12 @@ export class CloudRemotePref extends LitElement { right: 24px; top: 24px; } - :host([dir="rtl"]) .switch { - right: auto; - left: 24px; - } .card-actions { display: flex; } .card-actions a { text-decoration: none; } - .spacer { - flex-grow: 1; - } ha-svg-icon { --mdc-icon-size: 18px; color: var(--secondary-text-color); diff --git a/src/panels/config/cloud/alexa/cloud-alexa.ts b/src/panels/config/cloud/alexa/cloud-alexa.ts index d388466f8d..0ac078e360 100644 --- a/src/panels/config/cloud/alexa/cloud-alexa.ts +++ b/src/panels/config/cloud/alexa/cloud-alexa.ts @@ -5,6 +5,9 @@ import { mdiCheckboxMultipleMarked, mdiCloseBox, mdiCloseBoxMultiple, + mdiDotsVertical, + mdiFormatListChecks, + mdiSync, } from "@mdi/js"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; @@ -26,7 +29,11 @@ import "../../../../components/ha-card"; import "../../../../components/ha-formfield"; import "../../../../components/ha-icon-button"; import "../../../../components/ha-switch"; -import { AlexaEntity, fetchCloudAlexaEntities } from "../../../../data/alexa"; +import { + AlexaEntity, + fetchCloudAlexaEntities, + syncCloudAlexaEntities, +} from "../../../../data/alexa"; import { AlexaEntityConfig, CloudPreferences, @@ -59,6 +66,8 @@ class CloudAlexa extends SubscribeMixin(LitElement) { @state() private _entities?: AlexaEntity[]; + @state() private _syncing = false; + @state() private _entityConfigs: CloudPreferences["alexa_entity_configs"] = {}; @@ -222,74 +231,84 @@ class CloudAlexa extends SubscribeMixin(LitElement) { } return html` - - ${ - emptyFilter - ? html` - ${this.hass!.localize( - "ui.panel.config.cloud.alexa.manage_defaults" - )} - ` - : "" - } - ${ - !emptyFilter - ? html` - - ` - : "" - } - ${ - exposedCards.length > 0 - ? html` -
-

- ${this.hass!.localize( - "ui.panel.config.cloud.alexa.exposed_entities" - )} -

- ${!this.narrow - ? this.hass!.localize( - "ui.panel.config.cloud.alexa.exposed", - "selected", - selected - ) - : selected} -
-
${exposedCards}
- ` - : "" - } - ${ - notExposedCards.length > 0 - ? html` -
-

- ${this.hass!.localize( - "ui.panel.config.cloud.alexa.not_exposed_entities" - )} -

- ${!this.narrow - ? this.hass!.localize( - "ui.panel.config.cloud.alexa.not_exposed", - "selected", - this._entities.length - selected - ) - : this._entities.length - selected} -
-
${notExposedCards}
- ` - : "" - } -
+ + + + + + ${this.hass.localize("ui.panel.config.cloud.alexa.manage_defaults")} + + + + + ${this.hass.localize("ui.panel.config.cloud.alexa.sync_entities")} + + + + ${!emptyFilter + ? html` + + ` + : ""} + ${exposedCards.length > 0 + ? html` +
+

+ ${this.hass!.localize( + "ui.panel.config.cloud.alexa.exposed_entities" + )} +

+ ${!this.narrow + ? this.hass!.localize( + "ui.panel.config.cloud.alexa.exposed", + "selected", + selected + ) + : selected} +
+
${exposedCards}
+ ` + : ""} + ${notExposedCards.length > 0 + ? html` +
+

+ ${this.hass!.localize( + "ui.panel.config.cloud.alexa.not_exposed_entities" + )} +

+ ${!this.narrow + ? this.hass!.localize( + "ui.panel.config.cloud.alexa.not_exposed", + "selected", + this._entities.length - selected + ) + : this._entities.length - selected} +
+
${notExposedCards}
+ ` + : ""}
`; } @@ -423,6 +442,21 @@ class CloudAlexa extends SubscribeMixin(LitElement) { }); } + private async _handleSync() { + this._syncing = true; + try { + await syncCloudAlexaEntities(this.hass!); + } catch (err: any) { + alert( + `${this.hass!.localize( + "ui.panel.config.cloud.alexa.sync_entities_error" + )} ${err.body.message}` + ); + } finally { + this._syncing = false; + } + } + private async _updateDomainExposed(domain: string, expose: boolean) { const defaultExpose = this.cloudStatus.prefs.alexa_default_expose || diff --git a/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts b/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts index da05e069d8..a17cd270fc 100644 --- a/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts +++ b/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts @@ -3,6 +3,7 @@ import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { formatDateTime } from "../../../../common/datetime/format_date_time"; import { fireEvent } from "../../../../common/dom/fire_event"; +import { createCloseHeading } from "../../../../components/ha-dialog"; import { haStyleDialog } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; import type { CloudCertificateParams as CloudCertificateDialogParams } from "./show-dialog-cloud-certificate"; @@ -32,8 +33,13 @@ class DialogCloudCertificate extends LitElement { return html`
@@ -76,6 +82,13 @@ class DialogCloudCertificate extends LitElement { .break-word { overflow-wrap: break-word; } + p { + margin-top: 0; + margin-bottom: 12px; + } + p:last-child { + margin-bottom: 0; + } `, ]; } diff --git a/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts b/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts index 1dc48f780f..d9dce1d378 100644 --- a/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts +++ b/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts @@ -1,18 +1,19 @@ import "@material/mwc-button"; +import { mdiContentCopy, mdiOpenInNew } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement } from "lit"; import { query, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; import { copyToClipboard } from "../../../../common/util/copy-clipboard"; -import type { HaTextField } from "../../../../components/ha-textfield"; +import { createCloseHeading } from "../../../../components/ha-dialog"; import "../../../../components/ha-textfield"; +import type { HaTextField } from "../../../../components/ha-textfield"; import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; import { haStyle, haStyleDialog } from "../../../../resources/styles"; import { HomeAssistant } from "../../../../types"; import { documentationUrl } from "../../../../util/documentation-url"; +import { showToast } from "../../../../util/toast"; import { WebhookDialogParams } from "./show-dialog-manage-cloudhook"; -const inputLabel = "Public URL – Click to copy to clipboard"; - export class DialogManageCloudhook extends LitElement { protected hass?: HomeAssistant; @@ -44,26 +45,19 @@ export class DialogManageCloudhook extends LitElement { return html`

- ${this.hass!.localize( - "ui.panel.config.cloud.dialog_cloudhook.available_at" - )} -

- -

- ${cloudhook.managed + ${!cloudhook.managed ? html` ${this.hass!.localize( "ui.panel.config.cloud.dialog_cloudhook.managed_by_integration" @@ -79,7 +73,29 @@ export class DialogManageCloudhook extends LitElement { )}. `} +
+ + ${this.hass!.localize( + "ui.panel.config.cloud.dialog_cloudhook.view_documentation" + )} + +

+ + +
{ - this._params!.disableHook(); - this.closeDialog(); - }, + destructive: true, }); - } - - private _copyClipboard(ev: FocusEvent) { - const textField = ev.currentTarget as HaTextField; - try { - copyToClipboard(textField.value); - textField.label = this.hass!.localize( - "ui.panel.config.cloud.dialog_cloudhook.copied_to_clipboard" - ); - } catch (err: any) { - // Copying failed. Oh no + if (confirmed) { + this._params!.disableHook(); + this.closeDialog(); } } - private _restoreLabel() { - this._input.label = inputLabel; + private focusInput(ev) { + const inputElement = ev.currentTarget as HaTextField; + inputElement.select(); + } + + private async _copyUrl(ev): Promise { + if (!this.hass) return; + ev.stopPropagation(); + const inputElement = ev.target.parentElement as HaTextField; + inputElement.select(); + const url = this.hass.hassUrl(inputElement.value); + + await copyToClipboard(url); + showToast(this, { + message: this.hass.localize("ui.common.copied_clipboard"), + }); } static get styles(): CSSResultGroup { @@ -142,12 +165,24 @@ export class DialogManageCloudhook extends LitElement { ha-textfield { display: block; } + ha-textfield > ha-icon-button { + --mdc-icon-button-size: 24px; + --mdc-icon-size: 18px; + } button.link { color: var(--primary-color); + text-decoration: none; } a { text-decoration: none; } + a ha-svg-icon { + --mdc-icon-size: 16px; + } + p { + margin-top: 0; + margin-bottom: 16px; + } `, ]; } diff --git a/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts b/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts index f177094f8a..da22c0fc0b 100644 --- a/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts +++ b/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts @@ -5,6 +5,9 @@ import { mdiCheckboxMultipleMarked, mdiCloseBox, mdiCloseBoxMultiple, + mdiDotsVertical, + mdiFormatListChecks, + mdiSync, } from "@mdi/js"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; @@ -45,6 +48,7 @@ import { GoogleEntity, } from "../../../../data/google_assistant"; import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show-dialog-domain-toggler"; +import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box"; import "../../../../layouts/hass-loading-screen"; import "../../../../layouts/hass-subpage"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; @@ -64,6 +68,8 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) { @state() private _entities?: GoogleEntity[]; + @state() private _syncing = false; + @state() private _entityConfigs: CloudPreferences["google_entity_configs"] = {}; @@ -249,19 +255,39 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) { .hass=${this.hass} .header=${this.hass!.localize("ui.panel.config.cloud.google.title")} .narrow=${this.narrow}> - ${ - emptyFilter - ? html` - ${this.hass!.localize( - "ui.panel.config.cloud.google.manage_defaults" - )} - ` - : "" - } + + + + + ${this.hass.localize( + "ui.panel.config.cloud.google.manage_defaults" + )} + + + + + ${this.hass.localize("ui.panel.config.cloud.google.sync_entities")} + + + ${ !emptyFilter ? html` @@ -506,6 +532,31 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) { ); } + private async _handleSync() { + this._syncing = true; + try { + await cloudSyncGoogleAssistant(this.hass!); + } catch (err: any) { + showAlertDialog(this, { + title: this.hass.localize( + `ui.panel.config.cloud.google.${ + err.status_code === 404 + ? "not_configured_title" + : "sync_failed_title" + }` + ), + text: this.hass.localize( + `ui.panel.config.cloud.google.${ + err.status_code === 404 ? "not_configured_text" : "sync_failed_text" + }` + ), + }); + fireEvent(this, "ha-refresh-cloud-status"); + } finally { + this._syncing = false; + } + } + private _ensureEntitySync() { if (this._popstateSyncAttached) { return; diff --git a/src/panels/config/core/ha-config-section-updates.ts b/src/panels/config/core/ha-config-section-updates.ts index 4b2da37924..cbf8804759 100644 --- a/src/panels/config/core/ha-config-section-updates.ts +++ b/src/panels/config/core/ha-config-section-updates.ts @@ -24,13 +24,11 @@ import { checkForEntityUpdates, filterUpdateEntitiesWithInstall, } from "../../../data/update"; -import { - showAlertDialog, - showConfirmationDialog, -} from "../../../dialogs/generic/show-dialog-box"; +import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-subpage"; import type { HomeAssistant } from "../../../types"; import "../dashboard/ha-config-updates"; +import { showJoinBetaDialog } from "./updates/show-dialog-join-beta"; @customElement("ha-config-section-updates") class HaConfigSectionUpdates extends LitElement { @@ -46,9 +44,7 @@ class HaConfigSectionUpdates extends LitElement { super.firstUpdated(changedProps); if (isComponentLoaded(this.hass, "hassio")) { - fetchHassioSupervisorInfo(this.hass).then((data) => { - this._supervisorInfo = data; - }); + this._refreshSupervisorInfo(); } } @@ -126,6 +122,10 @@ class HaConfigSectionUpdates extends LitElement { `; } + private async _refreshSupervisorInfo() { + this._supervisorInfo = await fetchHassioSupervisorInfo(this.hass); + } + private _toggleSkipped(ev: CustomEvent): void { if (ev.detail.source !== "property") { return; @@ -142,35 +142,23 @@ class HaConfigSectionUpdates extends LitElement { } if (this._supervisorInfo!.channel === "stable") { - const confirmed = await showConfirmationDialog(this, { - title: this.hass.localize("ui.dialogs.join_beta_channel.title"), - text: html`${this.hass.localize("ui.dialogs.join_beta_channel.warning")} -
- ${this.hass.localize("ui.dialogs.join_beta_channel.backup")} -

- ${this.hass.localize("ui.dialogs.join_beta_channel.release_items")} -
    -
  • Home Assistant Core
  • -
  • Home Assistant Supervisor
  • -
  • Home Assistant Operating System
  • -
-
- ${this.hass.localize("ui.dialogs.join_beta_channel.confirm")}`, - confirmText: this.hass.localize("ui.panel.config.updates.join_beta"), - dismissText: this.hass.localize("ui.common.cancel"), + showJoinBetaDialog(this, { + join: async () => this._setChannel("beta"), }); - - if (!confirmed) { - return; - } + } else { + this._setChannel("stable"); } + } + private async _setChannel( + channel: SupervisorOptions["channel"] + ): Promise { try { - const data: Partial = { - channel: this._supervisorInfo!.channel === "stable" ? "beta" : "stable", - }; - await setSupervisorOption(this.hass, data); + await setSupervisorOption(this.hass, { + channel, + }); await reloadSupervisor(this.hass); + await this._refreshSupervisorInfo(); } catch (err: any) { showAlertDialog(this, { text: extractApiErrorMessage(err), diff --git a/src/panels/config/core/ha-config-system-navigation.ts b/src/panels/config/core/ha-config-system-navigation.ts index 42c71b95c5..22c5250849 100644 --- a/src/panels/config/core/ha-config-system-navigation.ts +++ b/src/panels/config/core/ha-config-system-navigation.ts @@ -181,6 +181,7 @@ class HaConfigSystemNavigation extends LitElement { }); }); }, + destructive: true, }); } @@ -205,7 +206,7 @@ class HaConfigSystemNavigation extends LitElement { const hardwareInfo: HardwareInfo = await this.hass.callWS({ type: "hardware/info", }); - this._boardName = hardwareInfo?.hardware?.[0].name; + this._boardName = hardwareInfo?.hardware?.[0]?.name; } else if (isHassioLoaded) { const osData: HassioHassOSInfo = await fetchHassioHassOsInfo(this.hass); if (osData.board) { diff --git a/src/panels/config/core/updates/dialog-join-beta.ts b/src/panels/config/core/updates/dialog-join-beta.ts new file mode 100644 index 0000000000..41723c88ad --- /dev/null +++ b/src/panels/config/core/updates/dialog-join-beta.ts @@ -0,0 +1,108 @@ +import "@material/mwc-button/mwc-button"; +import { mdiOpenInNew } from "@mdi/js"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-alert"; +import { createCloseHeading } from "../../../../components/ha-dialog"; +import "../../../../components/ha-header-bar"; +import { HassDialog } from "../../../../dialogs/make-dialog-manager"; +import { haStyleDialog } from "../../../../resources/styles"; +import { HomeAssistant } from "../../../../types"; +import { documentationUrl } from "../../../../util/documentation-url"; +import { JoinBetaDialogParams } from "./show-dialog-join-beta"; + +@customElement("dialog-join-beta") +export class DialogJoinBeta + extends LitElement + implements HassDialog +{ + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _dialogParams?: JoinBetaDialogParams; + + public showDialog(dialogParams: JoinBetaDialogParams): void { + this._dialogParams = dialogParams; + } + + public closeDialog(): void { + this._dialogParams = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._dialogParams) { + return html``; + } + + return html` + + + ${this.hass.localize("ui.dialogs.join_beta_channel.backup")} + +

+ ${this.hass.localize("ui.dialogs.join_beta_channel.warning")} + ${this.hass.localize("ui.dialogs.join_beta_channel.release_items")} +

+
    +
  • Home Assistant Core
  • +
  • Home Assistant Supervisor
  • +
  • Home Assistant Operating System
  • +
+
+ ${this.hass!.localize( + "ui.dialogs.join_beta_channel.view_documentation" + )} + + + + ${this.hass.localize("ui.common.cancel")} + + + ${this.hass.localize("ui.dialogs.join_beta_channel.join")} + +
+ `; + } + + private _cancel() { + this._dialogParams?.cancel?.(); + this.closeDialog(); + } + + private _join() { + this._dialogParams?.join?.(); + this.closeDialog(); + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + a { + text-decoration: none; + } + a ha-svg-icon { + --mdc-icon-size: 16px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-join-beta": DialogJoinBeta; + } +} diff --git a/src/panels/config/core/updates/show-dialog-join-beta.ts b/src/panels/config/core/updates/show-dialog-join-beta.ts new file mode 100644 index 0000000000..515f4d517c --- /dev/null +++ b/src/panels/config/core/updates/show-dialog-join-beta.ts @@ -0,0 +1,18 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; +import "./dialog-join-beta"; + +export interface JoinBetaDialogParams { + join?: () => any; + cancel?: () => any; +} + +export const showJoinBetaDialog = ( + element: HTMLElement, + dialogParams: JoinBetaDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-join-beta", + dialogImport: () => import("./dialog-join-beta"), + dialogParams, + }); +}; diff --git a/src/panels/config/devices/device-detail/integration-elements/zha/device-actions.ts b/src/panels/config/devices/device-detail/integration-elements/zha/device-actions.ts index e9e17a3f20..907393e7c1 100644 --- a/src/panels/config/devices/device-detail/integration-elements/zha/device-actions.ts +++ b/src/panels/config/devices/device-detail/integration-elements/zha/device-actions.ts @@ -1,9 +1,7 @@ import { mdiCogRefresh, mdiDelete, - mdiDrawPen, mdiFamilyTree, - mdiFileTree, mdiGroup, mdiPlus, } from "@mdi/js"; @@ -12,9 +10,7 @@ import type { DeviceRegistryEntry } from "../../../../../../data/device_registry import { fetchZHADevice } from "../../../../../../data/zha"; import { showConfirmationDialog } from "../../../../../../dialogs/generic/show-dialog-box"; import type { HomeAssistant } from "../../../../../../types"; -import { showZHAClusterDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-cluster"; -import { showZHADeviceChildrenDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-children"; -import { showZHADeviceZigbeeInfoDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info"; +import { showZHAManageZigbeeDeviceDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-manage-zigbee-device"; import { showZHAReconfigureDeviceDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-reconfigure-device"; import type { DeviceAction } from "../../../ha-config-device-page"; @@ -59,13 +55,6 @@ export const getZHADeviceActions = async ( icon: mdiPlus, action: () => navigate(`/config/zha/add/${zhaDevice!.ieee}`), }, - { - label: hass.localize( - "ui.dialogs.zha_device_info.buttons.device_children" - ), - icon: mdiFileTree, - action: () => showZHADeviceChildrenDialog(el, { device: zhaDevice! }), - }, ] ); } @@ -73,16 +62,10 @@ export const getZHADeviceActions = async ( actions.push( ...[ { - label: hass.localize( - "ui.dialogs.zha_device_info.buttons.zigbee_information" - ), - icon: mdiDrawPen, - action: () => showZHADeviceZigbeeInfoDialog(el, { device: zhaDevice }), - }, - { - label: hass.localize("ui.dialogs.zha_device_info.buttons.clusters"), + label: hass.localize("ui.dialogs.zha_device_info.buttons.manage"), icon: mdiGroup, - action: () => showZHAClusterDialog(el, { device: zhaDevice }), + action: () => + showZHAManageZigbeeDeviceDialog(el, { device: zhaDevice }), }, { label: hass.localize("ui.dialogs.zha_device_info.buttons.view_network"), diff --git a/src/panels/config/devices/device-detail/integration-elements/zha/ha-device-info-zha.ts b/src/panels/config/devices/device-detail/integration-elements/zha/ha-device-info-zha.ts index 5ccaf7c5e5..3f1f0fad16 100644 --- a/src/panels/config/devices/device-detail/integration-elements/zha/ha-device-info-zha.ts +++ b/src/panels/config/devices/device-detail/integration-elements/zha/ha-device-info-zha.ts @@ -23,6 +23,7 @@ export class HaDeviceActionsZha extends LitElement { @state() private _zhaDevice?: ZHADevice; protected updated(changedProperties: PropertyValues) { + super.updated(changedProperties); if (changedProperties.has("device")) { const zigbeeConnection = this.device.connections.find( (conn) => conn[0] === "zigbee" diff --git a/src/panels/config/energy/components/ha-energy-battery-settings.ts b/src/panels/config/energy/components/ha-energy-battery-settings.ts index 376ace7323..4d7ce0101a 100644 --- a/src/panels/config/energy/components/ha-energy-battery-settings.ts +++ b/src/panels/config/energy/components/ha-energy-battery-settings.ts @@ -16,7 +16,7 @@ import { import { StatisticsMetaData, getStatisticLabel, -} from "../../../../data/history"; +} from "../../../../data/recorder"; import { showAlertDialog, showConfirmationDialog, @@ -36,7 +36,7 @@ export class EnergyBatterySettings extends LitElement { public preferences!: EnergyPreferences; @property({ attribute: false }) - public statsMetadata!: Record; + public statsMetadata?: Record; @property({ attribute: false }) public validationResult?: EnergyPreferencesValidation; @@ -104,14 +104,14 @@ export class EnergyBatterySettings extends LitElement { >${getStatisticLabel( this.hass, source.stat_energy_from, - this.statsMetadata[source.stat_energy_from] + this.statsMetadata?.[source.stat_energy_from] )} ${getStatisticLabel( this.hass, source.stat_energy_to, - this.statsMetadata[source.stat_energy_to] + this.statsMetadata?.[source.stat_energy_to] )}
diff --git a/src/panels/config/energy/components/ha-energy-device-settings.ts b/src/panels/config/energy/components/ha-energy-device-settings.ts index bac990fbc8..52012f4106 100644 --- a/src/panels/config/energy/components/ha-energy-device-settings.ts +++ b/src/panels/config/energy/components/ha-energy-device-settings.ts @@ -15,7 +15,7 @@ import { import { StatisticsMetaData, getStatisticLabel, -} from "../../../../data/history"; +} from "../../../../data/recorder"; import { showAlertDialog, showConfirmationDialog, @@ -35,7 +35,7 @@ export class EnergyDeviceSettings extends LitElement { public preferences!: EnergyPreferences; @property({ attribute: false }) - public statsMetadata!: Record; + public statsMetadata?: Record; @property({ attribute: false }) public validationResult?: EnergyPreferencesValidation; @@ -90,7 +90,7 @@ export class EnergyDeviceSettings extends LitElement { >${getStatisticLabel( this.hass, device.stat_consumption, - this.statsMetadata[device.stat_consumption] + this.statsMetadata?.[device.stat_consumption] )} ; + public statsMetadata?: Record; @property({ attribute: false }) public validationResult?: EnergyPreferencesValidation; @@ -98,7 +98,7 @@ export class EnergyGasSettings extends LitElement { >${getStatisticLabel( this.hass, source.stat_energy_from, - this.statsMetadata[source.stat_energy_from] + this.statsMetadata?.[source.stat_energy_from] )} { delete source.unit_of_measurement; await this._savePreferences({ @@ -149,11 +152,12 @@ export class EnergyGasSettings extends LitElement { ev.currentTarget.closest(".row").source; showEnergySettingsGasDialog(this, { source: { ...origSource }, - unit: getEnergyGasUnitCategory( + allowedGasUnitCategory: getEnergyGasUnitCategory( this.preferences, this.statsMetadata, origSource.stat_energy_from ), + metadata: this.statsMetadata?.[origSource.stat_energy_from], saveCallback: async (newSource) => { await this._savePreferences({ ...this.preferences, diff --git a/src/panels/config/energy/components/ha-energy-grid-settings.ts b/src/panels/config/energy/components/ha-energy-grid-settings.ts index 0e78a01b95..c04993b0ec 100644 --- a/src/panels/config/energy/components/ha-energy-grid-settings.ts +++ b/src/panels/config/energy/components/ha-energy-grid-settings.ts @@ -30,7 +30,7 @@ import { import { StatisticsMetaData, getStatisticLabel, -} from "../../../../data/history"; +} from "../../../../data/recorder"; import { showConfigFlowDialog } from "../../../../dialogs/config-flow/show-dialog-config-flow"; import { showAlertDialog, @@ -55,7 +55,7 @@ export class EnergyGridSettings extends LitElement { public preferences!: EnergyPreferences; @property({ attribute: false }) - public statsMetadata!: Record; + public statsMetadata?: Record; @property({ attribute: false }) public validationResult?: EnergyPreferencesValidation; @@ -136,7 +136,7 @@ export class EnergyGridSettings extends LitElement { >${getStatisticLabel( this.hass, flow.stat_energy_from, - this.statsMetadata[flow.stat_energy_from] + this.statsMetadata?.[flow.stat_energy_from] )} ${getStatisticLabel( this.hass, flow.stat_energy_to, - this.statsMetadata[flow.stat_energy_to] + this.statsMetadata?.[flow.stat_energy_to] )} ; + public statsMetadata?: Record; @property({ attribute: false }) public validationResult?: EnergyPreferencesValidation; @@ -106,7 +106,7 @@ export class EnergySolarSettings extends LitElement { >${getStatisticLabel( this.hass, source.stat_energy_from, - this.statsMetadata[source.stat_energy_from] + this.statsMetadata?.[source.stat_energy_from] )} ${this.info diff --git a/src/panels/config/energy/components/ha-energy-validation-result.ts b/src/panels/config/energy/components/ha-energy-validation-result.ts index 88e26b42a3..4757e1f552 100644 --- a/src/panels/config/energy/components/ha-energy-validation-result.ts +++ b/src/panels/config/energy/components/ha-energy-validation-result.ts @@ -32,9 +32,7 @@ class EnergyValidationMessage extends LitElement { > ${this.hass.localize( `ui.panel.config.energy.validation.issues.${issueType}.description`, - issueType === "entity_unexpected_unit_price" - ? { currency: this.hass.config.currency } - : undefined + { currency: this.hass.config.currency } )} ${ issueType === "recorder_untracked" diff --git a/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts index 16ba76d3f8..e98621f903 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts @@ -21,6 +21,7 @@ import "../../../../components/ha-radio"; import "../../../../components/ha-formfield"; import "../../../../components/ha-textfield"; import type { HaRadio } from "../../../../components/ha-radio"; +import { getStatisticMetadata } from "../../../../data/recorder"; @customElement("dialog-energy-gas-settings") export class DialogEnergyGasSettings @@ -35,7 +36,9 @@ export class DialogEnergyGasSettings @state() private _costs?: "no-costs" | "number" | "entity" | "statistic"; - @state() private _unit?: string; + @state() private _pickableUnit?: string; + + @state() private _pickedDisplayUnit?: string; @state() private _error?: string; @@ -46,6 +49,7 @@ export class DialogEnergyGasSettings this._source = params.source ? { ...params.source } : emptyGasEnergyPreference(); + this._pickedDisplayUnit = params.metadata?.display_unit_of_measurement; this._costs = this._source.entity_energy_price ? "entity" : this._source.number_energy_price @@ -58,7 +62,8 @@ export class DialogEnergyGasSettings public closeDialog(): void { this._params = undefined; this._source = undefined; - this._unit = undefined; + this._pickableUnit = undefined; + this._pickedDisplayUnit = undefined; this._error = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); } @@ -68,13 +73,16 @@ export class DialogEnergyGasSettings return html``; } - const unit = - this._unit || - (this._params.unit === undefined - ? "m³ or kWh" - : this._params.unit === "energy" - ? "kWh" - : "m³"); + const pickableUnit = + this._pickableUnit || + (this._params.allowedGasUnitCategory === undefined + ? "ft³, m³, Wh, kWh or MWh" + : this._params.allowedGasUnitCategory === "energy" + ? "Wh, kWh or MWh" + : "ft³ or m³"); + + const externalSource = + this._source.stat_cost && this._source.stat_cost.includes(":"); return html` @@ -160,6 +168,7 @@ export class DialogEnergyGasSettings value="entity" name="costs" .checked=${this._costs === "entity"} + .disabled=${externalSource} @change=${this._handleCostChanged} > @@ -171,7 +180,7 @@ export class DialogEnergyGasSettings .value=${this._source.entity_energy_price} .label=${this.hass.localize( `ui.panel.config.energy.gas.dialog.cost_entity_input`, - { unit } + { unit: this._pickedDisplayUnit || pickableUnit } )} @value-changed=${this._priceEntityChanged} >` @@ -192,14 +201,16 @@ export class DialogEnergyGasSettings ? html` ` : ""} @@ -250,23 +261,25 @@ export class DialogEnergyGasSettings }; } - private _statisticChanged(ev: CustomEvent<{ value: string }>) { + private async _statisticChanged(ev: CustomEvent<{ value: string }>) { if (ev.detail.value) { const entity = this.hass.states[ev.detail.value]; if (entity?.attributes.unit_of_measurement) { - // Wh is normalized to kWh by stats generation - this._unit = - entity.attributes.unit_of_measurement === "Wh" - ? "kWh" - : entity.attributes.unit_of_measurement; + this._pickedDisplayUnit = entity.attributes.unit_of_measurement; + } else { + this._pickedDisplayUnit = ( + await getStatisticMetadata(this.hass, [ev.detail.value]) + )[0]?.display_unit_of_measurement; } } else { - this._unit = undefined; + this._pickedDisplayUnit = undefined; + } + if (ev.detail.value.includes(":") && this._costs !== "statistic") { + this._costs = "no-costs"; } this._source = { ...this._source!, stat_energy_from: ev.detail.value, - entity_energy_from: ev.detail.value, }; } diff --git a/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts index a2d688f2b3..c2cab0d7b2 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts @@ -269,9 +269,6 @@ export class DialogEnergyGridFlowSettings [this._params!.direction === "from" ? "stat_energy_from" : "stat_energy_to"]: ev.detail.value, - [this._params!.direction === "from" - ? "entity_energy_from" - : "entity_energy_to"]: ev.detail.value, }; } diff --git a/src/panels/config/energy/dialogs/show-dialogs-energy.ts b/src/panels/config/energy/dialogs/show-dialogs-energy.ts index 6feb7e7078..ac27b8206b 100644 --- a/src/panels/config/energy/dialogs/show-dialogs-energy.ts +++ b/src/panels/config/energy/dialogs/show-dialogs-energy.ts @@ -9,6 +9,7 @@ import { GasSourceTypeEnergyPreference, SolarSourceTypeEnergyPreference, } from "../../../../data/energy"; +import { StatisticsMetaData } from "../../../../data/recorder"; export interface EnergySettingsGridFlowDialogParams { source?: @@ -45,7 +46,8 @@ export interface EnergySettingsBatteryDialogParams { export interface EnergySettingsGasDialogParams { source?: GasSourceTypeEnergyPreference; - unit?: EnergyGasUnit; + allowedGasUnitCategory?: EnergyGasUnit; + metadata?: StatisticsMetaData; saveCallback: (source: GasSourceTypeEnergyPreference) => Promise; } diff --git a/src/panels/config/energy/ha-config-energy.ts b/src/panels/config/energy/ha-config-energy.ts index c6a2d5b150..22006e511a 100644 --- a/src/panels/config/energy/ha-config-energy.ts +++ b/src/panels/config/energy/ha-config-energy.ts @@ -13,7 +13,7 @@ import { import { getStatisticMetadata, StatisticsMetaData, -} from "../../../data/history"; +} from "../../../data/recorder"; import "../../../layouts/hass-loading-screen"; import "../../../layouts/hass-subpage"; import { haStyle } from "../../../resources/styles"; @@ -90,37 +90,37 @@ class HaConfigEnergy extends LitElement {
diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 8ae1e84046..e87fd4ea02 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -19,7 +19,10 @@ import { computeDomain } from "../../../common/entity/compute_domain"; import { domainIcon } from "../../../common/entity/domain_icon"; import { supportsFeature } from "../../../common/entity/supports-feature"; import { stringCompare } from "../../../common/string/compare"; -import { LocalizeFunc } from "../../../common/translations/localize"; +import { + LocalizeFunc, + LocalizeKeys, +} from "../../../common/translations/localize"; import "../../../components/ha-alert"; import "../../../components/ha-area-picker"; import "../../../components/ha-expansion-panel"; @@ -32,6 +35,7 @@ import type { HaSwitch } from "../../../components/ha-switch"; import "../../../components/ha-textfield"; import { CameraPreferences, + CAMERA_ORIENTATIONS, CAMERA_SUPPORT_STREAM, fetchCameraPrefs, STREAM_TYPE_HLS, @@ -111,8 +115,12 @@ const OVERRIDE_NUMBER_UNITS = { }; const OVERRIDE_SENSOR_UNITS = { - temperature: ["°C", "°F", "K"], + distance: ["cm", "ft", "in", "km", "m", "mi", "mm", "yd"], pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"], + speed: ["ft/s", "in/d", "in/h", "km/h", "kn", "m/s", "mm/d", "mph"], + temperature: ["°C", "°F", "K"], + volume: ["fl. oz.", "ft³", "gal", "L", "mL", "m³"], + weight: ["g", "kg", "lb", "mg", "oz", "µg"], }; const OVERRIDE_WEATHER_UNITS = { @@ -582,12 +590,12 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { ${this.hass.localize( - "ui.dialogs.entity_registry.editor.preload_stream" + "ui.dialogs.entity_registry.editor.stream.preload_stream" )} ${this.hass.localize( - "ui.dialogs.entity_registry.editor.preload_stream_description" + "ui.dialogs.entity_registry.editor.stream.preload_stream_description" )} + + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.stream.stream_orientation" + )} + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.stream.stream_orientation_description" + )} + + ${CAMERA_ORIENTATIONS.map((num) => { + const localizeStr = + "ui.dialogs.entity_registry.editor.stream.stream_orientation_" + + num.toString(); + return html` + + ${this.hass.localize(localizeStr as LocalizeKeys)} + + `; + })} + + ` : ""} { +export interface StateEntity + extends Omit { readonly?: boolean; selectable?: boolean; id?: string; + unique_id?: string; } export interface EntityRow extends StateEntity { diff --git a/src/panels/config/hardware/ha-config-hardware.ts b/src/panels/config/hardware/ha-config-hardware.ts index 2d225bca1c..f04b06e039 100644 --- a/src/panels/config/hardware/ha-config-hardware.ts +++ b/src/panels/config/hardware/ha-config-hardware.ts @@ -376,10 +376,11 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { private async _hostReboot(): Promise { const confirmed = await showConfirmationDialog(this, { - title: this.hass.localize("ui.panel.config.hardware.reboot_host"), - text: this.hass.localize("ui.panel.config.hardware.reboot_host_confirm"), - confirmText: this.hass.localize("ui.panel.config.hardware.reboot_host"), + title: this.hass.localize("ui.panel.config.hardware.reboot_host_title"), + text: this.hass.localize("ui.panel.config.hardware.reboot_host_text"), + confirmText: this.hass.localize("ui.panel.config.hardware.reboot"), dismissText: this.hass.localize("ui.common.cancel"), + destructive: true, }); if (!confirmed) { @@ -408,12 +409,11 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { private async _hostShutdown(): Promise { const confirmed = await showConfirmationDialog(this, { - title: this.hass.localize("ui.panel.config.hardware.shutdown_host"), - text: this.hass.localize( - "ui.panel.config.hardware.shutdown_host_confirm" - ), - confirmText: this.hass.localize("ui.panel.config.hardware.shutdown_host"), + title: this.hass.localize("ui.panel.config.hardware.shutdown_host_title"), + text: this.hass.localize("ui.panel.config.hardware.shutdown_host_text"), + confirmText: this.hass.localize("ui.panel.config.hardware.shutdown"), dismissText: this.hass.localize("ui.common.cancel"), + destructive: true, }); if (!confirmed) { diff --git a/src/panels/config/helpers/forms/ha-input_number-form.ts b/src/panels/config/helpers/forms/ha-input_number-form.ts index a1196cbf22..51e63c28ed 100644 --- a/src/panels/config/helpers/forms/ha-input_number-form.ts +++ b/src/panels/config/helpers/forms/ha-input_number-form.ts @@ -33,6 +33,8 @@ class HaInputNumberForm extends LitElement { // eslint-disable-next-line: variable-name @state() private _unit_of_measurement?: string; + /* Configuring initial value is intentionally not supported because the behavior + compared to restoring the value after restart is hard to explain */ set item(item: InputNumber) { this._item = item; if (item) { diff --git a/src/panels/config/helpers/forms/ha-schedule-form.ts b/src/panels/config/helpers/forms/ha-schedule-form.ts index a3387f9483..c28e90f8f0 100644 --- a/src/panels/config/helpers/forms/ha-schedule-form.ts +++ b/src/panels/config/helpers/forms/ha-schedule-form.ts @@ -206,6 +206,7 @@ class HaScheduleForm extends LitElement { private get _events() { const events: any[] = []; const currentDay = new Date().getDay(); + const baseDay = currentDay === 0 ? 7 : currentDay; for (const [i, day] of weekdays.entries()) { if (!this[`_${day}`].length) { @@ -214,7 +215,7 @@ class HaScheduleForm extends LitElement { this[`_${day}`].forEach((item: ScheduleDay, index: number) => { // Add 7 to 0 because we start the calendar on Monday - const distance = i - currentDay + (i === 0 ? 7 : 0); + const distance = i - baseDay + (i === 0 ? 7 : 0); const start = new Date(); start.setDate(start.getDate() + distance); diff --git a/src/panels/config/integrations/dialog-add-integration.ts b/src/panels/config/integrations/dialog-add-integration.ts new file mode 100644 index 0000000000..474bbfa57a --- /dev/null +++ b/src/panels/config/integrations/dialog-add-integration.ts @@ -0,0 +1,656 @@ +import "@material/mwc-button"; +import "@material/mwc-list/mwc-list"; +import Fuse from "fuse.js"; +import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, state } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; +import memoizeOne from "memoize-one"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { protocolIntegrationPicked } from "../../../common/integrations/protocolIntegrationPicked"; +import { navigate } from "../../../common/navigate"; +import { caseInsensitiveStringCompare } from "../../../common/string/compare"; +import { LocalizeFunc } from "../../../common/translations/localize"; +import { createCloseHeading } from "../../../components/ha-dialog"; +import "../../../components/ha-icon-button-prev"; +import "../../../components/search-input"; +import { fetchConfigFlowInProgress } from "../../../data/config_flow"; +import { DataEntryFlowProgress } from "../../../data/data_entry_flow"; +import { + domainToName, + fetchIntegrationManifest, +} from "../../../data/integration"; +import { + getIntegrationDescriptions, + Integrations, +} from "../../../data/integrations"; +import { + getSupportedBrands, + SupportedBrandHandler, +} from "../../../data/supported_brands"; +import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../dialogs/generic/show-dialog-box"; +import { haStyleDialog, haStyleScrollbar } from "../../../resources/styles"; +import type { HomeAssistant } from "../../../types"; +import { documentationUrl } from "../../../util/documentation-url"; +import "./ha-domain-integrations"; +import "./ha-integration-list-item"; + +export interface IntegrationListItem { + name: string; + domain: string; + config_flow?: boolean; + is_helper?: boolean; + integrations?: string[]; + iot_standards?: string[]; + supported_flows?: string[]; + cloud?: boolean; + is_built_in?: boolean; + is_add?: boolean; +} + +@customElement("dialog-add-integration") +class AddIntegrationDialog extends LitElement { + public hass!: HomeAssistant; + + @state() private _integrations?: Integrations; + + @state() private _helpers?: Integrations; + + @state() private _supportedBrands?: Record; + + @state() private _initialFilter?: string; + + @state() private _filter?: string; + + @state() private _pickedBrand?: string; + + @state() private _flowsInProgress?: DataEntryFlowProgress[]; + + @state() private _open = false; + + @state() private _narrow = false; + + private _width?: number; + + private _height?: number; + + public showDialog(params): void { + this._open = true; + this._initialFilter = params.initialFilter; + this._narrow = matchMedia( + "all and (max-width: 450px), all and (max-height: 500px)" + ).matches; + } + + public closeDialog() { + this._open = false; + this._integrations = undefined; + this._helpers = undefined; + this._supportedBrands = undefined; + this._pickedBrand = undefined; + this._flowsInProgress = undefined; + this._filter = undefined; + this._width = undefined; + this._height = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + public willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); + if (this._filter === undefined && this._initialFilter !== undefined) { + this._filter = this._initialFilter; + } + if (this._initialFilter !== undefined && this._filter === "") { + this._initialFilter = undefined; + this._filter = ""; + this._width = undefined; + this._height = undefined; + } else if ( + this.hasUpdated && + changedProps.has("_filter") && + (!this._width || !this._height) + ) { + // Store the width and height so that when we search, box doesn't jump + const boundingRect = + this.shadowRoot!.querySelector("mwc-list")?.getBoundingClientRect(); + this._width = boundingRect?.width; + this._height = boundingRect?.height; + } + } + + public updated(changedProps: PropertyValues) { + super.updated(changedProps); + if (changedProps.has("_open") && this._open) { + this._load(); + } + } + + private _filterIntegrations = memoizeOne( + ( + i: Integrations, + h: Integrations, + sb: Record, + components: HomeAssistant["config"]["components"], + localize: LocalizeFunc, + filter?: string + ): IntegrationListItem[] => { + const addDeviceRows: IntegrationListItem[] = ["zha", "zwave_js"] + .filter((domain) => components.includes(domain)) + .map((domain) => ({ + name: localize(`ui.panel.config.integrations.add_${domain}_device`), + domain, + config_flow: true, + is_built_in: true, + is_add: true, + })) + .sort((a, b) => caseInsensitiveStringCompare(a.name, b.name)); + + const integrations: IntegrationListItem[] = Object.entries(i) + .filter( + ([_domain, integration]) => + integration.config_flow || + integration.iot_standards || + integration.integrations + ) + .map(([domain, integration]) => ({ + domain, + name: integration.name || domainToName(localize, domain), + config_flow: integration.config_flow, + iot_standards: integration.iot_standards, + integrations: integration.integrations + ? Object.entries(integration.integrations).map( + ([dom, val]) => val.name || domainToName(localize, dom) + ) + : undefined, + is_built_in: integration.is_built_in !== false, + cloud: integration.iot_class?.startsWith("cloud_"), + })); + + for (const [domain, domainBrands] of Object.entries(sb)) { + const integration = i[domain]; + if ( + !integration.config_flow && + !integration.iot_standards && + !integration.integrations + ) { + continue; + } + for (const [slug, name] of Object.entries(domainBrands)) { + integrations.push({ + domain: slug, + name, + config_flow: integration.config_flow, + supported_flows: [domain], + is_built_in: true, + cloud: integration.iot_class?.startsWith("cloud_"), + }); + } + } + + if (filter) { + const options: Fuse.IFuseOptions = { + keys: [ + "name", + "domain", + "supported_flows", + "integrations", + "iot_standards", + ], + isCaseSensitive: false, + minMatchCharLength: 2, + threshold: 0.2, + }; + const helpers = Object.entries(h) + .filter( + ([_domain, integration]) => + integration.config_flow || + integration.iot_standards || + integration.integrations + ) + .map(([domain, integration]) => ({ + domain, + name: integration.name || domainToName(localize, domain), + config_flow: integration.config_flow, + is_helper: true, + is_built_in: integration.is_built_in !== false, + cloud: integration.iot_class?.startsWith("cloud_"), + })); + return [ + ...new Fuse(integrations, options) + .search(filter) + .map((result) => result.item), + ...new Fuse(helpers, options) + .search(filter) + .map((result) => result.item), + ]; + } + return [ + ...addDeviceRows, + ...integrations.sort((a, b) => + caseInsensitiveStringCompare(a.name || "", b.name || "") + ), + ]; + } + ); + + private _getIntegrations() { + return this._filterIntegrations( + this._integrations!, + this._helpers!, + this._supportedBrands!, + this.hass.config.components, + this.hass.localize, + this._filter + ); + } + + protected render(): TemplateResult { + if (!this._open) { + return html``; + } + const integrations = this._integrations + ? this._getIntegrations() + : undefined; + + return html` + ${this._pickedBrand + ? html`
+ +

+ ${this._calculateBrandHeading()} +

+
+ ${this._renderIntegration()}` + : this._renderAll(integrations)} +
`; + } + + private _calculateBrandHeading() { + const brand = this._integrations?.[this._pickedBrand!]; + if ( + brand?.iot_standards && + !brand.integrations && + !this._flowsInProgress?.length + ) { + return "What type of device is it?"; + } + if ( + !brand?.iot_standards && + !brand?.integrations && + this._flowsInProgress?.length + ) { + return "Want to add these discovered devices?"; + } + return "What do you want to add?"; + } + + private _renderIntegration(): TemplateResult { + return html``; + } + + private _renderAll(integrations?: IntegrationListItem[]): TemplateResult { + return html` + ${integrations + ? html` + + + ` + : html``} `; + } + + private _renderRow = (integration: IntegrationListItem) => { + if (!integration) { + return html``; + } + return html` + + + `; + }; + + private async _load() { + const [descriptions, supportedBrands] = await Promise.all([ + getIntegrationDescriptions(this.hass), + getSupportedBrands(this.hass), + ]); + for (const integration in descriptions.custom.integration) { + if ( + !Object.prototype.hasOwnProperty.call( + descriptions.custom.integration, + integration + ) + ) { + continue; + } + descriptions.custom.integration[integration].is_built_in = false; + } + this._integrations = { + ...descriptions.core.integration, + ...descriptions.custom.integration, + }; + for (const integration in descriptions.custom.helper) { + if ( + !Object.prototype.hasOwnProperty.call( + descriptions.custom.helper, + integration + ) + ) { + continue; + } + descriptions.custom.helper[integration].is_built_in = false; + } + this._helpers = { + ...descriptions.core.helper, + ...descriptions.custom.helper, + }; + this._supportedBrands = supportedBrands; + this.hass.loadBackendTranslation( + "title", + descriptions.core.translated_name, + true + ); + } + + private async _filterChanged(e) { + this._filter = e.detail.value; + } + + private _integrationPicked(ev) { + const listItem = ev.target.closest("ha-integration-list-item"); + const integration: IntegrationListItem = listItem.integration; + this._handleIntegrationPicked(integration); + } + + private async _handleIntegrationPicked(integration: IntegrationListItem) { + if ("supported_flows" in integration) { + const domain = integration.supported_flows![0]; + + showConfirmationDialog(this, { + text: this.hass.localize( + "ui.panel.config.integrations.config_flow.supported_brand_flow", + { + supported_brand: integration.name, + flow_domain_name: domainToName(this.hass.localize, domain), + } + ), + confirm: () => { + const supportIntegration = this._integrations?.[domain]; + this.closeDialog(); + if (["zha", "zwave_js"].includes(domain)) { + protocolIntegrationPicked(this, this.hass, domain); + return; + } + if (supportIntegration) { + this._handleIntegrationPicked({ + domain, + name: + supportIntegration.name || + domainToName(this.hass.localize, domain), + config_flow: supportIntegration.config_flow, + iot_standards: supportIntegration.iot_standards, + integrations: supportIntegration.integrations + ? Object.entries(supportIntegration.integrations).map( + ([dom, val]) => + val.name || domainToName(this.hass.localize, dom) + ) + : undefined, + }); + } else { + showAlertDialog(this, { + text: "Integration not found", + warning: true, + }); + } + }, + }); + + return; + } + + if (integration.is_add) { + protocolIntegrationPicked(this, this.hass, integration.domain); + this.closeDialog(); + return; + } + + if (integration.is_helper) { + this.closeDialog(); + navigate(`/config/helpers/add?domain=${integration.domain}`); + return; + } + + if (integration.integrations) { + this._fetchFlowsInProgress(Object.keys(integration.integrations)); + this._pickedBrand = integration.domain; + return; + } + + if ( + ["zha", "zwave_js"].includes(integration.domain) && + isComponentLoaded(this.hass, integration.domain) + ) { + this._pickedBrand = integration.domain; + return; + } + + if (integration.iot_standards) { + this._pickedBrand = integration.domain; + return; + } + + if (integration.config_flow) { + this._createFlow(integration); + return; + } + + const manifest = await fetchIntegrationManifest( + this.hass, + integration.domain + ); + this.closeDialog(); + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.integrations.config_flow.yaml_only_title" + ), + text: this.hass.localize( + "ui.panel.config.integrations.config_flow.yaml_only_text", + { + link: + manifest?.is_built_in || manifest?.documentation + ? html` + ${this.hass.localize( + "ui.panel.config.integrations.config_flow.documentation" + )} + ` + : this.hass.localize( + "ui.panel.config.integrations.config_flow.documentation" + ), + } + ), + }); + } + + private async _createFlow(integration: IntegrationListItem) { + const flowsInProgress = await this._fetchFlowsInProgress([ + integration.domain, + ]); + + if (flowsInProgress?.length) { + this._pickedBrand = integration.domain; + return; + } + + const manifest = await fetchIntegrationManifest( + this.hass, + integration.domain + ); + + this.closeDialog(); + + showConfigFlowDialog(this, { + startFlowHandler: integration.domain, + showAdvanced: this.hass.userData?.showAdvanced, + manifest, + }); + } + + private async _fetchFlowsInProgress(domains: string[]) { + const flowsInProgress = ( + await fetchConfigFlowInProgress(this.hass.connection) + ).filter((flow) => domains.includes(flow.handler)); + + if (flowsInProgress.length) { + this._flowsInProgress = flowsInProgress; + } + return flowsInProgress; + } + + private _maybeSubmit(ev: KeyboardEvent) { + if (ev.key !== "Enter") { + return; + } + + const integrations = this._getIntegrations(); + + if (integrations.length > 0) { + this._handleIntegrationPicked(integrations[0]); + } + } + + private _prevClicked() { + this._pickedBrand = undefined; + this._flowsInProgress = undefined; + } + + static styles = [ + haStyleScrollbar, + haStyleDialog, + css` + ha-dialog { + --dialog-content-padding: 0; + } + search-input { + display: block; + margin: 16px 16px 0; + } + .divider { + border-bottom-color: var(--divider-color); + } + h2 { + padding-inline-end: 66px; + direction: var(--direction); + } + p { + text-align: center; + padding: 16px; + margin: 0; + } + p > a { + color: var(--primary-color); + } + ha-circular-progress { + width: 100%; + display: flex; + justify-content: center; + margin: 24px 0; + } + lit-virtualizer { + contain: size layout !important; + } + ha-integration-list-item { + width: 100%; + } + ha-icon-button-prev { + color: var(--secondary-text-color); + position: absolute; + left: 16px; + top: 14px; + inset-inline-end: initial; + inset-inline-start: 16px; + direction: var(--direction); + } + .mdc-dialog__title { + margin: 0; + margin-bottom: 8px; + margin-left: 48px; + padding: 24px 24px 0 24px; + color: var(--mdc-dialog-heading-ink-color, rgba(0, 0, 0, 0.87)); + font-size: var(--mdc-typography-headline6-font-size, 1.25rem); + line-height: var(--mdc-typography-headline6-line-height, 2rem); + font-weight: var(--mdc-typography-headline6-font-weight, 500); + letter-spacing: var( + --mdc-typography-headline6-letter-spacing, + 0.0125em + ); + text-decoration: var( + --mdc-typography-headline6-text-decoration, + inherit + ); + text-transform: var(--mdc-typography-headline6-text-transform, inherit); + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-add-integration": AddIntegrationDialog; + } +} diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts index 765627cec0..ac1caa367d 100644 --- a/src/panels/config/integrations/ha-config-integrations.ts +++ b/src/panels/config/integrations/ha-config-integrations.ts @@ -14,7 +14,6 @@ import { customElement, property, state } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; -import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event"; import { protocolIntegrationPicked } from "../../../common/integrations/protocolIntegrationPicked"; import { navigate } from "../../../common/navigate"; import { caseInsensitiveStringCompare } from "../../../common/string/compare"; @@ -28,7 +27,10 @@ import "../../../components/ha-fab"; import "../../../components/ha-icon-button"; import "../../../components/ha-svg-icon"; import "../../../components/search-input"; -import { ConfigEntry, getConfigEntries } from "../../../data/config_entries"; +import { + ConfigEntry, + subscribeConfigEntries, +} from "../../../data/config_entries"; import { getConfigFlowHandlers, getConfigFlowInProgressCollection, @@ -72,6 +74,7 @@ import "./ha-ignored-config-entry-card"; import "./ha-integration-card"; import type { HaIntegrationCard } from "./ha-integration-card"; import "./ha-integration-overflow-menu"; +import { showAddIntegrationDialog } from "./show-add-integration-dialog"; export interface ConfigEntryUpdatedEvent { entry: ConfigEntry; @@ -151,7 +154,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { @state() private _diagnosticHandlers?: Record; - public hassSubscribe(): UnsubscribeFunc[] { + public hassSubscribe(): Array> { return [ subscribeEntityRegistry(this.hass.connection, (entries) => { this._entityRegistryEntries = entries; @@ -180,6 +183,53 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { localized_title: localizeConfigFlowTitle(this.hass.localize, flow), })); }), + subscribeConfigEntries( + this.hass, + (messages) => { + let fullUpdate = false; + const newEntries: ConfigEntryExtended[] = []; + messages.forEach((message) => { + if (message.type === null || message.type === "added") { + newEntries.push({ + ...message.entry, + localized_domain_name: domainToName( + this.hass.localize, + message.entry.domain + ), + }); + if (message.type === null) { + fullUpdate = true; + } + } else if (message.type === "removed") { + this._configEntries = this._configEntries!.filter( + (entry) => entry.entry_id !== message.entry.entry_id + ); + } else if (message.type === "updated") { + const newEntry = message.entry; + this._configEntries = this._configEntries!.map((entry) => + entry.entry_id === newEntry.entry_id + ? { + ...newEntry, + localized_domain_name: entry.localized_domain_name, + } + : entry + ); + } + }); + if (!newEntries.length && !fullUpdate) { + return; + } + const existingEntries = fullUpdate ? [] : this._configEntries; + this._configEntries = [...existingEntries!, ...newEntries].sort( + (conf1, conf2) => + caseInsensitiveStringCompare( + conf1.localized_domain_name + conf1.title, + conf2.localized_domain_name + conf2.title + ) + ); + }, + { type: "integration" } + ), ]; } @@ -257,13 +307,11 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { protected firstUpdated(changed: PropertyValues) { super.firstUpdated(changed); - this._loadConfigEntries(); const localizePromise = this.hass.loadBackendTranslation( "title", undefined, true ); - this._fetchManifests(); if (this.route.path === "/add") { this._handleAdd(localizePromise); } @@ -411,11 +459,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
`} -
+
${this._showIgnored ? ignoredConfigEntries.map( (entry: ConfigEntryExtended) => html` @@ -542,29 +586,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { ev.preventDefault(); } - private _loadConfigEntries() { - getConfigEntries(this.hass, { type: "integration" }).then( - (configEntries) => { - this._configEntries = configEntries - .map( - (entry: ConfigEntry): ConfigEntryExtended => ({ - ...entry, - localized_domain_name: domainToName( - this.hass.localize, - entry.domain - ), - }) - ) - .sort((conf1, conf2) => - caseInsensitiveStringCompare( - conf1.localized_domain_name + conf1.title, - conf2.localized_domain_name + conf2.title - ) - ); - } - ); - } - private async _scanUSBDevices() { if (!isComponentLoaded(this.hass, "usb")) { return; @@ -577,7 +598,9 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { // Make a copy so we can keep track of previously loaded manifests // for discovered flows (which are not part of these results) const manifests = { ...this._manifests }; - for (const manifest of fetched) manifests[manifest.domain] = manifest; + for (const manifest of fetched) { + manifests[manifest.domain] = manifest; + } this._manifests = manifests; } @@ -602,37 +625,15 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { } } - private _handleEntryRemoved(ev: HASSDomEvent) { - this._configEntries = this._configEntries!.filter( - (entry) => entry.entry_id !== ev.detail.entryId - ); - } - - private _handleEntryUpdated(ev: HASSDomEvent) { - const newEntry = ev.detail.entry; - this._configEntries = this._configEntries!.map((entry) => - entry.entry_id === newEntry.entry_id - ? { ...newEntry, localized_domain_name: entry.localized_domain_name } - : entry - ); - } - private _handleFlowUpdated() { - this._loadConfigEntries(); getConfigFlowInProgressCollection(this.hass.connection).refresh(); this._fetchManifests(); } private _createFlow() { - showConfigFlowDialog(this, { - searchQuery: this._filter, - dialogClosedCallback: () => { - this._handleFlowUpdated(); - }, - showAdvanced: this.showAdvanced, + showAddIntegrationDialog(this, { + initialFilter: this._filter, }); - // For config entries. Also loading config flow ones for added integration - this.hass.loadBackendTranslation("title", undefined, true); } private _handleMenuAction(ev: CustomEvent) { @@ -729,9 +730,13 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { protocolIntegrationPicked(this, this.hass, slug); return; } - - fireEvent(this, "handler-picked", { - handler: slug, + showConfigFlowDialog(this, { + dialogClosedCallback: () => { + this._handleFlowUpdated(); + }, + startFlowHandler: slug, + manifest: this._manifests[slug], + showAdvanced: this.hass.userData?.showAdvanced, }); }, }); diff --git a/src/panels/config/integrations/ha-domain-integrations.ts b/src/panels/config/integrations/ha-domain-integrations.ts new file mode 100644 index 0000000000..d54f9e2872 --- /dev/null +++ b/src/panels/config/integrations/ha-domain-integrations.ts @@ -0,0 +1,225 @@ +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { protocolIntegrationPicked } from "../../../common/integrations/protocolIntegrationPicked"; +import { localizeConfigFlowTitle } from "../../../data/config_flow"; +import { DataEntryFlowProgress } from "../../../data/data_entry_flow"; +import { + domainToName, + fetchIntegrationManifest, +} from "../../../data/integration"; +import { Integration } from "../../../data/integrations"; +import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; +import { haStyle } from "../../../resources/styles"; +import { HomeAssistant } from "../../../types"; +import { brandsUrl } from "../../../util/brands-url"; +import "./ha-integration-list-item"; + +const standardToDomain = { zigbee: "zha", "z-wave": "zwave_js" } as const; + +@customElement("ha-domain-integrations") +class HaDomainIntegrations extends LitElement { + public hass!: HomeAssistant; + + @property() public domain!: string; + + @property({ attribute: false }) public integration!: Integration; + + @property({ attribute: false }) + public flowsInProgress?: DataEntryFlowProgress[]; + + protected render() { + return html` + ${this.flowsInProgress?.length + ? html`

We discovered the following:

+ ${this.flowsInProgress.map( + (flow) => html` + + ${localizeConfigFlowTitle(this.hass.localize, flow)} + + ` + )}` + : ""} + ${this.integration?.iot_standards + ? this.integration.iot_standards.map((standard) => { + const domain: string = standardToDomain[standard] || standard; + return html` + + ${this.hass.localize( + `ui.panel.config.integrations.add_${domain}_device` + )} + + `; + }) + : ""} + ${this.integration?.integrations + ? Object.entries(this.integration.integrations).map( + ([dom, val]) => html` + ` + ) + : ""} + ${["zha", "zwave_js"].includes(this.domain) + ? html` + + ${this.hass.localize( + `ui.panel.config.integrations.add_${this.domain}_device` + )} + + ` + : ""} + ${this.integration?.config_flow + ? html`${this.flowsInProgress?.length + ? html` + Setup another instance of + ${this.integration.name || + domainToName(this.hass.localize, this.domain)} + + ` + : html` + `}` + : ""} + `; + } + + private async _integrationPicked(ev) { + const domain = ev.currentTarget.domain; + const root = this.getRootNode(); + showConfigFlowDialog( + root instanceof ShadowRoot ? (root.host as HTMLElement) : this, + { + startFlowHandler: domain, + showAdvanced: this.hass.userData?.showAdvanced, + manifest: await fetchIntegrationManifest(this.hass, domain), + } + ); + fireEvent(this, "close-dialog"); + } + + private async _flowInProgressPicked(ev) { + const flow: DataEntryFlowProgress = ev.currentTarget.flow; + const root = this.getRootNode(); + showConfigFlowDialog( + root instanceof ShadowRoot ? (root.host as HTMLElement) : this, + { + continueFlowId: flow.flow_id, + showAdvanced: this.hass.userData?.showAdvanced, + manifest: await fetchIntegrationManifest(this.hass, flow.handler), + } + ); + fireEvent(this, "close-dialog"); + } + + private _standardPicked(ev) { + const domain = ev.currentTarget.domain; + const root = this.getRootNode(); + fireEvent(this, "close-dialog"); + protocolIntegrationPicked( + root instanceof ShadowRoot ? (root.host as HTMLElement) : this, + this.hass, + domain + ); + } + + static styles = [ + haStyle, + css` + :host { + display: block; + } + h3 { + margin: 0 24px; + color: var(--primary-text-color); + font-size: 14px; + } + img { + width: 40px; + height: 40px; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-domain-integrations": HaDomainIntegrations; + } +} diff --git a/src/panels/config/integrations/ha-integration-card.ts b/src/panels/config/integrations/ha-integration-card.ts index 843bf2223c..b1bdf91318 100644 --- a/src/panels/config/integrations/ha-integration-card.ts +++ b/src/panels/config/integrations/ha-integration-card.ts @@ -11,6 +11,7 @@ import { mdiDotsVertical, mdiDownload, mdiOpenInNew, + mdiReloadAlert, mdiProgressHelper, mdiPlayCircleOutline, mdiReload, @@ -24,13 +25,16 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; -import { fireEvent } from "../../../common/dom/fire_event"; import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event"; import "../../../components/ha-button-menu"; import "../../../components/ha-card"; import "../../../components/ha-icon-button"; import "../../../components/ha-icon-next"; import "../../../components/ha-svg-icon"; +import { + fetchApplicationCredentialsConfigEntry, + deleteApplicationCredential, +} from "../../../data/application_credential"; import { getSignedPath } from "../../../data/auth"; import { ConfigEntry, @@ -184,7 +188,9 @@ export class HaIntegrationCard extends LitElement { ? html` ${this.hass.localize( @@ -231,6 +237,9 @@ export class HaIntegrationCard extends LitElement { "ui.panel.config.integrations.config_entry.setup_in_progress", ]; } else if (ERROR_STATES.includes(item.state)) { + if (item.state === "setup_retry") { + icon = mdiReloadAlert; + } stateText = [ `ui.panel.config.integrations.config_entry.state.${item.state}`, ]; @@ -622,10 +631,6 @@ export class HaIntegrationCard extends LitElement { showConfigEntrySystemOptionsDialog(this, { entry: configEntry, manifest: this.manifest, - entryUpdated: (entry) => - fireEvent(this, "entry-updated", { - entry, - }), }); } @@ -633,9 +638,16 @@ export class HaIntegrationCard extends LitElement { const entryId = configEntry.entry_id; const confirmed = await showConfirmationDialog(this, { - text: this.hass.localize( - "ui.panel.config.integrations.config_entry.disable.disable_confirm" + title: this.hass.localize( + "ui.panel.config.integrations.config_entry.disable_confirm_title", + { title: configEntry.title } ), + text: this.hass.localize( + "ui.panel.config.integrations.config_entry.disable_confirm_text" + ), + confirmText: this.hass!.localize("ui.common.disable"), + dismissText: this.hass!.localize("ui.common.cancel"), + destructive: true, }); if (!confirmed) { @@ -660,9 +672,6 @@ export class HaIntegrationCard extends LitElement { ), }); } - fireEvent(this, "entry-updated", { - entry: { ...configEntry, disabled_by: "user" }, - }); } private async _enableIntegration(configEntry: ConfigEntry) { @@ -688,26 +697,32 @@ export class HaIntegrationCard extends LitElement { ), }); } - fireEvent(this, "entry-updated", { - entry: { ...configEntry, disabled_by: null }, - }); } private async _removeIntegration(configEntry: ConfigEntry) { const entryId = configEntry.entry_id; + const applicationCredentialsId = await this._applicationCredentialForRemove( + entryId + ); + const confirmed = await showConfirmationDialog(this, { - text: this.hass.localize( - "ui.panel.config.integrations.config_entry.delete_confirm", + title: this.hass.localize( + "ui.panel.config.integrations.config_entry.delete_confirm_title", { title: configEntry.title } ), + text: this.hass.localize( + "ui.panel.config.integrations.config_entry.delete_confirm_text" + ), + confirmText: this.hass!.localize("ui.common.delete"), + dismissText: this.hass!.localize("ui.common.cancel"), + destructive: true, }); if (!confirmed) { return; } const result = await deleteConfigEntry(this.hass, entryId); - fireEvent(this, "entry-removed", { entryId }); if (result.require_restart) { showAlertDialog(this, { @@ -716,6 +731,70 @@ export class HaIntegrationCard extends LitElement { ), }); } + if (applicationCredentialsId) { + this._removeApplicationCredential(applicationCredentialsId); + } + } + + // Return an application credentials id for this config entry to prompt the + // user for removal. This is best effort so we don't stop overall removal + // if the integration isn't loaded or there is some other error. + private async _applicationCredentialForRemove(entryId: string) { + try { + return (await fetchApplicationCredentialsConfigEntry(this.hass, entryId)) + .application_credentials_id; + } catch (err: any) { + // We won't prompt the user to remove credentials + return null; + } + } + + private async _removeApplicationCredential(applicationCredentialsId: string) { + const confirmed = await showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.integrations.config_entry.application_credentials.delete_title" + ), + text: html`${this.hass.localize( + "ui.panel.config.integrations.config_entry.application_credentials.delete_prompt" + )}, +
+
+ ${this.hass.localize( + "ui.panel.config.integrations.config_entry.application_credentials.delete_detail" + )} +
+
+ + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.application_credentials.learn_more" + )} + `, + destructive: true, + confirmText: this.hass.localize("ui.common.remove"), + dismissText: this.hass.localize( + "ui.panel.config.integrations.config_entry.application_credentials.dismiss" + ), + }); + if (!confirmed) { + return; + } + try { + await deleteApplicationCredential(this.hass, applicationCredentialsId); + } catch (err: any) { + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.integrations.config_entry.application_credentials.delete_error_title" + ), + text: err.message, + }); + } } private async _reloadIntegration(configEntry: ConfigEntry) { @@ -743,10 +822,9 @@ export class HaIntegrationCard extends LitElement { if (newName === null) { return; } - const result = await updateConfigEntry(this.hass, configEntry.entry_id, { + await updateConfigEntry(this.hass, configEntry.entry_id, { title: newName, }); - fireEvent(this, "entry-updated", { entry: result.config_entry }); } private async _signUrl(ev) { diff --git a/src/panels/config/integrations/ha-integration-list-item.ts b/src/panels/config/integrations/ha-integration-list-item.ts new file mode 100644 index 0000000000..26f28b5051 --- /dev/null +++ b/src/panels/config/integrations/ha-integration-list-item.ts @@ -0,0 +1,151 @@ +import { + GraphicType, + ListItemBase, +} from "@material/mwc-list/mwc-list-item-base"; +import { styles } from "@material/mwc-list/mwc-list-item.css"; +import { mdiCloudOutline, mdiCodeBraces, mdiPackageVariant } from "@mdi/js"; +import { css, CSSResultGroup, html } from "lit"; +import { classMap } from "lit/directives/class-map"; +import { customElement, property } from "lit/decorators"; +import { domainToName } from "../../../data/integration"; +import { HomeAssistant } from "../../../types"; +import { brandsUrl } from "../../../util/brands-url"; +import { IntegrationListItem } from "./dialog-add-integration"; + +@customElement("ha-integration-list-item") +export class HaIntegrationListItem extends ListItemBase { + public hass!: HomeAssistant; + + @property({ attribute: false }) public integration?: IntegrationListItem; + + @property({ type: String, reflect: true }) graphic: GraphicType = "medium"; + + @property({ type: Boolean }) hasMeta = true; + + renderSingleLine() { + if (!this.integration) { + return html``; + } + return html`${this.integration.name || + domainToName(this.hass.localize, this.integration.domain)} + ${this.integration.is_helper ? " (helper)" : ""}`; + } + + protected renderGraphic() { + if (!this.integration) { + return html``; + } + const graphicClasses = { + multi: this.multipleGraphics, + }; + + return html` + + `; + } + + protected renderMeta() { + if (!this.integration) { + return html``; + } + return html` + ${!this.integration.config_flow && + !this.integration.integrations && + !this.integration.iot_standards + ? html`${this.hass.localize( + "ui.panel.config.integrations.config_entry.yaml_only" + )}` + : ""} + ${this.integration.cloud + ? html`${this.hass.localize( + "ui.panel.config.integrations.config_entry.depends_on_cloud" + )}` + : ""} + ${!this.integration.is_built_in + ? html`${this.hass.localize( + "ui.panel.config.integrations.config_entry.provided_by_custom_integration" + )}` + : ""} + + `; + } + + static get styles(): CSSResultGroup { + return [ + styles, + css` + :host { + padding-left: var(--mdc-list-side-padding, 20px); + padding-right: var(--mdc-list-side-padding, 20px); + } + :host([graphic="avatar"]:not([twoLine])), + :host([graphic="icon"]:not([twoLine])) { + height: 48px; + } + 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); + } + img { + width: 40px; + height: 40px; + } + .mdc-deprecated-list-item__meta { + width: auto; + } + .mdc-deprecated-list-item__meta > * { + margin-right: 8px; + } + .mdc-deprecated-list-item__meta > *:last-child { + margin-right: 0px; + } + ha-icon-next { + margin-right: 8px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-integration-list-item": HaIntegrationListItem; + } +} diff --git a/src/panels/config/integrations/integration-panels/zha/dialog-zha-cluster.ts b/src/panels/config/integrations/integration-panels/zha/dialog-zha-cluster.ts deleted file mode 100644 index 84ea96c629..0000000000 --- a/src/panels/config/integrations/integration-panels/zha/dialog-zha-cluster.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { HASSDomEvent } from "../../../../../common/dom/fire_event"; -import "../../../../../components/ha-code-editor"; -import { createCloseHeading } from "../../../../../components/ha-dialog"; -import { - Cluster, - fetchBindableDevices, - fetchGroups, - ZHADevice, - ZHAGroup, -} from "../../../../../data/zha"; -import { haStyleDialog } from "../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../types"; -import { sortZHADevices, sortZHAGroups } from "./functions"; -import { ZHADeviceZigbeeInfoDialogParams } from "./show-dialog-zha-device-zigbee-info"; -import { ZHAClusterSelectedParams } from "./types"; -import "./zha-cluster-attributes"; -import "./zha-cluster-commands"; -import "./zha-clusters"; -import "./zha-device-binding"; -import "./zha-group-binding"; - -@customElement("dialog-zha-cluster") -class DialogZHACluster extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @state() private _device?: ZHADevice; - - @state() private _selectedCluster?: Cluster; - - @state() private _bindableDevices: ZHADevice[] = []; - - @state() private _groups: ZHAGroup[] = []; - - public async showDialog( - params: ZHADeviceZigbeeInfoDialogParams - ): Promise { - this._device = params.device; - } - - protected updated(changedProperties: PropertyValues): void { - super.update(changedProperties); - if (changedProperties.has("_device")) { - this._fetchData(); - } - } - - protected render(): TemplateResult { - if (!this._device) { - return html``; - } - - return html` - - - ${this._selectedCluster - ? html` - - - ` - : ""} - ${this._bindableDevices.length > 0 - ? html` - - ` - : ""} - ${this._device && this._groups.length > 0 - ? html` - - ` - : ""} - - `; - } - - private _onClusterSelected( - selectedClusterEvent: HASSDomEvent - ): void { - this._selectedCluster = selectedClusterEvent.detail.cluster; - } - - private _close(): void { - this._device = undefined; - } - - private async _fetchData(): Promise { - if (this._device && this.hass) { - this._bindableDevices = - this._device && this._device.device_type !== "Coordinator" - ? (await fetchBindableDevices(this.hass, this._device.ieee)).sort( - sortZHADevices - ) - : []; - this._groups = (await fetchGroups(this.hass!)).sort(sortZHAGroups); - } - } - - static get styles(): CSSResultGroup { - return haStyleDialog; - } -} - -declare global { - interface HTMLElementTagNameMap { - "dialog-zha-cluster": DialogZHACluster; - } -} diff --git a/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-zigbee-info.ts b/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-zigbee-info.ts deleted file mode 100644 index b9334e46d4..0000000000 --- a/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-zigbee-info.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import "../../../../../components/ha-code-editor"; -import { createCloseHeading } from "../../../../../components/ha-dialog"; -import { haStyleDialog } from "../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../types"; -import { ZHADeviceZigbeeInfoDialogParams } from "./show-dialog-zha-device-zigbee-info"; - -@customElement("dialog-zha-device-zigbee-info") -class DialogZHADeviceZigbeeInfo extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @state() private _signature: any; - - public async showDialog( - params: ZHADeviceZigbeeInfoDialogParams - ): Promise { - this._signature = JSON.stringify( - { - ...params.device.signature, - manufacturer: params.device.manufacturer, - model: params.device.model, - class: params.device.quirk_class, - }, - null, - 2 - ); - } - - protected render(): TemplateResult { - if (!this._signature) { - return html``; - } - - return html` - - - - - `; - } - - private _close(): void { - this._signature = undefined; - } - - static get styles(): CSSResultGroup { - return haStyleDialog; - } -} - -declare global { - interface HTMLElementTagNameMap { - "dialog-zha-device-zigbee-info": DialogZHADeviceZigbeeInfo; - } -} diff --git a/src/panels/config/integrations/integration-panels/zha/dialog-zha-manage-zigbee-device.ts b/src/panels/config/integrations/integration-panels/zha/dialog-zha-manage-zigbee-device.ts new file mode 100644 index 0000000000..5ccacb9d90 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zha/dialog-zha-manage-zigbee-device.ts @@ -0,0 +1,291 @@ +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { mdiClose } from "@mdi/js"; +import { customElement, property, state } from "lit/decorators"; +import { cache } from "lit/directives/cache"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import "../../../../../components/ha-code-editor"; +import { createCloseHeading } from "../../../../../components/ha-dialog"; +import { + fetchBindableDevices, + fetchGroups, + ZHADevice, + ZHAGroup, +} from "../../../../../data/zha"; +import { haStyleDialog } from "../../../../../resources/styles"; +import { HomeAssistant } from "../../../../../types"; +import { sortZHADevices, sortZHAGroups } from "./functions"; +import "./zha-cluster-attributes"; +import "./zha-cluster-commands"; +import "./zha-manage-clusters"; +import "./zha-device-binding"; +import "./zha-group-binding"; +import "./zha-device-children"; +import "./zha-device-signature"; +import { + Tab, + ZHAManageZigbeeDeviceDialogParams, +} from "./show-dialog-zha-manage-zigbee-device"; +import "../../../../../components/ha-header-bar"; +import "@material/mwc-tab-bar/mwc-tab-bar"; +import "@material/mwc-tab/mwc-tab"; + +@customElement("dialog-zha-manage-zigbee-device") +class DialogZHAManageZigbeeDevice extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean, reflect: true }) public large = false; + + @state() private _currTab: Tab = "clusters"; + + @state() private _device?: ZHADevice; + + @state() private _bindableDevices: ZHADevice[] = []; + + @state() private _groups: ZHAGroup[] = []; + + public async showDialog( + params: ZHAManageZigbeeDeviceDialogParams + ): Promise { + this._device = params.device; + if (!this._device) { + this.closeDialog(); + return; + } + this._currTab = params.tab || "clusters"; + this.large = false; + } + + public closeDialog() { + this._device = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + this.addEventListener("close-dialog", () => this.closeDialog()); + } + + protected willUpdate(changedProps: PropertyValues) { + super.willUpdate(changedProps); + if (!this._device) { + return; + } + if (changedProps.has("_device")) { + const tabs = this._getTabs(this._device); + if (!tabs.includes(this._currTab)) { + this._currTab = tabs[0]; + } + this._fetchData(); + } + } + + protected render(): TemplateResult { + if (!this._device) { + return html``; + } + + const tabs = this._getTabs(this._device); + + return html` + +
+ + +
+ ${this.hass.localize("ui.dialogs.zha_manage_device.heading")} +
+
+ + ${tabs.map( + (tab) => html` + + ` + )} + +
+ +
+ ${cache( + this._currTab === "clusters" + ? html` + + ` + : this._currTab === "bindings" + ? html` + ${this._bindableDevices.length > 0 + ? html` + + ` + : ""} + ${this._device && this._groups.length > 0 + ? html` + + ` + : ""} + ` + : this._currTab === "signature" + ? html` + + ` + : html` + + ` + )} +
+
+ `; + } + + private async _fetchData(): Promise { + if (this._device && this.hass) { + this._bindableDevices = + this._device && this._device.device_type !== "Coordinator" + ? (await fetchBindableDevices(this.hass, this._device.ieee)).sort( + sortZHADevices + ) + : []; + this._groups = (await fetchGroups(this.hass!)).sort(sortZHAGroups); + } + } + + private _enlarge() { + this.large = !this.large; + } + + private _handleTabChanged(ev: CustomEvent): void { + const newTab = this._getTabs(this._device)[ev.detail.index]; + if (newTab === this._currTab) { + return; + } + this._currTab = newTab; + } + + private _getTabs = memoizeOne((device: ZHADevice | undefined) => { + const tabs: Tab[] = ["clusters", "bindings", "signature"]; + + if ( + device && + (device.device_type === "Router" || device.device_type === "Coordinator") + ) { + tabs.push("children"); + } + + return tabs; + }); + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + ha-dialog { + --dialog-surface-position: static; + --dialog-content-position: static; + --vertial-align-dialog: flex-start; + } + + ha-header-bar { + --mdc-theme-on-primary: var(--primary-text-color); + --mdc-theme-primary: var(--mdc-theme-surface); + flex-shrink: 0; + display: block; + } + .content { + outline: none; + } + @media all and (max-width: 450px), all and (max-height: 500px) { + ha-header-bar { + --mdc-theme-primary: var(--app-header-background-color); + --mdc-theme-on-primary: var(--app-header-text-color, white); + border-bottom: none; + } + } + + .heading { + border-bottom: 1px solid + var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); + } + + @media all and (min-width: 600px) and (min-height: 501px) { + ha-dialog { + --mdc-dialog-min-width: 560px; + --mdc-dialog-max-width: 560px; + --dialog-surface-margin-top: 40px; + --mdc-dialog-max-height: calc(100% - 72px); + } + + .main-title { + overflow: hidden; + text-overflow: ellipsis; + cursor: default; + } + + :host([large]) ha-dialog, + ha-dialog[data-domain="camera"] { + --mdc-dialog-min-width: 90vw; + --mdc-dialog-max-width: 90vw; + } + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-zha-manage-zigbee-device": DialogZHAManageZigbeeDevice; + } +} diff --git a/src/panels/config/integrations/integration-panels/zha/dialog-zha-reconfigure-device.ts b/src/panels/config/integrations/integration-panels/zha/dialog-zha-reconfigure-device.ts index 1ae26ad04d..4c43266385 100644 --- a/src/panels/config/integrations/integration-panels/zha/dialog-zha-reconfigure-device.ts +++ b/src/panels/config/integrations/integration-panels/zha/dialog-zha-reconfigure-device.ts @@ -12,7 +12,7 @@ import { Cluster, ClusterConfigurationEvent, ClusterConfigurationStatus, - fetchClustersForZhaNode, + fetchClustersForZhaDevice, reconfigureNode, ZHA_CHANNEL_CFG_DONE, ZHA_CHANNEL_MSG_BIND, @@ -321,16 +321,16 @@ class DialogZHAReconfigureDevice extends LitElement { return; } this._clusterConfigurationStatuses = new Map( - (await fetchClustersForZhaNode(this.hass, this._params.device.ieee)).map( - (cluster: Cluster) => [ - cluster.id, - { - cluster: cluster, - bindSuccess: undefined, - attributes: new Map(), - }, - ] - ) + ( + await fetchClustersForZhaDevice(this.hass, this._params.device.ieee) + ).map((cluster: Cluster) => [ + cluster.id, + { + cluster: cluster, + bindSuccess: undefined, + attributes: new Map(), + }, + ]) ); this._subscribe(this._params); this._status = "started"; diff --git a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-cluster.ts b/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-cluster.ts deleted file mode 100644 index 9b0c44ad5d..0000000000 --- a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-cluster.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { fireEvent } from "../../../../../common/dom/fire_event"; -import { ZHADevice } from "../../../../../data/zha"; - -export interface ZHAClusterDialogParams { - device: ZHADevice; -} - -export const loadZHAClusterDialog = () => import("./dialog-zha-cluster"); - -export const showZHAClusterDialog = ( - element: HTMLElement, - params: ZHAClusterDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-zha-cluster", - dialogImport: loadZHAClusterDialog, - dialogParams: params, - }); -}; diff --git a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-device-children.ts b/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-device-children.ts deleted file mode 100644 index f8a3e64ab0..0000000000 --- a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-device-children.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { fireEvent } from "../../../../../common/dom/fire_event"; -import { ZHADevice } from "../../../../../data/zha"; - -export interface ZHADeviceChildrenDialogParams { - device: ZHADevice; -} - -export const loadZHADeviceChildrenDialog = () => - import("./dialog-zha-device-children"); - -export const showZHADeviceChildrenDialog = ( - element: HTMLElement, - zhaDeviceChildrenParams: ZHADeviceChildrenDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-zha-device-children", - dialogImport: loadZHADeviceChildrenDialog, - dialogParams: zhaDeviceChildrenParams, - }); -}; diff --git a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info.ts b/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info.ts deleted file mode 100644 index e4a2191563..0000000000 --- a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { fireEvent } from "../../../../../common/dom/fire_event"; -import { ZHADevice } from "../../../../../data/zha"; - -export interface ZHADeviceZigbeeInfoDialogParams { - device: ZHADevice; -} - -export const loadZHADeviceZigbeeInfoDialog = () => - import("./dialog-zha-device-zigbee-info"); - -export const showZHADeviceZigbeeInfoDialog = ( - element: HTMLElement, - zhaDeviceZigbeeInfoParams: ZHADeviceZigbeeInfoDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-zha-device-zigbee-info", - dialogImport: loadZHADeviceZigbeeInfoDialog, - dialogParams: zhaDeviceZigbeeInfoParams, - }); -}; diff --git a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-manage-zigbee-device.ts b/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-manage-zigbee-device.ts new file mode 100644 index 0000000000..a2854748ff --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-manage-zigbee-device.ts @@ -0,0 +1,23 @@ +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { ZHADevice } from "../../../../../data/zha"; + +export type Tab = "clusters" | "bindings" | "signature" | "children"; + +export interface ZHAManageZigbeeDeviceDialogParams { + device: ZHADevice; + tab?: Tab; +} + +export const loadZHAManageZigbeeDeviceDialog = () => + import("./dialog-zha-manage-zigbee-device"); + +export const showZHAManageZigbeeDeviceDialog = ( + element: HTMLElement, + params: ZHAManageZigbeeDeviceDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-zha-manage-zigbee-device", + dialogImport: loadZHAManageZigbeeDeviceDialog, + dialogParams: params, + }); +}; diff --git a/src/panels/config/integrations/integration-panels/zha/types.ts b/src/panels/config/integrations/integration-panels/zha/types.ts index dbc2954f2d..3e9af96a18 100644 --- a/src/panels/config/integrations/integration-panels/zha/types.ts +++ b/src/panels/config/integrations/integration-panels/zha/types.ts @@ -1,5 +1,5 @@ import { HaSelect } from "../../../../../components/ha-select"; -import { Cluster, ZHADevice } from "../../../../../data/zha"; +import { ZHADevice } from "../../../../../data/zha"; export interface ItemSelectedEvent { target?: HaSelect; @@ -41,10 +41,6 @@ export interface ZHADeviceSelectedParams { node: ZHADevice; } -export interface ZHAClusterSelectedParams { - cluster: Cluster; -} - export interface NodeServiceData { ieee_address: string; } diff --git a/src/panels/config/integrations/integration-panels/zha/zha-cluster-attributes.ts b/src/panels/config/integrations/integration-panels/zha/zha-cluster-attributes.ts index c764a135b3..a0a3695dea 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-cluster-attributes.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-cluster-attributes.ts @@ -1,6 +1,4 @@ -import "@material/mwc-button"; import "@material/mwc-list/mwc-list-item"; -import { mdiHelpCircle } from "@mdi/js"; import "@polymer/paper-input/paper-input"; import { css, @@ -10,13 +8,12 @@ import { PropertyValues, TemplateResult, } from "lit"; -import { property, state } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import { stopPropagation } from "../../../../../common/dom/stop_propagation"; import "../../../../../components/buttons/ha-call-service-button"; import "../../../../../components/ha-card"; -import "../../../../../components/ha-icon-button"; import "../../../../../components/ha-select"; -import "../../../../../components/ha-service-description"; +import "../../../../../components/buttons/ha-progress-button"; import { Attribute, Cluster, @@ -27,7 +24,7 @@ import { } from "../../../../../data/zha"; import { haStyle } from "../../../../../resources/styles"; import { HomeAssistant } from "../../../../../types"; -import "../../../ha-config-section"; +import { forwardHaptic } from "../../../../../data/haptics"; import { formatAsPaddedHex } from "./functions"; import { ChangeEvent, @@ -35,18 +32,15 @@ import { SetAttributeServiceData, } from "./types"; +@customElement("zha-cluster-attributes") export class ZHAClusterAttributes extends LitElement { @property({ attribute: false }) public hass?: HomeAssistant; - @property() public isWide?: boolean; - - @property() public showHelp = false; - - @property() public selectedNode?: ZHADevice; + @property() public device?: ZHADevice; @property() public selectedCluster?: Cluster; - @state() private _attributes: Attribute[] = []; + @state() private _attributes: Attribute[] | undefined; @state() private _selectedAttributeId?: number; @@ -54,78 +48,52 @@ export class ZHAClusterAttributes extends LitElement { @state() private _manufacturerCodeOverride?: string | number; + @state() private _readingAttribute = false; + @state() private _setAttributeServiceData?: SetAttributeServiceData; protected updated(changedProperties: PropertyValues): void { if (changedProperties.has("selectedCluster")) { - this._attributes = []; + this._attributes = undefined; this._selectedAttributeId = undefined; this._attributeValue = ""; this._fetchAttributesForCluster(); } - super.update(changedProperties); + super.updated(changedProperties); } protected render(): TemplateResult { + if (!this.device || !this.selectedCluster || !this._attributes) { + return html``; + } return html` - -
- - ${this.hass!.localize( - "ui.panel.config.zha.cluster_attributes.header" + +
+ - - -
- - ${this.hass!.localize( - "ui.panel.config.zha.cluster_attributes.introduction" - )} - - - -
- - ${this._attributes.map( - (entry) => html` - - ${entry.name + " (id: " + formatAsPaddedHex(entry.id) + ")"} - - ` - )} - -
- ${this.showHelp - ? html` -
- ${this.hass!.localize( - "ui.panel.config.zha.cluster_attributes.help_attribute_dropdown" - )} -
+ ${this._attributes.map( + (entry) => html` + + ${`${entry.name} (id: ${formatAsPaddedHex(entry.id)})`} + ` - : ""} - ${this._selectedAttributeId !== undefined - ? this._renderAttributeInteractions() - : ""} -
- + )} + +
+ ${this._selectedAttributeId !== undefined + ? this._renderAttributeInteractions() + : ""} + `; } @@ -152,20 +120,15 @@ export class ZHAClusterAttributes extends LitElement { >
- + ${this.hass!.localize( - "ui.panel.config.zha.cluster_attributes.get_zigbee_attribute" + "ui.panel.config.zha.cluster_attributes.read_zigbee_attribute" )} - - ${this.showHelp - ? html` -
- ${this.hass!.localize( - "ui.panel.config.zha.cluster_attributes.help_get_zigbee_attribute" - )} -
- ` - : ""} + ${this.hass!.localize( - "ui.panel.config.zha.cluster_attributes.set_zigbee_attribute" + "ui.panel.config.zha.cluster_attributes.write_zigbee_attribute" )} - ${this.showHelp - ? html` - - ` - : ""}
`; } private async _fetchAttributesForCluster(): Promise { - if (this.selectedNode && this.selectedCluster && this.hass) { + if (this.device && this.selectedCluster && this.hass) { this._attributes = await fetchAttributesForCluster( this.hass, - this.selectedNode!.ieee, + this.device!.ieee, this.selectedCluster!.endpoint_id, this.selectedCluster!.id, this.selectedCluster!.type ); this._attributes.sort((a, b) => a.name.localeCompare(b.name)); + if (this._attributes.length > 0) { + this._selectedAttributeId = this._attributes[0].id; + } } } private _computeReadAttributeServiceData(): | ReadAttributeServiceData | undefined { - if (!this.selectedCluster || !this.selectedNode) { + if (!this.selectedCluster || !this.device) { return undefined; } return { - ieee: this.selectedNode!.ieee, + ieee: this.device!.ieee, endpoint_id: this.selectedCluster!.endpoint_id, cluster_id: this.selectedCluster!.id, cluster_type: this.selectedCluster!.type, @@ -224,11 +180,11 @@ export class ZHAClusterAttributes extends LitElement { private _computeSetAttributeServiceData(): | SetAttributeServiceData | undefined { - if (!this.selectedCluster || !this.selectedNode) { + if (!this.selectedCluster || !this.device) { return undefined; } return { - ieee: this.selectedNode!.ieee, + ieee: this.device!.ieee, endpoint_id: this.selectedCluster!.endpoint_id, cluster_id: this.selectedCluster!.id, cluster_type: this.selectedCluster!.type, @@ -250,17 +206,24 @@ export class ZHAClusterAttributes extends LitElement { this._setAttributeServiceData = this._computeSetAttributeServiceData(); } - private async _onGetZigbeeAttributeClick(): Promise { + private async _onGetZigbeeAttributeClick(ev: CustomEvent): Promise { + const button = ev.currentTarget as any; const data = this._computeReadAttributeServiceData(); if (data && this.hass) { - this._attributeValue = await readAttributeValue(this.hass, data); + this._readingAttribute = true; + try { + this._attributeValue = await readAttributeValue(this.hass, data); + forwardHaptic("success"); + button.actionSuccess(); + } catch (err: any) { + forwardHaptic("failure"); + button.actionError(); + } finally { + this._readingAttribute = false; + } } } - private _onHelpTap(): void { - this.showHelp = !this.showHelp; - } - private _selectedAttributeChanged(event: ItemSelectedEvent): void { this._selectedAttributeId = Number(event.target!.value); this._attributeValue = ""; @@ -278,14 +241,6 @@ export class ZHAClusterAttributes extends LitElement { width: 100%; } - .content { - margin-top: 24px; - } - - ha-card { - max-width: 680px; - } - .card-actions.warning ha-call-service-button { color: var(--error-color); } @@ -306,33 +261,6 @@ export class ZHAClusterAttributes extends LitElement { .header { flex-grow: 1; } - - .toggle-help-icon { - float: right; - top: -6px; - right: 0; - padding-right: 0px; - color: var(--primary-color); - } - - ha-service-description { - display: block; - color: grey; - } - - [hidden] { - display: none; - } - .help-text { - color: grey; - padding-left: 28px; - padding-right: 28px; - padding-bottom: 16px; - } - .help-text2 { - color: grey; - padding: 16px; - } `, ]; } @@ -343,5 +271,3 @@ declare global { "zha-cluster-attributes": ZHAClusterAttributes; } } - -customElements.define("zha-cluster-attributes", ZHAClusterAttributes); diff --git a/src/panels/config/integrations/integration-panels/zha/zha-cluster-commands.ts b/src/panels/config/integrations/integration-panels/zha/zha-cluster-commands.ts index ca3a29be5f..129e42e74f 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-cluster-commands.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-cluster-commands.ts @@ -1,5 +1,4 @@ import "@material/mwc-list/mwc-list-item"; -import { mdiHelpCircle } from "@mdi/js"; import "@polymer/paper-input/paper-input"; import { css, @@ -13,9 +12,7 @@ import { property, state } from "lit/decorators"; import { stopPropagation } from "../../../../../common/dom/stop_propagation"; import "../../../../../components/buttons/ha-call-service-button"; import "../../../../../components/ha-card"; -import "../../../../../components/ha-icon-button"; import "../../../../../components/ha-select"; -import "../../../../../components/ha-service-description"; import { Cluster, Command, @@ -24,7 +21,6 @@ import { } from "../../../../../data/zha"; import { haStyle } from "../../../../../resources/styles"; import { HomeAssistant } from "../../../../../types"; -import "../../../ha-config-section"; import { formatAsPaddedHex } from "./functions"; import { ChangeEvent, IssueCommandServiceData } from "./types"; @@ -33,13 +29,11 @@ export class ZHAClusterCommands extends LitElement { @property() public isWide?: boolean; - @property() public selectedNode?: ZHADevice; + @property() public device?: ZHADevice; @property() public selectedCluster?: Cluster; - @state() private _showHelp = false; - - @state() private _commands: Command[] = []; + @state() private _commands: Command[] | undefined; @state() private _selectedCommandId?: number; @@ -50,132 +44,97 @@ export class ZHAClusterCommands extends LitElement { protected updated(changedProperties: PropertyValues): void { if (changedProperties.has("selectedCluster")) { - this._commands = []; + this._commands = undefined; this._selectedCommandId = undefined; this._fetchCommandsForCluster(); } - super.update(changedProperties); + super.updated(changedProperties); } protected render(): TemplateResult { + if (!this.device || !this.selectedCluster || !this._commands) { + return html``; + } return html` - -
- - ${this.hass!.localize( - "ui.panel.config.zha.cluster_commands.header" + +
+ - - + ${this._commands.map( + (entry) => html` + + ${entry.name + " (id: " + formatAsPaddedHex(entry.id) + ")"} + + ` + )} +
- - ${this.hass!.localize( - "ui.panel.config.zha.cluster_commands.introduction" - )} - - - -
- - ${this._commands.map( - (entry) => html` - - ${entry.name + " (id: " + formatAsPaddedHex(entry.id) + ")"} - - ` - )} - -
- ${this._showHelp - ? html` -
- ${this.hass!.localize( - "ui.panel.config.zha.cluster_commands.help_command_dropdown" + ${this._selectedCommandId !== undefined + ? html` +
+ - ` - : ""} - ${this._selectedCommandId !== undefined - ? html` -
- -
-
- - ${this.hass!.localize( - "ui.panel.config.zha.cluster_commands.issue_zigbee_command" - )} - - ${this._showHelp - ? html` - - ` - : ""} -
- ` - : ""} - - + type="number" + .value=${this._manufacturerCodeOverride} + @value-changed=${this._onManufacturerCodeOverrideChanged} + placeholder=${this.hass!.localize( + "ui.panel.config.zha.common.value" + )} + >
+
+
+ + ${this.hass!.localize( + "ui.panel.config.zha.cluster_commands.issue_zigbee_command" + )} + +
+ ` + : ""} + `; } private async _fetchCommandsForCluster(): Promise { - if (this.selectedNode && this.selectedCluster && this.hass) { + if (this.device && this.selectedCluster && this.hass) { this._commands = await fetchCommandsForCluster( this.hass, - this.selectedNode!.ieee, + this.device!.ieee, this.selectedCluster!.endpoint_id, this.selectedCluster!.id, this.selectedCluster!.type ); this._commands.sort((a, b) => a.name.localeCompare(b.name)); + if (this._commands.length > 0) { + this._selectedCommandId = this._commands[0].id; + } } } private _computeIssueClusterCommandServiceData(): | IssueCommandServiceData | undefined { - if (!this.selectedNode || !this.selectedCluster) { + if (!this.device || !this.selectedCluster || !this._commands) { return undefined; } return { - ieee: this.selectedNode!.ieee, + ieee: this.device!.ieee, endpoint_id: this.selectedCluster!.endpoint_id, cluster_id: this.selectedCluster!.id, cluster_type: this.selectedCluster!.type, @@ -192,10 +151,6 @@ export class ZHAClusterCommands extends LitElement { this._computeIssueClusterCommandServiceData(); } - private _onHelpTap(): void { - this._showHelp = !this._showHelp; - } - private _selectedCommandChanged(event): void { this._selectedCommandId = Number(event.target.value); this._issueClusterCommandServiceData = @@ -213,14 +168,6 @@ export class ZHAClusterCommands extends LitElement { width: 100%; } - .content { - margin-top: 24px; - } - - ha-card { - max-width: 680px; - } - .card-actions.warning ha-call-service-button { color: var(--error-color); } @@ -238,18 +185,6 @@ export class ZHAClusterCommands extends LitElement { padding-bottom: 10px; } - .help-text { - color: grey; - padding-left: 28px; - padding-right: 28px; - padding-bottom: 16px; - } - - .help-text2 { - color: grey; - padding: 16px; - } - .header { flex-grow: 1; } @@ -261,15 +196,6 @@ export class ZHAClusterCommands extends LitElement { padding-right: 0px; color: var(--primary-color); } - - ha-service-description { - display: block; - color: grey; - } - - [hidden] { - display: none; - } `, ]; } diff --git a/src/panels/config/integrations/integration-panels/zha/zha-clusters-data-table.ts b/src/panels/config/integrations/integration-panels/zha/zha-clusters-data-table.ts index f4f0e5fdbf..9cebc071b2 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-clusters-data-table.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-clusters-data-table.ts @@ -59,7 +59,7 @@ export class ZHAClustersDataTable extends LitElement { title: "ID", template: (id: number) => html` ${formatAsPaddedHex(id)} `, sortable: true, - width: "15%", + width: "25%", }, endpoint_id: { title: "Endpoint ID", diff --git a/src/panels/config/integrations/integration-panels/zha/zha-clusters.ts b/src/panels/config/integrations/integration-panels/zha/zha-clusters.ts deleted file mode 100644 index 19f2d270d2..0000000000 --- a/src/panels/config/integrations/integration-panels/zha/zha-clusters.ts +++ /dev/null @@ -1,195 +0,0 @@ -import "@material/mwc-list/mwc-list-item"; -import { mdiHelpCircle } from "@mdi/js"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { property, state } from "lit/decorators"; -import { fireEvent } from "../../../../../common/dom/fire_event"; -import { stopPropagation } from "../../../../../common/dom/stop_propagation"; -import "../../../../../components/buttons/ha-call-service-button"; -import "../../../../../components/ha-card"; -import "../../../../../components/ha-icon-button"; -import "../../../../../components/ha-select"; -import "../../../../../components/ha-service-description"; -import { - Cluster, - fetchClustersForZhaNode, - ZHADevice, -} from "../../../../../data/zha"; -import { haStyle } from "../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../types"; -import "../../../ha-config-section"; -import { computeClusterKey } from "./functions"; - -declare global { - // for fire event - interface HASSDomEvents { - "zha-cluster-selected": { - cluster?: Cluster; - }; - } -} - -export class ZHAClusters extends LitElement { - @property({ attribute: false }) public hass?: HomeAssistant; - - @property() public isWide?: boolean; - - @property() public selectedDevice?: ZHADevice; - - @property() public showHelp = false; - - @state() private _selectedClusterIndex = -1; - - @state() private _clusters: Cluster[] = []; - - protected updated(changedProperties: PropertyValues): void { - if (changedProperties.has("selectedDevice")) { - this._clusters = []; - this._selectedClusterIndex = -1; - fireEvent(this, "zha-cluster-selected", { - cluster: undefined, - }); - this._fetchClustersForZhaNode(); - } - super.update(changedProperties); - } - - protected render(): TemplateResult { - return html` - -
- - -
- - ${this.hass!.localize("ui.panel.config.zha.clusters.introduction")} - - - -
- - ${this._clusters.map( - (entry, idx) => html` - ${computeClusterKey(entry)} - ` - )} - -
- ${this.showHelp - ? html` -
- ${this.hass!.localize( - "ui.panel.config.zha.clusters.help_cluster_dropdown" - )} -
- ` - : ""} -
-
- `; - } - - private async _fetchClustersForZhaNode(): Promise { - if (this.hass) { - this._clusters = await fetchClustersForZhaNode( - this.hass, - this.selectedDevice!.ieee - ); - this._clusters.sort((a, b) => a.name.localeCompare(b.name)); - } - } - - private _selectedClusterChanged(event): void { - this._selectedClusterIndex = Number(event.target!.value); - fireEvent(this, "zha-cluster-selected", { - cluster: this._clusters[this._selectedClusterIndex], - }); - } - - private _onHelpTap(): void { - this.showHelp = !this.showHelp; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - ha-select { - margin-top: 16px; - } - .menu { - width: 100%; - } - - .content { - margin-top: 24px; - } - - .header { - flex-grow: 1; - } - - ha-card { - max-width: 680px; - } - - .node-picker { - align-items: center; - padding-left: 28px; - padding-right: 28px; - padding-bottom: 10px; - } - - .toggle-help-icon { - float: right; - top: -6px; - right: 0; - padding-right: 0px; - color: var(--primary-color); - } - - [hidden] { - display: none; - } - - .help-text { - color: grey; - padding-left: 28px; - padding-right: 28px; - padding-bottom: 16px; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "zha-cluster": ZHAClusters; - } -} - -customElements.define("zha-clusters", ZHAClusters); diff --git a/src/panels/config/integrations/integration-panels/zha/zha-device-binding.ts b/src/panels/config/integrations/integration-panels/zha/zha-device-binding.ts index 1445a572d5..993aaad79e 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-device-binding.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-device-binding.ts @@ -1,6 +1,4 @@ -import "@material/mwc-button/mwc-button"; import "@material/mwc-list/mwc-list-item"; -import { mdiHelpCircle } from "@mdi/js"; import { css, CSSResultGroup, @@ -11,26 +9,19 @@ import { } from "lit"; import { customElement, property, state } from "lit/decorators"; import { stopPropagation } from "../../../../../common/dom/stop_propagation"; -import "../../../../../components/buttons/ha-call-service-button"; +import "../../../../../components/buttons/ha-progress-button"; import "../../../../../components/ha-card"; -import "../../../../../components/ha-icon-button"; import "../../../../../components/ha-select"; -import "../../../../../components/ha-service-description"; import { bindDevices, unbindDevices, ZHADevice } from "../../../../../data/zha"; import { haStyle } from "../../../../../resources/styles"; import { HomeAssistant } from "../../../../../types"; -import "../../../ha-config-section"; import { ItemSelectedEvent } from "./types"; @customElement("zha-device-binding-control") export class ZHADeviceBindingControl extends LitElement { @property({ attribute: false }) public hass?: HomeAssistant; - @property() public isWide?: boolean; - - @property() public selectedDevice?: ZHADevice; - - @state() private _showHelp = false; + @property() public device?: ZHADevice; @state() private _bindTargetIndex = -1; @@ -38,77 +29,58 @@ export class ZHADeviceBindingControl extends LitElement { @state() private _deviceToBind?: ZHADevice; + @state() private _bindingOperationInProgress = false; + protected updated(changedProperties: PropertyValues): void { - if (changedProperties.has("selectedDevice")) { + if (changedProperties.has("device")) { this._bindTargetIndex = -1; } - super.update(changedProperties); + super.updated(changedProperties); } protected render(): TemplateResult { return html` - -
- Device Binding - +
+ - -
- Bind and unbind devices. - - -
- - ${this.bindableDevices.map( - (device, idx) => html` - - ${device.user_given_name - ? device.user_given_name - : device.name} - - ` - )} - -
- ${this._showHelp - ? html` -
- Select a device to issue a bind command. -
+ ${this.bindableDevices.map( + (device, idx) => html` + + ${device.user_given_name + ? device.user_given_name + : device.name} + ` - : ""} -
- Bind - ${this._showHelp - ? html`
Bind devices.
` - : ""} - Unbind - ${this._showHelp - ? html`
Unbind devices.
` - : ""} -
-
- + )} + +
+
+ + ${this.hass!.localize("ui.panel.config.zha.device_binding.bind")} + + + ${this.hass!.localize("ui.panel.config.zha.device_binding.unbind")} + +
+ `; } @@ -120,27 +92,41 @@ export class ZHADeviceBindingControl extends LitElement { : this.bindableDevices[this._bindTargetIndex]; } - private _onHelpTap(): void { - this._showHelp = !this._showHelp; - } - - private async _onBindDevicesClick(): Promise { - if (this.hass && this._deviceToBind && this.selectedDevice) { - await bindDevices( - this.hass, - this.selectedDevice.ieee, - this._deviceToBind.ieee - ); + private async _onBindDevicesClick(ev: CustomEvent): Promise { + const button = ev.currentTarget as any; + if (this.hass && this._deviceToBind && this.device) { + this._bindingOperationInProgress = true; + button.progress = true; + try { + await bindDevices(this.hass, this.device.ieee, this._deviceToBind.ieee); + button.actionSuccess(); + } catch (err: any) { + button.actionError(); + } finally { + this._bindingOperationInProgress = false; + button.progress = false; + } } } - private async _onUnbindDevicesClick(): Promise { - if (this.hass && this._deviceToBind && this.selectedDevice) { - await unbindDevices( - this.hass, - this.selectedDevice.ieee, - this._deviceToBind.ieee - ); + private async _onUnbindDevicesClick(ev: CustomEvent): Promise { + const button = ev.currentTarget as any; + if (this.hass && this._deviceToBind && this.device) { + this._bindingOperationInProgress = true; + button.progress = true; + try { + await unbindDevices( + this.hass, + this.device.ieee, + this._deviceToBind.ieee + ); + button.actionSuccess(); + } catch (err: any) { + button.actionError(); + } finally { + this._bindingOperationInProgress = false; + button.progress = false; + } } } @@ -152,18 +138,6 @@ export class ZHADeviceBindingControl extends LitElement { width: 100%; } - .content { - margin-top: 24px; - } - - ha-card { - max-width: 680px; - } - - .card-actions.warning ha-call-service-button { - color: var(--error-color); - } - .command-picker { align-items: center; padding-left: 28px; @@ -171,33 +145,9 @@ export class ZHADeviceBindingControl extends LitElement { padding-bottom: 10px; } - .helpText { - color: grey; - padding-left: 28px; - padding-right: 28px; - padding-bottom: 10px; - } - .header { flex-grow: 1; } - - .toggle-help-icon { - float: right; - top: -6px; - right: 0; - padding-right: 0px; - color: var(--primary-color); - } - - ha-service-description { - display: block; - color: grey; - } - - [hidden] { - display: none; - } `, ]; } diff --git a/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-children.ts b/src/panels/config/integrations/integration-panels/zha/zha-device-children.ts similarity index 52% rename from src/panels/config/integrations/integration-panels/zha/dialog-zha-device-children.ts rename to src/panels/config/integrations/integration-panels/zha/zha-device-children.ts index 91bd86246c..52a1bdf1b3 100644 --- a/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-children.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-device-children.ts @@ -1,12 +1,9 @@ -import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import memoizeOne from "memoize-one"; import { customElement, property, state } from "lit/decorators"; import { computeRTLDirection } from "../../../../../common/util/compute_rtl"; import "../../../../../components/ha-code-editor"; -import { createCloseHeading } from "../../../../../components/ha-dialog"; -import { haStyleDialog } from "../../../../../resources/styles"; import { HomeAssistant } from "../../../../../types"; -import { ZHADeviceChildrenDialogParams } from "./show-dialog-zha-device-children"; import "../../../../../components/data-table/ha-data-table"; import type { DataTableColumnContainer, @@ -14,7 +11,6 @@ import type { } from "../../../../../components/data-table/ha-data-table"; import "../../../../../components/ha-circular-progress"; import { fetchDevices, ZHADevice } from "../../../../../data/zha"; -import { fireEvent } from "../../../../../common/dom/fire_event"; export interface DeviceRowData extends DataTableRowData { id: string; @@ -22,14 +18,21 @@ export interface DeviceRowData extends DataTableRowData { lqi: number; } -@customElement("dialog-zha-device-children") -class DialogZHADeviceChildren extends LitElement { +@customElement("zha-device-children") +class ZHADeviceChildren extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @state() private _device: ZHADevice | undefined; + @property() public device: ZHADevice | undefined; @state() private _devices: Map | undefined; + protected updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + if (this.hass && changedProperties.has("device")) { + this._fetchData(); + } + } + private _deviceChildren = memoizeOne( ( device: ZHADevice | undefined, @@ -69,70 +72,45 @@ class DialogZHADeviceChildren extends LitElement { }, }; - public showDialog(params: ZHADeviceChildrenDialogParams): void { - this._device = params.device; - this._fetchData(); - } - - public closeDialog(): void { - this._device = undefined; - this._devices = undefined; - fireEvent(this, "dialog-closed", { dialog: this.localName }); - } - protected render(): TemplateResult { - if (!this._device) { + if (!this.device) { return html``; } return html` - - ${!this._devices - ? html`` - : html``} - + ${!this._devices + ? html`` + : html``} `; } private async _fetchData(): Promise { - if (this._device && this.hass) { + if (this.device && this.hass) { const devices = await fetchDevices(this.hass!); this._devices = new Map( devices.map((device: ZHADevice) => [device.ieee, device]) ); } } - - static get styles(): CSSResultGroup { - return haStyleDialog; - } } declare global { interface HTMLElementTagNameMap { - "dialog-zha-device-children": DialogZHADeviceChildren; + "zha-device-children": ZHADeviceChildren; } } diff --git a/src/panels/config/integrations/integration-panels/zha/zha-device-signature.ts b/src/panels/config/integrations/integration-panels/zha/zha-device-signature.ts new file mode 100644 index 0000000000..0ce2b9767a --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zha/zha-device-signature.ts @@ -0,0 +1,47 @@ +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import "../../../../../components/ha-code-editor"; +import { ZHADevice } from "../../../../../data/zha"; +import { HomeAssistant } from "../../../../../types"; + +@customElement("zha-device-zigbee-info") +class ZHADeviceZigbeeInfo extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public device: ZHADevice | undefined; + + @state() private _signature: any; + + protected updated(changedProperties: PropertyValues): void { + if (changedProperties.has("device") && this.hass && this.device) { + this._signature = JSON.stringify( + { + ...this.device.signature, + manufacturer: this.device.manufacturer, + model: this.device.model, + class: this.device.quirk_class, + }, + null, + 2 + ); + } + super.updated(changedProperties); + } + + protected render(): TemplateResult { + if (!this._signature) { + return html``; + } + + return html` + + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "zha-device-zigbee-info": ZHADeviceZigbeeInfo; + } +} diff --git a/src/panels/config/integrations/integration-panels/zha/zha-group-binding.ts b/src/panels/config/integrations/integration-panels/zha/zha-group-binding.ts index 67671e9047..d96b27c9ad 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-group-binding.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-group-binding.ts @@ -1,5 +1,3 @@ -import "@material/mwc-button/mwc-button"; -import { mdiHelpCircle } from "@mdi/js"; import { css, CSSResultGroup, @@ -11,37 +9,29 @@ import { import { customElement, property, state, query } from "lit/decorators"; import type { HASSDomEvent } from "../../../../../common/dom/fire_event"; import { stopPropagation } from "../../../../../common/dom/stop_propagation"; -import "../../../../../components/buttons/ha-call-service-button"; +import "../../../../../components/buttons/ha-progress-button"; import { SelectionChangedEvent } from "../../../../../components/data-table/ha-data-table"; import "../../../../../components/ha-card"; -import "../../../../../components/ha-icon-button"; -import "../../../../../components/ha-service-description"; import { bindDeviceToGroup, Cluster, - fetchClustersForZhaNode, + fetchClustersForZhaDevice, unbindDeviceFromGroup, ZHADevice, ZHAGroup, } from "../../../../../data/zha"; import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant } from "../../../../../types"; -import "../../../ha-config-section"; import { ItemSelectedEvent } from "./types"; import "./zha-clusters-data-table"; import type { ZHAClustersDataTable } from "./zha-clusters-data-table"; +import "@material/mwc-list/mwc-list-item"; @customElement("zha-group-binding-control") export class ZHAGroupBindingControl extends LitElement { @property({ attribute: false }) public hass?: HomeAssistant; - @property() public isWide?: boolean; - - @property() public narrow?: boolean; - - @property() public selectedDevice?: ZHADevice; - - @state() private _showHelp = false; + @property() public device?: ZHADevice; @state() private _bindTargetIndex = -1; @@ -51,6 +41,8 @@ export class ZHAGroupBindingControl extends LitElement { @state() private _clusters: Cluster[] = []; + @state() private _bindingOperationInProgress = false; + private _groupToBind?: ZHAGroup; private _clustersToBind?: Cluster[]; @@ -59,38 +51,17 @@ export class ZHAGroupBindingControl extends LitElement { private _zhaClustersDataTable!: ZHAClustersDataTable; protected updated(changedProperties: PropertyValues): void { - if (changedProperties.has("selectedDevice")) { + if (changedProperties.has("device")) { this._bindTargetIndex = -1; this._selectedClusters = []; this._clustersToBind = []; this._fetchClustersForZhaNode(); } - super.update(changedProperties); + super.updated(changedProperties); } protected render(): TemplateResult { return html` - -
- ${this.hass!.localize( - "ui.panel.config.zha.group_binding.header" - )} - - -
- ${this.hass!.localize( - "ui.panel.config.zha.group_binding.introduction" - )} -
- ${this._showHelp - ? html` -
- ${this.hass!.localize( - "ui.panel.config.zha.group_binding.group_picker_help" - )} -
- ` - : ""}
- ${this._showHelp - ? html` -
- ${this.hass!.localize( - "ui.panel.config.zha.group_binding.cluster_selection_help" - )} -
- ` - : ""}
- ${this.hass!.localize( - "ui.panel.config.zha.group_binding.bind_button_label" - )} - ${this._showHelp - ? html` -
- ${this.hass!.localize( - "ui.panel.config.zha.group_binding.bind_button_help" - )} -
- ` - : ""} - ${this.hass!.localize( - "ui.panel.config.zha.group_binding.unbind_button_label" - )} - ${this._showHelp - ? html` -
- ${this.hass!.localize( - "ui.panel.config.zha.group_binding.unbind_button_help" - )} -
- ` - : ""} + + ${this.hass!.localize( + "ui.panel.config.zha.group_binding.bind_button_label" + )} + + + + ${this.hass!.localize( + "ui.panel.config.zha.group_binding.unbind_button_label" + )} +
@@ -186,31 +123,49 @@ export class ZHAGroupBindingControl extends LitElement { : this.groups[this._bindTargetIndex]; } - private _onHelpTap(): void { - this._showHelp = !this._showHelp; - } - - private async _onBindGroupClick(): Promise { + private async _onBindGroupClick(ev: CustomEvent): Promise { + const button = ev.currentTarget as any; if (this.hass && this._canBind) { - await bindDeviceToGroup( - this.hass, - this.selectedDevice!.ieee, - this._groupToBind!.group_id, - this._clustersToBind! - ); - this._zhaClustersDataTable.clearSelection(); + this._bindingOperationInProgress = true; + button.progress = true; + try { + await bindDeviceToGroup( + this.hass, + this.device!.ieee, + this._groupToBind!.group_id, + this._clustersToBind! + ); + this._zhaClustersDataTable.clearSelection(); + button.actionSuccess(); + } catch (err: any) { + button.actionError(); + } finally { + this._bindingOperationInProgress = false; + button.progress = false; + } } } - private async _onUnbindGroupClick(): Promise { + private async _onUnbindGroupClick(ev: CustomEvent): Promise { + const button = ev.currentTarget as any; if (this.hass && this._canBind) { - await unbindDeviceFromGroup( - this.hass, - this.selectedDevice!.ieee, - this._groupToBind!.group_id, - this._clustersToBind! - ); - this._zhaClustersDataTable.clearSelection(); + this._bindingOperationInProgress = true; + button.progress = true; + try { + await unbindDeviceFromGroup( + this.hass, + this.device!.ieee, + this._groupToBind!.group_id, + this._clustersToBind! + ); + this._zhaClustersDataTable.clearSelection(); + button.actionSuccess(); + } catch (err: any) { + button.actionError(); + } finally { + this._bindingOperationInProgress = false; + button.progress = false; + } } } @@ -230,9 +185,9 @@ export class ZHAGroupBindingControl extends LitElement { private async _fetchClustersForZhaNode(): Promise { if (this.hass) { - this._clusters = await fetchClustersForZhaNode( + this._clusters = await fetchClustersForZhaDevice( this.hass, - this.selectedDevice!.ieee + this.device!.ieee ); this._clusters = this._clusters .filter((cluster) => cluster.type.toLowerCase() === "out") @@ -245,7 +200,7 @@ export class ZHAGroupBindingControl extends LitElement { this._groupToBind && this._clustersToBind && this._clustersToBind?.length > 0 && - this.selectedDevice + this.device ); } @@ -257,18 +212,6 @@ export class ZHAGroupBindingControl extends LitElement { width: 100%; } - .content { - margin-top: 24px; - } - - ha-card { - max-width: 680px; - } - - .card-actions.warning ha-call-service-button { - color: var(--error-color); - } - .command-picker { align-items: center; padding-left: 28px; @@ -285,30 +228,6 @@ export class ZHAGroupBindingControl extends LitElement { .sectionHeader { flex-grow: 1; } - - .helpText { - color: grey; - padding-left: 28px; - padding-right: 28px; - padding-bottom: 10px; - } - - .toggle-help-icon { - float: right; - top: -6px; - right: 0; - padding-right: 0px; - color: var(--primary-color); - } - - ha-service-description { - display: block; - color: grey; - } - - [hidden] { - display: none; - } `, ]; } diff --git a/src/panels/config/integrations/integration-panels/zha/zha-manage-clusters.ts b/src/panels/config/integrations/integration-panels/zha/zha-manage-clusters.ts new file mode 100644 index 0000000000..d956723242 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zha/zha-manage-clusters.ts @@ -0,0 +1,198 @@ +import "@material/mwc-list/mwc-list-item"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { cache } from "lit/directives/cache"; +import { stopPropagation } from "../../../../../common/dom/stop_propagation"; +import "../../../../../components/ha-card"; +import "../../../../../components/ha-select"; +import { + Cluster, + fetchClustersForZhaDevice, + ZHADevice, +} from "../../../../../data/zha"; +import { haStyle } from "../../../../../resources/styles"; +import { HomeAssistant } from "../../../../../types"; +import { computeClusterKey } from "./functions"; +import "@material/mwc-tab-bar/mwc-tab-bar"; +import "@material/mwc-tab/mwc-tab"; +import "./zha-cluster-attributes"; +import "./zha-cluster-commands"; + +declare global { + // for fire event + interface HASSDomEvents { + "zha-cluster-selected": { + cluster?: Cluster; + }; + } +} + +const tabs = ["attributes", "commands"] as const; + +@customElement("zha-manage-clusters") +export class ZHAManageClusters extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public isWide?: boolean; + + @property() public device?: ZHADevice; + + @state() private _selectedClusterIndex = -1; + + @state() private _clusters: Cluster[] = []; + + @state() private _selectedCluster?: Cluster; + + @state() private _currTab: typeof tabs[number] = "attributes"; + + @state() private _clustersLoaded = false; + + protected willUpdate(changedProps: PropertyValues) { + super.willUpdate(changedProps); + if (!this.device) { + return; + } + if (!tabs.includes(this._currTab)) { + this._currTab = tabs[0]; + } + } + + protected updated(changedProperties: PropertyValues): void { + if (changedProperties.has("device")) { + this._clusters = []; + this._selectedClusterIndex = -1; + this._clustersLoaded = false; + this._fetchClustersForZhaDevice(); + } + super.updated(changedProperties); + } + + protected render(): TemplateResult { + if (!this.device || !this._clustersLoaded) { + return html``; + } + return html` + +
+ + ${this._clusters.map( + (entry, idx) => html` + ${computeClusterKey(entry)} + ` + )} + +
+ ${this._selectedCluster + ? html` + + ${tabs.map( + (tab) => html` + + ` + )} + + +
+ ${cache( + this._currTab === "attributes" + ? html` + + ` + : html` + + ` + )} +
+ ` + : ""} +
+ `; + } + + private async _fetchClustersForZhaDevice(): Promise { + if (this.hass) { + this._clusters = await fetchClustersForZhaDevice( + this.hass, + this.device!.ieee + ); + this._clusters.sort((a, b) => a.name.localeCompare(b.name)); + if (this._clusters.length > 0) { + this._selectedClusterIndex = 0; + this._selectedCluster = this._clusters[0]; + } + this._clustersLoaded = true; + } + } + + private _handleTabChanged(ev: CustomEvent): void { + const newTab = tabs[ev.detail.index]; + if (newTab === this._currTab) { + return; + } + this._currTab = newTab; + } + + private _selectedClusterChanged(event): void { + this._selectedClusterIndex = Number(event.target!.value); + this._selectedCluster = this._clusters[this._selectedClusterIndex]; + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + css` + ha-select { + margin-top: 16px; + } + .menu { + width: 100%; + } + .header { + flex-grow: 1; + } + .node-picker { + align-items: center; + padding-bottom: 10px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "zha-manage-clusters": ZHAManageClusters; + } +} diff --git a/src/panels/config/integrations/show-add-integration-dialog.ts b/src/panels/config/integrations/show-add-integration-dialog.ts new file mode 100644 index 0000000000..cb3fb8b9e1 --- /dev/null +++ b/src/panels/config/integrations/show-add-integration-dialog.ts @@ -0,0 +1,12 @@ +import { fireEvent } from "../../../common/dom/fire_event"; + +export const showAddIntegrationDialog = ( + element: HTMLElement, + dialogParams?: any +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-add-integration", + dialogImport: () => import("./dialog-add-integration"), + dialogParams: dialogParams, + }); +}; diff --git a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts index 4646d5361b..55af2f7899 100644 --- a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts +++ b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts @@ -365,6 +365,7 @@ export class HaConfigLovelaceDashboards extends LitElement { "ui.panel.config.lovelace.dashboards.confirm_delete_text" ), confirmText: this.hass!.localize("ui.common.delete"), + destructive: true, })) ) { return false; diff --git a/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts b/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts index 5aaad37f09..b93afcc17d 100644 --- a/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts +++ b/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts @@ -157,9 +157,16 @@ export class HaConfigLovelaceRescources extends LitElement { removeResource: async () => { if ( !(await showConfirmationDialog(this, { - text: this.hass!.localize( - "ui.panel.config.lovelace.resources.confirm_delete" + title: this.hass!.localize( + "ui.panel.config.lovelace.resources.confirm_delete_title" ), + text: this.hass!.localize( + "ui.panel.config.lovelace.resources.confirm_delete_text", + { url: resource!.url } + ), + dismissText: this.hass!.localize("ui.common.cancel"), + confirmText: this.hass!.localize("ui.common.delete"), + destructive: true, })) ) { return false; diff --git a/src/panels/config/person/dialog-person-detail.ts b/src/panels/config/person/dialog-person-detail.ts index c9d0c0c56a..a847150f07 100644 --- a/src/panels/config/person/dialog-person-detail.ts +++ b/src/panels/config/person/dialog-person-detail.ts @@ -5,10 +5,9 @@ import memoizeOne from "memoize-one"; import "../../../components/entity/ha-entities-picker"; import { createCloseHeading } from "../../../components/ha-dialog"; import "../../../components/ha-formfield"; -import "../../../components/ha-textfield"; import "../../../components/ha-picture-upload"; import type { HaPictureUpload } from "../../../components/ha-picture-upload"; -import { adminChangePassword } from "../../../data/auth"; +import "../../../components/ha-textfield"; import { PersonMutableParams } from "../../../data/person"; import { deleteUser, @@ -20,7 +19,6 @@ import { import { showAlertDialog, showConfirmationDialog, - showPromptDialog, } from "../../../dialogs/generic/show-dialog-box"; import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog"; import { PolymerChangedEvent } from "../../../polymer-types"; @@ -28,6 +26,7 @@ import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; import { showAddUserDialog } from "../users/show-dialog-add-user"; +import { showAdminChangePasswordDialog } from "../users/show-dialog-admin-change-password"; import { PersonDetailDialogParams } from "./show-dialog-person-detail"; const includeDomains = ["device_tracker"]; @@ -350,40 +349,7 @@ class DialogPersonDetail extends LitElement { }); return; } - const newPassword = await showPromptDialog(this, { - title: this.hass.localize("ui.panel.config.users.editor.change_password"), - inputType: "password", - inputLabel: this.hass.localize( - "ui.panel.config.users.editor.new_password" - ), - }); - if (!newPassword) { - return; - } - const confirmPassword = await showPromptDialog(this, { - title: this.hass.localize("ui.panel.config.users.editor.change_password"), - inputType: "password", - inputLabel: this.hass.localize( - "ui.panel.config.users.add_user.password_confirm" - ), - }); - if (!confirmPassword) { - return; - } - if (newPassword !== confirmPassword) { - showAlertDialog(this, { - title: this.hass.localize( - "ui.panel.config.users.add_user.password_not_match" - ), - }); - return; - } - await adminChangePassword(this.hass, this._user.id, newPassword); - showAlertDialog(this, { - title: this.hass.localize( - "ui.panel.config.users.editor.password_changed" - ), - }); + showAdminChangePasswordDialog(this, { userId: this._user.id }); } private async _updateEntry() { diff --git a/src/panels/config/person/ha-config-person.ts b/src/panels/config/person/ha-config-person.ts index 4546e79507..87d16caf94 100644 --- a/src/panels/config/person/ha-config-person.ts +++ b/src/panels/config/person/ha-config-person.ts @@ -233,10 +233,16 @@ class HaConfigPerson extends LitElement { removeEntry: async () => { if ( !(await showConfirmationDialog(this, { - title: this.hass!.localize("ui.panel.config.person.confirm_delete"), - text: this.hass!.localize("ui.panel.config.person.confirm_delete2"), + title: this.hass!.localize( + "ui.panel.config.person.confirm_delete_title", + { name: entry!.name } + ), + text: this.hass!.localize( + "ui.panel.config.person.confirm_delete_text" + ), dismissText: this.hass!.localize("ui.common.cancel"), confirmText: this.hass!.localize("ui.common.delete"), + destructive: true, })) ) { return false; diff --git a/src/panels/config/scene/ha-scene-dashboard.ts b/src/panels/config/scene/ha-scene-dashboard.ts index cd31e69268..afebd61588 100644 --- a/src/panels/config/scene/ha-scene-dashboard.ts +++ b/src/panels/config/scene/ha-scene-dashboard.ts @@ -268,21 +268,26 @@ class HaSceneDashboard extends LitElement { private _activateScene = async (scene: SceneEntity) => { await activateScene(this.hass, scene.entity_id); showToast(this, { - message: this.hass.localize( - "ui.panel.config.scene.activated", - "name", - computeStateName(scene) - ), + message: this.hass.localize("ui.panel.config.scene.activated", { + name: computeStateName(scene), + }), }); forwardHaptic("light"); }; private _deleteConfirm(scene: SceneEntity): void { showConfirmationDialog(this, { - text: this.hass!.localize("ui.panel.config.scene.picker.delete_confirm"), + title: this.hass!.localize( + "ui.panel.config.scene.picker.delete_confirm_title" + ), + text: this.hass!.localize( + "ui.panel.config.scene.picker.delete_confirm_text", + { name: computeStateName(scene) } + ), confirmText: this.hass!.localize("ui.common.delete"), dismissText: this.hass!.localize("ui.common.cancel"), confirm: () => this._delete(scene), + destructive: true, }); } diff --git a/src/panels/config/scene/ha-scene-editor.ts b/src/panels/config/scene/ha-scene-editor.ts index 0fb724c635..598f2e1d69 100644 --- a/src/panels/config/scene/ha-scene-editor.ts +++ b/src/panels/config/scene/ha-scene-editor.ts @@ -26,6 +26,7 @@ import { computeDomain } from "../../../common/entity/compute_domain"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { navigate } from "../../../common/navigate"; import { computeRTL } from "../../../common/util/compute_rtl"; +import { afterNextRender } from "../../../common/util/render-status"; import "../../../components/device/ha-device-picker"; import "../../../components/entity/ha-entities-picker"; import "../../../components/ha-area-picker"; @@ -763,32 +764,31 @@ export class HaSceneEditor extends SubscribeMixin( } } - private _backTapped = (): void => { - if (this._dirty) { - showConfirmationDialog(this, { - text: this.hass!.localize( - "ui.panel.config.scene.editor.unsaved_confirm" - ), - confirmText: this.hass!.localize("ui.common.leave"), - dismissText: this.hass!.localize("ui.common.stay"), - confirm: () => this._goBack(), - }); - } else { + private _backTapped = async (): Promise => { + const result = await this.confirmUnsavedChanged(); + if (result) { this._goBack(); } }; private _goBack(): void { applyScene(this.hass, this._storedStates); - history.back(); + afterNextRender(() => history.back()); } private _deleteTapped(): void { showConfirmationDialog(this, { - text: this.hass!.localize("ui.panel.config.scene.picker.delete_confirm"), + title: this.hass!.localize( + "ui.panel.config.scene.picker.delete_confirm_title" + ), + text: this.hass!.localize( + "ui.panel.config.scene.picker.delete_confirm_text", + { name: this._config?.name } + ), confirmText: this.hass!.localize("ui.common.delete"), dismissText: this.hass!.localize("ui.common.cancel"), confirm: () => this._delete(), + destructive: true, }); } @@ -798,32 +798,37 @@ export class HaSceneEditor extends SubscribeMixin( history.back(); } - private async _duplicate() { + private async confirmUnsavedChanged(): Promise { if (this._dirty) { - if ( - !(await showConfirmationDialog(this, { - text: this.hass!.localize( - "ui.panel.config.scene.editor.unsaved_confirm" - ), - confirmText: this.hass!.localize("ui.common.leave"), - dismissText: this.hass!.localize("ui.common.stay"), - })) - ) { - return; - } - // Wait for dialog to complete closing - await new Promise((resolve) => setTimeout(resolve, 0)); + return showConfirmationDialog(this, { + title: this.hass!.localize( + "ui.panel.config.scene.editor.unsaved_confirm_title" + ), + text: this.hass!.localize( + "ui.panel.config.scene.editor.unsaved_confirm_text" + ), + confirmText: this.hass!.localize("ui.common.leave"), + dismissText: this.hass!.localize("ui.common.stay"), + destructive: true, + }); + } + return true; + } + + private async _duplicate() { + const result = await this.confirmUnsavedChanged(); + if (result) { + showSceneEditor( + { + ...this._config, + id: undefined, + name: `${this._config?.name} (${this.hass.localize( + "ui.panel.config.scene.picker.duplicate" + )})`, + }, + this._sceneAreaIdCurrent || undefined + ); } - showSceneEditor( - { - ...this._config, - id: undefined, - name: `${this._config?.name} (${this.hass.localize( - "ui.panel.config.scene.picker.duplicate" - )})`, - }, - this._sceneAreaIdCurrent || undefined - ); } private _calculateMetaData(): SceneMetaData { diff --git a/src/panels/config/script/ha-config-script.ts b/src/panels/config/script/ha-config-script.ts index 4b8f9aefa8..6aabf90bf4 100644 --- a/src/panels/config/script/ha-config-script.ts +++ b/src/panels/config/script/ha-config-script.ts @@ -88,8 +88,8 @@ class HaConfigScript extends HassRouterPage { this._currentPage !== "dashboard" ) { pageEl.creatingNew = undefined; - const scriptEntityId = this.routeTail.path.substr(1); - pageEl.scriptEntityId = scriptEntityId === "new" ? null : scriptEntityId; + const scriptId = this.routeTail.path.substr(1); + pageEl.scriptId = scriptId === "new" ? null : scriptId; } } } diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index d39e81a473..34eb30b46d 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -24,11 +24,11 @@ import { property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; -import { computeObjectId } from "../../../common/entity/compute_object_id"; import { navigate } from "../../../common/navigate"; import { slugify } from "../../../common/string/slugify"; import { computeRTL } from "../../../common/util/compute_rtl"; import { copyToClipboard } from "../../../common/util/copy-clipboard"; +import { afterNextRender } from "../../../common/util/render-status"; import "../../../components/ha-button-menu"; import "../../../components/ha-card"; import "../../../components/ha-fab"; @@ -67,7 +67,7 @@ import type { HaManualScriptEditor } from "./manual-script-editor"; export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public scriptEntityId: string | null = null; + @property() public scriptId: string | null = null; @property({ attribute: false }) public route!: Route; @@ -161,7 +161,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { } const schema = this._schema( - !!this.scriptEntityId, + !!this.scriptId, "use_blueprint" in this._config, this._config.mode ); @@ -182,7 +182,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { .backCallback=${this._backTapped} .header=${!this._config?.alias ? "" : this._config.alias} > - ${this.scriptEntityId && !this.narrow + ${this.scriptId && !this.narrow ? html` ${this.hass.localize( @@ -200,7 +200,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { ${this.hass.localize("ui.panel.config.script.editor.show_info")} @@ -212,16 +212,16 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { ${this.hass.localize("ui.panel.config.script.picker.run_script")} - ${this.scriptEntityId && this.narrow + ${this.scriptId && this.narrow ? html` - + ${this.hass.localize( "ui.panel.config.script.editor.show_trace" @@ -294,7 +294,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
  • ${this.hass.localize("ui.panel.config.script.picker.delete")} @@ -331,7 +331,6 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { "yaml-mode": this._mode === "yaml", })}" > - ${this._errors ? html`
    ${this._errors}
    ` : ""} ${this._mode === "gui" ? html`
    + ${this._errors + ? html` + + ${this._errors} + + ` + : ""}
    ${this._errors} + ` + : ""} { // Normalize data: ensure sequence is a list // Happens when people copy paste their scripts into the config @@ -446,7 +457,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { : this.hass.localize( "ui.panel.config.script.editor.load_error_unknown", "err_no", - resp.status_code + resp.status_code || resp.code ) ); history.back(); @@ -454,11 +465,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { ); } - if ( - changedProps.has("scriptEntityId") && - !this.scriptEntityId && - this.hass - ) { + if (changedProps.has("scriptId") && !this.scriptId && this.hass) { const initData = getScriptEditorInitData(); this._dirty = !!initData; const baseConfig: Partial = { @@ -518,24 +525,30 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { }; private async _showInfo() { - if (!this.scriptEntityId) { + if (!this.scriptId) { return; } - fireEvent(this, "hass-more-info", { entityId: this.scriptEntityId }); + const entity = Object.values(this.hass.entities).find( + (entry) => entry.unique_id === this.scriptId + ); + if (!entity) { + return; + } + fireEvent(this, "hass-more-info", { entityId: entity.entity_id }); } private async _showTrace() { - if (this.scriptEntityId) { + if (this.scriptId) { const result = await this.confirmUnsavedChanged(); if (result) { - navigate(`/config/script/trace/${this.scriptEntityId}`); + navigate(`/config/script/trace/${this.scriptId}`); } } } private async _runScript(ev: CustomEvent) { ev.stopPropagation(); - await triggerScript(this.hass, this.scriptEntityId as string); + await triggerScript(this.hass, this.scriptId!); showToast(this, { message: this.hass.localize( "ui.notification_toast.triggered", @@ -545,28 +558,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { }); } - private _modeChanged(mode) { - const curMode = this._config!.mode || MODES[0]; - - if (mode === curMode) { - return; - } - - this._config = { ...this._config!, mode }; - if (!isMaxMode(mode)) { - delete this._config.max; - } - this._dirty = true; - } - - private _aliasChanged(alias: string) { - if ( - this.scriptEntityId || - (this._entityId && this._entityId !== slugify(this._config!.alias)) - ) { - return; - } - + private _computeEntityIdFromAlias(alias: string) { const aliasSlugify = slugify(alias); let id = aliasSlugify; let i = 2; @@ -574,11 +566,10 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { id = `${aliasSlugify}_${i}`; i++; } - - this._entityId = id; + return id; } - private _idChanged(id: string) { + private _setEntityId(id?: string) { this._entityId = id; if (this.hass.states[`script.${this._entityId}`]) { this._idError = true; @@ -587,47 +578,60 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { } } + private updateEntityId( + newId: string | undefined, + newAlias: string | undefined + ) { + const currentAlias = this._config?.alias ?? ""; + const currentEntityId = this._entityId ?? ""; + + if (newId !== this._entityId) { + this._setEntityId(newId || undefined); + return; + } + + const currentComputedEntity = this._computeEntityIdFromAlias(currentAlias); + + if (currentComputedEntity === currentEntityId || !this._entityId) { + const newComputedId = newAlias + ? this._computeEntityIdFromAlias(newAlias) + : undefined; + + this._setEntityId(newComputedId); + } + } + private _valueChanged(ev: CustomEvent) { ev.stopPropagation(); + this._errors = undefined; const values = ev.detail.value as any; - const currentId = this._entityId; + let changed = false; + const newValues: Omit = { + alias: values.alias ?? "", + icon: values.icon, + mode: values.mode, + max: isMaxMode(values.mode) ? values.max : undefined, + }; - for (const key of Object.keys(values)) { - if (key === "sequence") { + if (!this.scriptId) { + this.updateEntityId(values.id, values.alias); + } + + for (const key of Object.keys(newValues)) { + const value = newValues[key]; + + if (value === this._config![key]) { continue; } - - const value = values[key]; - - if ( - value === this._config![key] || - (key === "id" && currentId === value) - ) { - continue; - } - - changed = true; - - switch (key) { - case "id": - this._idChanged(value); - break; - case "alias": - this._aliasChanged(value); - break; - case "mode": - this._modeChanged(value); - break; - } - - if (values[key] === undefined) { + if (value === undefined) { const newConfig = { ...this._config! }; delete newConfig![key]; this._config = newConfig; } else { this._config = { ...this._config!, [key]: value }; } + changed = true; } if (changed) { @@ -637,6 +641,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { private _configChanged(ev) { this._config = ev.detail.value; + this._errors = undefined; this._dirty = true; } @@ -666,11 +671,15 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { private async confirmUnsavedChanged(): Promise { if (this._dirty) { return showConfirmationDialog(this, { + title: this.hass!.localize( + "ui.panel.config.automation.editor.unsaved_confirm_title" + ), text: this.hass!.localize( - "ui.panel.config.automation.editor.unsaved_confirm" + "ui.panel.config.automation.editor.unsaved_confirm_text" ), confirmText: this.hass!.localize("ui.common.leave"), dismissText: this.hass!.localize("ui.common.stay"), + destructive: true, }); } return true; @@ -679,7 +688,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { private _backTapped = async () => { const result = await this.confirmUnsavedChanged(); if (result) { - history.back(); + afterNextRender(() => history.back()); } }; @@ -697,18 +706,22 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { private async _deleteConfirm() { showConfirmationDialog(this, { - text: this.hass.localize("ui.panel.config.script.editor.delete_confirm"), + title: this.hass.localize( + "ui.panel.config.script.editor.delete_confirm_title" + ), + text: this.hass.localize( + "ui.panel.config.script.editor.delete_confirm_text", + { name: this._config?.alias } + ), confirmText: this.hass!.localize("ui.common.delete"), dismissText: this.hass!.localize("ui.common.cancel"), confirm: () => this._delete(), + destructive: true, }); } private async _delete() { - await deleteScript( - this.hass, - computeObjectId(this.scriptEntityId as string) - ); + await deleteScript(this.hass, this.scriptId!); history.back(); } @@ -726,7 +739,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { } } - private _saveScript(): void { + private async _saveScript(): Promise { if (this._idError) { showToast(this, { message: this.hass.localize( @@ -741,25 +754,27 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { }); return; } - const id = this.scriptEntityId - ? computeObjectId(this.scriptEntityId) - : this._entityId || Date.now(); - this.hass!.callApi("POST", "config/script/config/" + id, this._config).then( - () => { - this._dirty = false; - if (!this.scriptEntityId) { - navigate(`/config/script/edit/${id}`, { replace: true }); - } - }, - (errors) => { - this._errors = errors.body.message || errors.error || errors.body; - showToast(this, { - message: errors.body.message || errors.error || errors.body, - }); - throw errors; - } - ); + const id = this.scriptId || this._entityId || Date.now(); + try { + await this.hass!.callApi( + "POST", + "config/script/config/" + id, + this._config + ); + } catch (errors: any) { + this._errors = errors.body.message || errors.error || errors.body; + showToast(this, { + message: errors.body.message || errors.error || errors.body, + }); + throw errors; + } + + this._dirty = false; + + if (!this.scriptId) { + navigate(`/config/script/edit/${id}`, { replace: true }); + } } protected handleKeyboardSave() { @@ -794,6 +809,10 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { max-width: 1040px; padding: 28px 20px 0; } + .config-container ha-alert { + margin-bottom: 16px; + display: block; + } ha-yaml-editor { flex-grow: 1; --code-mirror-height: 100%; diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts index da705e7ef4..ef55423c10 100644 --- a/src/panels/config/script/ha-script-picker.ts +++ b/src/panels/config/script/ha-script-picker.ts @@ -13,7 +13,6 @@ import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { formatDateTime } from "../../../common/datetime/format_date_time"; import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event"; -import { computeObjectId } from "../../../common/entity/compute_object_id"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { navigate } from "../../../common/navigate"; import { computeRTL } from "../../../common/util/compute_rtl"; @@ -252,11 +251,15 @@ class HaScriptPicker extends LitElement { } private _handleRowClicked(ev: HASSDomEvent) { - navigate(`/config/script/edit/${ev.detail.id}`); + const entry = this.hass.entities[ev.detail.id]; + if (entry) { + navigate(`/config/script/edit/${entry.unique_id}`); + } } private _runScript = async (script: any) => { - await triggerScript(this.hass, script.entity_id); + const entry = this.hass.entities[script.entity_id]; + await triggerScript(this.hass, entry.unique_id); showToast(this, { message: this.hass.localize( "ui.notification_toast.triggered", @@ -271,7 +274,10 @@ class HaScriptPicker extends LitElement { } private _showTrace(script: any) { - navigate(`/config/script/trace/${script.entity_id}`); + const entry = this.hass.entities[script.entity_id]; + if (entry) { + navigate(`/config/script/trace/${entry.unique_id}`); + } } private _showHelp() { @@ -294,10 +300,8 @@ class HaScriptPicker extends LitElement { private async _duplicate(script: any) { try { - const config = await getScriptConfig( - this.hass, - computeObjectId(script.entity_id) - ); + const entry = this.hass.entities[script.entity_id]; + const config = await getScriptConfig(this.hass, entry.unique_id); showScriptEditor({ ...config, alias: `${config?.alias} (${this.hass.localize( @@ -322,16 +326,24 @@ class HaScriptPicker extends LitElement { private async _deleteConfirm(script: any) { showConfirmationDialog(this, { - text: this.hass.localize("ui.panel.config.script.editor.delete_confirm"), + title: this.hass.localize( + "ui.panel.config.script.editor.delete_confirm_title" + ), + text: this.hass.localize( + "ui.panel.config.script.editor.delete_confirm_text", + { name: script.name } + ), confirmText: this.hass!.localize("ui.common.delete"), dismissText: this.hass!.localize("ui.common.cancel"), confirm: () => this._delete(script), + destructive: true, }); } private async _delete(script: any) { try { - await deleteScript(this.hass, computeObjectId(script.entity_id)); + const entry = this.hass.entities[script.entity_id]; + await deleteScript(this.hass, entry.unique_id); } catch (err: any) { await showAlertDialog(this, { text: diff --git a/src/panels/config/script/ha-script-trace.ts b/src/panels/config/script/ha-script-trace.ts index ab35977cb1..ae001665ca 100644 --- a/src/panels/config/script/ha-script-trace.ts +++ b/src/panels/config/script/ha-script-trace.ts @@ -44,7 +44,7 @@ import { fireEvent } from "../../../common/dom/fire_event"; export class HaScriptTrace extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public scriptEntityId!: string; + @property() public scriptId!: string; @property({ attribute: false }) public scripts!: ScriptEntity[]; @@ -54,6 +54,8 @@ export class HaScriptTrace extends LitElement { @property({ attribute: false }) public route!: Route; + @state() private _entityId?: string; + @state() private _traces?: ScriptTrace[]; @state() private _runId?: string; @@ -74,15 +76,15 @@ export class HaScriptTrace extends LitElement { @query("hat-script-graph") private _graph?: HatScriptGraph; protected render(): TemplateResult { - const stateObj = this.scriptEntityId - ? this.hass.states[this.scriptEntityId] + const stateObj = this._entityId + ? this.hass.states[this._entityId] : undefined; const graph = this._graph; const trackedNodes = graph?.trackedNodes; const renderedNodes = graph?.renderedNodes; - const title = stateObj?.attributes.friendly_name || this.scriptEntityId; + const title = stateObj?.attributes.friendly_name || this._entityId; let devButtons: TemplateResult | string = ""; if (__DEV__) { @@ -95,11 +97,11 @@ export class HaScriptTrace extends LitElement { return html` ${devButtons} - ${!this.narrow && this.scriptEntityId + ${!this.narrow && this.scriptId ? html` @@ -120,7 +122,7 @@ export class HaScriptTrace extends LitElement { ${this.hass.localize("ui.panel.config.script.editor.show_info")} @@ -130,11 +132,11 @@ export class HaScriptTrace extends LitElement { > - ${this.narrow && this.scriptEntityId + ${this.narrow && this.scriptId ? html` ${this.hass.localize( @@ -309,25 +311,33 @@ export class HaScriptTrace extends LitElement { protected firstUpdated(changedProps) { super.firstUpdated(changedProps); - if (!this.scriptEntityId) { + if (!this.scriptId) { return; } const params = new URLSearchParams(location.search); this._loadTraces(params.get("run_id") || undefined); + + this._entityId = Object.values(this.hass.entities).find( + (entry) => entry.unique_id === this.scriptId + )?.entity_id; } public willUpdate(changedProps) { super.willUpdate(changedProps); - // Only reset if scriptEntityId has changed and we had one before. - if (changedProps.get("scriptEntityId")) { + // Only reset if scriptId has changed and we had one before. + if (changedProps.get("scriptId")) { this._traces = undefined; this._runId = undefined; this._trace = undefined; this._logbookEntries = undefined; - if (this.scriptEntityId) { + if (this.scriptId) { this._loadTraces(); + + this._entityId = Object.values(this.hass.entities).find( + (entry) => entry.unique_id === this.scriptId + )?.entity_id; } } @@ -364,11 +374,7 @@ export class HaScriptTrace extends LitElement { } private async _loadTraces(runId?: string) { - this._traces = await loadTraces( - this.hass, - "script", - this.scriptEntityId.split(".")[1] - ); + this._traces = await loadTraces(this.hass, "script", this.scriptId); // Newest will be on top. this._traces.reverse(); @@ -410,7 +416,7 @@ export class HaScriptTrace extends LitElement { const trace = await loadTrace( this.hass, "script", - this.scriptEntityId.split(".")[1], + this.scriptId, this._runId! ); this._logbookEntries = isComponentLoaded(this.hass, "logbook") @@ -426,7 +432,7 @@ export class HaScriptTrace extends LitElement { private _downloadTrace() { const aEl = document.createElement("a"); - aEl.download = `trace ${this.scriptEntityId} ${ + aEl.download = `trace ${this._entityId} ${ this._trace!.timestamp.start }.json`; aEl.href = `data:application/json;charset=utf-8,${encodeURI( @@ -476,10 +482,10 @@ export class HaScriptTrace extends LitElement { } private async _showInfo() { - if (!this.scriptEntityId) { + if (!this._entityId) { return; } - fireEvent(this, "hass-more-info", { entityId: this.scriptEntityId }); + fireEvent(this, "hass-more-info", { entityId: this._entityId }); } static get styles(): CSSResultGroup { diff --git a/src/panels/config/storage/dialog-move-datadisk.ts b/src/panels/config/storage/dialog-move-datadisk.ts index a005f29ebd..40a55395f0 100644 --- a/src/panels/config/storage/dialog-move-datadisk.ts +++ b/src/panels/config/storage/dialog-move-datadisk.ts @@ -52,7 +52,23 @@ class MoveDatadiskDialog extends LitElement { try { this._osInfo = await fetchHassioHassOsInfo(this.hass); + + const data = await listDatadisks(this.hass); + if (data.devices.length > 0) { + this._devices = data.devices; + } else { + this.closeDialog(); + await showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.storage.datadisk.no_devices_title" + ), + text: this.hass.localize( + "ui.panel.config.storage.datadisk.no_devices_text" + ), + }); + } } catch (err: any) { + this.closeDialog(); await showAlertDialog(this, { title: this.hass.localize( "ui.panel.config.hardware.available_hardware.failed_to_get" @@ -60,10 +76,6 @@ class MoveDatadiskDialog extends LitElement { text: extractApiErrorMessage(err), }); } - - listDatadisks(this.hass).then((data) => { - this._devices = data.devices; - }); } public closeDialog(): void { @@ -76,9 +88,10 @@ class MoveDatadiskDialog extends LitElement { } protected render(): TemplateResult { - if (!this._hostInfo || !this._osInfo) { + if (!this._hostInfo || !this._osInfo || !this._devices) { return html``; } + return html` ${this._moving - ? html` + ? html` +

    ${this.hass.localize( "ui.panel.config.storage.datadisk.moving_desc" )} -

    ` - : html`${this._devices?.length - ? html` - ${this.hass.localize( - "ui.panel.config.storage.datadisk.description", - { - current_path: this._osInfo.data_disk, - time: calculateMoveTime(this._hostInfo), - } - )} -

    +

    + ` + : html` + ${this.hass.localize( + "ui.panel.config.storage.datadisk.description", + { + current_path: this._osInfo.data_disk, + time: calculateMoveTime(this._hostInfo), + } + )} +

    - - ${this._devices.map( - (device) => - html`${device}` - )} - - ` - : this._devices === undefined - ? this.hass.localize( - "ui.panel.config.storage.datadisk.loading_devices" - ) - : this.hass.localize( - "ui.panel.config.storage.datadisk.no_devices" - )} + + ${this._devices.map( + (device) => + html` + ${device} + ` + )} + Test + Test + Test + Test + ${this.hass.localize("ui.panel.config.storage.datadisk.move")} - `} + + `}
    `; } @@ -168,8 +180,9 @@ class MoveDatadiskDialog extends LitElement { ), text: extractApiErrorMessage(err), }); - this.closeDialog(); } + } finally { + this.closeDialog(); } } diff --git a/src/panels/config/users/dialog-admin-change-password.ts b/src/panels/config/users/dialog-admin-change-password.ts new file mode 100644 index 0000000000..227d4fb057 --- /dev/null +++ b/src/panels/config/users/dialog-admin-change-password.ts @@ -0,0 +1,183 @@ +import "@material/mwc-button"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; + +import { fireEvent } from "../../../common/dom/fire_event"; +import { createCloseHeading } from "../../../components/ha-dialog"; +import "../../../components/ha-form/ha-form"; +import { SchemaUnion } from "../../../components/ha-form/types"; +import "../../../components/ha-textfield"; +import { adminChangePassword } from "../../../data/auth"; +import { haStyleDialog } from "../../../resources/styles"; +import { HomeAssistant } from "../../../types"; +import { showToast } from "../../../util/toast"; +import { AdminChangePasswordDialogParams } from "./show-dialog-admin-change-password"; + +const SCHEMA = [ + { + name: "new_password", + required: true, + selector: { + text: { + type: "password", + }, + }, + }, + { + name: "password_confirm", + required: true, + selector: { + text: { + type: "password", + }, + }, + }, +] as const; + +type FormData = { new_password?: string; password_confirm?: string }; + +@customElement("dialog-admin-change-password") +class DialogAdminChangePassword extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _params?: AdminChangePasswordDialogParams; + + @state() private _userId?: string; + + @state() private _data?: FormData; + + @state() private _error?: Record; + + @state() private _submitting = false; + + @state() private _success = false; + + public showDialog(params: AdminChangePasswordDialogParams): void { + this._params = params; + this._userId = params.userId; + } + + public closeDialog(): void { + this._params = undefined; + this._data = undefined; + this._submitting = false; + this._success = false; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + private _computeLabel = (schema: SchemaUnion) => + this.hass.localize(`ui.panel.config.users.change_password.${schema.name}`); + + private _computeError = (error: string) => + this.hass.localize( + `ui.panel.config.users.change_password.${error}` as any + ) || error; + + private _validate() { + if ( + this._data && + this._data.new_password && + this._data.password_confirm && + this._data.new_password !== this._data.password_confirm + ) { + this._error = { + password_confirm: "password_no_match", + }; + } else { + this._error = undefined; + } + } + + protected render(): TemplateResult { + if (!this._params) { + return html``; + } + + const canSubmit = Boolean( + this._data?.new_password && this._data?.password_confirm && !this._error + ); + + return html` + + ${this._success + ? html` +

    + ${this.hass.localize( + "ui.panel.config.users.change_password.password_changed" + )} +

    + + ${this.hass.localize("ui.dialogs.generic.ok")} + + ` + : html` + + + ${this.hass.localize("ui.common.cancel")} + + + ${this.hass.localize( + "ui.panel.config.users.change_password.change" + )} + + `} +
    + `; + } + + private _valueChanged(ev) { + this._data = ev.detail.value; + this._validate(); + } + + private async _changePassword(): Promise { + if (!this._userId || !this._data?.new_password) return; + try { + this._submitting = true; + await adminChangePassword( + this.hass, + this._userId!, + this._data.new_password + ); + this._success = true; + } catch (err: any) { + showToast(this, { + message: err.body?.message || err.message || err, + }); + } finally { + this._submitting = false; + } + } + + static get styles(): CSSResultGroup { + return [haStyleDialog, css``]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-admin-change-password": DialogAdminChangePassword; + } +} diff --git a/src/panels/config/users/dialog-user-detail.ts b/src/panels/config/users/dialog-user-detail.ts index 490839b3cd..da3ced4d61 100644 --- a/src/panels/config/users/dialog-user-detail.ts +++ b/src/panels/config/users/dialog-user-detail.ts @@ -4,26 +4,23 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { computeRTLDirection } from "../../../common/util/compute_rtl"; +import "../../../components/ha-chip"; +import "../../../components/ha-chip-set"; import { createCloseHeading } from "../../../components/ha-dialog"; import "../../../components/ha-formfield"; import "../../../components/ha-help-tooltip"; -import "../../../components/ha-chip-set"; -import "../../../components/ha-chip"; import "../../../components/ha-svg-icon"; -import "../../../components/ha-textfield"; import "../../../components/ha-switch"; -import { adminChangePassword } from "../../../data/auth"; +import "../../../components/ha-textfield"; import { computeUserBadges, SYSTEM_GROUP_ID_ADMIN, SYSTEM_GROUP_ID_USER, } from "../../../data/user"; -import { - showAlertDialog, - showPromptDialog, -} from "../../../dialogs/generic/show-dialog-box"; +import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; +import { showAdminChangePasswordDialog } from "./show-dialog-admin-change-password"; import { UserDetailDialogParams } from "./show-dialog-user-detail"; @customElement("dialog-user-detail") @@ -268,40 +265,8 @@ class DialogUserDetail extends LitElement { }); return; } - const newPassword = await showPromptDialog(this, { - title: this.hass.localize("ui.panel.config.users.editor.change_password"), - inputType: "password", - inputLabel: this.hass.localize( - "ui.panel.config.users.editor.new_password" - ), - }); - if (!newPassword) { - return; - } - const confirmPassword = await showPromptDialog(this, { - title: this.hass.localize("ui.panel.config.users.editor.change_password"), - inputType: "password", - inputLabel: this.hass.localize( - "ui.panel.config.users.add_user.password_confirm" - ), - }); - if (!confirmPassword) { - return; - } - if (newPassword !== confirmPassword) { - showAlertDialog(this, { - title: this.hass.localize( - "ui.panel.config.users.add_user.password_not_match" - ), - }); - return; - } - await adminChangePassword(this.hass, this._params!.entry.id, newPassword); - showAlertDialog(this, { - title: this.hass.localize( - "ui.panel.config.users.editor.password_changed" - ), - }); + + showAdminChangePasswordDialog(this, { userId: this._params!.entry.id }); } private _close(): void { diff --git a/src/panels/config/users/ha-config-users.ts b/src/panels/config/users/ha-config-users.ts index 9001fa82f1..b192716e43 100644 --- a/src/panels/config/users/ha-config-users.ts +++ b/src/panels/config/users/ha-config-users.ts @@ -207,12 +207,16 @@ export class HaConfigUsers extends LitElement { if ( !(await showConfirmationDialog(this, { title: this.hass!.localize( - "ui.panel.config.users.editor.confirm_user_deletion", + "ui.panel.config.users.editor.confirm_user_deletion_title", "name", entry.name ), + text: this.hass!.localize( + "ui.panel.config.users.editor.confirm_user_deletion_text" + ), dismissText: this.hass!.localize("ui.common.cancel"), confirmText: this.hass!.localize("ui.common.delete"), + destructive: true, })) ) { return false; diff --git a/src/panels/config/users/show-dialog-admin-change-password.ts b/src/panels/config/users/show-dialog-admin-change-password.ts new file mode 100644 index 0000000000..df6772a5c1 --- /dev/null +++ b/src/panels/config/users/show-dialog-admin-change-password.ts @@ -0,0 +1,19 @@ +import { fireEvent } from "../../../common/dom/fire_event"; + +export interface AdminChangePasswordDialogParams { + userId: string; +} + +export const loadAdminChangePasswordDialog = () => + import("./dialog-admin-change-password"); + +export const showAdminChangePasswordDialog = ( + element: HTMLElement, + dialogParams: AdminChangePasswordDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-admin-change-password", + dialogImport: loadAdminChangePasswordDialog, + dialogParams, + }); +}; diff --git a/src/panels/custom/ha-panel-custom.ts b/src/panels/custom/ha-panel-custom.ts index 907bc9c655..fd759575c4 100644 --- a/src/panels/custom/ha-panel-custom.ts +++ b/src/panels/custom/ha-panel-custom.ts @@ -1,6 +1,7 @@ import { PropertyValues, ReactiveElement } from "lit"; import { property } from "lit/decorators"; import { navigate, NavigateOptions } from "../../common/navigate"; +import { deepEqual } from "../../common/util/deep-equal"; import { CustomPanelInfo } from "../../data/panel_custom"; import { HomeAssistant, Route } from "../../types"; import { createCustomPanelElement } from "../../util/custom-panel/create-custom-panel-element"; @@ -54,12 +55,15 @@ export class HaPanelCustom extends ReactiveElement { protected update(changedProps: PropertyValues) { super.update(changedProps); if (changedProps.has("panel")) { - // Clean up old things if we had a panel - if (changedProps.get("panel")) { - this._cleanupPanel(); + // Clean up old things if we had a panel and the new one is different. + const oldPanel = changedProps.get("panel") as CustomPanelInfo | undefined; + if (!deepEqual(oldPanel, this.panel)) { + if (oldPanel) { + this._cleanupPanel(); + } + this._createPanel(this.panel); + return; } - this._createPanel(this.panel); - return; } if (!this._setProperties) { return; diff --git a/src/panels/developer-tools/statistics/developer-tools-statistics.ts b/src/panels/developer-tools/statistics/developer-tools-statistics.ts index cf0847bb05..edf957b025 100644 --- a/src/panels/developer-tools/statistics/developer-tools-statistics.ts +++ b/src/panels/developer-tools/statistics/developer-tools-statistics.ts @@ -15,7 +15,7 @@ import { StatisticsMetaData, StatisticsValidationResult, validateStatistics, -} from "../../../data/history"; +} from "../../../data/recorder"; import { showAlertDialog, showConfirmationDialog, @@ -72,8 +72,14 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) { hidden: this.narrow, width: "20%", }, - unit_of_measurement: { - title: "Unit", + display_unit_of_measurement: { + title: "Display unit", + sortable: true, + filterable: true, + width: "10%", + }, + statistics_unit_of_measurement: { + title: "Statistics unit", sortable: true, filterable: true, width: "10%", diff --git a/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts b/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts index ded0fc2c82..8598bf54f1 100644 --- a/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts +++ b/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts @@ -24,7 +24,7 @@ import { adjustStatisticsSum, fetchStatistics, StatisticValue, -} from "../../../data/history"; +} from "../../../data/recorder"; import type { DateTimeSelector, NumberSelector } from "../../../data/selector"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { haStyle, haStyleDialog } from "../../../resources/styles"; @@ -305,7 +305,8 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement { this.hass, this._params!.statistic.statistic_id, this._chosenStat!.start, - this._amount! - this._origAmount! + this._amount! - this._origAmount!, + this._params!.statistic.display_unit_of_measurement ); } catch (err: any) { this._busy = false; diff --git a/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts b/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts index 3168bffc9f..5d11ba98ed 100644 --- a/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts +++ b/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts @@ -8,7 +8,7 @@ import { HomeAssistant } from "../../../types"; import { clearStatistics, updateStatisticsMetadata, -} from "../../../data/history"; +} from "../../../data/recorder"; import "../../../components/ha-formfield"; import "../../../components/ha-radio"; import type { DialogStatisticsUnitsChangedParams } from "./show-dialog-statistics-fix-units-changed"; diff --git a/src/panels/developer-tools/statistics/dialog-statistics-fix-unsupported-unit-meta.ts b/src/panels/developer-tools/statistics/dialog-statistics-fix-unsupported-unit-meta.ts index 4bfaebe489..0058fa49d2 100644 --- a/src/panels/developer-tools/statistics/dialog-statistics-fix-unsupported-unit-meta.ts +++ b/src/panels/developer-tools/statistics/dialog-statistics-fix-unsupported-unit-meta.ts @@ -5,7 +5,7 @@ import "../../../components/ha-dialog"; import { fireEvent } from "../../../common/dom/fire_event"; import { haStyle, haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; -import { updateStatisticsMetadata } from "../../../data/history"; +import { updateStatisticsMetadata } from "../../../data/recorder"; import "../../../components/ha-formfield"; import "../../../components/ha-radio"; import type { DialogStatisticsUnsupportedUnitMetaParams } from "./show-dialog-statistics-fix-unsupported-unit-meta"; diff --git a/src/panels/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts b/src/panels/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts index 1db2c76307..6248a6c42f 100644 --- a/src/panels/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts +++ b/src/panels/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts @@ -1,5 +1,5 @@ import { fireEvent } from "../../../common/dom/fire_event"; -import { StatisticsMetaData } from "../../../data/history"; +import { StatisticsMetaData } from "../../../data/recorder"; export const loadAdjustSumDialog = () => import("./dialog-statistics-adjust-sum"); diff --git a/src/panels/developer-tools/statistics/show-dialog-statistics-fix-units-changed.ts b/src/panels/developer-tools/statistics/show-dialog-statistics-fix-units-changed.ts index c341bfdf21..1b6d7ee116 100644 --- a/src/panels/developer-tools/statistics/show-dialog-statistics-fix-units-changed.ts +++ b/src/panels/developer-tools/statistics/show-dialog-statistics-fix-units-changed.ts @@ -1,5 +1,5 @@ import { fireEvent } from "../../../common/dom/fire_event"; -import { StatisticsValidationResultUnitsChanged } from "../../../data/history"; +import { StatisticsValidationResultUnitsChanged } from "../../../data/recorder"; export const loadFixUnitsDialog = () => import("./dialog-statistics-fix-units-changed"); diff --git a/src/panels/developer-tools/statistics/show-dialog-statistics-fix-unsupported-unit-meta.ts b/src/panels/developer-tools/statistics/show-dialog-statistics-fix-unsupported-unit-meta.ts index 828f3094bc..8ea4c5d915 100644 --- a/src/panels/developer-tools/statistics/show-dialog-statistics-fix-unsupported-unit-meta.ts +++ b/src/panels/developer-tools/statistics/show-dialog-statistics-fix-unsupported-unit-meta.ts @@ -1,5 +1,5 @@ import { fireEvent } from "../../../common/dom/fire_event"; -import { StatisticsValidationResultUnsupportedUnitMetadata } from "../../../data/history"; +import { StatisticsValidationResultUnsupportedUnitMetadata } from "../../../data/recorder"; export const loadFixUnsupportedUnitMetaDialog = () => import("./dialog-statistics-fix-unsupported-unit-meta"); diff --git a/src/panels/logbook/ha-logbook-renderer.ts b/src/panels/logbook/ha-logbook-renderer.ts index c79bdc9e34..74321e70bc 100644 --- a/src/panels/logbook/ha-logbook-renderer.ts +++ b/src/panels/logbook/ha-logbook-renderer.ts @@ -170,11 +170,7 @@ class HaLogbookRenderer extends LitElement {
    diff --git a/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts index ea2c81d949..20dce6b0f6 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts @@ -13,7 +13,7 @@ import { energySourcesByType, getEnergyDataCollection, } from "../../../../data/energy"; -import { calculateStatisticsSumGrowth } from "../../../../data/history"; +import { calculateStatisticsSumGrowth } from "../../../../data/recorder"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../../../types"; import { createEntityNotFoundWarning } from "../../components/hui-warning"; diff --git a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts index a1419466c5..a83e9ca54c 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts @@ -27,7 +27,8 @@ import { fetchStatistics, getStatisticLabel, Statistics, -} from "../../../../data/history"; + StatisticsUnitConfiguration, +} from "../../../../data/recorder"; import { FrontendLocaleData } from "../../../../data/translation"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../../../types"; @@ -181,12 +182,19 @@ export class HuiEnergyDevicesGraphCard const startMinHour = addHours(energyData.start, -1); + const lengthUnit = this.hass.config.unit_system.length || ""; + const units: StatisticsUnitConfiguration = { + energy: "kWh", + volume: lengthUnit === "km" ? "m³" : "ft³", + }; + const data = await fetchStatistics( this.hass, startMinHour, energyData.end, devices, - period + period, + units ); Object.values(data).forEach((stat) => { @@ -211,7 +219,8 @@ export class HuiEnergyDevicesGraphCard startCompareMinHour, energyData.endCompare, devices, - period + period, + units ); Object.values(compareData).forEach((stat) => { diff --git a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts index d42863015f..d204562559 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts @@ -24,7 +24,7 @@ import { getEnergyDataCollection, getEnergyGasUnit, } from "../../../../data/energy"; -import { calculateStatisticsSumGrowth } from "../../../../data/history"; +import { calculateStatisticsSumGrowth } from "../../../../data/recorder"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../../../types"; import { LovelaceCard } from "../../types"; diff --git a/src/panels/lovelace/cards/energy/hui-energy-gas-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-gas-graph-card.ts index 6ec689b4f8..fc940d5605 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-gas-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-gas-graph-card.ts @@ -42,7 +42,7 @@ import { Statistics, StatisticsMetaData, getStatisticLabel, -} from "../../../../data/history"; +} from "../../../../data/recorder"; import { FrontendLocaleData } from "../../../../data/translation"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../../../types"; diff --git a/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts index 9e6ad9edec..1b3897b7d1 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts @@ -13,7 +13,7 @@ import { getEnergyDataCollection, GridSourceTypeEnergyPreference, } from "../../../../data/energy"; -import { calculateStatisticsSumGrowth } from "../../../../data/history"; +import { calculateStatisticsSumGrowth } from "../../../../data/recorder"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../../../types"; import type { LovelaceCard } from "../../types"; diff --git a/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts index c7c8f3c829..5e5df30f85 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts @@ -12,7 +12,7 @@ import { energySourcesByType, getEnergyDataCollection, } from "../../../../data/energy"; -import { calculateStatisticsSumGrowth } from "../../../../data/history"; +import { calculateStatisticsSumGrowth } from "../../../../data/recorder"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../../../types"; import type { LovelaceCard } from "../../types"; diff --git a/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts index 5b29e94c53..ac985edef9 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts @@ -43,7 +43,7 @@ import { Statistics, StatisticsMetaData, getStatisticLabel, -} from "../../../../data/history"; +} from "../../../../data/recorder"; import { FrontendLocaleData } from "../../../../data/translation"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../../../types"; diff --git a/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts b/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts index ec1c20ee45..dc5eb4b977 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts @@ -19,7 +19,6 @@ import { } from "../../../../common/color/convert-color"; import { labBrighten, labDarken } from "../../../../common/color/lab"; import { formatNumber } from "../../../../common/number/format_number"; -import "../../../../components/chart/statistics-chart"; import "../../../../components/ha-card"; import { EnergyData, @@ -30,7 +29,7 @@ import { import { calculateStatisticSumGrowth, getStatisticLabel, -} from "../../../../data/history"; +} from "../../../../data/recorder"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../../../types"; import { LovelaceCard } from "../../types"; diff --git a/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts index 1e315bcc37..ebae24dfca 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts @@ -37,7 +37,7 @@ import { Statistics, StatisticsMetaData, getStatisticLabel, -} from "../../../../data/history"; +} from "../../../../data/recorder"; import { FrontendLocaleData } from "../../../../data/translation"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../../../types"; diff --git a/src/panels/lovelace/cards/hui-light-card.ts b/src/panels/lovelace/cards/hui-light-card.ts index 595c299785..6717feef65 100644 --- a/src/panels/lovelace/cards/hui-light-card.ts +++ b/src/panels/lovelace/cards/hui-light-card.ts @@ -19,7 +19,7 @@ import "../../../components/ha-card"; import "../../../components/ha-icon-button"; import "../../../components/ha-state-icon"; import { UNAVAILABLE, UNAVAILABLE_STATES } from "../../../data/entity"; -import { LightEntity, lightSupportsDimming } from "../../../data/light"; +import { LightEntity, lightSupportsBrightness } from "../../../data/light"; import { ActionHandlerEvent } from "../../../data/lovelace"; import { HomeAssistant } from "../../../types"; import { actionHandler } from "../common/directives/action-handler-directive"; @@ -93,8 +93,9 @@ export class HuiLightCard extends LitElement implements LovelaceCard { `; } - const brightness = - Math.round((stateObj.attributes.brightness / 255) * 100) || 0; + const brightness = Math.round( + ((stateObj.attributes.brightness || 0) / 255) * 100 + ); const name = this._config.name ?? computeStateName(stateObj); @@ -121,14 +122,14 @@ export class HuiLightCard extends LitElement implements LovelaceCard { @value-changing=${this._dragEvent} @value-changed=${this._setBrightness} style=${styleMap({ - visibility: lightSupportsDimming(stateObj) + visibility: lightSupportsBrightness(stateObj) ? "visible" : "hidden", })} > = {}; - private _fetching = false; - private _interval?: number; public disconnectedCallback() { @@ -60,7 +58,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { clearInterval(this._interval); this._interval = window.setInterval( () => this._getStatistics(), - 1000 * 60 * 60 + this._intervalTimeout ); } @@ -92,7 +90,10 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { if (typeof config.stat_types === "string") { this._config = { ...config, stat_types: [config.stat_types] }; } else if (!config.stat_types) { - this._config = { ...config, stat_types: ["sum", "min", "max", "mean"] }; + this._config = { + ...config, + stat_types: ["state", "sum", "min", "max", "mean"], + }; } else { this._config = config; } @@ -125,7 +126,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { clearInterval(this._interval); this._interval = window.setInterval( () => this._getStatistics(), - 1000 * 60 * 60 + this._intervalTimeout ); } } @@ -155,16 +156,16 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { `; } + private get _intervalTimeout(): number { + return (this._config?.period === "5minute" ? 5 : 60) * 1000 * 60; + } + private async _getStatistics(): Promise { - if (this._fetching) { - return; - } const startDate = new Date(); startDate.setTime( startDate.getTime() - 1000 * 60 * 60 * (24 * (this._config!.days_to_show || 30) + 1) ); - this._fetching = true; try { this._statistics = await fetchStatistics( this.hass!, @@ -173,8 +174,8 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { this._entities, this._config!.period ); - } finally { - this._fetching = false; + } catch (err) { + this._statistics = undefined; } } diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index ffd0cc3555..7113ad1d13 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -1,4 +1,4 @@ -import { StatisticType } from "../../../data/history"; +import { StatisticType } from "../../../data/recorder"; import { ActionConfig, LovelaceCardConfig } from "../../../data/lovelace"; import { FullCalendarView, TranslationDict } from "../../../types"; import { Condition } from "../common/validate-condition"; diff --git a/src/panels/lovelace/components/hui-action-editor.ts b/src/panels/lovelace/components/hui-action-editor.ts index 0874c7e3b8..86d98726e4 100644 --- a/src/panels/lovelace/components/hui-action-editor.ts +++ b/src/panels/lovelace/components/hui-action-editor.ts @@ -14,6 +14,7 @@ import { import { ServiceAction } from "../../../data/script"; import { HomeAssistant } from "../../../types"; import { EditorTarget } from "../editor/types"; +import "../../../components/ha-navigation-picker"; @customElement("hui-action-editor") export class HuiActionEditor extends LitElement { @@ -89,14 +90,14 @@ export class HuiActionEditor extends LitElement {
    ${this.config?.action === "navigate" ? html` - + @value-changed=${this._navigateValueChanged} + > ` : ""} ${this.config?.action === "url" @@ -193,6 +194,16 @@ export class HuiActionEditor extends LitElement { fireEvent(this, "value-changed", { value }); } + private _navigateValueChanged(ev: CustomEvent) { + ev.stopPropagation(); + const value = { + ...this.config!, + navigation_path: ev.detail.value, + }; + + fireEvent(this, "value-changed", { value }); + } + static get styles(): CSSResultGroup { return css` .dropdown { diff --git a/src/panels/lovelace/components/hui-entity-editor.ts b/src/panels/lovelace/components/hui-entity-editor.ts index e5219ca990..dbfa9b1eaf 100644 --- a/src/panels/lovelace/components/hui-entity-editor.ts +++ b/src/panels/lovelace/components/hui-entity-editor.ts @@ -1,14 +1,7 @@ import { mdiDrag } from "@mdi/js"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { guard } from "lit/directives/guard"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; +import { repeat } from "lit/directives/repeat"; import type { SortableEvent } from "sortablejs"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/entity/ha-entity-picker"; @@ -30,20 +23,20 @@ export class HuiEntityEditor extends LitElement { @property() protected label?: string; - @state() private _attached = false; - - @state() private _renderEmptySortable = false; + private _entityKeys = new WeakMap(); private _sortable?: SortableInstance; - public connectedCallback() { - super.connectedCallback(); - this._attached = true; + public disconnectedCallback() { + this._destroySortable(); } - public disconnectedCallback() { - super.disconnectedCallback(); - this._attached = false; + private _getKey(action: EntityConfig) { + if (!this._entityKeys.has(action)) { + this._entityKeys.set(action, Math.random().toString()); + } + + return this._entityKeys.get(action)!; } protected render(): TemplateResult { @@ -60,23 +53,23 @@ export class HuiEntityEditor extends LitElement { ")"}
    - ${guard([this.entities, this._renderEmptySortable], () => - this._renderEmptySortable - ? "" - : this.entities!.map( - (entityConf, index) => html` -
    - - -
    - ` - ) + ${repeat( + this.entities, + (entityConf) => this._getKey(entityConf), + (entityConf, index) => html` +
    +
    + +
    + +
    + ` )}
    this._entityMoved(evt), + onChoose: (evt: SortableEvent) => { + (evt.item as any).placeholder = + document.createComment("sort-placeholder"); + evt.item.after((evt.item as any).placeholder); + }, + onEnd: (evt: SortableEvent) => { + // put back in original location + if ((evt.item as any).placeholder) { + (evt.item as any).placeholder.replaceWith(evt.item); + delete (evt.item as any).placeholder; + } + this._entityMoved(evt); + }, } ); } + private _destroySortable() { + this._sortable?.destroy(); + this._sortable = undefined; + } + private async _addEntity(ev: CustomEvent): Promise { const value = ev.detail.value; if (value === "") { @@ -198,9 +174,15 @@ export class HuiEntityEditor extends LitElement { display: flex; align-items: center; } - .entity ha-svg-icon { + .entity .handle { padding-right: 8px; cursor: move; + padding-inline-end: 8px; + padding-inline-start: initial; + direction: var(--direction); + } + .entity .handle > * { + pointer-events: none; } .entity ha-entity-picker { flex-grow: 1; diff --git a/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts index c1731bf95f..16c00d9224 100644 --- a/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts @@ -26,7 +26,11 @@ const SCHEMA = [ name: "", type: "grid", schema: [ - { name: "navigation_path", required: false, selector: { text: {} } }, + { + name: "navigation_path", + required: false, + selector: { navigation: {} }, + }, { name: "theme", required: false, selector: { theme: {} } }, ], }, diff --git a/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts index 879a6c8a41..606b1d2166 100644 --- a/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts @@ -1,5 +1,12 @@ import "../../../../components/ha-form/ha-form"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { @@ -23,8 +30,17 @@ import { processConfigEntities } from "../../common/process-config-entities"; import type { LovelaceCardEditor } from "../../types"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { entitiesConfigStruct } from "../structs/entities-struct"; +import { + getStatisticMetadata, + StatisticsMetaData, + statisticsMetaHasType, +} from "../../../../data/recorder"; +import { deepEqual } from "../../../../common/util/deep-equal"; +import { statTypeMap } from "../../../../components/chart/statistics-chart"; +import { ensureArray } from "../../../../common/ensure-array"; const statTypeStruct = union([ + literal("state"), literal("sum"), literal("min"), literal("max"), @@ -51,6 +67,7 @@ const cardConfigStruct = assign( ); const periods = ["5minute", "hour", "day", "month"] as const; +const stat_types = ["mean", "min", "max", "sum", "state"] as const; @customElement("hui-statistics-graph-card-editor") export class HuiStatisticsGraphCardEditor @@ -63,6 +80,8 @@ export class HuiStatisticsGraphCardEditor @state() private _configEntities?: string[]; + @state() private _metaDatas?: StatisticsMetaData[]; + public setConfig(config: StatisticsGraphCardConfig): void { assert(config, cardConfigStruct); this._config = config; @@ -71,8 +90,29 @@ export class HuiStatisticsGraphCardEditor : []; } + private _getStatisticsMetaData = async (statisticIds?: string[]) => { + this._metaDatas = await getStatisticMetadata( + this.hass!, + statisticIds || [] + ); + }; + + public willUpdate(changedProps: PropertyValues) { + if ( + changedProps.has("_configEntities") && + !deepEqual(this._configEntities, changedProps.get("_configEntities")) + ) { + this._metaDatas = undefined; + this._getStatisticsMetaData(this._configEntities); + } + } + private _schema = memoizeOne( - (localize: LocalizeFunc) => + ( + localize: LocalizeFunc, + statisticIds: string[] | undefined, + metaDatas: StatisticsMetaData[] | undefined + ) => [ { name: "title", selector: { text: {} } }, { @@ -89,6 +129,13 @@ export class HuiStatisticsGraphCardEditor label: localize( `ui.panel.lovelace.editor.card.statistics-graph.periods.${period}` ), + disabled: + period === "5minute" && + // External statistics don't support 5-minute statistics. + // External statistics is formatted as : + statisticIds?.some((statistic_id) => + statistic_id.includes(":") + ), })), }, }, @@ -101,13 +148,22 @@ export class HuiStatisticsGraphCardEditor { name: "stat_types", required: true, - type: "multi_select", - options: [ - ["mean", "Mean"], - ["min", "Min"], - ["max", "Max"], - ["sum", "Sum"], - ], + selector: { + select: { + multiple: true, + options: stat_types.map((stat_type) => ({ + value: stat_type, + label: localize( + `ui.panel.lovelace.editor.card.statistics-graph.stat_type_labels.${stat_type}` + ), + disabled: + !metaDatas || + !metaDatas?.every((metaData) => + statisticsMetaHasType(metaData, statTypeMap[stat_type]) + ), + })), + }, + }, }, { name: "chart_type", @@ -128,19 +184,28 @@ export class HuiStatisticsGraphCardEditor return html``; } - const schema = this._schema(this.hass.localize); - const stat_types = this._config!.stat_types + const schema = this._schema( + this.hass.localize, + this._configEntities, + this._metaDatas + ); + const configured_stat_types = this._config!.stat_types ? Array.isArray(this._config!.stat_types) ? this._config!.stat_types : [this._config!.stat_types] - : ["mean", "min", "max", "sum"]; + : stat_types.filter((stat_type) => + this._metaDatas?.every((metaData) => + statisticsMetaHasType(metaData, statTypeMap[stat_type]) + ) + ); const data = { chart_type: "line", period: "hour", days_to_show: 30, ...this._config, - stat_types, + stat_types: configured_stat_types, }; + const displayUnit = this._metaDatas?.[0]?.display_unit_of_measurement; return html` { + const config = { ...this._config!, entities: ev.detail.value }; + if ( + config.entities?.some((statistic_id) => statistic_id.includes(":")) && + config.period === "5minute" + ) { + delete config.period; + } + if (config.stat_types && config.entities.length) { + const metadata = await getStatisticMetadata(this.hass!, config.entities); + config.stat_types = ensureArray(config.stat_types).filter((stat_type) => + metadata.every((metaData) => + statisticsMetaHasType(metaData, statTypeMap[stat_type]) + ) + ); + if (!config.stat_types.length) { + delete config.stat_types; + } + } fireEvent(this, "config-changed", { - config: { ...this._config!, entities: ev.detail.value }, + config, }); } diff --git a/src/panels/lovelace/editor/hui-entities-card-row-editor.ts b/src/panels/lovelace/editor/hui-entities-card-row-editor.ts index 3d3925054c..eaf7b4c78b 100644 --- a/src/panels/lovelace/editor/hui-entities-card-row-editor.ts +++ b/src/panels/lovelace/editor/hui-entities-card-row-editor.ts @@ -1,14 +1,7 @@ import { mdiClose, mdiDrag, mdiPencil } from "@mdi/js"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { guard } from "lit/directives/guard"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; +import { repeat } from "lit/directives/repeat"; import type { SortableEvent } from "sortablejs"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/entity/ha-entity-picker"; @@ -39,20 +32,20 @@ export class HuiEntitiesCardRowEditor extends LitElement { @property() protected label?: string; - @state() private _attached = false; - - @state() private _renderEmptySortable = false; + private _entityKeys = new WeakMap(); private _sortable?: SortableInstance; - public connectedCallback() { - super.connectedCallback(); - this._attached = true; + public disconnectedCallback() { + this._destroySortable(); } - public disconnectedCallback() { - super.disconnectedCallback(); - this._attached = false; + private _getKey(action: LovelaceRowConfig) { + if (!this._entityKeys.has(action)) { + this._entityKeys.set(action, Math.random().toString()); + } + + return this._entityKeys.get(action)!; } protected render(): TemplateResult { @@ -70,63 +63,61 @@ export class HuiEntitiesCardRowEditor extends LitElement { )})`}
    - ${guard([this.entities, this._renderEmptySortable], () => - this._renderEmptySortable - ? "" - : this.entities!.map( - (entityConf, index) => html` -
    -
    - + ${repeat( + this.entities, + (entityConf) => this._getKey(entityConf), + (entityConf, index) => html` +
    +
    + +
    + ${entityConf.type + ? html` +
    +
    + + ${this.hass!.localize( + `ui.panel.lovelace.editor.card.entities.entity_row.${entityConf.type}` + )} + + ${this.hass!.localize( + "ui.panel.lovelace.editor.card.entities.edit_special_row" + )} +
    - ${entityConf.type - ? html` -
    -
    - - ${this.hass!.localize( - `ui.panel.lovelace.editor.card.entities.entity_row.${entityConf.type}` - )} - - ${this.hass!.localize( - "ui.panel.lovelace.editor.card.entities.edit_special_row" - )} -
    -
    - ` - : html` - - `} - - -
    - ` - ) + @value-changed=${this._valueChanged} + > + `} + + +
    + ` )}
    this._rowMoved(evt), + onChoose: (evt: SortableEvent) => { + (evt.item as any).placeholder = + document.createComment("sort-placeholder"); + evt.item.after((evt.item as any).placeholder); + }, + onEnd: (evt: SortableEvent) => { + // put back in original location + if ((evt.item as any).placeholder) { + (evt.item as any).placeholder.replaceWith(evt.item); + delete (evt.item as any).placeholder; + } + this._rowMoved(evt); + }, } ); } + private _destroySortable() { + this._sortable?.destroy(); + this._sortable = undefined; + } + private async _addEntity(ev: CustomEvent): Promise { const value = ev.detail.value; if (value === "") { diff --git a/src/panels/lovelace/editor/view-editor/hui-view-editor.ts b/src/panels/lovelace/editor/view-editor/hui-view-editor.ts index 323e6dab96..e25345e36f 100644 --- a/src/panels/lovelace/editor/view-editor/hui-view-editor.ts +++ b/src/panels/lovelace/editor/view-editor/hui-view-editor.ts @@ -1,10 +1,10 @@ -import "../../../../components/ha-form/ha-form"; import { html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../common/dom/fire_event"; import { slugify } from "../../../../common/string/slugify"; import type { LocalizeFunc } from "../../../../common/translations/localize"; +import "../../../../components/ha-form/ha-form"; import type { SchemaUnion } from "../../../../components/ha-form/types"; import type { LovelaceViewConfig } from "../../../../data/lovelace"; import type { HomeAssistant } from "../../../../types"; @@ -33,7 +33,7 @@ export class HuiViewEditor extends LitElement { private _suggestedPath = false; private _schema = memoizeOne( - (localize: LocalizeFunc) => + (localize: LocalizeFunc, subview: boolean, showAdvanced: boolean) => [ { name: "title", selector: { text: {} } }, { @@ -63,6 +63,20 @@ export class HuiViewEditor extends LitElement { }, }, }, + { + name: "subview", + selector: { + boolean: {}, + }, + }, + ...(subview && showAdvanced + ? [ + { + name: "back_path", + selector: { navigation: {} }, + }, + ] + : []), ] as const ); @@ -84,7 +98,12 @@ export class HuiViewEditor extends LitElement { return html``; } - const schema = this._schema(this.hass.localize); + const schema = this._schema( + this.hass.localize, + this._config.subview ?? false, + this.hass.userData?.showAdvanced ?? false + ); + const data = { theme: "Backend-selected", ...this._config, @@ -96,18 +115,22 @@ export class HuiViewEditor extends LitElement { .hass=${this.hass} .data=${data} .schema=${schema} - .computeLabel=${this._computeLabelCallback} + .computeLabel=${this._computeLabel} + .computeHelper=${this._computeHelper} @value-changed=${this._valueChanged} > `; } private _valueChanged(ev: CustomEvent): void { - const config = ev.detail.value; + const config = ev.detail.value as LovelaceViewConfig; if (config.type === "masonry") { delete config.type; } + if (!config.subview) { + delete config.back_path; + } if ( this.isNew && @@ -122,7 +145,7 @@ export class HuiViewEditor extends LitElement { fireEvent(this, "view-config-changed", { config }); } - private _computeLabelCallback = ( + private _computeLabel = ( schema: SchemaUnion> ) => { switch (schema.name) { @@ -130,12 +153,35 @@ export class HuiViewEditor extends LitElement { return this.hass!.localize("ui.panel.lovelace.editor.card.generic.url"); case "type": return this.hass.localize("ui.panel.lovelace.editor.edit_view.type"); + case "subview": + return this.hass.localize("ui.panel.lovelace.editor.edit_view.subview"); + case "back_path": + return this.hass.localize( + "ui.panel.lovelace.editor.edit_view.back_path" + ); default: return this.hass!.localize( `ui.panel.lovelace.editor.card.generic.${schema.name}` ); } }; + + private _computeHelper = ( + schema: SchemaUnion> + ) => { + switch (schema.name) { + case "subview": + return this.hass.localize( + "ui.panel.lovelace.editor.edit_view.subview_helper" + ); + case "back_path": + return this.hass.localize( + "ui.panel.lovelace.editor.edit_view.back_path_helper" + ); + default: + return undefined; + } + }; } declare global { diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index 3539e6f26f..05e5187259 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -112,6 +112,11 @@ class HUIRoot extends LitElement { } protected render(): TemplateResult { + const views = this.lovelace?.config.views ?? []; + + const curViewConfig = + typeof this._curView === "number" ? views[this._curView] : undefined; + return html` - - ${this.lovelace!.config.views.length > 1 + ${curViewConfig?.subview + ? html` + + ` + : html` + + `} + ${curViewConfig?.subview + ? html`
    ${curViewConfig.title}
    ` + : views.filter((view) => !view.subview).length > 1 ? html` - ${this.lovelace!.config.views.map( + ${views.map( (view) => html` e.user === this.hass!.user!.id - )) || - view.visible === false) + view.subview || + (view.visible !== undefined && + ((Array.isArray(view.visible) && + !view.visible.some( + (e) => + e.user === this.hass!.user!.id + )) || + view.visible === false)) ), })} > @@ -473,7 +490,7 @@ class HUIRoot extends LitElement { @iron-activate=${this._handleViewSelected} dir=${computeRTLDirection(this.hass!)} > - ${this.lovelace!.config.views.map( + ${views.map( (view) => html` @@ -528,7 +548,7 @@ class HUIRoot extends LitElement { class="edit-icon view" @click=${this._moveViewRight} ?disabled=${(this._curView! as number) + 1 === - this.lovelace!.config.views.length} + views.length} > ` : ""} @@ -720,6 +740,20 @@ class HUIRoot extends LitElement { }); } + private _goBack(): void { + const views = this.lovelace?.config.views ?? []; + const curViewConfig = + typeof this._curView === "number" ? views[this._curView] : undefined; + + if (curViewConfig?.back_path) { + navigate(curViewConfig.back_path); + } else if (history.length > 0) { + history.back(); + } else { + navigate(views[0].path!); + } + } + private _handleRawEditor(ev: CustomEvent): void { if (!shouldHandleRequestSelectedEvent(ev)) { return; @@ -1019,6 +1053,9 @@ class HUIRoot extends LitElement { --mdc-button-outline-color: var(--app-header-edit-text-color, #fff); --mdc-typography-button-font-size: 14px; } + .child-view-icon { + opacity: 0.5; + } `, ]; } diff --git a/src/state-summary/state-card-input_number.js b/src/state-summary/state-card-input_number.js index 2e0cd3f8b7..49efced50a 100644 --- a/src/state-summary/state-card-input_number.js +++ b/src/state-summary/state-card-input_number.js @@ -31,8 +31,7 @@ class StateCardInputNumber extends mixinBehaviors( .sliderstate { min-width: 45px; } - ha-slider[hidden], - ha-textfield[hidden] { + [hidden] { display: none !important; } ha-textfield { diff --git a/src/state/quick-bar-mixin.ts b/src/state/quick-bar-mixin.ts index b359c70e7f..8882cbcd3b 100644 --- a/src/state/quick-bar-mixin.ts +++ b/src/state/quick-bar-mixin.ts @@ -48,6 +48,11 @@ export default >(superClass: T) => private _registerShortcut() { tinykeys(window, { + // Those are for latin keyboards that have e, c, m keys + e: (ev) => this._showQuickBar(ev), + c: (ev) => this._showQuickBar(ev, true), + m: (ev) => this._createMyLink(ev), + // Those are fallbacks for non-latin keyboards that don't have e, c, m keys (qwerty-based shortcuts) KeyE: (ev) => this._showQuickBar(ev), KeyC: (ev) => this._showQuickBar(ev, true), KeyM: (ev) => this._createMyLink(ev), diff --git a/src/translations/en.json b/src/translations/en.json index c3fa44cf73..848a3c5e4f 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -517,6 +517,7 @@ "min": "min", "max": "max", "mean": "mean", + "state": "state", "sum": "sum" } }, @@ -897,8 +898,20 @@ "follow_device_area": "Follow device area", "change_device_area": "Change device area", "configure_state": "{integration} options", - "preload_stream": "Preload camera stream", - "preload_stream_description": "This keeps the camera stream open in the background so it shows quicker. Warning! This is device intensive." + "stream": { + "preload_stream": "Preload camera stream", + "preload_stream_description": "This keeps the camera stream open in the background so it shows quicker. Warning! This is device intensive.", + "stream_orientation": "Camera stream orientation", + "stream_orientation_description": "The orientation transformation to use for the camera stream.", + "stream_orientation_1": "No orientation transform", + "stream_orientation_2": "Mirror", + "stream_orientation_3": "Rotate 180", + "stream_orientation_4": "Flip", + "stream_orientation_5": "Rotate left and flip", + "stream_orientation_6": "Rotate left", + "stream_orientation_7": "Rotate right and flip", + "stream_orientation_8": "Rotate right" + } } }, "helper_settings": { @@ -1002,6 +1015,15 @@ "attribute": "Attribute", "min_max_change": "min/max/change" }, + "zha_manage_device": { + "heading": "Manage Zigbee Device", + "tabs": { + "clusters": "Clusters", + "bindings": "Bindings", + "signature": "Signature", + "children": "Children" + } + }, "zha_device_info": { "manuf": "by {manufacturer}", "no_area": "No Area", @@ -1010,10 +1032,8 @@ "buttons": { "add": "Add devices via this device", "remove": "Remove", - "clusters": "Manage clusters", + "manage": "Manage zigbee device", "reconfigure": "Reconfigure", - "zigbee_information": "Zigbee signature", - "device_children": "View children", "view_network": "View network" }, "services": { @@ -1083,10 +1103,11 @@ }, "join_beta_channel": { "title": "Join the beta channel", - "warning": "[%key:supervisor::system::supervisor::beta_warning%]", - "backup": "[%key:supervisor::system::supervisor::beta_backup%]", - "release_items": "[%key:supervisor::system::supervisor::beta_release_items%]", - "confirm": "[%key:supervisor::system::supervisor::beta_join_confirm%]" + "backup": "Make sure you have backups of your data before you activate this feature.", + "warning": "Beta releases are for testers and early adopters and can contain unstable code changes", + "release_items": "This includes beta releases for:", + "view_documentation": "View documentation", + "join": "Join" } }, "duration": { @@ -1293,7 +1314,7 @@ }, "delete": { "confirmation_title": "Are you sure you want to delete this area?", - "confirmation_text": "All devices in this area will become unassigned." + "confirmation_text": "This user will be permanently deleted." } }, "backup": { @@ -1622,14 +1643,18 @@ }, "processor": "Processor", "memory": "Memory", - "reboot_host": "Reboot host", - "rebooting_host": "Rebooting host", - "reboot_host_confirm": "Are you sure you want to reboot your host?", - "failed_to_reboot_host": "Failed to reboot host", - "shutdown_host": "Shutdown host", - "host_shutting_down": "Host shutting down", - "shutdown_host_confirm": "Are you sure you want to shutdown your host?", - "failed_to_shutdown_host": "Failed to shutdown host", + "rebooting_host": "Rebooting system", + "reboot": "Reboot", + "reboot_host": "Reboot system", + "reboot_host_title": "Reboot system?", + "reboot_host_text": "This will reboot the complete system which includes the Core and all Add-ons.", + "failed_to_reboot_host": "Failed to reboot system", + "host_shutting_down": "system shutting down", + "shutdown": "Shutdown", + "shutdown_host": "Shutdown system", + "shutdown_host_title": "Shutdown system?", + "shutdown_host_text": "This will shutdown the complete system which includes the Core and all Add-ons.", + "failed_to_shutdown_host": "Failed to shutdown system", "board": "Board", "documentation": "Documentation", "documentation_description": "Find extra information about your device" @@ -1720,7 +1745,7 @@ "add_dashboard": "Add dashboard" }, "confirm_delete_title": "Delete {dashboard_title}?", - "confirm_delete_text": "Your dashboard will be permanently deleted.", + "confirm_delete_text": "This dashboard will be permanently deleted.", "cant_edit_yaml": "Dashboards created in YAML cannot be edited from the UI. Change them in configuration.yaml.", "cant_edit_default": "The default dashboard, Overview, cannot be edited from the UI. You can hide it by setting another dashboard as default.", "detail": { @@ -1757,7 +1782,8 @@ "no_resources": "No resources", "add_resource": "Add resource" }, - "confirm_delete": "Are you sure you want to delete this resource?", + "confirm_delete_title": "Delete resource?", + "confirm_delete_text": "{url} will be permanently deleted.", "refresh_header": "Do you want to refresh?", "refresh_body": "You have to refresh the page to complete the removal. Do you want to refresh now?", "cant_edit_yaml": "You are using your dashboard in YAML mode, therefore you cannot manage your resources through the UI. Manage them in configuration.yaml.", @@ -1791,7 +1817,8 @@ "dev_automation": "Debug automation", "show_info_automation": "Show info about automation", "delete": "[%key:ui::common::delete%]", - "delete_confirm": "Are you sure you want to delete this automation?", + "delete_confirm_title": "Delete automation?", + "delete_confirm_text": "{name} will be permanently deleted.", "duplicate": "[%key:ui::common::duplicate%]", "disabled": "Disabled", "headers": { @@ -1831,7 +1858,8 @@ "load_error_not_deletable": "Only automations in automations.yaml can be deleted.", "load_error_unknown": "Error loading automation ({err_no}).", "save": "Save", - "unsaved_confirm": "You have unsaved changes. Are you sure you want to leave?", + "unsaved_confirm_title": "Leave editor?", + "unsaved_confirm_text": "Unsaved changes will be lost.", "alias": "Name", "automation_alias": "Automation name", "automation_settings": "Automation settings", @@ -1882,7 +1910,8 @@ "change_alias": "Rename trigger", "alias": "Trigger name", "delete": "[%key:ui::common::delete%]", - "delete_confirm": "Are you sure you want to delete this?", + "delete_confirm_title": "Delete trigger?", + "delete_confirm_text": "It will be permanently deleted.", "unsupported_platform": "No visual editor support for platform: {platform}", "type_select": "Trigger type", "type": { @@ -2002,7 +2031,8 @@ "change_alias": "Rename condition", "alias": "Condition name", "delete": "[%key:ui::common::delete%]", - "delete_confirm": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm%]", + "delete_confirm_title": "Delete condition?", + "delete_confirm_text": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm_text%]", "unsupported_condition": "No visual editor support for condition: {condition}", "type_select": "Condition type", "type": { @@ -2096,7 +2126,8 @@ "disable": "Disable", "disabled": "Disabled", "delete": "[%key:ui::common::delete%]", - "delete_confirm": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm%]", + "delete_confirm_title": "Delete action?", + "delete_confirm_text": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm_text%]", "unsupported_action": "No visual editor support for action: {action}", "type_select": "Action type", "type": { @@ -2234,8 +2265,8 @@ "automation": "Automation", "script": "Script" }, - "confirm_delete_header": "Delete this blueprint?", - "confirm_delete_text": "Are you sure you want to delete this blueprint?", + "confirm_delete_title": "Delete blueprint?", + "confirm_delete_text": "{name} will be permanently deleted.", "add_blueprint": "Import blueprint", "no_blueprints": "[%key:ui::panel::config::automation::editor::blueprint::no_blueprints%]", "create_automation": "Create automation", @@ -2248,15 +2279,15 @@ "add": { "header": "Import a blueprint", "import_header": "Blueprint ''{name}''", - "import_introduction_link": "You can import blueprints of other users from Github and the {community_link}. Enter the URL of the blueprint below.", - "community_forums": "community forums", - "url": "URL of the blueprint", + "import_introduction": "Import blueprints of other users from GitHub and the community forums by pasting the address below.", + "community_forums": "View blueprints on the community forums", + "url": "Blueprint address", "raw_blueprint": "Blueprint content", "importing": "Loading blueprint…", - "import_btn": "Preview blueprint", + "import_btn": "Preview", "saving": "Importing blueprint…", "save_btn": "Import blueprint", - "error_no_url": "Please enter the URL of the blueprint.", + "error_no_url": "Please enter the blueprint address.", "unsupported_blueprint": "This blueprint is not supported", "file_name": "Blueprint Path" } @@ -2308,7 +2339,8 @@ "load_error_not_duplicable": "Only scripts in scripts.yaml can be duplicated.", "load_error_not_deletable": "Only scripts in scripts.yaml can be deleted.", "load_error_unknown": "Error loading script ({err_no}).", - "delete_confirm": "Are you sure you want to delete this script?", + "delete_confirm_title": "Delete script?", + "delete_confirm_text": "{name} will be permanently deleted.", "save_script": "Save script", "sequence": "Sequence", "sequence_sentence": "The sequence of actions of this script.", @@ -2333,7 +2365,8 @@ "activate": "Activate", "delete_scene": "Delete scene", "delete": "[%key:ui::common::delete%]", - "delete_confirm": "Are you sure you want to delete this scene?", + "delete_confirm_title": "Delete scene?", + "delete_confirm_text": "{name} will be permanently deleted.", "duplicate_scene": "Duplicate scene", "duplicate": "[%key:ui::common::duplicate%]", "headers": { @@ -2346,7 +2379,8 @@ "load_error_not_editable": "Only scenes in scenes.yaml are editable.", "load_error_unknown": "Error loading scene ({err_no}).", "save": "Save", - "unsaved_confirm": "You have unsaved changes. Are you sure you want to leave?", + "unsaved_confirm_title": "Leave editor?", + "unsaved_confirm_text": "Unsaved changes will be lost.", "name": "Name", "icon": "Icon", "area": "Area", @@ -2472,14 +2506,13 @@ "config_documentation": "Configuration documentation", "enable_state_reporting": "Enable State Reporting", "info_state_reporting": "If you enable state reporting, Home Assistant will send all state changes of exposed entities to Amazon. This allows you to always see the latest states in the Alexa app and use the state changes to create routines.", - "sync_entities": "Sync Entities to Amazon", - "manage_entities": "Manage Entities", - "sync_entities_error": "Failed to sync entities:", "state_reporting_error": "Unable to {enable_disable} report state.", + "manage_entities": "Manage Entities", "enable": "enable", "disable": "disable", "not_configured_title": "Alexa is not activated", - "not_configured_text": "Before you can use Alexa, you need to activate the Home Assistant skill for Alexa in the Alexa app." + "not_configured_text": "Before you can use Alexa, you need to activate the Home Assistant skill for Alexa in the Alexa app.", + "link_learn_how_it_works": "[%key:ui::panel::config::cloud::account::remote::link_learn_how_it_works%]" }, "google": { "title": "Google Assistant", @@ -2494,13 +2527,11 @@ "enter_pin_info": "Please enter a PIN to interact with security devices. Security devices are doors, garage doors and locks. You will be asked to say/enter this PIN when interacting with such devices via Google Assistant.", "devices_pin": "Security Devices PIN", "enter_pin_hint": "Enter a PIN to use security devices", - "sync_entities": "Sync Entities to Google", "manage_entities": "Manage Entities", "enter_pin_error": "Unable to store PIN:", "not_configured_title": "Google Assistant is not activated", "not_configured_text": "Before you can use Google Assistant, you need to activate the Home Assistant Cloud skill for Google Assistant in the Google Home app.", - "sync_failed_title": "Syncing failed", - "sync_failed_text": "Syncing your entities failed, try again or check the logs." + "link_learn_how_it_works": "[%key:ui::panel::config::cloud::account::remote::link_learn_how_it_works%]" }, "webhooks": { "title": "Webhooks", @@ -2527,7 +2558,9 @@ "follow_domain": "[%key:ui::panel::config::cloud::google::follow_domain%]", "exposed": "[%key:ui::panel::config::cloud::google::exposed%]", "not_exposed": "[%key:ui::panel::config::cloud::google::not_exposed%]", - "expose": "Expose to Alexa" + "expose": "Expose to Alexa", + "sync_entities": "Synchronize entities", + "sync_entities_error": "Failed to sync entities:" }, "dialog_certificate": { "certificate_information": "Certificate Information", @@ -2550,18 +2583,24 @@ "follow_domain": "Follow domain", "exposed": "{selected} exposed", "not_exposed": "{selected} not exposed", - "sync_to_google": "Synchronizing changes to Google." + "sync_to_google": "Synchronizing changes to Google.", + "sync_entities": "Synchronize entities", + "sync_entities_error": "Failed to sync entities:", + "not_configured_title": "[%key:ui::panel::config::cloud::account::google::not_configured_title%]", + "not_configured_text": "[%key:ui::panel::config::cloud::account::google::not_configured_text%]", + "sync_failed_title": "Syncing failed", + "sync_failed_text": "Syncing your entities failed, try again or check the logs." }, "dialog_cloudhook": { "webhook_for": "Webhook for {name}", - "available_at": "The webhook is available at the following URL:", "managed_by_integration": "This webhook is managed by an integration and cannot be disabled.", "info_disable_webhook": "If you no longer want to use this webhook, you can", "link_disable_webhook": "disable it", + "public_url": "Public address", "view_documentation": "View documentation", "close": "Close", - "confirm_disable": "Are you sure you want to disable this webhook?", - "copied_to_clipboard": "Copied to clipboard" + "confirm_disable_title": "Disable webhook", + "confirm_disable_text": "Webhook for {name} will be disabled." } }, "devices": { @@ -2743,8 +2782,8 @@ "no_persons_created_yet": "Looks like you have not added any people yet.", "create_person": "Create Person", "add_person": "Add Person", - "confirm_delete": "Are you sure you want to delete this person?", - "confirm_delete2": "All devices belonging to this person will become unassigned.", + "confirm_delete_title": "Delete {name}?", + "confirm_delete_text": "This person will be permanently deleted and all devices belonging to this person will become unassigned.", "person_not_found_title": "Person Not Found", "person_not_found": "We couldn't find the person you were trying to edit.", "detail": { @@ -2808,7 +2847,7 @@ "discovered": "Discovered", "attention": "Attention required", "configured": "Configured", - "new": "Set up a new integration", + "new": "Select brand", "confirm_new": "Do you want to set up {integration}?", "add_integration": "Add integration", "no_integrations": "Seems like you don't have any integrations configured yet. Click on the button below to add your first integration!", @@ -2825,6 +2864,7 @@ "rename_dialog": "Edit the name of this config entry", "rename_input_label": "Entry name", "search": "Search integrations", + "search_brand": "Search for a brand name", "add_zwave_js_device": "Add Z-Wave device", "add_zha_device": "Add Zigbee device", "disable": { @@ -2843,6 +2883,14 @@ "stop_ignore": "Stop ignoring" }, "config_entry": { + "application_credentials": { + "delete_title": "Application Credentials", + "delete_prompt": "Would you like to also remove Application Credentials for this integration?", + "delete_detail": "If you remove them, you will need to enter credentials when setting up the integration again. If you keep them, they will be used automatically when setting up the integration again or may be acccessed from the Application Credentials menu.", + "delete_error_title": "Removing Application Credential failed", + "dismiss": "Keep", + "learn_more": "Learn more about Application Credentials" + }, "devices": "{count} {count, plural,\n one {device}\n other {devices}\n}", "entities": "{count} {count, plural,\n one {entity}\n other {entities}\n}", "services": "{count} {count, plural,\n one {service}\n other {services}\n}", @@ -2853,13 +2901,16 @@ "download_diagnostics": "Download diagnostics", "known_issues": "Known issues", "delete": "Delete", - "delete_confirm": "Are you sure you want to delete the {title} integration?", + "delete_confirm_title": "Delete {title}?", + "delete_confirm_text": "Its devices and entities will be permantly deleted.", "reload": "Reload", "restart_confirm": "Restart Home Assistant to finish removing this integration", "reload_confirm": "The integration was reloaded", "reload_restart_confirm": "Restart Home Assistant to finish reloading this integration", "disable_restart_confirm": "Restart Home Assistant to finish disabling this integration", "enable_restart_confirm": "Restart Home Assistant to finish enabling this integration", + "disable_confirm_title": "Disable {title}?", + "disable_confirm_text": "Its devices and entities will be disabled.", "disable_error": "Enabling or disabling of the integration failed", "manuf": "by {manufacturer}", "via": "Connected via", @@ -2880,11 +2931,11 @@ "user": "user", "integration": "integration", "device": "device" - }, - "disable_confirm": "Are you sure you want to disable this config entry? Its devices and entities will be disabled." + } }, "provided_by_custom_integration": "Provided by a custom integration", "depends_on_cloud": "Depends on the cloud", + "yaml_only": "Can not be setup from the UI", "disabled_polling": "Automatic polling for updated data disabled", "state": { "loaded": "Loaded", @@ -2906,6 +2957,9 @@ "submit": "Submit", "next": "Next", "found_following_devices": "We found the following devices", + "yaml_only_title": "This device can not be added from the UI", + "yaml_only_text": "You can add this device by adding it to your `configuration.yaml`. See the {link} for more information.", + "documentation": "documentation", "no_config_flow": "This integration does not support configuration via the UI. If you followed this link from the Home Assistant website, make sure you run the latest version of Home Assistant.", "not_all_required_fields": "Not all required fields are filled in.", "error_saving_area": "Error saving area: {error}", @@ -2926,6 +2980,7 @@ "error": "Error", "could_not_load": "Config flow could not be loaded", "not_loaded": "The integration could not be loaded, try to restart Home Assistant.", + "missing_credentials_title": "Add application credentials?", "missing_credentials": "Setting up {integration} requires configuring application credentials. Do you want to do that now?", "supported_brand_flow": "Support for {supported_brand} devices is provided by {flow_domain_name}. Do you want to continue?", "missing_zwave_zigbee": "To add a {integration} device, you first need {supported_hardware_link} and the {integration} integration set up. If you already have the hardware then you can proceed with the setup of {integration}.", @@ -2958,8 +3013,6 @@ "name": "Display name", "username": "Username", "change_password": "Change password", - "new_password": "New Password", - "password_changed": "Password was changed successfully", "activate_user": "Activate user", "deactivate_user": "Deactivate user", "delete_user": "Delete user", @@ -2974,7 +3027,8 @@ "system_generated_users_not_removable": "Unable to remove system users.", "system_generated_users_not_editable": "Unable to update system users.", "unnamed_user": "Unnamed User", - "confirm_user_deletion": "Are you sure you want to delete {name}?", + "confirm_user_deletion_title": "Delete {name}?", + "confirm_user_deletion_text": "This user will be permanently deleted.", "active_tooltip": "Controls if user can login" }, "add_user": { @@ -2984,18 +3038,30 @@ "password_not_match": "Passwords don't match", "local_only": "Local only", "create": "Create" + }, + "change_password": { + "caption": "Change password", + "new_password": "New Password", + "password_confirm": "Confirm Password", + "change": "Change", + "password_no_match": "Passwords don't match", + "password_changed": "The password has been changed successfully." } }, "application_credentials": { "caption": "Application Credentials", "description": "Manage the OAuth Application Credentials used by Integrations", "editor": { - "caption": "Add Application Credential", - "create": "Create", + "caption": "Add Credential", + "description": "OAuth is used to grant Home Assistant access to information on other websites without giving a passwords. This mechanism is used by companies such as Spotify, Google, Withings, Microsoft, and Twitter.", + "view_documentation": "View documentation", + "add": "Add", "domain": "Integration", "name": "Name", "client_id": "OAuth Client ID", - "client_secret": "OAuth Client Secret" + "client_id_helper": "Public identifier of the OAuth application", + "client_secret": "OAuth Client Secret", + "client_secret_helper": "Secret of the OAuth application" }, "picker": { "add_application_credential": "Add Application Credential", @@ -3050,17 +3116,17 @@ "clusters": { "header": "Clusters", "help_cluster_dropdown": "Select a cluster to view attributes and commands.", - "introduction": "Clusters are the building blocks for Zigbee functionality. They separate functionality into logical units. There are client and server types and that are comprised of attributes and commands." + "tabs": { + "attributes": "Attributes", + "commands": "Commands" + } }, "cluster_attributes": { "header": "Cluster Attributes", "introduction": "View and edit cluster attributes.", "attributes_of_cluster": "Attributes of the selected cluster", - "get_zigbee_attribute": "Get Zigbee Attribute", - "set_zigbee_attribute": "Set Zigbee Attribute", - "help_attribute_dropdown": "Select an attribute to view or set its value.", - "help_get_zigbee_attribute": "Get the value for the selected attribute.", - "help_set_zigbee_attribute": "Set attribute value for the specified cluster on the specified entity." + "read_zigbee_attribute": "Read Attribute", + "write_zigbee_attribute": "Write Attribute" }, "cluster_commands": { "header": "Cluster Commands", @@ -3110,6 +3176,11 @@ "enable_physics": "Enable Physics", "refresh_topology": "Refresh Topology" }, + "device_binding": { + "bind": "Bind", + "unbind": "Unbind", + "picker_label": "Bindable Devices" + }, "group_binding": { "header": "Group Binding", "introduction": "Bind and unbind groups.", @@ -3492,7 +3563,8 @@ "title": "Move datadisk", "description": "You are currently using ''{current_path}'' as datadisk. Moving data disks will reboot your device and it's estimated to take {time} minutes. Your Home Assistant installation will not be accessible during this period. Do not disconnect the power during the move!", "select_device": "Select new datadisk", - "no_devices": "No suitable attached devices found", + "no_devices_title": "No suitable storage found", + "no_devices_text": "There is no suitable external device found. The storage capacity of the external data disk must be larger than the storage capacity of the existing disk", "moving_desc": "Rebooting and moving datadisk. Please have patience", "moving": "Moving datadisk", "loading_devices": "Loading devices", @@ -3511,7 +3583,7 @@ "integration_start_time": "Integration Startup Time" }, "system_dashboard": { - "confirm_restart_text": "Restarting Home Assistant will stop all your active dashboards, automations and scripts.", + "confirm_restart_text": "This will stop all your active dashboards, automations and scripts.", "confirm_restart_title": "Restart Home Assistant?", "restart_homeassistant_short": "Restart", "restart_error": "Failed to restart Home Assistant" @@ -3724,7 +3796,11 @@ "masonry": "Masonry (default)", "sidebar": "Sidebar", "panel": "Panel (1 card)" - } + }, + "subview": "Subview", + "subview_helper": "Subviews don't appear in tabs and have a back button.", + "back_path": "Back path (optional)", + "back_path_helper": "Only for subviews. If empty, clicking on back button will go to the previous page." }, "edit_badges": { "view_no_badges": "Badges are not be supported by the current view type." @@ -3913,13 +3989,22 @@ "description": "The Statistics Graph card allows you to display a graph of the statistics for each of the entities listed.", "period": "Period", "stat_types": "Show stat types", + "stat_type_labels": { + "mean": "Mean", + "min": "Min", + "max": "Max", + "state": "State", + "sum": "Sum (change during period)" + }, "chart_type": "Chart type", "periods": { "hour": "Hour", "day": "Day", "month": "Month", "5minute": "5 Minutes" - } + }, + "pick_statistic": "Add a statistic", + "picked_statistic": "Statistic" }, "horizontal-stack": { "name": "Horizontal Stack", @@ -4940,10 +5025,6 @@ "reload_supervisor": "Reload Supervisor", "warning": "WARNING", "search": "Search", - "beta_warning": "Beta releases are for testers and early adopters and can contain unstable code changes", - "beta_backup": "Make sure you have backups of your data before you activate this feature.", - "beta_release_items": "This includes beta releases for:", - "beta_join_confirm": "Do you want to join the beta channel?", "share_diagonstics_title": "Help Improve Home Assistant", "share_diagonstics_description": "Would you want to automatically share crash reports and diagnostic information when the Supervisor encounters unexpected errors? {line_break} This will allow us to fix the problems, the information is only accessible to the Home Assistant Core team and will not be shared with others.{line_break} The data does not include any private/sensitive information and you can disable this in settings at any time you want.", "unsupported_reason": { @@ -5070,8 +5151,14 @@ "used": "Repository is in use for installed add-ons and can't be removed." }, "restart_addon": { - "confirm_text": "Restart add-on", - "text": "Do you want to restart the add-on with your changes?" + "title": "Restart {name}?", + "text": "To use the new saved configuration this add-on must be restarted.", + "restart": "Restart" + }, + "uninstall_addon": { + "title": "Uninstall {name}?", + "text": "Its configuration will be permanently deleted.", + "uninstall": "Uninstall" }, "hardware": { "title": "Hardware", diff --git a/yarn.lock b/yarn.lock index 40f398ce50..6c3e5251be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8940,10 +8940,10 @@ fsevents@^1.2.7: languageName: node linkType: hard -"hls.js@npm:^1.2.1": - version: 1.2.1 - resolution: "hls.js@npm:1.2.1" - checksum: 19dd63a0bae5ca5e58a12cf0d4d7da353cdb88bb2ffbc691edcd0ceca7e57ca6ab4189bb55b326b16ed8c12d0053d94ea14eb1617e4279fc73cee11d33bf7f90 +"hls.js@npm:^1.2.3": + version: 1.2.3 + resolution: "hls.js@npm:1.2.3" + checksum: 9d07957d94e84b8f187aa209ffb0e6f65302178c6fdebc6effd63644d8cf2e304b2232109a30336cfee02c908aba86a402fe5d5dfa6487aca3387fd5748cccfa languageName: node linkType: hard @@ -9095,7 +9095,7 @@ fsevents@^1.2.7: gulp-merge-json: ^1.3.1 gulp-rename: ^2.0.0 gulp-zopfli-green: ^3.0.1 - hls.js: ^1.2.1 + hls.js: ^1.2.3 home-assistant-js-websocket: ^8.0.0 html-minifier: ^4.0.0 husky: ^8.0.1