mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-22 16:56:35 +00:00
Merge pull request #9024 from home-assistant/dev
This commit is contained in:
commit
6b7e78320d
35
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
35
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -1,8 +1,6 @@
|
||||
name: Report a bug with the UI, Frontend or Lovelace
|
||||
about: Report an issue related to the Home Assistant frontend.
|
||||
description: Report an issue related to the Home Assistant frontend.
|
||||
labels: bug
|
||||
title: ""
|
||||
issue_body: true
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@ -97,11 +95,7 @@ body:
|
||||
If your issue is about how an entity is shown in the UI, please add the
|
||||
state and attributes for all situations. You can find this information
|
||||
at Developer Tools -> States.
|
||||
value: |
|
||||
```yaml
|
||||
# Paste your state here.
|
||||
|
||||
```
|
||||
render: txt
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Problem-relevant frontend configuration
|
||||
@ -110,29 +104,18 @@ body:
|
||||
configuration of the used cards. Fill this out even if it seems
|
||||
unimportant to you. Please be sure to remove personal information like
|
||||
passwords, private URLs and other credentials.
|
||||
value: |
|
||||
```yaml
|
||||
# Paste your YAML here.
|
||||
|
||||
```
|
||||
render: yaml
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Javascript errors shown in your browser console/inspector
|
||||
description: >
|
||||
If you come across any Javascript or other error logs, e.g., in your
|
||||
browser console/inspector please provide them.
|
||||
value: |
|
||||
```txt
|
||||
# Paste your logs here.
|
||||
|
||||
```
|
||||
- type: markdown
|
||||
render: txt
|
||||
- type: textarea
|
||||
attributes:
|
||||
value: |
|
||||
## Additional information
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
label: Additional information
|
||||
description: >
|
||||
If you have any additional information for us, use the field below.
|
||||
Please note, you can attach screenshots or screen recordings here,
|
||||
by dragging and dropping files in the field below.
|
||||
Please note, you can attach screenshots or screen recordings here, by
|
||||
dragging and dropping files in the field below.
|
||||
|
@ -35,6 +35,7 @@ class HcLovelace extends LitElement {
|
||||
}
|
||||
const lovelace: Lovelace = {
|
||||
config: this.lovelaceConfig,
|
||||
rawConfig: this.lovelaceConfig,
|
||||
editMode: false,
|
||||
urlPath: this.urlPath!,
|
||||
enableFullEditMode: () => undefined,
|
||||
|
@ -221,11 +221,17 @@ export class HcMain extends HassElement {
|
||||
}
|
||||
|
||||
private async _generateLovelaceConfig() {
|
||||
const { generateLovelaceConfigFromHass } = await import(
|
||||
"../../../../src/panels/lovelace/common/generate-lovelace-config"
|
||||
const { generateLovelaceDashboardStrategy } = await import(
|
||||
"../../../../src/panels/lovelace/strategies/get-strategy"
|
||||
);
|
||||
this._handleNewLovelaceConfig(
|
||||
await generateLovelaceConfigFromHass(this.hass!)
|
||||
await generateLovelaceDashboardStrategy(
|
||||
{
|
||||
hass: this.hass!,
|
||||
narrow: false,
|
||||
},
|
||||
"original-states"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -246,11 +246,15 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
||||
|
||||
"light.living_room_lights": {
|
||||
entity_id: "light.living_room_lights",
|
||||
state: "off",
|
||||
state: "on",
|
||||
attributes: {
|
||||
min_mireds: 111,
|
||||
max_mireds: 400,
|
||||
brightness: 175,
|
||||
color_temp: 300,
|
||||
supported_color_modes: ["brightness", "color_temp"],
|
||||
friendly_name: "Living Room Lights",
|
||||
color_mode: "color_temp",
|
||||
supported_features: 55,
|
||||
},
|
||||
},
|
||||
@ -263,13 +267,27 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
||||
},
|
||||
"light.kitchen_lights": {
|
||||
entity_id: "light.kitchen_lights",
|
||||
state: "on",
|
||||
attributes: {
|
||||
min_mireds: 111,
|
||||
max_mireds: 400,
|
||||
brightness: 200,
|
||||
rgb_color: [255, 175, 96],
|
||||
supported_color_modes: ["brightness", "color_temp", "rgb"],
|
||||
color_mode: "rgb",
|
||||
friendly_name: "Kitchen Lights",
|
||||
supported_features: 55,
|
||||
},
|
||||
},
|
||||
"light.lifx5": {
|
||||
entity_id: "light.lifx5",
|
||||
state: "off",
|
||||
attributes: {
|
||||
friendly_name: "Kitchen Lights",
|
||||
supported_color_modes: ["brightness"],
|
||||
friendly_name: "Garage Lights",
|
||||
supported_features: 1,
|
||||
},
|
||||
},
|
||||
|
||||
"sensor.plexspy": {
|
||||
entity_id: "sensor.plexspy",
|
||||
state: "0",
|
||||
@ -482,16 +500,6 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
||||
icon: "hademo:history",
|
||||
},
|
||||
},
|
||||
"light.lifx5": {
|
||||
entity_id: "light.lifx5",
|
||||
state: "on",
|
||||
attributes: {
|
||||
min_mireds: 111,
|
||||
max_mireds: 400,
|
||||
friendly_name: "Garage Lights",
|
||||
supported_features: 55,
|
||||
},
|
||||
},
|
||||
"sensor.alok_to_home": {
|
||||
entity_id: "sensor.alok_to_home",
|
||||
state: "41",
|
||||
|
@ -1114,6 +1114,9 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
|
||||
min_mireds: 153,
|
||||
max_mireds: 500,
|
||||
brightness: 63,
|
||||
color_temp: 200,
|
||||
supported_color_modes: ["brightness", "color_temp", "rgb"],
|
||||
color_mode: "color_temp",
|
||||
friendly_name: "Upstairs lights",
|
||||
supported_features: 63,
|
||||
custom_ui_state_card: "state-card-custom-ui",
|
||||
@ -1125,6 +1128,7 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
|
||||
attributes: {
|
||||
friendly_name: "Walk in closet lights",
|
||||
supported_features: 41,
|
||||
supported_color_modes: ["brightness", "color_temp"],
|
||||
custom_ui_state_card: "state-card-custom-ui",
|
||||
icon: "mdi:wall-sconce",
|
||||
},
|
||||
@ -1136,6 +1140,8 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
|
||||
brightness: 254,
|
||||
friendly_name: "Outdoor lights",
|
||||
supported_features: 41,
|
||||
supported_color_modes: ["brightness"],
|
||||
color_mode: "brightness",
|
||||
custom_ui_state_card: "state-card-custom-ui",
|
||||
icon: "mdi:wall-sconce",
|
||||
},
|
||||
@ -1148,6 +1154,8 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
|
||||
max_mireds: 500,
|
||||
brightness: 128,
|
||||
color_temp: 366,
|
||||
supported_color_modes: ["brightness", "color_temp", "rgb"],
|
||||
color_mode: "color_temp",
|
||||
effect_list: ["colorloop"],
|
||||
friendly_name: "Downstairs lights",
|
||||
supported_features: 63,
|
||||
@ -1307,6 +1315,7 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
|
||||
attributes: {
|
||||
min_mireds: 153,
|
||||
max_mireds: 500,
|
||||
supported_color_modes: ["brightness", "color_temp"],
|
||||
is_deconz_group: false,
|
||||
friendly_name: "Bedside Lamp",
|
||||
supported_features: 63,
|
||||
@ -1320,6 +1329,7 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
|
||||
attributes: {
|
||||
min_mireds: 153,
|
||||
max_mireds: 500,
|
||||
supported_color_modes: ["brightness", "color_temp"],
|
||||
is_deconz_group: false,
|
||||
friendly_name: "Floorlamp Reading Light",
|
||||
supported_features: 43,
|
||||
@ -1335,6 +1345,8 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
|
||||
max_mireds: 500,
|
||||
brightness: 128,
|
||||
color_temp: 366,
|
||||
supported_color_modes: ["brightness", "color_temp", "rgb"],
|
||||
color_mode: "color_temp",
|
||||
effect_list: ["colorloop"],
|
||||
is_deconz_group: false,
|
||||
friendly_name: "Hallway window light",
|
||||
@ -1349,6 +1361,7 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
|
||||
attributes: {
|
||||
brightness: 77,
|
||||
is_deconz_group: false,
|
||||
supported_color_modes: ["brightness"],
|
||||
friendly_name: "Isa Ceiling Light",
|
||||
supported_features: 41,
|
||||
custom_ui_state_card: "state-card-custom-ui",
|
||||
@ -1363,6 +1376,8 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
|
||||
max_mireds: 500,
|
||||
brightness: 150,
|
||||
color_temp: 366,
|
||||
supported_color_modes: ["brightness", "color_temp"],
|
||||
color_mode: "color_temp",
|
||||
effect_list: ["colorloop"],
|
||||
is_deconz_group: false,
|
||||
friendly_name: "Floorlamp",
|
||||
@ -1377,6 +1392,7 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
|
||||
attributes: {
|
||||
friendly_name: "Bedroom Ceiling Light",
|
||||
supported_features: 41,
|
||||
supported_color_modes: ["brightness"],
|
||||
custom_ui_state_card: "state-card-custom-ui",
|
||||
icon: "mdi:ceiling-light",
|
||||
},
|
||||
@ -1387,6 +1403,7 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
|
||||
attributes: {
|
||||
friendly_name: "Nightlight",
|
||||
supported_features: 17,
|
||||
supported_color_modes: ["brightness"],
|
||||
custom_ui_state_card: "state-card-custom-ui",
|
||||
icon: "mdi:lamp",
|
||||
},
|
||||
@ -1753,6 +1770,7 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
|
||||
power_consumption: 2.2,
|
||||
friendly_name: "Upstairs Hallway Light",
|
||||
supported_features: 33,
|
||||
supported_color_modes: ["brightness"],
|
||||
custom_ui_state_card: "state-card-custom-ui",
|
||||
icon: "mdi:ceiling-light",
|
||||
},
|
||||
@ -1768,6 +1786,7 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
|
||||
power_consumption: 0,
|
||||
friendly_name: "Dining Room Light",
|
||||
supported_features: 33,
|
||||
supported_color_modes: ["brightness"],
|
||||
custom_ui_state_card: "state-card-custom-ui",
|
||||
icon: "mdi:ceiling-light",
|
||||
},
|
||||
@ -1783,6 +1802,7 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
|
||||
power_consumption: 0,
|
||||
friendly_name: "Living room Spotlights",
|
||||
supported_features: 33,
|
||||
supported_color_modes: ["brightness"],
|
||||
custom_ui_state_card: "state-card-custom-ui",
|
||||
icon: "mdi:track-light",
|
||||
},
|
||||
@ -1799,6 +1819,7 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
|
||||
power_consumption: 2.5,
|
||||
friendly_name: "Passage Lights",
|
||||
supported_features: 33,
|
||||
supported_color_modes: ["brightness"],
|
||||
custom_ui_state_card: "state-card-custom-ui",
|
||||
icon: "mdi:track-light",
|
||||
},
|
||||
@ -1843,6 +1864,7 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
|
||||
power_consumption: 37.4,
|
||||
friendly_name: "Kitchen Lights",
|
||||
supported_features: 33,
|
||||
supported_color_modes: ["brightness"],
|
||||
custom_ui_state_card: "state-card-custom-ui",
|
||||
icon: "mdi:track-light",
|
||||
},
|
||||
|
350
gallery/src/demos/demo-integration-card.ts
Normal file
350
gallery/src/demos/demo-integration-card.ts
Normal file
@ -0,0 +1,350 @@
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "../../../src/components/ha-switch";
|
||||
|
||||
import { IntegrationManifest } from "../../../src/data/integration";
|
||||
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import "../../../src/panels/config/integrations/ha-integration-card";
|
||||
import "../../../src/panels/config/integrations/ha-ignored-config-entry-card";
|
||||
import "../../../src/panels/config/integrations/ha-config-flow-card";
|
||||
import type {
|
||||
ConfigEntryExtended,
|
||||
DataEntryFlowProgressExtended,
|
||||
} from "../../../src/panels/config/integrations/ha-config-integrations";
|
||||
import { DeviceRegistryEntry } from "../../../src/data/device_registry";
|
||||
import { EntityRegistryEntry } from "../../../src/data/entity_registry";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
|
||||
const createConfigEntry = (
|
||||
title: string,
|
||||
override: Partial<ConfigEntryExtended> = {}
|
||||
): ConfigEntryExtended => ({
|
||||
entry_id: title,
|
||||
domain: "esphome",
|
||||
localized_domain_name: "ESPHome",
|
||||
title,
|
||||
source: "zeroconf",
|
||||
state: "loaded",
|
||||
connection_class: "local_push",
|
||||
supports_options: false,
|
||||
supports_unload: true,
|
||||
disabled_by: null,
|
||||
reason: null,
|
||||
...override,
|
||||
});
|
||||
|
||||
const createManifest = (
|
||||
isCustom: boolean,
|
||||
isCloud: boolean,
|
||||
name = "ESPHome"
|
||||
): IntegrationManifest => ({
|
||||
name,
|
||||
domain: "esphome",
|
||||
is_built_in: !isCustom,
|
||||
config_flow: false,
|
||||
documentation: "https://www.home-assistant.io/integrations/esphome/",
|
||||
iot_class: isCloud ? "cloud_polling" : "local_polling",
|
||||
});
|
||||
|
||||
const loadedEntry = createConfigEntry("Loaded");
|
||||
const nameAsDomainEntry = createConfigEntry("ESPHome");
|
||||
const longNameEntry = createConfigEntry(
|
||||
"Entry with a super long name that is going to the next line"
|
||||
);
|
||||
const configPanelEntry = createConfigEntry("Config Panel", {
|
||||
domain: "mqtt",
|
||||
localized_domain_name: "MQTT",
|
||||
});
|
||||
const optionsFlowEntry = createConfigEntry("Options Flow", {
|
||||
supports_options: true,
|
||||
});
|
||||
const setupErrorEntry = createConfigEntry("Setup Error", {
|
||||
state: "setup_error",
|
||||
});
|
||||
const migrationErrorEntry = createConfigEntry("Migration Error", {
|
||||
state: "migration_error",
|
||||
});
|
||||
const setupRetryEntry = createConfigEntry("Setup Retry", {
|
||||
state: "setup_retry",
|
||||
});
|
||||
const setupRetryReasonEntry = createConfigEntry("Setup Retry", {
|
||||
state: "setup_retry",
|
||||
reason: "connection_error",
|
||||
});
|
||||
const setupRetryReasonMissingKeyEntry = createConfigEntry("Setup Retry", {
|
||||
state: "setup_retry",
|
||||
reason: "resolve_error",
|
||||
});
|
||||
const failedUnloadEntry = createConfigEntry("Failed Unload", {
|
||||
state: "failed_unload",
|
||||
});
|
||||
const notLoadedEntry = createConfigEntry("Not Loaded", { state: "not_loaded" });
|
||||
const disabledEntry = createConfigEntry("Disabled", {
|
||||
state: "not_loaded",
|
||||
disabled_by: "user",
|
||||
});
|
||||
const disabledFailedUnloadEntry = createConfigEntry(
|
||||
"Disabled - Failed Unload",
|
||||
{
|
||||
state: "failed_unload",
|
||||
disabled_by: "user",
|
||||
}
|
||||
);
|
||||
|
||||
const configFlows: DataEntryFlowProgressExtended[] = [
|
||||
{
|
||||
flow_id: "adbb401329d8439ebb78ef29837826a8",
|
||||
handler: "roku",
|
||||
context: {
|
||||
source: "ssdp",
|
||||
unique_id: "YF008D862864",
|
||||
title_placeholders: {
|
||||
name: "Living room Roku",
|
||||
},
|
||||
},
|
||||
step_id: "discovery_confirm",
|
||||
localized_title: "Living room Roku",
|
||||
},
|
||||
{
|
||||
flow_id: "adbb401329d8439ebb78ef29837826a8",
|
||||
handler: "hue",
|
||||
context: {
|
||||
source: "reauth",
|
||||
unique_id: "YF008D862864",
|
||||
title_placeholders: {
|
||||
name: "Living room Roku",
|
||||
},
|
||||
},
|
||||
step_id: "discovery_confirm",
|
||||
localized_title: "Philips Hue",
|
||||
},
|
||||
];
|
||||
|
||||
const configEntries: Array<{
|
||||
items: ConfigEntryExtended[];
|
||||
is_custom?: boolean;
|
||||
disabled?: boolean;
|
||||
highlight?: string;
|
||||
}> = [
|
||||
{ items: [loadedEntry] },
|
||||
{ items: [configPanelEntry] },
|
||||
{ items: [optionsFlowEntry] },
|
||||
{ items: [nameAsDomainEntry] },
|
||||
{ items: [longNameEntry] },
|
||||
{ items: [setupErrorEntry] },
|
||||
{ items: [migrationErrorEntry] },
|
||||
{ items: [setupRetryEntry] },
|
||||
{ items: [setupRetryReasonEntry] },
|
||||
{ items: [setupRetryReasonMissingKeyEntry] },
|
||||
{ items: [failedUnloadEntry] },
|
||||
{ items: [notLoadedEntry] },
|
||||
{
|
||||
items: [
|
||||
loadedEntry,
|
||||
setupErrorEntry,
|
||||
migrationErrorEntry,
|
||||
longNameEntry,
|
||||
setupRetryEntry,
|
||||
failedUnloadEntry,
|
||||
notLoadedEntry,
|
||||
disabledEntry,
|
||||
nameAsDomainEntry,
|
||||
configPanelEntry,
|
||||
optionsFlowEntry,
|
||||
],
|
||||
},
|
||||
{ disabled: true, items: [disabledEntry] },
|
||||
{ disabled: true, items: [disabledFailedUnloadEntry] },
|
||||
{
|
||||
disabled: true,
|
||||
items: [disabledEntry, disabledFailedUnloadEntry],
|
||||
},
|
||||
{
|
||||
items: [loadedEntry, configPanelEntry],
|
||||
highlight: "Loaded",
|
||||
},
|
||||
];
|
||||
|
||||
const createEntityRegistryEntries = (
|
||||
item: ConfigEntryExtended
|
||||
): EntityRegistryEntry[] => [
|
||||
{
|
||||
config_entry_id: item.entry_id,
|
||||
device_id: "mock-device-id",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
entity_id: "binary_sensor.updater",
|
||||
name: null,
|
||||
icon: null,
|
||||
platform: "updater",
|
||||
},
|
||||
];
|
||||
|
||||
const createDeviceRegistryEntries = (
|
||||
item: ConfigEntryExtended
|
||||
): DeviceRegistryEntry[] => [
|
||||
{
|
||||
entry_type: null,
|
||||
config_entries: [item.entry_id],
|
||||
connections: [],
|
||||
manufacturer: "ESPHome",
|
||||
model: "Mock Device",
|
||||
name: "Tag Reader",
|
||||
sw_version: null,
|
||||
id: "mock-device-id",
|
||||
identifiers: [],
|
||||
via_device_id: null,
|
||||
area_id: null,
|
||||
name_by_user: null,
|
||||
disabled_by: null,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-integration-card")
|
||||
export class DemoIntegrationCard extends LitElement {
|
||||
@property({ attribute: false }) hass?: HomeAssistant;
|
||||
|
||||
@internalProperty() isCustomIntegration = false;
|
||||
|
||||
@internalProperty() isCloud = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="filters">
|
||||
<ha-formfield label="Custom Integration">
|
||||
<ha-switch @change=${this._toggleCustomIntegration}></ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="Relies on cloud">
|
||||
<ha-switch @change=${this._toggleCloud}></ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
|
||||
<ha-ignored-config-entry-card
|
||||
.hass=${this.hass}
|
||||
.entry=${createConfigEntry("Ignored Entry")}
|
||||
.manifest=${createManifest(this.isCustomIntegration, this.isCloud)}
|
||||
></ha-ignored-config-entry-card>
|
||||
|
||||
${configFlows.map(
|
||||
(flow) => html`
|
||||
<ha-config-flow-card
|
||||
.hass=${this.hass}
|
||||
.flow=${flow}
|
||||
.manifest=${createManifest(
|
||||
this.isCustomIntegration,
|
||||
this.isCloud,
|
||||
flow.handler === "roku" ? "Roku" : "Philips Hue"
|
||||
)}
|
||||
></ha-config-flow-card>
|
||||
`
|
||||
)}
|
||||
${configEntries.map(
|
||||
(info) => html`
|
||||
<ha-integration-card
|
||||
class=${classMap({
|
||||
highlight: info.highlight !== undefined,
|
||||
})}
|
||||
.hass=${this.hass}
|
||||
domain="esphome"
|
||||
.items=${info.items}
|
||||
.manifest=${createManifest(
|
||||
this.isCustomIntegration,
|
||||
this.isCloud
|
||||
)}
|
||||
.entityRegistryEntries=${createEntityRegistryEntries(
|
||||
info.items[0]
|
||||
)}
|
||||
.deviceRegistryEntries=${createDeviceRegistryEntries(
|
||||
info.items[0]
|
||||
)}
|
||||
?disabled=${info.disabled}
|
||||
.selectedConfigEntryId=${info.highlight}
|
||||
></ha-integration-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
<div class="container">
|
||||
<!-- One that is standalone to see how it increases height if height
|
||||
not defined by other cards. -->
|
||||
<ha-integration-card
|
||||
.hass=${this.hass}
|
||||
domain="esphome"
|
||||
.items=${[
|
||||
loadedEntry,
|
||||
setupErrorEntry,
|
||||
migrationErrorEntry,
|
||||
setupRetryEntry,
|
||||
failedUnloadEntry,
|
||||
]}
|
||||
.manifest=${createManifest(this.isCustomIntegration, this.isCloud)}
|
||||
.entityRegistryEntries=${createEntityRegistryEntries(loadedEntry)}
|
||||
.deviceRegistryEntries=${createDeviceRegistryEntries(loadedEntry)}
|
||||
></ha-integration-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("config", "en");
|
||||
// Normally this string is loaded from backend
|
||||
hass.addTranslations(
|
||||
{
|
||||
"component.esphome.config.error.connection_error":
|
||||
"Can't connect to ESP. Please make sure your YAML file contains an 'api:' line.",
|
||||
},
|
||||
"en"
|
||||
);
|
||||
}
|
||||
|
||||
private _toggleCustomIntegration() {
|
||||
this.isCustomIntegration = !this.isCustomIntegration;
|
||||
}
|
||||
|
||||
private _toggleCloud() {
|
||||
this.isCloud = !this.isCloud;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-gap: 16px 16px;
|
||||
padding: 8px 16px 16px;
|
||||
margin-bottom: 64px;
|
||||
}
|
||||
|
||||
.container > * {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
ha-formfield {
|
||||
margin: 8px 0;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-integration-card": DemoIntegrationCard;
|
||||
}
|
||||
}
|
@ -9,13 +9,10 @@ import {
|
||||
} from "lit-element";
|
||||
import "../../../src/components/ha-card";
|
||||
import {
|
||||
SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_COLOR,
|
||||
SUPPORT_COLOR_TEMP,
|
||||
LightColorModes,
|
||||
SUPPORT_EFFECT,
|
||||
SUPPORT_FLASH,
|
||||
SUPPORT_TRANSITION,
|
||||
SUPPORT_WHITE_VALUE,
|
||||
} from "../../../src/data/light";
|
||||
import "../../../src/dialogs/more-info/more-info-content";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
@ -32,7 +29,8 @@ const ENTITIES = [
|
||||
getEntity("light", "kitchen_light", "on", {
|
||||
friendly_name: "Brightness Light",
|
||||
brightness: 200,
|
||||
supported_features: SUPPORT_BRIGHTNESS,
|
||||
supported_color_modes: [LightColorModes.BRIGHTNESS],
|
||||
color_mode: LightColorModes.BRIGHTNESS,
|
||||
}),
|
||||
getEntity("light", "color_temperature_light", "on", {
|
||||
friendly_name: "White Color Temperature Light",
|
||||
@ -40,20 +38,96 @@ const ENTITIES = [
|
||||
color_temp: 75,
|
||||
min_mireds: 30,
|
||||
max_mireds: 150,
|
||||
supported_features: SUPPORT_BRIGHTNESS + SUPPORT_COLOR_TEMP,
|
||||
supported_color_modes: [
|
||||
LightColorModes.BRIGHTNESS,
|
||||
LightColorModes.COLOR_TEMP,
|
||||
],
|
||||
color_mode: LightColorModes.COLOR_TEMP,
|
||||
}),
|
||||
getEntity("light", "color_effectslight", "on", {
|
||||
friendly_name: "Color Effets Light",
|
||||
getEntity("light", "color_hs_light", "on", {
|
||||
friendly_name: "Color HS Light",
|
||||
brightness: 255,
|
||||
hs_color: [30, 100],
|
||||
white_value: 36,
|
||||
supported_features:
|
||||
SUPPORT_BRIGHTNESS +
|
||||
SUPPORT_EFFECT +
|
||||
SUPPORT_FLASH +
|
||||
SUPPORT_COLOR +
|
||||
SUPPORT_TRANSITION +
|
||||
SUPPORT_WHITE_VALUE,
|
||||
rgb_color: [30, 100, 255],
|
||||
min_mireds: 30,
|
||||
max_mireds: 150,
|
||||
supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION,
|
||||
supported_color_modes: [
|
||||
LightColorModes.BRIGHTNESS,
|
||||
LightColorModes.COLOR_TEMP,
|
||||
LightColorModes.HS,
|
||||
],
|
||||
color_mode: LightColorModes.HS,
|
||||
effect_list: ["random", "colorloop"],
|
||||
}),
|
||||
getEntity("light", "color_rgb_ct_light", "on", {
|
||||
friendly_name: "Color RGB + CT Light",
|
||||
brightness: 255,
|
||||
color_temp: 75,
|
||||
min_mireds: 30,
|
||||
max_mireds: 150,
|
||||
supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION,
|
||||
supported_color_modes: [
|
||||
LightColorModes.BRIGHTNESS,
|
||||
LightColorModes.COLOR_TEMP,
|
||||
LightColorModes.RGB,
|
||||
],
|
||||
color_mode: LightColorModes.COLOR_TEMP,
|
||||
effect_list: ["random", "colorloop"],
|
||||
}),
|
||||
getEntity("light", "color_RGB_light", "on", {
|
||||
friendly_name: "Color Effets 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,
|
||||
effect_list: ["random", "colorloop"],
|
||||
}),
|
||||
getEntity("light", "color_rgbw_light", "on", {
|
||||
friendly_name: "Color RGBW Light",
|
||||
brightness: 255,
|
||||
rgbw_color: [30, 100, 255, 125],
|
||||
min_mireds: 30,
|
||||
max_mireds: 150,
|
||||
supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION,
|
||||
supported_color_modes: [
|
||||
LightColorModes.BRIGHTNESS,
|
||||
LightColorModes.COLOR_TEMP,
|
||||
LightColorModes.RGBW,
|
||||
],
|
||||
color_mode: LightColorModes.RGBW,
|
||||
effect_list: ["random", "colorloop"],
|
||||
}),
|
||||
getEntity("light", "color_rgbww_light", "on", {
|
||||
friendly_name: "Color RGBWW Light",
|
||||
brightness: 255,
|
||||
rgbww_color: [30, 100, 255, 125, 10],
|
||||
min_mireds: 30,
|
||||
max_mireds: 150,
|
||||
supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION,
|
||||
supported_color_modes: [
|
||||
LightColorModes.BRIGHTNESS,
|
||||
LightColorModes.COLOR_TEMP,
|
||||
LightColorModes.RGBWW,
|
||||
],
|
||||
color_mode: LightColorModes.RGBWW,
|
||||
effect_list: ["random", "colorloop"],
|
||||
}),
|
||||
getEntity("light", "color_xy_light", "on", {
|
||||
friendly_name: "Color XY Light",
|
||||
brightness: 255,
|
||||
xy_color: [30, 100],
|
||||
rgb_color: [30, 100, 255],
|
||||
min_mireds: 30,
|
||||
max_mireds: 150,
|
||||
supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION,
|
||||
supported_color_modes: [
|
||||
LightColorModes.BRIGHTNESS,
|
||||
LightColorModes.COLOR_TEMP,
|
||||
LightColorModes.XY,
|
||||
],
|
||||
color_mode: LightColorModes.XY,
|
||||
effect_list: ["random", "colorloop"],
|
||||
}),
|
||||
];
|
||||
|
@ -177,8 +177,9 @@ class HassioAddonDashboard extends LitElement {
|
||||
const requestedAddon = extractSearchParam("addon");
|
||||
if (requestedAddon) {
|
||||
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
|
||||
const validAddon = addonsInfo.addons
|
||||
.some((addon) => addon.slug === requestedAddon);
|
||||
const validAddon = addonsInfo.addons.some(
|
||||
(addon) => addon.slug === requestedAddon
|
||||
);
|
||||
if (!validAddon) {
|
||||
this._error = this.supervisor.localize("my.error_addon_not_found");
|
||||
} else {
|
||||
|
@ -242,14 +242,18 @@ class HassioAddonInfo extends LitElement {
|
||||
? html`
|
||||
Current version: ${this.addon.version}
|
||||
<div class="changelog" @click=${this._openChangelog}>
|
||||
(<span class="changelog-link">${
|
||||
this.supervisor.localize("addon.dashboard.changelog")}</span
|
||||
(<span class="changelog-link"
|
||||
>${this.supervisor.localize(
|
||||
"addon.dashboard.changelog"
|
||||
)}</span
|
||||
>)
|
||||
</div>
|
||||
`
|
||||
: html`<span class="changelog-link" @click=${this._openChangelog}>${
|
||||
this.supervisor.localize("addon.dashboard.changelog")
|
||||
}</span>`}
|
||||
: html`<span class="changelog-link" @click=${this._openChangelog}
|
||||
>${this.supervisor.localize(
|
||||
"addon.dashboard.changelog"
|
||||
)}</span
|
||||
>`}
|
||||
</div>
|
||||
|
||||
<div class="description light-color">
|
||||
|
@ -73,7 +73,7 @@ class SupervisorMetric extends LitElement {
|
||||
);
|
||||
}
|
||||
.value {
|
||||
width: 42px;
|
||||
width: 48px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
`;
|
||||
|
@ -44,7 +44,10 @@ export class HassioMain extends SupervisorBaseElement {
|
||||
// We changed the navigate event to fire directly on the window, as that's
|
||||
// where we are listening for it. However, the older panel_custom will
|
||||
// listen on this element for navigation events, so we need to forward them.
|
||||
window.addEventListener("location-changed", (ev) =>
|
||||
|
||||
// Joakim - April 26, 2021
|
||||
// Due to changes in behavior in Google Chrome, we changed navigate to fire on the top element
|
||||
top.addEventListener("location-changed", (ev) =>
|
||||
// @ts-ignore
|
||||
fireEvent(this, ev.type, ev.detail, {
|
||||
bubbles: false,
|
||||
|
@ -269,13 +269,15 @@ class HassioSupervisorInfo extends LitElement {
|
||||
</b>
|
||||
<br /><br />
|
||||
${this.supervisor.localize("system.supervisor.beta_release_items")}
|
||||
<li>Home Assistant Core</li>
|
||||
<li>Home Assistant Supervisor</li>
|
||||
<li>Home Assistant Operating System</li>
|
||||
<ul>
|
||||
<li>Home Assistant Core</li>
|
||||
<li>Home Assistant Supervisor</li>
|
||||
<li>Home Assistant Operating System</li>
|
||||
</ul>
|
||||
<br />
|
||||
${this.supervisor.localize("system.supervisor.join_beta_action")}`,
|
||||
${this.supervisor.localize("system.supervisor.beta_join_confirm")}`,
|
||||
confirmText: this.supervisor.localize(
|
||||
"system.supervisor.beta_join_confirm"
|
||||
"system.supervisor.join_beta_action"
|
||||
),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
});
|
||||
|
21
package.json
21
package.json
@ -25,7 +25,7 @@
|
||||
"@braintree/sanitize-url": "^5.0.0",
|
||||
"@codemirror/commands": "^0.18.0",
|
||||
"@codemirror/gutter": "^0.18.0",
|
||||
"@codemirror/highlight": "^0.18.1",
|
||||
"@codemirror/highlight": "^0.18.0",
|
||||
"@codemirror/history": "^0.18.0",
|
||||
"@codemirror/legacy-modes": "^0.18.0",
|
||||
"@codemirror/rectangular-selection": "^0.18.0",
|
||||
@ -100,7 +100,6 @@
|
||||
"@webcomponents/webcomponentsjs": "^2.2.7",
|
||||
"chart.js": "~2.8.0",
|
||||
"chartjs-chart-timeline": "^0.3.0",
|
||||
"codemirror": "^5.49.0",
|
||||
"comlink": "^4.3.0",
|
||||
"core-js": "^3.6.5",
|
||||
"cropperjs": "^1.5.7",
|
||||
@ -109,7 +108,7 @@
|
||||
"fecha": "^4.2.0",
|
||||
"fuse.js": "^6.0.0",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hls.js": "^0.13.2",
|
||||
"hls.js": "^1.0.1",
|
||||
"home-assistant-js-websocket": "^5.9.0",
|
||||
"idb-keyval": "^3.2.0",
|
||||
"intl-messageformat": "^8.3.9",
|
||||
@ -139,10 +138,12 @@
|
||||
"vue": "^2.6.11",
|
||||
"vue2-daterange-picker": "^0.5.1",
|
||||
"web-animations-js": "^2.3.2",
|
||||
"workbox-core": "^5.1.3",
|
||||
"workbox-precaching": "^5.1.3",
|
||||
"workbox-routing": "^5.1.3",
|
||||
"workbox-strategies": "^5.1.3",
|
||||
"workbox-cacheable-response": "^6.1.5",
|
||||
"workbox-core": "^6.1.5",
|
||||
"workbox-expiration": "^6.1.5",
|
||||
"workbox-precaching": "^6.1.5",
|
||||
"workbox-routing": "^6.1.5",
|
||||
"workbox-strategies": "^6.1.5",
|
||||
"xss": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -167,8 +168,6 @@
|
||||
"@types/chai": "^4.1.7",
|
||||
"@types/chromecast-caf-receiver": "^5.0.11",
|
||||
"@types/chromecast-caf-sender": "^1.0.3",
|
||||
"@types/codemirror": "^0.0.97",
|
||||
"@types/hls.js": "^0.12.3",
|
||||
"@types/js-yaml": "^3.12.1",
|
||||
"@types/leaflet": "^1.4.3",
|
||||
"@types/leaflet-draw": "^1.0.1",
|
||||
@ -228,14 +227,14 @@
|
||||
"terser-webpack-plugin": "^5.1.1",
|
||||
"ts-lit-plugin": "^1.2.1",
|
||||
"ts-mocha": "^7.0.0",
|
||||
"typescript": "^4.0.3",
|
||||
"typescript": "^4.2.4",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"vinyl-source-stream": "^2.0.0",
|
||||
"webpack": "^5.24.1",
|
||||
"webpack-cli": "^4.5.0",
|
||||
"webpack-dev-server": "^3.11.2",
|
||||
"webpack-manifest-plugin": "^3.0.0",
|
||||
"workbox-build": "^5.1.3"
|
||||
"workbox-build": "^6.1.5"
|
||||
},
|
||||
"_comment": "Polymer fixed to 3.1 because 3.2 throws on logbook page",
|
||||
"_comment_2": "Fix in https://github.com/Polymer/polymer/pull/5569",
|
||||
|
2
setup.py
2
setup.py
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20210407.3",
|
||||
version="20210428.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import punycode from "punycode";
|
||||
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
|
||||
import { extractSearchParamsObject } from "../common/url/search-params";
|
||||
import {
|
||||
AuthProvider,
|
||||
@ -116,6 +117,20 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
this._fetchAuthProviders();
|
||||
this._fetchDiscoveryInfo();
|
||||
|
||||
if (matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
applyThemesOnElement(
|
||||
document.documentElement,
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: null,
|
||||
themes: {},
|
||||
darkMode: false,
|
||||
},
|
||||
"default",
|
||||
{ dark: true }
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.redirectUri) {
|
||||
return;
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ export const ensureConnectedCastSession = (cast: CastManager, auth: Auth) => {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
const unsub = cast.addEventListener("connection-changed", () => {
|
||||
if (cast.castConnectedToOurHass) {
|
||||
unsub();
|
||||
|
@ -102,3 +102,18 @@ export const lab2hex = (lab: [number, number, number]): string => {
|
||||
const rgb = lab2rgb(lab);
|
||||
return rgb2hex(rgb);
|
||||
};
|
||||
|
||||
export const rgb2hsv = (
|
||||
rgb: [number, number, number]
|
||||
): [number, number, number] => {
|
||||
const [r, g, b] = rgb;
|
||||
const v = Math.max(r, g, b);
|
||||
const c = v - Math.min(r, g, b);
|
||||
const h =
|
||||
c && (v === r ? (g - b) / c : v === g ? 2 + (b - r) / c : 4 + (r - g) / c);
|
||||
return [60 * (h < 0 ? h + 6 : h), v && c / v, v];
|
||||
};
|
||||
|
||||
export const rgb2hs = (rgb: [number, number, number]): [number, number] => {
|
||||
return rgb2hsv(rgb).slice(0, 2) as [number, number];
|
||||
};
|
||||
|
@ -70,13 +70,18 @@ export const applyThemesOnElement = (
|
||||
themeRules["text-accent-color"] =
|
||||
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
|
||||
}
|
||||
|
||||
// Nothing was changed
|
||||
if (element._themes?.cacheKey === cacheKey) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedTheme && themes.themes[selectedTheme]) {
|
||||
themeRules = themes.themes[selectedTheme];
|
||||
}
|
||||
|
||||
if (!element._themes && !Object.keys(themeRules).length) {
|
||||
if (!element._themes?.keys && !Object.keys(themeRules).length) {
|
||||
// No styles to reset, and no styles to set
|
||||
return;
|
||||
}
|
||||
@ -87,8 +92,8 @@ export const applyThemesOnElement = (
|
||||
: undefined;
|
||||
|
||||
// Add previous set keys to reset them, and new theme
|
||||
const styles = { ...element._themes, ...newTheme?.styles };
|
||||
element._themes = newTheme?.keys;
|
||||
const styles = { ...element._themes?.keys, ...newTheme?.styles };
|
||||
element._themes = { cacheKey, keys: newTheme?.keys };
|
||||
|
||||
// Set and/or reset styles
|
||||
if (element.updateStyles) {
|
||||
|
@ -12,20 +12,24 @@ declare global {
|
||||
export const navigate = (_node: any, path: string, replace = false) => {
|
||||
if (__DEMO__) {
|
||||
if (replace) {
|
||||
history.replaceState(
|
||||
history.state?.root ? { root: true } : null,
|
||||
top.history.replaceState(
|
||||
top.history.state?.root ? { root: true } : null,
|
||||
"",
|
||||
`${location.pathname}#${path}`
|
||||
`${top.location.pathname}#${path}`
|
||||
);
|
||||
} else {
|
||||
window.location.hash = path;
|
||||
top.location.hash = path;
|
||||
}
|
||||
} else if (replace) {
|
||||
history.replaceState(history.state?.root ? { root: true } : null, "", path);
|
||||
top.history.replaceState(
|
||||
top.history.state?.root ? { root: true } : null,
|
||||
"",
|
||||
path
|
||||
);
|
||||
} else {
|
||||
history.pushState(null, "", path);
|
||||
top.history.pushState(null, "", path);
|
||||
}
|
||||
fireEvent(window, "location-changed", {
|
||||
fireEvent(top, "location-changed", {
|
||||
replace,
|
||||
});
|
||||
};
|
||||
|
@ -10,10 +10,13 @@ import { fuzzyScore } from "./filter";
|
||||
* @return {number} Score representing how well the word matches the filter. Return of 0 means no match.
|
||||
*/
|
||||
|
||||
export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {
|
||||
export const fuzzySequentialMatch = (
|
||||
filter: string,
|
||||
item: ScorableTextItem
|
||||
) => {
|
||||
let topScore = Number.NEGATIVE_INFINITY;
|
||||
|
||||
for (const word of words) {
|
||||
for (const word of item.strings) {
|
||||
const scores = fuzzyScore(
|
||||
filter,
|
||||
filter.toLowerCase(),
|
||||
@ -28,13 +31,9 @@ export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The VS Code implementation of filter returns a:
|
||||
// - Negative score for a good match that starts in the middle of the string
|
||||
// - Positive score if the match starts at the beginning of the string
|
||||
// - 0 if the filter string is just barely a match
|
||||
// - undefined for no match
|
||||
// The "0" return is problematic since .filter() will remove that match, even though a 0 == good match.
|
||||
// So, if we encounter a 0 return, set it to 1 so the match will be included, and still respect ordering.
|
||||
// The VS Code implementation of filter returns a 0 for a weak match.
|
||||
// But if .filter() sees a "0", it considers that a failed match and will remove it.
|
||||
// So, we set score to 1 in these cases so the match will be included, and mostly respect correct ordering.
|
||||
const score = scores[0] === 0 ? 1 : scores[0];
|
||||
|
||||
if (score > topScore) {
|
||||
@ -49,10 +48,22 @@ export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {
|
||||
return topScore;
|
||||
};
|
||||
|
||||
/**
|
||||
* An interface that objects must extend in order to use the fuzzy sequence matcher
|
||||
*
|
||||
* @param {number} score - A number representing the existence and strength of a match.
|
||||
* - `< 0` means a good match that starts in the middle of the string
|
||||
* - `> 0` means a good match that starts at the beginning of the string
|
||||
* - `0` means just barely a match
|
||||
* - `undefined` means not a match
|
||||
*
|
||||
* @param {string} strings - Array of strings (aliases) representing the item. The filter string will be compared against each of these for a match.
|
||||
*
|
||||
*/
|
||||
|
||||
export interface ScorableTextItem {
|
||||
score?: number;
|
||||
filterText: string;
|
||||
altText?: string;
|
||||
strings: string[];
|
||||
}
|
||||
|
||||
type FuzzyFilterSort = <T extends ScorableTextItem>(
|
||||
@ -63,9 +74,7 @@ type FuzzyFilterSort = <T extends ScorableTextItem>(
|
||||
export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
|
||||
return items
|
||||
.map((item) => {
|
||||
item.score = item.altText
|
||||
? fuzzySequentialMatch(filter, item.filterText, item.altText)
|
||||
: fuzzySequentialMatch(filter, item.filterText);
|
||||
item.score = fuzzySequentialMatch(filter, item);
|
||||
return item;
|
||||
})
|
||||
.filter((item) => item.score !== undefined)
|
||||
|
@ -58,7 +58,7 @@ export const formatNumber = (
|
||||
).format(Number(num));
|
||||
}
|
||||
}
|
||||
return num ? num.toString() : "";
|
||||
return num.toString();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,4 @@
|
||||
export const afterNextRender = (cb: () => void): void => {
|
||||
export const afterNextRender = (cb: (value: unknown) => void): void => {
|
||||
requestAnimationFrame(() => setTimeout(cb, 0));
|
||||
};
|
||||
|
||||
|
@ -15,6 +15,7 @@ import { computeActiveState } from "../../common/entity/compute_active_state";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { stateIcon } from "../../common/entity/state_icon";
|
||||
import { iconColorCSS } from "../../common/style/icon_color_css";
|
||||
import { getLightRgbColor, LightEntity } from "../../data/light";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-icon";
|
||||
|
||||
@ -99,11 +100,13 @@ export class StateBadge extends LitElement {
|
||||
hostStyle.backgroundImage = `url(${imageUrl})`;
|
||||
this._showIcon = false;
|
||||
} else if (stateObj.state === "on") {
|
||||
if (stateObj.attributes.hs_color && this.stateColor !== false) {
|
||||
const hue = stateObj.attributes.hs_color[0];
|
||||
const sat = stateObj.attributes.hs_color[1];
|
||||
if (sat > 10) {
|
||||
iconStyle.color = `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
|
||||
if (
|
||||
computeStateDomain(stateObj) === "light" &&
|
||||
this.stateColor !== false
|
||||
) {
|
||||
const rgb = getLightRgbColor(stateObj as LightEntity);
|
||||
if (rgb) {
|
||||
iconStyle.color = `rgb(${rgb.slice(0, 3).join(",")})`;
|
||||
}
|
||||
}
|
||||
if (stateObj.attributes.brightness && this.stateColor !== false) {
|
||||
|
@ -6,5 +6,6 @@ export const analyticsLearnMore = (hass: HomeAssistant) => html`<a
|
||||
.href=${documentationUrl(hass, "/integrations/analytics/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${hass.localize("ui.panel.config.core.section.core.analytics.learn_more")}</a
|
||||
>`;
|
||||
>
|
||||
How we process your data
|
||||
</a>`;
|
||||
|
@ -8,7 +8,6 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { Analytics, AnalyticsPreferences } from "../data/analytics";
|
||||
import { haStyle } from "../resources/styles";
|
||||
@ -17,7 +16,18 @@ import "./ha-checkbox";
|
||||
import type { HaCheckbox } from "./ha-checkbox";
|
||||
import "./ha-settings-row";
|
||||
|
||||
const ADDITIONAL_PREFERENCES = ["usage", "statistics"];
|
||||
const ADDITIONAL_PREFERENCES = [
|
||||
{
|
||||
key: "usage",
|
||||
title: "Usage",
|
||||
description: "Details of what you use with Home Assistant",
|
||||
},
|
||||
{
|
||||
key: "statistics",
|
||||
title: "Statistical data",
|
||||
description: "Counts containing total number of datapoints",
|
||||
},
|
||||
];
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@ -48,14 +58,10 @@ export class HaAnalytics extends LitElement {
|
||||
</ha-checkbox>
|
||||
</span>
|
||||
<span slot="heading" data-for="base">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.base.title`
|
||||
)}
|
||||
Basic analytics
|
||||
</span>
|
||||
<span slot="description" data-for="base">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.base.description`
|
||||
)}
|
||||
This includes information about your system.
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
${ADDITIONAL_PREFERENCES.map(
|
||||
@ -64,44 +70,23 @@ export class HaAnalytics extends LitElement {
|
||||
<span slot="prefix">
|
||||
<ha-checkbox
|
||||
@change=${this._handleRowCheckboxClick}
|
||||
.checked=${this.analytics?.preferences[preference]}
|
||||
.preference=${preference}
|
||||
name=${preference}
|
||||
.checked=${this.analytics?.preferences[preference.key]}
|
||||
.preference=${preference.key}
|
||||
name=${preference.key}
|
||||
>
|
||||
</ha-checkbox>
|
||||
${!baseEnabled
|
||||
? html`<paper-tooltip animation-delay="0" position="right"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.analytics.needs_base"
|
||||
)}
|
||||
? html`<paper-tooltip animation-delay="0" position="right">
|
||||
You need to enable basic analytics for this option to be
|
||||
available
|
||||
</paper-tooltip>`
|
||||
: ""}
|
||||
</span>
|
||||
<span slot="heading" data-for=${preference}>
|
||||
${preference === "usage"
|
||||
? isComponentLoaded(this.hass, "hassio")
|
||||
? this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.usage_supervisor.title`
|
||||
)
|
||||
: this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.usage.title`
|
||||
)
|
||||
: this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.${preference}.title`
|
||||
)}
|
||||
<span slot="heading" data-for=${preference.key}>
|
||||
${preference.title}
|
||||
</span>
|
||||
<span slot="description" data-for=${preference}>
|
||||
${preference !== "usage"
|
||||
? this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.${preference}.description`
|
||||
)
|
||||
: isComponentLoaded(this.hass, "hassio")
|
||||
? this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.usage_supervisor.description`
|
||||
)
|
||||
: this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.usage.description`
|
||||
)}
|
||||
<span slot="description" data-for=${preference.key}>
|
||||
${preference.description}
|
||||
</span>
|
||||
</ha-settings-row>`
|
||||
)}
|
||||
@ -117,14 +102,10 @@ export class HaAnalytics extends LitElement {
|
||||
</ha-checkbox>
|
||||
</span>
|
||||
<span slot="heading" data-for="diagnostics">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.diagnostics.title`
|
||||
)}
|
||||
Diagnostics
|
||||
</span>
|
||||
<span slot="description" data-for="diagnostics">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.diagnostics.description`
|
||||
)}
|
||||
Share crash reports when unexpected errors occur.
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
`;
|
||||
@ -161,7 +142,10 @@ export class HaAnalytics extends LitElement {
|
||||
|
||||
preferences[preference] = checkbox.checked;
|
||||
|
||||
if (ADDITIONAL_PREFERENCES.includes(preference) && checkbox.checked) {
|
||||
if (
|
||||
ADDITIONAL_PREFERENCES.some((entry) => entry.key === preference) &&
|
||||
checkbox.checked
|
||||
) {
|
||||
preferences.base = true;
|
||||
} else if (preference === "base" && !checkbox.checked) {
|
||||
preferences.usage = false;
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { ToggleButton } from "../types";
|
||||
import "./ha-svg-icon";
|
||||
@ -19,6 +20,8 @@ export class HaButtonToggleGroup extends LitElement {
|
||||
|
||||
@property() public active?: string;
|
||||
|
||||
@property({ type: Boolean }) public fullWidth = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div>
|
||||
@ -33,6 +36,11 @@ export class HaButtonToggleGroup extends LitElement {
|
||||
<ha-svg-icon .path=${button.iconPath}></ha-svg-icon>
|
||||
</mwc-icon-button>`
|
||||
: html`<mwc-button
|
||||
style=${styleMap({
|
||||
width: this.fullWidth
|
||||
? `${100 / this.buttons.length}%`
|
||||
: "initial",
|
||||
})}
|
||||
.value=${button.value}
|
||||
?active=${this.active === button.value}
|
||||
@click=${this._handleClick}
|
||||
|
@ -2,7 +2,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { EventsMixin } from "../mixins/events-mixin";
|
||||
|
||||
import { rgb2hs } from "../common/color/convert-color";
|
||||
/**
|
||||
* Color-picker custom element
|
||||
*
|
||||
@ -114,6 +114,12 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
observer: "applyHsColor",
|
||||
},
|
||||
|
||||
// use these properties to update the state via attributes
|
||||
desiredRgbColor: {
|
||||
type: Object,
|
||||
observer: "applyRgbColor",
|
||||
},
|
||||
|
||||
// width, height and radius apply to the coordinates of
|
||||
// of the canvas.
|
||||
// border width are relative to these numbers
|
||||
@ -177,8 +183,11 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
this.drawMarker();
|
||||
|
||||
if (this.desiredHsColor) {
|
||||
this.setMarkerOnColor(this.desiredHsColor);
|
||||
this.applyColorToCanvas(this.desiredHsColor);
|
||||
this.applyHsColor(this.desiredHsColor);
|
||||
}
|
||||
|
||||
if (this.desiredRgbColor) {
|
||||
this.applyRgbColor(this.desiredRgbColor);
|
||||
}
|
||||
|
||||
this.interactionLayer.addEventListener("mousedown", (ev) =>
|
||||
@ -282,12 +291,13 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
processUserSelect(ev) {
|
||||
const canvasXY = this.convertToCanvasCoordinates(ev.clientX, ev.clientY);
|
||||
const hs = this.getColor(canvasXY.x, canvasXY.y);
|
||||
this.onColorSelect(hs);
|
||||
const rgb = this.getRgbColor(canvasXY.x, canvasXY.y);
|
||||
this.onColorSelect(hs, rgb);
|
||||
}
|
||||
|
||||
// apply color to marker position and canvas
|
||||
onColorSelect(hs) {
|
||||
this.setMarkerOnColor(hs); // marker always follows mounse 'raw' hs value (= mouse position)
|
||||
onColorSelect(hs, rgb) {
|
||||
this.setMarkerOnColor(hs); // marker always follows mouse 'raw' hs value (= mouse position)
|
||||
if (!this.ignoreSegments) {
|
||||
// apply segments if needed
|
||||
hs = this.applySegmentFilter(hs);
|
||||
@ -301,11 +311,11 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
// eventually after throttle limit has passed
|
||||
clearTimeout(this.ensureFinalSelect);
|
||||
this.ensureFinalSelect = setTimeout(() => {
|
||||
this.fireColorSelected(hs); // do it for the final time
|
||||
this.fireColorSelected(hs, rgb); // do it for the final time
|
||||
}, this.throttle);
|
||||
return;
|
||||
}
|
||||
this.fireColorSelected(hs); // do it
|
||||
this.fireColorSelected(hs, rgb); // do it
|
||||
this.colorSelectIsThrottled = true;
|
||||
setTimeout(() => {
|
||||
this.colorSelectIsThrottled = false;
|
||||
@ -313,9 +323,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
}
|
||||
|
||||
// set color values and fire colorselected event
|
||||
fireColorSelected(hs) {
|
||||
fireColorSelected(hs, rgb) {
|
||||
this.hsColor = hs;
|
||||
this.fire("colorselected", { hs: { h: hs.h, s: hs.s } });
|
||||
this.fire("colorselected", { hs, rgb });
|
||||
}
|
||||
|
||||
/*
|
||||
@ -363,6 +373,11 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
this.applyColorToCanvas(hs);
|
||||
}
|
||||
|
||||
applyRgbColor(rgb) {
|
||||
const [h, s] = rgb2hs(rgb);
|
||||
this.applyHsColor({ h, s });
|
||||
}
|
||||
|
||||
/*
|
||||
* input processing helpers
|
||||
*/
|
||||
@ -395,6 +410,15 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
return { h: hue, s: sat };
|
||||
}
|
||||
|
||||
getRgbColor(x, y) {
|
||||
// get current pixel
|
||||
const imageData = this.backgroundLayer
|
||||
.getContext("2d")
|
||||
.getImageData(x + 250, y + 250, 1, 1);
|
||||
const pixel = imageData.data;
|
||||
return { r: pixel[0], g: pixel[1], b: pixel[2] };
|
||||
}
|
||||
|
||||
applySegmentFilter(hs) {
|
||||
// apply hue segment steps
|
||||
if (this.hueSegments) {
|
||||
@ -468,7 +492,7 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
.getPropertyValue("--wheel-bordercolor")
|
||||
.trim();
|
||||
const wheelShadow = wheelStyle.getPropertyValue("--wheel-shadow").trim();
|
||||
// extract shadow properties from CCS variable
|
||||
// extract shadow properties from CSS variable
|
||||
// the shadow should be defined as: "10px 5px 5px 0px COLOR"
|
||||
if (wheelShadow !== "none") {
|
||||
const values = wheelShadow.split("px ");
|
||||
|
@ -1,3 +1,4 @@
|
||||
import type HlsType from "hls.js";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
@ -15,8 +16,6 @@ import { nextRender } from "../common/util/render-status";
|
||||
import { getExternalConfig } from "../external_app/external_config";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
type HLSModule = typeof import("hls.js");
|
||||
|
||||
@customElement("ha-hls-player")
|
||||
class HaHLSPlayer extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@ -43,7 +42,7 @@ class HaHLSPlayer extends LitElement {
|
||||
|
||||
@internalProperty() private _attached = false;
|
||||
|
||||
private _hlsPolyfillInstance?: Hls;
|
||||
private _hlsPolyfillInstance?: HlsType;
|
||||
|
||||
private _useExoPlayer = false;
|
||||
|
||||
@ -107,8 +106,8 @@ class HaHLSPlayer extends LitElement {
|
||||
const useExoPlayerPromise = this._getUseExoPlayer();
|
||||
const masterPlaylistPromise = fetch(this.url);
|
||||
|
||||
const hls = ((await import("hls.js")) as any).default as HLSModule;
|
||||
let hlsSupported = hls.isSupported();
|
||||
const Hls = (await import("hls.js")).default;
|
||||
let hlsSupported = Hls.isSupported();
|
||||
|
||||
if (!hlsSupported) {
|
||||
hlsSupported =
|
||||
@ -144,8 +143,8 @@ class HaHLSPlayer extends LitElement {
|
||||
// If codec is HEVC and ExoPlayer is supported, use ExoPlayer.
|
||||
if (this._useExoPlayer && match !== null && match[1] !== undefined) {
|
||||
this._renderHLSExoPlayer(playlist_url);
|
||||
} else if (hls.isSupported()) {
|
||||
this._renderHLSPolyfill(videoEl, hls, playlist_url);
|
||||
} else if (Hls.isSupported()) {
|
||||
this._renderHLSPolyfill(videoEl, Hls, playlist_url);
|
||||
} else {
|
||||
this._renderHLSNative(videoEl, playlist_url);
|
||||
}
|
||||
@ -182,7 +181,7 @@ class HaHLSPlayer extends LitElement {
|
||||
|
||||
private async _renderHLSPolyfill(
|
||||
videoEl: HTMLVideoElement,
|
||||
Hls: HLSModule,
|
||||
Hls: typeof HlsType,
|
||||
url: string
|
||||
) {
|
||||
const hls = new Hls({
|
||||
|
@ -1,12 +1,9 @@
|
||||
import { customElement, html, LitElement, property } from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { TimeSelector } from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../paper-time-input";
|
||||
|
||||
const test = new Date().toLocaleString();
|
||||
const useAMPM = test.includes("AM") || test.includes("PM");
|
||||
|
||||
@customElement("ha-selector-time")
|
||||
export class HaTimeSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@ -19,16 +16,24 @@ export class HaTimeSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
private _useAmPm = memoizeOne((language: string) => {
|
||||
const test = new Date().toLocaleString(language);
|
||||
return test.includes("AM") || test.includes("PM");
|
||||
});
|
||||
|
||||
protected render() {
|
||||
const useAMPM = this._useAmPm(this.hass.locale.language);
|
||||
|
||||
const parts = this.value?.split(":") || [];
|
||||
const hours = useAMPM ? parts[0] ?? "12" : parts[0] ?? "0";
|
||||
const hours = parts[0];
|
||||
|
||||
return html`
|
||||
<paper-time-input
|
||||
.label=${this.label}
|
||||
.hour=${useAMPM && Number(hours) > 12 ? Number(hours) - 12 : hours}
|
||||
.min=${parts[1] ?? "00"}
|
||||
.sec=${parts[2] ?? "00"}
|
||||
.hour=${hours &&
|
||||
(useAMPM && Number(hours) > 12 ? Number(hours) - 12 : hours)}
|
||||
.min=${parts[1]}
|
||||
.sec=${parts[2]}
|
||||
.format=${useAMPM ? 12 : 24}
|
||||
.amPm=${useAMPM && (Number(hours) > 12 ? "PM" : "AM")}
|
||||
.disabled=${this.disabled}
|
||||
@ -42,12 +47,16 @@ export class HaTimeSelector extends LitElement {
|
||||
|
||||
private _timeChanged(ev) {
|
||||
let value = ev.target.value;
|
||||
if (useAMPM) {
|
||||
let hours = Number(ev.target.hour);
|
||||
const useAMPM = this._useAmPm(this.hass.locale.language);
|
||||
let hours = Number(ev.target.hour || 0);
|
||||
if (value && useAMPM) {
|
||||
if (ev.target.amPm === "PM") {
|
||||
hours += 12;
|
||||
}
|
||||
value = `${hours}:${ev.target.min}:${ev.target.sec}`;
|
||||
value = `${hours}:${ev.target.min || "00"}:${ev.target.sec || "00"}`;
|
||||
}
|
||||
if (value === this.value) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import { HassService, HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
@ -18,11 +19,12 @@ import { ENTITY_COMPONENT_DOMAINS } from "../data/entity";
|
||||
import { Selector } from "../data/selector";
|
||||
import { PolymerChangedEvent } from "../polymer-types";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import "./ha-checkbox";
|
||||
import "./ha-selector/ha-selector";
|
||||
import "./ha-service-picker";
|
||||
import "./ha-settings-row";
|
||||
import "./ha-yaml-editor";
|
||||
import "./ha-checkbox";
|
||||
import type { HaYamlEditor } from "./ha-yaml-editor";
|
||||
|
||||
interface ExtHassService extends Omit<HassService, "fields"> {
|
||||
@ -49,6 +51,8 @@ export class HaServiceControl extends LitElement {
|
||||
data?: Record<string, any>;
|
||||
};
|
||||
|
||||
@internalProperty() private _value!: this["value"];
|
||||
|
||||
@property({ reflect: true, type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ type: Boolean }) public showAdvanced?: boolean;
|
||||
@ -57,7 +61,7 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
protected updated(changedProperties: PropertyValues<this>) {
|
||||
if (!changedProperties.has("value")) {
|
||||
return;
|
||||
}
|
||||
@ -92,21 +96,23 @@ export class HaServiceControl extends LitElement {
|
||||
target.device_id = this.value.data.device_id;
|
||||
}
|
||||
|
||||
this.value = {
|
||||
this._value = {
|
||||
...this.value,
|
||||
target,
|
||||
data: { ...this.value.data },
|
||||
};
|
||||
|
||||
delete this.value.data!.entity_id;
|
||||
delete this.value.data!.device_id;
|
||||
delete this.value.data!.area_id;
|
||||
delete this._value.data!.entity_id;
|
||||
delete this._value.data!.device_id;
|
||||
delete this._value.data!.area_id;
|
||||
} else {
|
||||
this._value = this.value;
|
||||
}
|
||||
|
||||
if (this.value?.data) {
|
||||
if (this._value?.data) {
|
||||
const yamlEditor = this._yamlEditor;
|
||||
if (yamlEditor && yamlEditor.value !== this.value.data) {
|
||||
yamlEditor.setValue(this.value.data);
|
||||
if (yamlEditor && yamlEditor.value !== this._value.data) {
|
||||
yamlEditor.setValue(this._value.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -151,12 +157,12 @@ export class HaServiceControl extends LitElement {
|
||||
});
|
||||
|
||||
protected render() {
|
||||
const serviceData = this._getServiceInfo(this.value?.service);
|
||||
const serviceData = this._getServiceInfo(this._value?.service);
|
||||
|
||||
const shouldRenderServiceDataYaml =
|
||||
(serviceData?.fields.length && !serviceData.hasSelector.length) ||
|
||||
(serviceData &&
|
||||
Object.keys(this.value?.data || {}).some(
|
||||
Object.keys(this._value?.data || {}).some(
|
||||
(key) => !serviceData!.hasSelector.includes(key)
|
||||
));
|
||||
|
||||
@ -171,10 +177,32 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
return html`<ha-service-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value?.service}
|
||||
.value=${this._value?.service}
|
||||
@value-changed=${this._serviceChanged}
|
||||
></ha-service-picker>
|
||||
<p>${serviceData?.description}</p>
|
||||
<div class="description">
|
||||
<p>${serviceData?.description}</p>
|
||||
${this.value?.service
|
||||
? html` <a
|
||||
href="${documentationUrl(
|
||||
this.hass,
|
||||
"/integrations/" + computeDomain(this.value?.service)
|
||||
)}"
|
||||
title="${this.hass.localize(
|
||||
"ui.components.service-control.integration_doc"
|
||||
)}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<mwc-icon-button>
|
||||
<ha-svg-icon
|
||||
path=${mdiHelpCircle}
|
||||
class="help-icon"
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</a>`
|
||||
: ""}
|
||||
</div>
|
||||
${serviceData && "target" in serviceData
|
||||
? html`<ha-settings-row .narrow=${this.narrow}>
|
||||
${hasOptional
|
||||
@ -195,19 +223,19 @@ export class HaServiceControl extends LitElement {
|
||||
? { target: serviceData.target }
|
||||
: {
|
||||
target: {
|
||||
entity: { domain: computeDomain(this.value!.service) },
|
||||
entity: { domain: computeDomain(this._value!.service) },
|
||||
},
|
||||
}}
|
||||
@value-changed=${this._targetChanged}
|
||||
.value=${this.value?.target}
|
||||
.value=${this._value?.target}
|
||||
></ha-selector
|
||||
></ha-settings-row>`
|
||||
: entityId
|
||||
? html`<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value?.data?.entity_id}
|
||||
.value=${this._value?.data?.entity_id}
|
||||
.label=${entityId.description}
|
||||
.includeDomains=${this._domainFilter(this.value!.service)}
|
||||
.includeDomains=${this._domainFilter(this._value!.service)}
|
||||
@value-changed=${this._entityPicked}
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>`
|
||||
@ -218,15 +246,15 @@ export class HaServiceControl extends LitElement {
|
||||
"ui.components.service-control.service_data"
|
||||
)}
|
||||
.name=${"data"}
|
||||
.defaultValue=${this.value?.data}
|
||||
.defaultValue=${this._value?.data}
|
||||
@value-changed=${this._dataChanged}
|
||||
></ha-yaml-editor>`
|
||||
: serviceData?.fields.map((dataField) =>
|
||||
dataField.selector &&
|
||||
(!dataField.advanced ||
|
||||
this.showAdvanced ||
|
||||
(this.value?.data &&
|
||||
this.value.data[dataField.key] !== undefined))
|
||||
(this._value?.data &&
|
||||
this._value.data[dataField.key] !== undefined))
|
||||
? html`<ha-settings-row .narrow=${this.narrow}>
|
||||
${dataField.required
|
||||
? hasOptional
|
||||
@ -235,8 +263,8 @@ export class HaServiceControl extends LitElement {
|
||||
: html`<ha-checkbox
|
||||
.key=${dataField.key}
|
||||
.checked=${this._checkedKeys.has(dataField.key) ||
|
||||
(this.value?.data &&
|
||||
this.value.data[dataField.key] !== undefined)}
|
||||
(this._value?.data &&
|
||||
this._value.data[dataField.key] !== undefined)}
|
||||
@change=${this._checkboxChanged}
|
||||
slot="prefix"
|
||||
></ha-checkbox>`}
|
||||
@ -245,15 +273,15 @@ export class HaServiceControl extends LitElement {
|
||||
><ha-selector
|
||||
.disabled=${!dataField.required &&
|
||||
!this._checkedKeys.has(dataField.key) &&
|
||||
(!this.value?.data ||
|
||||
this.value.data[dataField.key] === undefined)}
|
||||
(!this._value?.data ||
|
||||
this._value.data[dataField.key] === undefined)}
|
||||
.hass=${this.hass}
|
||||
.selector=${dataField.selector}
|
||||
.key=${dataField.key}
|
||||
@value-changed=${this._serviceDataChanged}
|
||||
.value=${this.value?.data &&
|
||||
this.value.data[dataField.key] !== undefined
|
||||
? this.value.data[dataField.key]
|
||||
.value=${this._value?.data &&
|
||||
this._value.data[dataField.key] !== undefined
|
||||
? this._value.data[dataField.key]
|
||||
: dataField.default}
|
||||
></ha-selector
|
||||
></ha-settings-row>`
|
||||
@ -268,13 +296,13 @@ export class HaServiceControl extends LitElement {
|
||||
this._checkedKeys.add(key);
|
||||
} else {
|
||||
this._checkedKeys.delete(key);
|
||||
const data = { ...this.value?.data };
|
||||
const data = { ...this._value?.data };
|
||||
|
||||
delete data[key];
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.value,
|
||||
...this._value,
|
||||
data,
|
||||
},
|
||||
});
|
||||
@ -284,7 +312,7 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
private _serviceChanged(ev: PolymerChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
if (ev.detail.value === this.value?.service) {
|
||||
if (ev.detail.value === this._value?.service) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
@ -295,17 +323,17 @@ export class HaServiceControl extends LitElement {
|
||||
private _entityPicked(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
if (this.value?.data?.entity_id === newValue) {
|
||||
if (this._value?.data?.entity_id === newValue) {
|
||||
return;
|
||||
}
|
||||
let value;
|
||||
if (!newValue && this.value?.data) {
|
||||
value = { ...this.value };
|
||||
if (!newValue && this._value?.data) {
|
||||
value = { ...this._value };
|
||||
delete value.data.entity_id;
|
||||
} else {
|
||||
value = {
|
||||
...this.value,
|
||||
data: { ...this.value?.data, entity_id: ev.detail.value },
|
||||
...this._value,
|
||||
data: { ...this._value?.data, entity_id: ev.detail.value },
|
||||
};
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
@ -316,15 +344,15 @@ export class HaServiceControl extends LitElement {
|
||||
private _targetChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
if (this.value?.target === newValue) {
|
||||
if (this._value?.target === newValue) {
|
||||
return;
|
||||
}
|
||||
let value;
|
||||
if (!newValue) {
|
||||
value = { ...this.value };
|
||||
value = { ...this._value };
|
||||
delete value.target;
|
||||
} else {
|
||||
value = { ...this.value, target: ev.detail.value };
|
||||
value = { ...this._value, target: ev.detail.value };
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
@ -336,13 +364,13 @@ export class HaServiceControl extends LitElement {
|
||||
const key = (ev.currentTarget as any).key;
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
this.value?.data?.[key] === value ||
|
||||
(!this.value?.data?.[key] && (value === "" || value === undefined))
|
||||
this._value?.data?.[key] === value ||
|
||||
(!this._value?.data?.[key] && (value === "" || value === undefined))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = { ...this.value?.data, [key]: value };
|
||||
const data = { ...this._value?.data, [key]: value };
|
||||
|
||||
if (value === "" || value === undefined) {
|
||||
delete data[key];
|
||||
@ -350,7 +378,7 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.value,
|
||||
...this._value,
|
||||
data,
|
||||
},
|
||||
});
|
||||
@ -363,7 +391,7 @@ export class HaServiceControl extends LitElement {
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.value,
|
||||
...this._value,
|
||||
data: ev.detail.value,
|
||||
},
|
||||
});
|
||||
@ -406,6 +434,15 @@ export class HaServiceControl extends LitElement {
|
||||
ha-checkbox {
|
||||
margin-left: -16px;
|
||||
}
|
||||
.help-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.description {
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 2px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -133,7 +133,7 @@ export class PaperTimeInput extends PolymerElement {
|
||||
always-float-label$="[[alwaysFloatInputLabels]]"
|
||||
disabled="[[disabled]]"
|
||||
>
|
||||
<span suffix="" slot="suffix">:</span>
|
||||
<span suffix slot="suffix">:</span>
|
||||
</paper-input>
|
||||
|
||||
<!-- Min Input -->
|
||||
@ -303,28 +303,28 @@ export class PaperTimeInput extends PolymerElement {
|
||||
notify: true,
|
||||
},
|
||||
/**
|
||||
* Suffix for the hour input
|
||||
* Label for the hour input
|
||||
*/
|
||||
hourLabel: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
/**
|
||||
* Suffix for the min input
|
||||
* Label for the min input
|
||||
*/
|
||||
minLabel: {
|
||||
type: String,
|
||||
value: ":",
|
||||
value: "",
|
||||
},
|
||||
/**
|
||||
* Suffix for the sec input
|
||||
* Label for the sec input
|
||||
*/
|
||||
secLabel: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
/**
|
||||
* Suffix for the milli sec input
|
||||
* Label for the milli sec input
|
||||
*/
|
||||
millisecLabel: {
|
||||
type: String,
|
||||
|
@ -314,16 +314,18 @@ class ActionRenderer {
|
||||
|
||||
if (defaultExecuted) {
|
||||
this._renderEntry(choosePath, `${name}: Default action executed`);
|
||||
} else {
|
||||
} else if (chooseTrace.result) {
|
||||
const choiceConfig = this._getDataFromPath(
|
||||
`${this.keys[index]}/choose/${chooseTrace.result?.choice}`
|
||||
`${this.keys[index]}/choose/${chooseTrace.result.choice}`
|
||||
) as ChooseActionChoice | undefined;
|
||||
const choiceName = choiceConfig
|
||||
? `${
|
||||
choiceConfig.alias || `Choice ${chooseTrace.result?.choice}`
|
||||
choiceConfig.alias || `Choice ${chooseTrace.result.choice}`
|
||||
} executed`
|
||||
: `Error: ${chooseTrace.error}`;
|
||||
this._renderEntry(choosePath, `${name}: ${choiceName}`);
|
||||
} else {
|
||||
this._renderEntry(choosePath, `${name}: No action taken`);
|
||||
}
|
||||
|
||||
let i;
|
||||
|
16
src/data/bootstrap_integrations.ts
Normal file
16
src/data/bootstrap_integrations.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export type BootstrapIntegrationsTimings = { [key: string]: number };
|
||||
|
||||
export const subscribeBootstrapIntegrations = (
|
||||
hass: HomeAssistant,
|
||||
callback: (message: BootstrapIntegrationsTimings) => void
|
||||
) => {
|
||||
const unsubProm = hass.connection.subscribeMessage<
|
||||
BootstrapIntegrationsTimings
|
||||
>((message) => callback(message), {
|
||||
type: "subscribe_bootstrap_integrations",
|
||||
});
|
||||
|
||||
return unsubProm;
|
||||
};
|
@ -5,11 +5,18 @@ export interface ConfigEntry {
|
||||
domain: string;
|
||||
title: string;
|
||||
source: string;
|
||||
state: string;
|
||||
state:
|
||||
| "loaded"
|
||||
| "setup_error"
|
||||
| "migration_error"
|
||||
| "setup_retry"
|
||||
| "not_loaded"
|
||||
| "failed_unload";
|
||||
connection_class: string;
|
||||
supports_options: boolean;
|
||||
supports_unload: boolean;
|
||||
disabled_by: string | null;
|
||||
disabled_by: "user" | null;
|
||||
reason: string | null;
|
||||
}
|
||||
|
||||
export interface ConfigEntryMutableParams {
|
||||
|
@ -28,6 +28,7 @@ export interface DataEntryFlowStepForm {
|
||||
data_schema: HaFormSchema[];
|
||||
errors: Record<string, string>;
|
||||
description_placeholders: Record<string, string>;
|
||||
last_step: boolean | null;
|
||||
}
|
||||
|
||||
export interface DataEntryFlowStepExternal {
|
||||
|
@ -9,13 +9,13 @@ export interface DeviceRegistryEntry {
|
||||
config_entries: string[];
|
||||
connections: Array<[string, string]>;
|
||||
identifiers: Array<[string, string]>;
|
||||
manufacturer: string;
|
||||
model?: string;
|
||||
name?: string;
|
||||
sw_version?: string;
|
||||
via_device_id?: string;
|
||||
area_id?: string;
|
||||
name_by_user?: string;
|
||||
manufacturer: string | null;
|
||||
model: string | null;
|
||||
name: string | null;
|
||||
sw_version: string | null;
|
||||
via_device_id: string | null;
|
||||
area_id: string | null;
|
||||
name_by_user: string | null;
|
||||
entry_type: "service" | null;
|
||||
disabled_by: string | null;
|
||||
}
|
||||
|
@ -5,12 +5,12 @@ import { HomeAssistant } from "../types";
|
||||
|
||||
export interface EntityRegistryEntry {
|
||||
entity_id: string;
|
||||
name: string;
|
||||
icon?: string;
|
||||
name: string | null;
|
||||
icon: string | null;
|
||||
platform: string;
|
||||
config_entry_id?: string;
|
||||
device_id?: string;
|
||||
area_id?: string;
|
||||
config_entry_id: string | null;
|
||||
device_id: string | null;
|
||||
area_id: string | null;
|
||||
disabled_by: string | null;
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,18 @@ export interface IntegrationManifest {
|
||||
ssdp?: Array<{ manufacturer?: string; modelName?: string; st?: string }>;
|
||||
zeroconf?: string[];
|
||||
homekit?: { models: string[] };
|
||||
quality_scale?: string;
|
||||
quality_scale?: "gold" | "internal" | "platinum" | "silver";
|
||||
iot_class:
|
||||
| "assumed_state"
|
||||
| "cloud_polling"
|
||||
| "cloud_push"
|
||||
| "local_polling"
|
||||
| "local_push";
|
||||
}
|
||||
|
||||
export interface IntegrationSetup {
|
||||
domain: string;
|
||||
seconds?: number;
|
||||
}
|
||||
|
||||
export const integrationIssuesUrl = (
|
||||
@ -38,3 +49,6 @@ export const fetchIntegrationManifest = (
|
||||
hass: HomeAssistant,
|
||||
integration: string
|
||||
) => hass.callWS<IntegrationManifest>({ type: "manifest/get", integration });
|
||||
|
||||
export const fetchIntegrationSetups = (hass: HomeAssistant) =>
|
||||
hass.callWS<IntegrationSetup[]>({ type: "integration/setup_info" });
|
||||
|
@ -3,26 +3,82 @@ import {
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
|
||||
export enum LightColorModes {
|
||||
UNKNOWN = "unknown",
|
||||
ONOFF = "onoff",
|
||||
BRIGHTNESS = "brightness",
|
||||
COLOR_TEMP = "color_temp",
|
||||
HS = "hs",
|
||||
XY = "xy",
|
||||
RGB = "rgb",
|
||||
RGBW = "rgbw",
|
||||
RGBWW = "rgbww",
|
||||
}
|
||||
|
||||
const modesSupportingColor = [
|
||||
LightColorModes.HS,
|
||||
LightColorModes.XY,
|
||||
LightColorModes.RGB,
|
||||
LightColorModes.RGBW,
|
||||
LightColorModes.RGBWW,
|
||||
];
|
||||
|
||||
const modesSupportingDimming = [
|
||||
...modesSupportingColor,
|
||||
LightColorModes.COLOR_TEMP,
|
||||
LightColorModes.BRIGHTNESS,
|
||||
];
|
||||
|
||||
export const SUPPORT_EFFECT = 4;
|
||||
export const SUPPORT_FLASH = 8;
|
||||
export const SUPPORT_TRANSITION = 32;
|
||||
|
||||
export const lightSupportsColorMode = (
|
||||
entity: LightEntity,
|
||||
mode: LightColorModes
|
||||
) => {
|
||||
return entity.attributes.supported_color_modes?.includes(mode);
|
||||
};
|
||||
|
||||
export const lightIsInColorMode = (entity: LightEntity) => {
|
||||
return modesSupportingColor.includes(entity.attributes.color_mode);
|
||||
};
|
||||
|
||||
export const lightSupportsColor = (entity: LightEntity) => {
|
||||
return entity.attributes.supported_color_modes?.some((mode) =>
|
||||
modesSupportingColor.includes(mode)
|
||||
);
|
||||
};
|
||||
|
||||
export const lightSupportsDimming = (entity: LightEntity) => {
|
||||
return entity.attributes.supported_color_modes?.some((mode) =>
|
||||
modesSupportingDimming.includes(mode)
|
||||
);
|
||||
};
|
||||
|
||||
export const getLightRgbColor = (entity: LightEntity): number[] | undefined =>
|
||||
entity.attributes.color_mode === LightColorModes.RGBWW
|
||||
? entity.attributes.rgbww_color
|
||||
: entity.attributes.color_mode === LightColorModes.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[];
|
||||
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;
|
||||
white_value: number;
|
||||
effect?: string;
|
||||
effect_list: string[] | null;
|
||||
supported_color_modes: LightColorModes[];
|
||||
color_mode: LightColorModes;
|
||||
}
|
||||
|
||||
export interface LightEntity extends HassEntityBase {
|
||||
attributes: LightEntityAttributes;
|
||||
}
|
||||
|
||||
export const SUPPORT_BRIGHTNESS = 1;
|
||||
export const SUPPORT_COLOR_TEMP = 2;
|
||||
export const SUPPORT_EFFECT = 4;
|
||||
export const SUPPORT_FLASH = 8;
|
||||
export const SUPPORT_COLOR = 16;
|
||||
export const SUPPORT_TRANSITION = 32;
|
||||
export const SUPPORT_WHITE_VALUE = 128;
|
||||
|
@ -19,6 +19,10 @@ export interface LovelacePanelConfig {
|
||||
|
||||
export interface LovelaceConfig {
|
||||
title?: string;
|
||||
strategy?: {
|
||||
type: string;
|
||||
options?: Record<string, unknown>;
|
||||
};
|
||||
views: LovelaceViewConfig[];
|
||||
background?: string;
|
||||
}
|
||||
@ -77,6 +81,10 @@ export interface LovelaceViewConfig {
|
||||
index?: number;
|
||||
title?: string;
|
||||
type?: string;
|
||||
strategy?: {
|
||||
type: string;
|
||||
options?: Record<string, unknown>;
|
||||
};
|
||||
badges?: Array<string | LovelaceBadgeConfig>;
|
||||
cards?: LovelaceCardConfig[];
|
||||
path?: string;
|
||||
@ -94,6 +102,7 @@ export interface LovelaceViewElement extends HTMLElement {
|
||||
index?: number;
|
||||
cards?: Array<LovelaceCard | HuiErrorCard>;
|
||||
badges?: LovelaceBadge[];
|
||||
isStrategy: boolean;
|
||||
setConfig(config: LovelaceViewConfig): void;
|
||||
}
|
||||
|
||||
|
@ -292,9 +292,11 @@ export const computeMediaControls = (
|
||||
? "hass:pause"
|
||||
: "hass:stop",
|
||||
action:
|
||||
state === "playing" && !supportsFeature(stateObj, SUPPORT_PAUSE)
|
||||
? "media_stop"
|
||||
: "media_play_pause",
|
||||
state !== "playing"
|
||||
? "media_play"
|
||||
: supportsFeature(stateObj, SUPPORT_PAUSE)
|
||||
? "media_pause"
|
||||
: "media_stop",
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -6,3 +6,6 @@ export const callExecuteScript = (hass: HomeAssistant, sequence: Action[]) =>
|
||||
type: "execute_script",
|
||||
sequence,
|
||||
});
|
||||
|
||||
export const serviceCallWillDisconnect = (domain: string, service: string) =>
|
||||
domain === "homeassistant" && ["restart", "stop"].includes(service);
|
||||
|
@ -16,9 +16,27 @@ export interface LoggedError {
|
||||
export const fetchSystemLog = (hass: HomeAssistant) =>
|
||||
hass.callApi<LoggedError[]>("GET", "error/all");
|
||||
|
||||
export const getLoggedErrorIntegration = (item: LoggedError) =>
|
||||
item.name.startsWith("homeassistant.components.")
|
||||
? item.name.split(".")[2]
|
||||
: item.name.startsWith("custom_components.")
|
||||
? item.name.split(".")[1]
|
||||
: undefined;
|
||||
export const getLoggedErrorIntegration = (item: LoggedError) => {
|
||||
// Try to derive from logger name
|
||||
if (item.name.startsWith("homeassistant.components.")) {
|
||||
return item.name.split(".")[2];
|
||||
}
|
||||
if (item.name.startsWith("custom_components.")) {
|
||||
return item.name.split(".")[1];
|
||||
}
|
||||
|
||||
// Try to derive from logged location
|
||||
if (item.source[0].startsWith("custom_components/")) {
|
||||
return item.source[0].split("/")[1];
|
||||
}
|
||||
|
||||
if (item.source[0].startsWith("homeassistant/components/")) {
|
||||
return item.source[0].split("/")[2];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const isCustomIntegrationError = (item: LoggedError) =>
|
||||
item.name.startsWith("custom_components.") ||
|
||||
item.source[0].startsWith("custom_components/");
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { HaFormSchema } from "../components/ha-form/ha-form";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface ZHAEntityReference extends HassEntity {
|
||||
@ -54,6 +55,52 @@ export interface Cluster {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface ClusterConfigurationData {
|
||||
cluster_name: string;
|
||||
cluster_id: number;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface ClusterAttributeData {
|
||||
cluster_name: string;
|
||||
cluster_id: number;
|
||||
attributes: AttributeConfigurationStatus[];
|
||||
}
|
||||
|
||||
export interface AttributeConfigurationStatus {
|
||||
id: number;
|
||||
name: string;
|
||||
success: boolean | undefined;
|
||||
min: number;
|
||||
max: number;
|
||||
change: number;
|
||||
}
|
||||
|
||||
export interface ClusterConfigurationStatus {
|
||||
cluster: Cluster;
|
||||
bindSuccess: boolean | undefined;
|
||||
attributes: Map<number, AttributeConfigurationStatus>;
|
||||
}
|
||||
|
||||
interface ClusterConfigurationBindEvent {
|
||||
type: "zha_channel_bind";
|
||||
zha_channel_msg_data: ClusterConfigurationData;
|
||||
}
|
||||
|
||||
interface ClusterConfigurationReportConfigurationEvent {
|
||||
type: "zha_channel_configure_reporting";
|
||||
zha_channel_msg_data: ClusterAttributeData;
|
||||
}
|
||||
|
||||
interface ClusterConfigurationEventFinish {
|
||||
type: "zha_channel_cfg_done";
|
||||
}
|
||||
|
||||
export type ClusterConfigurationEvent =
|
||||
| ClusterConfigurationReportConfigurationEvent
|
||||
| ClusterConfigurationBindEvent
|
||||
| ClusterConfigurationEventFinish;
|
||||
|
||||
export interface Command {
|
||||
name: string;
|
||||
id: number;
|
||||
@ -75,6 +122,11 @@ export interface ZHAGroup {
|
||||
members: ZHADeviceEndpoint[];
|
||||
}
|
||||
|
||||
export interface ZHAConfiguration {
|
||||
data: Record<string, Record<string, unknown>>;
|
||||
schemas: Record<string, HaFormSchema[]>;
|
||||
}
|
||||
|
||||
export interface ZHAGroupMember {
|
||||
ieee: string;
|
||||
endpoint_id: string;
|
||||
@ -83,10 +135,10 @@ export interface ZHAGroupMember {
|
||||
export const reconfigureNode = (
|
||||
hass: HomeAssistant,
|
||||
ieeeAddress: string,
|
||||
callbackFunction: any
|
||||
callbackFunction: (message: ClusterConfigurationEvent) => void
|
||||
) => {
|
||||
return hass.connection.subscribeMessage(
|
||||
(message) => callbackFunction(message),
|
||||
(message: ClusterConfigurationEvent) => callbackFunction(message),
|
||||
{
|
||||
type: "zha/devices/reconfigure",
|
||||
ieee: ieeeAddress,
|
||||
@ -282,6 +334,22 @@ export const addGroup = (
|
||||
members: membersToAdd,
|
||||
});
|
||||
|
||||
export const fetchZHAConfiguration = (
|
||||
hass: HomeAssistant
|
||||
): Promise<ZHAConfiguration> =>
|
||||
hass.callWS({
|
||||
type: "zha/configuration",
|
||||
});
|
||||
|
||||
export const updateZHAConfiguration = (
|
||||
hass: HomeAssistant,
|
||||
data: any
|
||||
): Promise<any> =>
|
||||
hass.callWS({
|
||||
type: "zha/configuration/update",
|
||||
data: data,
|
||||
});
|
||||
|
||||
export const INITIALIZED = "INITIALIZED";
|
||||
export const INTERVIEW_COMPLETE = "INTERVIEW_COMPLETE";
|
||||
export const CONFIGURED = "CONFIGURED";
|
||||
@ -301,3 +369,7 @@ export const DEVICE_MESSAGE_TYPES = [
|
||||
DEVICE_FULLY_INITIALIZED,
|
||||
];
|
||||
export const LOG_OUTPUT = "log_output";
|
||||
export const ZHA_CHANNEL_MSG = "zha_channel_message";
|
||||
export const ZHA_CHANNEL_MSG_BIND = "zha_channel_bind";
|
||||
export const ZHA_CHANNEL_MSG_CFG_RPT = "zha_channel_configure_reporting";
|
||||
export const ZHA_CHANNEL_CFG_DONE = "zha_channel_cfg_done";
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { DeviceRegistryEntry } from "./device_registry";
|
||||
|
||||
@ -29,6 +30,10 @@ export interface ZWaveJSNode {
|
||||
}
|
||||
|
||||
export interface ZWaveJSNodeConfigParams {
|
||||
[key: string]: ZWaveJSNodeConfigParam;
|
||||
}
|
||||
|
||||
export interface ZWaveJSNodeConfigParam {
|
||||
property: number;
|
||||
value: any;
|
||||
configuration_value_type: string;
|
||||
@ -56,6 +61,22 @@ export interface ZWaveJSSetConfigParamData {
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export interface ZWaveJSSetConfigParamResult {
|
||||
value_id?: string;
|
||||
status?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ZWaveJSDataCollectionStatus {
|
||||
enabled: boolean;
|
||||
opted_in: boolean;
|
||||
}
|
||||
|
||||
export interface ZWaveJSRefreshNodeStatusMessage {
|
||||
event: string;
|
||||
stage?: string;
|
||||
}
|
||||
|
||||
export enum NodeStatus {
|
||||
Unknown,
|
||||
Asleep,
|
||||
@ -75,6 +96,26 @@ export const fetchNetworkStatus = (
|
||||
entry_id,
|
||||
});
|
||||
|
||||
export const fetchDataCollectionStatus = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string
|
||||
): Promise<ZWaveJSDataCollectionStatus> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/data_collection_status",
|
||||
entry_id,
|
||||
});
|
||||
|
||||
export const setDataCollectionPreference = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
opted_in: boolean
|
||||
): Promise<any> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/update_data_collection_preference",
|
||||
entry_id,
|
||||
opted_in,
|
||||
});
|
||||
|
||||
export const fetchNodeStatus = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
@ -90,7 +131,7 @@ export const fetchNodeConfigParameters = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number
|
||||
): Promise<ZWaveJSNodeConfigParams[]> =>
|
||||
): Promise<ZWaveJSNodeConfigParams> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/get_config_parameters",
|
||||
entry_id,
|
||||
@ -104,7 +145,7 @@ export const setNodeConfigParameter = (
|
||||
property: number,
|
||||
value: number,
|
||||
property_key?: number
|
||||
): Promise<unknown> => {
|
||||
): Promise<ZWaveJSSetConfigParamResult> => {
|
||||
const data: ZWaveJSSetConfigParamData = {
|
||||
type: "zwave_js/set_config_parameter",
|
||||
entry_id,
|
||||
@ -116,9 +157,25 @@ export const setNodeConfigParameter = (
|
||||
return hass.callWS(data);
|
||||
};
|
||||
|
||||
export const getIdentifiersFromDevice = function (
|
||||
export const reinterviewNode = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number,
|
||||
callbackFunction: (message: ZWaveJSRefreshNodeStatusMessage) => void
|
||||
): Promise<UnsubscribeFunc> => {
|
||||
return hass.connection.subscribeMessage(
|
||||
(message: any) => callbackFunction(message),
|
||||
{
|
||||
type: "zwave_js/refresh_node_info",
|
||||
entry_id: entry_id,
|
||||
node_id: node_id,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const getIdentifiersFromDevice = (
|
||||
device: DeviceRegistryEntry
|
||||
): ZWaveJSNodeIdentifiers | undefined {
|
||||
): ZWaveJSNodeIdentifiers | undefined => {
|
||||
if (!device) {
|
||||
return undefined;
|
||||
}
|
||||
@ -136,3 +193,48 @@ export const getIdentifiersFromDevice = function (
|
||||
home_id: identifiers[0],
|
||||
};
|
||||
};
|
||||
|
||||
export interface ZWaveJSLogMessage {
|
||||
timestamp: string;
|
||||
level: string;
|
||||
primary_tags: string;
|
||||
message: string | string[];
|
||||
}
|
||||
|
||||
export const subscribeZWaveJSLogs = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
callback: (message: ZWaveJSLogMessage) => void
|
||||
) =>
|
||||
hass.connection.subscribeMessage<ZWaveJSLogMessage>(callback, {
|
||||
type: "zwave_js/subscribe_logs",
|
||||
entry_id,
|
||||
});
|
||||
|
||||
export interface ZWaveJSLogConfig {
|
||||
level: string;
|
||||
enabled: boolean;
|
||||
filename: string;
|
||||
log_to_file: boolean;
|
||||
force_console: boolean;
|
||||
}
|
||||
|
||||
export const fetchZWaveJSLogConfig = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string
|
||||
): Promise<ZWaveJSLogConfig> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/get_log_config",
|
||||
entry_id,
|
||||
});
|
||||
|
||||
export const setZWaveJSLogLevel = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
level: string
|
||||
): Promise<ZWaveJSLogConfig> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/update_log_config",
|
||||
entry_id,
|
||||
config: { level },
|
||||
});
|
||||
|
@ -45,7 +45,8 @@ export const showDialog = async (
|
||||
root: ShadowRoot | HTMLElement,
|
||||
dialogTag: string,
|
||||
dialogParams: unknown,
|
||||
dialogImport?: () => Promise<unknown>
|
||||
dialogImport?: () => Promise<unknown>,
|
||||
addHistory = true
|
||||
) => {
|
||||
if (!(dialogTag in LOADED)) {
|
||||
if (!dialogImport) {
|
||||
@ -59,36 +60,37 @@ export const showDialog = async (
|
||||
});
|
||||
}
|
||||
|
||||
history.replaceState(
|
||||
{
|
||||
dialog: dialogTag,
|
||||
open: false,
|
||||
oldState:
|
||||
history.state?.open && history.state?.dialog !== dialogTag
|
||||
? history.state
|
||||
: null,
|
||||
},
|
||||
""
|
||||
);
|
||||
try {
|
||||
history.pushState(
|
||||
{ dialog: dialogTag, dialogParams: dialogParams, open: true },
|
||||
""
|
||||
);
|
||||
} catch (err) {
|
||||
// dialogParams could not be cloned, probably contains callback
|
||||
history.pushState(
|
||||
{ dialog: dialogTag, dialogParams: null, open: true },
|
||||
if (addHistory) {
|
||||
top.history.replaceState(
|
||||
{
|
||||
dialog: dialogTag,
|
||||
open: false,
|
||||
oldState:
|
||||
top.history.state?.open && top.history.state?.dialog !== dialogTag
|
||||
? top.history.state
|
||||
: null,
|
||||
},
|
||||
""
|
||||
);
|
||||
try {
|
||||
top.history.pushState(
|
||||
{ dialog: dialogTag, dialogParams: dialogParams, open: true },
|
||||
""
|
||||
);
|
||||
} catch (err) {
|
||||
// dialogParams could not be cloned, probably contains callback
|
||||
top.history.pushState(
|
||||
{ dialog: dialogTag, dialogParams: null, open: true },
|
||||
""
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const dialogElement = await LOADED[dialogTag];
|
||||
dialogElement.showDialog(dialogParams);
|
||||
};
|
||||
|
||||
export const replaceDialog = () => {
|
||||
history.replaceState({ ...history.state, replaced: true }, "");
|
||||
top.history.replaceState({ ...top.history.state, replaced: true }, "");
|
||||
};
|
||||
|
||||
export const closeDialog = async (dialogTag: string): Promise<boolean> => {
|
||||
|
@ -11,7 +11,6 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-attributes";
|
||||
import "../../../components/ha-color-picker";
|
||||
@ -19,20 +18,22 @@ import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-labeled-slider";
|
||||
import "../../../components/ha-paper-dropdown-menu";
|
||||
import {
|
||||
getLightRgbColor,
|
||||
LightColorModes,
|
||||
LightEntity,
|
||||
SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_COLOR,
|
||||
SUPPORT_COLOR_TEMP,
|
||||
lightIsInColorMode,
|
||||
lightSupportsColor,
|
||||
lightSupportsColorMode,
|
||||
lightSupportsDimming,
|
||||
SUPPORT_EFFECT,
|
||||
SUPPORT_WHITE_VALUE,
|
||||
} from "../../../data/light";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../../../components/ha-button-toggle-group";
|
||||
|
||||
interface HueSatColor {
|
||||
h: number;
|
||||
s: number;
|
||||
}
|
||||
|
||||
const toggleButtons = [
|
||||
{ label: "Color", value: "color" },
|
||||
{ label: "Temperature", value: LightColorModes.COLOR_TEMP },
|
||||
];
|
||||
@customElement("more-info-light")
|
||||
class MoreInfoLight extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@ -41,28 +42,51 @@ class MoreInfoLight extends LitElement {
|
||||
|
||||
@internalProperty() private _brightnessSliderValue = 0;
|
||||
|
||||
@internalProperty() private _ctSliderValue = 0;
|
||||
@internalProperty() private _ctSliderValue?: number;
|
||||
|
||||
@internalProperty() private _wvSliderValue = 0;
|
||||
@internalProperty() private _cwSliderValue?: number;
|
||||
|
||||
@internalProperty() private _wwSliderValue?: number;
|
||||
|
||||
@internalProperty() private _wvSliderValue?: number;
|
||||
|
||||
@internalProperty() private _colorBrightnessSliderValue?: number;
|
||||
|
||||
@internalProperty() private _brightnessAdjusted?: number;
|
||||
|
||||
@internalProperty() private _hueSegments = 24;
|
||||
|
||||
@internalProperty() private _saturationSegments = 8;
|
||||
|
||||
@internalProperty() private _colorPickerColor?: HueSatColor;
|
||||
@internalProperty() private _colorPickerColor?: [number, number, number];
|
||||
|
||||
@internalProperty() private _mode?: "color" | LightColorModes.COLOR_TEMP;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const supportsTemp = lightSupportsColorMode(
|
||||
this.stateObj,
|
||||
LightColorModes.COLOR_TEMP
|
||||
);
|
||||
|
||||
const supportsRgbww = lightSupportsColorMode(
|
||||
this.stateObj,
|
||||
LightColorModes.RGBWW
|
||||
);
|
||||
|
||||
const supportsRgbw =
|
||||
!supportsRgbww &&
|
||||
lightSupportsColorMode(this.stateObj, LightColorModes.RGBW);
|
||||
|
||||
const supportsColor =
|
||||
supportsRgbww || supportsRgbw || lightSupportsColor(this.stateObj);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="content ${classMap({
|
||||
"is-on": this.stateObj.state === "on",
|
||||
})}"
|
||||
>
|
||||
${supportsFeature(this.stateObj!, SUPPORT_BRIGHTNESS)
|
||||
<div class="content">
|
||||
${lightSupportsDimming(this.stateObj)
|
||||
? html`
|
||||
<ha-labeled-slider
|
||||
caption=${this.hass.localize("ui.card.light.brightness")}
|
||||
@ -77,7 +101,17 @@ class MoreInfoLight extends LitElement {
|
||||
: ""}
|
||||
${this.stateObj.state === "on"
|
||||
? html`
|
||||
${supportsFeature(this.stateObj, SUPPORT_COLOR_TEMP)
|
||||
${supportsTemp || supportsColor ? html`<hr></hr>` : ""}
|
||||
${supportsTemp && supportsColor
|
||||
? html`<ha-button-toggle-group
|
||||
fullWidth
|
||||
.buttons=${toggleButtons}
|
||||
.active=${this._mode}
|
||||
@value-changed=${this._modeChanged}
|
||||
></ha-button-toggle-group>`
|
||||
: ""}
|
||||
${supportsTemp &&
|
||||
(!supportsColor || this._mode === LightColorModes.COLOR_TEMP)
|
||||
? html`
|
||||
<ha-labeled-slider
|
||||
class="color_temp"
|
||||
@ -91,27 +125,16 @@ class MoreInfoLight extends LitElement {
|
||||
@change=${this._ctSliderChanged}
|
||||
pin
|
||||
></ha-labeled-slider>
|
||||
<hr></hr>
|
||||
`
|
||||
: ""}
|
||||
${supportsFeature(this.stateObj, SUPPORT_WHITE_VALUE)
|
||||
? html`
|
||||
<ha-labeled-slider
|
||||
caption=${this.hass.localize("ui.card.light.white_value")}
|
||||
icon="hass:file-word-box"
|
||||
max="255"
|
||||
.value=${this._wvSliderValue}
|
||||
@change=${this._wvSliderChanged}
|
||||
pin
|
||||
></ha-labeled-slider>
|
||||
`
|
||||
: ""}
|
||||
${supportsFeature(this.stateObj, SUPPORT_COLOR)
|
||||
${supportsColor && (!supportsTemp || this._mode === "color")
|
||||
? html`
|
||||
<div class="segmentationContainer">
|
||||
<ha-color-picker
|
||||
class="color"
|
||||
@colorselected=${this._colorPicked}
|
||||
.desiredHsColor=${this._colorPickerColor}
|
||||
.desiredRgbColor=${this._colorPickerColor}
|
||||
throttle="500"
|
||||
.hueSegments=${this._hueSegments}
|
||||
.saturationSegments=${this._saturationSegments}
|
||||
@ -123,6 +146,67 @@ class MoreInfoLight extends LitElement {
|
||||
class="segmentationButton"
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
|
||||
${
|
||||
supportsRgbw || supportsRgbww
|
||||
? html`<ha-labeled-slider
|
||||
.caption=${this.hass.localize(
|
||||
"ui.card.light.color_brightness"
|
||||
)}
|
||||
icon="hass:brightness-7"
|
||||
max="100"
|
||||
.value=${this._colorBrightnessSliderValue ?? 255}
|
||||
@change=${this._colorBrightnessSliderChanged}
|
||||
pin
|
||||
></ha-labeled-slider>`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
supportsRgbw
|
||||
? html`
|
||||
<ha-labeled-slider
|
||||
.caption=${this.hass.localize(
|
||||
"ui.card.light.white_value"
|
||||
)}
|
||||
icon="hass:file-word-box"
|
||||
max="100"
|
||||
.name=${"wv"}
|
||||
.value=${this._wvSliderValue}
|
||||
@change=${this._wvSliderChanged}
|
||||
pin
|
||||
></ha-labeled-slider>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
supportsRgbww
|
||||
? html`
|
||||
<ha-labeled-slider
|
||||
.caption=${this.hass.localize(
|
||||
"ui.card.light.cold_white_value"
|
||||
)}
|
||||
icon="hass:file-word-box-outline"
|
||||
max="100"
|
||||
.name=${"cw"}
|
||||
.value=${this._cwSliderValue}
|
||||
@change=${this._wvSliderChanged}
|
||||
pin
|
||||
></ha-labeled-slider>
|
||||
<ha-labeled-slider
|
||||
.caption=${this.hass.localize(
|
||||
"ui.card.light.warm_white_value"
|
||||
)}
|
||||
icon="hass:file-word-box"
|
||||
max="100"
|
||||
.name=${"ww"}
|
||||
.value=${this._wwSliderValue}
|
||||
@change=${this._wvSliderChanged}
|
||||
pin
|
||||
></ha-labeled-slider>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
<hr></hr>
|
||||
`
|
||||
: ""}
|
||||
${supportsFeature(this.stateObj, SUPPORT_EFFECT) &&
|
||||
@ -151,34 +235,85 @@ class MoreInfoLight extends LitElement {
|
||||
: ""}
|
||||
<ha-attributes
|
||||
.stateObj=${this.stateObj}
|
||||
extra-filters="brightness,color_temp,white_value,effect_list,effect,hs_color,rgb_color,xy_color,min_mireds,max_mireds,entity_id"
|
||||
extra-filters="brightness,color_temp,white_value,effect_list,effect,hs_color,rgb_color,rgbw_color,rgbww_color,xy_color,min_mireds,max_mireds,entity_id,supported_color_modes,color_mode"
|
||||
></ha-attributes>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
protected updated(changedProps: PropertyValues<this>) {
|
||||
if (!changedProps.has("stateObj")) {
|
||||
return;
|
||||
}
|
||||
const stateObj = this.stateObj! as LightEntity;
|
||||
if (changedProps.has("stateObj")) {
|
||||
if (stateObj.state === "on") {
|
||||
this._brightnessSliderValue = Math.round(
|
||||
(stateObj.attributes.brightness * 100) / 255
|
||||
);
|
||||
this._ctSliderValue = stateObj.attributes.color_temp;
|
||||
this._wvSliderValue = stateObj.attributes.white_value;
|
||||
const oldStateObj = changedProps.get("stateObj") as LightEntity | undefined;
|
||||
|
||||
if (stateObj.attributes.hs_color) {
|
||||
this._colorPickerColor = {
|
||||
h: stateObj.attributes.hs_color[0],
|
||||
s: stateObj.attributes.hs_color[1] / 100,
|
||||
};
|
||||
if (stateObj.state === "on") {
|
||||
// Don't change tab when the color mode changes
|
||||
if (
|
||||
oldStateObj?.entity_id !== stateObj.entity_id ||
|
||||
oldStateObj?.state !== stateObj.state
|
||||
) {
|
||||
this._mode = lightIsInColorMode(this.stateObj!)
|
||||
? "color"
|
||||
: LightColorModes.COLOR_TEMP;
|
||||
}
|
||||
|
||||
let brightnessAdjust = 100;
|
||||
if (
|
||||
stateObj.attributes.color_mode === LightColorModes.RGB &&
|
||||
!lightSupportsColorMode(stateObj, LightColorModes.RGBWW) &&
|
||||
!lightSupportsColorMode(stateObj, LightColorModes.RGBW)
|
||||
) {
|
||||
const maxVal = Math.max(...stateObj.attributes.rgb_color);
|
||||
if (maxVal < 255) {
|
||||
this._brightnessAdjusted = maxVal;
|
||||
brightnessAdjust = (this._brightnessAdjusted / 255) * 100;
|
||||
}
|
||||
} else {
|
||||
this._brightnessSliderValue = 0;
|
||||
this._brightnessAdjusted = undefined;
|
||||
}
|
||||
this._brightnessSliderValue = Math.round(
|
||||
(stateObj.attributes.brightness * 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)
|
||||
: undefined;
|
||||
this._cwSliderValue =
|
||||
stateObj.attributes.color_mode === LightColorModes.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)
|
||||
: undefined;
|
||||
this._colorBrightnessSliderValue =
|
||||
stateObj.attributes.color_mode === LightColorModes.RGBWW
|
||||
? Math.round(
|
||||
(Math.max(...stateObj.attributes.rgbww_color.slice(0, 3)) * 100) /
|
||||
255
|
||||
)
|
||||
: stateObj.attributes.color_mode === LightColorModes.RGBW
|
||||
? Math.round(
|
||||
(Math.max(...stateObj.attributes.rgbw_color.slice(0, 3)) * 100) /
|
||||
255
|
||||
)
|
||||
: undefined;
|
||||
|
||||
this._colorPickerColor = getLightRgbColor(stateObj)?.slice(0, 3) as
|
||||
| [number, number, number]
|
||||
| undefined;
|
||||
} else {
|
||||
this._brightnessSliderValue = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private _modeChanged(ev: CustomEvent) {
|
||||
this._mode = ev.detail.value;
|
||||
}
|
||||
|
||||
private _effectChanged(ev: CustomEvent) {
|
||||
const newVal = ev.detail.item.itemName;
|
||||
|
||||
@ -193,12 +328,29 @@ class MoreInfoLight extends LitElement {
|
||||
}
|
||||
|
||||
private _brightnessSliderChanged(ev: CustomEvent) {
|
||||
const bri = parseInt((ev.target as any).value, 10);
|
||||
const bri = Number((ev.target as any).value);
|
||||
|
||||
if (isNaN(bri)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._brightnessAdjusted) {
|
||||
const rgb =
|
||||
this.stateObj!.attributes.rgb_color ||
|
||||
([0, 0, 0] as [number, number, number]);
|
||||
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
brightness_pct: bri,
|
||||
rgb_color: this._adjustColorBrightness(
|
||||
rgb,
|
||||
this._brightnessAdjusted,
|
||||
true
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
brightness_pct: bri,
|
||||
@ -206,7 +358,7 @@ class MoreInfoLight extends LitElement {
|
||||
}
|
||||
|
||||
private _ctSliderChanged(ev: CustomEvent) {
|
||||
const ct = parseInt((ev.target as any).value, 10);
|
||||
const ct = Number((ev.target as any).value);
|
||||
|
||||
if (isNaN(ct)) {
|
||||
return;
|
||||
@ -219,18 +371,64 @@ class MoreInfoLight extends LitElement {
|
||||
}
|
||||
|
||||
private _wvSliderChanged(ev: CustomEvent) {
|
||||
const wv = parseInt((ev.target as any).value, 10);
|
||||
const target = ev.target as any;
|
||||
let wv = Number(target.value);
|
||||
const name = target.name;
|
||||
|
||||
if (isNaN(wv)) {
|
||||
return;
|
||||
}
|
||||
|
||||
wv = (wv * 255) / 100;
|
||||
|
||||
const rgb = getLightRgbColor(this.stateObj!);
|
||||
|
||||
if (name === "wv") {
|
||||
const rgbw_color = rgb || [0, 0, 0, 0];
|
||||
rgbw_color[3] = wv;
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
rgbw_color,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const rgbww_color = rgb || [0, 0, 0, 0, 0];
|
||||
while (rgbww_color.length < 5) {
|
||||
rgbww_color.push(0);
|
||||
}
|
||||
rgbww_color[name === "cw" ? 3 : 4] = wv;
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
white_value: wv,
|
||||
rgbww_color,
|
||||
});
|
||||
}
|
||||
|
||||
private _colorBrightnessSliderChanged(ev: CustomEvent) {
|
||||
const target = ev.target as any;
|
||||
const value = Number(target.value);
|
||||
|
||||
const rgb = (getLightRgbColor(this.stateObj!)?.slice(0, 3) || [
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
]) as [number, number, number];
|
||||
|
||||
this._setRgbColor(
|
||||
this._adjustColorBrightness(
|
||||
// first normalize the value
|
||||
this._colorBrightnessSliderValue
|
||||
? this._adjustColorBrightness(
|
||||
rgb,
|
||||
this._colorBrightnessSliderValue,
|
||||
true
|
||||
)
|
||||
: rgb,
|
||||
value
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _segmentClick() {
|
||||
if (this._hueSegments === 24 && this._saturationSegments === 8) {
|
||||
this._hueSegments = 0;
|
||||
@ -241,15 +439,90 @@ class MoreInfoLight extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _adjustColorBrightness(
|
||||
rgbColor: [number, number, number],
|
||||
value?: number,
|
||||
invert = false
|
||||
) {
|
||||
if (value !== undefined && value !== 255) {
|
||||
let ratio = value / 255;
|
||||
if (invert) {
|
||||
ratio = 1 / ratio;
|
||||
}
|
||||
rgbColor[0] *= ratio;
|
||||
rgbColor[1] *= ratio;
|
||||
rgbColor[2] *= ratio;
|
||||
}
|
||||
return rgbColor;
|
||||
}
|
||||
|
||||
private _setRgbColor(rgbColor: [number, number, number]) {
|
||||
if (lightSupportsColorMode(this.stateObj!, LightColorModes.RGBWW)) {
|
||||
const rgbww_color: [number, number, number, number, number] = this
|
||||
.stateObj!.attributes.rgbww_color
|
||||
? [...this.stateObj!.attributes.rgbww_color]
|
||||
: [0, 0, 0, 0, 0];
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
rgbww_color: rgbColor.concat(rgbww_color.slice(3)),
|
||||
});
|
||||
} else if (lightSupportsColorMode(this.stateObj!, LightColorModes.RGBW)) {
|
||||
const rgbw_color: [number, number, number, number] = this.stateObj!
|
||||
.attributes.rgbw_color
|
||||
? [...this.stateObj!.attributes.rgbw_color]
|
||||
: [0, 0, 0, 0];
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
rgbw_color: rgbColor.concat(rgbw_color.slice(3)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new color has been picked.
|
||||
* should be throttled with the 'throttle=' attribute of the color picker
|
||||
*/
|
||||
private _colorPicked(ev: CustomEvent) {
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
hs_color: [ev.detail.hs.h, ev.detail.hs.s * 100],
|
||||
});
|
||||
if (
|
||||
lightSupportsColorMode(this.stateObj!, LightColorModes.RGBWW) ||
|
||||
lightSupportsColorMode(this.stateObj!, LightColorModes.RGBW)
|
||||
) {
|
||||
this._setRgbColor(
|
||||
this._colorBrightnessSliderValue
|
||||
? this._adjustColorBrightness(
|
||||
[ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b],
|
||||
this._colorBrightnessSliderValue
|
||||
)
|
||||
: [ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b]
|
||||
);
|
||||
} else if (lightSupportsColorMode(this.stateObj!, LightColorModes.RGB)) {
|
||||
const rgb_color = [ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b] as [
|
||||
number,
|
||||
number,
|
||||
number
|
||||
];
|
||||
if (this._brightnessAdjusted) {
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
brightness_pct: this._brightnessSliderValue,
|
||||
rgb_color: this._adjustColorBrightness(
|
||||
rgb_color,
|
||||
this._brightnessAdjusted,
|
||||
true
|
||||
),
|
||||
});
|
||||
} else {
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
rgb_color,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
hs_color: [ev.detail.hs.h, ev.detail.hs.s * 100],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
@ -275,11 +548,18 @@ class MoreInfoLight extends LitElement {
|
||||
);
|
||||
/* The color temp minimum value shouldn't be rendered differently. It's not "off". */
|
||||
--paper-slider-knob-start-border-color: var(--primary-color);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.segmentationContainer {
|
||||
position: relative;
|
||||
max-height: 500px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
ha-button-toggle-group {
|
||||
margin: 8px 0px;
|
||||
}
|
||||
|
||||
ha-color-picker {
|
||||
@ -293,12 +573,19 @@ class MoreInfoLight extends LitElement {
|
||||
.segmentationButton {
|
||||
position: absolute;
|
||||
top: 5%;
|
||||
left: 0;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: var(--divider-color);
|
||||
border-bottom: none;
|
||||
margin: 8px 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -66,10 +66,11 @@ interface CommandItem extends QuickBarItem {
|
||||
}
|
||||
|
||||
interface EntityItem extends QuickBarItem {
|
||||
altText: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
const isCommandItem = (item: EntityItem | CommandItem): item is CommandItem => {
|
||||
const isCommandItem = (item: QuickBarItem): item is CommandItem => {
|
||||
return (item as CommandItem).categoryKey !== undefined;
|
||||
};
|
||||
|
||||
@ -230,7 +231,7 @@ export class QuickBar extends LitElement {
|
||||
private _renderItem(item: QuickBarItem, index?: number) {
|
||||
return isCommandItem(item)
|
||||
? this._renderCommandItem(item, index)
|
||||
: this._renderEntityItem(item, index);
|
||||
: this._renderEntityItem(item as EntityItem, index);
|
||||
}
|
||||
|
||||
private _renderEntityItem(item: EntityItem, index?: number) {
|
||||
@ -289,13 +290,6 @@ export class QuickBar extends LitElement {
|
||||
</span>
|
||||
|
||||
<span class="command-text">${item.primaryText}</span>
|
||||
${item.altText
|
||||
? html`
|
||||
<span slot="secondary" class="item-text secondary"
|
||||
>${item.altText}</span
|
||||
>
|
||||
`
|
||||
: null}
|
||||
</mwc-list-item>
|
||||
`;
|
||||
}
|
||||
@ -389,17 +383,20 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _generateEntityItems(): QuickBarItem[] {
|
||||
private _generateEntityItems(): EntityItem[] {
|
||||
return Object.keys(this.hass.states)
|
||||
.map((entityId) => {
|
||||
const primaryText = computeStateName(this.hass.states[entityId]);
|
||||
return {
|
||||
primaryText,
|
||||
filterText: primaryText,
|
||||
const entityItem = {
|
||||
primaryText: computeStateName(this.hass.states[entityId]),
|
||||
altText: entityId,
|
||||
icon: domainIcon(computeDomain(entityId), this.hass.states[entityId]),
|
||||
action: () => fireEvent(this, "hass-more-info", { entityId }),
|
||||
};
|
||||
|
||||
return {
|
||||
...entityItem,
|
||||
strings: [entityItem.primaryText, entityItem.altText],
|
||||
};
|
||||
})
|
||||
.sort((a, b) =>
|
||||
compare(a.primaryText.toLowerCase(), b.primaryText.toLowerCase())
|
||||
@ -412,7 +409,10 @@ export class QuickBar extends LitElement {
|
||||
...this._generateServerControlCommands(),
|
||||
...this._generateNavigationCommands(),
|
||||
].sort((a, b) =>
|
||||
compare(a.filterText.toLowerCase(), b.filterText.toLowerCase())
|
||||
compare(
|
||||
a.strings.join(" ").toLowerCase(),
|
||||
b.strings.join(" ").toLowerCase()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -420,24 +420,27 @@ export class QuickBar extends LitElement {
|
||||
const reloadableDomains = componentsWithService(this.hass, "reload").sort();
|
||||
|
||||
return reloadableDomains.map((domain) => {
|
||||
const categoryText = this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.reload`
|
||||
);
|
||||
const primaryText =
|
||||
this.hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) ||
|
||||
this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.reload.reload",
|
||||
"domain",
|
||||
domainToName(this.hass.localize, domain)
|
||||
);
|
||||
const commandItem = {
|
||||
primaryText:
|
||||
this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.reload.${domain}`
|
||||
) ||
|
||||
this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.reload.reload",
|
||||
"domain",
|
||||
domainToName(this.hass.localize, domain)
|
||||
),
|
||||
action: () => this.hass.callService(domain, "reload"),
|
||||
iconPath: mdiReload,
|
||||
categoryText: this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.reload`
|
||||
),
|
||||
};
|
||||
|
||||
return {
|
||||
primaryText,
|
||||
filterText: `${categoryText} ${primaryText}`,
|
||||
action: () => this.hass.callService(domain, "reload"),
|
||||
...commandItem,
|
||||
categoryKey: "reload",
|
||||
iconPath: mdiReload,
|
||||
categoryText,
|
||||
strings: [`${commandItem.categoryText} ${commandItem.primaryText}`],
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -446,26 +449,28 @@ export class QuickBar extends LitElement {
|
||||
const serverActions = ["restart", "stop"];
|
||||
|
||||
return serverActions.map((action) => {
|
||||
const categoryKey = "server_control";
|
||||
const categoryText = this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||
);
|
||||
const primaryText = this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.server_control.perform_action",
|
||||
"action",
|
||||
this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.server_control.${action}`
|
||||
)
|
||||
);
|
||||
const categoryKey: CommandItem["categoryKey"] = "server_control";
|
||||
|
||||
const item = {
|
||||
primaryText: this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.server_control.perform_action",
|
||||
"action",
|
||||
this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.server_control.${action}`
|
||||
)
|
||||
),
|
||||
iconPath: mdiServerNetwork,
|
||||
categoryText: this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||
),
|
||||
categoryKey,
|
||||
action: () => this.hass.callService("homeassistant", action),
|
||||
};
|
||||
|
||||
return this._generateConfirmationCommand(
|
||||
{
|
||||
primaryText,
|
||||
filterText: `${categoryText} ${primaryText}`,
|
||||
categoryKey,
|
||||
iconPath: mdiServerNetwork,
|
||||
categoryText,
|
||||
action: () => this.hass.callService("homeassistant", action),
|
||||
...item,
|
||||
strings: [`${item.categoryText} ${item.primaryText}`],
|
||||
},
|
||||
this.hass.localize("ui.dialogs.generic.ok")
|
||||
);
|
||||
@ -550,18 +555,21 @@ export class QuickBar extends LitElement {
|
||||
items: BaseNavigationCommand[]
|
||||
): CommandItem[] {
|
||||
return items.map((item) => {
|
||||
const categoryKey = "navigation";
|
||||
const categoryText = this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||
);
|
||||
const categoryKey: CommandItem["categoryKey"] = "navigation";
|
||||
|
||||
const navItem = {
|
||||
...item,
|
||||
iconPath: mdiEarth,
|
||||
categoryText: this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||
),
|
||||
action: () => navigate(this, item.path),
|
||||
};
|
||||
|
||||
return {
|
||||
...item,
|
||||
...navItem,
|
||||
strings: [`${navItem.categoryText} ${navItem.primaryText}`],
|
||||
categoryKey,
|
||||
iconPath: mdiEarth,
|
||||
categoryText,
|
||||
filterText: `${categoryText} ${item.primaryText}`,
|
||||
action: () => navigate(this, item.path),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -10,9 +10,11 @@ import {
|
||||
NetworkOnly,
|
||||
StaleWhileRevalidate,
|
||||
} from "workbox-strategies";
|
||||
import { CacheableResponsePlugin } from "workbox-cacheable-response";
|
||||
import { ExpirationPlugin } from "workbox-expiration";
|
||||
|
||||
const noFallBackRegEx = new RegExp(
|
||||
`${location.host}/(api|static|auth|frontend_latest|frontend_es5|local)/.*`
|
||||
"/(api|static|auth|frontend_latest|frontend_es5|local)/.*"
|
||||
);
|
||||
|
||||
// Clean up caches from older workboxes and old service workers.
|
||||
@ -31,27 +33,22 @@ function initRouting() {
|
||||
|
||||
// Cache static content (including translations) on first access.
|
||||
registerRoute(
|
||||
new RegExp(`${location.host}/(static|frontend_latest|frontend_es5)/.+`),
|
||||
new RegExp("/(static|frontend_latest|frontend_es5)/.+"),
|
||||
new CacheFirst({ matchOptions: { ignoreSearch: true } })
|
||||
);
|
||||
|
||||
// Get api from network.
|
||||
registerRoute(
|
||||
new RegExp(`${location.host}/(api|auth)/.*`),
|
||||
new NetworkOnly()
|
||||
);
|
||||
registerRoute(new RegExp("/(api|auth)/.*"), new NetworkOnly());
|
||||
|
||||
// Get manifest, service worker, onboarding from network.
|
||||
registerRoute(
|
||||
new RegExp(
|
||||
`${location.host}/(service_worker.js|manifest.json|onboarding.html)`
|
||||
),
|
||||
new RegExp("/(service_worker.js|manifest.json|onboarding.html)"),
|
||||
new NetworkOnly()
|
||||
);
|
||||
|
||||
// For the root "/" we ignore search
|
||||
registerRoute(
|
||||
new RegExp(`^${location.host}/(\\?.*)?$`),
|
||||
new RegExp(/\/(\?.*)?$/),
|
||||
new StaleWhileRevalidate({ matchOptions: { ignoreSearch: true } })
|
||||
);
|
||||
|
||||
@ -59,7 +56,20 @@ function initRouting() {
|
||||
// This includes "/states" response and user files from "/local".
|
||||
// First access might bring stale data from cache, but a single refresh will bring updated
|
||||
// file.
|
||||
registerRoute(new RegExp(`${location.host}/.*`), new StaleWhileRevalidate());
|
||||
registerRoute(
|
||||
new RegExp(/\/.*/),
|
||||
new StaleWhileRevalidate({
|
||||
cacheName: "file-cache",
|
||||
plugins: [
|
||||
new CacheableResponsePlugin({
|
||||
statuses: [0, 200],
|
||||
}),
|
||||
new ExpirationPlugin({
|
||||
maxAgeSeconds: 60 * 60 * 24,
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function initPushNotifications() {
|
||||
|
@ -81,9 +81,27 @@ class LightEntity extends Entity {
|
||||
|
||||
if (service === "turn_on") {
|
||||
// eslint-disable-next-line
|
||||
let { brightness, hs_color, brightness_pct } = data;
|
||||
brightness = (255 * brightness_pct) / 100;
|
||||
this.update("on", { ...this.attributes, brightness, hs_color });
|
||||
let { hs_color, brightness_pct, rgb_color, color_temp } = data;
|
||||
const attrs = { ...this.attributes };
|
||||
if (brightness_pct) {
|
||||
attrs.brightness = (255 * brightness_pct) / 100;
|
||||
} else if (!attrs.brightness) {
|
||||
attrs.brightness = 255;
|
||||
}
|
||||
if (hs_color) {
|
||||
attrs.color_mode = "hs";
|
||||
attrs.hs_color = hs_color;
|
||||
}
|
||||
if (rgb_color) {
|
||||
attrs.color_mode = "rgb";
|
||||
attrs.rgb_color = rgb_color;
|
||||
}
|
||||
if (color_temp) {
|
||||
attrs.color_mode = "color_temp";
|
||||
attrs.color_temp = color_temp;
|
||||
delete attrs.rgb_color;
|
||||
}
|
||||
this.update("on", attrs);
|
||||
} else if (service === "turn_off") {
|
||||
this.update("off");
|
||||
} else if (service === "toggle") {
|
||||
|
@ -30,6 +30,7 @@ export interface MockHomeAssistant extends HomeAssistant {
|
||||
updateStates(newStates: HassEntities);
|
||||
addEntities(entites: Entity | Entity[], replace?: boolean);
|
||||
updateTranslations(fragment: null | string, language?: string);
|
||||
addTranslations(translations: Record<string, string>, language?: string);
|
||||
mockWS(
|
||||
type: string,
|
||||
callback: (msg: any, onChange?: (response: any) => void) => any
|
||||
@ -60,15 +61,25 @@ export const provideHass = (
|
||||
) {
|
||||
const lang = language || getLocalLanguage();
|
||||
const translation = await getTranslation(fragment, lang);
|
||||
await addTranslations(translation.data, lang);
|
||||
}
|
||||
|
||||
async function addTranslations(
|
||||
translations: Record<string, string>,
|
||||
language?: string
|
||||
) {
|
||||
const lang = language || getLocalLanguage();
|
||||
const resources = {
|
||||
[lang]: {
|
||||
...(hass().resources && hass().resources[lang]),
|
||||
...translation.data,
|
||||
...translations,
|
||||
},
|
||||
};
|
||||
hass().updateHass({
|
||||
resources,
|
||||
localize: await computeLocalize(elements[0], lang, resources),
|
||||
});
|
||||
hass().updateHass({
|
||||
localize: await computeLocalize(elements[0], lang, hass().resources),
|
||||
});
|
||||
}
|
||||
|
||||
@ -209,6 +220,9 @@ export const provideHass = (
|
||||
localize: () => "",
|
||||
|
||||
translationMetadata: translationMetadata as any,
|
||||
async loadBackendTranslation() {
|
||||
return hass().localize;
|
||||
},
|
||||
dockedSidebar: "auto",
|
||||
vibrate: true,
|
||||
suspendWhenHidden: false,
|
||||
@ -250,6 +264,7 @@ export const provideHass = (
|
||||
},
|
||||
updateStates,
|
||||
updateTranslations,
|
||||
addTranslations,
|
||||
addEntities,
|
||||
mockWS(type, callback) {
|
||||
wsCommands[type] = callback;
|
||||
|
@ -23,11 +23,9 @@
|
||||
margin-right: 16px;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
html {
|
||||
background-color: #111111;
|
||||
color: #e1e1e1;
|
||||
--primary-text-color: #e1e1e1;
|
||||
--secondary-text-color: #9b9b9b;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -51,6 +51,7 @@
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background-color: #111111;
|
||||
color: #e1e1e1;
|
||||
}
|
||||
#ha-init-skeleton::before {
|
||||
background-color: #1c1c1c;
|
||||
|
@ -34,17 +34,8 @@
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color: #e1e1e1;
|
||||
}
|
||||
ha-onboarding {
|
||||
--primary-text-color: #e1e1e1;
|
||||
--secondary-text-color: #9b9b9b;
|
||||
--disabled-text-color: #6f6f6f;
|
||||
--mdc-theme-surface: #1e1e1e;
|
||||
--ha-card-background: #1e1e1e;
|
||||
}
|
||||
.content {
|
||||
background-color: #111111;
|
||||
color: #e1e1e1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,7 @@ import { registerServiceWorker } from "../util/register-service-worker";
|
||||
import "./onboarding-create-user";
|
||||
import "./onboarding-loading";
|
||||
import "./onboarding-analytics";
|
||||
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
|
||||
|
||||
type OnboardingEvent =
|
||||
| {
|
||||
@ -137,6 +138,19 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
if (window.innerWidth > 450) {
|
||||
import("./particles");
|
||||
}
|
||||
if (matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
applyThemesOnElement(
|
||||
document.documentElement,
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: null,
|
||||
themes: {},
|
||||
darkMode: false,
|
||||
},
|
||||
"default",
|
||||
{ dark: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
|
@ -32,13 +32,9 @@ class OnboardingAnalytics extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.analytics.introduction",
|
||||
"link",
|
||||
html`<a href="https://analytics.home-assistant.io" target="_blank"
|
||||
>analytics.home-assistant.io</a
|
||||
>`
|
||||
)}
|
||||
Share anonymized information from your installation to help make Home
|
||||
Assistant better and help us convince manufacturers to add local control
|
||||
and privacy-focused features.
|
||||
</p>
|
||||
<ha-analytics
|
||||
@analytics-preferences-changed=${this._preferencesChanged}
|
||||
|
@ -22,12 +22,16 @@ import {
|
||||
import {
|
||||
computeDeviceName,
|
||||
DeviceRegistryEntry,
|
||||
devicesInArea,
|
||||
} from "../../../data/device_registry";
|
||||
import {
|
||||
computeEntityRegistryName,
|
||||
EntityRegistryEntry,
|
||||
} from "../../../data/entity_registry";
|
||||
import { findRelated, RelatedResult } from "../../../data/search";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import {
|
||||
loadAreaRegistryDetailDialog,
|
||||
@ -44,6 +48,8 @@ class HaConfigAreaPage extends LitElement {
|
||||
|
||||
@property() public devices!: DeviceRegistryEntry[];
|
||||
|
||||
@property() public entities!: EntityRegistryEntry[];
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
||||
|
||||
@property() public isWide!: boolean;
|
||||
@ -58,9 +64,39 @@ class HaConfigAreaPage extends LitElement {
|
||||
| AreaRegistryEntry
|
||||
| undefined => areas.find((area) => area.area_id === areaId));
|
||||
|
||||
private _devices = memoizeOne(
|
||||
(areaId: string, devices: DeviceRegistryEntry[]): DeviceRegistryEntry[] =>
|
||||
devicesInArea(devices, areaId)
|
||||
private _memberships = memoizeOne(
|
||||
(
|
||||
areaId: string,
|
||||
registryDevices: DeviceRegistryEntry[],
|
||||
registryEntities: EntityRegistryEntry[]
|
||||
) => {
|
||||
const devices = new Map();
|
||||
|
||||
for (const device of registryDevices) {
|
||||
if (device.area_id === areaId) {
|
||||
devices.set(device.id, device);
|
||||
}
|
||||
}
|
||||
|
||||
const entities: EntityRegistryEntry[] = [];
|
||||
const indirectEntities: EntityRegistryEntry[] = [];
|
||||
|
||||
for (const entity of registryEntities) {
|
||||
if (entity.area_id) {
|
||||
if (entity.area_id === areaId) {
|
||||
entities.push(entity);
|
||||
}
|
||||
} else if (devices.has(entity.device_id)) {
|
||||
indirectEntities.push(entity);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
devices: Array.from(devices.values()),
|
||||
entities,
|
||||
indirectEntities,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
@ -87,7 +123,11 @@ class HaConfigAreaPage extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
const devices = this._devices(this.areaId, this.devices);
|
||||
const { devices, entities } = this._memberships(
|
||||
this.areaId,
|
||||
this.devices,
|
||||
this.entities
|
||||
);
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
@ -144,6 +184,33 @@ class HaConfigAreaPage extends LitElement {
|
||||
>
|
||||
`}
|
||||
</ha-card>
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.areas.editor.linked_entities_caption"
|
||||
)}
|
||||
>${entities.length
|
||||
? entities.map(
|
||||
(entity) =>
|
||||
html`
|
||||
<paper-item
|
||||
@click=${this._openEntity}
|
||||
.entity=${entity}
|
||||
>
|
||||
<paper-item-body>
|
||||
${computeEntityRegistryName(this.hass, entity)}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
`
|
||||
)
|
||||
: html`
|
||||
<paper-item class="no-link"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.areas.editor.no_linked_entities"
|
||||
)}</paper-item
|
||||
>
|
||||
`}
|
||||
</ha-card>
|
||||
</div>
|
||||
<div class="column">
|
||||
${isComponentLoaded(this.hass, "automation")
|
||||
@ -299,6 +366,14 @@ class HaConfigAreaPage extends LitElement {
|
||||
this._openDialog(entry);
|
||||
}
|
||||
|
||||
private _openEntity(ev) {
|
||||
const entry: EntityRegistryEntry = (ev.currentTarget as any).entity;
|
||||
showEntityEditorDialog(this, {
|
||||
entity_id: entry.entity_id,
|
||||
entry,
|
||||
});
|
||||
}
|
||||
|
||||
private _openDialog(entry?: AreaRegistryEntry) {
|
||||
showAreaRegistryDetailDialog(this, {
|
||||
entry,
|
||||
|
@ -24,10 +24,8 @@ import {
|
||||
AreaRegistryEntry,
|
||||
createAreaRegistryEntry,
|
||||
} from "../../../data/area_registry";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
devicesInArea,
|
||||
} from "../../../data/device_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
@ -53,12 +51,39 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
|
||||
@property() public devices!: DeviceRegistryEntry[];
|
||||
|
||||
@property() public entities!: EntityRegistryEntry[];
|
||||
|
||||
private _areas = memoizeOne(
|
||||
(areas: AreaRegistryEntry[], devices: DeviceRegistryEntry[]) => {
|
||||
(
|
||||
areas: AreaRegistryEntry[],
|
||||
devices: DeviceRegistryEntry[],
|
||||
entities: EntityRegistryEntry[]
|
||||
) => {
|
||||
return areas.map((area) => {
|
||||
const devicesInArea = new Set();
|
||||
|
||||
for (const device of devices) {
|
||||
if (device.area_id === area.area_id) {
|
||||
devicesInArea.add(device.id);
|
||||
}
|
||||
}
|
||||
|
||||
let entitiesInArea = 0;
|
||||
|
||||
for (const entity of entities) {
|
||||
if (
|
||||
entity.area_id
|
||||
? entity.area_id === area.area_id
|
||||
: devicesInArea.has(entity.device_id)
|
||||
) {
|
||||
entitiesInArea++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...area,
|
||||
devices: devicesInArea(devices, area.area_id).length,
|
||||
devices: devicesInArea.size,
|
||||
entities: entitiesInArea,
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -97,6 +122,15 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
width: "20%",
|
||||
direction: "asc",
|
||||
},
|
||||
entities: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.areas.data_table.entities"
|
||||
),
|
||||
sortable: true,
|
||||
type: "numeric",
|
||||
width: "20%",
|
||||
direction: "asc",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@ -110,7 +144,7 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
.tabs=${configSections.integrations}
|
||||
.route=${this.route}
|
||||
.columns=${this._columns(this.narrow)}
|
||||
.data=${this._areas(this.areas, this.devices)}
|
||||
.data=${this._areas(this.areas, this.devices, this.entities)}
|
||||
@row-click=${this._handleRowClicked}
|
||||
.noDataText=${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.no_areas"
|
||||
|
@ -15,6 +15,10 @@ import {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../../data/entity_registry";
|
||||
import {
|
||||
HassRouterPage,
|
||||
RouterOptions,
|
||||
@ -51,6 +55,9 @@ class HaConfigAreas extends HassRouterPage {
|
||||
@internalProperty()
|
||||
private _deviceRegistryEntries: DeviceRegistryEntry[] = [];
|
||||
|
||||
@internalProperty()
|
||||
private _entityRegistryEntries: EntityRegistryEntry[] = [];
|
||||
|
||||
@internalProperty() private _areas: AreaRegistryEntry[] = [];
|
||||
|
||||
private _unsubs?: UnsubscribeFunc[];
|
||||
@ -90,6 +97,7 @@ class HaConfigAreas extends HassRouterPage {
|
||||
|
||||
pageEl.entries = this._configEntries;
|
||||
pageEl.devices = this._deviceRegistryEntries;
|
||||
pageEl.entities = this._entityRegistryEntries;
|
||||
pageEl.areas = this._areas;
|
||||
pageEl.narrow = this.narrow;
|
||||
pageEl.isWide = this.isWide;
|
||||
@ -113,6 +121,9 @@ class HaConfigAreas extends HassRouterPage {
|
||||
subscribeDeviceRegistry(this.hass.connection, (entries) => {
|
||||
this._deviceRegistryEntries = entries;
|
||||
}),
|
||||
subscribeEntityRegistry(this.hass.connection, (entries) => {
|
||||
this._entityRegistryEntries = entries;
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -99,33 +99,63 @@ export class HaAutomationTracePathDetails extends LitElement {
|
||||
return "This node was not executed and so no further trace information is available.";
|
||||
}
|
||||
|
||||
const data: ActionTraceStep[] = paths[this.selected.path];
|
||||
const parts: TemplateResult[][] = [];
|
||||
|
||||
return data.map((trace, idx) => {
|
||||
const {
|
||||
path,
|
||||
timestamp,
|
||||
result,
|
||||
error,
|
||||
changed_variables,
|
||||
...rest
|
||||
} = trace as any;
|
||||
let active = false;
|
||||
const childConditionsPrefix = `${this.selected.path}/conditions/`;
|
||||
|
||||
return html`
|
||||
${data.length === 1 ? "" : html`<h3>Iteration ${idx + 1}</h3>`}
|
||||
Executed:
|
||||
${formatDateTimeWithSeconds(new Date(timestamp), this.hass.locale)}<br />
|
||||
${result
|
||||
? html`Result:
|
||||
<pre>${safeDump(result)}</pre>`
|
||||
: error
|
||||
? html`<div class="error">Error: ${error}</div>`
|
||||
: ""}
|
||||
${Object.keys(rest).length === 0
|
||||
? ""
|
||||
: html`<pre>${safeDump(rest)}</pre>`}
|
||||
`;
|
||||
});
|
||||
for (const curPath of Object.keys(this.trace.trace)) {
|
||||
// Include all child conditions too
|
||||
if (active) {
|
||||
if (!curPath.startsWith(childConditionsPrefix)) {
|
||||
break;
|
||||
}
|
||||
} else if (curPath === this.selected.path) {
|
||||
active = true;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
const data: ActionTraceStep[] = paths[curPath];
|
||||
|
||||
parts.push(
|
||||
data.map((trace, idx) => {
|
||||
const {
|
||||
path,
|
||||
timestamp,
|
||||
result,
|
||||
error,
|
||||
changed_variables,
|
||||
...rest
|
||||
} = trace as any;
|
||||
|
||||
return html`
|
||||
${curPath === this.selected.path
|
||||
? ""
|
||||
: html`<h2>
|
||||
Condition ${curPath.substr(childConditionsPrefix.length)}
|
||||
</h2>`}
|
||||
${data.length === 1 ? "" : html`<h3>Iteration ${idx + 1}</h3>`}
|
||||
Executed:
|
||||
${formatDateTimeWithSeconds(
|
||||
new Date(timestamp),
|
||||
this.hass.locale
|
||||
)}<br />
|
||||
${result
|
||||
? html`Result:
|
||||
<pre>${safeDump(result)}</pre>`
|
||||
: error
|
||||
? html`<div class="error">Error: ${error}</div>`
|
||||
: ""}
|
||||
${Object.keys(rest).length === 0
|
||||
? ""
|
||||
: html`<pre>${safeDump(rest)}</pre>`}
|
||||
`;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
private _renderSelectedConfig() {
|
||||
|
@ -87,12 +87,24 @@ export class HaAutomationTrace extends LitElement {
|
||||
|
||||
const title = stateObj?.attributes.friendly_name || this._entityId;
|
||||
|
||||
let devButtons: TemplateResult | string = "";
|
||||
if (__DEV__) {
|
||||
devButtons = html`<div style="position: absolute; right: 0;">
|
||||
<button @click=${this._importTrace}>
|
||||
Import trace
|
||||
</button>
|
||||
<button @click=${this._loadLocalStorageTrace}>
|
||||
Load stored trace
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const actionButtons = html`
|
||||
<mwc-icon-button label="Refresh" @click=${() => this._loadTraces()}>
|
||||
<ha-svg-icon .path=${mdiRefresh}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<mwc-icon-button
|
||||
.disabled=${!this._runId}
|
||||
.disabled=${!this._trace}
|
||||
label="Download Trace"
|
||||
@click=${this._downloadTrace}
|
||||
>
|
||||
@ -101,6 +113,7 @@ export class HaAutomationTrace extends LitElement {
|
||||
`;
|
||||
|
||||
return html`
|
||||
${devButtons}
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
@ -410,6 +423,27 @@ export class HaAutomationTrace extends LitElement {
|
||||
aEl.click();
|
||||
}
|
||||
|
||||
private _importTrace() {
|
||||
const traceText = prompt("Enter downloaded trace");
|
||||
if (!traceText) {
|
||||
return;
|
||||
}
|
||||
localStorage.devTrace = traceText;
|
||||
this._loadLocalTrace(traceText);
|
||||
}
|
||||
|
||||
private _loadLocalStorageTrace() {
|
||||
if (localStorage.devTrace) {
|
||||
this._loadLocalTrace(localStorage.devTrace);
|
||||
}
|
||||
}
|
||||
|
||||
private _loadLocalTrace(traceText: string) {
|
||||
const traceInfo = JSON.parse(traceText);
|
||||
this._trace = traceInfo.trace;
|
||||
this._logbookEntries = traceInfo.logbookEntries;
|
||||
}
|
||||
|
||||
private _showTab(ev) {
|
||||
this._view = (ev.target as any).view;
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ class HaConfigCloud extends HassRouterPage {
|
||||
|
||||
private _resolveCloudStatusLoaded!: () => void;
|
||||
|
||||
private _cloudStatusLoaded = new Promise((resolve) => {
|
||||
private _cloudStatusLoaded = new Promise<void>((resolve) => {
|
||||
this._resolveCloudStatusLoaded = resolve;
|
||||
});
|
||||
|
||||
|
@ -40,21 +40,13 @@ class ConfigAnalytics extends LitElement {
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.analytics.header"
|
||||
)}
|
||||
>
|
||||
<ha-card header="Analytics">
|
||||
<div class="card-content">
|
||||
${error ? html`<div class="error">${error}</div>` : ""}
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.analytics.introduction",
|
||||
"link",
|
||||
html`<a href="https://analytics.home-assistant.io" target="_blank"
|
||||
>analytics.home-assistant.io</a
|
||||
>`
|
||||
)}
|
||||
Share anonymized information from your installation to help make
|
||||
Home Assistant better and help us convince manufacturers to add
|
||||
local control and privacy-focused features.
|
||||
</p>
|
||||
<ha-analytics
|
||||
@analytics-preferences-changed=${this._preferencesChanged}
|
||||
|
@ -79,36 +79,6 @@ class HaConfigDashboard extends LitElement {
|
||||
</ha-card>
|
||||
`
|
||||
)}
|
||||
${isComponentLoaded(this.hass, "zha")
|
||||
? html`
|
||||
<div class="promo-advanced">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integration_panel_move.missing_zha",
|
||||
"integrations_page",
|
||||
html`<a href="/config/integrations">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integration_panel_move.link_integration_page"
|
||||
)}
|
||||
</a>`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${isComponentLoaded(this.hass, "zwave")
|
||||
? html`
|
||||
<div class="promo-advanced">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integration_panel_move.missing_zwave",
|
||||
"integrations_page",
|
||||
html`<a href="/config/integrations">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integration_panel_move.link_integration_page"
|
||||
)}
|
||||
</a>`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${!this.showAdvanced
|
||||
? html`
|
||||
<div class="promo-advanced">
|
||||
|
@ -11,9 +11,13 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
||||
import {
|
||||
getIdentifiersFromDevice,
|
||||
ZWaveJSNodeIdentifiers,
|
||||
} from "../../../../../../data/zwave_js";
|
||||
import { haStyle } from "../../../../../../resources/styles";
|
||||
|
||||
import { HomeAssistant } from "../../../../../../types";
|
||||
import { showZWaveJSReinterviewNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-reinterview-node";
|
||||
|
||||
@customElement("ha-device-actions-zwave_js")
|
||||
export class HaDeviceActionsZWaveJS extends LitElement {
|
||||
@ -23,9 +27,19 @@ export class HaDeviceActionsZWaveJS extends LitElement {
|
||||
|
||||
@internalProperty() private _entryId?: string;
|
||||
|
||||
@internalProperty() private _nodeId?: number;
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has("device")) {
|
||||
this._entryId = this.device.config_entries[0];
|
||||
|
||||
const identifiers:
|
||||
| ZWaveJSNodeIdentifiers
|
||||
| undefined = getIdentifiersFromDevice(this.device);
|
||||
if (!identifiers) {
|
||||
return;
|
||||
}
|
||||
this._nodeId = identifiers.node_id;
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,9 +54,22 @@ export class HaDeviceActionsZWaveJS extends LitElement {
|
||||
)}
|
||||
</mwc-button>
|
||||
</a>
|
||||
<mwc-button @click=${this._reinterviewClicked}
|
||||
>Re-interview Device</mwc-button
|
||||
>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _reinterviewClicked() {
|
||||
if (!this._nodeId || !this._entryId) {
|
||||
return;
|
||||
}
|
||||
showZWaveJSReinterviewNodeDialog(this, {
|
||||
entry_id: this._entryId,
|
||||
node_id: this._nodeId,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
|
@ -33,7 +33,7 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
|
||||
@internalProperty() private _params?: DeviceRegistryDetailDialogParams;
|
||||
|
||||
@internalProperty() private _areaId?: string;
|
||||
@internalProperty() private _areaId?: string | null;
|
||||
|
||||
@internalProperty() private _disabledBy!: string | null;
|
||||
|
||||
|
@ -728,7 +728,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
}
|
||||
|
||||
if (!newName && !newEntityId) {
|
||||
return new Promise((resolve) => resolve());
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return updateEntityRegistryEntry(this.hass!, entity.entity_id, {
|
||||
|
@ -38,7 +38,7 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||
|
||||
@internalProperty() private _entityId!: string;
|
||||
|
||||
@internalProperty() private _areaId?: string;
|
||||
@internalProperty() private _areaId?: string | null;
|
||||
|
||||
@internalProperty() private _disabledBy!: string | null;
|
||||
|
||||
|
@ -663,6 +663,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
entity_id: entityId,
|
||||
platform: computeDomain(entityId),
|
||||
disabled_by: null,
|
||||
area_id: null,
|
||||
config_entry_id: null,
|
||||
device_id: null,
|
||||
icon: null,
|
||||
readonly: true,
|
||||
selectable: false,
|
||||
});
|
||||
|
@ -140,7 +140,10 @@ class HaConfigInfo extends LitElement {
|
||||
</div>
|
||||
<div class="content">
|
||||
<system-health-card .hass=${this.hass}></system-health-card>
|
||||
<integrations-card .hass=${this.hass}></integrations-card>
|
||||
<integrations-card
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></integrations-card>
|
||||
</div>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
|
@ -13,8 +13,10 @@ import "../../../components/ha-card";
|
||||
import {
|
||||
domainToName,
|
||||
fetchIntegrationManifests,
|
||||
fetchIntegrationSetups,
|
||||
integrationIssuesUrl,
|
||||
IntegrationManifest,
|
||||
IntegrationSetup,
|
||||
} from "../../../data/integration";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
@ -23,15 +25,22 @@ import { brandsUrl } from "../../../util/brands-url";
|
||||
class IntegrationsCard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@internalProperty() private _manifests?: {
|
||||
[domain: string]: IntegrationManifest;
|
||||
};
|
||||
|
||||
@internalProperty() private _setups?: {
|
||||
[domain: string]: IntegrationSetup;
|
||||
};
|
||||
|
||||
private _sortedIntegrations = memoizeOne((components: string[]) => {
|
||||
return Array.from(
|
||||
new Set(
|
||||
components
|
||||
.map((comp) => (comp.includes(".") ? comp.split(".")[1] : comp))
|
||||
components.map((comp) =>
|
||||
comp.includes(".") ? comp.split(".")[1] : comp
|
||||
)
|
||||
)
|
||||
).sort();
|
||||
});
|
||||
@ -39,6 +48,7 @@ class IntegrationsCard extends LitElement {
|
||||
firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._fetchManifests();
|
||||
this._fetchSetups();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@ -47,10 +57,47 @@ class IntegrationsCard extends LitElement {
|
||||
.header=${this.hass.localize("ui.panel.config.info.integrations")}
|
||||
>
|
||||
<table class="card-content">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
${!this.narrow
|
||||
? html`<th></th>
|
||||
<th></th>
|
||||
<th></th>`
|
||||
: ""}
|
||||
<th>Setup time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${this._sortedIntegrations(this.hass!.config.components).map(
|
||||
(domain) => {
|
||||
const manifest = this._manifests && this._manifests[domain];
|
||||
const docLink = manifest
|
||||
? html`<a
|
||||
href=${manifest.documentation}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.info.documentation"
|
||||
)}</a
|
||||
>`
|
||||
: "";
|
||||
const issueLink =
|
||||
manifest && (manifest.is_built_in || manifest.issue_tracker)
|
||||
? html`
|
||||
<a
|
||||
href=${integrationIssuesUrl(domain, manifest)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.info.issues"
|
||||
)}</a
|
||||
>
|
||||
`
|
||||
: "";
|
||||
const setupSeconds = this._setups?.[domain]?.seconds?.toFixed(
|
||||
2
|
||||
);
|
||||
return html`
|
||||
<tr>
|
||||
<td>
|
||||
@ -63,39 +110,25 @@ class IntegrationsCard extends LitElement {
|
||||
<td class="name">
|
||||
${domainToName(this.hass.localize, domain, manifest)}<br />
|
||||
<span class="domain">${domain}</span>
|
||||
${this.narrow
|
||||
? html`<div class="mobile-row">
|
||||
<div>${docLink} ${issueLink}</div>
|
||||
${setupSeconds ? html`${setupSeconds}s` : ""}
|
||||
</div>`
|
||||
: ""}
|
||||
</td>
|
||||
${!manifest
|
||||
${this.narrow
|
||||
? ""
|
||||
: html`
|
||||
<td>
|
||||
<a
|
||||
href=${manifest.documentation}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.info.documentation"
|
||||
)}
|
||||
</a>
|
||||
${docLink}
|
||||
</td>
|
||||
<td>
|
||||
${issueLink}
|
||||
</td>
|
||||
<td class="setup">
|
||||
${setupSeconds ? html`${setupSeconds}s` : ""}
|
||||
</td>
|
||||
${manifest.is_built_in || manifest.issue_tracker
|
||||
? html`
|
||||
<td>
|
||||
<a
|
||||
href=${integrationIssuesUrl(
|
||||
domain,
|
||||
manifest
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.info.issues"
|
||||
)}
|
||||
</a>
|
||||
</td>
|
||||
`
|
||||
: ""}
|
||||
`}
|
||||
</tr>
|
||||
`;
|
||||
@ -115,9 +148,21 @@ class IntegrationsCard extends LitElement {
|
||||
this._manifests = manifests;
|
||||
}
|
||||
|
||||
private async _fetchSetups() {
|
||||
const setups = {};
|
||||
for (const setup of await fetchIntegrationSetups(this.hass)) {
|
||||
setups[setup.domain] = setup;
|
||||
}
|
||||
this._setups = setups;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
td {
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
td,
|
||||
th {
|
||||
padding: 0 8px;
|
||||
}
|
||||
td:first-child {
|
||||
@ -126,9 +171,22 @@ class IntegrationsCard extends LitElement {
|
||||
td.name {
|
||||
padding: 8px;
|
||||
}
|
||||
td.setup {
|
||||
text-align: right;
|
||||
}
|
||||
th {
|
||||
text-align: right;
|
||||
}
|
||||
.domain {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.mobile-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.mobile-row a:not(:last-of-type) {
|
||||
margin-right: 4px;
|
||||
}
|
||||
img {
|
||||
display: block;
|
||||
max-height: 40px;
|
||||
|
130
src/panels/config/integrations/ha-config-flow-card.ts
Normal file
130
src/panels/config/integrations/ha-config-flow-card.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import {
|
||||
customElement,
|
||||
LitElement,
|
||||
property,
|
||||
css,
|
||||
html,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import {
|
||||
ATTENTION_SOURCES,
|
||||
DISCOVERY_SOURCES,
|
||||
ignoreConfigFlow,
|
||||
localizeConfigFlowTitle,
|
||||
} from "../../../data/config_flow";
|
||||
import type { IntegrationManifest } from "../../../data/integration";
|
||||
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { DataEntryFlowProgressExtended } from "./ha-config-integrations";
|
||||
import "./ha-integration-action-card";
|
||||
|
||||
@customElement("ha-config-flow-card")
|
||||
export class HaConfigFlowCard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public flow!: DataEntryFlowProgressExtended;
|
||||
|
||||
@property() public manifest?: IntegrationManifest;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const attention = ATTENTION_SOURCES.includes(this.flow.context.source);
|
||||
return html`
|
||||
<ha-integration-action-card
|
||||
class=${classMap({
|
||||
discovered: !attention,
|
||||
attention: attention,
|
||||
})}
|
||||
.hass=${this.hass}
|
||||
.manifest=${this.manifest}
|
||||
.banner=${this.hass.localize(
|
||||
`ui.panel.config.integrations.${
|
||||
attention ? "attention" : "discovered"
|
||||
}`
|
||||
)}
|
||||
.domain=${this.flow.handler}
|
||||
.label=${this.flow.localized_title}
|
||||
>
|
||||
<mwc-button
|
||||
unelevated
|
||||
@click=${this._continueFlow}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.integrations.${
|
||||
attention ? "reconfigure" : "configure"
|
||||
}`
|
||||
)}
|
||||
></mwc-button>
|
||||
${DISCOVERY_SOURCES.includes(this.flow.context.source) &&
|
||||
this.flow.context.unique_id
|
||||
? html`
|
||||
<mwc-button
|
||||
@click=${this._ignoreFlow}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.ignore.ignore"
|
||||
)}
|
||||
></mwc-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-integration-action-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _continueFlow() {
|
||||
showConfigFlowDialog(this, {
|
||||
continueFlowId: this.flow.flow_id,
|
||||
dialogClosedCallback: () => {
|
||||
this._handleFlowUpdated();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async _ignoreFlow() {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass!.localize(
|
||||
"ui.panel.config.integrations.ignore.confirm_ignore_title",
|
||||
"name",
|
||||
localizeConfigFlowTitle(this.hass.localize, this.flow)
|
||||
),
|
||||
text: this.hass!.localize(
|
||||
"ui.panel.config.integrations.ignore.confirm_ignore"
|
||||
),
|
||||
confirmText: this.hass!.localize(
|
||||
"ui.panel.config.integrations.ignore.ignore"
|
||||
),
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
await ignoreConfigFlow(
|
||||
this.hass,
|
||||
this.flow.flow_id,
|
||||
localizeConfigFlowTitle(this.hass.localize, this.flow)
|
||||
);
|
||||
this._handleFlowUpdated();
|
||||
}
|
||||
|
||||
private _handleFlowUpdated() {
|
||||
fireEvent(this, "change", undefined, {
|
||||
bubbles: false,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.attention {
|
||||
--state-color: var(--error-color);
|
||||
--text-on-state-color: var(--text-primary-color);
|
||||
}
|
||||
.discovered {
|
||||
--state-color: var(--primary-color);
|
||||
--text-on-state-color: var(--text-primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-flow-card": HaConfigFlowCard;
|
||||
}
|
||||
}
|
@ -2,9 +2,8 @@ import "@material/mwc-icon-button";
|
||||
import { ActionDetail } from "@material/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiFilterVariant, mdiPlus } from "@mdi/js";
|
||||
import "@polymer/app-route/app-route";
|
||||
import Fuse from "fuse.js";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
@ -16,31 +15,15 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import "../../../common/search/search-input";
|
||||
import { caseInsensitiveCompare } from "../../../common/string/compare";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import { nextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-checkbox";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
|
||||
import {
|
||||
ConfigEntry,
|
||||
deleteConfigEntry,
|
||||
getConfigEntries,
|
||||
} from "../../../data/config_entries";
|
||||
import {
|
||||
ATTENTION_SOURCES,
|
||||
DISCOVERY_SOURCES,
|
||||
getConfigFlowInProgressCollection,
|
||||
ignoreConfigFlow,
|
||||
localizeConfigFlowTitle,
|
||||
subscribeConfigFlowInProgress,
|
||||
} from "../../../data/config_flow";
|
||||
@ -55,26 +38,49 @@ import {
|
||||
} from "../../../data/entity_registry";
|
||||
import {
|
||||
domainToName,
|
||||
fetchIntegrationManifest,
|
||||
fetchIntegrationManifests,
|
||||
IntegrationManifest,
|
||||
} from "../../../data/integration";
|
||||
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import "./ha-integration-card";
|
||||
import type {
|
||||
ConfigEntryRemovedEvent,
|
||||
ConfigEntryUpdatedEvent,
|
||||
HaIntegrationCard,
|
||||
} from "./ha-integration-card";
|
||||
|
||||
interface DataEntryFlowProgressExtended extends DataEntryFlowProgress {
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import type { HaIntegrationCard } from "./ha-integration-card";
|
||||
|
||||
import "../../../common/search/search-input";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-checkbox";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import "./ha-integration-card";
|
||||
import "./ha-config-flow-card";
|
||||
import "./ha-ignored-config-entry-card";
|
||||
|
||||
export interface ConfigEntryUpdatedEvent {
|
||||
entry: ConfigEntry;
|
||||
}
|
||||
|
||||
export interface ConfigEntryRemovedEvent {
|
||||
entryId: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"entry-updated": ConfigEntryUpdatedEvent;
|
||||
"entry-removed": ConfigEntryRemovedEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataEntryFlowProgressExtended extends DataEntryFlowProgress {
|
||||
localized_title?: string;
|
||||
}
|
||||
|
||||
@ -119,9 +125,10 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
@internalProperty()
|
||||
private _deviceRegistryEntries: DeviceRegistryEntry[] = [];
|
||||
|
||||
@internalProperty() private _manifests!: {
|
||||
[domain: string]: IntegrationManifest;
|
||||
};
|
||||
@internalProperty()
|
||||
private _manifests: Record<string, IntegrationManifest> = {};
|
||||
|
||||
private _extraFetchedManifests?: Set<string>;
|
||||
|
||||
@internalProperty() private _showIgnored = false;
|
||||
|
||||
@ -150,15 +157,14 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
this.hass.loadBackendTranslation("config", flow.handler)
|
||||
);
|
||||
}
|
||||
this._fetchManifest(flow.handler);
|
||||
});
|
||||
await Promise.all(translationsPromisses);
|
||||
await nextRender();
|
||||
this._configEntriesInProgress = flowsInProgress.map((flow) => {
|
||||
return {
|
||||
...flow,
|
||||
localized_title: localizeConfigFlowTitle(this.hass.localize, flow),
|
||||
};
|
||||
});
|
||||
this._configEntriesInProgress = flowsInProgress.map((flow) => ({
|
||||
...flow,
|
||||
localized_title: localizeConfigFlowTitle(this.hass.localize, flow),
|
||||
}));
|
||||
}),
|
||||
];
|
||||
}
|
||||
@ -217,12 +223,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
configEntriesInProgress: DataEntryFlowProgressExtended[],
|
||||
filter?: string
|
||||
): DataEntryFlowProgressExtended[] => {
|
||||
configEntriesInProgress = configEntriesInProgress.map(
|
||||
(flow: DataEntryFlowProgressExtended) => ({
|
||||
...flow,
|
||||
title: localizeConfigFlowTitle(this.hass.localize, flow),
|
||||
})
|
||||
);
|
||||
if (!filter) {
|
||||
return configEntriesInProgress;
|
||||
}
|
||||
@ -349,11 +349,12 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
"number",
|
||||
disabledConfigEntries.size
|
||||
)}
|
||||
<mwc-button @click=${this._toggleShowDisabled}>
|
||||
${this.hass.localize(
|
||||
<mwc-button
|
||||
@click=${this._toggleShowDisabled}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.disable.show"
|
||||
)}
|
||||
</mwc-button>
|
||||
></mwc-button>
|
||||
</div>`
|
||||
: ""}
|
||||
${filterMenu}
|
||||
@ -362,112 +363,31 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
|
||||
<div
|
||||
class="container"
|
||||
@entry-removed=${this._handleRemoved}
|
||||
@entry-updated=${this._handleUpdated}
|
||||
@entry-removed=${this._handleEntryRemoved}
|
||||
@entry-updated=${this._handleEntryUpdated}
|
||||
>
|
||||
${this._showIgnored
|
||||
? ignoredConfigEntries.map(
|
||||
(item: ConfigEntryExtended) => html`
|
||||
<ha-card outlined class="ignored">
|
||||
<div class="header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.ignore.ignored"
|
||||
)}
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="image">
|
||||
<img
|
||||
src=${brandsUrl(item.domain, "logo")}
|
||||
referrerpolicy="no-referrer"
|
||||
@error=${this._onImageError}
|
||||
@load=${this._onImageLoad}
|
||||
/>
|
||||
</div>
|
||||
<h2>
|
||||
${// In 2020.2 we added support for item.title. All ignored entries before
|
||||
// that have title "Ignored" so we fallback to localized domain name.
|
||||
item.title === "Ignored"
|
||||
? item.localized_domain_name
|
||||
: item.title}
|
||||
</h2>
|
||||
<mwc-button
|
||||
@click=${this._removeIgnoredIntegration}
|
||||
.entry=${item}
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.ignore.stop_ignore"
|
||||
)}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.ignore.stop_ignore"
|
||||
)}</mwc-button
|
||||
>
|
||||
</div>
|
||||
</ha-card>
|
||||
(entry: ConfigEntryExtended) => html`
|
||||
<ha-ignored-config-entry-card
|
||||
.hass=${this.hass}
|
||||
.manifest=${this._manifests[entry.domain]}
|
||||
.entry=${entry}
|
||||
@change=${this._handleFlowUpdated}
|
||||
></ha-ignored-config-entry-card>
|
||||
`
|
||||
)
|
||||
: ""}
|
||||
${configEntriesInProgress.length
|
||||
? configEntriesInProgress.map(
|
||||
(flow: DataEntryFlowProgressExtended) => {
|
||||
const attention = ATTENTION_SOURCES.includes(
|
||||
flow.context.source
|
||||
);
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
class=${classMap({
|
||||
discovered: !attention,
|
||||
attention: attention,
|
||||
})}
|
||||
>
|
||||
<div class="header">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.${
|
||||
attention ? "attention" : "discovered"
|
||||
}`
|
||||
)}
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="image">
|
||||
<img
|
||||
src=${brandsUrl(flow.handler, "logo")}
|
||||
referrerpolicy="no-referrer"
|
||||
@error=${this._onImageError}
|
||||
@load=${this._onImageLoad}
|
||||
/>
|
||||
</div>
|
||||
<h2>
|
||||
${flow.localized_title}
|
||||
</h2>
|
||||
<div>
|
||||
<mwc-button
|
||||
unelevated
|
||||
@click=${this._continueFlow}
|
||||
.flowId=${flow.flow_id}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.${
|
||||
attention ? "reconfigure" : "configure"
|
||||
}`
|
||||
)}
|
||||
</mwc-button>
|
||||
${DISCOVERY_SOURCES.includes(flow.context.source) &&
|
||||
flow.context.unique_id
|
||||
? html`
|
||||
<mwc-button
|
||||
@click=${this._ignoreFlow}
|
||||
.flow=${flow}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.ignore.ignore"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
(flow: DataEntryFlowProgressExtended) => html`
|
||||
<ha-config-flow-card
|
||||
.hass=${this.hass}
|
||||
.manifest=${this._manifests[flow.handler]}
|
||||
.flow=${flow}
|
||||
@change=${this._handleFlowUpdated}
|
||||
></ha-config-flow-card>
|
||||
`
|
||||
)
|
||||
: ""}
|
||||
${this._showDisabled
|
||||
@ -498,25 +418,28 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
.deviceRegistryEntries=${this._deviceRegistryEntries}
|
||||
></ha-integration-card>`
|
||||
)
|
||||
: !this._configEntries.length
|
||||
: // If we're showing 0 cards, show empty state text
|
||||
(!this._showIgnored || ignoredConfigEntries.length === 0) &&
|
||||
(!this._showDisabled || disabledConfigEntries.size === 0) &&
|
||||
groupedConfigEntries.size === 0
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<h1>
|
||||
${this.hass.localize("ui.panel.config.integrations.none")}
|
||||
</h1>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.no_integrations"
|
||||
)}
|
||||
</p>
|
||||
<mwc-button @click=${this._createFlow} unelevated
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.add_integration"
|
||||
)}</mwc-button
|
||||
>
|
||||
</div>
|
||||
</ha-card>
|
||||
<div class="empty-message">
|
||||
<h1>
|
||||
${this.hass.localize("ui.panel.config.integrations.none")}
|
||||
</h1>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.no_integrations"
|
||||
)}
|
||||
</p>
|
||||
<mwc-button
|
||||
@click=${this._createFlow}
|
||||
unelevated
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.add_integration"
|
||||
)}
|
||||
></mwc-button>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this._filter &&
|
||||
@ -524,7 +447,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
!groupedConfigEntries.size &&
|
||||
this._configEntries.length
|
||||
? html`
|
||||
<div class="none-found">
|
||||
<div class="empty-message">
|
||||
<h1>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.none_found"
|
||||
@ -575,19 +498,40 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private async _fetchManifests() {
|
||||
const manifests = {};
|
||||
const fetched = await fetchIntegrationManifests(this.hass);
|
||||
// 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;
|
||||
this._manifests = manifests;
|
||||
}
|
||||
|
||||
private _handleRemoved(ev: HASSDomEvent<ConfigEntryRemovedEvent>) {
|
||||
private async _fetchManifest(domain: string) {
|
||||
if (domain in this._manifests) {
|
||||
return;
|
||||
}
|
||||
if (this._extraFetchedManifests) {
|
||||
if (this._extraFetchedManifests.has(domain)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this._extraFetchedManifests = new Set();
|
||||
}
|
||||
this._extraFetchedManifests.add(domain);
|
||||
const manifest = await fetchIntegrationManifest(this.hass, domain);
|
||||
this._manifests = {
|
||||
...this._manifests,
|
||||
[domain]: manifest,
|
||||
};
|
||||
}
|
||||
|
||||
private _handleEntryRemoved(ev: HASSDomEvent<ConfigEntryRemovedEvent>) {
|
||||
this._configEntries = this._configEntries!.filter(
|
||||
(entry) => entry.entry_id !== ev.detail.entryId
|
||||
);
|
||||
}
|
||||
|
||||
private _handleUpdated(ev: HASSDomEvent<ConfigEntryUpdatedEvent>) {
|
||||
private _handleEntryUpdated(ev: HASSDomEvent<ConfigEntryUpdatedEvent>) {
|
||||
const newEntry = ev.detail.entry;
|
||||
this._configEntries = this._configEntries!.map((entry) =>
|
||||
entry.entry_id === newEntry.entry_id
|
||||
@ -599,6 +543,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
private _handleFlowUpdated() {
|
||||
this._loadConfigEntries();
|
||||
getConfigFlowInProgressCollection(this.hass.connection).refresh();
|
||||
this._fetchManifests();
|
||||
}
|
||||
|
||||
private _createFlow() {
|
||||
@ -608,50 +553,14 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
showAdvanced: this.showAdvanced,
|
||||
});
|
||||
// For config entries. Also loading config flow ones for add integration
|
||||
// For config entries. Also loading config flow ones for added integration
|
||||
this.hass.loadBackendTranslation("title", undefined, true);
|
||||
}
|
||||
|
||||
private _continueFlow(ev: Event) {
|
||||
showConfigFlowDialog(this, {
|
||||
continueFlowId: (ev.target! as any).flowId,
|
||||
dialogClosedCallback: () => {
|
||||
this._handleFlowUpdated();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async _ignoreFlow(ev: Event) {
|
||||
const flow = (ev.target! as any).flow;
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass!.localize(
|
||||
"ui.panel.config.integrations.ignore.confirm_ignore_title",
|
||||
"name",
|
||||
localizeConfigFlowTitle(this.hass.localize, flow)
|
||||
),
|
||||
text: this.hass!.localize(
|
||||
"ui.panel.config.integrations.ignore.confirm_ignore"
|
||||
),
|
||||
confirmText: this.hass!.localize(
|
||||
"ui.panel.config.integrations.ignore.ignore"
|
||||
),
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
await ignoreConfigFlow(
|
||||
this.hass,
|
||||
flow.flow_id,
|
||||
localizeConfigFlowTitle(this.hass.localize, flow)
|
||||
);
|
||||
this._loadConfigEntries();
|
||||
getConfigFlowInProgressCollection(this.hass.connection).refresh();
|
||||
}
|
||||
|
||||
private _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._toggleShowIgnored();
|
||||
this._showIgnored = !this._showIgnored;
|
||||
break;
|
||||
case 1:
|
||||
this._toggleShowDisabled();
|
||||
@ -659,54 +568,14 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleShowIgnored() {
|
||||
this._showIgnored = !this._showIgnored;
|
||||
}
|
||||
|
||||
private _toggleShowDisabled() {
|
||||
this._showDisabled = !this._showDisabled;
|
||||
}
|
||||
|
||||
private async _removeIgnoredIntegration(ev: Event) {
|
||||
const entry = (ev.target! as any).entry;
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass!.localize(
|
||||
"ui.panel.config.integrations.ignore.confirm_delete_ignore_title",
|
||||
"name",
|
||||
this.hass.localize(`component.${entry.domain}.title`)
|
||||
),
|
||||
text: this.hass!.localize(
|
||||
"ui.panel.config.integrations.ignore.confirm_delete_ignore"
|
||||
),
|
||||
confirmText: this.hass!.localize(
|
||||
"ui.panel.config.integrations.ignore.stop_ignore"
|
||||
),
|
||||
confirm: async () => {
|
||||
const result = await deleteConfigEntry(this.hass, entry.entry_id);
|
||||
if (result.require_restart) {
|
||||
alert(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.restart_confirm"
|
||||
)
|
||||
);
|
||||
}
|
||||
this._loadConfigEntries();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _onImageLoad(ev) {
|
||||
ev.target.style.visibility = "initial";
|
||||
}
|
||||
|
||||
private _onImageError(ev) {
|
||||
ev.target.style.visibility = "hidden";
|
||||
}
|
||||
|
||||
private async _highlightEntry() {
|
||||
await nextRender();
|
||||
const entryId = this._searchParms.get("config_entry")!;
|
||||
@ -769,66 +638,18 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
padding: 8px 16px 16px;
|
||||
margin-bottom: 64px;
|
||||
}
|
||||
ha-card {
|
||||
.container > * {
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.attention {
|
||||
--ha-card-border-color: var(--error-color);
|
||||
}
|
||||
.attention .header {
|
||||
background: var(--error-color);
|
||||
color: var(--text-primary-color);
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.attention mwc-button {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
.discovered {
|
||||
--ha-card-border-color: var(--primary-color);
|
||||
}
|
||||
.discovered .header {
|
||||
background: var(--primary-color);
|
||||
color: var(--text-primary-color);
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.ignored {
|
||||
--ha-card-border-color: var(--light-theme-disabled-color);
|
||||
}
|
||||
.ignored img {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
.ignored .header {
|
||||
background: var(--light-theme-disabled-color);
|
||||
color: var(--text-primary-color);
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
margin-top: 0;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.image {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 60px;
|
||||
margin-bottom: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.none-found {
|
||||
|
||||
.empty-message {
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
.empty-message h1 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
search-input.header {
|
||||
display: block;
|
||||
position: relative;
|
||||
@ -848,27 +669,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
img {
|
||||
max-height: 100%;
|
||||
max-width: 90%;
|
||||
}
|
||||
.none-found {
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
word-wrap: break-word;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.active-filters {
|
||||
color: var(--primary-text-color);
|
||||
position: relative;
|
||||
|
@ -0,0 +1,95 @@
|
||||
import {
|
||||
customElement,
|
||||
LitElement,
|
||||
property,
|
||||
css,
|
||||
html,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { deleteConfigEntry } from "../../../data/config_entries";
|
||||
import type { IntegrationManifest } from "../../../data/integration";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { ConfigEntryExtended } from "./ha-config-integrations";
|
||||
import "./ha-integration-action-card";
|
||||
|
||||
@customElement("ha-ignored-config-entry-card")
|
||||
export class HaIgnoredConfigEntryCard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public entry!: ConfigEntryExtended;
|
||||
|
||||
@property() public manifest?: IntegrationManifest;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-integration-action-card
|
||||
.hass=${this.hass}
|
||||
.manifest=${this.manifest}
|
||||
.banner=${this.hass.localize(
|
||||
"ui.panel.config.integrations.ignore.ignored"
|
||||
)}
|
||||
.domain=${this.entry.domain}
|
||||
.localizedDomainName=${this.entry.localized_domain_name}
|
||||
.label=${this.entry.title === "Ignored"
|
||||
? // In 2020.2 we added support for entry.title. All ignored entries before
|
||||
// that have title "Ignored" so we fallback to localized domain name.
|
||||
this.entry.localized_domain_name
|
||||
: this.entry.title}
|
||||
>
|
||||
<mwc-button
|
||||
@click=${this._removeIgnoredIntegration}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.ignore.stop_ignore"
|
||||
)}
|
||||
></mwc-button>
|
||||
</ha-integration-action-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _removeIgnoredIntegration() {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass!.localize(
|
||||
"ui.panel.config.integrations.ignore.confirm_delete_ignore_title",
|
||||
"name",
|
||||
this.hass.localize(`component.${this.entry.domain}.title`)
|
||||
),
|
||||
text: this.hass!.localize(
|
||||
"ui.panel.config.integrations.ignore.confirm_delete_ignore"
|
||||
),
|
||||
confirmText: this.hass!.localize(
|
||||
"ui.panel.config.integrations.ignore.stop_ignore"
|
||||
),
|
||||
confirm: async () => {
|
||||
const result = await deleteConfigEntry(this.hass, this.entry.entry_id);
|
||||
if (result.require_restart) {
|
||||
alert(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.restart_confirm"
|
||||
)
|
||||
);
|
||||
}
|
||||
fireEvent(this, "change", undefined, {
|
||||
bubbles: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
--state-color: var(--divider-color, #e0e0e0);
|
||||
}
|
||||
|
||||
mwc-button {
|
||||
--mdc-theme-primary: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-ignored-config-entry-card": HaIgnoredConfigEntryCard;
|
||||
}
|
||||
}
|
77
src/panels/config/integrations/ha-integration-action-card.ts
Normal file
77
src/panels/config/integrations/ha-integration-action-card.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import {
|
||||
TemplateResult,
|
||||
html,
|
||||
customElement,
|
||||
LitElement,
|
||||
property,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import type { IntegrationManifest } from "../../../data/integration";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "./ha-integration-header";
|
||||
|
||||
@customElement("ha-integration-action-card")
|
||||
export class HaIntegrationActionCard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public banner!: string;
|
||||
|
||||
@property() public localizedDomainName?: string;
|
||||
|
||||
@property() public domain!: string;
|
||||
|
||||
@property() public label!: string;
|
||||
|
||||
@property() public manifest?: IntegrationManifest;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
<ha-integration-header
|
||||
.hass=${this.hass}
|
||||
.banner=${this.banner}
|
||||
.domain=${this.domain}
|
||||
.label=${this.label}
|
||||
.localizedDomainName=${this.localizedDomainName}
|
||||
.manifest=${this.manifest}
|
||||
></ha-integration-header>
|
||||
<div class="filler"></div>
|
||||
<div class="actions"><slot></slot></div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
--ha-card-border-color: var(--state-color);
|
||||
--mdc-theme-primary: var(--state-color);
|
||||
}
|
||||
.filler {
|
||||
flex: 1;
|
||||
}
|
||||
.attention {
|
||||
--state-color: var(--error-color);
|
||||
--text-on-state-color: var(--text-primary-color);
|
||||
}
|
||||
.discovered {
|
||||
--state-color: var(--primary-color);
|
||||
--text-on-state-color: var(--text-primary-color);
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 6px 0;
|
||||
height: 48px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-integration-action-card": HaIntegrationActionCard;
|
||||
}
|
||||
}
|
@ -1,4 +1,8 @@
|
||||
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "@polymer/paper-listbox";
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-item";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import { mdiAlertCircle, mdiDotsVertical, mdiOpenInNew } from "@mdi/js";
|
||||
import {
|
||||
@ -14,7 +18,9 @@ import { classMap } from "lit-html/directives/class-map";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-card";
|
||||
import {
|
||||
ConfigEntry,
|
||||
deleteConfigEntry,
|
||||
@ -23,9 +29,9 @@ import {
|
||||
reloadConfigEntry,
|
||||
updateConfigEntry,
|
||||
} from "../../../data/config_entries";
|
||||
import { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||
import { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import { domainToName, IntegrationManifest } from "../../../data/integration";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import type { IntegrationManifest } from "../../../data/integration";
|
||||
import { showConfigEntrySystemOptionsDialog } from "../../../dialogs/config-entry-system-options/show-dialog-config-entry-system-options";
|
||||
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
|
||||
import {
|
||||
@ -34,51 +40,23 @@ import {
|
||||
showPromptDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import { ConfigEntryExtended } from "./ha-config-integrations";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { ConfigEntryExtended } from "./ha-config-integrations";
|
||||
import "./ha-integration-header";
|
||||
|
||||
export interface ConfigEntryUpdatedEvent {
|
||||
entry: ConfigEntry;
|
||||
}
|
||||
|
||||
export interface ConfigEntryRemovedEvent {
|
||||
entryId: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"entry-updated": ConfigEntryUpdatedEvent;
|
||||
"entry-removed": ConfigEntryRemovedEvent;
|
||||
}
|
||||
}
|
||||
const ERROR_STATES: ConfigEntry["state"][] = [
|
||||
"migration_error",
|
||||
"setup_error",
|
||||
"setup_retry",
|
||||
];
|
||||
|
||||
const integrationsWithPanel = {
|
||||
hassio: {
|
||||
buttonLocalizeKey: "ui.panel.config.hassio.button",
|
||||
path: "/hassio/dashboard",
|
||||
},
|
||||
mqtt: {
|
||||
buttonLocalizeKey: "ui.panel.config.mqtt.button",
|
||||
path: "/config/mqtt",
|
||||
},
|
||||
zha: {
|
||||
buttonLocalizeKey: "ui.panel.config.zha.button",
|
||||
path: "/config/zha/dashboard",
|
||||
},
|
||||
ozw: {
|
||||
buttonLocalizeKey: "ui.panel.config.ozw.button",
|
||||
path: "/config/ozw/dashboard",
|
||||
},
|
||||
zwave: {
|
||||
buttonLocalizeKey: "ui.panel.config.zwave.button",
|
||||
path: "/config/zwave",
|
||||
},
|
||||
zwave_js: {
|
||||
buttonLocalizeKey: "ui.panel.config.zwave_js.button",
|
||||
path: "/config/zwave_js/dashboard",
|
||||
},
|
||||
hassio: "/hassio/dashboard",
|
||||
mqtt: "/config/mqtt",
|
||||
zha: "/config/zha/dashboard",
|
||||
ozw: "/config/ozw/dashboard",
|
||||
zwave: "/config/zwave",
|
||||
zwave_js: "/config/zwave_js/dashboard",
|
||||
};
|
||||
|
||||
@customElement("ha-integration-card")
|
||||
@ -89,7 +67,7 @@ export class HaIntegrationCard extends LitElement {
|
||||
|
||||
@property() public items!: ConfigEntryExtended[];
|
||||
|
||||
@property() public manifest!: IntegrationManifest;
|
||||
@property() public manifest?: IntegrationManifest;
|
||||
|
||||
@property() public entityRegistryEntries!: EntityRegistryEntry[];
|
||||
|
||||
@ -99,80 +77,97 @@ export class HaIntegrationCard extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
let item = this._selectededConfigEntry;
|
||||
|
||||
if (this.items.length === 1) {
|
||||
return this._renderSingleEntry(this.items[0]);
|
||||
}
|
||||
if (this.selectedConfigEntryId) {
|
||||
const configEntry = this.items.find(
|
||||
item = this.items[0];
|
||||
} else if (this.selectedConfigEntryId) {
|
||||
item = this.items.find(
|
||||
(entry) => entry.entry_id === this.selectedConfigEntryId
|
||||
);
|
||||
if (configEntry) {
|
||||
return this._renderSingleEntry(configEntry);
|
||||
}
|
||||
}
|
||||
return this._renderGroupedIntegration();
|
||||
|
||||
const hasItem = item !== undefined;
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
class="${classMap({
|
||||
single: hasItem,
|
||||
group: !hasItem,
|
||||
hasMultiple: this.items.length > 1,
|
||||
disabled: this.disabled,
|
||||
"state-not-loaded": hasItem && item!.state === "not_loaded",
|
||||
"state-failed-unload": hasItem && item!.state === "failed_unload",
|
||||
"state-error": hasItem && ERROR_STATES.includes(item!.state),
|
||||
})}"
|
||||
.configEntry=${item}
|
||||
>
|
||||
<ha-integration-header
|
||||
.hass=${this.hass}
|
||||
.banner=${this.disabled
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.disable.disabled"
|
||||
)
|
||||
: undefined}
|
||||
.domain=${this.domain}
|
||||
.label=${item
|
||||
? item.title || item.localized_domain_name || this.domain
|
||||
: undefined}
|
||||
.localizedDomainName=${item ? item.localized_domain_name : undefined}
|
||||
.manifest=${this.manifest}
|
||||
>
|
||||
${this.items.length > 1
|
||||
? html`
|
||||
<div class="back-btn" slot="above-header">
|
||||
<ha-icon-button
|
||||
icon="hass:chevron-left"
|
||||
@click=${this._back}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-integration-header>
|
||||
|
||||
${item
|
||||
? this._renderSingleEntry(item)
|
||||
: this._renderGroupedIntegration()}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderGroupedIntegration(): TemplateResult {
|
||||
return html`
|
||||
<ha-card outlined class="group ${classMap({ disabled: this.disabled })}">
|
||||
${this.disabled
|
||||
? html`<div class="header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.disable.disabled"
|
||||
)}
|
||||
</div>`
|
||||
: ""}
|
||||
<div class="group-header">
|
||||
<img
|
||||
src=${brandsUrl(this.domain, "icon")}
|
||||
referrerpolicy="no-referrer"
|
||||
@error=${this._onImageError}
|
||||
@load=${this._onImageLoad}
|
||||
/>
|
||||
<h2>
|
||||
${domainToName(this.hass.localize, this.domain)}
|
||||
</h2>
|
||||
</div>
|
||||
<paper-listbox>
|
||||
${this.items.map(
|
||||
(item) =>
|
||||
html`<paper-item
|
||||
.entryId=${item.entry_id}
|
||||
@click=${this._selectConfigEntry}
|
||||
><paper-item-body
|
||||
>${item.title ||
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.unnamed_entry"
|
||||
)}</paper-item-body
|
||||
>
|
||||
${item.state === "not_loaded"
|
||||
? html`<span>
|
||||
<ha-svg-icon
|
||||
class="error"
|
||||
.path=${mdiAlertCircle}
|
||||
></ha-svg-icon
|
||||
><paper-tooltip animation-delay="0" position="left">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.not_loaded",
|
||||
"logs_link",
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.logs"
|
||||
)
|
||||
)}
|
||||
</paper-tooltip>
|
||||
</span>`
|
||||
: ""}
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</ha-card>
|
||||
<paper-listbox>
|
||||
${this.items.map(
|
||||
(item) =>
|
||||
html`<paper-item
|
||||
.entryId=${item.entry_id}
|
||||
@click=${this._selectConfigEntry}
|
||||
><paper-item-body
|
||||
>${item.title ||
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.unnamed_entry"
|
||||
)}</paper-item-body
|
||||
>
|
||||
${ERROR_STATES.includes(item.state)
|
||||
? html`<span>
|
||||
<ha-svg-icon
|
||||
class="error"
|
||||
.path=${mdiAlertCircle}
|
||||
></ha-svg-icon
|
||||
><paper-tooltip animation-delay="0" position="left">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.state.${item.state}`
|
||||
)}
|
||||
</paper-tooltip>
|
||||
</span>`
|
||||
: ""}
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>`
|
||||
)}
|
||||
</paper-listbox>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -181,209 +176,220 @@ export class HaIntegrationCard extends LitElement {
|
||||
const services = this._getServices(item);
|
||||
const entities = this._getEntities(item);
|
||||
|
||||
let stateText: [string, ...unknown[]] | undefined;
|
||||
let stateTextExtra: TemplateResult | string | undefined;
|
||||
|
||||
if (item.disabled_by) {
|
||||
stateText = [
|
||||
"ui.panel.config.integrations.config_entry.disable.disabled_cause",
|
||||
"cause",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.disable.disabled_by.${item.disabled_by}`
|
||||
) || item.disabled_by,
|
||||
];
|
||||
if (item.state === "failed_unload") {
|
||||
stateTextExtra = html`.
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.disable_restart_confirm"
|
||||
)}.`;
|
||||
}
|
||||
} else if (item.state === "not_loaded") {
|
||||
stateText = ["ui.panel.config.integrations.config_entry.not_loaded"];
|
||||
} else if (ERROR_STATES.includes(item.state)) {
|
||||
stateText = [
|
||||
`ui.panel.config.integrations.config_entry.state.${item.state}`,
|
||||
];
|
||||
if (item.reason) {
|
||||
this.hass.loadBackendTranslation("config", item.domain);
|
||||
stateTextExtra = html`:
|
||||
${this.hass.localize(
|
||||
`component.${item.domain}.config.error.${item.reason}`
|
||||
) || item.reason}`;
|
||||
} else {
|
||||
stateTextExtra = html`
|
||||
<br />
|
||||
<a href="/config/logs"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.check_the_logs"
|
||||
)}</a
|
||||
>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
class="single integration ${classMap({
|
||||
disabled: Boolean(item.disabled_by),
|
||||
"not-loaded": !item.disabled_by && item.state === "not_loaded",
|
||||
})}"
|
||||
.configEntry=${item}
|
||||
.id=${item.entry_id}
|
||||
>
|
||||
${this.items.length > 1
|
||||
? html`<ha-icon-button
|
||||
class="back-btn"
|
||||
icon="hass:chevron-left"
|
||||
@click=${this._back}
|
||||
></ha-icon-button>`
|
||||
${stateText
|
||||
? html`
|
||||
<div class="message">
|
||||
<ha-svg-icon .path=${mdiAlertCircle}></ha-svg-icon>
|
||||
<div>
|
||||
${this.hass.localize(...stateText)}${stateTextExtra}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<div class="content">
|
||||
${devices.length || services.length || entities.length
|
||||
? html`
|
||||
<div>
|
||||
${devices.length
|
||||
? html`
|
||||
<a
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.devices",
|
||||
"count",
|
||||
devices.length
|
||||
)}</a
|
||||
>${services.length ? "," : ""}
|
||||
`
|
||||
: ""}
|
||||
${services.length
|
||||
? html`
|
||||
<a
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.services",
|
||||
"count",
|
||||
services.length
|
||||
)}</a
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
${(devices.length || services.length) && entities.length
|
||||
? this.hass.localize("ui.common.and")
|
||||
: ""}
|
||||
${entities.length
|
||||
? html`
|
||||
<a
|
||||
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.entities",
|
||||
"count",
|
||||
entities.length
|
||||
)}</a
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${item.disabled_by
|
||||
? html`<div class="header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.disable.disabled_cause",
|
||||
"cause",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.disable.disabled_by.${item.disabled_by}`
|
||||
) || item.disabled_by
|
||||
)}
|
||||
</div>`
|
||||
: item.state === "not_loaded"
|
||||
? html`<div class="header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.not_loaded",
|
||||
"logs_link",
|
||||
html`<a href="/config/logs"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.logs"
|
||||
)}</a
|
||||
>`
|
||||
)}
|
||||
</div>`
|
||||
: ""}
|
||||
<div class="card-content">
|
||||
<div class="image">
|
||||
<img
|
||||
src=${brandsUrl(item.domain, "logo")}
|
||||
referrerpolicy="no-referrer"
|
||||
@error=${this._onImageError}
|
||||
@load=${this._onImageLoad}
|
||||
/>
|
||||
</div>
|
||||
<h2>
|
||||
${item.localized_domain_name}
|
||||
</h2>
|
||||
<h3>
|
||||
${item.localized_domain_name === item.title ? "" : item.title}
|
||||
</h3>
|
||||
${devices.length || services.length || entities.length
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div>
|
||||
${item.disabled_by === "user"
|
||||
? html`<mwc-button unelevated @click=${this._handleEnable}>
|
||||
${this.hass.localize("ui.common.enable")}
|
||||
</mwc-button>`
|
||||
: item.domain in integrationsWithPanel
|
||||
? html`<a
|
||||
href=${`${integrationsWithPanel[item.domain]}?config_entry=${
|
||||
item.entry_id
|
||||
}`}
|
||||
><mwc-button>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.configure"
|
||||
)}
|
||||
</mwc-button></a
|
||||
>`
|
||||
: item.supports_options
|
||||
? html`
|
||||
<div>
|
||||
${devices.length
|
||||
? html`
|
||||
<a
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.devices",
|
||||
"count",
|
||||
devices.length
|
||||
)}</a
|
||||
>${services.length ? "," : ""}
|
||||
`
|
||||
: ""}
|
||||
${services.length
|
||||
? html`
|
||||
<a
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.services",
|
||||
"count",
|
||||
services.length
|
||||
)}</a
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
${(devices.length || services.length) && entities.length
|
||||
? this.hass.localize("ui.common.and")
|
||||
: ""}
|
||||
${entities.length
|
||||
? html`
|
||||
<a
|
||||
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.entities",
|
||||
"count",
|
||||
entities.length
|
||||
)}</a
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<mwc-button @click=${this._showOptions}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.configure"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<div>
|
||||
${item.disabled_by === "user"
|
||||
? html`<mwc-button unelevated @click=${this._handleEnable}>
|
||||
${this.hass.localize("ui.common.enable")}
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
<mwc-button @click=${this._editEntryName}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.rename"
|
||||
)}
|
||||
</mwc-button>
|
||||
${item.domain in integrationsWithPanel
|
||||
? html`<a
|
||||
href=${`${
|
||||
integrationsWithPanel[item.domain].path
|
||||
}?config_entry=${item.entry_id}`}
|
||||
><mwc-button>
|
||||
${!this.manifest
|
||||
? ""
|
||||
: html`
|
||||
<ha-button-menu corner="BOTTOM_START">
|
||||
<mwc-icon-button
|
||||
.title=${this.hass.localize("ui.common.menu")}
|
||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||
slot="trigger"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<mwc-list-item @request-selected="${this._editEntryName}">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.rename"
|
||||
)}
|
||||
</mwc-list-item>
|
||||
<mwc-list-item @request-selected="${this._handleSystemOptions}">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.system_options"
|
||||
)}
|
||||
</mwc-list-item>
|
||||
|
||||
<a
|
||||
href=${this.manifest.documentation}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<mwc-list-item hasMeta>
|
||||
${this.hass.localize(
|
||||
integrationsWithPanel[item.domain].buttonLocalizeKey
|
||||
)}
|
||||
</mwc-button></a
|
||||
>`
|
||||
: item.supports_options
|
||||
? html`
|
||||
<mwc-button @click=${this._showOptions}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.options"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<ha-button-menu corner="BOTTOM_START">
|
||||
<mwc-icon-button
|
||||
.title=${this.hass.localize("ui.common.menu")}
|
||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||
slot="trigger"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<mwc-list-item @request-selected="${this._handleSystemOptions}">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.system_options"
|
||||
)}
|
||||
</mwc-list-item>
|
||||
${!this.manifest
|
||||
? ""
|
||||
: html`
|
||||
<a
|
||||
href=${this.manifest.documentation}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<mwc-list-item hasMeta>
|
||||
"ui.panel.config.integrations.config_entry.documentation"
|
||||
)}<ha-svg-icon
|
||||
slot="meta"
|
||||
.path=${mdiOpenInNew}
|
||||
></ha-svg-icon>
|
||||
</mwc-list-item>
|
||||
</a>
|
||||
${!item.disabled_by &&
|
||||
item.state === "loaded" &&
|
||||
item.supports_unload &&
|
||||
item.source !== "system"
|
||||
? html`<mwc-list-item
|
||||
@request-selected="${this._handleReload}"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.documentation"
|
||||
)}<ha-svg-icon
|
||||
slot="meta"
|
||||
.path=${mdiOpenInNew}
|
||||
></ha-svg-icon>
|
||||
</mwc-list-item>
|
||||
</a>
|
||||
`}
|
||||
${!item.disabled_by &&
|
||||
item.state === "loaded" &&
|
||||
item.supports_unload &&
|
||||
item.source !== "system"
|
||||
? html`<mwc-list-item @request-selected="${this._handleReload}">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.reload"
|
||||
)}
|
||||
</mwc-list-item>`
|
||||
: ""}
|
||||
${item.disabled_by === "user"
|
||||
? html`<mwc-list-item @request-selected="${this._handleEnable}">
|
||||
${this.hass.localize("ui.common.enable")}
|
||||
</mwc-list-item>`
|
||||
: item.source !== "system"
|
||||
? html`<mwc-list-item
|
||||
class="warning"
|
||||
@request-selected="${this._handleDisable}"
|
||||
>
|
||||
${this.hass.localize("ui.common.disable")}
|
||||
</mwc-list-item>`
|
||||
: ""}
|
||||
${item.source !== "system"
|
||||
? html`<mwc-list-item
|
||||
class="warning"
|
||||
@request-selected="${this._handleDelete}"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.delete"
|
||||
)}
|
||||
</mwc-list-item>`
|
||||
: ""}
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
</ha-card>
|
||||
"ui.panel.config.integrations.config_entry.reload"
|
||||
)}
|
||||
</mwc-list-item>`
|
||||
: ""}
|
||||
${item.disabled_by === "user"
|
||||
? html`<mwc-list-item
|
||||
@request-selected="${this._handleEnable}"
|
||||
>
|
||||
${this.hass.localize("ui.common.enable")}
|
||||
</mwc-list-item>`
|
||||
: item.source !== "system"
|
||||
? html`<mwc-list-item
|
||||
class="warning"
|
||||
@request-selected="${this._handleDisable}"
|
||||
>
|
||||
${this.hass.localize("ui.common.disable")}
|
||||
</mwc-list-item>`
|
||||
: ""}
|
||||
${item.source !== "system"
|
||||
? html`<mwc-list-item
|
||||
class="warning"
|
||||
@request-selected="${this._handleDelete}"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.delete"
|
||||
)}
|
||||
</mwc-list-item>`
|
||||
: ""}
|
||||
</ha-button-menu>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _selectededConfigEntry(): ConfigEntryExtended | undefined {
|
||||
return this.items.length === 1
|
||||
? this.items[0]
|
||||
: this.selectedConfigEntryId
|
||||
? this.items.find(
|
||||
(entry) => entry.entry_id === this.selectedConfigEntryId
|
||||
)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private _selectConfigEntry(ev: Event) {
|
||||
this.selectedConfigEntryId = (ev.currentTarget as any).entryId;
|
||||
}
|
||||
@ -424,14 +430,6 @@ export class HaIntegrationCard extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _onImageLoad(ev) {
|
||||
ev.target.style.visibility = "initial";
|
||||
}
|
||||
|
||||
private _onImageError(ev) {
|
||||
ev.target.style.visibility = "hidden";
|
||||
}
|
||||
|
||||
private _showOptions(ev) {
|
||||
showOptionsFlowDialog(this, ev.target.closest("ha-card").configEntry);
|
||||
}
|
||||
@ -589,123 +587,115 @@ export class HaIntegrationCard extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
max-width: 500px;
|
||||
}
|
||||
ha-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
--state-color: var(--divider-color, #e0e0e0);
|
||||
--ha-card-border-color: var(--state-color);
|
||||
--state-message-color: var(--state-color);
|
||||
}
|
||||
ha-card.single {
|
||||
justify-content: space-between;
|
||||
.state-error {
|
||||
--state-color: var(--error-color);
|
||||
--text-on-state-color: var(--text-primary-color);
|
||||
}
|
||||
.state-failed-unload {
|
||||
--state-color: var(--warning-color);
|
||||
--text-on-state-color: var(--primary-text-color);
|
||||
}
|
||||
.state-not-loaded {
|
||||
--state-message-color: var(--primary-text-color);
|
||||
}
|
||||
:host(.highlight) ha-card {
|
||||
border: 1px solid var(--accent-color);
|
||||
--state-color: var(--primary-color);
|
||||
--text-on-state-color: var(--text-primary-color);
|
||||
}
|
||||
.disabled {
|
||||
--ha-card-border-color: var(--warning-color);
|
||||
|
||||
.back-btn {
|
||||
background-color: var(--state-color);
|
||||
color: var(--text-on-state-color);
|
||||
--mdc-icon-button-size: 32px;
|
||||
transition: height 0.1s;
|
||||
overflow: hidden;
|
||||
}
|
||||
.not-loaded {
|
||||
--ha-card-border-color: var(--error-color);
|
||||
.hasMultiple.single .back-btn {
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
.hasMultiple.group .back-btn {
|
||||
height: 0px;
|
||||
}
|
||||
.disabled .header {
|
||||
background: var(--warning-color);
|
||||
color: var(--text-primary-color);
|
||||
|
||||
.message {
|
||||
font-weight: bold;
|
||||
padding-bottom: 16px;
|
||||
display: flex;
|
||||
margin-left: 40px;
|
||||
}
|
||||
.not-loaded .header {
|
||||
background: var(--error-color);
|
||||
color: var(--text-primary-color);
|
||||
.message ha-svg-icon {
|
||||
color: var(--state-message-color);
|
||||
}
|
||||
.not-loaded .header a {
|
||||
color: var(--text-primary-color);
|
||||
.message div {
|
||||
flex: 1;
|
||||
margin-left: 8px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.card-content {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 0px 16px 0 72px;
|
||||
}
|
||||
ha-card.integration .card-content {
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
.card-actions {
|
||||
border-top: none;
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-right: 5px;
|
||||
padding: 8px 0 0 8px;
|
||||
height: 48px;
|
||||
}
|
||||
.group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
padding: 16px 16px 8px 16px;
|
||||
justify-content: center;
|
||||
}
|
||||
.group-header h1 {
|
||||
margin: 0;
|
||||
}
|
||||
.group-header img {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.image {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 60px;
|
||||
margin-bottom: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
img {
|
||||
max-height: 100%;
|
||||
max-width: 90%;
|
||||
}
|
||||
.none-found {
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
.actions a {
|
||||
text-decoration: none;
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
h1 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
h2 {
|
||||
min-height: 24px;
|
||||
}
|
||||
h3 {
|
||||
word-wrap: break-word;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
ha-button-menu {
|
||||
color: var(--secondary-text-color);
|
||||
--mdc-menu-min-width: 200px;
|
||||
}
|
||||
@media (min-width: 563px) {
|
||||
ha-card.group {
|
||||
position: relative;
|
||||
min-height: 164px;
|
||||
}
|
||||
paper-listbox {
|
||||
max-height: 150px;
|
||||
position: absolute;
|
||||
top: 64px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
.disabled paper-listbox {
|
||||
top: 88px;
|
||||
}
|
||||
}
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
min-height: 35px;
|
||||
}
|
||||
paper-item-body {
|
||||
word-wrap: break-word;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
mwc-list-item ha-svg-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.back-btn {
|
||||
position: absolute;
|
||||
background: rgba(var(--rgb-card-background-color), 0.6);
|
||||
border-radius: 50%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
176
src/panels/config/integrations/ha-integration-header.ts
Normal file
176
src/panels/config/integrations/ha-integration-header.ts
Normal file
@ -0,0 +1,176 @@
|
||||
import { mdiPackageVariant, mdiCloud } from "@mdi/js";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
customElement,
|
||||
property,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { domainToName, IntegrationManifest } from "../../../data/integration";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
|
||||
@customElement("ha-integration-header")
|
||||
export class HaIntegrationHeader extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public banner!: string;
|
||||
|
||||
@property() public localizedDomainName?: string;
|
||||
|
||||
@property() public domain!: string;
|
||||
|
||||
@property() public label!: string;
|
||||
|
||||
@property() public manifest?: IntegrationManifest;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
let primary: string;
|
||||
let secondary: string | undefined;
|
||||
|
||||
const domainName =
|
||||
this.localizedDomainName ||
|
||||
domainToName(this.hass.localize, this.domain, this.manifest);
|
||||
|
||||
if (this.label) {
|
||||
primary = this.label;
|
||||
secondary = primary === domainName ? undefined : domainName;
|
||||
} else {
|
||||
primary = domainName;
|
||||
}
|
||||
|
||||
const icons: [string, string][] = [];
|
||||
|
||||
if (this.manifest) {
|
||||
if (!this.manifest.is_built_in) {
|
||||
icons.push([
|
||||
mdiPackageVariant,
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.provided_by_custom_integration"
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
if (
|
||||
this.manifest.iot_class &&
|
||||
this.manifest.iot_class.startsWith("cloud_")
|
||||
) {
|
||||
icons.push([
|
||||
mdiCloud,
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.depends_on_cloud"
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
${!this.banner
|
||||
? ""
|
||||
: html`<div class="banner">
|
||||
${this.banner}
|
||||
</div>`}
|
||||
<slot name="above-header"></slot>
|
||||
<div class="header">
|
||||
<img
|
||||
src=${brandsUrl(this.domain, "icon")}
|
||||
referrerpolicy="no-referrer"
|
||||
@error=${this._onImageError}
|
||||
@load=${this._onImageLoad}
|
||||
/>
|
||||
<div class="info">
|
||||
<div class="primary">${primary}</div>
|
||||
${secondary ? html`<div class="secondary">${secondary}</div>` : ""}
|
||||
</div>
|
||||
${icons.length === 0
|
||||
? ""
|
||||
: html`
|
||||
<div class="icons">
|
||||
${icons.map(
|
||||
([icon, description]) => html`
|
||||
<span>
|
||||
<ha-svg-icon .path=${icon}></ha-svg-icon>
|
||||
<paper-tooltip animation-delay="0"
|
||||
>${description}</paper-tooltip
|
||||
>
|
||||
</span>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _onImageLoad(ev) {
|
||||
ev.target.style.visibility = "initial";
|
||||
}
|
||||
|
||||
private _onImageError(ev) {
|
||||
ev.target.style.visibility = "hidden";
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.banner {
|
||||
background-color: var(--state-color);
|
||||
color: var(--text-on-state-color);
|
||||
text-align: center;
|
||||
padding: 2px;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding: 16px 8px 8px 16px;
|
||||
}
|
||||
.header img {
|
||||
margin-right: 16px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.header .info {
|
||||
align-self: center;
|
||||
}
|
||||
.header .info div {
|
||||
word-wrap: break-word;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.primary {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.secondary {
|
||||
font-size: 14px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.icons {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 16px;
|
||||
color: var(--text-on-state-color, var(--secondary-text-color));
|
||||
background-color: var(--state-color, #e0e0e0);
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
padding: 1px 4px 2px;
|
||||
}
|
||||
.icons ha-svg-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
paper-tooltip {
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-integration-header": HaIntegrationHeader;
|
||||
}
|
||||
}
|
@ -76,9 +76,7 @@ class DialogZHADeviceChildren extends LitElement {
|
||||
},
|
||||
};
|
||||
|
||||
public showDialog(
|
||||
params: ZHADeviceChildrenDialogParams
|
||||
): void {
|
||||
public showDialog(params: ZHADeviceChildrenDialogParams): void {
|
||||
this._device = params.device;
|
||||
this._fetchData();
|
||||
}
|
||||
|
@ -8,42 +8,62 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { createCloseHeading } from "../../../../../components/ha-dialog";
|
||||
import { mdiCheckCircle, mdiCloseCircle } from "@mdi/js";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { haStyleDialog } from "../../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { ZHAReconfigureDeviceDialogParams } from "./show-dialog-zha-reconfigure-device";
|
||||
import { IronAutogrowTextareaElement } from "@polymer/iron-autogrow-textarea";
|
||||
import "@polymer/paper-input/paper-textarea";
|
||||
import "../../../../../components/ha-circular-progress";
|
||||
import { LOG_OUTPUT, reconfigureNode } from "../../../../../data/zha";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import {
|
||||
AttributeConfigurationStatus,
|
||||
Cluster,
|
||||
ClusterConfigurationEvent,
|
||||
ClusterConfigurationStatus,
|
||||
fetchClustersForZhaNode,
|
||||
reconfigureNode,
|
||||
ZHA_CHANNEL_CFG_DONE,
|
||||
ZHA_CHANNEL_MSG_BIND,
|
||||
ZHA_CHANNEL_MSG_CFG_RPT,
|
||||
} from "../../../../../data/zha";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { createCloseHeading } from "../../../../../components/ha-dialog";
|
||||
|
||||
@customElement("dialog-zha-reconfigure-device")
|
||||
class DialogZHAReconfigureDevice extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@internalProperty() private _active = false;
|
||||
@internalProperty() private _status?: string;
|
||||
|
||||
@internalProperty() private _formattedEvents = "";
|
||||
@internalProperty() private _stages?: string[];
|
||||
|
||||
@internalProperty()
|
||||
private _params: ZHAReconfigureDeviceDialogParams | undefined = undefined;
|
||||
@internalProperty() private _clusterConfigurationStatuses?: Map<
|
||||
number,
|
||||
ClusterConfigurationStatus
|
||||
> = new Map();
|
||||
|
||||
private _subscribed?: Promise<() => Promise<void>>;
|
||||
@internalProperty() private _params:
|
||||
| ZHAReconfigureDeviceDialogParams
|
||||
| undefined = undefined;
|
||||
|
||||
private _reconfigureDeviceTimeoutHandle: any = undefined;
|
||||
@internalProperty() private _allSuccessful = true;
|
||||
|
||||
public async showDialog(
|
||||
params: ZHAReconfigureDeviceDialogParams
|
||||
): Promise<void> {
|
||||
@internalProperty() private _showDetails = false;
|
||||
|
||||
private _subscribed?: Promise<UnsubscribeFunc>;
|
||||
|
||||
public showDialog(params: ZHAReconfigureDeviceDialogParams): void {
|
||||
this._params = params;
|
||||
this._subscribe(params);
|
||||
this._stages = undefined;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._unsubscribe();
|
||||
this._formattedEvents = "";
|
||||
this._params = undefined;
|
||||
this._status = undefined;
|
||||
this._stages = undefined;
|
||||
this._clusterConfigurationStatuses = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@ -51,58 +71,311 @@ class DialogZHAReconfigureDevice extends LitElement {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
hideActions
|
||||
@closing="${this.closeDialog}"
|
||||
@closed="${this.closeDialog}"
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize(`ui.dialogs.zha_reconfigure_device.heading`)
|
||||
this.hass.localize(`ui.dialogs.zha_reconfigure_device.heading`) +
|
||||
": " +
|
||||
(this._params?.device.user_given_name || this._params?.device.name)
|
||||
)}
|
||||
>
|
||||
<div class="searching">
|
||||
${this._active
|
||||
? html`
|
||||
<h1>
|
||||
${this._params?.device.user_given_name ||
|
||||
this._params?.device.name}
|
||||
</h1>
|
||||
<ha-circular-progress
|
||||
active
|
||||
alt="Searching"
|
||||
></ha-circular-progress>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<paper-textarea
|
||||
readonly
|
||||
max-rows="10"
|
||||
class="log"
|
||||
value="${this._formattedEvents}"
|
||||
>
|
||||
</paper-textarea>
|
||||
${!this._status
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.zha_reconfigure_device.introduction"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<em>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.zha_reconfigure_device.battery_device_warning"
|
||||
)}
|
||||
</em>
|
||||
</p>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._startReconfiguration}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.zha_reconfigure_device.start_reconfiguration"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
${this._status === "started"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
<div class="status">
|
||||
<p>
|
||||
<b>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.zha_reconfigure_device.in_progress"
|
||||
)}
|
||||
</b>
|
||||
</p>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.zha_reconfigure_device.run_in_background"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.dialogs.generic.close")}
|
||||
</mwc-button>
|
||||
<mwc-button slot="secondaryAction" @click=${this._toggleDetails}>
|
||||
${this._showDetails
|
||||
? this.hass.localize(
|
||||
`ui.dialogs.zha_reconfigure_device.button_hide`
|
||||
)
|
||||
: this.hass.localize(
|
||||
`ui.dialogs.zha_reconfigure_device.button_show`
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
${this._status === "failed"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCloseCircle}
|
||||
class="failed"
|
||||
></ha-svg-icon>
|
||||
<div class="status">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.zha_reconfigure_device.configuration_failed"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.dialogs.generic.close")}
|
||||
</mwc-button>
|
||||
<mwc-button slot="secondaryAction" @click=${this._toggleDetails}>
|
||||
${this._showDetails
|
||||
? this.hass.localize(
|
||||
`ui.dialogs.zha_reconfigure_device.button_hide`
|
||||
)
|
||||
: this.hass.localize(
|
||||
`ui.dialogs.zha_reconfigure_device.button_show`
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
${this._status === "finished"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCheckCircle}
|
||||
class="success"
|
||||
></ha-svg-icon>
|
||||
<div class="status">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.zha_reconfigure_device.configuration_complete"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.dialogs.generic.close")}
|
||||
</mwc-button>
|
||||
<mwc-button slot="secondaryAction" @click=${this._toggleDetails}>
|
||||
${this._showDetails
|
||||
? this.hass.localize(
|
||||
`ui.dialogs.zha_reconfigure_device.button_hide`
|
||||
)
|
||||
: this.hass.localize(
|
||||
`ui.dialogs.zha_reconfigure_device.button_show`
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
${this._stages
|
||||
? html`
|
||||
<div class="stages">
|
||||
${this._stages.map(
|
||||
(stage) => html`
|
||||
<span class="stage">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCheckCircle}
|
||||
class="success"
|
||||
></ha-svg-icon>
|
||||
${stage}
|
||||
</span>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this._showDetails
|
||||
? html`
|
||||
<div class="wrapper">
|
||||
<h2 class="grid-item">
|
||||
${this.hass.localize(
|
||||
`ui.dialogs.zha_reconfigure_device.cluster_header`
|
||||
)}
|
||||
</h2>
|
||||
<h2 class="grid-item">
|
||||
${this.hass.localize(
|
||||
`ui.dialogs.zha_reconfigure_device.bind_header`
|
||||
)}
|
||||
</h2>
|
||||
<h2 class="grid-item">
|
||||
${this.hass.localize(
|
||||
`ui.dialogs.zha_reconfigure_device.reporting_header`
|
||||
)}
|
||||
</h2>
|
||||
|
||||
${this._clusterConfigurationStatuses!.size > 0
|
||||
? html`
|
||||
${Array.from(
|
||||
this._clusterConfigurationStatuses!.values()
|
||||
).map(
|
||||
(clusterStatus) => html`
|
||||
<div class="grid-item">
|
||||
${clusterStatus.cluster.name}
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
${clusterStatus.bindSuccess !== undefined
|
||||
? clusterStatus.bindSuccess
|
||||
? html`
|
||||
<span class="stage">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCheckCircle}
|
||||
class="success"
|
||||
></ha-svg-icon>
|
||||
</span>
|
||||
`
|
||||
: html`
|
||||
<span class="stage">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCloseCircle}
|
||||
class="failed"
|
||||
></ha-svg-icon>
|
||||
</span>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
${clusterStatus.attributes.size > 0
|
||||
? html`
|
||||
<div class="attributes">
|
||||
<div class="grid-item">
|
||||
${this.hass.localize(
|
||||
`ui.dialogs.zha_reconfigure_device.attribute`
|
||||
)}
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
`ui.dialogs.zha_reconfigure_device.min_max_change`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
${Array.from(
|
||||
clusterStatus.attributes.values()
|
||||
).map(
|
||||
(attribute) => html`
|
||||
<span class="grid-item">
|
||||
${attribute.name}:
|
||||
${attribute.success
|
||||
? html`
|
||||
<span class="stage">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCheckCircle}
|
||||
class="success"
|
||||
></ha-svg-icon>
|
||||
</span>
|
||||
`
|
||||
: html`
|
||||
<span class="stage">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCloseCircle}
|
||||
class="failed"
|
||||
></ha-svg-icon>
|
||||
</span>
|
||||
`}
|
||||
</span>
|
||||
<div class="grid-item">
|
||||
${attribute.min}/${attribute.max}/${attribute.change}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleMessage(message: any): void {
|
||||
if (message.type === LOG_OUTPUT) {
|
||||
this._formattedEvents += message.log_entry.message + "\n";
|
||||
const paperTextArea = this.shadowRoot!.querySelector("paper-textarea");
|
||||
if (paperTextArea) {
|
||||
const textArea = (paperTextArea.inputElement as IronAutogrowTextareaElement)
|
||||
.textarea;
|
||||
textArea.scrollTop = textArea.scrollHeight;
|
||||
private async _startReconfiguration(): Promise<void> {
|
||||
if (!this.hass || !this._params) {
|
||||
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<number, AttributeConfigurationStatus>(),
|
||||
},
|
||||
]
|
||||
)
|
||||
);
|
||||
this._subscribe(this._params);
|
||||
this._status = "started";
|
||||
}
|
||||
|
||||
private _handleMessage(message: ClusterConfigurationEvent): void {
|
||||
if (message.type === ZHA_CHANNEL_CFG_DONE) {
|
||||
this._unsubscribe();
|
||||
this._status = this._allSuccessful ? "finished" : "failed";
|
||||
} else {
|
||||
const clusterConfigurationStatus = this._clusterConfigurationStatuses!.get(
|
||||
message.zha_channel_msg_data.cluster_id
|
||||
);
|
||||
if (message.type === ZHA_CHANNEL_MSG_BIND) {
|
||||
if (!this._stages) {
|
||||
this._stages = ["binding"];
|
||||
}
|
||||
const success = message.zha_channel_msg_data.success;
|
||||
clusterConfigurationStatus!.bindSuccess = success;
|
||||
this._allSuccessful = this._allSuccessful && success;
|
||||
}
|
||||
if (message.type === ZHA_CHANNEL_MSG_CFG_RPT) {
|
||||
if (this._stages && !this._stages.includes("reporting")) {
|
||||
this._stages.push("reporting");
|
||||
}
|
||||
const attributes = message.zha_channel_msg_data.attributes;
|
||||
Object.keys(attributes).forEach((name) => {
|
||||
const attribute = attributes[name];
|
||||
clusterConfigurationStatus!.attributes.set(attribute.id, attribute);
|
||||
this._allSuccessful = this._allSuccessful && attribute.success;
|
||||
});
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private _unsubscribe(): void {
|
||||
this._active = false;
|
||||
if (this._reconfigureDeviceTimeoutHandle) {
|
||||
clearTimeout(this._reconfigureDeviceTimeoutHandle);
|
||||
}
|
||||
if (this._subscribed) {
|
||||
this._subscribed.then((unsub) => unsub());
|
||||
this._subscribed = undefined;
|
||||
@ -113,33 +386,66 @@ class DialogZHAReconfigureDevice extends LitElement {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
this._active = true;
|
||||
this._subscribed = reconfigureNode(
|
||||
this.hass,
|
||||
params.device.ieee,
|
||||
this._handleMessage.bind(this)
|
||||
);
|
||||
this._reconfigureDeviceTimeoutHandle = setTimeout(
|
||||
() => this._unsubscribe(),
|
||||
60000
|
||||
);
|
||||
}
|
||||
|
||||
private _toggleDetails() {
|
||||
this._showDetails = !this._showDetails;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-circular-progress {
|
||||
padding: 20px;
|
||||
.wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 1fr 2fr;
|
||||
}
|
||||
.searching {
|
||||
margin-top: 20px;
|
||||
.attributes {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.grid-item {
|
||||
border: 1px solid;
|
||||
padding: 7px;
|
||||
}
|
||||
.success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.failed {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.flex-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.log {
|
||||
padding: 16px;
|
||||
|
||||
.stages {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.stage ha-svg-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.stage {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
ha-svg-icon {
|
||||
width: 68px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.flex-container ha-circular-progress,
|
||||
.flex-container ha-svg-icon {
|
||||
margin-right: 20px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { computeRTL } from "../../../../../common/util/compute_rtl";
|
||||
@ -20,6 +21,12 @@ import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import "../../../ha-config-section";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import {
|
||||
fetchZHAConfiguration,
|
||||
updateZHAConfiguration,
|
||||
ZHAConfiguration,
|
||||
} from "../../../../../data/zha";
|
||||
|
||||
export const zhaTabs: PageNavigation[] = [
|
||||
{
|
||||
@ -51,6 +58,15 @@ class ZHAConfigDashboard extends LitElement {
|
||||
|
||||
@property() public configEntryId?: string;
|
||||
|
||||
@property() private _configuration?: ZHAConfiguration;
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (this.hass) {
|
||||
this._fetchConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
@ -60,10 +76,11 @@ class ZHAConfigDashboard extends LitElement {
|
||||
.tabs=${zhaTabs}
|
||||
back-path="/config/integrations"
|
||||
>
|
||||
<ha-card header="Zigbee Network">
|
||||
<div class="card-content">
|
||||
In the future you can change network settings for ZHA here.
|
||||
</div>
|
||||
<ha-card
|
||||
header=${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.shortcuts_title"
|
||||
)}
|
||||
>
|
||||
${this.configEntryId
|
||||
? html`<div class="card-actions">
|
||||
<a
|
||||
@ -87,6 +104,38 @@ class ZHAConfigDashboard extends LitElement {
|
||||
</div>`
|
||||
: ""}
|
||||
</ha-card>
|
||||
${this._configuration
|
||||
? Object.entries(this._configuration.schemas).map(
|
||||
([section, schema]) => html` <ha-card
|
||||
header=${this.hass.localize(
|
||||
`ui.panel.config.zha.configuration_page.${section}.title`
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
<ha-form
|
||||
.schema=${schema}
|
||||
.data=${this._configuration!.data[section]}
|
||||
@value-changed=${this._dataChanged}
|
||||
.section=${section}
|
||||
.computeLabel=${this._computeLabelCallback(
|
||||
this.hass.localize,
|
||||
section
|
||||
)}
|
||||
></ha-form>
|
||||
</div>
|
||||
</ha-card>`
|
||||
)
|
||||
: ""}
|
||||
<ha-card>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._updateConfiguration}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.update_button"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
<a href="/config/zha/add" slot="fab">
|
||||
<ha-fab
|
||||
.label=${this.hass.localize("ui.panel.config.zha.add_device")}
|
||||
@ -100,6 +149,26 @@ class ZHAConfigDashboard extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchConfiguration(): Promise<void> {
|
||||
this._configuration = await fetchZHAConfiguration(this.hass!);
|
||||
}
|
||||
|
||||
private _dataChanged(ev) {
|
||||
this._configuration!.data[ev.currentTarget!.section] = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _updateConfiguration(): Promise<any> {
|
||||
await updateZHAConfiguration(this.hass!, this._configuration!.data);
|
||||
}
|
||||
|
||||
private _computeLabelCallback(localize, section: string) {
|
||||
// Returns a callback for ha-form to calculate labels per schema object
|
||||
return (schema) =>
|
||||
localize(
|
||||
`ui.panel.config.zha.configuration_page.${section}.${schema.name}`
|
||||
) || schema.name;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyle,
|
||||
|
@ -159,7 +159,7 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
if (!newName && !newEntityId) {
|
||||
return new Promise((resolve) => resolve());
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return updateEntityRegistryEntry(this.hass!, entity.entity_id, {
|
||||
@ -177,7 +177,7 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _computeEntityName(entity: EntityRegistryEntry): string {
|
||||
private _computeEntityName(entity: EntityRegistryEntry): string | null {
|
||||
if (this.hass.states[entity.entity_id]) {
|
||||
return computeStateName(this.hass.states[entity.entity_id]);
|
||||
}
|
||||
|
@ -17,8 +17,8 @@ import {
|
||||
refreshTopology,
|
||||
ZHADevice,
|
||||
} from "../../../../../data/zha";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "../../../../../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import { Network, Edge, Node, EdgeOptions } from "vis-network";
|
||||
import "../../../../../common/search/search-input";
|
||||
import "../../../../../components/device/ha-device-picker";
|
||||
@ -29,12 +29,17 @@ import { formatAsPaddedHex } from "./functions";
|
||||
import { DeviceRegistryEntry } from "../../../../../data/device_registry";
|
||||
import "../../../../../components/ha-checkbox";
|
||||
import type { HaCheckbox } from "../../../../../components/ha-checkbox";
|
||||
import { zhaTabs } from "./zha-config-dashboard";
|
||||
|
||||
@customElement("zha-network-visualization-page")
|
||||
export class ZHANetworkVisualizationPage extends LitElement {
|
||||
@property({ type: Object }) public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ type: Boolean }) public isWide!: boolean;
|
||||
|
||||
@property()
|
||||
public zoomedDeviceId?: string;
|
||||
@ -133,9 +138,12 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<hass-subpage
|
||||
<hass-tabs-subpage
|
||||
.tabs=${zhaTabs}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
.route=${this.route}
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.zha.visualization.header"
|
||||
)}
|
||||
@ -172,7 +180,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
>
|
||||
</div>
|
||||
<div id="visualization"></div>
|
||||
</hass-subpage>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,262 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiCheckCircle, mdiCloseCircle } from "@mdi/js";
|
||||
import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import "../../../../../components/ha-circular-progress";
|
||||
import { createCloseHeading } from "../../../../../components/ha-dialog";
|
||||
import { haStyleDialog } from "../../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { ZWaveJSReinterviewNodeDialogParams } from "./show-dialog-zwave_js-reinterview-node";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { reinterviewNode } from "../../../../../data/zwave_js";
|
||||
|
||||
@customElement("dialog-zwave_js-reinterview-node")
|
||||
class DialogZWaveJSReinterviewNode extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@internalProperty() private entry_id?: string;
|
||||
|
||||
@internalProperty() private node_id?: number;
|
||||
|
||||
@internalProperty() private _status?: string;
|
||||
|
||||
@internalProperty() private _stages?: string[];
|
||||
|
||||
private _subscribed?: Promise<UnsubscribeFunc>;
|
||||
|
||||
public async showDialog(
|
||||
params: ZWaveJSReinterviewNodeDialogParams
|
||||
): Promise<void> {
|
||||
this._stages = undefined;
|
||||
this.entry_id = params.entry_id;
|
||||
this.node_id = params.node_id;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.entry_id) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.panel.config.zwave_js.reinterview_node.title")
|
||||
)}
|
||||
>
|
||||
${!this._status
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.reinterview_node.introduction"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<em>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.reinterview_node.battery_device_warning"
|
||||
)}
|
||||
</em>
|
||||
</p>
|
||||
<mwc-button slot="primaryAction" @click=${this._startReinterview}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.reinterview_node.start_reinterview"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
${this._status === "started"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
<div class="status">
|
||||
<p>
|
||||
<b>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.reinterview_node.in_progress"
|
||||
)}
|
||||
</b>
|
||||
</p>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.reinterview_node.run_in_background"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
${this._status === "failed"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCloseCircle}
|
||||
class="failed"
|
||||
></ha-svg-icon>
|
||||
<div class="status">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.reinterview_node.interview_failed"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
${this._status === "finished"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCheckCircle}
|
||||
class="success"
|
||||
></ha-svg-icon>
|
||||
<div class="status">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.reinterview_node.interview_complete"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
${this._stages
|
||||
? html`
|
||||
<div class="stages">
|
||||
${this._stages.map(
|
||||
(stage) => html`
|
||||
<span class="stage">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCheckCircle}
|
||||
class="success"
|
||||
></ha-svg-icon>
|
||||
${stage}
|
||||
</span>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _startReinterview(): void {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
this._subscribed = reinterviewNode(
|
||||
this.hass,
|
||||
this.entry_id!,
|
||||
this.node_id!,
|
||||
this._handleMessage.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
private _handleMessage(message: any): void {
|
||||
if (message.event === "interview started") {
|
||||
this._status = "started";
|
||||
}
|
||||
if (message.event === "interview stage completed") {
|
||||
if (this._stages === undefined) {
|
||||
this._stages = [message.stage];
|
||||
} else {
|
||||
this._stages = [...this._stages, message.stage];
|
||||
}
|
||||
}
|
||||
if (message.event === "interview failed") {
|
||||
this._unsubscribe();
|
||||
this._status = "failed";
|
||||
}
|
||||
if (message.event === "interview completed") {
|
||||
this._unsubscribe();
|
||||
this._status = "finished";
|
||||
}
|
||||
}
|
||||
|
||||
private _unsubscribe(): void {
|
||||
if (this._subscribed) {
|
||||
this._subscribed.then((unsub) => unsub());
|
||||
this._subscribed = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this.entry_id = undefined;
|
||||
this.node_id = undefined;
|
||||
this._status = undefined;
|
||||
this._stages = undefined;
|
||||
|
||||
this._unsubscribe();
|
||||
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
.success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.failed {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.flex-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stages {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.stage ha-svg-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.stage {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
ha-svg-icon {
|
||||
width: 68px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.flex-container ha-circular-progress,
|
||||
.flex-container ha-svg-icon {
|
||||
margin-right: 20px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-zwave_js-reinterview-node": DialogZWaveJSReinterviewNode;
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
|
||||
export interface ZWaveJSReinterviewNodeDialogParams {
|
||||
entry_id: string;
|
||||
node_id: number;
|
||||
}
|
||||
|
||||
export const loadReinterviewNodeDialog = () =>
|
||||
import("./dialog-zwave_js-reinterview-node");
|
||||
|
||||
export const showZWaveJSReinterviewNodeDialog = (
|
||||
element: HTMLElement,
|
||||
reinterviewNodeDialogParams: ZWaveJSReinterviewNodeDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-zwave_js-reinterview-node",
|
||||
dialogImport: loadReinterviewNodeDialog,
|
||||
dialogParams: reinterviewNodeDialogParams,
|
||||
});
|
||||
};
|
@ -17,9 +17,11 @@ import "../../../../../components/ha-svg-icon";
|
||||
import "../../../../../components/ha-icon-next";
|
||||
import { getSignedPath } from "../../../../../data/auth";
|
||||
import {
|
||||
fetchDataCollectionStatus,
|
||||
fetchNetworkStatus,
|
||||
fetchNodeStatus,
|
||||
NodeStatus,
|
||||
setDataCollectionPreference,
|
||||
ZWaveJSNetwork,
|
||||
ZWaveJSNode,
|
||||
} from "../../../../../data/zwave_js";
|
||||
@ -55,6 +57,8 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
|
||||
@internalProperty() private _icon = mdiCircle;
|
||||
|
||||
@internalProperty() private _dataCollectionOptIn?: boolean;
|
||||
|
||||
protected firstUpdated() {
|
||||
if (this.hass) {
|
||||
this._fetchData();
|
||||
@ -167,6 +171,39 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
<h1>Third-Party Data Reporting</h1>
|
||||
${this._dataCollectionOptIn !== undefined
|
||||
? html`
|
||||
<ha-switch
|
||||
.checked=${this._dataCollectionOptIn === true}
|
||||
@change=${this._dataCollectionToggled}
|
||||
></ha-switch>
|
||||
`
|
||||
: html`
|
||||
<ha-circular-progress
|
||||
size="small"
|
||||
active
|
||||
></ha-circular-progress>
|
||||
`}
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
Enable the reporting of anonymized telemetry and
|
||||
statistics to the <em>Z-Wave JS organization</em>. This
|
||||
data will be used to focus development efforts and improve
|
||||
the user experience. Information about the data that is
|
||||
collected and how it is used, including an example of the
|
||||
data collected, can be found in the
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://zwave-js.github.io/node-zwave-js/#/data-collection/data-collection?id=usage-statistics"
|
||||
>Z-Wave JS data collection documentation</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
: ``}
|
||||
<button class="link dump" @click=${this._dumpDebugClicked}>
|
||||
@ -183,11 +220,22 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
if (!this.configEntryId) {
|
||||
return;
|
||||
}
|
||||
this._network = await fetchNetworkStatus(this.hass!, this.configEntryId);
|
||||
const [network, dataCollectionStatus] = await Promise.all([
|
||||
fetchNetworkStatus(this.hass!, this.configEntryId),
|
||||
fetchDataCollectionStatus(this.hass!, this.configEntryId),
|
||||
]);
|
||||
|
||||
this._network = network;
|
||||
|
||||
this._status = this._network.client.state;
|
||||
if (this._status === "connected") {
|
||||
this._icon = mdiCheckCircle;
|
||||
}
|
||||
|
||||
this._dataCollectionOptIn =
|
||||
dataCollectionStatus.opted_in === true ||
|
||||
dataCollectionStatus.enabled === true;
|
||||
|
||||
this._fetchNodeStatus();
|
||||
}
|
||||
|
||||
@ -213,6 +261,14 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _dataCollectionToggled(ev) {
|
||||
setDataCollectionPreference(
|
||||
this.hass!,
|
||||
this.configEntryId!,
|
||||
ev.target.checked
|
||||
);
|
||||
}
|
||||
|
||||
private async _dumpDebugClicked() {
|
||||
await this._fetchNodeStatus();
|
||||
|
||||
@ -321,8 +377,19 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
}
|
||||
.card-header h1 {
|
||||
flex: 1;
|
||||
}
|
||||
.card-header ha-switch {
|
||||
width: 48px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
margin: 0 auto;
|
||||
margin: 0px auto 24px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ import { HomeAssistant } from "../../../../../types";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
|
||||
import { mdiServerNetwork } from "@mdi/js";
|
||||
import { mdiServerNetwork, mdiMathLog } from "@mdi/js";
|
||||
|
||||
export const configTabs: PageNavigation[] = [
|
||||
{
|
||||
@ -15,6 +15,11 @@ export const configTabs: PageNavigation[] = [
|
||||
path: `/config/zwave_js/dashboard`,
|
||||
iconPath: mdiServerNetwork,
|
||||
},
|
||||
{
|
||||
translationKey: "ui.panel.config.zwave_js.navigation.logs",
|
||||
path: `/config/zwave_js/logs`,
|
||||
iconPath: mdiMathLog,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("zwave_js-config-router")
|
||||
@ -41,6 +46,10 @@ class ZWaveJSConfigRouter extends HassRouterPage {
|
||||
tag: "zwave_js-node-config",
|
||||
load: () => import("./zwave_js-node-config"),
|
||||
},
|
||||
logs: {
|
||||
tag: "zwave_js-logs",
|
||||
load: () => import("./zwave_js-logs"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,157 @@
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
property,
|
||||
customElement,
|
||||
LitElement,
|
||||
CSSResultArray,
|
||||
internalProperty,
|
||||
query,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
|
||||
import {
|
||||
fetchZWaveJSLogConfig,
|
||||
setZWaveJSLogLevel,
|
||||
subscribeZWaveJSLogs,
|
||||
ZWaveJSLogConfig,
|
||||
} from "../../../../../data/zwave_js";
|
||||
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant, Route } from "../../../../../types";
|
||||
import { configTabs } from "./zwave_js-config-router";
|
||||
import "../../../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
|
||||
@customElement("zwave_js-logs")
|
||||
class ZWaveJSLogs extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Object }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property() public configEntryId!: string;
|
||||
|
||||
@internalProperty() private _logConfig?: ZWaveJSLogConfig;
|
||||
|
||||
@query("textarea", true) private _textarea?: HTMLTextAreaElement;
|
||||
|
||||
public hassSubscribe(): Array<UnsubscribeFunc | Promise<UnsubscribeFunc>> {
|
||||
return [
|
||||
subscribeZWaveJSLogs(this.hass, this.configEntryId, (log) => {
|
||||
if (!this.hasUpdated) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(log.message)) {
|
||||
for (const line of log.message) {
|
||||
this._textarea!.value += `${line}\n`;
|
||||
}
|
||||
} else {
|
||||
this._textarea!.value += `${log.message}\n`;
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${configTabs}
|
||||
>
|
||||
<div class="container">
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
<h1>
|
||||
${this.hass.localize("ui.panel.config.zwave_js.logs.title")}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
${this._logConfig
|
||||
? html`
|
||||
<paper-dropdown-menu
|
||||
dynamic-align
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.logs.log_level"
|
||||
)}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
.selected=${this._logConfig.level}
|
||||
attr-for-selected="value"
|
||||
@iron-select=${this._dropdownSelected}
|
||||
>
|
||||
<paper-item value="error">Error</paper-item>
|
||||
<paper-item value="warn">Warn</paper-item>
|
||||
<paper-item value="info">Info</paper-item>
|
||||
<paper-item value="verbose">Verbose</paper-item>
|
||||
<paper-item value="debug">Debug</paper-item>
|
||||
<paper-item value="silly">Silly</paper-item>
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</ha-card>
|
||||
<textarea readonly></textarea>
|
||||
</div>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._fetchData();
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
if (!this.configEntryId) {
|
||||
return;
|
||||
}
|
||||
this._logConfig = await fetchZWaveJSLogConfig(
|
||||
this.hass!,
|
||||
this.configEntryId
|
||||
);
|
||||
}
|
||||
|
||||
private _dropdownSelected(ev) {
|
||||
if (ev.target === undefined || this._logConfig === undefined) {
|
||||
return;
|
||||
}
|
||||
if (this._logConfig.level === ev.target.selected) {
|
||||
return;
|
||||
}
|
||||
setZWaveJSLogLevel(this.hass!, this.configEntryId, ev.target.selected);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
}
|
||||
textarea {
|
||||
flex-grow: 1;
|
||||
padding: 16px;
|
||||
}
|
||||
ha-card {
|
||||
margin: 16px 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"zwave_js-logs": ZWaveJSLogs;
|
||||
}
|
||||
}
|
@ -1,3 +1,9 @@
|
||||
import {
|
||||
mdiCheckCircle,
|
||||
mdiCircle,
|
||||
mdiProgressClock,
|
||||
mdiCloseCircle,
|
||||
} from "@mdi/js";
|
||||
import "../../../../../components/ha-settings-row";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
@ -24,6 +30,7 @@ import {
|
||||
fetchNodeConfigParameters,
|
||||
setNodeConfigParameter,
|
||||
ZWaveJSNodeConfigParams,
|
||||
ZWaveJSSetConfigParamResult,
|
||||
} from "../../../../../data/zwave_js";
|
||||
import "../../../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
@ -38,6 +45,13 @@ import {
|
||||
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
|
||||
const icons = {
|
||||
accepted: mdiCheckCircle,
|
||||
queued: mdiProgressClock,
|
||||
error: mdiCloseCircle,
|
||||
};
|
||||
|
||||
const getDevice = memoizeOne(
|
||||
(
|
||||
@ -77,7 +91,12 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
|
||||
@property({ type: Array })
|
||||
private _deviceRegistryEntries?: DeviceRegistryEntry[];
|
||||
|
||||
@internalProperty() private _config?: ZWaveJSNodeConfigParams[];
|
||||
@internalProperty() private _config?: ZWaveJSNodeConfigParams;
|
||||
|
||||
@internalProperty() private _results: Record<
|
||||
string,
|
||||
ZWaveJSSetConfigParamResult
|
||||
> = {};
|
||||
|
||||
@internalProperty() private _error?: string;
|
||||
|
||||
@ -178,6 +197,7 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _generateConfigBox(id, item): TemplateResult {
|
||||
const result = this._results[id];
|
||||
const labelAndDescription = html`
|
||||
<span slot="heading">${item.metadata.label}</span>
|
||||
<span slot="description">
|
||||
@ -192,6 +212,26 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
|
||||
)}
|
||||
</em>`
|
||||
: ""}
|
||||
${result?.status
|
||||
? html` <p
|
||||
class="result ${classMap({
|
||||
[result.status]: true,
|
||||
})}"
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${icons[result.status] ? icons[result.status] : mdiCircle}
|
||||
class="result-icon"
|
||||
slot="item-icon"
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_config.set_param_" +
|
||||
result.status
|
||||
)}
|
||||
${result.status === "error" && result.error
|
||||
? html` <br /><em>${result.error}</em> `
|
||||
: ""}
|
||||
</p>`
|
||||
: ""}
|
||||
</span>
|
||||
`;
|
||||
|
||||
@ -293,6 +333,7 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _switchToggled(ev) {
|
||||
this.setResult(ev.target.key, undefined);
|
||||
this._updateConfigParameter(ev.target, ev.target.checked ? 1 : 0);
|
||||
}
|
||||
|
||||
@ -303,6 +344,7 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
|
||||
if (this._config![ev.target.key].value === ev.target.selected) {
|
||||
return;
|
||||
}
|
||||
this.setResult(ev.target.key, undefined);
|
||||
|
||||
this._updateConfigParameter(ev.target, Number(ev.target.selected));
|
||||
}
|
||||
@ -321,20 +363,41 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
|
||||
if (Number(this._config![ev.target.key].value) === value) {
|
||||
return;
|
||||
}
|
||||
this.setResult(ev.target.key, undefined);
|
||||
this.debouncedUpdate(ev.target, value);
|
||||
}
|
||||
|
||||
private _updateConfigParameter(target, value) {
|
||||
private async _updateConfigParameter(target, value) {
|
||||
const nodeId = getNodeId(this._device!);
|
||||
setNodeConfigParameter(
|
||||
this.hass,
|
||||
this.configEntryId!,
|
||||
nodeId!,
|
||||
target.property,
|
||||
value,
|
||||
target.propertyKey ? target.propertyKey : undefined
|
||||
);
|
||||
this._config![target.key].value = value;
|
||||
try {
|
||||
const result = await setNodeConfigParameter(
|
||||
this.hass,
|
||||
this.configEntryId!,
|
||||
nodeId!,
|
||||
target.property,
|
||||
value,
|
||||
target.propertyKey ? target.propertyKey : undefined
|
||||
);
|
||||
this._config![target.key].value = value;
|
||||
|
||||
this.setResult(target.key, result.status);
|
||||
} catch (error) {
|
||||
this.setError(target.key, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
private setResult(key: string, value: string | undefined) {
|
||||
if (value === undefined) {
|
||||
delete this._results[key];
|
||||
this.requestUpdate();
|
||||
} else {
|
||||
this._results = { ...this._results, [key]: { status: value } };
|
||||
}
|
||||
}
|
||||
|
||||
private setError(key: string, message: string) {
|
||||
const errorParam = { status: "error", error: message };
|
||||
this._results = { ...this._results, [key]: errorParam };
|
||||
}
|
||||
|
||||
private get _device(): DeviceRegistryEntry | undefined {
|
||||
@ -369,6 +432,18 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.accepted {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.queued {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.secondary {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import { mdiClose, mdiContentCopy } from "@mdi/js";
|
||||
import { mdiClose, mdiContentCopy, mdiPackageVariant } from "@mdi/js";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import {
|
||||
css,
|
||||
@ -21,7 +21,10 @@ import {
|
||||
integrationIssuesUrl,
|
||||
IntegrationManifest,
|
||||
} from "../../../data/integration";
|
||||
import { getLoggedErrorIntegration } from "../../../data/system_log";
|
||||
import {
|
||||
getLoggedErrorIntegration,
|
||||
isCustomIntegrationError,
|
||||
} from "../../../data/system_log";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showToast } from "../../../util/toast";
|
||||
@ -65,6 +68,12 @@ class DialogSystemLogDetail extends LitElement {
|
||||
|
||||
const integration = getLoggedErrorIntegration(item);
|
||||
|
||||
const showDocumentation =
|
||||
this._manifest &&
|
||||
(this._manifest.is_built_in ||
|
||||
// Custom components with our offical docs should not link to our docs
|
||||
!this._manifest.documentation.includes("www.home-assistant.io"));
|
||||
|
||||
return html`
|
||||
<ha-dialog open @closed=${this.closeDialog} hideActions heading=${true}>
|
||||
<ha-header-bar slot="heading">
|
||||
@ -86,6 +95,14 @@ class DialogSystemLogDetail extends LitElement {
|
||||
<ha-svg-icon .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</ha-header-bar>
|
||||
${this.isCustomIntegration
|
||||
? html`<div class="custom">
|
||||
<ha-svg-icon .path=${mdiPackageVariant}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.logs.error_from_custom_integration"
|
||||
)}
|
||||
</div>`
|
||||
: ""}
|
||||
<div class="contents">
|
||||
<p>
|
||||
Logger: ${item.name}<br />
|
||||
@ -96,7 +113,7 @@ class DialogSystemLogDetail extends LitElement {
|
||||
Integration: ${domainToName(this.hass.localize, integration)}
|
||||
${!this._manifest ||
|
||||
// Can happen with custom integrations
|
||||
!this._manifest.documentation
|
||||
!showDocumentation
|
||||
? ""
|
||||
: html`
|
||||
(<a
|
||||
@ -144,6 +161,12 @@ class DialogSystemLogDetail extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private get isCustomIntegration(): boolean {
|
||||
return this._manifest
|
||||
? !this._manifest.is_built_in
|
||||
: isCustomIntegrationError(this._params!.item);
|
||||
}
|
||||
|
||||
private async _fetchManifest(integration: string) {
|
||||
try {
|
||||
this._manifest = await fetchIntegrationManifest(this.hass, integration);
|
||||
@ -157,7 +180,18 @@ class DialogSystemLogDetail extends LitElement {
|
||||
".contents"
|
||||
) as HTMLElement;
|
||||
|
||||
await copyToClipboard(copyElement.innerText);
|
||||
let text = copyElement.innerText;
|
||||
|
||||
if (this.isCustomIntegration) {
|
||||
text =
|
||||
this.hass.localize(
|
||||
"ui.panel.config.logs.error_from_custom_integration"
|
||||
) +
|
||||
"\n\n" +
|
||||
text;
|
||||
}
|
||||
|
||||
await copyToClipboard(text);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
@ -167,6 +201,10 @@ class DialogSystemLogDetail extends LitElement {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
@ -177,6 +215,13 @@ class DialogSystemLogDetail extends LitElement {
|
||||
margin-bottom: 0;
|
||||
font-family: var(--code-font-family, monospace);
|
||||
}
|
||||
.custom {
|
||||
padding: 8px 16px;
|
||||
background-color: var(--warning-color);
|
||||
}
|
||||
.contents {
|
||||
padding: 16px;
|
||||
}
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import { domainToName } from "../../../data/integration";
|
||||
import {
|
||||
fetchSystemLog,
|
||||
getLoggedErrorIntegration,
|
||||
isCustomIntegrationError,
|
||||
LoggedError,
|
||||
} from "../../../data/system_log";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
@ -78,10 +79,16 @@ export class SystemLogCard extends LitElement {
|
||||
)}</span
|
||||
>) `}
|
||||
${integrations[idx]
|
||||
? domainToName(
|
||||
? `${domainToName(
|
||||
this.hass!.localize,
|
||||
integrations[idx]!
|
||||
)
|
||||
)}${
|
||||
isCustomIntegrationError(item)
|
||||
? ` (${this.hass.localize(
|
||||
"ui.panel.config.logs.custom_integration"
|
||||
)})`
|
||||
: ""
|
||||
}`
|
||||
: item.source[0]}
|
||||
${item.count > 1
|
||||
? html`
|
||||
|
@ -228,7 +228,7 @@ class HaSceneDashboard extends LitElement {
|
||||
|
||||
private async _activateScene(ev) {
|
||||
ev.stopPropagation();
|
||||
const scene = ev.target.scene as SceneEntity;
|
||||
const scene = ev.currentTarget.scene as SceneEntity;
|
||||
await activateScene(this.hass, scene.entity_id);
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
|
@ -556,20 +556,18 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
if (this._entities.includes(entityId)) {
|
||||
return;
|
||||
}
|
||||
this._entities = [...this._entities, entityId];
|
||||
this._storeState(entityId);
|
||||
|
||||
const entityRegistry = this._entityRegistryEntries.find(
|
||||
(entityReg) => entityReg.entity_id === entityId
|
||||
);
|
||||
|
||||
if (
|
||||
entityRegistry?.device_id &&
|
||||
!this._devices.includes(entityRegistry.device_id)
|
||||
) {
|
||||
this._devices = [...this._devices, entityRegistry.device_id];
|
||||
this._pickDevice(entityRegistry.device_id);
|
||||
} else {
|
||||
this._entities = [...this._entities, entityId];
|
||||
this._storeState(entityId);
|
||||
}
|
||||
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
@ -582,14 +580,12 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
private _devicePicked(ev: CustomEvent) {
|
||||
const device = ev.detail.value;
|
||||
(ev.target as any).value = "";
|
||||
if (this._devices.includes(device)) {
|
||||
private _pickDevice(device_id: string) {
|
||||
if (this._devices.includes(device_id)) {
|
||||
return;
|
||||
}
|
||||
this._devices = [...this._devices, device];
|
||||
const deviceEntities = this._deviceEntityLookup[device];
|
||||
this._devices = [...this._devices, device_id];
|
||||
const deviceEntities = this._deviceEntityLookup[device_id];
|
||||
if (!deviceEntities) {
|
||||
return;
|
||||
}
|
||||
@ -600,6 +596,12 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
private _devicePicked(ev: CustomEvent) {
|
||||
const device = ev.detail.value;
|
||||
(ev.target as any).value = "";
|
||||
this._pickDevice(device);
|
||||
}
|
||||
|
||||
private _deleteDevice(ev: Event) {
|
||||
const deviceId = (ev.target as any).device;
|
||||
this._devices = this._devices.filter((device) => device !== deviceId);
|
||||
@ -627,7 +629,12 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
if ((this._config![name] || "") === newVal) {
|
||||
return;
|
||||
}
|
||||
this._config = { ...this._config!, [name]: newVal };
|
||||
if (!newVal) {
|
||||
delete this._config![name];
|
||||
this._config = { ...this._config! };
|
||||
} else {
|
||||
this._config = { ...this._config!, [name]: newVal };
|
||||
}
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
|
@ -176,7 +176,11 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
${this.narrow
|
||||
? html` <span slot="header">${this._config?.alias}</span> `
|
||||
: ""}
|
||||
<div class="content">
|
||||
<div
|
||||
class="content ${classMap({
|
||||
"yaml-mode": this._mode === "yaml",
|
||||
})}"
|
||||
>
|
||||
${this._errors
|
||||
? html` <div class="errors">${this._errors}</div> `
|
||||
: ""}
|
||||
@ -350,44 +354,43 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
`
|
||||
: this._mode === "yaml"
|
||||
? html`
|
||||
<ha-config-section vertical .isWide=${false}>
|
||||
${!this.narrow
|
||||
? html`<span slot="header">${this._config?.alias}</span>`
|
||||
: ``}
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<ha-yaml-editor
|
||||
.defaultValue=${this._preprocessYaml()}
|
||||
@value-changed=${this._yamlChanged}
|
||||
></ha-yaml-editor>
|
||||
<mwc-button @click=${this._copyYaml}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.copy_to_clipboard"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
${this.scriptEntityId
|
||||
? html`
|
||||
<div
|
||||
class="card-actions layout horizontal justified center"
|
||||
${!this.narrow
|
||||
? html`
|
||||
<ha-card
|
||||
><div class="card-header">
|
||||
${this._config?.alias}
|
||||
</div>
|
||||
<div
|
||||
class="card-actions layout horizontal justified center"
|
||||
>
|
||||
<mwc-button
|
||||
@click=${this._runScript}
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.script.picker.run_script"
|
||||
)}"
|
||||
?disabled=${this._dirty}
|
||||
>
|
||||
<span></span>
|
||||
<mwc-button
|
||||
@click=${this._runScript}
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.script.picker.run_script"
|
||||
)}"
|
||||
?disabled=${this._dirty}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.picker.run_script"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
: ``}
|
||||
</ha-card>
|
||||
</ha-config-section>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.picker.run_script"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
: ``}
|
||||
<ha-yaml-editor
|
||||
.defaultValue=${this._preprocessYaml()}
|
||||
@value-changed=${this._yamlChanged}
|
||||
></ha-yaml-editor>
|
||||
<ha-card
|
||||
><div class="card-actions">
|
||||
<mwc-button @click=${this._copyYaml}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.copy_to_clipboard"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
: ``}
|
||||
</div>
|
||||
@ -532,7 +535,12 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
if ((this._config![name] || "") === newVal) {
|
||||
return;
|
||||
}
|
||||
this._config = { ...this._config!, [name]: newVal };
|
||||
if (!newVal) {
|
||||
delete this._config![name];
|
||||
this._config = { ...this._config! };
|
||||
} else {
|
||||
this._config = { ...this._config!, [name]: newVal };
|
||||
}
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
@ -693,6 +701,22 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
.content {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.yaml-mode {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
ha-yaml-editor {
|
||||
flex-grow: 1;
|
||||
--code-mirror-height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
.yaml-mode ha-card {
|
||||
overflow: initial;
|
||||
--ha-card-border-radius: 0;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
span[slot="introduction"] a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
@ -270,6 +270,7 @@ export class DialogAddUser extends LitElement {
|
||||
css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 500px;
|
||||
--dialog-z-index: 10;
|
||||
}
|
||||
ha-switch {
|
||||
margin-top: 8px;
|
||||
|
@ -24,17 +24,21 @@ class HaPanelDevEvent extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
return html`
|
||||
<style include="ha-style iron-flex iron-positioning"></style>
|
||||
<style>
|
||||
.content {
|
||||
padding: 16px;
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
:host {
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
@apply --paper-font-body1;
|
||||
padding: 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ha-form {
|
||||
margin-right: 16px;
|
||||
.inputs {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
@ -42,14 +46,17 @@ class HaPanelDevEvent extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.code-editor {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply --paper-font-title;
|
||||
}
|
||||
|
||||
event-subscribe-card {
|
||||
display: block;
|
||||
max-width: 800px;
|
||||
margin: 16px auto;
|
||||
margin: 16px 16px 0 0;
|
||||
}
|
||||
|
||||
a {
|
||||
@ -70,7 +77,7 @@ class HaPanelDevEvent extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
)]]
|
||||
</a>
|
||||
</p>
|
||||
<div class="ha-form">
|
||||
<div class="inputs">
|
||||
<paper-input
|
||||
label="[[localize(
|
||||
'ui.panel.developer-tools.tabs.events.type'
|
||||
@ -82,17 +89,20 @@ class HaPanelDevEvent extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
<p>
|
||||
[[localize( 'ui.panel.developer-tools.tabs.events.data' )]]
|
||||
</p>
|
||||
</div>
|
||||
<div class="code-editor">
|
||||
<ha-code-editor
|
||||
mode="yaml"
|
||||
value="[[eventData]]"
|
||||
error="[[!validJSON]]"
|
||||
on-value-changed="_yamlChanged"
|
||||
></ha-code-editor>
|
||||
<mwc-button on-click="fireEvent" raised disabled="[[!validJSON]]"
|
||||
>[[localize( 'ui.panel.developer-tools.tabs.events.fire_event'
|
||||
)]]</mwc-button
|
||||
>
|
||||
</div>
|
||||
<mwc-button on-click="fireEvent" raised disabled="[[!validJSON]]"
|
||||
>[[localize( 'ui.panel.developer-tools.tabs.events.fire_event'
|
||||
)]]</mwc-button
|
||||
>
|
||||
<event-subscribe-card hass="[[hass]]"></event-subscribe-card>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -106,7 +116,6 @@ class HaPanelDevEvent extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
></events-list>
|
||||
</div>
|
||||
</div>
|
||||
<event-subscribe-card hass="[[hass]]"></event-subscribe-card>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -185,7 +194,7 @@ class HaPanelDevEvent extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
}
|
||||
|
||||
computeFormClasses(narrow) {
|
||||
return narrow ? "" : "layout horizontal";
|
||||
return narrow ? "content" : "content layout horizontal";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,19 +122,23 @@ class EventSubscribeCard extends LitElement {
|
||||
return css`
|
||||
form {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
padding: 0 0 16px 16px;
|
||||
}
|
||||
paper-input {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
}
|
||||
mwc-button {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.events {
|
||||
margin: -16px 0;
|
||||
padding: 0 16px;
|
||||
}
|
||||
.event {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
padding-bottom: 16px;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
.event:last-child {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import { ERR_CONNECTION_LOST } from "home-assistant-js-websocket";
|
||||
import { safeLoad } from "js-yaml";
|
||||
import {
|
||||
css,
|
||||
@ -22,12 +24,18 @@ import "../../../components/ha-service-control";
|
||||
import "../../../components/ha-service-picker";
|
||||
import "../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../components/ha-yaml-editor";
|
||||
import { forwardHaptic } from "../../../data/haptics";
|
||||
import { ServiceAction } from "../../../data/script";
|
||||
import { callExecuteScript } from "../../../data/service";
|
||||
import {
|
||||
callExecuteScript,
|
||||
serviceCallWillDisconnect,
|
||||
} from "../../../data/service";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import "../../../styles/polymer-ha-style";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "../../../util/app-localstorage-document";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
|
||||
class HaPanelDevService extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@ -156,12 +164,39 @@ class HaPanelDevService extends LitElement {
|
||||
outlined
|
||||
.expanded=${this._yamlMode}
|
||||
>
|
||||
${this._yamlMode && target
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.services.accepts_target"
|
||||
)}
|
||||
</h3>`
|
||||
${this._yamlMode
|
||||
? html` <div class="description">
|
||||
<h3>
|
||||
${target
|
||||
? html`
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.services.accepts_target"
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
</h3>
|
||||
${this._serviceData?.service
|
||||
? html` <a
|
||||
href="${documentationUrl(
|
||||
this.hass,
|
||||
"/integrations/" +
|
||||
computeDomain(this._serviceData?.service)
|
||||
)}"
|
||||
title="${this.hass.localize(
|
||||
"ui.components.service-control.integration_doc"
|
||||
)}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<mwc-icon-button>
|
||||
<ha-svg-icon
|
||||
path=${mdiHelpCircle}
|
||||
class="help-icon"
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</a>`
|
||||
: ""}
|
||||
</div>`
|
||||
: ""}
|
||||
<table class="attributes">
|
||||
<tr>
|
||||
@ -267,11 +302,30 @@ class HaPanelDevService extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _callService() {
|
||||
private async _callService() {
|
||||
if (!this._serviceData?.service) {
|
||||
return;
|
||||
}
|
||||
callExecuteScript(this.hass, [this._serviceData]);
|
||||
try {
|
||||
await callExecuteScript(this.hass, [this._serviceData]);
|
||||
} catch (err) {
|
||||
const [domain, service] = this._serviceData.service.split(".", 2);
|
||||
if (
|
||||
err.error?.code === ERR_CONNECTION_LOST &&
|
||||
serviceCallWillDisconnect(domain, service)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
forwardHaptic("failure");
|
||||
showToast(this, {
|
||||
message:
|
||||
this.hass.localize(
|
||||
"ui.notification_toast.service_call_failed",
|
||||
"service",
|
||||
this._serviceData.service
|
||||
) + ` ${err.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleYaml() {
|
||||
@ -394,6 +448,15 @@ class HaPanelDevService extends LitElement {
|
||||
padding: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.help-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.description {
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ import { stateIcon } from "../../../common/entity/state_icon";
|
||||
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||
import { iconColorCSS } from "../../../common/style/icon_color_css";
|
||||
import "../../../components/ha-card";
|
||||
import { LightEntity } from "../../../data/light";
|
||||
import { getLightRgbColor, LightEntity } from "../../../data/light";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
@ -301,14 +301,14 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
private _computeColor(stateObj: HassEntity | LightEntity): string {
|
||||
if (!stateObj.attributes.hs_color || !this._config?.state_color) {
|
||||
if (
|
||||
!this._config?.state_color ||
|
||||
computeStateDomain(stateObj) !== "light"
|
||||
) {
|
||||
return "";
|
||||
}
|
||||
const [hue, sat] = stateObj.attributes.hs_color;
|
||||
if (sat <= 10) {
|
||||
return "";
|
||||
}
|
||||
return `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
|
||||
const rgb = getLightRgbColor(stateObj as LightEntity);
|
||||
return rgb ? `rgb(${rgb.slice(0, 3).join(",")})` : "";
|
||||
}
|
||||
|
||||
private _handleAction(ev: ActionHandlerEvent) {
|
||||
|
@ -31,11 +31,18 @@ export class HuiErrorCard extends LitElement implements LovelaceCard {
|
||||
return html``;
|
||||
}
|
||||
|
||||
let dumped: string | undefined;
|
||||
|
||||
if (this._config.origConfig) {
|
||||
try {
|
||||
dumped = safeDump(this._config.origConfig);
|
||||
} catch (err) {
|
||||
dumped = `[Error dumping ${this._config.origConfig}]`;
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
${this._config.error}
|
||||
${this._config.origConfig
|
||||
? html`<pre>${safeDump(this._config.origConfig)}</pre>`
|
||||
: ""}
|
||||
${this._config.error}${dumped ? html`<pre>${dumped}</pre>` : ""}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -18,11 +18,14 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { stateIcon } from "../../../common/entity/state_icon";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
import { UNAVAILABLE, UNAVAILABLE_STATES } from "../../../data/entity";
|
||||
import { LightEntity, SUPPORT_BRIGHTNESS } from "../../../data/light";
|
||||
import {
|
||||
getLightRgbColor,
|
||||
LightEntity,
|
||||
lightSupportsDimming,
|
||||
} from "../../../data/light";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
@ -121,17 +124,14 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
|
||||
@value-changing=${this._dragEvent}
|
||||
@value-changed=${this._setBrightness}
|
||||
style=${styleMap({
|
||||
visibility: supportsFeature(stateObj, SUPPORT_BRIGHTNESS)
|
||||
visibility: lightSupportsDimming(stateObj)
|
||||
? "visible"
|
||||
: "hidden",
|
||||
})}
|
||||
></round-slider>
|
||||
<ha-icon-button
|
||||
class="light-button ${classMap({
|
||||
"slider-center": supportsFeature(
|
||||
stateObj,
|
||||
SUPPORT_BRIGHTNESS
|
||||
),
|
||||
"slider-center": lightSupportsDimming(stateObj),
|
||||
"state-on": stateObj.state === "on",
|
||||
"state-unavailable": stateObj.state === UNAVAILABLE,
|
||||
})}"
|
||||
@ -244,14 +244,11 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
private _computeColor(stateObj: LightEntity): string {
|
||||
if (stateObj.state === "off" || !stateObj.attributes.hs_color) {
|
||||
if (stateObj.state === "off") {
|
||||
return "";
|
||||
}
|
||||
const [hue, sat] = stateObj.attributes.hs_color;
|
||||
if (sat <= 10) {
|
||||
return "";
|
||||
}
|
||||
return `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
|
||||
const rgb = getLightRgbColor(stateObj);
|
||||
return rgb ? `rgb(${rgb.slice(0, 3).join(",")})` : "";
|
||||
}
|
||||
|
||||
private _handleAction(ev: ActionHandlerEvent) {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user