mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-15 20:29:26 +00:00
Compare commits
97 Commits
20210407.2
...
analytics-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f69bce534a | ||
![]() |
575f58bd88 | ||
![]() |
35535628fc | ||
![]() |
8e018c9cfe | ||
![]() |
5ae268b792 | ||
![]() |
329732ac30 | ||
![]() |
7f88bab552 | ||
![]() |
9f3bb7f4d6 | ||
![]() |
73bb346c00 | ||
![]() |
33703a3b53 | ||
![]() |
b7a4f97eca | ||
![]() |
dd4efe0f51 | ||
![]() |
7e0522c3b3 | ||
![]() |
e682abfb75 | ||
![]() |
24e202a3d7 | ||
![]() |
ac9a881ab5 | ||
![]() |
4d287a1f83 | ||
![]() |
b8d6b1ebdd | ||
![]() |
8ca1b9320d | ||
![]() |
cba3992d2b | ||
![]() |
96d6e337be | ||
![]() |
959f7ae046 | ||
![]() |
9572a58764 | ||
![]() |
393ae9e5dc | ||
![]() |
63e10314bd | ||
![]() |
b599417a37 | ||
![]() |
899eab4e5c | ||
![]() |
3f21c87a3d | ||
![]() |
c296a60bab | ||
![]() |
5f78f18cb4 | ||
![]() |
0b8d356865 | ||
![]() |
e8d1318a5b | ||
![]() |
07ce07c4a5 | ||
![]() |
a07220f383 | ||
![]() |
f21ed24a49 | ||
![]() |
e3c38b93f4 | ||
![]() |
b398727413 | ||
![]() |
9bc2ab29a1 | ||
![]() |
51f1ff26f1 | ||
![]() |
97d5e6512d | ||
![]() |
b76c67fc9b | ||
![]() |
b96a70cd55 | ||
![]() |
982ab93cdb | ||
![]() |
c7f4e1152d | ||
![]() |
519988326b | ||
![]() |
b518f4b03c | ||
![]() |
5493fdfcb7 | ||
![]() |
179767e9f8 | ||
![]() |
25b3bb1285 | ||
![]() |
841c8ab1f1 | ||
![]() |
1ce17e2847 | ||
![]() |
a09b206b0e | ||
![]() |
bb4617c53b | ||
![]() |
cfd18bfb74 | ||
![]() |
e225d6f546 | ||
![]() |
60fe48d355 | ||
![]() |
2dcd0d2b0a | ||
![]() |
8e11aa9130 | ||
![]() |
f6e223c18d | ||
![]() |
9d29b55bee | ||
![]() |
92aa8580db | ||
![]() |
538028a003 | ||
![]() |
c53575a74f | ||
![]() |
193016a46a | ||
![]() |
aaa50b4d1d | ||
![]() |
a43120320e | ||
![]() |
b8bb0c038d | ||
![]() |
dc79fc2919 | ||
![]() |
30787fef60 | ||
![]() |
445ae156ef | ||
![]() |
62a0cfb0f6 | ||
![]() |
96bc3ef99a | ||
![]() |
1d3b95d24f | ||
![]() |
56fe4b07f3 | ||
![]() |
ea60f7005b | ||
![]() |
9eb59062aa | ||
![]() |
d00927c31f | ||
![]() |
c03017208d | ||
![]() |
73f945458a | ||
![]() |
db12234611 | ||
![]() |
ed1cd4632f | ||
![]() |
9833accc79 | ||
![]() |
d46123771a | ||
![]() |
87fe84b1ac | ||
![]() |
21140f437e | ||
![]() |
ba9e410393 | ||
![]() |
587fb2a170 | ||
![]() |
7d801ff84c | ||
![]() |
d69accd9a5 | ||
![]() |
1127750c5e | ||
![]() |
7758bd89c1 | ||
![]() |
de7264327a | ||
![]() |
c3f0932794 | ||
![]() |
367907e037 | ||
![]() |
2d15bd651e | ||
![]() |
4b1d7863f8 | ||
![]() |
e425d768dd |
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"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -17,7 +17,7 @@ import { DemoTrace } from "../data/traces/types";
|
||||
const traces: DemoTrace[] = [
|
||||
mockDemoTrace({ state: "running" }),
|
||||
mockDemoTrace({ state: "debugged" }),
|
||||
mockDemoTrace({ state: "stopped", script_execution: "failed_condition" }),
|
||||
mockDemoTrace({ state: "stopped", script_execution: "failed_conditions" }),
|
||||
mockDemoTrace({ state: "stopped", script_execution: "failed_single" }),
|
||||
mockDemoTrace({ state: "stopped", script_execution: "failed_max_runs" }),
|
||||
mockDemoTrace({ state: "stopped", script_execution: "finished" }),
|
||||
|
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;
|
||||
}
|
||||
}
|
@@ -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"),
|
||||
});
|
||||
|
@@ -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",
|
||||
@@ -168,7 +167,6 @@
|
||||
"@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,7 +226,7 @@
|
||||
"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",
|
||||
|
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20210407.2",
|
||||
version="20210423.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();
|
||||
|
@@ -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));
|
||||
};
|
||||
|
||||
|
@@ -118,6 +118,8 @@ export class HaDateInput extends LitElement {
|
||||
!this.value ||
|
||||
(this._inited && !this._compareStringDates(ev.detail.value, this.value))
|
||||
) {
|
||||
this.value = ev.detail.value;
|
||||
fireEvent(this, "change");
|
||||
fireEvent(this, "value-changed", { value: ev.detail.value });
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
@@ -475,7 +477,7 @@ export class HaAutomationTracer extends LitElement {
|
||||
let extra: TemplateResult | undefined;
|
||||
|
||||
switch (this.trace.script_execution) {
|
||||
case "failed_condition":
|
||||
case "failed_conditions":
|
||||
reason = "a condition failed";
|
||||
break;
|
||||
case "failed_single":
|
||||
|
@@ -9,6 +9,7 @@ export interface AnalyticsPreferences {
|
||||
|
||||
export interface Analytics {
|
||||
preferences: AnalyticsPreferences;
|
||||
onboarded: boolean;
|
||||
}
|
||||
|
||||
export const getAnalyticsDetails = (hass: HomeAssistant) =>
|
||||
|
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,13 @@ 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 const integrationIssuesUrl = (
|
||||
|
@@ -19,6 +19,10 @@ export interface LovelacePanelConfig {
|
||||
|
||||
export interface LovelaceConfig {
|
||||
title?: string;
|
||||
strategy?: {
|
||||
name: string;
|
||||
options?: Record<string, unknown>;
|
||||
};
|
||||
views: LovelaceViewConfig[];
|
||||
background?: string;
|
||||
}
|
||||
@@ -77,6 +81,10 @@ export interface LovelaceViewConfig {
|
||||
index?: number;
|
||||
title?: string;
|
||||
type?: string;
|
||||
strategy?: {
|
||||
name: 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/");
|
||||
|
@@ -66,7 +66,7 @@ export interface AutomationTrace {
|
||||
};
|
||||
script_execution:
|
||||
| // The script was not executed because the automation's condition failed
|
||||
"failed_condition"
|
||||
"failed_conditions"
|
||||
// The script was not executed because the run mode is single
|
||||
| "failed_single"
|
||||
// The script was not executed because max parallel runs would be exceeded
|
||||
@@ -80,8 +80,7 @@ export interface AutomationTrace {
|
||||
| "error"
|
||||
// The exception is in the trace itself or in the last element of the trace
|
||||
// Script execution stopped by async_stop called on the script run because home assistant is shutting down, script mode is SCRIPT_MODE_RESTART etc:
|
||||
| "cancelled"
|
||||
| string;
|
||||
| "cancelled";
|
||||
// Automation only, should become it's own type when we support script in frontend
|
||||
trigger: string;
|
||||
}
|
||||
|
@@ -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 {
|
||||
@@ -75,6 +76,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;
|
||||
@@ -282,6 +288,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";
|
||||
|
@@ -29,6 +29,10 @@ export interface ZWaveJSNode {
|
||||
}
|
||||
|
||||
export interface ZWaveJSNodeConfigParams {
|
||||
[key: string]: ZWaveJSNodeConfigParam;
|
||||
}
|
||||
|
||||
export interface ZWaveJSNodeConfigParam {
|
||||
property: number;
|
||||
value: any;
|
||||
configuration_value_type: string;
|
||||
@@ -56,6 +60,17 @@ 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 enum NodeStatus {
|
||||
Unknown,
|
||||
Asleep,
|
||||
@@ -75,6 +90,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 +125,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 +139,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,
|
||||
|
165
src/dialogs/analytics/dialog-analytics-optin.ts
Normal file
165
src/dialogs/analytics/dialog-analytics-optin.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import "../../components/ha-analytics";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-dialog";
|
||||
import { Analytics, setAnalyticsPreferences } from "../../data/analytics";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { DialogAnalyticsOptInParams } from "./show-dialog-analytics-optin";
|
||||
import { analyticsLearnMore } from "../../components/ha-analytics-learn-more";
|
||||
|
||||
@customElement("dialog-analytics-optin")
|
||||
class DialogAnalyticsOptIn extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@internalProperty() private _error?: string;
|
||||
|
||||
@internalProperty() private _submitting = false;
|
||||
|
||||
@internalProperty() private _showPreferences = false;
|
||||
|
||||
@internalProperty() private _analyticsDetails?: Analytics;
|
||||
|
||||
public showDialog(params: DialogAnalyticsOptInParams): void {
|
||||
this._error = undefined;
|
||||
this._submitting = false;
|
||||
this._analyticsDetails = params.analytics;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._error = undefined;
|
||||
this._submitting = false;
|
||||
this._showPreferences = false;
|
||||
this._analyticsDetails = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._analyticsDetails) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
heading="Analytics"
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
>
|
||||
<div class="content">
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
${this._showPreferences
|
||||
? html`<ha-analytics
|
||||
@analytics-preferences-changed=${this._preferencesChanged}
|
||||
.hass=${this.hass}
|
||||
.analytics=${this._analyticsDetails!}
|
||||
></ha-analytics>`
|
||||
: html` <div class="introduction">
|
||||
To help us better understand how you use Home Assistant, and to
|
||||
ensure our priorities align with yours, we ask that you share
|
||||
anonymized information from your installation. This will help make Home
|
||||
Assistant better and help us convince manufacturers to add local
|
||||
control and privacy-focused features.
|
||||
<p>
|
||||
If you want to change what you share, you can find this in
|
||||
under "General" here in the configuration panel
|
||||
</p>
|
||||
</div>`}
|
||||
${analyticsLearnMore(this.hass)}
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<mwc-button @click=${this._ignore} .disabled=${this._submitting}>
|
||||
Ignore
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
@click=${this._customize}
|
||||
.disabled=${this._submitting || this._showPreferences}
|
||||
>
|
||||
Customize
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._submit} .disabled=${this._submitting}>
|
||||
${this._showPreferences ? "Submit" : "Enable analytics"}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _preferencesChanged(event: CustomEvent): void {
|
||||
this._analyticsDetails = {
|
||||
...this._analyticsDetails!,
|
||||
preferences: event.detail.preferences,
|
||||
};
|
||||
}
|
||||
|
||||
private async _ignore() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
await setAnalyticsPreferences(this.hass, {});
|
||||
} catch (err) {
|
||||
this._error = err.message;
|
||||
this._submitting = false;
|
||||
return;
|
||||
}
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private async _customize() {
|
||||
this._showPreferences = true;
|
||||
}
|
||||
|
||||
private async _submit() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
await setAnalyticsPreferences(
|
||||
this.hass,
|
||||
this._showPreferences
|
||||
? this._analyticsDetails!.preferences
|
||||
: { base: true, usage: true, statistics: true }
|
||||
);
|
||||
} catch (err) {
|
||||
this._error = err.message;
|
||||
this._submitting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
.content {
|
||||
padding-bottom: 54px;
|
||||
}
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
bottom: 16px;
|
||||
position: absolute;
|
||||
width: calc(100% - 48px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-analytics-optin": DialogAnalyticsOptIn;
|
||||
}
|
||||
}
|
20
src/dialogs/analytics/show-dialog-analytics-optin.ts
Normal file
20
src/dialogs/analytics/show-dialog-analytics-optin.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { Analytics } from "../../data/analytics";
|
||||
|
||||
export interface DialogAnalyticsOptInParams {
|
||||
analytics: Analytics;
|
||||
}
|
||||
|
||||
export const loadConfigEntrySystemOptionsDialog = () =>
|
||||
import("./dialog-analytics-optin");
|
||||
|
||||
export const showDialogAnalyticsOptIn = (
|
||||
element: HTMLElement,
|
||||
dialogParams: DialogAnalyticsOptInParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-analytics-optin",
|
||||
dialogImport: loadConfigEntrySystemOptionsDialog,
|
||||
dialogParams,
|
||||
});
|
||||
};
|
@@ -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> => {
|
||||
|
@@ -16,7 +16,6 @@ class DatetimeInput extends PolymerElement {
|
||||
<div>
|
||||
<ha-date-input
|
||||
id="dateInput"
|
||||
on-value-changed="dateTimeChanged"
|
||||
label="Date"
|
||||
value="{{selectedDate}}"
|
||||
></ha-date-input>
|
||||
|
@@ -151,7 +151,7 @@ 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,xy_color,min_mireds,max_mireds,entity_id,supported_color_modes,color_mode"
|
||||
></ha-attributes>
|
||||
</div>
|
||||
`;
|
||||
|
@@ -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),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -27,6 +27,7 @@ class OnboardingAnalytics extends LitElement {
|
||||
|
||||
@internalProperty() private _analyticsDetails: Analytics = {
|
||||
preferences: {},
|
||||
onboarded: false,
|
||||
};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
|
@@ -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}
|
||||
@@ -242,6 +255,7 @@ export class HaAutomationTrace extends LitElement {
|
||||
? html`
|
||||
<ha-automation-trace-logbook
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.logbookEntries=${this._logbookEntries}
|
||||
></ha-automation-trace-logbook>
|
||||
`
|
||||
@@ -409,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;
|
||||
});
|
||||
|
||||
|
@@ -15,7 +15,9 @@ import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-menu-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { getAnalyticsDetails } from "../../../data/analytics";
|
||||
import { CloudStatus } from "../../../data/cloud";
|
||||
import { showDialogAnalyticsOptIn } from "../../../dialogs/analytics/show-dialog-analytics-optin";
|
||||
import "../../../layouts/ha-app-layout";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
@@ -36,6 +38,15 @@ class HaConfigDashboard extends LitElement {
|
||||
|
||||
@property() public showAdvanced!: boolean;
|
||||
|
||||
protected firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
getAnalyticsDetails(this.hass).then((analytics) => {
|
||||
if (!analytics.onboarded) {
|
||||
showDialogAnalyticsOptIn(this, { analytics });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const content = html` <ha-config-section
|
||||
.narrow=${this.narrow}
|
||||
@@ -79,36 +90,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">
|
||||
|
@@ -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,
|
||||
});
|
||||
|
@@ -30,8 +30,9 @@ class IntegrationsCard extends LitElement {
|
||||
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();
|
||||
});
|
||||
|
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();
|
||||
}
|
||||
|
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -230,6 +270,9 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
|
||||
.disabled=${!item.metadata.writeable}
|
||||
@value-changed=${this._numericInputChanged}
|
||||
>
|
||||
${item.metadata.unit
|
||||
? html`<span slot="suffix">${item.metadata.unit}</span>`
|
||||
: ""}
|
||||
</paper-input> `;
|
||||
}
|
||||
|
||||
@@ -290,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);
|
||||
}
|
||||
|
||||
@@ -300,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));
|
||||
}
|
||||
@@ -318,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 {
|
||||
@@ -366,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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -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>` : ""}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -666,6 +666,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||
|
||||
ha-icon-button[action="media_play"],
|
||||
ha-icon-button[action="media_play_pause"],
|
||||
ha-icon-button[action="media_pause"],
|
||||
ha-icon-button[action="media_stop"],
|
||||
ha-icon-button[action="turn_on"],
|
||||
ha-icon-button[action="turn_off"] {
|
||||
@@ -743,6 +744,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||
|
||||
.narrow ha-icon-button[action="media_play"],
|
||||
.narrow ha-icon-button[action="media_play_pause"],
|
||||
.narrow ha-icon-button[action="media_pause"],
|
||||
.narrow ha-icon-button[action="turn_on"] {
|
||||
--mdc-icon-button-size: 50px;
|
||||
--mdc-icon-size: 36px;
|
||||
|
@@ -1,40 +1,16 @@
|
||||
import {
|
||||
HassEntities,
|
||||
HassEntity,
|
||||
STATE_NOT_RUNNING,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../../../common/entity/compute_object_id";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { extractViews } from "../../../common/entity/extract_views";
|
||||
import { getViewEntities } from "../../../common/entity/get_view_entities";
|
||||
import { splitByGroups } from "../../../common/entity/split_by_groups";
|
||||
import { compare } from "../../../common/string/compare";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { subscribeOne } from "../../../common/util/subscribe-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
} from "../../../data/area_registry";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../../data/entity_registry";
|
||||
import { GroupEntity } from "../../../data/group";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import {
|
||||
LovelaceCardConfig,
|
||||
LovelaceConfig,
|
||||
LovelaceViewConfig,
|
||||
} from "../../../data/lovelace";
|
||||
import { LovelaceCardConfig, LovelaceViewConfig } from "../../../data/lovelace";
|
||||
import { SENSOR_DEVICE_CLASS_BATTERY } from "../../../data/sensor";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import {
|
||||
AlarmPanelCardConfig,
|
||||
EntitiesCardConfig,
|
||||
@@ -45,7 +21,6 @@ import {
|
||||
} from "../cards/types";
|
||||
import { LovelaceRowConfig } from "../entity-rows/types";
|
||||
|
||||
const DEFAULT_VIEW_ENTITY_ID = "group.default_view";
|
||||
const HIDE_DOMAIN = new Set([
|
||||
"automation",
|
||||
"configurator",
|
||||
@@ -57,8 +32,6 @@ const HIDE_DOMAIN = new Set([
|
||||
|
||||
const HIDE_PLATFORM = new Set(["mobile_app"]);
|
||||
|
||||
let subscribedRegistries = false;
|
||||
|
||||
interface SplittedByAreas {
|
||||
areasWithEntities: Array<[AreaRegistryEntry, HassEntity[]]>;
|
||||
otherEntities: HassEntities;
|
||||
@@ -181,7 +154,8 @@ export const computeCards = (
|
||||
titlePrefix &&
|
||||
stateObj &&
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
(name = computeStateName(stateObj)).startsWith(titlePrefix)
|
||||
(name = computeStateName(stateObj)) !== titlePrefix &&
|
||||
name.startsWith(titlePrefix)
|
||||
? {
|
||||
entity: entityId,
|
||||
name: adjustName(name.substr(titlePrefix.length)),
|
||||
@@ -238,7 +212,7 @@ const computeDefaultViewStates = (
|
||||
return states;
|
||||
};
|
||||
|
||||
const generateViewConfig = (
|
||||
export const generateViewConfig = (
|
||||
localize: LocalizeFunc,
|
||||
path: string,
|
||||
title: string | undefined,
|
||||
@@ -372,141 +346,3 @@ export const generateDefaultViewConfig = (
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
export const generateLovelaceConfigFromData = async (
|
||||
hass: HomeAssistant,
|
||||
areaEntries: AreaRegistryEntry[],
|
||||
deviceEntries: DeviceRegistryEntry[],
|
||||
entityEntries: EntityRegistryEntry[],
|
||||
entities: HassEntities,
|
||||
localize: LocalizeFunc
|
||||
): Promise<LovelaceConfig> => {
|
||||
if (hass.config.safe_mode) {
|
||||
return {
|
||||
title: hass.config.location_name,
|
||||
views: [
|
||||
{
|
||||
cards: [{ type: "safe-mode" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const viewEntities = extractViews(entities);
|
||||
|
||||
const views = viewEntities.map((viewEntity: GroupEntity) => {
|
||||
const states = getViewEntities(entities, viewEntity);
|
||||
|
||||
// In the case of a normal view, we use group order as specified in view
|
||||
const groupOrders = {};
|
||||
Object.keys(states).forEach((entityId, idx) => {
|
||||
groupOrders[entityId] = idx;
|
||||
});
|
||||
|
||||
return generateViewConfig(
|
||||
localize,
|
||||
computeObjectId(viewEntity.entity_id),
|
||||
computeStateName(viewEntity),
|
||||
viewEntity.attributes.icon,
|
||||
states,
|
||||
groupOrders
|
||||
);
|
||||
});
|
||||
|
||||
let title = hass.config.location_name;
|
||||
|
||||
// User can override default view. If they didn't, we will add one
|
||||
// that contains all entities.
|
||||
if (
|
||||
viewEntities.length === 0 ||
|
||||
viewEntities[0].entity_id !== DEFAULT_VIEW_ENTITY_ID
|
||||
) {
|
||||
views.unshift(
|
||||
generateDefaultViewConfig(
|
||||
areaEntries,
|
||||
deviceEntries,
|
||||
entityEntries,
|
||||
entities,
|
||||
localize
|
||||
)
|
||||
);
|
||||
|
||||
// Add map of geo locations to default view if loaded
|
||||
if (isComponentLoaded(hass, "geo_location")) {
|
||||
if (views[0] && views[0].cards) {
|
||||
views[0].cards.push({
|
||||
type: "map",
|
||||
geo_location_sources: ["all"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we don't have Home as title and first tab.
|
||||
if (views.length > 1 && title === "Home") {
|
||||
title = "Home Assistant";
|
||||
}
|
||||
}
|
||||
|
||||
// User has no entities
|
||||
if (views.length === 1 && views[0].cards!.length === 0) {
|
||||
views[0].cards!.push({
|
||||
type: "empty-state",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
views,
|
||||
};
|
||||
};
|
||||
|
||||
export const generateLovelaceConfigFromHass = async (
|
||||
hass: HomeAssistant,
|
||||
localize?: LocalizeFunc
|
||||
): Promise<LovelaceConfig> => {
|
||||
if (hass.config.state === STATE_NOT_RUNNING) {
|
||||
return {
|
||||
title: hass.config.location_name,
|
||||
views: [
|
||||
{
|
||||
cards: [{ type: "starting" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (hass.config.safe_mode) {
|
||||
return {
|
||||
title: hass.config.location_name,
|
||||
views: [
|
||||
{
|
||||
cards: [{ type: "safe-mode" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// We want to keep the registry subscriptions alive after generating the UI
|
||||
// so that we don't serve up stale data after changing areas.
|
||||
if (!subscribedRegistries) {
|
||||
subscribedRegistries = true;
|
||||
subscribeAreaRegistry(hass.connection, () => undefined);
|
||||
subscribeDeviceRegistry(hass.connection, () => undefined);
|
||||
subscribeEntityRegistry(hass.connection, () => undefined);
|
||||
}
|
||||
|
||||
const [areaEntries, deviceEntries, entityEntries] = await Promise.all([
|
||||
subscribeOne(hass.connection, subscribeAreaRegistry),
|
||||
subscribeOne(hass.connection, subscribeDeviceRegistry),
|
||||
subscribeOne(hass.connection, subscribeEntityRegistry),
|
||||
]);
|
||||
|
||||
return generateLovelaceConfigFromData(
|
||||
hass,
|
||||
areaEntries,
|
||||
deviceEntries,
|
||||
entityEntries,
|
||||
hass.states,
|
||||
localize || hass.localize
|
||||
);
|
||||
};
|
||||
|
@@ -129,7 +129,8 @@ class HuiGenericEntityRow extends LitElement {
|
||||
stateObj.attributes.brightness
|
||||
? html`${Math.round(
|
||||
(stateObj.attributes.brightness / 255) * 100
|
||||
)} %`
|
||||
)}
|
||||
%`
|
||||
: "")}
|
||||
</div>
|
||||
`
|
||||
|
@@ -2,6 +2,7 @@ import {
|
||||
LovelaceViewConfig,
|
||||
LovelaceViewElement,
|
||||
} from "../../../data/lovelace";
|
||||
import { HuiErrorCard } from "../cards/hui-error-card";
|
||||
import "../views/hui-masonry-view";
|
||||
import { createLovelaceElement } from "./create-element-base";
|
||||
|
||||
@@ -13,7 +14,7 @@ const LAZY_LOAD_LAYOUTS = {
|
||||
|
||||
export const createViewElement = (
|
||||
config: LovelaceViewConfig
|
||||
): LovelaceViewElement => {
|
||||
): LovelaceViewElement | HuiErrorCard => {
|
||||
return createLovelaceElement(
|
||||
"view",
|
||||
config,
|
||||
|
@@ -19,13 +19,15 @@ import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-yaml-editor";
|
||||
import type { LovelaceConfig } from "../../../data/lovelace";
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { expandLovelaceConfigStrategies } from "../strategies/get-strategy";
|
||||
import type { SaveDialogParams } from "./show-save-config-dialog";
|
||||
|
||||
const EMPTY_CONFIG = { views: [] };
|
||||
const EMPTY_CONFIG: LovelaceConfig = { views: [{ title: "Home" }] };
|
||||
|
||||
@customElement("hui-dialog-save-config")
|
||||
export class HuiSaveConfig extends LitElement implements HassDialog {
|
||||
@@ -125,14 +127,17 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
|
||||
</div>
|
||||
${this._params.mode === "storage"
|
||||
? html`
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.save_config.cancel"
|
||||
)}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
.label=${this.hass!.localize("ui.common.cancel")}
|
||||
@click=${this.closeDialog}
|
||||
></mwc-button>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
?disabled=${this._saving}
|
||||
aria-label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.save_config.save"
|
||||
)}
|
||||
@click=${this._saveConfig}
|
||||
>
|
||||
${this._saving
|
||||
@@ -148,11 +153,13 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
|
||||
</mwc-button>
|
||||
`
|
||||
: html`
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}
|
||||
>${this.hass!.localize(
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.save_config.close"
|
||||
)}
|
||||
</mwc-button>
|
||||
@click=${this.closeDialog}
|
||||
></mwc-button>
|
||||
`}
|
||||
</ha-dialog>
|
||||
`;
|
||||
@@ -177,7 +184,13 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
|
||||
try {
|
||||
const lovelace = this._params!.lovelace;
|
||||
await lovelace.saveConfig(
|
||||
this._emptyConfig ? EMPTY_CONFIG : lovelace.config
|
||||
this._emptyConfig
|
||||
? EMPTY_CONFIG
|
||||
: await expandLovelaceConfigStrategies({
|
||||
config: lovelace.config,
|
||||
hass: this.hass!,
|
||||
narrow: this._params!.narrow,
|
||||
})
|
||||
);
|
||||
lovelace.setEditMode(true);
|
||||
this._saving = false;
|
||||
|
@@ -14,6 +14,7 @@ const dialogTag = "hui-dialog-save-config";
|
||||
export interface SaveDialogParams {
|
||||
lovelace: Lovelace;
|
||||
mode: "yaml" | "storage";
|
||||
narrow: boolean;
|
||||
}
|
||||
|
||||
let registeredDialog = false;
|
||||
|
@@ -57,7 +57,7 @@ class HuiStateLabelElement extends LitElement implements LovelaceElement {
|
||||
|
||||
if (
|
||||
this._config.attribute &&
|
||||
!stateObj.attributes[this._config.attribute]
|
||||
!(this._config.attribute in stateObj.attributes)
|
||||
) {
|
||||
return html`
|
||||
<hui-warning-element
|
||||
|
@@ -115,7 +115,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
|
||||
? html`
|
||||
<ha-icon-button
|
||||
icon=${this._computeControlIcon(stateObj)}
|
||||
@click=${this._playPause}
|
||||
@click=${this._playPauseStop}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
@@ -256,8 +256,17 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
|
||||
);
|
||||
}
|
||||
|
||||
private _playPause(): void {
|
||||
this.hass!.callService("media_player", "media_play_pause", {
|
||||
private _playPauseStop(): void {
|
||||
const stateObj = this.hass!.states[this._config!.entity];
|
||||
|
||||
const service =
|
||||
stateObj.state !== "playing"
|
||||
? "media_play"
|
||||
: supportsFeature(stateObj, SUPPORT_PAUSE)
|
||||
? "media_pause"
|
||||
: "media_stop";
|
||||
|
||||
this.hass!.callService("media_player", service, {
|
||||
entity_id: this._config!.entity,
|
||||
});
|
||||
}
|
||||
|
@@ -7,6 +7,11 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||
import {
|
||||
addSearchParam,
|
||||
removeSearchParam,
|
||||
} from "../../common/url/search-params";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import {
|
||||
deleteConfig,
|
||||
@@ -21,14 +26,16 @@ import "../../layouts/hass-error-screen";
|
||||
import "../../layouts/hass-loading-screen";
|
||||
import { HomeAssistant, PanelInfo, Route } from "../../types";
|
||||
import { showToast } from "../../util/toast";
|
||||
import { generateLovelaceConfigFromHass } from "./common/generate-lovelace-config";
|
||||
import { loadLovelaceResources } from "./common/load-resources";
|
||||
import { showSaveDialog } from "./editor/show-save-config-dialog";
|
||||
import "./hui-root";
|
||||
import { generateLovelaceDashboardStrategy } from "./strategies/get-strategy";
|
||||
import { Lovelace } from "./types";
|
||||
|
||||
(window as any).loadCardHelpers = () => import("./custom-card-helpers");
|
||||
|
||||
const DEFAULT_STRATEGY = "original-states";
|
||||
|
||||
interface LovelacePanelConfig {
|
||||
mode: "yaml" | "storage";
|
||||
}
|
||||
@@ -71,7 +78,11 @@ class LovelacePanel extends LitElement {
|
||||
this.lovelace.locale !== this.hass.locale
|
||||
) {
|
||||
// language has been changed, rebuild UI
|
||||
this._setLovelaceConfig(this.lovelace.config, this.lovelace.mode);
|
||||
this._setLovelaceConfig(
|
||||
this.lovelace.config,
|
||||
this.lovelace.rawConfig,
|
||||
this.lovelace.mode
|
||||
);
|
||||
} else if (this.lovelace && this.lovelace.mode === "generated") {
|
||||
// When lovelace is generated, we re-generate each time a user goes
|
||||
// to the states panel to make sure new entities are shown.
|
||||
@@ -139,7 +150,9 @@ class LovelacePanel extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
this._fetchConfig(false);
|
||||
if (!this._unsubUpdates) {
|
||||
this._subscribeUpdates();
|
||||
@@ -153,8 +166,14 @@ class LovelacePanel extends LitElement {
|
||||
}
|
||||
|
||||
private async _regenerateConfig() {
|
||||
const conf = await generateLovelaceConfigFromHass(this.hass!);
|
||||
this._setLovelaceConfig(conf, "generated");
|
||||
const conf = await generateLovelaceDashboardStrategy(
|
||||
{
|
||||
hass: this.hass!,
|
||||
narrow: this.narrow,
|
||||
},
|
||||
DEFAULT_STRATEGY
|
||||
);
|
||||
this._setLovelaceConfig(conf, undefined, "generated");
|
||||
this._state = "loaded";
|
||||
}
|
||||
|
||||
@@ -202,6 +221,7 @@ class LovelacePanel extends LitElement {
|
||||
|
||||
private async _fetchConfig(forceDiskRefresh: boolean) {
|
||||
let conf: LovelaceConfig;
|
||||
let rawConf: LovelaceConfig | undefined;
|
||||
let confMode: Lovelace["mode"] = this.panel!.config.mode;
|
||||
let confProm: Promise<LovelaceConfig> | undefined;
|
||||
const llWindow = window as WindowWithLovelaceProm;
|
||||
@@ -236,7 +256,18 @@ class LovelacePanel extends LitElement {
|
||||
}
|
||||
|
||||
try {
|
||||
conf = await confProm!;
|
||||
rawConf = await confProm!;
|
||||
|
||||
// If strategy defined, apply it here.
|
||||
if (rawConf.strategy) {
|
||||
conf = await generateLovelaceDashboardStrategy({
|
||||
config: rawConf,
|
||||
hass: this.hass!,
|
||||
narrow: this.narrow,
|
||||
});
|
||||
} else {
|
||||
conf = rawConf;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code !== "config_not_found") {
|
||||
// eslint-disable-next-line
|
||||
@@ -245,8 +276,13 @@ class LovelacePanel extends LitElement {
|
||||
this._errorMsg = err.message;
|
||||
return;
|
||||
}
|
||||
const localize = await this.hass!.loadBackendTranslation("title");
|
||||
conf = await generateLovelaceConfigFromHass(this.hass!, localize);
|
||||
conf = await generateLovelaceDashboardStrategy(
|
||||
{
|
||||
hass: this.hass!,
|
||||
narrow: this.narrow,
|
||||
},
|
||||
DEFAULT_STRATEGY
|
||||
);
|
||||
confMode = "generated";
|
||||
} finally {
|
||||
// Ignore updates for another 2 seconds.
|
||||
@@ -258,7 +294,7 @@ class LovelacePanel extends LitElement {
|
||||
}
|
||||
|
||||
this._state = this._state === "yaml-editor" ? this._state : "loaded";
|
||||
this._setLovelaceConfig(conf, confMode);
|
||||
this._setLovelaceConfig(conf, rawConf, confMode);
|
||||
}
|
||||
|
||||
private _checkLovelaceConfig(config: LovelaceConfig) {
|
||||
@@ -277,11 +313,16 @@ class LovelacePanel extends LitElement {
|
||||
return checkedConfig ? deepFreeze(checkedConfig) : config;
|
||||
}
|
||||
|
||||
private _setLovelaceConfig(config: LovelaceConfig, mode: Lovelace["mode"]) {
|
||||
private _setLovelaceConfig(
|
||||
config: LovelaceConfig,
|
||||
rawConfig: LovelaceConfig | undefined,
|
||||
mode: Lovelace["mode"]
|
||||
) {
|
||||
config = this._checkLovelaceConfig(config);
|
||||
const urlPath = this.urlPath;
|
||||
this.lovelace = {
|
||||
config,
|
||||
rawConfig,
|
||||
mode,
|
||||
urlPath: this.urlPath,
|
||||
editMode: this.lovelace ? this.lovelace.editMode : false,
|
||||
@@ -294,22 +335,39 @@ class LovelacePanel extends LitElement {
|
||||
this._state = "yaml-editor";
|
||||
},
|
||||
setEditMode: (editMode: boolean) => {
|
||||
// If we use a strategy for dashboard, we cannot show the edit UI
|
||||
// So go straight to the YAML editor
|
||||
if (
|
||||
this.lovelace!.rawConfig &&
|
||||
this.lovelace!.rawConfig !== this.lovelace!.config
|
||||
) {
|
||||
this.lovelace!.enableFullEditMode();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!editMode || this.lovelace!.mode !== "generated") {
|
||||
this._updateLovelace({ editMode });
|
||||
return;
|
||||
}
|
||||
|
||||
showSaveDialog(this, {
|
||||
lovelace: this.lovelace!,
|
||||
mode: this.panel!.config.mode,
|
||||
narrow: this.narrow!,
|
||||
});
|
||||
},
|
||||
saveConfig: async (newConfig: LovelaceConfig): Promise<void> => {
|
||||
const { config: previousConfig, mode: previousMode } = this.lovelace!;
|
||||
const {
|
||||
config: previousConfig,
|
||||
rawConfig: previousRawConfig,
|
||||
mode: previousMode,
|
||||
} = this.lovelace!;
|
||||
newConfig = this._checkLovelaceConfig(newConfig);
|
||||
try {
|
||||
// Optimistic update
|
||||
this._updateLovelace({
|
||||
config: newConfig,
|
||||
rawConfig: undefined,
|
||||
mode: "storage",
|
||||
});
|
||||
this._ignoreNextUpdateEvent = true;
|
||||
@@ -320,18 +378,30 @@ class LovelacePanel extends LitElement {
|
||||
// Rollback the optimistic update
|
||||
this._updateLovelace({
|
||||
config: previousConfig,
|
||||
rawConfig: previousRawConfig,
|
||||
mode: previousMode,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
deleteConfig: async (): Promise<void> => {
|
||||
const { config: previousConfig, mode: previousMode } = this.lovelace!;
|
||||
const {
|
||||
config: previousConfig,
|
||||
rawConfig: previousRawConfig,
|
||||
mode: previousMode,
|
||||
} = this.lovelace!;
|
||||
try {
|
||||
// Optimistic update
|
||||
const localize = await this.hass!.loadBackendTranslation("title");
|
||||
const generatedConf = await generateLovelaceDashboardStrategy(
|
||||
{
|
||||
hass: this.hass!,
|
||||
narrow: this.narrow,
|
||||
},
|
||||
DEFAULT_STRATEGY
|
||||
);
|
||||
this._updateLovelace({
|
||||
config: await generateLovelaceConfigFromHass(this.hass!, localize),
|
||||
config: generatedConf,
|
||||
rawConfig: undefined,
|
||||
mode: "generated",
|
||||
editMode: false,
|
||||
});
|
||||
@@ -343,6 +413,7 @@ class LovelacePanel extends LitElement {
|
||||
// Rollback the optimistic update
|
||||
this._updateLovelace({
|
||||
config: previousConfig,
|
||||
rawConfig: previousRawConfig,
|
||||
mode: previousMode,
|
||||
});
|
||||
throw err;
|
||||
@@ -356,6 +427,18 @@ class LovelacePanel extends LitElement {
|
||||
...this.lovelace!,
|
||||
...props,
|
||||
};
|
||||
|
||||
if ("editMode" in props) {
|
||||
window.history.replaceState(
|
||||
null,
|
||||
"",
|
||||
constructUrlCurrentPath(
|
||||
props.editMode
|
||||
? addSearchParam({ edit: "1" })
|
||||
: removeSearchParam("edit")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -106,7 +106,7 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.yamlEditor.value = safeDump(this.lovelace!.config);
|
||||
this.yamlEditor.value = safeDump(this.lovelace!.rawConfig);
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
|
@@ -43,9 +43,7 @@ import { navigate } from "../../common/navigate";
|
||||
import {
|
||||
addSearchParam,
|
||||
extractSearchParam,
|
||||
removeSearchParam,
|
||||
} from "../../common/url/search-params";
|
||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { afterNextRender } from "../../common/util/render-status";
|
||||
@@ -539,7 +537,7 @@ class HUIRoot extends LitElement {
|
||||
protected firstUpdated() {
|
||||
// Check for requested edit mode
|
||||
if (extractSearchParam("edit") === "1") {
|
||||
this._enableEditMode();
|
||||
this.lovelace!.setEditMode(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -613,7 +611,7 @@ class HUIRoot extends LitElement {
|
||||
}
|
||||
|
||||
if (!force && huiView) {
|
||||
huiView.lovelace = this.lovelace;
|
||||
huiView.lovelace = this.lovelace!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -715,25 +713,11 @@ class HUIRoot extends LitElement {
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._enableEditMode();
|
||||
}
|
||||
|
||||
private _enableEditMode(): void {
|
||||
this.lovelace!.setEditMode(true);
|
||||
window.history.replaceState(
|
||||
null,
|
||||
"",
|
||||
constructUrlCurrentPath(addSearchParam({ edit: "1" }))
|
||||
);
|
||||
}
|
||||
|
||||
private _editModeDisable(): void {
|
||||
this.lovelace!.setEditMode(false);
|
||||
window.history.replaceState(
|
||||
null,
|
||||
"",
|
||||
constructUrlCurrentPath(removeSearchParam("edit"))
|
||||
);
|
||||
}
|
||||
|
||||
private _editLovelace() {
|
||||
@@ -837,7 +821,7 @@ class HUIRoot extends LitElement {
|
||||
const viewConfig = this.config.views[viewIndex];
|
||||
|
||||
if (!viewConfig) {
|
||||
this._enableEditMode();
|
||||
this.lovelace!.setEditMode(true);
|
||||
return;
|
||||
}
|
||||
|
||||
|
158
src/panels/lovelace/strategies/get-strategy.ts
Normal file
158
src/panels/lovelace/strategies/get-strategy.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { LovelaceConfig, LovelaceViewConfig } from "../../../data/lovelace";
|
||||
import { AsyncReturnType, HomeAssistant } from "../../../types";
|
||||
import { OriginalStatesStrategy } from "./original-states-strategy";
|
||||
|
||||
const MAX_WAIT_STRATEGY_LOAD = 5000;
|
||||
const CUSTOM_PREFIX = "custom:";
|
||||
|
||||
export interface LovelaceDashboardStrategy {
|
||||
generateDashboard(info: {
|
||||
config?: LovelaceConfig;
|
||||
hass: HomeAssistant;
|
||||
narrow: boolean | undefined;
|
||||
}): Promise<LovelaceConfig>;
|
||||
}
|
||||
|
||||
export interface LovelaceViewStrategy {
|
||||
generateView(info: {
|
||||
view: LovelaceViewConfig;
|
||||
config: LovelaceConfig;
|
||||
hass: HomeAssistant;
|
||||
narrow: boolean | undefined;
|
||||
}): Promise<LovelaceViewConfig>;
|
||||
}
|
||||
|
||||
const strategies: Record<
|
||||
string,
|
||||
LovelaceDashboardStrategy & LovelaceViewStrategy
|
||||
> = {
|
||||
"original-states": OriginalStatesStrategy,
|
||||
};
|
||||
|
||||
const getLovelaceStrategy = async <
|
||||
T extends LovelaceDashboardStrategy | LovelaceViewStrategy
|
||||
>(
|
||||
name: string
|
||||
): Promise<T> => {
|
||||
if (name in strategies) {
|
||||
return strategies[name] as T;
|
||||
}
|
||||
|
||||
if (!name.startsWith(CUSTOM_PREFIX)) {
|
||||
throw new Error("Unknown strategy");
|
||||
}
|
||||
|
||||
const tag = `ll-strategy-${name.substr(CUSTOM_PREFIX.length)}`;
|
||||
|
||||
if (
|
||||
(await Promise.race([
|
||||
customElements.whenDefined(tag),
|
||||
new Promise((resolve) =>
|
||||
setTimeout(() => resolve(true), MAX_WAIT_STRATEGY_LOAD)
|
||||
),
|
||||
])) === true
|
||||
) {
|
||||
throw new Error(
|
||||
`Timeout waiting for strategy element ${tag} to be registered`
|
||||
);
|
||||
}
|
||||
|
||||
return customElements.get(tag);
|
||||
};
|
||||
|
||||
interface GenerateMethods {
|
||||
generateDashboard: LovelaceDashboardStrategy["generateDashboard"];
|
||||
generateView: LovelaceViewStrategy["generateView"];
|
||||
}
|
||||
|
||||
const generateStrategy = async <T extends keyof GenerateMethods>(
|
||||
generateMethod: T,
|
||||
renderError: (err: string | Error) => AsyncReturnType<GenerateMethods[T]>,
|
||||
info: Parameters<GenerateMethods[T]>[0],
|
||||
name: string | undefined
|
||||
): Promise<ReturnType<GenerateMethods[T]>> => {
|
||||
if (!name) {
|
||||
return renderError("No strategy name found");
|
||||
}
|
||||
|
||||
try {
|
||||
const strategy = (await getLovelaceStrategy(name)) as any;
|
||||
return await strategy[generateMethod](info);
|
||||
} catch (err) {
|
||||
if (err.message !== "timeout") {
|
||||
// eslint-disable-next-line
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
return renderError(err);
|
||||
}
|
||||
};
|
||||
|
||||
export const generateLovelaceDashboardStrategy = async (
|
||||
info: Parameters<LovelaceDashboardStrategy["generateDashboard"]>[0],
|
||||
name?: string
|
||||
): ReturnType<LovelaceDashboardStrategy["generateDashboard"]> =>
|
||||
generateStrategy(
|
||||
"generateDashboard",
|
||||
(err) => ({
|
||||
views: [
|
||||
{
|
||||
title: "Error",
|
||||
cards: [
|
||||
{
|
||||
type: "markdown",
|
||||
content: `Error loading the dashboard strategy:\n> ${err}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
info,
|
||||
name || info.config?.strategy?.name
|
||||
);
|
||||
|
||||
export const generateLovelaceViewStrategy = async (
|
||||
info: Parameters<LovelaceViewStrategy["generateView"]>[0],
|
||||
name?: string
|
||||
): ReturnType<LovelaceViewStrategy["generateView"]> =>
|
||||
generateStrategy(
|
||||
"generateView",
|
||||
(err) => ({
|
||||
cards: [
|
||||
{
|
||||
type: "markdown",
|
||||
content: `Error loading the view strategy:\n> ${err}`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
info,
|
||||
name || info.view?.strategy?.name
|
||||
);
|
||||
|
||||
/**
|
||||
* Find all references to strategies and replaces them with the generated output
|
||||
*/
|
||||
export const expandLovelaceConfigStrategies = async (
|
||||
info: Parameters<LovelaceDashboardStrategy["generateDashboard"]>[0] & {
|
||||
config: LovelaceConfig;
|
||||
}
|
||||
): Promise<LovelaceConfig> => {
|
||||
const config = info.config.strategy
|
||||
? await generateLovelaceDashboardStrategy(info)
|
||||
: { ...info.config };
|
||||
|
||||
config.views = await Promise.all(
|
||||
config.views.map((view) =>
|
||||
view.strategy
|
||||
? generateLovelaceViewStrategy({
|
||||
hass: info.hass,
|
||||
narrow: info.narrow,
|
||||
config,
|
||||
view,
|
||||
})
|
||||
: view
|
||||
)
|
||||
);
|
||||
|
||||
return config;
|
||||
};
|
94
src/panels/lovelace/strategies/original-states-strategy.ts
Normal file
94
src/panels/lovelace/strategies/original-states-strategy.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { STATE_NOT_RUNNING } from "home-assistant-js-websocket";
|
||||
import { subscribeOne } from "../../../common/util/subscribe-one";
|
||||
import { subscribeAreaRegistry } from "../../../data/area_registry";
|
||||
import { subscribeDeviceRegistry } from "../../../data/device_registry";
|
||||
import { subscribeEntityRegistry } from "../../../data/entity_registry";
|
||||
import { generateDefaultViewConfig } from "../common/generate-lovelace-config";
|
||||
import {
|
||||
LovelaceViewStrategy,
|
||||
LovelaceDashboardStrategy,
|
||||
} from "./get-strategy";
|
||||
|
||||
let subscribedRegistries = false;
|
||||
|
||||
export class OriginalStatesStrategy {
|
||||
static async generateView(
|
||||
info: Parameters<LovelaceViewStrategy["generateView"]>[0]
|
||||
): ReturnType<LovelaceViewStrategy["generateView"]> {
|
||||
const hass = info.hass;
|
||||
|
||||
if (hass.config.state === STATE_NOT_RUNNING) {
|
||||
return {
|
||||
cards: [{ type: "starting" }],
|
||||
};
|
||||
}
|
||||
|
||||
if (hass.config.safe_mode) {
|
||||
return {
|
||||
cards: [{ type: "safe-mode" }],
|
||||
};
|
||||
}
|
||||
|
||||
// We leave this here so we always have the freshest data.
|
||||
if (!subscribedRegistries) {
|
||||
subscribedRegistries = true;
|
||||
subscribeAreaRegistry(hass.connection, () => undefined);
|
||||
subscribeDeviceRegistry(hass.connection, () => undefined);
|
||||
subscribeEntityRegistry(hass.connection, () => undefined);
|
||||
}
|
||||
|
||||
const [
|
||||
areaEntries,
|
||||
deviceEntries,
|
||||
entityEntries,
|
||||
localize,
|
||||
] = await Promise.all([
|
||||
subscribeOne(hass.connection, subscribeAreaRegistry),
|
||||
subscribeOne(hass.connection, subscribeDeviceRegistry),
|
||||
subscribeOne(hass.connection, subscribeEntityRegistry),
|
||||
hass.loadBackendTranslation("title"),
|
||||
]);
|
||||
|
||||
// User can override default view. If they didn't, we will add one
|
||||
// that contains all entities.
|
||||
const view = generateDefaultViewConfig(
|
||||
areaEntries,
|
||||
deviceEntries,
|
||||
entityEntries,
|
||||
hass.states,
|
||||
localize
|
||||
);
|
||||
|
||||
// Add map of geo locations to default view if loaded
|
||||
if (hass.config.components.includes("geo_location")) {
|
||||
if (view && view.cards) {
|
||||
view.cards.push({
|
||||
type: "map",
|
||||
geo_location_sources: ["all"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// User has no entities
|
||||
if (view.cards!.length === 0) {
|
||||
view.cards!.push({
|
||||
type: "empty-state",
|
||||
});
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
static async generateDashboard(
|
||||
info: Parameters<LovelaceDashboardStrategy["generateDashboard"]>[0]
|
||||
): ReturnType<LovelaceDashboardStrategy["generateDashboard"]> {
|
||||
return {
|
||||
views: [
|
||||
{
|
||||
strategy: { name: "original-states" },
|
||||
title: info.hass.config.location_name,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
@@ -18,6 +18,8 @@ declare global {
|
||||
|
||||
export interface Lovelace {
|
||||
config: LovelaceConfig;
|
||||
// If not set, a strategy was used to generate everything
|
||||
rawConfig: LovelaceConfig | undefined;
|
||||
editMode: boolean;
|
||||
urlPath: string | null;
|
||||
mode: "generated" | "yaml" | "storage";
|
||||
|
@@ -53,6 +53,8 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
@property({ type: Number }) public index?: number;
|
||||
|
||||
@property({ type: Boolean }) public isStrategy = false;
|
||||
|
||||
@property({ attribute: false }) public cards: Array<
|
||||
LovelaceCard | HuiErrorCard
|
||||
> = [];
|
||||
@@ -228,7 +230,7 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
private _addCardToColumn(columnEl, index, editMode) {
|
||||
const card: LovelaceCard = this.cards[index];
|
||||
if (!editMode) {
|
||||
if (!editMode || this.isStrategy) {
|
||||
card.editMode = false;
|
||||
columnEl.appendChild(card);
|
||||
} else {
|
||||
|
@@ -31,6 +31,8 @@ export class PanelView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
@property({ type: Number }) public index?: number;
|
||||
|
||||
@property({ type: Boolean }) public isStrategy = false;
|
||||
|
||||
@property({ attribute: false }) public cards: Array<
|
||||
LovelaceCard | HuiErrorCard
|
||||
> = [];
|
||||
@@ -101,10 +103,15 @@ export class PanelView extends LitElement implements LovelaceViewElement {
|
||||
}
|
||||
|
||||
private _createCard(): void {
|
||||
if (this.cards.length === 0) {
|
||||
this._card = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const card: LovelaceCard = this.cards[0];
|
||||
card.isPanel = true;
|
||||
|
||||
if (!this.lovelace?.editMode) {
|
||||
if (this.isStrategy || !this.lovelace?.editMode) {
|
||||
card.editMode = false;
|
||||
this._card = card;
|
||||
return;
|
||||
|
@@ -23,6 +23,7 @@ import { createViewElement } from "../create-element/create-view-element";
|
||||
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||
import { confDeleteCard } from "../editor/delete-card";
|
||||
import { generateLovelaceViewStrategy } from "../strategies/get-strategy";
|
||||
import type { Lovelace, LovelaceBadge, LovelaceCard } from "../types";
|
||||
|
||||
const DEFAULT_VIEW_LAYOUT = "masonry";
|
||||
@@ -39,13 +40,13 @@ declare global {
|
||||
|
||||
@customElement("hui-view")
|
||||
export class HUIView extends UpdatingElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public lovelace?: Lovelace;
|
||||
@property({ attribute: false }) public lovelace!: Lovelace;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ type: Number }) public index?: number;
|
||||
@property({ type: Number }) public index!: number;
|
||||
|
||||
@internalProperty() private _cards: Array<LovelaceCard | HuiErrorCard> = [];
|
||||
|
||||
@@ -55,6 +56,8 @@ export class HUIView extends UpdatingElement {
|
||||
|
||||
private _layoutElement?: LovelaceViewElement;
|
||||
|
||||
private _viewConfigTheme?: string;
|
||||
|
||||
// Public to make demo happy
|
||||
public createCardElement(cardConfig: LovelaceCardConfig) {
|
||||
const element = createCardElement(cardConfig) as LovelaceCard;
|
||||
@@ -89,129 +92,142 @@ export class HUIView extends UpdatingElement {
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
const hass = this.hass!;
|
||||
const lovelace = this.lovelace!;
|
||||
/*
|
||||
We need to handle the following use cases:
|
||||
- initialization: create layout element, populate
|
||||
- config changed to view with same layout element
|
||||
- config changed to view with different layout element
|
||||
- forwarded properties hass/narrow/lovelace/cards/badges change
|
||||
- cards/badges change if one is rebuild when it was loaded later
|
||||
- lovelace changes if edit mode is enabled or config has changed
|
||||
*/
|
||||
|
||||
const hassChanged = changedProperties.has("hass");
|
||||
const oldLovelace = changedProperties.get("lovelace") as Lovelace;
|
||||
|
||||
let editModeChanged = false;
|
||||
let configChanged = false;
|
||||
|
||||
if (changedProperties.has("index")) {
|
||||
configChanged = true;
|
||||
} else if (changedProperties.has("lovelace")) {
|
||||
editModeChanged =
|
||||
oldLovelace && lovelace.editMode !== oldLovelace.editMode;
|
||||
configChanged = !oldLovelace || lovelace.config !== oldLovelace.config;
|
||||
}
|
||||
|
||||
let viewConfig: LovelaceViewConfig | undefined;
|
||||
|
||||
if (configChanged) {
|
||||
viewConfig = lovelace.config.views[this.index!];
|
||||
viewConfig = {
|
||||
...viewConfig,
|
||||
type: viewConfig.panel
|
||||
? PANEL_VIEW_LAYOUT
|
||||
: viewConfig.type || DEFAULT_VIEW_LAYOUT,
|
||||
};
|
||||
}
|
||||
|
||||
let replace = false;
|
||||
const oldLovelace = changedProperties.get("lovelace") as this["lovelace"];
|
||||
|
||||
// If config has changed, create element if necessary and set all values.
|
||||
if (
|
||||
configChanged &&
|
||||
(!this._layoutElement || this._layoutElementType !== viewConfig!.type)
|
||||
changedProperties.has("index") ||
|
||||
(changedProperties.has("lovelace") &&
|
||||
(!oldLovelace ||
|
||||
this.lovelace.config.views[this.index] !==
|
||||
oldLovelace.config.views[this.index]))
|
||||
) {
|
||||
replace = true;
|
||||
this._layoutElement = createViewElement(viewConfig!);
|
||||
this._layoutElementType = viewConfig!.type;
|
||||
this._layoutElement.addEventListener("ll-create-card", () => {
|
||||
showCreateCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace!.config,
|
||||
saveConfig: this.lovelace!.saveConfig,
|
||||
path: [this.index!],
|
||||
this._initializeConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
// If no layout element, we're still creating one
|
||||
if (this._layoutElement) {
|
||||
// Config has not changed. Just props
|
||||
if (changedProperties.has("hass")) {
|
||||
this._badges.forEach((badge) => {
|
||||
badge.hass = this.hass;
|
||||
});
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-edit-card", (ev) => {
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace!.config,
|
||||
saveConfig: this.lovelace!.saveConfig,
|
||||
path: ev.detail.path,
|
||||
|
||||
this._cards.forEach((element) => {
|
||||
element.hass = this.hass;
|
||||
});
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-delete-card", (ev) => {
|
||||
confDeleteCard(this, this.hass!, this.lovelace!, ev.detail.path);
|
||||
});
|
||||
}
|
||||
|
||||
if (configChanged) {
|
||||
this._createBadges(viewConfig!);
|
||||
this._createCards(viewConfig!);
|
||||
|
||||
this._layoutElement!.hass = this.hass;
|
||||
this._layoutElement!.narrow = this.narrow;
|
||||
this._layoutElement!.lovelace = lovelace;
|
||||
this._layoutElement!.index = this.index;
|
||||
}
|
||||
|
||||
if (hassChanged) {
|
||||
this._badges.forEach((badge) => {
|
||||
badge.hass = hass;
|
||||
});
|
||||
|
||||
this._cards.forEach((element) => {
|
||||
element.hass = hass;
|
||||
});
|
||||
|
||||
this._layoutElement!.hass = this.hass;
|
||||
}
|
||||
|
||||
if (changedProperties.has("narrow")) {
|
||||
this._layoutElement!.narrow = this.narrow;
|
||||
}
|
||||
|
||||
if (editModeChanged) {
|
||||
this._layoutElement!.lovelace = lovelace;
|
||||
}
|
||||
|
||||
if (
|
||||
configChanged ||
|
||||
hassChanged ||
|
||||
editModeChanged ||
|
||||
changedProperties.has("_cards") ||
|
||||
changedProperties.has("_badges")
|
||||
) {
|
||||
this._layoutElement!.cards = this._cards;
|
||||
this._layoutElement!.badges = this._badges;
|
||||
this._layoutElement.hass = this.hass;
|
||||
}
|
||||
if (changedProperties.has("narrow")) {
|
||||
this._layoutElement.narrow = this.narrow;
|
||||
}
|
||||
if (changedProperties.has("lovelace")) {
|
||||
this._layoutElement.lovelace = this.lovelace;
|
||||
}
|
||||
if (changedProperties.has("_cards")) {
|
||||
this._layoutElement.cards = this._cards;
|
||||
}
|
||||
if (changedProperties.has("_badges")) {
|
||||
this._layoutElement.badges = this._badges;
|
||||
}
|
||||
}
|
||||
|
||||
const oldHass = changedProperties.get("hass") as this["hass"] | undefined;
|
||||
|
||||
if (
|
||||
configChanged ||
|
||||
editModeChanged ||
|
||||
(hassChanged &&
|
||||
oldHass &&
|
||||
(hass.themes !== oldHass.themes ||
|
||||
hass.selectedTheme !== oldHass.selectedTheme))
|
||||
changedProperties.has("hass") &&
|
||||
(!oldHass ||
|
||||
this.hass.themes !== oldHass.themes ||
|
||||
this.hass.selectedTheme !== oldHass.selectedTheme)
|
||||
) {
|
||||
applyThemesOnElement(
|
||||
this,
|
||||
hass.themes,
|
||||
lovelace.config.views[this.index!].theme
|
||||
);
|
||||
applyThemesOnElement(this, this.hass.themes, this._viewConfigTheme);
|
||||
}
|
||||
}
|
||||
|
||||
private async _initializeConfig() {
|
||||
let viewConfig = this.lovelace.config.views[this.index];
|
||||
let isStrategy = false;
|
||||
|
||||
if (viewConfig.strategy) {
|
||||
isStrategy = true;
|
||||
viewConfig = await generateLovelaceViewStrategy({
|
||||
hass: this.hass,
|
||||
config: this.lovelace.config,
|
||||
narrow: this.narrow,
|
||||
view: viewConfig,
|
||||
});
|
||||
}
|
||||
|
||||
if (this._layoutElement && replace) {
|
||||
viewConfig = {
|
||||
...viewConfig,
|
||||
type: viewConfig.panel
|
||||
? PANEL_VIEW_LAYOUT
|
||||
: viewConfig.type || DEFAULT_VIEW_LAYOUT,
|
||||
};
|
||||
|
||||
// Create a new layout element if necessary.
|
||||
let addLayoutElement = false;
|
||||
|
||||
if (!this._layoutElement || this._layoutElementType !== viewConfig.type) {
|
||||
addLayoutElement = true;
|
||||
this._createLayoutElement(viewConfig);
|
||||
}
|
||||
|
||||
this._createBadges(viewConfig);
|
||||
this._createCards(viewConfig);
|
||||
this._layoutElement!.isStrategy = isStrategy;
|
||||
this._layoutElement!.hass = this.hass;
|
||||
this._layoutElement!.narrow = this.narrow;
|
||||
this._layoutElement!.lovelace = this.lovelace;
|
||||
this._layoutElement!.index = this.index;
|
||||
this._layoutElement!.cards = this._cards;
|
||||
this._layoutElement!.badges = this._badges;
|
||||
|
||||
applyThemesOnElement(this, this.hass.themes, viewConfig.theme);
|
||||
this._viewConfigTheme = viewConfig.theme;
|
||||
|
||||
if (addLayoutElement) {
|
||||
while (this.lastChild) {
|
||||
this.removeChild(this.lastChild);
|
||||
}
|
||||
this.appendChild(this._layoutElement);
|
||||
this.appendChild(this._layoutElement!);
|
||||
}
|
||||
}
|
||||
|
||||
private _createLayoutElement(config: LovelaceViewConfig): void {
|
||||
this._layoutElement = createViewElement(config) as LovelaceViewElement;
|
||||
this._layoutElementType = config.type;
|
||||
this._layoutElement.addEventListener("ll-create-card", () => {
|
||||
showCreateCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: [this.index],
|
||||
});
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-edit-card", (ev) => {
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: ev.detail.path,
|
||||
});
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-delete-card", (ev) => {
|
||||
confDeleteCard(this, this.hass!, this.lovelace!, ev.detail.path);
|
||||
});
|
||||
}
|
||||
|
||||
private _createBadges(config: LovelaceViewConfig): void {
|
||||
if (!config || !config.badges || !Array.isArray(config.badges)) {
|
||||
this._badges = [];
|
||||
|
@@ -72,8 +72,6 @@ export const theme = EditorView.theme({
|
||||
".cm-panels.top": { borderBottom: "1px solid var(--divider-color)" },
|
||||
".cm-panels.bottom": { borderTop: "1px solid var(--divider-color)" },
|
||||
|
||||
".cm-panel.search input": { margin: "4px 4px 0" },
|
||||
|
||||
".cm-button": {
|
||||
border: "1px solid var(--primary-color)",
|
||||
padding: "0px 16px",
|
||||
|
@@ -3,6 +3,7 @@ import {
|
||||
callService,
|
||||
Connection,
|
||||
ERR_INVALID_AUTH,
|
||||
ERR_CONNECTION_LOST,
|
||||
HassConfig,
|
||||
subscribeConfig,
|
||||
subscribeEntities,
|
||||
@@ -13,6 +14,7 @@ import { broadcastConnectionStatus } from "../data/connection-status";
|
||||
import { subscribeFrontendUserData } from "../data/frontend";
|
||||
import { forwardHaptic } from "../data/haptics";
|
||||
import { DEFAULT_PANEL } from "../data/panel";
|
||||
import { serviceCallWillDisconnect } from "../data/service";
|
||||
import { NumberFormat } from "../data/translation";
|
||||
import { subscribePanels } from "../data/ws-panels";
|
||||
import { translationMetadata } from "../resources/translations-metadata";
|
||||
@@ -78,6 +80,12 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
target
|
||||
)) as Promise<ServiceCallResponse>;
|
||||
} catch (err) {
|
||||
if (
|
||||
err.error?.code === ERR_CONNECTION_LOST &&
|
||||
serviceCallWillDisconnect(domain, service)
|
||||
) {
|
||||
throw err;
|
||||
}
|
||||
if (__DEV__) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user