Compare commits

...

80 Commits

Author SHA1 Message Date
Ludeeus
faf8e49c12 Use autofit when we detect location 2021-10-29 07:10:10 +00:00
Bram Kragten
4624c3d75b Fix missing import (#10456) 2021-10-28 20:53:49 -05:00
Bram Kragten
7d196b4b95 Bumped version to 20211028.0 2021-10-28 20:06:14 +02:00
Bram Kragten
6347e44d94 Energy: Dont shrink today button (#10451) 2021-10-28 20:03:28 +02:00
Allen Porter
719d9386c5 Render Nest battery cam vertical video on screen correctly (#10431)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-10-28 18:02:06 +00:00
Bram Kragten
bb734be4bc Remove keep logged in query string after login, dont show on select_mfa_module step (#10439) 2021-10-28 19:05:30 +02:00
Bram Kragten
7cadaf1dc3 Use min value instead of hard coded 0 (#10443) 2021-10-28 17:16:09 +02:00
Bram Kragten
c30453a86f Fix title though close button in config/options flow (#10444) 2021-10-28 17:15:55 +02:00
Bram Kragten
c2e3d0188e Update translations 2021-10-28 17:06:48 +02:00
Bram Kragten
aabb8ea16f Fix camera more info pre load toggle (#10442) 2021-10-28 16:03:08 +02:00
Paul Bottein
df572d59c5 Custom iconsets in Icon Picker (#10399)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-10-28 13:28:14 +00:00
Allen Porter
5ef7a37c20 Fix for Nest WebRTC cams to not require stream component (#10432) 2021-10-28 13:47:15 +02:00
Paulus Schoutsen
4b44e197ae ha-form-integer to only show slider if < 256 steps (#10430) 2021-10-28 13:44:25 +02:00
Joakim Sørensen
8b5b21ae69 Fix icon overrides in logbook (#10434) 2021-10-28 13:43:14 +02:00
Joakim Sørensen
f5417fad6f Fix alignment in card editor elements (#10428) 2021-10-28 13:39:18 +02:00
Philip Allgaier
7fa6317f5c Invert "update" binary sensor device class color (#10427) 2021-10-28 11:50:52 +02:00
Paul Bottein
74533cebc6 Use tags and aliases when filtering icons in Icon Picker (#10425) 2021-10-27 20:12:12 +00:00
Bram Kragten
10986db7c6 Bumped version to 20211027.0 2021-10-27 21:03:31 +02:00
Joakim Sørensen
67648baca7 Fix Keep me logged in (#10422) 2021-10-27 15:40:24 +02:00
Joakim Sørensen
dc9182e9ab Fix missing logbook icons (#10423) 2021-10-27 15:39:39 +02:00
Paulus Schoutsen
4a7a81ffdb Improve rendering person card (#10419) 2021-10-27 15:38:57 +02:00
Joakim Sørensen
09ef72647e Add running to not inverted (#10420) 2021-10-27 09:17:53 +02:00
Paulus Schoutsen
da38e6f986 Hide script/sun from generated Lovelace (#10418) 2021-10-27 08:20:50 +02:00
Bram Kragten
bd1a9f2cb0 Add support for external stats (#10411) 2021-10-26 23:15:57 -07:00
Philip Allgaier
171eddd779 Shrink new section titles in more-info dialog a bit (#10414) 2021-10-26 22:19:37 +02:00
Paulus Schoutsen
7acc2f9e08 Merge remote-tracking branch 'origin/master' into dev 2021-10-26 13:13:54 -07:00
Paulus Schoutsen
27a6341137 Bumped version to 20211026.0 2021-10-26 13:12:06 -07:00
Paulus Schoutsen
6c5e15e707 Move entities to center column on device page (#10412) 2021-10-26 12:48:05 -07:00
Philip Allgaier
06b1718ade Add navigation option from more-info to history (#9717) 2021-10-26 21:12:52 +02:00
Paulus Schoutsen
e50d2e16a7 Improve device info add to Lovelace (#10413) 2021-10-26 21:03:19 +02:00
Philip Allgaier
0b2404a0f2 Make device classes in logbook translatable (#10376) 2021-10-26 21:00:28 +02:00
Bram Kragten
371804591d Add blueprint scripts (#9504) 2021-10-26 09:32:40 -07:00
Bram Kragten
54c64c15f3 Bump and patch material elements (#10406) 2021-10-26 16:39:35 +02:00
Bram Kragten
0e1124cd4f Bump codemirror (#10404) 2021-10-26 16:28:13 +02:00
Bram Kragten
70fd759e18 Bump format js (#10405) 2021-10-26 14:24:14 +02:00
Bram Kragten
8e383b2bec Bump Lit (#10409) 2021-10-26 14:23:18 +02:00
Joakim Sørensen
63cd576d56 Allow configuration_url to point to an internal panel (#10395) 2021-10-26 13:24:08 +02:00
Marc Hörsken
32ac04ea78 Add support for hiding current weather in forecast card (#10267) 2021-10-26 10:18:26 +00:00
Joakim Sørensen
5d6bacb0bd Use ha-alert to warn about logs from custom integrations (#10396) 2021-10-26 12:11:27 +02:00
Tobias Kündig
398d777681 Introduced ha-icon-overflow-menu component (#10352)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-10-26 12:10:53 +02:00
Paulus Schoutsen
549a360d98 Update delay label (#10284) 2021-10-26 12:09:35 +02:00
MartinT
1140e6026c Add "Keep me logged in" checkbox within login flow (#10226)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-10-26 12:05:13 +02:00
Philip Allgaier
29a1167782 Ensure consistent card look on device config page (#10386) 2021-10-26 11:12:27 +02:00
Joakim Sørensen
d61a77f2d9 Add running device class to binary sensor (#10400) 2021-10-25 23:05:35 +02:00
Philip Allgaier
b9bde1960b Ensure explicit false values from customize form get stored (#10381) 2021-10-25 20:33:26 +02:00
Nathan Orick
a12c2eea5d Ensure Sortable is recreated when card editors are reopened (#10382) 2021-10-25 19:49:00 +02:00
MartinT
b5c717a559 Do not close edit dialog when more info is escaped (#10249) 2021-10-25 19:48:17 +02:00
Joakim Sørensen
3adbc4cfaf Use ha-chip instead of ha-label-badge for add-on capabilities (#10398) 2021-10-25 18:25:37 +02:00
Paulus Schoutsen
dd11fb1b99 Add automation editor to gallery (#10392) 2021-10-25 15:53:32 +00:00
Bram Kragten
bf0d102c86 Fix timezone issues with date formatting for ES5 (#10370) 2021-10-25 08:33:15 -07:00
Joakim Sørensen
dad2b92d2e Use ha-chip for alarm control panel card (#10393)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-10-25 15:28:29 +00:00
Nathan Orick
d027ec0018 Add stopPropagation to move click handlers (#10379) 2021-10-25 17:08:30 +02:00
Philip Allgaier
0c038398aa Fix various slugify() issues + add tests (#10383) 2021-10-25 16:26:38 +02:00
Raman Gupta
5c3e0cc016 Add additional properties to zwave_js device info panel (#10132) 2021-10-25 16:13:59 +02:00
Zack Barett
9bcd26ce57 Fix Full Calendar Background color (#10373) 2021-10-25 15:23:55 +02:00
Rogério Ribeiro
3e8a6c418c Update markdown card to allow word to be broken (#10387) 2021-10-25 12:43:38 +00:00
Paulus Schoutsen
279f3e1183 Trim device name from entities on device page (#10285) 2021-10-25 12:56:33 +02:00
Paulus Schoutsen
f77339ad85 Make all automation type pickers use natural width to be able to show… (#10391) 2021-10-25 12:55:26 +02:00
Bram Kragten
da73b316ff Remove deprecated icons that where replaced (#10371) 2021-10-25 12:12:16 +02:00
Michael Irigoyen
82a49d2cbf Update MDI to v6.4.95 (#10389) 2021-10-25 11:00:32 +02:00
Bram Kragten
05711b4636 Catch error if input_datetime state is incorrect (#10237) 2021-10-22 09:46:58 -07:00
Kyle Niewiada
2c2809573f Add to do list support to markdown (#10129) 2021-10-22 08:49:00 -07:00
Philip Allgaier
bbbeafcc92 Restore proper state badge image behavior (#10369) 2021-10-22 14:09:23 +02:00
Bram Kragten
95c6adc739 Convert cloud account config to Lit (#10350) 2021-10-21 09:49:55 -07:00
Philip Allgaier
7c2e0aea92 Correct automation editor event action translation (#10355) 2021-10-21 15:14:26 +02:00
Franck Nijhof
d05c76356f Add auto slider/box mode to number entity (#10272)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-10-20 22:12:44 -07:00
Bram Kragten
cddf6ce1f4 Bumped version to 20211007.1 2021-10-09 17:39:08 +02:00
Bram Kragten
8abb212ae7 Fix alarm panel badge (#10221) 2021-10-09 17:38:46 +02:00
Joakim Sørensen
0056d75127 Fix icon overlay for person badges (#10201) 2021-10-09 17:38:29 +02:00
Bram Kragten
5be475ea17 Fix dirty check/leaving automation editor (#10211) 2021-10-09 17:38:09 +02:00
Philip Allgaier
b157cf5294 Make zone names readable on map in dark mode (#10195) 2021-10-09 17:37:45 +02:00
Philip Allgaier
48c9c89e3d Add "gas" device_class to customize (and sort existing ones) (#10196) 2021-10-09 17:37:28 +02:00
Bram Kragten
80bbc9990a Merge pull request #10190 from home-assistant/dev 2021-10-07 21:20:25 +02:00
Bram Kragten
736e117eca Merge pull request #10162 from home-assistant/dev 2021-10-06 10:18:40 +02:00
Bram Kragten
5e52bd905d Merge pull request #10154 from home-assistant/dev 2021-10-05 00:00:33 +02:00
Bram Kragten
31b69147f4 Merge pull request #10136 from home-assistant/dev 2021-10-02 22:41:19 +02:00
Bram Kragten
bc5010a953 Merge pull request #10109 from home-assistant/dev
20210930.0
2021-09-30 12:57:53 +02:00
Bram Kragten
49947f3337 Merge pull request #9915 from home-assistant/dev 2021-08-30 22:35:41 +02:00
Bram Kragten
d3ce4af541 Merge pull request #9761 from home-assistant/dev
20210809.0
2021-08-09 21:00:41 +02:00
Bram Kragten
d45f47d908 Merge pull request #9715 from home-assistant/dev
20210804.0
2021-08-04 23:53:37 +02:00
174 changed files with 17823 additions and 5641 deletions

View File

@@ -0,0 +1,12 @@
diff --git a/mwc-icon-button-base.js b/mwc-icon-button-base.js
index 45cdaab93ccc0a6daaaaabc01266dcdc32e46bfd..b3ea5b541597308d85f86ce6c23fd00785fda835 100644
--- a/mwc-icon-button-base.js
+++ b/mwc-icon-button-base.js
@@ -63,7 +63,6 @@ export class IconButtonBase extends LitElement {
@touchend="${this.handleRippleDeactivate}"
@touchcancel="${this.handleRippleDeactivate}"
>${this.renderRipple()}
- <i class="material-icons">${this.icon}</i>
<span
><slot></slot
></span>

View File

@@ -26,6 +26,7 @@ const getMeta = () => {
path: svg.match(/ d="([^"]+)"/)[1], path: svg.match(/ d="([^"]+)"/)[1],
name: icon.name, name: icon.name,
tags: icon.tags, tags: icon.tags,
aliases: icon.aliases,
}; };
}); });
}; };
@@ -37,6 +38,7 @@ const addRemovedMeta = (meta) => {
path: removeIcon.path, path: removeIcon.path,
name: removeIcon.name, name: removeIcon.name,
tags: [], tags: [],
aliases: [],
})); }));
const combinedMeta = [...meta, ...removedMeta]; const combinedMeta = [...meta, ...removedMeta];
return combinedMeta.sort((a, b) => a.name.localeCompare(b.name)); return combinedMeta.sort((a, b) => a.name.localeCompare(b.name));
@@ -99,6 +101,7 @@ const findDifferentiator = (curString, prevString) => {
gulp.task("gen-icons-json", (done) => { gulp.task("gen-icons-json", (done) => {
const meta = getMeta(); const meta = getMeta();
const metaAndRemoved = addRemovedMeta(meta); const metaAndRemoved = addRemovedMeta(meta);
const split = splitBySize(metaAndRemoved); const split = splitBySize(metaAndRemoved);
@@ -138,11 +141,17 @@ gulp.task("gen-icons-json", (done) => {
JSON.stringify({ version: package.version, parts }) JSON.stringify({ version: package.version, parts })
); );
const orderedMeta = orderMeta(meta);
fs.writeFileSync( fs.writeFileSync(
path.resolve(OUTPUT_DIR, "iconList.json"), path.resolve(OUTPUT_DIR, "iconList.json"),
JSON.stringify(orderedMeta.map((icon) => icon.name)) JSON.stringify(
orderMeta(meta).map((icon) => ({
name: icon.name,
keywords: [
...icon.tags.map((t) => t.toLowerCase().replace(/\s\/\s/g, " ")),
...icon.aliases,
],
}))
)
); );
done(); done();

View File

@@ -0,0 +1,91 @@
/* eslint-disable lit/no-template-arrow */
import { LitElement, TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators";
import { provideHass } from "../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../src/types";
import "../components/demo-black-white-row";
import { mockEntityRegistry } from "../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../demo/src/stubs/hassio_supervisor";
import "../../../src/panels/config/automation/action/ha-automation-action";
import { HaChooseAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-choose";
import { HaDelayAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-delay";
import { HaDeviceAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-device_id";
import { HaEventAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-event";
import { HaRepeatAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-repeat";
import { HaSceneAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-scene";
import { HaServiceAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-service";
import { HaWaitForTriggerAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger";
import { HaWaitAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-wait_template";
import { Action } from "../../../src/data/script";
import { HaConditionAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-condition";
const SCHEMAS: { name: string; actions: Action[] }[] = [
{ name: "Event", actions: [HaEventAction.defaultConfig] },
{ name: "Device", actions: [HaDeviceAction.defaultConfig] },
{ name: "Service", actions: [HaServiceAction.defaultConfig] },
{ name: "Condition", actions: [HaConditionAction.defaultConfig] },
{ name: "Delay", actions: [HaDelayAction.defaultConfig] },
{ name: "Scene", actions: [HaSceneAction.defaultConfig] },
{ name: "Wait", actions: [HaWaitAction.defaultConfig] },
{ name: "WaitForTrigger", actions: [HaWaitForTriggerAction.defaultConfig] },
{ name: "Repeat", actions: [HaRepeatAction.defaultConfig] },
{ name: "Choose", actions: [HaChooseAction.defaultConfig] },
{ name: "Variables", actions: [{ variables: { hello: "1" } }] },
];
@customElement("demo-automation-editor-action")
class DemoHaAutomationEditorAction extends LitElement {
@state() private hass!: HomeAssistant;
private data: any = SCHEMAS.map((info) => info.actions);
constructor() {
super();
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
mockEntityRegistry(hass);
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockHassioSupervisor(hass);
}
protected render(): TemplateResult {
const valueChanged = (ev) => {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
};
return html`
${SCHEMAS.map(
(info, sampleIdx) => html`
<demo-black-white-row
.title=${info.name}
.value=${this.data[sampleIdx]}
>
${["light", "dark"].map(
(slot) =>
html`
<ha-automation-action
slot=${slot}
.hass=${this.hass}
.actions=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx}
@value-changed=${valueChanged}
></ha-automation-action>
`
)}
</demo-black-white-row>
`
)}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-ha-automation-editor-action": DemoHaAutomationEditorAction;
}
}

View File

@@ -0,0 +1,127 @@
/* eslint-disable lit/no-template-arrow */
import { LitElement, TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators";
import { provideHass } from "../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../src/types";
import "../components/demo-black-white-row";
import { mockEntityRegistry } from "../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../demo/src/stubs/hassio_supervisor";
import type { Condition } from "../../../src/data/automation";
import "../../../src/panels/config/automation/condition/ha-automation-condition";
import { HaDeviceCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-device";
import { HaLogicalCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-logical";
import HaNumericStateCondition from "../../../src/panels/config/automation/condition/types/ha-automation-condition-numeric_state";
import { HaStateCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-state";
import { HaSunCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-sun";
import { HaTemplateCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-template";
import { HaTimeCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-time";
import { HaTriggerCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-trigger";
import { HaZoneCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-zone";
const SCHEMAS: { name: string; conditions: Condition[] }[] = [
{
name: "State",
conditions: [{ condition: "state", ...HaStateCondition.defaultConfig }],
},
{
name: "Numeric State",
conditions: [
{ condition: "numeric_state", ...HaNumericStateCondition.defaultConfig },
],
},
{
name: "Sun",
conditions: [{ condition: "sun", ...HaSunCondition.defaultConfig }],
},
{
name: "Zone",
conditions: [{ condition: "zone", ...HaZoneCondition.defaultConfig }],
},
{
name: "Time",
conditions: [{ condition: "time", ...HaTimeCondition.defaultConfig }],
},
{
name: "Template",
conditions: [
{ condition: "template", ...HaTemplateCondition.defaultConfig },
],
},
{
name: "Device",
conditions: [{ condition: "device", ...HaDeviceCondition.defaultConfig }],
},
{
name: "And",
conditions: [{ condition: "and", ...HaLogicalCondition.defaultConfig }],
},
{
name: "Or",
conditions: [{ condition: "or", ...HaLogicalCondition.defaultConfig }],
},
{
name: "Not",
conditions: [{ condition: "not", ...HaLogicalCondition.defaultConfig }],
},
{
name: "Trigger",
conditions: [{ condition: "trigger", ...HaTriggerCondition.defaultConfig }],
},
];
@customElement("demo-automation-editor-condition")
class DemoHaAutomationEditorCondition extends LitElement {
@state() private hass!: HomeAssistant;
private data: any = SCHEMAS.map((info) => info.conditions);
constructor() {
super();
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
mockEntityRegistry(hass);
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockHassioSupervisor(hass);
}
protected render(): TemplateResult {
const valueChanged = (ev) => {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
};
return html`
${SCHEMAS.map(
(info, sampleIdx) => html`
<demo-black-white-row
.title=${info.name}
.value=${this.data[sampleIdx]}
>
${["light", "dark"].map(
(slot) =>
html`
<ha-automation-condition
slot=${slot}
.hass=${this.hass}
.conditions=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx}
@value-changed=${valueChanged}
></ha-automation-condition>
`
)}
</demo-black-white-row>
`
)}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-ha-automation-editor-condition": DemoHaAutomationEditorCondition;
}
}

View File

@@ -0,0 +1,159 @@
/* eslint-disable lit/no-template-arrow */
import { LitElement, TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators";
import { provideHass } from "../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../src/types";
import "../components/demo-black-white-row";
import { mockEntityRegistry } from "../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../demo/src/stubs/hassio_supervisor";
import type { Trigger } from "../../../src/data/automation";
import { HaGeolocationTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location";
import { HaEventTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-event";
import { HaHassTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-homeassistant";
import { HaNumericStateTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state";
import { HaSunTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-sun";
import { HaTagTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-tag";
import { HaTemplateTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-template";
import { HaTimeTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-time";
import { HaTimePatternTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-time_pattern";
import { HaWebhookTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-webhook";
import { HaZoneTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-zone";
import { HaDeviceTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-device";
import { HaStateTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-state";
import { HaMQTTTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
import "../../../src/panels/config/automation/trigger/ha-automation-trigger";
const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
{
name: "State",
triggers: [{ platform: "state", ...HaStateTrigger.defaultConfig }],
},
{
name: "MQTT",
triggers: [{ platform: "mqtt", ...HaMQTTTrigger.defaultConfig }],
},
{
name: "GeoLocation",
triggers: [
{ platform: "geo_location", ...HaGeolocationTrigger.defaultConfig },
],
},
{
name: "Home Assistant",
triggers: [{ platform: "homeassistant", ...HaHassTrigger.defaultConfig }],
},
{
name: "Numeric State",
triggers: [
{ platform: "numeric_state", ...HaNumericStateTrigger.defaultConfig },
],
},
{
name: "Sun",
triggers: [{ platform: "sun", ...HaSunTrigger.defaultConfig }],
},
{
name: "Time Pattern",
triggers: [
{ platform: "time_pattern", ...HaTimePatternTrigger.defaultConfig },
],
},
{
name: "Webhook",
triggers: [{ platform: "webhook", ...HaWebhookTrigger.defaultConfig }],
},
{
name: "Zone",
triggers: [{ platform: "zone", ...HaZoneTrigger.defaultConfig }],
},
{
name: "Tag",
triggers: [{ platform: "tag", ...HaTagTrigger.defaultConfig }],
},
{
name: "Time",
triggers: [{ platform: "time", ...HaTimeTrigger.defaultConfig }],
},
{
name: "Template",
triggers: [{ platform: "template", ...HaTemplateTrigger.defaultConfig }],
},
{
name: "Event",
triggers: [{ platform: "event", ...HaEventTrigger.defaultConfig }],
},
{
name: "Device Trigger",
triggers: [{ platform: "device", ...HaDeviceTrigger.defaultConfig }],
},
];
@customElement("demo-automation-editor-trigger")
class DemoHaAutomationEditorTrigger extends LitElement {
@state() private hass!: HomeAssistant;
private data: any = SCHEMAS.map((info) => info.triggers);
constructor() {
super();
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
mockEntityRegistry(hass);
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockHassioSupervisor(hass);
}
protected render(): TemplateResult {
const valueChanged = (ev) => {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
};
return html`
${SCHEMAS.map(
(info, sampleIdx) => html`
<demo-black-white-row
.title=${info.name}
.value=${this.data[sampleIdx]}
>
${["light", "dark"].map(
(slot) =>
html`
<ha-automation-trigger
slot=${slot}
.hass=${this.hass}
.triggers=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx}
@value-changed=${valueChanged}
></ha-automation-trigger>
`
)}
</demo-black-white-row>
`
)}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-ha-automation-editor-trigger": DemoHaAutomationEditorTrigger;
}
}

View File

@@ -222,6 +222,30 @@ const SCHEMAS: {
}, },
], ],
}, },
{
title: "OctoPrint",
translations: {
username: "Username",
host: "Host",
port: "Port Number",
path: "Application Path",
ssl: "Use SSL",
},
schema: [
{ type: "string", name: "username", required: true, default: "" },
{ type: "string", name: "host", required: true, default: "" },
{
type: "integer",
valueMin: 1,
valueMax: 65535,
name: "port",
optional: true,
default: 80,
},
{ type: "string", name: "path", optional: true, default: "/" },
{ type: "boolean", name: "ssl", optional: true, default: false },
],
},
]; ];
@customElement("demo-ha-form") @customElement("demo-ha-form")

View File

@@ -11,6 +11,12 @@ import {
mdiHomeAssistant, mdiHomeAssistant,
mdiKey, mdiKey,
mdiNetwork, mdiNetwork,
mdiNumeric1,
mdiNumeric2,
mdiNumeric3,
mdiNumeric4,
mdiNumeric5,
mdiNumeric6,
mdiPound, mdiPound,
mdiShield, mdiShield,
} from "@mdi/js"; } from "@mdi/js";
@@ -25,7 +31,7 @@ import "../../../../src/components/buttons/ha-call-api-button";
import "../../../../src/components/buttons/ha-progress-button"; import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert"; import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
import "../../../../src/components/ha-label-badge"; import "../../../../src/components/ha-chip";
import "../../../../src/components/ha-markdown"; import "../../../../src/components/ha-markdown";
import "../../../../src/components/ha-settings-row"; import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
@@ -73,6 +79,15 @@ const STAGE_ICON = {
deprecated: mdiExclamationThick, deprecated: mdiExclamationThick,
}; };
const RATING_ICON = {
1: mdiNumeric1,
2: mdiNumeric2,
3: mdiNumeric3,
4: mdiNumeric4,
5: mdiNumeric5,
6: mdiNumeric6,
};
@customElement("hassio-addon-info") @customElement("hassio-addon-info")
class HassioAddonInfo extends LitElement { class HassioAddonInfo extends LitElement {
@property({ type: Boolean }) public narrow!: boolean; @property({ type: Boolean }) public narrow!: boolean;
@@ -246,6 +261,163 @@ class HassioAddonInfo extends LitElement {
>`} >`}
</div> </div>
<div class="capabilities">
${this.addon.stage !== "stable"
? html` <ha-chip
hasIcon
class=${classMap({
yellow: this.addon.stage === "experimental",
red: this.addon.stage === "deprecated",
})}
@click=${this._showMoreInfo}
id="stage"
>
<ha-svg-icon
slot="icon"
.path=${STAGE_ICON[this.addon.stage]}
>
</ha-svg-icon>
${this.supervisor.localize(
`addon.dashboard.capability.stages.${this.addon.stage}`
)}
</ha-chip>`
: ""}
<ha-chip
hasIcon
class=${classMap({
green: [5, 6].includes(Number(this.addon.rating)),
yellow: [3, 4].includes(Number(this.addon.rating)),
red: [1, 2].includes(Number(this.addon.rating)),
})}
@click=${this._showMoreInfo}
id="rating"
>
<ha-svg-icon slot="icon" .path=${RATING_ICON[this.addon.rating]}>
</ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.rating"
)}
</ha-chip>
${this.addon.host_network
? html`
<ha-chip
hasIcon
@click=${this._showMoreInfo}
id="host_network"
>
<ha-svg-icon slot="icon" .path=${mdiNetwork}> </ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.host"
)}
</ha-chip>
`
: ""}
${this.addon.full_access
? html`
<ha-chip
hasIcon
@click=${this._showMoreInfo}
id="full_access"
>
<ha-svg-icon slot="icon" .path=${mdiChip}></ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.hardware"
)}
</ha-chip>
`
: ""}
${this.addon.homeassistant_api
? html`
<ha-chip
hasIcon
@click=${this._showMoreInfo}
id="homeassistant_api"
>
<ha-svg-icon
slot="icon"
.path=${mdiHomeAssistant}
></ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.core"
)}
</ha-chip>
`
: ""}
${this._computeHassioApi
? html`
<ha-chip hasIcon @click=${this._showMoreInfo} id="hassio_api">
<ha-svg-icon
slot="icon"
.path=${mdiHomeAssistant}
></ha-svg-icon>
${this.supervisor.localize(
`addon.dashboard.capability.role.${this.addon.hassio_role}`
) || this.addon.hassio_role}
</ha-chip>
`
: ""}
${this.addon.docker_api
? html`
<ha-chip hasIcon @click=${this._showMoreInfo} id="docker_api">
<ha-svg-icon slot="icon" .path=${mdiDocker}></ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.docker"
)}
</ha-chip>
`
: ""}
${this.addon.host_pid
? html`
<ha-chip hasIcon @click=${this._showMoreInfo} id="host_pid">
<ha-svg-icon slot="icon" .path=${mdiPound}></ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.host_pid"
)}
</ha-chip>
`
: ""}
${this.addon.apparmor !== "default"
? html`
<ha-chip
hasIcon
@click=${this._showMoreInfo}
class=${this._computeApparmorClassName}
id="apparmor"
>
<ha-svg-icon slot="icon" .path=${mdiShield}></ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.apparmor"
)}
</ha-chip>
`
: ""}
${this.addon.auth_api
? html`
<ha-chip hasIcon @click=${this._showMoreInfo} id="auth_api">
<ha-svg-icon slot="icon" .path=${mdiKey}></ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.auth"
)}
</ha-chip>
`
: ""}
${this.addon.ingress
? html`
<ha-chip hasIcon @click=${this._showMoreInfo} id="ingress">
<ha-svg-icon
slot="icon"
.path=${mdiCursorDefaultClickOutline}
></ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.ingress"
)}
</ha-chip>
`
: ""}
</div>
<div class="description light-color"> <div class="description light-color">
${this.addon.description}.<br /> ${this.addon.description}.<br />
${this.supervisor.localize( ${this.supervisor.localize(
@@ -266,172 +438,6 @@ class HassioAddonInfo extends LitElement {
/> />
` `
: ""} : ""}
<div class="security">
${this.addon.stage !== "stable"
? html` <ha-label-badge
class=${classMap({
yellow: this.addon.stage === "experimental",
red: this.addon.stage === "deprecated",
})}
@click=${this._showMoreInfo}
id="stage"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.stage"
)}
description=""
>
<ha-svg-icon
.path=${STAGE_ICON[this.addon.stage]}
></ha-svg-icon>
</ha-label-badge>`
: ""}
<ha-label-badge
class=${classMap({
green: [5, 6].includes(Number(this.addon.rating)),
yellow: [3, 4].includes(Number(this.addon.rating)),
red: [1, 2].includes(Number(this.addon.rating)),
})}
@click=${this._showMoreInfo}
id="rating"
label="rating"
description=""
>
${this.addon.rating}
</ha-label-badge>
${this.addon.host_network
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="host_network"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.host"
)}
description=""
>
<ha-svg-icon .path=${mdiNetwork}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this.addon.full_access
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="full_access"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.hardware"
)}
description=""
>
<ha-svg-icon .path=${mdiChip}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this.addon.homeassistant_api
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="homeassistant_api"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.hass"
)}
description=""
>
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this._computeHassioApi
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="hassio_api"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.hassio"
)}
.description=${this.supervisor.localize(
`addon.dashboard.capability.role.${this.addon.hassio_role}`
) || this.addon.hassio_role}
>
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this.addon.docker_api
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="docker_api"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.docker"
)}
description=""
>
<ha-svg-icon .path=${mdiDocker}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this.addon.host_pid
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="host_pid"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.host_pid"
)}
description=""
>
<ha-svg-icon .path=${mdiPound}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this.addon.apparmor
? html`
<ha-label-badge
@click=${this._showMoreInfo}
class=${this._computeApparmorClassName}
id="apparmor"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.apparmor"
)}
description=""
>
<ha-svg-icon .path=${mdiShield}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this.addon.auth_api
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="auth_api"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.auth"
)}
description=""
>
<ha-svg-icon .path=${mdiKey}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this.addon.ingress
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="ingress"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.ingress"
)}
description=""
>
<ha-svg-icon
.path=${mdiCursorDefaultClickOutline}
></ha-svg-icon>
</ha-label-badge>
`
: ""}
</div>
${this.addon.version ${this.addon.version
? html` ? html`
<div <div
@@ -1175,34 +1181,31 @@ class HassioAddonInfo extends LitElement {
.description a { .description a {
color: var(--primary-color); color: var(--primary-color);
} }
ha-chip {
text-transform: capitalize;
--ha-chip-text-color: var(--text-primary-color);
--ha-chip-background-color: var(--primary-color);
}
.red { .red {
--ha-label-badge-color: var(--label-badge-red, #df4c1e); --ha-chip-background-color: var(--label-badge-red, #df4c1e);
} }
.blue { .blue {
--ha-label-badge-color: var(--label-badge-blue, #039be5); --ha-chip-background-color: var(--label-badge-blue, #039be5);
} }
.green { .green {
--ha-label-badge-color: var(--label-badge-green, #0da035); --ha-chip-background-color: var(--label-badge-green, #0da035);
} }
.yellow { .yellow {
--ha-label-badge-color: var(--label-badge-yellow, #f4b400); --ha-chip-background-color: var(--label-badge-yellow, #f4b400);
} }
.security { .capabilities {
margin-bottom: 16px; margin-bottom: 16px;
} }
.card-actions { .card-actions {
justify-content: space-between; justify-content: space-between;
display: flex; display: flex;
} }
.security h3 {
margin-bottom: 8px;
font-weight: normal;
}
.security ha-label-badge {
cursor: pointer;
margin-right: 4px;
--ha-label-badge-padding: 8px 0 0 0;
}
.changelog { .changelog {
display: contents; display: contents;
} }
@@ -1242,6 +1245,9 @@ class HassioAddonInfo extends LitElement {
} }
@media (max-width: 720px) { @media (max-width: 720px) {
ha-chip {
line-height: 36px;
}
.addon-options { .addon-options {
max-width: 100%; max-width: 100%;
} }

View File

@@ -22,23 +22,23 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@braintree/sanitize-url": "^5.0.2", "@braintree/sanitize-url": "^5.0.2",
"@codemirror/commands": "^0.19.2", "@codemirror/commands": "^0.19.5",
"@codemirror/gutter": "^0.19.1", "@codemirror/gutter": "^0.19.3",
"@codemirror/highlight": "^0.19.2", "@codemirror/highlight": "^0.19.6",
"@codemirror/history": "^0.19.0", "@codemirror/history": "^0.19.0",
"@codemirror/legacy-modes": "^0.19.0", "@codemirror/legacy-modes": "^0.19.0",
"@codemirror/rectangular-selection": "^0.19.0", "@codemirror/rectangular-selection": "^0.19.1",
"@codemirror/search": "^0.19.0", "@codemirror/search": "^0.19.2",
"@codemirror/state": "^0.19.1", "@codemirror/state": "^0.19.2",
"@codemirror/stream-parser": "^0.19.1", "@codemirror/stream-parser": "^0.19.2",
"@codemirror/text": "^0.19.2", "@codemirror/text": "^0.19.4",
"@codemirror/view": "^0.19.4", "@codemirror/view": "^0.19.9",
"@formatjs/intl-datetimeformat": "^4.2.4", "@formatjs/intl-datetimeformat": "^4.2.5",
"@formatjs/intl-getcanonicallocales": "^1.7.3", "@formatjs/intl-getcanonicallocales": "^1.8.0",
"@formatjs/intl-locale": "^2.4.38", "@formatjs/intl-locale": "^2.4.40",
"@formatjs/intl-numberformat": "^7.2.4", "@formatjs/intl-numberformat": "^7.2.5",
"@formatjs/intl-pluralrules": "^4.1.4", "@formatjs/intl-pluralrules": "^4.1.5",
"@formatjs/intl-relativetimeformat": "^9.3.1", "@formatjs/intl-relativetimeformat": "^9.3.2",
"@formatjs/intl-utils": "^3.8.4", "@formatjs/intl-utils": "^3.8.4",
"@fullcalendar/common": "5.9.0", "@fullcalendar/common": "5.9.0",
"@fullcalendar/core": "5.9.0", "@fullcalendar/core": "5.9.0",
@@ -46,29 +46,29 @@
"@fullcalendar/interaction": "5.9.0", "@fullcalendar/interaction": "5.9.0",
"@fullcalendar/list": "5.9.0", "@fullcalendar/list": "5.9.0",
"@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.6.0#./.yarn/patches/@lit-labs/virtualizer/0.7.0.patch", "@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.6.0#./.yarn/patches/@lit-labs/virtualizer/0.7.0.patch",
"@material/chips": "14.0.0-canary.353ca7e9f.0", "@material/chips": "14.0.0-canary.261f2db59.0",
"@material/data-table": "14.0.0-canary.353ca7e9f.0", "@material/data-table": "14.0.0-canary.261f2db59.0",
"@material/mwc-button": "0.25.2", "@material/mwc-button": "0.25.3",
"@material/mwc-checkbox": "0.25.2", "@material/mwc-checkbox": "0.25.3",
"@material/mwc-circular-progress": "0.25.2", "@material/mwc-circular-progress": "0.25.3",
"@material/mwc-dialog": "0.25.2", "@material/mwc-dialog": "0.25.3",
"@material/mwc-fab": "0.25.2", "@material/mwc-fab": "0.25.3",
"@material/mwc-formfield": "0.25.2", "@material/mwc-formfield": "0.25.3",
"@material/mwc-icon-button": "0.25.2", "@material/mwc-icon-button": "patch:@material/mwc-icon-button@0.25.3#./.yarn/patches/@material/mwc-icon-button/remove-icon.patch",
"@material/mwc-linear-progress": "0.25.2", "@material/mwc-linear-progress": "0.25.3",
"@material/mwc-list": "0.25.2", "@material/mwc-list": "0.25.3",
"@material/mwc-menu": "0.25.2", "@material/mwc-menu": "0.25.3",
"@material/mwc-radio": "0.25.2", "@material/mwc-radio": "0.25.3",
"@material/mwc-ripple": "0.25.2", "@material/mwc-ripple": "0.25.3",
"@material/mwc-select": "0.25.2", "@material/mwc-select": "0.25.3",
"@material/mwc-slider": "0.25.2", "@material/mwc-slider": "0.25.3",
"@material/mwc-switch": "0.25.2", "@material/mwc-switch": "0.25.3",
"@material/mwc-tab": "0.25.2", "@material/mwc-tab": "0.25.3",
"@material/mwc-tab-bar": "0.25.2", "@material/mwc-tab-bar": "0.25.3",
"@material/mwc-textfield": "0.25.2", "@material/mwc-textfield": "0.25.3",
"@material/top-app-bar": "14.0.0-canary.353ca7e9f.0", "@material/top-app-bar": "14.0.0-canary.261f2db59.0",
"@mdi/js": "6.3.95", "@mdi/js": "6.4.95",
"@mdi/svg": "6.3.95", "@mdi/svg": "6.4.95",
"@polymer/app-layout": "^3.1.0", "@polymer/app-layout": "^3.1.0",
"@polymer/iron-flex-layout": "^3.0.1", "@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1", "@polymer/iron-icon": "^3.0.1",
@@ -78,7 +78,6 @@
"@polymer/paper-input": "^3.2.1", "@polymer/paper-input": "^3.2.1",
"@polymer/paper-item": "^3.0.1", "@polymer/paper-item": "^3.0.1",
"@polymer/paper-listbox": "^3.0.1", "@polymer/paper-listbox": "^3.0.1",
"@polymer/paper-ripple": "^3.0.2",
"@polymer/paper-slider": "^3.0.1", "@polymer/paper-slider": "^3.0.1",
"@polymer/paper-styles": "^3.0.1", "@polymer/paper-styles": "^3.0.1",
"@polymer/paper-tabs": "^3.1.0", "@polymer/paper-tabs": "^3.1.0",
@@ -109,7 +108,7 @@
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",
"leaflet-draw": "^1.0.4", "leaflet-draw": "^1.0.4",
"lit": "^2.0.0", "lit": "^2.0.2",
"lit-vaadin-helpers": "^0.2.1", "lit-vaadin-helpers": "^0.2.1",
"marked": "^3.0.2", "marked": "^3.0.2",
"memoize-one": "^5.2.1", "memoize-one": "^5.2.1",
@@ -181,7 +180,7 @@
"eslint-import-resolver-webpack": "^0.13.1", "eslint-import-resolver-webpack": "^0.13.1",
"eslint-plugin-disable": "^2.0.1", "eslint-plugin-disable": "^2.0.1",
"eslint-plugin-import": "^2.24.2", "eslint-plugin-import": "^2.24.2",
"eslint-plugin-lit": "^1.5.1", "eslint-plugin-lit": "^1.6.1",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-unused-imports": "^1.1.5", "eslint-plugin-unused-imports": "^1.1.5",
"eslint-plugin-wc": "^1.3.2", "eslint-plugin-wc": "^1.3.2",
@@ -231,10 +230,10 @@
"resolutions": { "resolutions": {
"@polymer/polymer": "patch:@polymer/polymer@3.4.1#./.yarn/patches/@polymer/polymer/pr-5569.patch", "@polymer/polymer": "patch:@polymer/polymer@3.4.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
"@webcomponents/webcomponentsjs": "^2.2.10", "@webcomponents/webcomponentsjs": "^2.2.10",
"lit": "^2.0.0", "lit": "^2.0.2",
"lit-html": "2.0.0", "lit-html": "2.0.1",
"lit-element": "3.0.0", "lit-element": "3.0.1",
"@lit/reactive-element": "1.0.0" "@lit/reactive-element": "1.0.1"
}, },
"main": "src/home-assistant.js", "main": "src/home-assistant.js",
"husky": { "husky": {

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup( setup(
name="home-assistant-frontend", name="home-assistant-frontend",
version="20211020.0", version="20211028.0",
description="The Home Assistant frontend", description="The Home Assistant frontend",
url="https://github.com/home-assistant/frontend", url="https://github.com/home-assistant/frontend",
author="The Home Assistant Authors", author="The Home Assistant Authors",

View File

@@ -1,4 +1,5 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { genClientId } from "home-assistant-js-websocket";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -7,18 +8,20 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import "./ha-password-manager-polyfill";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import "../components/ha-form/ha-form";
import "../components/ha-markdown";
import "../components/ha-alert"; import "../components/ha-alert";
import "../components/ha-checkbox";
import { computeInitialHaFormData } from "../components/ha-form/compute-initial-ha-form-data";
import "../components/ha-form/ha-form";
import "../components/ha-formfield";
import "../components/ha-markdown";
import { AuthProvider } from "../data/auth"; import { AuthProvider } from "../data/auth";
import { import {
DataEntryFlowStep, DataEntryFlowStep,
DataEntryFlowStepForm, DataEntryFlowStepForm,
} from "../data/data_entry_flow"; } from "../data/data_entry_flow";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin"; import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import { computeInitialHaFormData } from "../components/ha-form/compute-initial-ha-form-data"; import "./ha-password-manager-polyfill";
type State = "loading" | "error" | "step"; type State = "loading" | "error" | "step";
@@ -41,6 +44,8 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
@state() private _submitting = false; @state() private _submitting = false;
@state() private _storeToken = false;
willUpdate(changedProps: PropertyValues) { willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps); super.willUpdate(changedProps);
@@ -201,12 +206,30 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
.computeError=${this._computeErrorCallback(step)} .computeError=${this._computeErrorCallback(step)}
@value-changed=${this._stepDataChanged} @value-changed=${this._stepDataChanged}
></ha-form> ></ha-form>
${this.clientId === genClientId() &&
!["select_mfa_module", "mfa"].includes(step.step_id)
? html`
<ha-formfield
class="store-token"
.label=${this.localize("ui.panel.page-authorize.store_token")}
>
<ha-checkbox
.checked=${this._storeToken}
@change=${this._storeTokenChanged}
></ha-checkbox>
</ha-formfield>
`
: ""}
`; `;
default: default:
return html``; return html``;
} }
} }
private _storeTokenChanged(e: CustomEvent<HTMLInputElement>) {
this._storeToken = (e.currentTarget as HTMLInputElement).checked;
}
private async _providerChanged(newProvider?: AuthProvider) { private async _providerChanged(newProvider?: AuthProvider) {
if (this._step && this._step.type === "form") { if (this._step && this._step.type === "form") {
fetch(`/auth/login_flow/${this._step.flow_id}`, { fetch(`/auth/login_flow/${this._step.flow_id}`, {
@@ -274,6 +297,9 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
if (this.oauth2State) { if (this.oauth2State) {
url += `&state=${encodeURIComponent(this.oauth2State)}`; url += `&state=${encodeURIComponent(this.oauth2State)}`;
} }
if (this._storeToken) {
url += `&storeToken=true`;
}
document.location.assign(url); document.location.assign(url);
} }
@@ -357,6 +383,11 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
margin: 24px 0 8px; margin: 24px 0 8px;
text-align: center; text-align: center;
} }
/* Align with the rest of the form. */
.store-token {
margin-top: 10px;
margin-left: -16px;
}
`; `;
} }
} }

View File

@@ -1,4 +1,5 @@
import { AuthData } from "home-assistant-js-websocket"; import { AuthData } from "home-assistant-js-websocket";
import { extractSearchParam } from "../url/search-params";
const storage = window.localStorage || {}; const storage = window.localStorage || {};
@@ -30,6 +31,11 @@ export function askWrite() {
export function saveTokens(tokens: AuthData | null) { export function saveTokens(tokens: AuthData | null) {
tokenCache.tokens = tokens; tokenCache.tokens = tokens;
if (!tokenCache.writeEnabled && extractSearchParam("storeToken") === "true") {
tokenCache.writeEnabled = true;
}
if (tokenCache.writeEnabled) { if (tokenCache.writeEnabled) {
try { try {
storage.hassTokens = JSON.stringify(tokens); storage.hassTokens = JSON.stringify(tokens);
@@ -45,7 +51,6 @@ export function enableWrite() {
saveTokens(tokenCache.tokens); saveTokens(tokenCache.tokens);
} }
} }
export function loadTokens() { export function loadTokens() {
if (tokenCache.tokens === undefined) { if (tokenCache.tokens === undefined) {
try { try {

View File

@@ -1,2 +1,3 @@
/** An empty image which can be set as src of an img element. */ /** An empty image which can be set as src of an img element. */
export default "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; export const emptyImageBase64 =
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";

View File

@@ -1,43 +1,45 @@
import { import {
mdiBattery,
mdiBatteryOutline,
mdiBatteryCharging,
mdiThermometer,
mdiSnowflake,
mdiServerNetworkOff,
mdiServerNetwork,
mdiDoorClosed,
mdiDoorOpen,
mdiGarage,
mdiGarageOpen,
mdiPowerPlugOff,
mdiPowerPlug,
mdiCheckCircle,
mdiAlertCircle, mdiAlertCircle,
mdiSmoke, mdiBattery,
mdiFire, mdiBatteryCharging,
mdiBatteryOutline,
mdiBrightness5, mdiBrightness5,
mdiBrightness7, mdiBrightness7,
mdiCheckboxMarkedCircle,
mdiCheckCircle,
mdiCropPortrait,
mdiDoorClosed,
mdiDoorOpen,
mdiFire,
mdiGarage,
mdiGarageOpen,
mdiHome,
mdiHomeOutline,
mdiLock, mdiLock,
mdiLockOpen, mdiLockOpen,
mdiWaterOff,
mdiWater,
mdiWalk,
mdiRun,
mdiHomeOutline,
mdiHome,
mdiSquare,
mdiSquareOutline,
mdiMusicNoteOff,
mdiMusicNote, mdiMusicNote,
mdiMusicNoteOff,
mdiPackage, mdiPackage,
mdiPackageUp, mdiPackageUp,
mdiCropPortrait, mdiPlay,
mdiPowerPlug,
mdiPowerPlugOff,
mdiRadioboxBlank,
mdiRun,
mdiServerNetwork,
mdiServerNetworkOff,
mdiSmoke,
mdiSnowflake,
mdiSquare,
mdiSquareOutline,
mdiStop,
mdiThermometer,
mdiVibrate, mdiVibrate,
mdiWalk,
mdiWater,
mdiWaterOff,
mdiWindowClosed, mdiWindowClosed,
mdiWindowOpen, mdiWindowOpen,
mdiRadioboxBlank,
mdiCheckboxMarkedCircle,
} from "@mdi/js"; } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
@@ -85,6 +87,8 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
return is_off ? mdiPowerPlugOff : mdiPowerPlug; return is_off ? mdiPowerPlugOff : mdiPowerPlug;
case "presence": case "presence":
return is_off ? mdiHomeOutline : mdiHome; return is_off ? mdiHomeOutline : mdiHome;
case "running":
return is_off ? mdiStop : mdiPlay;
case "sound": case "sound":
return is_off ? mdiMusicNoteOff : mdiMusicNote; return is_off ? mdiMusicNoteOff : mdiMusicNote;
case "update": case "update":

View File

@@ -39,7 +39,7 @@ export const computeStateDisplay = (
const domain = computeStateDomain(stateObj); const domain = computeStateDomain(stateObj);
if (domain === "input_datetime") { if (domain === "input_datetime") {
if (state) { if (state !== undefined) {
// If trying to display an explicit state, need to parse the explict state to `Date` then format. // If trying to display an explicit state, need to parse the explict state to `Date` then format.
// Attributes aren't available, we have to use `state`. // Attributes aren't available, we have to use `state`.
try { try {
@@ -63,7 +63,7 @@ export const computeStateDisplay = (
} }
} }
return state; return state;
} catch { } catch (_e) {
// Formatting methods may throw error if date parsing doesn't go well, // Formatting methods may throw error if date parsing doesn't go well,
// just return the state string in that case. // just return the state string in that case.
return state; return state;
@@ -71,7 +71,17 @@ export const computeStateDisplay = (
} else { } else {
// If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format. // If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format.
let date: Date; let date: Date;
if (!stateObj.attributes.has_time) { if (stateObj.attributes.has_date && stateObj.attributes.has_time) {
date = new Date(
stateObj.attributes.year,
stateObj.attributes.month - 1,
stateObj.attributes.day,
stateObj.attributes.hour,
stateObj.attributes.minute
);
return formatDateTime(date, locale);
}
if (stateObj.attributes.has_date) {
date = new Date( date = new Date(
stateObj.attributes.year, stateObj.attributes.year,
stateObj.attributes.month - 1, stateObj.attributes.month - 1,
@@ -79,20 +89,12 @@ export const computeStateDisplay = (
); );
return formatDate(date, locale); return formatDate(date, locale);
} }
if (!stateObj.attributes.has_date) { if (stateObj.attributes.has_time) {
date = new Date(); date = new Date();
date.setHours(stateObj.attributes.hour, stateObj.attributes.minute); date.setHours(stateObj.attributes.hour, stateObj.attributes.minute);
return formatTime(date, locale); return formatTime(date, locale);
} }
return stateObj.state;
date = new Date(
stateObj.attributes.year,
stateObj.attributes.month - 1,
stateObj.attributes.day,
stateObj.attributes.hour,
stateObj.attributes.minute
);
return formatDateTime(date, locale);
} }
} }

View File

@@ -0,0 +1,24 @@
/**
* Strips a device name from an entity name.
* @param entityName the entity name
* @param lowerCasedPrefixWithSpaceSuffix the prefix to strip, lower cased with a space suffix
* @returns
*/
export const stripPrefixFromEntityName = (
entityName: string,
lowerCasedPrefixWithSpaceSuffix: string
) => {
if (!entityName.toLowerCase().startsWith(lowerCasedPrefixWithSpaceSuffix)) {
return undefined;
}
const newName = entityName.substring(lowerCasedPrefixWithSpaceSuffix.length);
// If first word already has an upper case letter (e.g. from brand name)
// leave as-is, otherwise capitalize the first word.
return hasUpperCase(newName.substr(0, newName.indexOf(" ")))
? newName
: newName[0].toUpperCase() + newName.slice(1);
};
const hasUpperCase = (str: string): boolean => str.toLowerCase() !== str;

View File

@@ -12,8 +12,8 @@ export const slugify = (value: string, delimiter = "_") => {
.replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters .replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters
.replace(/&/g, `${delimiter}and${delimiter}`) // Replace & with 'and' .replace(/&/g, `${delimiter}and${delimiter}`) // Replace & with 'and'
.replace(/[^\w-]+/g, "") // Remove all non-word characters .replace(/[^\w-]+/g, "") // Remove all non-word characters
.replace(/-/, delimiter) // Replace - with delimiter .replace(/-/g, delimiter) // Replace - with delimiter
.replace(new RegExp(`/${delimiter}${delimiter}+/`, "g"), delimiter) // Replace multiple delimiters with single delimiter .replace(new RegExp(`(${delimiter})\\1+`, "g"), "$1") // Replace multiple delimiters with single delimiter
.replace(new RegExp(`/^${delimiter}+/`), "") // Trim delimiter from start of text .replace(new RegExp(`^${delimiter}+`), "") // Trim delimiter from start of text
.replace(new RegExp(`/-+$/`), ""); // Trim delimiter from end of text .replace(new RegExp(`${delimiter}+$`), ""); // Trim delimiter from end of text
}; };

View File

@@ -24,7 +24,7 @@ const BINARY_SENSOR_DEVICE_CLASS_COLOR_NOT_INVERTED = new Set([
"plug", "plug",
"power", "power",
"presence", "presence",
"update", "running",
]); ]);
const STATIC_STATE_COLORS = new Set([ const STATIC_STATE_COLORS = new Set([

View File

@@ -70,7 +70,7 @@ export interface DataTableSortColumnData {
export interface DataTableColumnData extends DataTableSortColumnData { export interface DataTableColumnData extends DataTableSortColumnData {
title: TemplateResult | string; title: TemplateResult | string;
type?: "numeric" | "icon" | "icon-button"; type?: "numeric" | "icon" | "icon-button" | "overflow-menu";
template?: <T>(data: any, row: T) => TemplateResult | string; template?: <T>(data: any, row: T) => TemplateResult | string;
width?: string; width?: string;
maxWidth?: string; maxWidth?: string;
@@ -281,15 +281,13 @@ export class HaDataTable extends LitElement {
} }
const sorted = key === this._sortColumn; const sorted = key === this._sortColumn;
const classes = { const classes = {
"mdc-data-table__header-cell--numeric": Boolean( "mdc-data-table__header-cell--numeric":
column.type === "numeric" column.type === "numeric",
), "mdc-data-table__header-cell--icon": column.type === "icon",
"mdc-data-table__header-cell--icon": Boolean( "mdc-data-table__header-cell--icon-button":
column.type === "icon" column.type === "icon-button",
), "mdc-data-table__header-cell--overflow-menu":
"mdc-data-table__header-cell--icon-button": Boolean( column.type === "overflow-menu",
column.type === "icon-button"
),
sortable: Boolean(column.sortable), sortable: Boolean(column.sortable),
"not-sorted": Boolean(column.sortable && !sorted), "not-sorted": Boolean(column.sortable && !sorted),
grows: Boolean(column.grows), grows: Boolean(column.grows),
@@ -405,14 +403,14 @@ export class HaDataTable extends LitElement {
<div <div
role="cell" role="cell"
class="mdc-data-table__cell ${classMap({ class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--numeric": Boolean( "mdc-data-table__cell--numeric":
column.type === "numeric" column.type === "numeric",
), "mdc-data-table__cell--icon":
"mdc-data-table__cell--icon": Boolean( column.type === "icon",
column.type === "icon"
),
"mdc-data-table__cell--icon-button": "mdc-data-table__cell--icon-button":
Boolean(column.type === "icon-button"), column.type === "icon-button",
"mdc-data-table__cell--overflow-menu":
column.type === "overflow-menu",
grows: Boolean(column.grows), grows: Boolean(column.grows),
forceLTR: Boolean(column.forceLTR), forceLTR: Boolean(column.forceLTR),
})}" })}"
@@ -769,40 +767,65 @@ export class HaDataTable extends LitElement {
margin-left: -8px; margin-left: -8px;
} }
.mdc-data-table__cell--overflow-menu,
.mdc-data-table__header-cell--overflow-menu,
.mdc-data-table__header-cell--icon-button, .mdc-data-table__header-cell--icon-button,
.mdc-data-table__cell--icon-button { .mdc-data-table__cell--icon-button {
width: 56px;
padding: 8px; padding: 8px;
} }
.mdc-data-table__header-cell--icon-button,
.mdc-data-table__cell--icon-button {
width: 56px;
}
.mdc-data-table__cell--overflow-menu,
.mdc-data-table__cell--icon-button { .mdc-data-table__cell--icon-button {
color: var(--secondary-text-color); color: var(--secondary-text-color);
text-overflow: clip; text-overflow: clip;
} }
.mdc-data-table__header-cell--icon-button:first-child, .mdc-data-table__header-cell--icon-button:first-child,
.mdc-data-table__cell--icon-button:first-child { .mdc-data-table__cell--icon-button:first-child,
width: 64px;
padding-left: 16px;
}
:host([dir="rtl"])
.mdc-data-table__header-cell--icon-button:first-child,
:host([dir="rtl"]) .mdc-data-table__cell--icon-button:first-child {
padding-left: auto;
padding-right: 16px;
}
.mdc-data-table__header-cell--icon-button:last-child, .mdc-data-table__header-cell--icon-button:last-child,
.mdc-data-table__cell--icon-button:last-child { .mdc-data-table__cell--icon-button:last-child {
width: 64px; width: 64px;
padding-right: 16px;
}
:host([dir="rtl"]) .mdc-data-table__header-cell--icon-button:last-child,
:host([dir="rtl"]) .mdc-data-table__cell--icon-button:last-child {
padding-right: auto;
padding-left: 16px;
} }
.mdc-data-table__cell--overflow-menu:first-child,
.mdc-data-table__header-cell--overflow-menu:first-child,
.mdc-data-table__header-cell--icon-button:first-child,
.mdc-data-table__cell--icon-button:first-child {
padding-left: 16px;
}
:host([dir="rtl"])
.mdc-data-table__header-cell--overflow-menu:first-child,
:host([dir="rtl"]) .mdc-data-table__cell--overflow-menu:first-child,
:host([dir="rtl"])
.mdc-data-table__header-cell--overflow-menu:first-child,
:host([dir="rtl"]) .mdc-data-table__cell--overflow-menu:first-child {
padding-left: 8px;
padding-right: 16px;
}
.mdc-data-table__cell--overflow-menu:last-child,
.mdc-data-table__header-cell--overflow-menu:last-child,
.mdc-data-table__header-cell--icon-button:last-child,
.mdc-data-table__cell--icon-button:last-child {
padding-right: 16px;
}
:host([dir="rtl"])
.mdc-data-table__header-cell--overflow-menu:last-child,
:host([dir="rtl"]) .mdc-data-table__cell--overflow-menu:last-child,
:host([dir="rtl"]) .mdc-data-table__header-cell--icon-button:last-child,
:host([dir="rtl"]) .mdc-data-table__cell--icon-button:last-child {
padding-right: 8px;
padding-left: 16px;
}
.mdc-data-table__cell--overflow-menu,
.mdc-data-table__header-cell--overflow-menu {
overflow: initial;
}
.mdc-data-table__cell--icon-button a { .mdc-data-table__cell--icon-button a {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }

View File

@@ -77,7 +77,7 @@ export class HaStateLabelBadge extends LitElement {
const domain = computeStateDomain(entityState); const domain = computeStateDomain(entityState);
const showIcon = this.icon || this._computeShowIcon(domain, entityState); const showIcon = this.icon || this._computeShowIcon(domain, entityState);
const image = showIcon const image = this.icon
? "" ? ""
: this.image : this.image
? this.image ? this.image

View File

@@ -102,7 +102,12 @@ export class HaStatisticPicker extends LitElement {
</style> </style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon> <ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-icon-item> <paper-icon-item>
<state-badge slot="item-icon" .stateObj=${item.state}></state-badge> ${item.state
? html`<state-badge
slot="item-icon"
.stateObj=${item.state}
></state-badge>`
: ""}
<paper-item-body two-line=""> <paper-item-body two-line="">
${item.name} ${item.name}
<span secondary <span secondary
@@ -153,7 +158,10 @@ export class HaStatisticPicker extends LitElement {
const entityState = this.hass.states[meta.statistic_id]; const entityState = this.hass.states[meta.statistic_id];
if (!entityState) { if (!entityState) {
if (!entitiesOnly) { if (!entitiesOnly) {
output.push({ id: meta.statistic_id, name: meta.statistic_id }); output.push({
id: meta.statistic_id,
name: meta.name || meta.statistic_id,
});
} }
return; return;
} }

View File

@@ -80,16 +80,14 @@ export class StateBadge extends LitElement {
this._showIcon = true; this._showIcon = true;
if (stateObj) { if (stateObj && this.overrideImage === undefined) {
// hide icon if we have entity picture // hide icon if we have entity picture
if ( if (
((stateObj.attributes.entity_picture_local || (stateObj.attributes.entity_picture_local ||
stateObj.attributes.entity_picture) && stateObj.attributes.entity_picture) &&
!this.overrideIcon) || !this.overrideIcon
this.overrideImage
) { ) {
let imageUrl = let imageUrl =
this.overrideImage ||
stateObj.attributes.entity_picture_local || stateObj.attributes.entity_picture_local ||
stateObj.attributes.entity_picture; stateObj.attributes.entity_picture;
if (this.hass) { if (this.hass) {

View File

@@ -86,7 +86,7 @@ class HaCameraStream extends LitElement {
} }
if (this.stateObj.attributes.frontend_stream_type === STREAM_TYPE_HLS) { if (this.stateObj.attributes.frontend_stream_type === STREAM_TYPE_HLS) {
return this._url return this._url
? html` <ha-hls-player ? html`<ha-hls-player
autoplay autoplay
playsinline playsinline
.allowExoPlayer=${this.allowExoPlayer} .allowExoPlayer=${this.allowExoPlayer}
@@ -98,7 +98,7 @@ class HaCameraStream extends LitElement {
: html``; : html``;
} }
if (this.stateObj.attributes.frontend_stream_type === STREAM_TYPE_WEB_RTC) { if (this.stateObj.attributes.frontend_stream_type === STREAM_TYPE_WEB_RTC) {
return html` <ha-web-rtc-player return html`<ha-web-rtc-player
autoplay autoplay
playsinline playsinline
.muted=${this.muted} .muted=${this.muted}
@@ -115,23 +115,18 @@ class HaCameraStream extends LitElement {
// Fallback when unable to fetch stream url // Fallback when unable to fetch stream url
return true; return true;
} }
if ( if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) {
!isComponentLoaded(this.hass!, "stream") ||
!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)
) {
// Steaming is not supported by the camera so fallback to MJPEG stream // Steaming is not supported by the camera so fallback to MJPEG stream
return true; return true;
} }
if ( if (
this.stateObj!.attributes.frontend_stream_type === STREAM_TYPE_WEB_RTC && this.stateObj!.attributes.frontend_stream_type === STREAM_TYPE_WEB_RTC
typeof RTCPeerConnection === "undefined"
) { ) {
// Stream requires WebRTC but browser does not support, so fallback to // Browser support required for WebRTC
// MJPEG stream. return typeof RTCPeerConnection === "undefined";
return true;
} }
// Render stream // Server side stream component required for HLS
return false; return !isComponentLoaded(this.hass!, "stream");
} }
private async _getStreamUrl(): Promise<void> { private async _getStreamUrl(): Promise<void> {

View File

@@ -36,7 +36,11 @@ export class HaFormInteger extends LitElement implements HaFormElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if ("valueMin" in this.schema && "valueMax" in this.schema) { if (
this.schema.valueMin !== undefined &&
this.schema.valueMax !== undefined &&
this.schema.valueMax - this.schema.valueMin < 256
) {
return html` return html`
<div> <div>
${this.label} ${this.label}
@@ -96,10 +100,15 @@ export class HaFormInteger extends LitElement implements HaFormElement {
} }
if (this.schema.optional) { if (this.schema.optional) {
return 0; return this.schema.valueMin || 0;
} }
return this.schema.description?.suggested_value || this.schema.default || 0; return (
this.schema.description?.suggested_value ||
this.schema.default ||
this.schema.valueMin ||
0
);
} }
private _handleCheckboxChange(ev: Event) { private _handleCheckboxChange(ev: Event) {

View File

@@ -52,6 +52,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
return html` return html`
<mwc-select <mwc-select
fixedMenuPosition fixedMenuPosition
naturalMenuWidth
.label=${this.label} .label=${this.label}
.value=${this.data} .value=${this.data}
.disabled=${this.disabled} .disabled=${this.disabled}

View File

@@ -229,6 +229,7 @@ class HaHLSPlayer extends LitElement {
video { video {
width: 100%; width: 100%;
max-height: var(--video-max-height, calc(100vh - 97px));
} }
`; `;
} }

View File

@@ -0,0 +1,119 @@
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "./ha-button-menu";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-icon-button";
import "./ha-svg-icon";
import { mdiDotsVertical } from "@mdi/js";
import { HomeAssistant } from "../types";
import "@polymer/paper-tooltip/paper-tooltip";
export interface IconOverflowMenuItem {
[key: string]: any;
path: string;
label: string;
narrowOnly?: boolean;
disabled?: boolean;
tooltip?: string;
onClick: CallableFunction;
}
@customElement("ha-icon-overflow-menu")
export class HaIconOverflowMenu extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Array }) public items: IconOverflowMenuItem[] = [];
@property({ type: Boolean }) public narrow = false;
protected render(): TemplateResult {
return html`
${this.narrow
? html` <!-- Collapsed Representation for Small Screens -->
<ha-button-menu
@click=${this._handleIconOverflowMenuOpened}
@closed=${this._handleIconOverflowMenuClosed}
class="ha-icon-overflow-menu-overflow"
corner="BOTTOM_START"
absolute
>
<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>
${this.items.map(
(item) => html`
<mwc-list-item
graphic="icon"
.disabled=${item.disabled}
@click=${item.action}
>
<div slot="graphic">
<ha-svg-icon .path=${item.path}></ha-svg-icon>
</div>
${item.label}
</mwc-list-item>
`
)}
</ha-button-menu>`
: html`
<!-- Icon Representation for Big Screens -->
${this.items.map((item) =>
item.narrowOnly
? ""
: html`<div>
${item.tooltip
? html`<paper-tooltip animation-delay="0" position="left">
${item.tooltip}
</paper-tooltip>`
: ""}
<mwc-icon-button
@click=${item.action}
.label=${item.label}
.disabled=${item.disabled}
>
<ha-svg-icon .path=${item.path}></ha-svg-icon>
</mwc-icon-button>
</div> `
)}
`}
`;
}
protected _handleIconOverflowMenuOpened() {
// If this component is used inside a data table, the z-index of the row
// needs to be increased. Otherwise the ha-button-menu would be displayed
// underneath the next row in the table.
const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
if (row) {
row.style.zIndex = "1";
}
}
protected _handleIconOverflowMenuClosed() {
const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
if (row) {
row.style.zIndex = "";
}
}
static get styles() {
return css`
:host {
display: flex;
justify-content: flex-end;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-icon-overflow-menu": HaIconOverflowMenu;
}
}

View File

@@ -7,14 +7,19 @@ import { css, html, LitElement, TemplateResult } from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers"; import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { customIcons } from "../data/custom_icons";
import { PolymerChangedEvent } from "../polymer-types"; import { PolymerChangedEvent } from "../polymer-types";
import "./ha-icon"; import "./ha-icon";
import "./ha-icon-button"; import "./ha-icon-button";
let mdiIconList: string[] = []; type IconItem = {
icon: string;
keywords: string[];
};
let iconItems: IconItem[] = [];
// eslint-disable-next-line lit/prefer-static-styles // eslint-disable-next-line lit/prefer-static-styles
const rowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style> const rowRenderer: ComboBoxLitRenderer<IconItem> = (item) => html`<style>
paper-icon-item { paper-icon-item {
padding: 0; padding: 0;
margin: -8px; margin: -8px;
@@ -37,8 +42,8 @@ const rowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon> <ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-icon-item> <paper-icon-item>
<ha-icon .icon=${item} slot="item-icon"></ha-icon> <ha-icon .icon=${item.icon} slot="item-icon"></ha-icon>
<paper-item-body>${item}</paper-item-body> <paper-item-body>${item.icon}</paper-item-body>
</paper-icon-item>`; </paper-icon-item>`;
@customElement("ha-icon-picker") @customElement("ha-icon-picker")
@@ -66,7 +71,7 @@ export class HaIconPicker extends LitElement {
item-label-path="icon" item-label-path="icon"
.value=${this._value} .value=${this._value}
allow-custom-value allow-custom-value
.filteredItems=${mdiIconList} .filteredItems=${iconItems}
${comboBoxRenderer(rowRenderer)} ${comboBoxRenderer(rowRenderer)}
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@@ -105,10 +110,38 @@ export class HaIconPicker extends LitElement {
private async _openedChanged(ev: PolymerChangedEvent<boolean>) { private async _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value; this._opened = ev.detail.value;
if (this._opened && !mdiIconList.length) { if (this._opened && !iconItems.length) {
const iconList = await import("../../build/mdi/iconList.json"); const iconList = await import("../../build/mdi/iconList.json");
mdiIconList = iconList.default.map((icon) => `mdi:${icon}`);
(this.comboBox as any).filteredItems = mdiIconList; iconItems = iconList.default.map((icon) => ({
icon: `mdi:${icon.name}`,
keywords: icon.keywords,
}));
(this.comboBox as any).filteredItems = iconItems;
Object.keys(customIcons).forEach((iconSet) => {
this._loadCustomIconItems(iconSet);
});
}
}
private async _loadCustomIconItems(iconsetPrefix: string) {
try {
const getIconList = customIcons[iconsetPrefix].getIconList;
if (typeof getIconList !== "function") {
return;
}
const iconList = await getIconList();
const customIconItems = iconList.map((icon) => ({
icon: `${iconsetPrefix}:${icon.name}`,
keywords: icon.keywords ?? [],
}));
iconItems.push(...customIconItems);
(this.comboBox as any).filteredItems = iconItems;
} catch (e) {
// eslint-disable-next-line
console.warn(`Unable to load icon list for ${iconsetPrefix} iconset`);
} }
} }
@@ -133,16 +166,30 @@ export class HaIconPicker extends LitElement {
const filterString = ev.detail.value.toLowerCase(); const filterString = ev.detail.value.toLowerCase();
const characterCount = filterString.length; const characterCount = filterString.length;
if (characterCount >= 2) { if (characterCount >= 2) {
const filteredItems = mdiIconList.filter((icon) => const filteredItems: IconItem[] = [];
icon.includes(filterString) const filteredItemsByKeywords: IconItem[] = [];
);
iconItems.forEach((item) => {
if (item.icon.includes(filterString)) {
filteredItems.push(item);
return;
}
if (item.keywords.some((t) => t.includes(filterString))) {
filteredItemsByKeywords.push(item);
}
});
filteredItems.push(...filteredItemsByKeywords);
if (filteredItems.length > 0) { if (filteredItems.length > 0) {
(this.comboBox as any).filteredItems = filteredItems; (this.comboBox as any).filteredItems = filteredItems;
} else { } else {
(this.comboBox as any).filteredItems = [filterString]; (this.comboBox as any).filteredItems = [
{ icon: filterString, keywords: [] },
];
} }
} else { } else {
(this.comboBox as any).filteredItems = mdiIconList; (this.comboBox as any).filteredItems = iconItems;
} }
} }

View File

@@ -10,7 +10,7 @@ import {
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { debounce } from "../common/util/debounce"; import { debounce } from "../common/util/debounce";
import { CustomIcon, customIconsets } from "../data/custom_iconsets"; import { CustomIcon, customIcons } from "../data/custom_icons";
import { import {
checkCacheVersion, checkCacheVersion,
Chunks, Chunks,
@@ -52,18 +52,6 @@ const mdiDeprecatedIcons: DeprecatedIcon = {
newName: "cast-variant", newName: "cast-variant",
removeIn: "2021.12", removeIn: "2021.12",
}, },
application: {
newName: "application-outline",
removeIn: "2021.12",
},
"application-cog": {
newName: "application-cog-outline",
removeIn: "2021.12",
},
"application-settings": {
newName: "application-settings-outline",
removeIn: "2021.12",
},
bandcamp: { bandcamp: {
removeIn: "2021.12", removeIn: "2021.12",
}, },
@@ -77,14 +65,6 @@ const mdiDeprecatedIcons: DeprecatedIcon = {
newName: "cross-bolnisi", newName: "cross-bolnisi",
removeIn: "2021.12", removeIn: "2021.12",
}, },
"boom-gate-up": {
newName: "boom-gate-arrow-up",
removeIn: "2021.12",
},
"boom-gate-up-outline": {
newName: "boom-gate-arrow-up-outline",
removeIn: "2021.12",
},
"boom-gate-down": { "boom-gate-down": {
newName: "boom-gate-arrow-down", newName: "boom-gate-arrow-down",
removeIn: "2021.12", removeIn: "2021.12",
@@ -376,7 +356,7 @@ export class HaIcon extends LitElement {
@state() private _path?: string; @state() private _path?: string;
@state() private _viewBox?; @state() private _viewBox?: string;
@state() private _legacy = false; @state() private _legacy = false;
@@ -406,6 +386,7 @@ export class HaIcon extends LitElement {
if (!this.icon) { if (!this.icon) {
return; return;
} }
const requestedIcon = this.icon;
const [iconPrefix, origIconName] = this.icon.split(":", 2); const [iconPrefix, origIconName] = this.icon.split(":", 2);
let iconName = origIconName; let iconName = origIconName;
@@ -415,10 +396,10 @@ export class HaIcon extends LitElement {
} }
if (!MDI_PREFIXES.includes(iconPrefix)) { if (!MDI_PREFIXES.includes(iconPrefix)) {
if (iconPrefix in customIconsets) { if (iconPrefix in customIcons) {
const customIconset = customIconsets[iconPrefix]; const customIcon = customIcons[iconPrefix];
if (customIconset) { if (customIcon && typeof customIcon.getIcon === "function") {
this._setCustomPath(customIconset(iconName)); this._setCustomPath(customIcon.getIcon(iconName), requestedIcon);
} }
return; return;
} }
@@ -461,14 +442,16 @@ export class HaIcon extends LitElement {
} }
if (databaseIcon) { if (databaseIcon) {
this._path = databaseIcon; if (this.icon === requestedIcon) {
this._path = databaseIcon;
}
cachedIcons[iconName] = databaseIcon; cachedIcons[iconName] = databaseIcon;
return; return;
} }
const chunk = findIconChunk(iconName); const chunk = findIconChunk(iconName);
if (chunk in chunks) { if (chunk in chunks) {
this._setPath(chunks[chunk], iconName); this._setPath(chunks[chunk], iconName, requestedIcon);
return; return;
} }
@@ -476,19 +459,31 @@ export class HaIcon extends LitElement {
response.json() response.json()
); );
chunks[chunk] = iconPromise; chunks[chunk] = iconPromise;
this._setPath(iconPromise, iconName); this._setPath(iconPromise, iconName, requestedIcon);
debouncedWriteCache(); debouncedWriteCache();
} }
private async _setCustomPath(promise: Promise<CustomIcon>) { private async _setCustomPath(
promise: Promise<CustomIcon>,
requestedIcon: string
) {
const icon = await promise; const icon = await promise;
if (this.icon !== requestedIcon) {
return;
}
this._path = icon.path; this._path = icon.path;
this._viewBox = icon.viewBox; this._viewBox = icon.viewBox;
} }
private async _setPath(promise: Promise<Icons>, iconName: string) { private async _setPath(
promise: Promise<Icons>,
iconName: string,
requestedIcon: string
) {
const iconPack = await promise; const iconPack = await promise;
this._path = iconPack[iconName]; if (this.icon === requestedIcon) {
this._path = iconPack[iconName];
}
cachedIcons[iconName] = iconPack[iconName]; cachedIcons[iconName] = iconPack[iconName];
} }

View File

@@ -145,9 +145,15 @@ class HaWebRtcPlayer extends LitElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host,
video { video {
display: block; display: block;
} }
video {
width: 100%;
max-height: var(--video-max-height, calc(100vh - 97px));
}
`; `;
} }
} }

View File

@@ -3,7 +3,7 @@ import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { Person } from "../../data/person"; import { Person } from "../../data/person";
import { computeInitials } from "./ha-user-badge"; import { computeUserInitials } from "../../data/user";
@customElement("ha-person-badge") @customElement("ha-person-badge")
class PersonBadge extends LitElement { class PersonBadge extends LitElement {
@@ -22,7 +22,7 @@ class PersonBadge extends LitElement {
class="picture" class="picture"
></div>`; ></div>`;
} }
const initials = computeInitials(this.person.name); const initials = computeUserInitials(this.person.name);
return html`<div return html`<div
class="initials ${classMap({ long: initials!.length > 2 })}" class="initials ${classMap({ long: initials!.length > 2 })}"
> >

View File

@@ -10,25 +10,9 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { User } from "../../data/user"; import { computeUserInitials, User } from "../../data/user";
import { CurrentUser, HomeAssistant } from "../../types"; import { CurrentUser, HomeAssistant } from "../../types";
export const computeInitials = (name: string) => {
if (!name) {
return "?";
}
return (
name
.trim()
// Split by space and take first 3 words
.split(" ")
.slice(0, 3)
// Of each word, take first letter
.map((s) => s.substr(0, 1))
.join("")
);
};
@customElement("ha-user-badge") @customElement("ha-user-badge")
class UserBadge extends LitElement { class UserBadge extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -75,7 +59,7 @@ class UserBadge extends LitElement {
class="picture" class="picture"
></div>`; ></div>`;
} }
const initials = computeInitials(this.user.name); const initials = computeUserInitials(this.user.name);
return html`<div return html`<div
class="initials ${classMap({ long: initials!.length > 2 })}" class="initials ${classMap({ long: initials!.length > 2 })}"
> >

View File

@@ -194,10 +194,10 @@ export interface NumericStateCondition extends BaseCondition {
export interface SunCondition extends BaseCondition { export interface SunCondition extends BaseCondition {
condition: "sun"; condition: "sun";
after_offset: number; after_offset?: number;
before_offset: number; before_offset?: number;
after: "sunrise" | "sunset"; after?: "sunrise" | "sunset";
before: "sunrise" | "sunset"; before?: "sunrise" | "sunset";
} }
export interface ZoneCondition extends BaseCondition { export interface ZoneCondition extends BaseCondition {

View File

@@ -62,6 +62,8 @@ export type CloudStatus = CloudStatusNotLoggedIn | CloudStatusLoggedIn;
export interface SubscriptionInfo { export interface SubscriptionInfo {
human_description: string; human_description: string;
provider: string;
plan_renewal_date?: number;
} }
export interface CloudWebhook { export interface CloudWebhook {
@@ -76,6 +78,39 @@ export interface ThingTalkConversion {
placeholders: PlaceholderContainer; placeholders: PlaceholderContainer;
} }
export const cloudLogin = (
hass: HomeAssistant,
email: string,
password: string
) =>
hass.callApi("POST", "cloud/login", {
email,
password,
});
export const cloudLogout = (hass: HomeAssistant) =>
hass.callApi("POST", "cloud/logout");
export const cloudForgotPassword = (hass: HomeAssistant, email: string) =>
hass.callApi("POST", "cloud/forgot_password", {
email,
});
export const cloudRegister = (
hass: HomeAssistant,
email: string,
password: string
) =>
hass.callApi("POST", "cloud/register", {
email,
password,
});
export const cloudResendVerification = (hass: HomeAssistant, email: string) =>
hass.callApi("POST", "cloud/resend_confirm", {
email,
});
export const fetchCloudStatus = (hass: HomeAssistant) => export const fetchCloudStatus = (hass: HomeAssistant) =>
hass.callWS<CloudStatus>({ type: "cloud/status" }); hass.callWS<CloudStatus>({ type: "cloud/status" });

40
src/data/custom_icons.ts Normal file
View File

@@ -0,0 +1,40 @@
import { customIconsets } from "./custom_iconsets";
export interface CustomIcon {
path: string;
viewBox?: string;
}
export interface CustomIconListItem {
name: string;
keywords?: string[];
}
export interface CustomIconHelpers {
getIcon: (name: string) => Promise<CustomIcon>;
getIconList?: () => Promise<CustomIconListItem[]>;
}
export interface CustomIconsWindow {
customIcons?: {
[key: string]: CustomIconHelpers;
};
}
const customIconsWindow = window as CustomIconsWindow;
if (!("customIcons" in customIconsWindow)) {
customIconsWindow.customIcons = {};
}
// Proxy for backward compatibility with icon sets
export const customIcons = new Proxy(customIconsWindow.customIcons!, {
get: (obj, prop: string) =>
obj[prop] ??
(customIconsets[prop]
? {
getIcon: customIconsets[prop],
}
: undefined),
has: (obj, prop: string) => prop in obj || prop in customIconsets,
});

View File

@@ -1,9 +1,6 @@
export interface CustomIcon { import { CustomIcon } from "./custom_icons";
path: string;
viewBox?: string;
}
export interface CustomIconsetsWindow { interface CustomIconsetsWindow {
customIconsets?: { [key: string]: (name: string) => Promise<CustomIcon> }; customIconsets?: { [key: string]: (name: string) => Promise<CustomIcon> };
} }

View File

@@ -74,6 +74,8 @@ export interface StatisticValue {
export interface StatisticsMetaData { export interface StatisticsMetaData {
unit_of_measurement: string; unit_of_measurement: string;
statistic_id: string; statistic_id: string;
source: string;
name?: string | null;
} }
export type StatisticsValidationResult = export type StatisticsValidationResult =

View File

@@ -2,6 +2,7 @@ import { HassEntity } from "home-assistant-js-websocket";
import { BINARY_STATE_OFF, BINARY_STATE_ON } from "../common/const"; import { BINARY_STATE_OFF, BINARY_STATE_ON } from "../common/const";
import { computeDomain } from "../common/entity/compute_domain"; import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDisplay } from "../common/entity/compute_state_display"; import { computeStateDisplay } from "../common/entity/compute_state_display";
import { LocalizeFunc } from "../common/translations/localize";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { UNAVAILABLE_STATES } from "./entity"; import { UNAVAILABLE_STATES } from "./entity";
@@ -35,9 +36,11 @@ export const getLogbookDataForContext = async (
hass: HomeAssistant, hass: HomeAssistant,
startDate: string, startDate: string,
contextId?: string contextId?: string
): Promise<LogbookEntry[]> => ): Promise<LogbookEntry[]> => {
addLogbookMessage( const localize = await hass.loadBackendTranslation("device_class");
return addLogbookMessage(
hass, hass,
localize,
await getLogbookDataFromServer( await getLogbookDataFromServer(
hass, hass,
startDate, startDate,
@@ -47,6 +50,7 @@ export const getLogbookDataForContext = async (
contextId contextId
) )
); );
};
export const getLogbookData = async ( export const getLogbookData = async (
hass: HomeAssistant, hass: HomeAssistant,
@@ -54,9 +58,11 @@ export const getLogbookData = async (
endDate: string, endDate: string,
entityId?: string, entityId?: string,
entity_matches_only?: boolean entity_matches_only?: boolean
): Promise<LogbookEntry[]> => ): Promise<LogbookEntry[]> => {
addLogbookMessage( const localize = await hass.loadBackendTranslation("device_class");
return addLogbookMessage(
hass, hass,
localize,
await getLogbookDataCache( await getLogbookDataCache(
hass, hass,
startDate, startDate,
@@ -65,9 +71,11 @@ export const getLogbookData = async (
entity_matches_only entity_matches_only
) )
); );
};
export const addLogbookMessage = ( export const addLogbookMessage = (
hass: HomeAssistant, hass: HomeAssistant,
localize: LocalizeFunc,
logbookData: LogbookEntry[] logbookData: LogbookEntry[]
): LogbookEntry[] => { ): LogbookEntry[] => {
for (const entry of logbookData) { for (const entry of logbookData) {
@@ -75,6 +83,7 @@ export const addLogbookMessage = (
if (entry.state && stateObj) { if (entry.state && stateObj) {
entry.message = getLogbookMessage( entry.message = getLogbookMessage(
hass, hass,
localize,
entry.state, entry.state,
stateObj, stateObj,
computeDomain(entry.entity_id!) computeDomain(entry.entity_id!)
@@ -157,6 +166,7 @@ export const clearLogbookCache = (startDate: string, endDate: string) => {
export const getLogbookMessage = ( export const getLogbookMessage = (
hass: HomeAssistant, hass: HomeAssistant,
localize: LocalizeFunc,
state: string, state: string,
stateObj: HassEntity, stateObj: HassEntity,
domain: string domain: string
@@ -165,21 +175,17 @@ export const getLogbookMessage = (
case "device_tracker": case "device_tracker":
case "person": case "person":
if (state === "not_home") { if (state === "not_home") {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_away`); return localize(`${LOGBOOK_LOCALIZE_PATH}.was_away`);
} }
if (state === "home") { if (state === "home") {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_at_home`); return localize(`${LOGBOOK_LOCALIZE_PATH}.was_at_home`);
} }
return hass.localize( return localize(`${LOGBOOK_LOCALIZE_PATH}.was_at_state`, "state", state);
`${LOGBOOK_LOCALIZE_PATH}.was_at_state`,
"state",
state
);
case "sun": case "sun":
return state === "above_horizon" return state === "above_horizon"
? hass.localize(`${LOGBOOK_LOCALIZE_PATH}.rose`) ? localize(`${LOGBOOK_LOCALIZE_PATH}.rose`)
: hass.localize(`${LOGBOOK_LOCALIZE_PATH}.set`); : localize(`${LOGBOOK_LOCALIZE_PATH}.set`);
case "binary_sensor": { case "binary_sensor": {
const isOn = state === BINARY_STATE_ON; const isOn = state === BINARY_STATE_ON;
@@ -189,19 +195,19 @@ export const getLogbookMessage = (
switch (device_class) { switch (device_class) {
case "battery": case "battery":
if (isOn) { if (isOn) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_low`); return localize(`${LOGBOOK_LOCALIZE_PATH}.was_low`);
} }
if (isOff) { if (isOff) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_normal`); return localize(`${LOGBOOK_LOCALIZE_PATH}.was_normal`);
} }
break; break;
case "connectivity": case "connectivity":
if (isOn) { if (isOn) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_connected`); return localize(`${LOGBOOK_LOCALIZE_PATH}.was_connected`);
} }
if (isOff) { if (isOff) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_disconnected`); return localize(`${LOGBOOK_LOCALIZE_PATH}.was_disconnected`);
} }
break; break;
@@ -210,46 +216,46 @@ export const getLogbookMessage = (
case "opening": case "opening":
case "window": case "window":
if (isOn) { if (isOn) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_opened`); return localize(`${LOGBOOK_LOCALIZE_PATH}.was_opened`);
} }
if (isOff) { if (isOff) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_closed`); return localize(`${LOGBOOK_LOCALIZE_PATH}.was_closed`);
} }
break; break;
case "lock": case "lock":
if (isOn) { if (isOn) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_unlocked`); return localize(`${LOGBOOK_LOCALIZE_PATH}.was_unlocked`);
} }
if (isOff) { if (isOff) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_locked`); return localize(`${LOGBOOK_LOCALIZE_PATH}.was_locked`);
} }
break; break;
case "plug": case "plug":
if (isOn) { if (isOn) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_plugged_in`); return localize(`${LOGBOOK_LOCALIZE_PATH}.was_plugged_in`);
} }
if (isOff) { if (isOff) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_unplugged`); return localize(`${LOGBOOK_LOCALIZE_PATH}.was_unplugged`);
} }
break; break;
case "presence": case "presence":
if (isOn) { if (isOn) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_at_home`); return localize(`${LOGBOOK_LOCALIZE_PATH}.was_at_home`);
} }
if (isOff) { if (isOff) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_away`); return localize(`${LOGBOOK_LOCALIZE_PATH}.was_away`);
} }
break; break;
case "safety": case "safety":
if (isOn) { if (isOn) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_unsafe`); return localize(`${LOGBOOK_LOCALIZE_PATH}.was_unsafe`);
} }
if (isOff) { if (isOff) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_safe`); return localize(`${LOGBOOK_LOCALIZE_PATH}.was_safe`);
} }
break; break;
@@ -265,27 +271,27 @@ export const getLogbookMessage = (
case "sound": case "sound":
case "vibration": case "vibration":
if (isOn) { if (isOn) {
return hass.localize( return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_device_class`, {
`${LOGBOOK_LOCALIZE_PATH}.detected_device_class`, device_class: localize(
"device_class", `component.binary_sensor.device_class.${device_class}`
device_class ),
); });
} }
if (isOff) { if (isOff) {
return hass.localize( return localize(`${LOGBOOK_LOCALIZE_PATH}.cleared_device_class`, {
`${LOGBOOK_LOCALIZE_PATH}.cleared_device_class`, device_class: localize(
"device_class", `component.binary_sensor.device_class.${device_class}`
device_class ),
); });
} }
break; break;
case "tamper": case "tamper":
if (isOn) { if (isOn) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.detected_tampering`); return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_tampering`);
} }
if (isOff) { if (isOff) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.cleared_tampering`); return localize(`${LOGBOOK_LOCALIZE_PATH}.cleared_tampering`);
} }
break; break;
} }
@@ -296,43 +302,43 @@ export const getLogbookMessage = (
case "cover": case "cover":
switch (state) { switch (state) {
case "open": case "open":
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_opened`); return localize(`${LOGBOOK_LOCALIZE_PATH}.was_opened`);
case "opening": case "opening":
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.is_opening`); return localize(`${LOGBOOK_LOCALIZE_PATH}.is_opening`);
case "closing": case "closing":
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.is_closing`); return localize(`${LOGBOOK_LOCALIZE_PATH}.is_closing`);
case "closed": case "closed":
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_closed`); return localize(`${LOGBOOK_LOCALIZE_PATH}.was_closed`);
} }
break; break;
case "lock": case "lock":
if (state === "unlocked") { if (state === "unlocked") {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_unlocked`); return localize(`${LOGBOOK_LOCALIZE_PATH}.was_unlocked`);
} }
if (state === "locked") { if (state === "locked") {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_locked`); return localize(`${LOGBOOK_LOCALIZE_PATH}.was_locked`);
} }
break; break;
} }
if (state === BINARY_STATE_ON) { if (state === BINARY_STATE_ON) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.turned_on`); return localize(`${LOGBOOK_LOCALIZE_PATH}.turned_on`);
} }
if (state === BINARY_STATE_OFF) { if (state === BINARY_STATE_OFF) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.turned_off`); return localize(`${LOGBOOK_LOCALIZE_PATH}.turned_off`);
} }
if (UNAVAILABLE_STATES.includes(state)) { if (UNAVAILABLE_STATES.includes(state)) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.became_unavailable`); return localize(`${LOGBOOK_LOCALIZE_PATH}.became_unavailable`);
} }
return hass.localize( return hass.localize(
`${LOGBOOK_LOCALIZE_PATH}.changed_to_state`, `${LOGBOOK_LOCALIZE_PATH}.changed_to_state`,
"state", "state",
stateObj stateObj
? computeStateDisplay(hass.localize, stateObj, hass.locale, state) ? computeStateDisplay(localize, stateObj, hass.locale, state)
: state : state
); );
}; };

View File

@@ -21,7 +21,9 @@ export interface ScriptEntity extends HassEntityBase {
}; };
} }
export interface ScriptConfig { export type ScriptConfig = ManualScriptConfig | BlueprintScriptConfig;
export interface ManualScriptConfig {
alias: string; alias: string;
sequence: Action | Action[]; sequence: Action | Action[];
icon?: string; icon?: string;
@@ -29,7 +31,7 @@ export interface ScriptConfig {
max?: number; max?: number;
} }
export interface BlueprintScriptConfig extends ScriptConfig { export interface BlueprintScriptConfig extends ManualScriptConfig {
use_blueprint: { path: string; input?: BlueprintInput }; use_blueprint: { path: string; input?: BlueprintInput };
} }

View File

@@ -37,7 +37,8 @@ export type TranslationCategory =
| "options" | "options"
| "device_automation" | "device_automation"
| "mfa_setup" | "mfa_setup"
| "system_health"; | "system_health"
| "device_class";
export const fetchTranslationPreferences = (hass: HomeAssistant) => export const fetchTranslationPreferences = (hass: HomeAssistant) =>
fetchFrontendUserData(hass.connection, "language"); fetchFrontendUserData(hass.connection, "language");

View File

@@ -57,3 +57,19 @@ export const deleteUser = async (hass: HomeAssistant, userId: string) =>
type: "config/auth/delete", type: "config/auth/delete",
user_id: userId, user_id: userId,
}); });
export const computeUserInitials = (name: string) => {
if (!name) {
return "?";
}
return (
name
.trim()
// Split by space and take first 3 words
.split(" ")
.slice(0, 3)
// Of each word, take first letter
.map((s) => s.substr(0, 1))
.join("")
);
};

View File

@@ -84,6 +84,9 @@ export interface ZWaveJSNodeStatus {
ready: boolean; ready: boolean;
status: number; status: number;
is_secure: boolean | string; is_secure: boolean | string;
is_routing: boolean | null;
zwave_plus_version: number | null;
highest_security_class: SecurityClass | null;
} }
export interface ZwaveJSNodeMetadata { export interface ZwaveJSNodeMetadata {

View File

@@ -2,7 +2,7 @@ import { css } from "lit";
export const configFlowContentStyles = css` export const configFlowContentStyles = css`
h2 { h2 {
margin: 24px 0 0; margin: 24px 38px 0 0;
padding: 0 24px; padding: 0 24px;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;

View File

@@ -1,78 +0,0 @@
import { LitElement, TemplateResult, html, css } from "lit";
import { property } from "lit/decorators";
import { enableWrite } from "../common/auth/token_storage";
import { HomeAssistant } from "../types";
import "../components/ha-card";
import type { HaCard } from "../components/ha-card";
import "@material/mwc-button/mwc-button";
class HaStoreAuth extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
protected render(): TemplateResult {
return html`
<ha-card>
<div class="card-content">
${this.hass.localize("ui.auth_store.ask")}
</div>
<div class="card-actions">
<mwc-button @click=${this._dismiss}>
${this.hass.localize("ui.auth_store.decline")}
</mwc-button>
<mwc-button raised @click=${this._save}>
${this.hass.localize("ui.auth_store.confirm")}
</mwc-button>
</div>
</ha-card>
`;
}
firstUpdated() {
this.classList.toggle("small", window.innerWidth < 600);
}
private _save(): void {
enableWrite();
this._dismiss();
}
private _dismiss(): void {
const card = this.shadowRoot!.querySelector("ha-card") as HaCard;
card.style.bottom = `-${card.offsetHeight + 8}px`;
setTimeout(() => this.parentNode!.removeChild(this), 300);
}
static get styles() {
return css`
ha-card {
position: fixed;
padding: 8px 0;
bottom: 16px;
right: 16px;
transition: bottom 0.25s;
--ha-card-box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2),
0px 6px 10px 0px rgba(0, 0, 0, 0.14),
0px 1px 18px 0px rgba(0, 0, 0, 0.12);
}
.card-actions {
text-align: right;
border-top: 0;
}
:host(.small) ha-card {
bottom: 0;
left: 0;
right: 0;
}
`;
}
}
customElements.define("ha-store-auth-card", HaStoreAuth);
declare global {
interface HTMLElementTagNameMap {
"ha-store-auth-card": HaStoreAuth;
}
}

View File

@@ -10,7 +10,8 @@ import { property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-camera-stream"; import "../../../components/ha-camera-stream";
import { HaCheckbox } from "../../../components/ha-checkbox"; import type { HaCheckbox } from "../../../components/ha-checkbox";
import "../../../components/ha-checkbox";
import { import {
CameraEntity, CameraEntity,
CameraPreferences, CameraPreferences,
@@ -20,6 +21,7 @@ import {
updateCameraPrefs, updateCameraPrefs,
} from "../../../data/camera"; } from "../../../data/camera";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import "../../../components/ha-formfield";
class MoreInfoCamera extends LitElement { class MoreInfoCamera extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@@ -54,12 +56,11 @@ class MoreInfoCamera extends LitElement {
></ha-camera-stream> ></ha-camera-stream>
${this._cameraPrefs ${this._cameraPrefs
? html` ? html`
<ha-formfield> <ha-formfield label="Preload stream">
<ha-checkbox <ha-checkbox
.checked=${this._cameraPrefs.preload_stream} .checked=${this._cameraPrefs.preload_stream}
@change=${this._handleCheckboxChanged} @change=${this._handleCheckboxChanged}
> >
Preload stream
</ha-checkbox> </ha-checkbox>
</ha-formfield> </ha-formfield>
` `
@@ -123,12 +124,12 @@ class MoreInfoCamera extends LitElement {
display: block; display: block;
position: relative; position: relative;
} }
ha-checkbox { ha-formfield {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
background-color: var(--secondary-background-color); background-color: var(--secondary-background-color);
padding: 5px; padding-right: 16px;
border-bottom-left-radius: 4px; border-bottom-left-radius: 4px;
} }
`; `;

View File

@@ -1,12 +1,19 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event";
import { throttle } from "../../common/util/throttle"; import { throttle } from "../../common/util/throttle";
import "../../components/chart/state-history-charts"; import "../../components/chart/state-history-charts";
import { getRecentWithCache } from "../../data/cached-history"; import { getRecentWithCache } from "../../data/cached-history";
import { HistoryResult } from "../../data/history"; import { HistoryResult } from "../../data/history";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
declare global {
interface HASSDomEvents {
closed: undefined;
}
}
@customElement("ha-more-info-history") @customElement("ha-more-info-history")
export class MoreInfoHistory extends LitElement { export class MoreInfoHistory extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -24,14 +31,26 @@ export class MoreInfoHistory extends LitElement {
return html``; return html``;
} }
const href = "/history?entity_id=" + this.entityId;
return html`${isComponentLoaded(this.hass, "history") return html`${isComponentLoaded(this.hass, "history")
? html`<state-history-charts ? html` <div class="header">
up-to-now <div class="title">
.hass=${this.hass} ${this.hass.localize("ui.dialogs.more_info_control.history")}
.historyData=${this._stateHistory} </div>
.isLoadingData=${!this._stateHistory} <a href=${href} @click=${this._close}
></state-history-charts>` >${this.hass.localize(
: ""} `; "ui.dialogs.more_info_control.show_more"
)}</a
>
</div>
<state-history-charts
up-to-now
.hass=${this.hass}
.historyData=${this._stateHistory}
.isLoadingData=${!this._stateHistory}
></state-history-charts>`
: ""}`;
} }
protected updated(changedProps: PropertyValues): void { protected updated(changedProps: PropertyValues): void {
@@ -78,6 +97,38 @@ export class MoreInfoHistory extends LitElement {
this.hass!.language this.hass!.language
); );
} }
private _close(): void {
setTimeout(() => fireEvent(this, "closed"), 500);
}
static get styles() {
return [
css`
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.header > a,
a:visited {
color: var(--primary-color);
}
.title {
font-family: var(--paper-font-title_-_font-family);
-webkit-font-smoothing: var(
--paper-font-title_-_-webkit-font-smoothing
);
font-size: var(--paper-font-subhead_-_font-size);
font-weight: var(--paper-font-title_-_font-weight);
letter-spacing: var(--paper-font-title_-_letter-spacing);
line-height: var(--paper-font-title_-_line-height);
}
`,
];
}
} }
declare global { declare global {

View File

@@ -1,16 +1,16 @@
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event";
import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { throttle } from "../../common/util/throttle"; import { throttle } from "../../common/util/throttle";
import "../../components/ha-circular-progress"; import "../../components/ha-circular-progress";
import { fetchUsers } from "../../data/user";
import { getLogbookData, LogbookEntry } from "../../data/logbook"; import { getLogbookData, LogbookEntry } from "../../data/logbook";
import { loadTraceContexts, TraceContexts } from "../../data/trace"; import { loadTraceContexts, TraceContexts } from "../../data/trace";
import { fetchUsers } from "../../data/user";
import "../../panels/logbook/ha-logbook"; import "../../panels/logbook/ha-logbook";
import { haStyle } from "../../resources/styles"; import { haStyle } from "../../resources/styles";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { closeDialog } from "../make-dialog-manager";
@customElement("ha-more-info-logbook") @customElement("ha-more-info-logbook")
export class MoreInfoLogbook extends LitElement { export class MoreInfoLogbook extends LitElement {
@@ -44,6 +44,8 @@ export class MoreInfoLogbook extends LitElement {
return html``; return html``;
} }
const href = "/logbook?entity_id=" + this.entityId;
return html` return html`
${isComponentLoaded(this.hass, "logbook") ${isComponentLoaded(this.hass, "logbook")
? this._error ? this._error
@@ -61,6 +63,16 @@ export class MoreInfoLogbook extends LitElement {
` `
: this._logbookEntries.length : this._logbookEntries.length
? html` ? html`
<div class="header">
<div class="title">
${this.hass.localize("ui.dialogs.more_info_control.logbook")}
</div>
<a href=${href} @click=${this._close}
>${this.hass.localize(
"ui.dialogs.more_info_control.show_more"
)}</a
>
</div>
<ha-logbook <ha-logbook
narrow narrow
no-icon no-icon
@@ -81,11 +93,6 @@ export class MoreInfoLogbook extends LitElement {
protected firstUpdated(): void { protected firstUpdated(): void {
this._fetchUserPromise = this._fetchUserNames(); this._fetchUserPromise = this._fetchUserNames();
this.addEventListener("click", (ev) => {
if ((ev.composedPath()[0] as HTMLElement).tagName === "A") {
setTimeout(() => closeDialog("ha-more-info-dialog"), 500);
}
});
} }
protected updated(changedProps: PropertyValues): void { protected updated(changedProps: PropertyValues): void {
@@ -182,6 +189,10 @@ export class MoreInfoLogbook extends LitElement {
this._userIdToName = userIdToName; this._userIdToName = userIdToName;
} }
private _close(): void {
setTimeout(() => fireEvent(this, "closed"), 500);
}
static get styles() { static get styles() {
return [ return [
haStyle, haStyle,
@@ -203,6 +214,27 @@ export class MoreInfoLogbook extends LitElement {
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.header > a,
a:visited {
color: var(--primary-color);
}
.title {
font-family: var(--paper-font-title_-_font-family);
-webkit-font-smoothing: var(
--paper-font-title_-_-webkit-font-smoothing
);
font-size: var(--paper-font-subhead_-_font-size);
font-weight: var(--paper-font-title_-_font-weight);
letter-spacing: var(--paper-font-title_-_letter-spacing);
line-height: var(--paper-font-title_-_line-height);
}
`, `,
]; ];
} }

View File

@@ -60,10 +60,12 @@ const connProm = async (auth) => {
searchParams.delete("auth_callback"); searchParams.delete("auth_callback");
searchParams.delete("code"); searchParams.delete("code");
searchParams.delete("state"); searchParams.delete("state");
searchParams.delete("storeToken");
const search = searchParams.toString();
history.replaceState( history.replaceState(
null, null,
"", "",
`${location.pathname}?${searchParams.toString()}` `${location.pathname}${search ? `?${search}` : ""}`
); );
} }

View File

@@ -2,13 +2,16 @@ import "@material/mwc-button/mwc-button";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state, query } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize"; import type { LocalizeFunc } from "../common/translations/localize";
import { createCurrencyListEl } from "../components/currency-datalist"; import { createCurrencyListEl } from "../components/currency-datalist";
import "../components/map/ha-locations-editor"; import "../components/map/ha-locations-editor";
import type { MarkerLocation } from "../components/map/ha-locations-editor"; import type {
HaLocationsEditor,
MarkerLocation,
} from "../components/map/ha-locations-editor";
import { createTimezoneListEl } from "../components/timezone-datalist"; import { createTimezoneListEl } from "../components/timezone-datalist";
import { import {
ConfigUpdateValues, ConfigUpdateValues,
@@ -46,6 +49,8 @@ class OnboardingCoreConfig extends LitElement {
@state() private _timeZone?: string; @state() private _timeZone?: string;
@query("ha-locations-editor", true) private map!: HaLocationsEditor;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<p> <p>
@@ -305,6 +310,7 @@ class OnboardingCoreConfig extends LitElement {
if (values.latitude && values.longitude) { if (values.latitude && values.longitude) {
this._location = [Number(values.latitude), Number(values.longitude)]; this._location = [Number(values.latitude), Number(values.longitude)];
this.map.autoFit = true;
} }
if (values.elevation) { if (values.elevation) {
this._elevation = String(values.elevation); this._elevation = String(values.elevation);

View File

@@ -371,6 +371,10 @@ export class HAFullCalendar extends LitElement {
); );
--fc-theme-standard-border-color: var(--divider-color); --fc-theme-standard-border-color: var(--divider-color);
--fc-border-color: var(--divider-color); --fc-border-color: var(--divider-color);
--fc-page-bg-color: var(
--ha-card-background,
var(--card-background-color, white)
);
} }
a { a {

View File

@@ -230,6 +230,7 @@ export default class HaAutomationActionRow extends LitElement {
"ui.panel.config.automation.editor.actions.type_select" "ui.panel.config.automation.editor.actions.type_select"
)} )}
.value=${getType(this.action)} .value=${getType(this.action)}
naturalMenuWidth
@selected=${this._typeChanged} @selected=${this._typeChanged}
> >
${this._processedTypes(this.hass.localize).map( ${this._processedTypes(this.hass.localize).map(

View File

@@ -90,6 +90,7 @@ export default class HaAutomationConditionEditor extends LitElement {
"ui.panel.config.automation.editor.conditions.type_select" "ui.panel.config.automation.editor.conditions.type_select"
)} )}
.value=${this.condition.condition} .value=${this.condition.condition}
naturalMenuWidth
@selected=${this._typeChanged} @selected=${this._typeChanged}
> >
${this._processedTypes(this.hass.localize).map( ${this._processedTypes(this.hass.localize).map(

View File

@@ -1,10 +1,11 @@
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import { LogicalCondition } from "../../../../../data/automation"; import { Condition, LogicalCondition } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import "../ha-automation-condition"; import "../ha-automation-condition";
import { ConditionElement } from "../ha-automation-condition-row"; import { ConditionElement } from "../ha-automation-condition-row";
import { HaStateCondition } from "./ha-automation-condition-state";
@customElement("ha-automation-condition-logical") @customElement("ha-automation-condition-logical")
export class HaLogicalCondition extends LitElement implements ConditionElement { export class HaLogicalCondition extends LitElement implements ConditionElement {
@@ -13,7 +14,14 @@ export class HaLogicalCondition extends LitElement implements ConditionElement {
@property() public condition!: LogicalCondition; @property() public condition!: LogicalCondition;
public static get defaultConfig() { public static get defaultConfig() {
return { conditions: [{ condition: "state" }] }; return {
conditions: [
{
condition: "state",
...HaStateCondition.defaultConfig,
},
] as Condition[],
};
} }
protected render() { protected render() {

View File

@@ -464,7 +464,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
) { ) {
return; return;
} }
// Wait for dialog to complate closing // Wait for dialog to complete closing
await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0));
} }
showAutomationEditor({ showAutomationEditor({

View File

@@ -4,12 +4,12 @@ import {
mdiInformationOutline, mdiInformationOutline,
mdiPencil, mdiPencil,
mdiPencilOff, mdiPencilOff,
mdiPlayCircleOutline,
mdiPlus, mdiPlus,
} from "@mdi/js"; } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { formatDateTime } from "../../../common/datetime/format_date_time"; import { formatDateTime } from "../../../common/datetime/format_date_time";
@@ -22,6 +22,7 @@ import "../../../components/ha-button-related-filter-menu";
import "../../../components/ha-fab"; import "../../../components/ha-fab";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import "../../../components/ha-icon-overflow-menu";
import { import {
AutomationEntity, AutomationEntity,
triggerAutomationActions, triggerAutomationActions,
@@ -135,7 +136,7 @@ class HaAutomationPicker extends LitElement {
template: (_info, automation: any) => html` template: (_info, automation: any) => html`
<mwc-button <mwc-button
.automation=${automation} .automation=${automation}
@click=${this._runActions} @click=${this._triggerRunActions}
.disabled=${UNAVAILABLE_STATES.includes(automation.state)} .disabled=${UNAVAILABLE_STATES.includes(automation.state)}
> >
${this.hass.localize("ui.card.automation.trigger")} ${this.hass.localize("ui.card.automation.trigger")}
@@ -143,78 +144,73 @@ class HaAutomationPicker extends LitElement {
`, `,
}; };
} }
columns.info = { columns.actions = {
title: "", title: "",
type: "icon-button", type: "overflow-menu",
template: (_info, automation) => html`
<ha-icon-button
.automation=${automation}
@click=${this._showInfo}
.label=${this.hass.localize(
"ui.panel.config.automation.picker.show_info_automation"
)}
.path=${mdiInformationOutline}
></ha-icon-button>
`,
};
columns.trace = {
title: "",
type: "icon-button",
template: (_info, automation: any) => html` template: (_info, automation: any) => html`
<a <ha-icon-overflow-menu
href=${ifDefined( .hass=${this.hass}
automation.attributes.id .narrow=${this.narrow}
? `/config/automation/trace/${automation.attributes.id}` .items=${[
: undefined // Info Button
)} {
path: mdiInformationOutline,
label: this.hass.localize(
"ui.panel.config.automation.picker.show_info_automation"
),
action: () => this._showInfo(automation),
},
// Trigger Button
{
path: mdiPlayCircleOutline,
label: this.hass.localize("ui.card.automation.trigger"),
narrowOnly: true,
action: () => this._runActions(automation),
},
// Trace Button
{
path: mdiHistory,
disabled: !automation.attributes.id,
label: this.hass.localize(
"ui.panel.config.automation.picker.dev_automation"
),
tooltip: !automation.attributes.id
? this.hass.localize(
"ui.panel.config.automation.picker.dev_only_editable"
)
: "",
action: () => {
if (automation.attributes.id) {
navigate(
`/config/automation/trace/${automation.attributes.id}`
);
}
},
},
// Edit Button
{
path: automation.attributes.id ? mdiPencil : mdiPencilOff,
disabled: !automation.attributes.id,
label: this.hass.localize(
"ui.panel.config.automation.picker.edit_automation"
),
tooltip: !automation.attributes.id
? this.hass.localize(
"ui.panel.config.automation.picker.dev_only_editable"
)
: "",
action: () => {
if (automation.attributes.id) {
navigate(
`/config/automation/edit/${automation.attributes.id}`
);
}
},
},
]}
style="color: var(--secondary-text-color)"
> >
<ha-icon-button </ha-icon-overflow-menu>
.label=${this.hass.localize(
"ui.panel.config.automation.picker.dev_automation"
)}
.path=${mdiHistory}
.disabled=${!automation.attributes.id}
></ha-icon-button>
</a>
${!automation.attributes.id
? html`
<paper-tooltip animation-delay="0" position="left">
${this.hass.localize(
"ui.panel.config.automation.picker.dev_only_editable"
)}
</paper-tooltip>
`
: ""}
`,
};
columns.edit = {
title: "",
type: "icon-button",
template: (_info, automation: any) => html`
<a
href=${ifDefined(
automation.attributes.id
? `/config/automation/edit/${automation.attributes.id}`
: undefined
)}
>
<ha-icon-button
.disabled=${!automation.attributes.id}
.label=${this.hass.localize(
"ui.panel.config.automation.picker.edit_automation"
)}
.path=${automation.attributes.id ? mdiPencil : mdiPencilOff}
></ha-icon-button>
</a>
${!automation.attributes.id
? html`
<paper-tooltip animation-delay="0" position="left">
${this.hass.localize(
"ui.panel.config.automation.picker.only_editable"
)}
</paper-tooltip>
`
: ""}
`, `,
}; };
return columns; return columns;
@@ -285,9 +281,8 @@ class HaAutomationPicker extends LitElement {
this._filterValue = undefined; this._filterValue = undefined;
} }
private _showInfo(ev) { private _showInfo(automation: AutomationEntity) {
ev.stopPropagation(); const entityId = automation.entity_id;
const entityId = ev.currentTarget.automation.entity_id;
fireEvent(this, "hass-more-info", { entityId }); fireEvent(this, "hass-more-info", { entityId });
} }
@@ -311,9 +306,12 @@ class HaAutomationPicker extends LitElement {
}); });
} }
private _runActions = (ev) => { private _triggerRunActions = (ev) => {
const entityId = ev.currentTarget.automation.entity_id; this._runActions(ev.currentTarget.automation);
triggerAutomationActions(this.hass, entityId); };
private _runActions = (automation: AutomationEntity) => {
triggerAutomationActions(this.hass, automation.entity_id);
}; };
private _createNew() { private _createNew() {

View File

@@ -179,6 +179,7 @@ export default class HaAutomationTriggerRow extends LitElement {
"ui.panel.config.automation.editor.triggers.type_select" "ui.panel.config.automation.editor.triggers.type_select"
)} )}
.value=${this.trigger.platform} .value=${this.trigger.platform}
naturalMenuWidth
@selected=${this._typeChanged} @selected=${this._typeChanged}
> >
${this._processedTypes(this.hass.localize).map( ${this._processedTypes(this.hass.localize).map(

View File

@@ -10,7 +10,7 @@ import { handleChangeEvent } from "../ha-automation-trigger-row";
const includeDomains = ["zone"]; const includeDomains = ["zone"];
@customElement("ha-automation-trigger-geo_location") @customElement("ha-automation-trigger-geo_location")
export default class HaGeolocationTrigger extends LitElement { export class HaGeolocationTrigger extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public trigger!: GeoLocationTrigger; @property({ attribute: false }) public trigger!: GeoLocationTrigger;
@@ -19,7 +19,7 @@ export default class HaGeolocationTrigger extends LitElement {
return { return {
source: "", source: "",
zone: "", zone: "",
event: "enter", event: "enter" as GeoLocationTrigger["event"],
}; };
} }

View File

@@ -8,14 +8,14 @@ import "../../../../../components/ha-formfield";
import "../../../../../components/ha-radio"; import "../../../../../components/ha-radio";
@customElement("ha-automation-trigger-homeassistant") @customElement("ha-automation-trigger-homeassistant")
export default class HaHassTrigger extends LitElement { export class HaHassTrigger extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public trigger!: HassTrigger; @property({ attribute: false }) public trigger!: HassTrigger;
public static get defaultConfig() { public static get defaultConfig() {
return { return {
event: "start", event: "start" as HassTrigger["event"],
}; };
} }

View File

@@ -12,7 +12,7 @@ import { handleChangeEvent } from "../ha-automation-trigger-row";
import "../../../../../components/ha-duration-input"; import "../../../../../components/ha-duration-input";
@customElement("ha-automation-trigger-numeric_state") @customElement("ha-automation-trigger-numeric_state")
export default class HaNumericStateTrigger extends LitElement { export class HaNumericStateTrigger extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public trigger!: NumericStateTrigger; @property() public trigger!: NumericStateTrigger;

View File

@@ -20,7 +20,8 @@ export class HaSunTrigger extends LitElement implements TriggerElement {
public static get defaultConfig() { public static get defaultConfig() {
return { return {
event: "sunrise", event: "sunrise" as SunTrigger["event"],
offset: 0,
}; };
} }

View File

@@ -27,7 +27,7 @@ export class HaZoneTrigger extends LitElement {
return { return {
entity_id: "", entity_id: "",
zone: "", zone: "",
event: "enter", event: "enter" as ZoneTrigger["event"],
}; };
} }

View File

@@ -29,6 +29,7 @@ import {
Blueprints, Blueprints,
deleteBlueprint, deleteBlueprint,
} from "../../../data/blueprint"; } from "../../../data/blueprint";
import { showScriptEditor } from "../../../data/script";
import { import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
@@ -52,6 +53,12 @@ const createNewFunctions = {
use_blueprint: { path: blueprintMeta.path }, use_blueprint: { path: blueprintMeta.path },
}); });
}, },
script: (blueprintMeta: BlueprintMetaDataPath) => {
showScriptEditor({
alias: blueprintMeta.name,
use_blueprint: { path: blueprintMeta.path },
});
},
}; };
@customElement("ha-blueprint-overview") @customElement("ha-blueprint-overview")
@@ -62,27 +69,38 @@ class HaBlueprintOverview extends LitElement {
@property({ type: Boolean }) public narrow!: boolean; @property({ type: Boolean }) public narrow!: boolean;
@property() public route!: Route; @property({ attribute: false }) public route!: Route;
@property() public blueprints!: Blueprints; @property({ attribute: false }) public blueprints!: Record<
string,
Blueprints
>;
private _processedBlueprints = memoizeOne((blueprints: Blueprints) => { private _processedBlueprints = memoizeOne(
const result = Object.entries(blueprints).map(([path, blueprint]) => { (blueprints: Record<string, Blueprints>) => {
if ("error" in blueprint) { const result: any[] = [];
return { Object.entries(blueprints).forEach(([type, typeBlueprints]) =>
name: blueprint.error, Object.entries(typeBlueprints).forEach(([path, blueprint]) => {
error: true, if ("error" in blueprint) {
path, result.push({
}; name: blueprint.error,
} type,
return { error: true,
...blueprint.metadata, path,
error: false, });
path, } else {
}; result.push({
}); ...blueprint.metadata,
return result; type,
}); error: false,
path,
});
}
})
);
return result;
}
);
private _columns = memoizeOne( private _columns = memoizeOne(
(narrow, _language): DataTableColumnContainer => ({ (narrow, _language): DataTableColumnContainer => ({
@@ -102,6 +120,20 @@ class HaBlueprintOverview extends LitElement {
` `
: undefined, : undefined,
}, },
type: {
title: this.hass.localize(
"ui.panel.config.blueprint.overview.headers.type"
),
template: (type: string) =>
html`${this.hass.localize(
`ui.panel.config.blueprint.overview.types.${type}`
)}`,
sortable: true,
filterable: true,
hidden: narrow,
direction: "asc",
width: "10%",
},
path: { path: {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.blueprint.overview.headers.file_name" "ui.panel.config.blueprint.overview.headers.file_name"
@@ -114,25 +146,27 @@ class HaBlueprintOverview extends LitElement {
}, },
create: { create: {
title: "", title: "",
width: narrow ? undefined : "20%",
type: narrow ? "icon-button" : undefined, type: narrow ? "icon-button" : undefined,
template: (_, blueprint: any) => template: (_, blueprint: any) =>
blueprint.error blueprint.error
? "" ? ""
: narrow : narrow
? html` <ha-icon-button ? html`<ha-icon-button
.blueprint=${blueprint} .blueprint=${blueprint}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.blueprint.overview.use_blueprint" `ui.panel.config.blueprint.overview.create_${blueprint.domain}`
)} )}
.path=${mdiRobot}
@click=${this._createNew} @click=${this._createNew}
></ha-icon-button>` .path=${mdiRobot}
>
</ha-icon-button>`
: html`<mwc-button : html`<mwc-button
.blueprint=${blueprint} .blueprint=${blueprint}
@click=${this._createNew} @click=${this._createNew}
> >
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.blueprint.overview.use_blueprint" `ui.panel.config.blueprint.overview.create_${blueprint.domain}`
)} )}
</mwc-button>`, </mwc-button>`,
}, },

View File

@@ -25,7 +25,7 @@ class HaConfigBlueprint extends HassRouterPage {
@property() public showAdvanced!: boolean; @property() public showAdvanced!: boolean;
@property() public blueprints: Blueprints = {}; @property() public blueprints: Record<string, Blueprints> = {};
protected routerOptions: RouterOptions = { protected routerOptions: RouterOptions = {
defaultPage: "dashboard", defaultPage: "dashboard",
@@ -41,7 +41,11 @@ class HaConfigBlueprint extends HassRouterPage {
}; };
private async _getBlueprints() { private async _getBlueprints() {
this.blueprints = await fetchBlueprints(this.hass, "automation"); const [automation, script] = await Promise.all([
fetchBlueprints(this.hass, "automation"),
fetchBlueprints(this.hass, "script"),
]);
this.blueprints = { automation, script };
} }
protected firstUpdated(changedProps) { protected firstUpdated(changedProps) {

View File

@@ -1,246 +0,0 @@
import "@material/mwc-button";
import "@polymer/paper-item/paper-item-body";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { formatDateTime } from "../../../../common/datetime/format_date_time";
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
import "../../../../components/buttons/ha-call-api-button";
import "../../../../components/ha-card";
import { fetchCloudSubscriptionInfo } from "../../../../data/cloud";
import "../../../../layouts/hass-subpage";
import { EventsMixin } from "../../../../mixins/events-mixin";
import LocalizeMixin from "../../../../mixins/localize-mixin";
import "../../../../styles/polymer-ha-style";
import "../../ha-config-section";
import "./cloud-alexa-pref";
import "./cloud-google-pref";
import "./cloud-remote-pref";
import "./cloud-tts-pref";
import "./cloud-webhooks";
/*
* @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin
*/
class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
static get template() {
return html`
<style include="iron-flex ha-style">
[slot="introduction"] {
margin: -1em 0;
}
[slot="introduction"] a {
color: var(--primary-color);
}
.content {
padding-bottom: 24px;
}
.account-row {
display: flex;
padding: 0 16px;
}
mwc-button {
align-self: center;
}
.soon {
font-style: italic;
margin-top: 24px;
text-align: center;
}
.nowrap {
white-space: nowrap;
}
.wrap {
white-space: normal;
}
.status {
text-transform: capitalize;
padding: 16px;
}
a {
color: var(--primary-color);
}
</style>
<hass-subpage
hass="[[hass]]"
narrow="[[narrow]]"
header="Home Assistant Cloud"
>
<div class="content">
<ha-config-section is-wide="[[isWide]]">
<span slot="header">Home Assistant Cloud</span>
<div slot="introduction">
<p>
[[localize('ui.panel.config.cloud.account.thank_you_note')]]
</p>
</div>
<ha-card
header="[[localize('ui.panel.config.cloud.account.nabu_casa_account')]]"
>
<div class="account-row">
<paper-item-body two-line="">
[[cloudStatus.email]]
<div secondary class="wrap">
[[_formatSubscription(_subscription)]]
</div>
</paper-item-body>
</div>
<div class="account-row">
<paper-item-body
>[[localize('ui.panel.config.cloud.account.connection_status')]]</paper-item-body
>
<div class="status">
[[_computeConnectionStatus(cloudStatus.cloud)]]
</div>
</div>
<div class="card-actions">
<a
href="https://account.nabucasa.com"
target="_blank"
rel="noreferrer"
>
<mwc-button
>[[localize('ui.panel.config.cloud.account.manage_account')]]</mwc-button
>
</a>
<mwc-button style="float: right" on-click="handleLogout"
>[[localize('ui.panel.config.cloud.account.sign_out')]]</mwc-button
>
</div>
</ha-card>
</ha-config-section>
<ha-config-section is-wide="[[isWide]]">
<span slot="header"
>[[localize('ui.panel.config.cloud.account.integrations')]]</span
>
<div slot="introduction">
<p>
[[localize('ui.panel.config.cloud.account.integrations_introduction')]]
</p>
<p>
[[localize('ui.panel.config.cloud.account.integrations_introduction2')]]
<a
href="https://www.nabucasa.com"
target="_blank"
rel="noreferrer"
>
[[localize('ui.panel.config.cloud.account.integrations_link_all_features')]]</a
>.
</p>
</div>
<cloud-remote-pref
hass="[[hass]]"
cloud-status="[[cloudStatus]]"
dir="[[_rtlDirection]]"
></cloud-remote-pref>
<cloud-tts-pref
hass="[[hass]]"
cloud-status="[[cloudStatus]]"
dir="[[_rtlDirection]]"
></cloud-tts-pref>
<cloud-alexa-pref
hass="[[hass]]"
cloud-status="[[cloudStatus]]"
dir="[[_rtlDirection]]"
></cloud-alexa-pref>
<cloud-google-pref
hass="[[hass]]"
cloud-status="[[cloudStatus]]"
dir="[[_rtlDirection]]"
></cloud-google-pref>
<cloud-webhooks
hass="[[hass]]"
cloud-status="[[cloudStatus]]"
dir="[[_rtlDirection]]"
></cloud-webhooks>
</ha-config-section>
</div>
</hass-subpage>
`;
}
static get properties() {
return {
hass: Object,
isWide: Boolean,
narrow: Boolean,
cloudStatus: Object,
_subscription: {
type: Object,
value: null,
},
_rtlDirection: {
type: Boolean,
computed: "_computeRTLDirection(hass)",
},
};
}
ready() {
super.ready();
this._fetchSubscriptionInfo();
}
_computeConnectionStatus(status) {
return status === "connected"
? this.hass.localize("ui.panel.config.cloud.account.connected")
: status === "disconnected"
? this.hass.localize("ui.panel.config.cloud.account.not_connected")
: this.hass.localize("ui.panel.config.cloud.account.connecting");
}
async _fetchSubscriptionInfo() {
this._subscription = await fetchCloudSubscriptionInfo(this.hass);
if (
this._subscription.provider &&
this.cloudStatus &&
this.cloudStatus.cloud !== "connected"
) {
this.fire("ha-refresh-cloud-status");
}
}
handleLogout() {
this.hass
.callApi("post", "cloud/logout")
.then(() => this.fire("ha-refresh-cloud-status"));
}
_formatSubscription(subInfo) {
if (subInfo === null) {
return this.hass.localize(
"ui.panel.config.cloud.account.fetching_subscription"
);
}
let description = subInfo.human_description;
if (subInfo.plan_renewal_date) {
description = description.replace(
"{periodEnd}",
formatDateTime(
new Date(subInfo.plan_renewal_date * 1000),
this.hass.locale
)
);
}
return description;
}
_computeRTLDirection(hass) {
return computeRTLDirection(hass);
}
}
customElements.define("cloud-account", CloudAccount);

View File

@@ -0,0 +1,268 @@
import "@material/mwc-button";
import "@polymer/paper-item/paper-item-body";
import { LitElement, css, html, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { formatDateTime } from "../../../../common/datetime/format_date_time";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
import "../../../../components/buttons/ha-call-api-button";
import "../../../../components/ha-card";
import {
cloudLogout,
CloudStatusLoggedIn,
fetchCloudSubscriptionInfo,
SubscriptionInfo,
} from "../../../../data/cloud";
import "../../../../layouts/hass-subpage";
import { HomeAssistant } from "../../../../types";
import "../../ha-config-section";
import "./cloud-alexa-pref";
import "./cloud-google-pref";
import "./cloud-remote-pref";
import "./cloud-tts-pref";
import "./cloud-webhooks";
@customElement("cloud-account")
export class CloudAccount extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public cloudStatus!: CloudStatusLoggedIn;
@state() private _subscription?: SubscriptionInfo;
@state() private _rtlDirection: "rtl" | "ltr" = "rtl";
protected render() {
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
header="Home Assistant Cloud"
>
<div class="content">
<ha-config-section .isWide=${this.isWide}>
<span slot="header">Home Assistant Cloud</span>
<div slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.cloud.account.thank_you_note"
)}
</p>
</div>
<ha-card
.header=${this.hass.localize(
"ui.panel.config.cloud.account.nabu_casa_account"
)}
>
<div class="account-row">
<paper-item-body two-line>
${this.cloudStatus.email}
<div secondary class="wrap">
${this._subscription
? this._subscription.human_description.replace(
"{periodEnd}",
this._subscription.plan_renewal_date
? formatDateTime(
new Date(
this._subscription.plan_renewal_date * 1000
),
this.hass.locale
)
: ""
)
: this.hass.localize(
"ui.panel.config.cloud.account.fetching_subscription"
)}
</div>
</paper-item-body>
</div>
<div class="account-row">
<paper-item-body>
${this.hass.localize(
"ui.panel.config.cloud.account.connection_status"
)}
</paper-item-body>
<div class="status">
${this.cloudStatus.cloud === "connected"
? this.hass.localize(
"ui.panel.config.cloud.account.connected"
)
: this.cloudStatus.cloud === "disconnected"
? this.hass.localize(
"ui.panel.config.cloud.account.not_connected"
)
: this.hass.localize(
"ui.panel.config.cloud.account.connecting"
)}
</div>
</div>
<div class="card-actions">
<a
href="https://account.nabucasa.com"
target="_blank"
rel="noreferrer"
>
<mwc-button>
${this.hass.localize(
"ui.panel.config.cloud.account.manage_account"
)}
</mwc-button>
</a>
<mwc-button @click=${this._handleLogout}
>${this.hass.localize(
"ui.panel.config.cloud.account.sign_out"
)}</mwc-button
>
</div>
</ha-card>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header"
>${this.hass.localize(
"ui.panel.config.cloud.account.integrations"
)}</span
>
<div slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.cloud.account.integrations_introduction"
)}
</p>
<p>
${this.hass.localize(
"ui.panel.config.cloud.account.integrations_introduction2"
)}
<a
href="https://www.nabucasa.com"
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.cloud.account.integrations_link_all_features"
)}</a
>.
</p>
</div>
<cloud-remote-pref
.hass=${this.hass}
.cloudStatus=${this.cloudStatus}
dir=${this._rtlDirection}
></cloud-remote-pref>
<cloud-tts-pref
.hass=${this.hass}
.cloudStatus=${this.cloudStatus}
dir=${this._rtlDirection}
></cloud-tts-pref>
<cloud-alexa-pref
.hass=${this.hass}
.cloudStatus=${this.cloudStatus}
dir=${this._rtlDirection}
></cloud-alexa-pref>
<cloud-google-pref
.hass=${this.hass}
.cloudStatus=${this.cloudStatus}
dir=${this._rtlDirection}
></cloud-google-pref>
<cloud-webhooks
.hass=${this.hass}
.cloudStatus=${this.cloudStatus}
dir=${this._rtlDirection}
></cloud-webhooks>
</ha-config-section>
</div>
</hass-subpage>
`;
}
firstUpdated() {
this._fetchSubscriptionInfo();
}
protected updated(changedProps: PropertyValues) {
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.locale !== this.hass.locale) {
this._rtlDirection = computeRTLDirection(this.hass);
}
}
}
private async _fetchSubscriptionInfo() {
this._subscription = await fetchCloudSubscriptionInfo(this.hass);
if (
this._subscription.provider &&
this.cloudStatus &&
this.cloudStatus.cloud !== "connected"
) {
fireEvent(this, "ha-refresh-cloud-status");
}
}
private async _handleLogout() {
await cloudLogout(this.hass);
fireEvent(this, "ha-refresh-cloud-status");
}
_computeRTLDirection(hass) {
return computeRTLDirection(hass);
}
static get styles() {
return css`
[slot="introduction"] {
margin: -1em 0;
}
[slot="introduction"] a {
color: var(--primary-color);
}
.content {
padding-bottom: 24px;
}
.account-row {
display: flex;
padding: 0 16px;
}
.card-actions {
display: flex;
justify-content: space-between;
}
.card-actions a {
text-decoration: none;
}
mwc-button {
align-self: center;
}
.wrap {
white-space: normal;
}
.status {
text-transform: capitalize;
padding: 16px;
}
a {
color: var(--primary-color);
}
`;
}
}
customElements.define("cloud-account", CloudAccount);
declare global {
interface HTMLElementTagNameMap {
"cloud-account": CloudAccount;
}
}

View File

@@ -1,152 +0,0 @@
import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../../components/buttons/ha-progress-button";
import "../../../../components/ha-card";
import "../../../../layouts/hass-subpage";
import { EventsMixin } from "../../../../mixins/events-mixin";
import LocalizeMixin from "../../../../mixins/localize-mixin";
import "../../../../styles/polymer-ha-style";
/*
* @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin
*/
class CloudForgotPassword extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style include="iron-flex ha-style">
.content {
padding-bottom: 24px;
}
ha-card {
max-width: 600px;
margin: 0 auto;
margin-top: 24px;
}
h1 {
@apply --paper-font-headline;
margin: 0;
}
.error {
color: var(--error-color);
}
.card-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-actions a {
color: var(--primary-text-color);
}
[hidden] {
display: none;
}
</style>
<hass-subpage
hass="[[hass]]"
narrow="[[narrow]]"
header="[[localize('ui.panel.config.cloud.forgot_password.title')]]"
>
<div class="content">
<ha-card
header="[[localize('ui.panel.config.cloud.forgot_password.subtitle')]]"
>
<div class="card-content">
<p>
[[localize('ui.panel.config.cloud.forgot_password.instructions')]]
</p>
<div class="error" hidden$="[[!_error]]">[[_error]]</div>
<paper-input
autofocus=""
id="email"
label="[[localize('ui.panel.config.cloud.forgot_password.email')]]"
value="{{email}}"
type="email"
on-keydown="_keyDown"
error-message="[[localize('ui.panel.config.cloud.forgot_password.email_error_msg')]]"
></paper-input>
</div>
<div class="card-actions">
<ha-progress-button
on-click="_handleEmailPasswordReset"
progress="[[_requestInProgress]]"
>[[localize('ui.panel.config.cloud.forgot_password.send_reset_email')]]</ha-progress-button
>
</div>
</ha-card>
</div>
</hass-subpage>
`;
}
static get properties() {
return {
hass: Object,
narrow: Boolean,
email: {
type: String,
notify: true,
observer: "_emailChanged",
},
_requestInProgress: {
type: Boolean,
value: false,
},
_error: {
type: String,
value: "",
},
};
}
_emailChanged() {
this._error = "";
this.$.email.invalid = false;
}
_keyDown(ev) {
// validate on enter
if (ev.keyCode === 13) {
this._handleEmailPasswordReset();
ev.preventDefault();
}
}
_handleEmailPasswordReset() {
if (!this.email || !this.email.includes("@")) {
this.$.email.invalid = true;
}
if (this.$.email.invalid) return;
this._requestInProgress = true;
this.hass
.callApi("post", "cloud/forgot_password", {
email: this.email,
})
.then(
() => {
this._requestInProgress = false;
this.fire("cloud-done", {
flashMessage: this.hass.localize(
"ui.panel.config.cloud.forgot_password.check_your_email"
),
});
},
(err) =>
this.setProperties({
_requestInProgress: false,
_error:
err && err.body && err.body.message
? err.body.message
: "Unknown error",
})
);
}
}
customElements.define("cloud-forgot-password", CloudForgotPassword);

View File

@@ -0,0 +1,156 @@
import "@material/mwc-textfield/mwc-textfield";
import type { TextField } from "@material/mwc-textfield/mwc-textfield";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/buttons/ha-progress-button";
import "../../../../components/ha-alert";
import "../../../../components/ha-card";
import { cloudForgotPassword } from "../../../../data/cloud";
import "../../../../layouts/hass-subpage";
import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
@customElement("cloud-forgot-password")
export class CloudForgotPassword extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@property() public email?: string;
@state() public _requestInProgress = false;
@state() private _error?: string;
@query("#email", true) private _emailField!: TextField;
protected render(): TemplateResult {
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize(
"ui.panel.config.cloud.forgot_password.title"
)}
>
<div class="content">
<ha-card
.header=${this.hass.localize(
"ui.panel.config.cloud.forgot_password.subtitle"
)}
>
<div class="card-content">
<p>
${this.hass.localize(
"ui.panel.config.cloud.forgot_password.instructions"
)}
</p>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<mwc-textfield
autofocus
id="email"
label=${this.hass.localize(
"ui.panel.config.cloud.forgot_password.email"
)}
.value=${this.email}
type="email"
required
@keydown=${this._keyDown}
.validationMessage=${this.hass.localize(
"ui.panel.config.cloud.forgot_password.email_error_msg"
)}
></mwc-textfield>
</div>
<div class="card-actions">
<ha-progress-button
@click=${this._handleEmailPasswordReset}
.progress=${this._requestInProgress}
>
${this.hass.localize(
"ui.panel.config.cloud.forgot_password.send_reset_email"
)}
</ha-progress-button>
</div>
</ha-card>
</div>
</hass-subpage>
`;
}
private _keyDown(ev: KeyboardEvent) {
if (ev.key === "Enter") {
this._handleEmailPasswordReset();
}
}
private async _handleEmailPasswordReset() {
const emailField = this._emailField;
const email = emailField.value;
if (!emailField.reportValidity()) {
emailField.focus();
return;
}
this._requestInProgress = true;
try {
await cloudForgotPassword(this.hass, email);
// @ts-ignore
fireEvent(this, "email-changed", { value: email });
this._requestInProgress = false;
// @ts-ignore
fireEvent(this, "cloud-done", {
flashMessage: this.hass.localize(
"ui.panel.config.cloud.forgot_password.check_your_email"
),
});
} catch (err: any) {
this._requestInProgress = false;
this._error =
err && err.body && err.body.message
? err.body.message
: "Unknown error";
}
}
static get styles() {
return [
haStyle,
css`
.content {
padding-bottom: 24px;
}
ha-card {
max-width: 600px;
margin: 0 auto;
margin-top: 24px;
}
h1 {
margin: 0;
}
mwc-textfield {
width: 100%;
}
.card-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-actions a {
color: var(--primary-text-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"cloud-forgot-password": CloudForgotPassword;
}
}

View File

@@ -1,336 +0,0 @@
import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-ripple/paper-ripple";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { computeRTL } from "../../../../common/util/compute_rtl";
import "../../../../components/buttons/ha-progress-button";
import "../../../../components/ha-card";
import "../../../../components/ha-icon";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-icon-next";
import "../../../../layouts/hass-subpage";
import { EventsMixin } from "../../../../mixins/events-mixin";
import LocalizeMixin from "../../../../mixins/localize-mixin";
import NavigateMixin from "../../../../mixins/navigate-mixin";
import "../../../../styles/polymer-ha-style";
import "../../ha-config-section";
/*
* @appliesMixin NavigateMixin
* @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin
*/
class CloudLogin extends LocalizeMixin(
NavigateMixin(EventsMixin(PolymerElement))
) {
static get template() {
return html`
<style include="iron-flex ha-style">
.content {
padding-bottom: 24px;
}
[slot="introduction"] {
margin: -1em 0;
}
[slot="introduction"] a {
color: var(--primary-color);
}
paper-item {
cursor: pointer;
}
ha-card {
overflow: hidden;
}
ha-card .card-header {
margin-bottom: -8px;
}
h1 {
@apply --paper-font-headline;
margin: 0;
}
.error {
color: var(--error-color);
}
.card-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
[hidden] {
display: none;
}
.flash-msg {
padding-right: 44px;
}
.flash-msg ha-icon-button {
position: absolute;
top: 4px;
right: 8px;
color: var(--secondary-text-color);
}
:host([rtl]) .flash-msg ha-icon-button {
right: auto;
left: 8px;
}
.login-form {
display: flex;
flex-direction: column;
}
.pwd-forgot-link {
color: var(--secondary-text-color) !important;
text-align: right !important;
align-self: flex-end;
}
</style>
<hass-subpage
hass="[[hass]]"
narrow="[[narrow]]"
header="Home Assistant Cloud"
>
<div class="content">
<ha-config-section is-wide="[[isWide]]">
<span slot="header">Home Assistant Cloud</span>
<div slot="introduction">
<p>[[localize('ui.panel.config.cloud.login.introduction')]]</p>
<p>
[[localize('ui.panel.config.cloud.login.introduction2')]]
<a
href="https://www.nabucasa.com"
target="_blank"
rel="noreferrer"
>
Nabu&nbsp;Casa,&nbsp;Inc</a
>[[localize('ui.panel.config.cloud.login.introduction2a')]]
</p>
<p>[[localize('ui.panel.config.cloud.login.introduction3')]]</p>
<p>
<a
href="https://www.nabucasa.com"
target="_blank"
rel="noreferrer"
>
[[localize('ui.panel.config.cloud.login.learn_more_link')]]
</a>
</p>
</div>
<ha-card hidden$="[[!flashMessage]]">
<div class="card-content flash-msg">
[[flashMessage]]
<ha-icon-button
label="[[localize('ui.panel.config.cloud.login.dismiss')]]"
on-click="_dismissFlash"
>
<ha-icon icon="hass:close"></ha-icon>
</ha-icon-button>
<paper-ripple id="flashRipple" noink=""></paper-ripple>
</div>
</ha-card>
<ha-card
header="[[localize('ui.panel.config.cloud.login.sign_in')]]"
>
<div class="card-content login-form">
<div class="error" hidden$="[[!_error]]">[[_error]]</div>
<paper-input
label="[[localize('ui.panel.config.cloud.login.email')]]"
id="email"
type="email"
value="{{email}}"
on-keydown="_keyDown"
error-message="[[localize('ui.panel.config.cloud.login.email_error_msg')]]"
></paper-input>
<paper-input
id="password"
label="[[localize('ui.panel.config.cloud.login.password')]]"
value="{{_password}}"
type="password"
on-keydown="_keyDown"
error-message="[[localize('ui.panel.config.cloud.login.password_error_msg')]]"
></paper-input>
<button
class="link pwd-forgot-link"
hidden="[[_requestInProgress]]"
on-click="_handleForgotPassword"
>
[[localize('ui.panel.config.cloud.login.forgot_password')]]
</button>
</div>
<div class="card-actions">
<ha-progress-button
on-click="_handleLogin"
progress="[[_requestInProgress]]"
>[[localize('ui.panel.config.cloud.login.sign_in')]]</ha-progress-button
>
</div>
</ha-card>
<ha-card>
<paper-item on-click="_handleRegister">
<paper-item-body two-line="">
[[localize('ui.panel.config.cloud.login.start_trial')]]
<div secondary="">
[[localize('ui.panel.config.cloud.login.trial_info')]]
</div>
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</ha-card>
</ha-config-section>
</div>
</hass-subpage>
`;
}
static get properties() {
return {
hass: Object,
isWide: Boolean,
narrow: Boolean,
email: {
type: String,
notify: true,
},
_password: {
type: String,
value: "",
},
_requestInProgress: {
type: Boolean,
value: false,
},
flashMessage: {
type: String,
notify: true,
},
rtl: {
type: Boolean,
reflectToAttribute: true,
computed: "_computeRTL(hass)",
},
_error: String,
};
}
static get observers() {
return ["_inputChanged(email, _password)"];
}
connectedCallback() {
super.connectedCallback();
if (this.flashMessage) {
// Wait for DOM to be drawn
requestAnimationFrame(() =>
requestAnimationFrame(() => this.$.flashRipple.simulatedRipple())
);
}
}
_inputChanged() {
this.$.email.invalid = false;
this.$.password.invalid = false;
this._error = false;
}
_keyDown(ev) {
// validate on enter
if (ev.keyCode === 13) {
this._handleLogin();
ev.preventDefault();
}
}
_handleLogin() {
let invalid = false;
if (!this.email || !this.email.includes("@")) {
this.$.email.invalid = true;
this.$.email.focus();
invalid = true;
}
if (this._password.length < 8) {
this.$.password.invalid = true;
if (!invalid) {
invalid = true;
this.$.password.focus();
}
}
if (invalid) return;
this._requestInProgress = true;
this.hass
.callApi("post", "cloud/login", {
email: this.email,
password: this._password,
})
.then(
() => {
this.fire("ha-refresh-cloud-status");
this.setProperties({
email: "",
_password: "",
});
},
(err) => {
// Do this before setProperties because changing it clears errors.
this._password = "";
const errCode = err && err.body && err.body.code;
if (errCode === "PasswordChangeRequired") {
alert(
"[[localize('ui.panel.config.cloud.login.alert_password_change_required')]]"
);
this.navigate("/config/cloud/forgot-password");
return;
}
const props = {
_requestInProgress: false,
_error:
err && err.body && err.body.message
? err.body.message
: "Unknown error",
};
if (errCode === "UserNotConfirmed") {
props._error =
"[[localize('ui.panel.config.cloud.login.alert_email_confirm_necessary')]]";
}
this.setProperties(props);
this.$.email.focus();
}
);
}
_handleRegister() {
this.flashMessage = "";
this.navigate("/config/cloud/register");
}
_handleForgotPassword() {
this.flashMessage = "";
this.navigate("/config/cloud/forgot-password");
}
_dismissFlash() {
// give some time to let the ripple finish.
setTimeout(() => {
this.flashMessage = "";
}, 200);
}
_computeRTL(hass) {
return computeRTL(hass);
}
}
customElements.define("cloud-login", CloudLogin);

View File

@@ -0,0 +1,310 @@
import "@material/mwc-button";
import "@material/mwc-textfield/mwc-textfield";
import type { TextField } from "@material/mwc-textfield/mwc-textfield";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { navigate } from "../../../../common/navigate";
import "../../../../components/buttons/ha-progress-button";
import "../../../../components/ha-alert";
import "../../../../components/ha-card";
import "../../../../components/ha-icon-next";
import { cloudLogin } from "../../../../data/cloud";
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
import "../../../../layouts/hass-subpage";
import { haStyle } from "../../../../resources/styles";
import "../../../../styles/polymer-ha-style";
import { HomeAssistant } from "../../../../types";
import "../../ha-config-section";
@customElement("cloud-login")
export class CloudLogin extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide = false;
@property({ type: Boolean }) public narrow = false;
@property() public email?: string;
@property() public flashMessage?: string;
@state() private _password?: string;
@state() private _requestInProgress = false;
@state() private _error?: string;
@query("#email", true) private _emailField!: TextField;
@query("#password", true) private _passwordField!: TextField;
protected render(): TemplateResult {
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
header="Home Assistant Cloud"
>
<div class="content">
<ha-config-section isWide=${this.isWide}>
<span slot="header">Home Assistant Cloud</span>
<div slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.cloud.login.introduction"
)}
</p>
<p>
${this.hass.localize(
"ui.panel.config.cloud.login.introduction2"
)}
<a
href="https://www.nabucasa.com"
target="_blank"
rel="noreferrer"
>
Nabu&nbsp;Casa,&nbsp;Inc</a
>${this.hass.localize(
"ui.panel.config.cloud.login.introduction2a"
)}
</p>
<p>
${this.hass.localize(
"ui.panel.config.cloud.login.introduction3"
)}
</p>
<p>
<a
href="https://www.nabucasa.com"
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.cloud.login.learn_more_link"
)}
</a>
</p>
</div>
${this.flashMessage
? html`<ha-alert
dismissable
@alert-dismissed-clicked=${this._dismissFlash}
>
${this.flashMessage}
</ha-alert>`
: ""}
<ha-card
.header=${this.hass.localize(
"ui.panel.config.cloud.login.sign_in"
)}
>
<div class="card-content login-form">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<mwc-textfield
.label=${this.hass.localize(
"ui.panel.config.cloud.login.email"
)}
id="email"
type="email"
required
.value=${this.email}
@keydown=${this._keyDown}
.disabled=${this._requestInProgress}
.validationMessage=${this.hass.localize(
"ui.panel.config.cloud.login.email_error_msg"
)}
></mwc-textfield>
<mwc-textfield
id="password"
.label=${this.hass.localize(
"ui.panel.config.cloud.login.password"
)}
.value=${this._password || ""}
type="password"
required
minlength="8"
@keydown=${this._keyDown}
.disabled=${this._requestInProgress}
.validationMessage=${this.hass.localize(
"ui.panel.config.cloud.login.password_error_msg"
)}
></mwc-textfield>
<button
class="link pwd-forgot-link"
.disabled=${this._requestInProgress}
@click=${this._handleForgotPassword}
>
${this.hass.localize(
"ui.panel.config.cloud.login.forgot_password"
)}
</button>
</div>
<div class="card-actions">
<ha-progress-button
@click=${this._handleLogin}
.progress=${this._requestInProgress}
>${this.hass.localize(
"ui.panel.config.cloud.login.sign_in"
)}</ha-progress-button
>
</div>
</ha-card>
<ha-card>
<paper-item @click=${this._handleRegister}>
<paper-item-body two-line>
${this.hass.localize(
"ui.panel.config.cloud.login.start_trial"
)}
<div secondary>
${this.hass.localize(
"ui.panel.config.cloud.login.trial_info"
)}
</div>
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</ha-card>
</ha-config-section>
</div>
</hass-subpage>
`;
}
private _keyDown(ev: KeyboardEvent) {
if (ev.key === "Enter") {
this._handleLogin();
}
}
private async _handleLogin() {
const emailField = this._emailField;
const passwordField = this._passwordField;
const email = emailField.value;
const password = passwordField.value;
if (!emailField.reportValidity()) {
passwordField.reportValidity();
emailField.focus();
return;
}
if (!passwordField.reportValidity()) {
passwordField.focus();
return;
}
this._requestInProgress = true;
try {
await cloudLogin(this.hass, email, password);
fireEvent(this, "ha-refresh-cloud-status");
this.email = "";
this._password = "";
} catch (err: any) {
const errCode = err && err.body && err.body.code;
if (errCode === "PasswordChangeRequired") {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.cloud.login.alert_password_change_required"
),
});
navigate("/config/cloud/forgot-password");
return;
}
this._password = "";
this._requestInProgress = false;
if (errCode === "UserNotConfirmed") {
this._error = this.hass.localize(
"ui.panel.config.cloud.login.alert_email_confirm_necessary"
);
} else {
this._error =
err && err.body && err.body.message
? err.body.message
: "Unknown error";
}
emailField.focus();
}
}
private _handleRegister() {
this._dismissFlash();
// @ts-ignore
fireEvent(this, "email-changed", { value: this._emailField.value });
navigate("/config/cloud/register");
}
private _handleForgotPassword() {
this._dismissFlash();
// @ts-ignore
fireEvent(this, "email-changed", { value: this._emailField.value });
navigate("/config/cloud/forgot-password");
}
private _dismissFlash() {
// @ts-ignore
fireEvent(this, "flash-message-changed", { value: "" });
}
static get styles() {
return [
haStyle,
css`
.content {
padding-bottom: 24px;
}
[slot="introduction"] {
margin: -1em 0;
}
[slot="introduction"] a {
color: var(--primary-color);
}
paper-item {
cursor: pointer;
}
ha-card {
overflow: hidden;
}
ha-card .card-header {
margin-bottom: -8px;
}
h1 {
margin: 0;
}
.card-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.login-form {
display: flex;
flex-direction: column;
}
.pwd-forgot-link {
color: var(--secondary-text-color) !important;
text-align: right !important;
align-self: flex-end;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"cloud-login": CloudLogin;
}
}

View File

@@ -1,229 +0,0 @@
import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../../components/buttons/ha-progress-button";
import "../../../../components/ha-card";
import "../../../../layouts/hass-subpage";
import { EventsMixin } from "../../../../mixins/events-mixin";
import LocalizeMixin from "../../../../mixins/localize-mixin";
import "../../../../styles/polymer-ha-style";
import { documentationUrl } from "../../../../util/documentation-url";
import "../../ha-config-section";
/*
* @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin
*/
class CloudRegister extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style include="iron-flex ha-style">
[slot=introduction] {
margin: -1em 0;
}
[slot=introduction] a {
color: var(--primary-color);
}
a {
color: var(--primary-color);
}
paper-item {
cursor: pointer;
}
h1 {
@apply --paper-font-headline;
margin: 0;
}
.error {
color: var(--error-color);
}
.card-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
[hidden] {
display: none;
}
</style>
<hass-subpage hass="[[hass]]" narrow="[[narrow]]" header="[[localize('ui.panel.config.cloud.register.title')]]">
<div class="content">
<ha-config-section is-wide="[[isWide]]">
<span slot="header">[[localize('ui.panel.config.cloud.register.headline')]]</span>
<div slot="introduction">
<p>
[[localize('ui.panel.config.cloud.register.information')]]
</p>
<p>
[[localize('ui.panel.config.cloud.register.information2')]]
</p>
<ul>
<li>[[localize('ui.panel.config.cloud.register.feature_remote_control')]]</li>
<li>[[localize('ui.panel.config.cloud.register.feature_google_home')]]</li>
<li>[[localize('ui.panel.config.cloud.register.feature_amazon_alexa')]]</li>
<li>[[localize('ui.panel.config.cloud.register.feature_webhook_apps')]]</li>
</ul>
<p>
[[localize('ui.panel.config.cloud.register.information3')]] <a href='https://www.nabucasa.com' target='_blank'>Nabu&nbsp;Casa,&nbsp;Inc</a>[[localize('ui.panel.config.cloud.register.information3a')]]
</p>
<p>
[[localize('ui.panel.config.cloud.register.information4')]]
</p><ul>
<li><a href="[[_computeDocumentationUrlTos(hass)]]" target="_blank" rel="noreferrer">[[localize('ui.panel.config.cloud.register.link_terms_conditions')]]</a></li>
<li><a href="[[_computeDocumentationUrlPrivacy(hass)]]" target="_blank" rel="noreferrer">[[localize('ui.panel.config.cloud.register.link_privacy_policy')]]</a></li>
</ul>
</p>
</div>
<ha-card header="[[localize('ui.panel.config.cloud.register.create_account')]]">
<div class="card-content">
<div class="header">
<div class="error" hidden$="[[!_error]]">[[_error]]</div>
</div>
<paper-input autofocus="" id="email" label="[[localize('ui.panel.config.cloud.register.email_address')]]" type="email" value="{{email}}" on-keydown="_keyDown" error-message="[[localize('ui.panel.config.cloud.register.email_error_msg')]]"></paper-input>
<paper-input id="password" label="Password" value="{{_password}}" type="password" on-keydown="_keyDown" error-message="[[localize('ui.panel.config.cloud.register.password_error_msg')]]"></paper-input>
</div>
<div class="card-actions">
<ha-progress-button on-click="_handleRegister" progress="[[_requestInProgress]]">[[localize('ui.panel.config.cloud.register.start_trial')]]</ha-progress-button>
<button class="link" hidden="[[_requestInProgress]]" on-click="_handleResendVerifyEmail">[[localize('ui.panel.config.cloud.register.resend_confirmation_email')]]</button>
</div>
</ha-card>
</ha-config-section>
</div>
</hass-subpage>
`;
}
static get properties() {
return {
hass: Object,
isWide: Boolean,
narrow: Boolean,
email: {
type: String,
notify: true,
},
_requestInProgress: {
type: Boolean,
value: false,
},
_password: {
type: String,
value: "",
},
_error: {
type: String,
value: "",
},
};
}
static get observers() {
return ["_inputChanged(email, _password)"];
}
_inputChanged() {
this._error = "";
this.$.email.invalid = false;
this.$.password.invalid = false;
}
_keyDown(ev) {
// validate on enter
if (ev.keyCode === 13) {
this._handleRegister();
ev.preventDefault();
}
}
_computeDocumentationUrlTos(hass) {
return documentationUrl(hass, "/tos/");
}
_computeDocumentationUrlPrivacy(hass) {
return documentationUrl(hass, "/privacy/");
}
_handleRegister() {
let invalid = false;
if (!this.email || !this.email.includes("@")) {
this.$.email.invalid = true;
this.$.email.focus();
invalid = true;
}
if (this._password.length < 8) {
this.$.password.invalid = true;
if (!invalid) {
invalid = true;
this.$.password.focus();
}
}
if (invalid) return;
this._requestInProgress = true;
this.hass
.callApi("post", "cloud/register", {
email: this.email,
password: this._password,
})
.then(
() => this._verificationEmailSent(),
(err) => {
// Do this before setProperties because changing it clears errors.
this._password = "";
this.setProperties({
_requestInProgress: false,
_error:
err && err.body && err.body.message
? err.body.message
: "Unknown error",
});
}
);
}
_handleResendVerifyEmail() {
if (!this.email) {
this.$.email.invalid = true;
return;
}
this.hass
.callApi("post", "cloud/resend_confirm", {
email: this.email,
})
.then(
() => this._verificationEmailSent(),
(err) =>
this.setProperties({
_error:
err && err.body && err.body.message
? err.body.message
: "Unknown error",
})
);
}
_verificationEmailSent() {
this.setProperties({
_requestInProgress: false,
_password: "",
});
this.fire("cloud-done", {
flashMessage: this.hass.localize(
"ui.panel.config.cloud.register.account_created"
),
});
}
}
customElements.define("cloud-register", CloudRegister);

View File

@@ -0,0 +1,293 @@
import "@material/mwc-textfield/mwc-textfield";
import type { TextField } from "@material/mwc-textfield/mwc-textfield";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/buttons/ha-progress-button";
import "../../../../components/ha-alert";
import "../../../../components/ha-card";
import { cloudRegister, cloudResendVerification } from "../../../../data/cloud";
import "../../../../layouts/hass-subpage";
import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import "../../ha-config-section";
@customElement("cloud-register")
export class CloudRegister extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide = false;
@property({ type: Boolean }) public narrow = false;
@property() public email?: string;
@state() private _requestInProgress = false;
@state() private _password = "";
@state() private _error?: string;
@query("#email", true) private _emailField!: TextField;
@query("#password", true) private _passwordField!: TextField;
protected render(): TemplateResult {
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.cloud.register.title")}
>
<div class="content">
<ha-config-section .isWide=${this.isWide}>
<span slot="header"
>${this.hass.localize(
"ui.panel.config.cloud.register.headline"
)}</span
>
<div slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.cloud.register.information"
)}
</p>
<p>
${this.hass.localize(
"ui.panel.config.cloud.register.information2"
)}
</p>
<ul>
<li>
${this.hass.localize(
"ui.panel.config.cloud.register.feature_remote_control"
)}
</li>
<li>
${this.hass.localize(
"ui.panel.config.cloud.register.feature_google_home"
)}
</li>
<li>
${this.hass.localize(
"ui.panel.config.cloud.register.feature_amazon_alexa"
)}
</li>
<li>
${this.hass.localize(
"ui.panel.config.cloud.register.feature_webhook_apps"
)}
</li>
</ul>
<p>
${this.hass.localize(
"ui.panel.config.cloud.register.information3"
)}
<a href="https://www.nabucasa.com" target="_blank"
>Nabu&nbsp;Casa,&nbsp;Inc</a
>
${this.hass.localize(
"ui.panel.config.cloud.register.information3a"
)}
</p>
<p>
${this.hass.localize(
"ui.panel.config.cloud.register.information4"
)}
</p>
<ul>
<li>
<a
href="https://www.nabucasa.com/tos/"
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.cloud.register.link_terms_conditions"
)}
</a>
</li>
<li>
<a
href="https://www.nabucasa.com/privacy_policy/"
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.cloud.register.link_privacy_policy"
)}
</a>
</li>
</ul>
</div>
<ha-card
.header=${this.hass.localize(
"ui.panel.config.cloud.register.create_account"
)}
><div class="card-content register-form">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<mwc-textfield
autofocus
id="email"
.label=${this.hass.localize(
"ui.panel.config.cloud.register.email_address"
)}
type="email"
required
.value=${this.email}
@keydown=${this._keyDown}
validationMessage=${this.hass.localize(
"ui.panel.config.cloud.register.email_error_msg"
)}
></mwc-textfield>
<mwc-textfield
id="password"
label="Password"
.value=${this._password}
type="password"
minlength="8"
required
@keydown=${this._keyDown}
validationMessage=${this.hass.localize(
"ui.panel.config.cloud.register.password_error_msg"
)}
></mwc-textfield>
</div>
<div class="card-actions">
<ha-progress-button
@click=${this._handleRegister}
.progress=${this._requestInProgress}
>${this.hass.localize(
"ui.panel.config.cloud.register.start_trial"
)}</ha-progress-button
>
<button
class="link"
.disabled=${this._requestInProgress}
@click=${this._handleResendVerifyEmail}
>
${this.hass.localize(
"ui.panel.config.cloud.register.resend_confirm_email"
)}
</button>
</div>
</ha-card>
</ha-config-section>
</div>
</hass-subpage>
`;
}
private _keyDown(ev: KeyboardEvent) {
if (ev.key === "Enter") {
this._handleRegister();
}
}
private async _handleRegister() {
const emailField = this._emailField;
const passwordField = this._passwordField;
const email = emailField.value;
const password = passwordField.value;
if (!emailField.reportValidity()) {
passwordField.reportValidity();
emailField.focus();
return;
}
if (!passwordField.reportValidity()) {
passwordField.focus();
return;
}
this._requestInProgress = true;
try {
await cloudRegister(this.hass, email, password);
this._verificationEmailSent(email);
} catch (err: any) {
this._password = "";
this._requestInProgress = false;
this._error =
err && err.body && err.body.message
? err.body.message
: "Unknown error";
}
}
private async _handleResendVerifyEmail() {
const emailField = this._emailField;
const email = emailField.value;
if (!emailField.reportValidity()) {
emailField.focus();
return;
}
try {
await cloudResendVerification(this.hass, email);
this._verificationEmailSent(email);
} catch (err: any) {
this._error =
err && err.body && err.body.message
? err.body.message
: "Unknown error";
}
}
private _verificationEmailSent(email: string) {
this._requestInProgress = false;
this._password = "";
// @ts-ignore
fireEvent(this, "email-changed", { value: email });
// @ts-ignore
fireEvent(this, "cloud-done", {
flashMessage: this.hass.localize(
"ui.panel.config.cloud.register.account_created"
),
});
}
static get styles() {
return [
haStyle,
css`
[slot="introduction"] {
margin: -1em 0;
}
[slot="introduction"] a {
color: var(--primary-color);
}
a {
color: var(--primary-color);
}
paper-item {
cursor: pointer;
}
h1 {
margin: 0;
}
.register-form {
display: flex;
flex-direction: column;
}
.card-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"cloud-register": CloudRegister;
}
}

View File

@@ -25,7 +25,7 @@ class HaConfigCustomize extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<hass-tabs-subpage <hass-tabs-subpage
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.route=${this.route} .route=${this.route}
back-path="/config" back-path="/config"

View File

@@ -180,10 +180,16 @@ export class HaFormCustomize extends LocalizeMixin(PolymerElement) {
this.newAttributes this.newAttributes
); );
attrs.forEach((attr) => { attrs.forEach((attr) => {
if (attr.closed || attr.secondary || !attr.attribute || !attr.value) if (
attr.closed ||
attr.secondary ||
!attr.attribute ||
attr.value === null ||
attr.value === undefined
)
return; return;
const value = attr.type === "json" ? JSON.parse(attr.value) : attr.value; const value = attr.type === "json" ? JSON.parse(attr.value) : attr.value;
if (!value) return; if (value === null || value === undefined) return;
data[attr.attribute] = value; data[attr.attribute] = value;
}); });

View File

@@ -15,18 +15,23 @@ import { domainIcon } from "../../../../common/entity/domain_icon";
import "../../../../components/entity/state-badge"; import "../../../../components/entity/state-badge";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-icon"; import "../../../../components/ha-icon";
import { HomeAssistant } from "../../../../types"; import type { LovelaceRowConfig } from "../../../lovelace/entity-rows/types";
import { HuiErrorCard } from "../../../lovelace/cards/hui-error-card"; import type { HomeAssistant } from "../../../../types";
import type { HuiErrorCard } from "../../../lovelace/cards/hui-error-card";
import { createRowElement } from "../../../lovelace/create-element/create-row-element"; import { createRowElement } from "../../../lovelace/create-element/create-row-element";
import { addEntitiesToLovelaceView } from "../../../lovelace/editor/add-entities-to-view"; import { addEntitiesToLovelaceView } from "../../../lovelace/editor/add-entities-to-view";
import { LovelaceRow } from "../../../lovelace/entity-rows/types"; import { LovelaceRow } from "../../../lovelace/entity-rows/types";
import { showEntityEditorDialog } from "../../entities/show-dialog-entity-editor"; import { showEntityEditorDialog } from "../../entities/show-dialog-entity-editor";
import { EntityRegistryStateEntry } from "../ha-config-device-page"; import { EntityRegistryStateEntry } from "../ha-config-device-page";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import { stripPrefixFromEntityName } from "../../../../common/entity/strip_prefix_from_entity_name";
@customElement("ha-device-entities-card") @customElement("ha-device-entities-card")
export class HaDeviceEntitiesCard extends LitElement { export class HaDeviceEntitiesCard extends LitElement {
@property() public header!: string; @property() public header!: string;
@property() public deviceName!: string;
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public entities!: EntityRegistryStateEntry[]; @property() public entities!: EntityRegistryStateEntry[];
@@ -100,14 +105,8 @@ export class HaDeviceEntitiesCard extends LitElement {
</div> </div>
` `
: html` : html`
<div class="config-entry-row"> <div class="empty card-content">
<paper-item-body two-line> ${this.hass.localize("ui.panel.config.devices.entities.none")}
<div>
${this.hass.localize(
"ui.panel.config.devices.entities.none"
)}
</div>
</paper-item-body>
</div> </div>
`} `}
</ha-card> </ha-card>
@@ -119,9 +118,21 @@ export class HaDeviceEntitiesCard extends LitElement {
} }
private _renderEntity(entry: EntityRegistryStateEntry): TemplateResult { private _renderEntity(entry: EntityRegistryStateEntry): TemplateResult {
const element = createRowElement({ entity: entry.entity_id }); const config: LovelaceRowConfig = {
entity: entry.entity_id,
};
const element = createRowElement(config);
if (this.hass) { if (this.hass) {
element.hass = this.hass; element.hass = this.hass;
const state = this.hass.states[entry.entity_id];
const name = stripPrefixFromEntityName(
computeStateName(state),
`${this.deviceName} `.toLowerCase()
);
if (name) {
config.name = name;
}
} }
// @ts-ignore // @ts-ignore
element.entry = entry; element.entry = entry;
@@ -131,7 +142,11 @@ export class HaDeviceEntitiesCard extends LitElement {
private _renderEntry(entry: EntityRegistryStateEntry): TemplateResult { private _renderEntry(entry: EntityRegistryStateEntry): TemplateResult {
return html` return html`
<paper-icon-item .entry=${entry} @click=${this._openEditEntry}> <paper-icon-item
class="disabled-entry"
.entry=${entry}
@click=${this._openEditEntry}
>
<ha-svg-icon <ha-svg-icon
slot="item-icon" slot="item-icon"
.path=${domainIcon(computeDomain(entry.entity_id))} .path=${domainIcon(computeDomain(entry.entity_id))}
@@ -166,7 +181,8 @@ export class HaDeviceEntitiesCard extends LitElement {
this.hass, this.hass,
this.entities this.entities
.filter((entity) => !entity.disabled_by) .filter((entity) => !entity.disabled_by)
.map((entity) => entity.entity_id) .map((entity) => entity.entity_id),
this.deviceName
); );
} }
@@ -188,6 +204,9 @@ export class HaDeviceEntitiesCard extends LitElement {
.disabled-entry { .disabled-entry {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
#entities {
margin-top: -24px; /* match the spacing between card title and content of the device info card above it */
}
#entities > * { #entities > * {
margin: 8px 16px 8px 8px; margin: 8px 16px 8px 8px;
} }
@@ -196,12 +215,16 @@ export class HaDeviceEntitiesCard extends LitElement {
} }
paper-icon-item { paper-icon-item {
min-height: 40px; min-height: 40px;
padding: 0 8px; padding: 0 16px;
cursor: pointer; cursor: pointer;
--paper-item-icon-width: 48px;
} }
.name { .name {
font-size: 14px; font-size: 14px;
} }
.empty {
text-align: center;
}
button.show-more { button.show-more {
color: var(--primary-color); color: var(--primary-color);
text-align: left; text-align: left;

View File

@@ -18,6 +18,7 @@ import {
nodeStatus, nodeStatus,
ZWaveJSNodeStatus, ZWaveJSNodeStatus,
ZWaveJSNodeIdentifiers, ZWaveJSNodeIdentifiers,
SecurityClass,
} from "../../../../../../data/zwave_js"; } from "../../../../../../data/zwave_js";
import { haStyle } from "../../../../../../resources/styles"; import { haStyle } from "../../../../../../resources/styles";
import { HomeAssistant } from "../../../../../../types"; import { HomeAssistant } from "../../../../../../types";
@@ -117,13 +118,33 @@ export class HaDeviceInfoZWaveJS extends LitElement {
: this.hass.localize("ui.common.no")} : this.hass.localize("ui.common.no")}
</div> </div>
<div> <div>
${this.hass.localize("ui.panel.config.zwave_js.device_info.is_secure")}: ${this.hass.localize(
${this._node.is_secure === true "ui.panel.config.zwave_js.device_info.highest_security"
? this.hass.localize("ui.common.yes") )}:
${this._node.highest_security_class !== null
? this.hass.localize(
`ui.panel.config.zwave_js.security_classes.${
SecurityClass[this._node.highest_security_class]
}.title`
)
: this._node.is_secure === false : this._node.is_secure === false
? this.hass.localize("ui.common.no") ? this.hass.localize(
"ui.panel.config.zwave_js.security_classes.none.title"
)
: this.hass.localize("ui.panel.config.zwave_js.device_info.unknown")} : this.hass.localize("ui.panel.config.zwave_js.device_info.unknown")}
</div> </div>
<div>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.zwave_plus"
)}:
${this._node.zwave_plus_version
? this.hass.localize(
"ui.panel.config.zwave_js.device_info.zwave_plus_version",
"version",
this._node.zwave_plus_version
)
: this.hass.localize("ui.common.no")}
</div>
`; `;
} }

View File

@@ -52,6 +52,7 @@ import {
loadDeviceRegistryDetailDialog, loadDeviceRegistryDetailDialog,
showDeviceRegistryDetailDialog, showDeviceRegistryDetailDialog,
} from "./device-registry-detail/show-dialog-device-registry-detail"; } from "./device-registry-detail/show-dialog-device-registry-detail";
import { computeDomain } from "../../../common/entity/compute_domain";
export interface EntityRegistryStateEntry extends EntityRegistryEntry { export interface EntityRegistryStateEntry extends EntityRegistryEntry {
stateName?: string | null; stateName?: string | null;
@@ -117,14 +118,19 @@ export class HaConfigDevicePage extends LitElement {
private _entitiesByCategory = memoizeOne( private _entitiesByCategory = memoizeOne(
(entities: EntityRegistryEntry[]) => { (entities: EntityRegistryEntry[]) => {
const result = groupBy( const result = groupBy(entities, (entry) =>
entities, entry.entity_category
(entry) => entry.entity_category || "state" ? entry.entity_category
: ["sensor", "binary_sensor"].includes(computeDomain(entry.entity_id))
? "sensor"
: "control"
) as Record< ) as Record<
"state" | NonNullable<EntityRegistryEntry["entity_category"]>, | "control"
| "sensor"
| NonNullable<EntityRegistryEntry["entity_category"]>,
EntityRegistryStateEntry[] EntityRegistryStateEntry[]
>; >;
for (const key of ["state", "diagnostic", "config"]) { for (const key of ["control", "sensor", "diagnostic", "config"]) {
if (!(key in result)) { if (!(key in result)) {
result[key] = []; result[key] = [];
} }
@@ -179,6 +185,7 @@ export class HaConfigDevicePage extends LitElement {
`; `;
} }
const deviceName = computeDeviceName(device, this.hass);
const integrations = this._integrations(device, this.entries); const integrations = this._integrations(device, this.entries);
const entities = this._entities(this.deviceId, this.entities); const entities = this._entities(this.deviceId, this.entities);
const entitiesByCategory = this._entitiesByCategory(entities); const entitiesByCategory = this._entitiesByCategory(entities);
@@ -194,6 +201,13 @@ export class HaConfigDevicePage extends LitElement {
: undefined; : undefined;
const area = this._computeArea(this.areas, device); const area = this._computeArea(this.areas, device);
const configurationUrlIsHomeAssistant =
device.configuration_url?.startsWith("homeassistant://") || false;
const configurationUrl = configurationUrlIsHomeAssistant
? device.configuration_url!.replace("homeassistant://", "/")
: device.configuration_url;
return html` return html`
<hass-tabs-subpage <hass-tabs-subpage
.hass=${this.hass} .hass=${this.hass}
@@ -204,9 +218,7 @@ export class HaConfigDevicePage extends LitElement {
${ ${
this.narrow this.narrow
? html` ? html`
<span slot="header"> <span slot="header">${deviceName}</span>
${computeDeviceName(device, this.hass)}
</span>
<ha-icon-button <ha-icon-button
slot="toolbar-icon" slot="toolbar-icon"
.path=${mdiPencil} .path=${mdiPencil}
@@ -230,7 +242,7 @@ export class HaConfigDevicePage extends LitElement {
: html` : html`
<div class="header-name"> <div class="header-name">
<div> <div>
<h1>${computeDeviceName(device, this.hass)}</h1> <h1>${deviceName}</h1>
${area ${area
? html` ? html`
<a href="/config/areas/area/${area.area_id}" <a href="/config/areas/area/${area.area_id}"
@@ -317,13 +329,15 @@ export class HaConfigDevicePage extends LitElement {
: html`` : html``
} }
${ ${
device.configuration_url configurationUrl
? html` ? html`
<div class="card-actions" slot="actions"> <div class="card-actions" slot="actions">
<a <a
href=${device.configuration_url} href=${configurationUrl}
target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
.target=${configurationUrlIsHomeAssistant
? "_self"
: "_blank"}
> >
<mwc-button> <mwc-button>
${this.hass.localize( ${this.hass.localize(
@@ -343,25 +357,28 @@ export class HaConfigDevicePage extends LitElement {
} }
${this._renderIntegrationInfo(device, integrations)} ${this._renderIntegrationInfo(device, integrations)}
</ha-device-info-card> </ha-device-info-card>
</div>
${["state", "config", "diagnostic"].map((category) => <div class="column">
!entitiesByCategory[category].length ${["control", "sensor", "config", "diagnostic"].map((category) =>
? "" // Make sure we render controls if no other cards will be rendered
: html` entitiesByCategory[category].length > 0 ||
(entities.length === 0 && category === "control")
? html`
<ha-device-entities-card <ha-device-entities-card
.hass=${this.hass} .hass=${this.hass}
.header=${this.hass.localize( .header=${this.hass.localize(
`ui.panel.config.devices.entities.${category}` `ui.panel.config.devices.entities.${category}`
)} )}
.deviceName=${deviceName}
.entities=${entitiesByCategory[category]} .entities=${entitiesByCategory[category]}
.showDisabled=${device.disabled_by !== null} .showDisabled=${device.disabled_by !== null}
> >
</ha-device-entities-card> </ha-device-entities-card>
` `
: ""
)} )}
</div> </div>
<div class="column"> <div class="column">
${ ${
isComponentLoaded(this.hass, "automation") isComponentLoaded(this.hass, "automation")
? html` ? html`
@@ -420,7 +437,7 @@ export class HaConfigDevicePage extends LitElement {
: ""; : "";
}) })
: html` : html`
<paper-item class="no-link"> <div class="card-content">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.devices.add_prompt", "ui.panel.config.devices.add_prompt",
"name", "name",
@@ -428,92 +445,82 @@ export class HaConfigDevicePage extends LitElement {
"ui.panel.config.devices.automation.automations" "ui.panel.config.devices.automation.automations"
) )
)} )}
</paper-item> </div>
`} `}
</ha-card> </ha-card>
` `
: "" : ""
} }
</div>
<div class="column">
${ ${
isComponentLoaded(this.hass, "scene") && entities.length isComponentLoaded(this.hass, "scene") && entities.length
? html` ? html`
<ha-card> <ha-card>
<h1 class="card-header"> <h1 class="card-header">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.devices.scene.scenes" "ui.panel.config.devices.scene.scenes"
)} )}
<ha-icon-button @click=${ <ha-icon-button
this._createScene @click=${this._createScene}
} .disabled=${device.disabled_by} .disabled=${device.disabled_by}
.label=${ .label=${device.disabled_by
device.disabled_by ? this.hass.localize(
? this.hass.localize( "ui.panel.config.devices.scene.create_disabled"
"ui.panel.config.devices.scene.create_disabled" )
) : this.hass.localize(
: this.hass.localize( "ui.panel.config.devices.scene.create"
"ui.panel.config.devices.scene.create" )}
) .path=${mdiPlusCircle}
} ></ha-icon-button>
.path=${mdiPlusCircle} </h1>
></ha-icon-button>
</h1>
${ ${this._related?.scene?.length
this._related?.scene?.length ? this._related.scene.map((scene) => {
? this._related.scene.map((scene) => { const entityState = this.hass.states[scene];
const entityState = this.hass.states[scene]; return entityState
return entityState ? html`
? html` <div>
<div> <a
<a href=${ifDefined(
href=${ifDefined( entityState.attributes.id
entityState.attributes.id ? `/config/scene/edit/${entityState.attributes.id}`
? `/config/scene/edit/${entityState.attributes.id}` : undefined
: undefined )}
)} >
> <paper-item
<paper-item .scene=${entityState}
.scene=${entityState} .disabled=${!entityState.attributes.id}
.disabled=${!entityState.attributes >
.id} <paper-item-body>
> ${computeStateName(entityState)}
<paper-item-body> </paper-item-body>
${computeStateName(entityState)} <ha-icon-next></ha-icon-next>
</paper-item-body> </paper-item>
<ha-icon-next></ha-icon-next> </a>
</paper-item> ${!entityState.attributes.id
</a> ? html`
${!entityState.attributes.id <paper-tooltip animation-delay="0">
? html` ${this.hass.localize(
<paper-tooltip "ui.panel.config.devices.cant_edit"
animation-delay="0" )}
> </paper-tooltip>
${this.hass.localize( `
"ui.panel.config.devices.cant_edit" : ""}
)} </div>
</paper-tooltip> `
` : "";
: ""} })
</div> : html`
` <div class="card-content">
: ""; ${this.hass.localize(
}) "ui.panel.config.devices.add_prompt",
: html` "name",
<paper-item class="no-link"> this.hass.localize(
${this.hass.localize( "ui.panel.config.devices.scene.scenes"
"ui.panel.config.devices.add_prompt", )
"name", )}
this.hass.localize( </div>
"ui.panel.config.devices.scene.scenes" `}
)
)}
</paper-item>
`
}
</ha-card>
</ha-card> </ha-card>
` `
: "" : ""
@@ -558,7 +565,7 @@ export class HaConfigDevicePage extends LitElement {
: ""; : "";
}) })
: html` : html`
<paper-item class="no-link"> <div class="card-content">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.devices.add_prompt", "ui.panel.config.devices.add_prompt",
"name", "name",
@@ -566,14 +573,14 @@ export class HaConfigDevicePage extends LitElement {
"ui.panel.config.devices.script.scripts" "ui.panel.config.devices.script.scripts"
) )
)} )}
</paper-item> </div>
`} `}
</ha-card> </ha-card>
` `
: "" : ""
} }
</div> </div>
</div> </div>
</ha-config-section> </ha-config-section>
</hass-tabs-subpage> `; </hass-tabs-subpage> `;
} }
@@ -953,19 +960,11 @@ export class HaConfigDevicePage extends LitElement {
font-size: var(--paper-font-body1_-_font-size); font-size: var(--paper-font-body1_-_font-size);
} }
paper-item.no-link {
cursor: default;
}
a { a {
text-decoration: none; text-decoration: none;
color: var(--primary-color); color: var(--primary-color);
} }
ha-card {
padding-bottom: 8px;
}
ha-card a { ha-card a {
color: var(--primary-text-color); color: var(--primary-text-color);
} }

View File

@@ -73,7 +73,6 @@ export class DialogEnergyBatterySettings
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.energy.battery.dialog.energy_into_battery" "ui.panel.config.energy.battery.dialog.energy_into_battery"
)} )}
entities-only
@value-changed=${this._statisticToChanged} @value-changed=${this._statisticToChanged}
></ha-statistic-picker> ></ha-statistic-picker>
@@ -85,7 +84,6 @@ export class DialogEnergyBatterySettings
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.energy.battery.dialog.energy_out_of_battery" "ui.panel.config.energy.battery.dialog.energy_out_of_battery"
)} )}
entities-only
@value-changed=${this._statisticFromChanged} @value-changed=${this._statisticFromChanged}
></ha-statistic-picker> ></ha-statistic-picker>

View File

@@ -74,7 +74,6 @@ export class DialogEnergyDeviceSettings
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.energy.device_consumption.dialog.device_consumption_energy" "ui.panel.config.energy.device_consumption.dialog.device_consumption_energy"
)} )}
entities-only
@value-changed=${this._statisticChanged} @value-changed=${this._statisticChanged}
></ha-statistic-picker> ></ha-statistic-picker>

View File

@@ -106,7 +106,6 @@ export class DialogEnergyGasSettings
? "kWh" ? "kWh"
: "m³" : "m³"
})`} })`}
entities-only
@value-changed=${this._statisticChanged} @value-changed=${this._statisticChanged}
></ha-statistic-picker> ></ha-statistic-picker>

View File

@@ -103,7 +103,6 @@ export class DialogEnergyGridFlowSettings
.label=${this.hass.localize( .label=${this.hass.localize(
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.energy_stat` `ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.energy_stat`
)} )}
entities-only
@value-changed=${this._statisticChanged} @value-changed=${this._statisticChanged}
></ha-statistic-picker> ></ha-statistic-picker>

View File

@@ -85,7 +85,6 @@ export class DialogEnergySolarSettings
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.energy.solar.dialog.solar_production_energy" "ui.panel.config.energy.solar.dialog.solar_production_energy"
)} )}
entities-only
@value-changed=${this._statisticChanged} @value-changed=${this._statisticChanged}
></ha-statistic-picker> ></ha-statistic-picker>

View File

@@ -202,12 +202,12 @@ class DialogZWaveJSAddNode extends LitElement {
(securityClass) => html`<ha-formfield (securityClass) => html`<ha-formfield
.label=${html`<b .label=${html`<b
>${this.hass.localize( >${this.hass.localize(
`ui.panel.config.zwave_js.add_node.security_classes.${SecurityClass[securityClass]}.title` `ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.title`
)}</b )}</b
> >
<div class="secondary"> <div class="secondary">
${this.hass.localize( ${this.hass.localize(
`ui.panel.config.zwave_js.add_node.security_classes.${SecurityClass[securityClass]}.description` `ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.description`
)} )}
</div>`} </div>`}
> >

View File

@@ -1,9 +1,10 @@
import { mdiClose, mdiContentCopy, mdiPackageVariant } from "@mdi/js"; import { mdiClose, mdiContentCopy } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { copyToClipboard } from "../../../common/util/copy-clipboard"; import { copyToClipboard } from "../../../common/util/copy-clipboard";
import "../../../components/ha-alert";
import "../../../components/ha-dialog"; import "../../../components/ha-dialog";
import "../../../components/ha-header-bar"; import "../../../components/ha-header-bar";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
@@ -96,12 +97,11 @@ class DialogSystemLogDetail extends LitElement {
></ha-icon-button> ></ha-icon-button>
</ha-header-bar> </ha-header-bar>
${this.isCustomIntegration ${this.isCustomIntegration
? html`<div class="custom"> ? html`<ha-alert alert-type="warning">
<ha-svg-icon .path=${mdiPackageVariant}></ha-svg-icon>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.logs.error_from_custom_integration" "ui.panel.config.logs.error_from_custom_integration"
)} )}
</div>` </ha-alert>`
: ""} : ""}
<div class="contents"> <div class="contents">
<p> <p>
@@ -215,9 +215,9 @@ class DialogSystemLogDetail extends LitElement {
margin-bottom: 0; margin-bottom: 0;
font-family: var(--code-font-family, monospace); font-family: var(--code-font-family, monospace);
} }
.custom { ha-alert {
padding: 8px 16px; display: block;
background-color: var(--warning-color); margin: -4px 0;
} }
.contents { .contents {
padding: 16px; padding: 16px;

View File

@@ -0,0 +1,205 @@
import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-blueprint-picker";
import "../../../components/ha-card";
import "../../../components/ha-circular-progress";
import "../../../components/ha-markdown";
import "../../../components/ha-selector/ha-selector";
import "../../../components/ha-settings-row";
import {
BlueprintOrError,
Blueprints,
fetchBlueprints,
} from "../../../data/blueprint";
import { BlueprintScriptConfig } from "../../../data/script";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
@customElement("blueprint-script-editor")
export class HaBlueprintScriptEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide!: boolean;
@property({ reflect: true, type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public config!: BlueprintScriptConfig;
@state() private _blueprints?: Blueprints;
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._getBlueprints();
}
private get _blueprint(): BlueprintOrError | undefined {
if (!this._blueprints) {
return undefined;
}
return this._blueprints[this.config.use_blueprint.path];
}
protected render() {
const blueprint = this._blueprint;
return html` <ha-config-section vertical .isWide=${this.isWide}>
<span slot="header"
>${this.hass.localize(
"ui.panel.config.automation.editor.blueprint.header"
)}</span
>
<ha-card>
<div class="blueprint-picker-container">
${this._blueprints
? Object.keys(this._blueprints).length
? html`
<ha-blueprint-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.blueprint.blueprint_to_use"
)}
.blueprints=${this._blueprints}
.value=${this.config.use_blueprint.path}
@value-changed=${this._blueprintChanged}
></ha-blueprint-picker>
`
: this.hass.localize(
"ui.panel.config.automation.editor.blueprint.no_blueprints"
)
: html`<ha-circular-progress active></ha-circular-progress>`}
</div>
${this.config.use_blueprint.path
? blueprint && "error" in blueprint
? html`<p class="warning padding">
There is an error in this Blueprint: ${blueprint.error}
</p>`
: html`${blueprint?.metadata.description
? html`<ha-markdown
class="card-content"
breaks
.content=${blueprint.metadata.description}
></ha-markdown>`
: ""}
${blueprint?.metadata?.input &&
Object.keys(blueprint.metadata.input).length
? Object.entries(blueprint.metadata.input).map(
([key, value]) =>
html`<ha-settings-row .narrow=${this.narrow}>
<span slot="heading">${value?.name || key}</span>
<span slot="description">${value?.description}</span>
${value?.selector
? html`<ha-selector
.hass=${this.hass}
.selector=${value.selector}
.key=${key}
.value=${(this.config.use_blueprint.input &&
this.config.use_blueprint.input[key]) ??
value?.default}
@value-changed=${this._inputChanged}
></ha-selector>`
: html`<paper-input
.key=${key}
required
.value=${(this.config.use_blueprint.input &&
this.config.use_blueprint.input[key]) ??
value?.default}
@value-changed=${this._inputChanged}
no-label-float
></paper-input>`}
</ha-settings-row>`
)
: html`<p class="padding">
${this.hass.localize(
"ui.panel.config.automation.editor.blueprint.no_inputs"
)}
</p>`}`
: ""}
</ha-card>
</ha-config-section>`;
}
private async _getBlueprints() {
this._blueprints = await fetchBlueprints(this.hass, "script");
}
private _blueprintChanged(ev) {
ev.stopPropagation();
if (this.config.use_blueprint.path === ev.detail.value) {
return;
}
fireEvent(this, "value-changed", {
value: {
...this.config,
use_blueprint: {
path: ev.detail.value,
},
},
});
}
private _inputChanged(ev) {
ev.stopPropagation();
const target = ev.target as any;
const key = target.key;
const value = ev.detail.value;
if (
(this.config.use_blueprint.input &&
this.config.use_blueprint.input[key] === value) ||
(!this.config.use_blueprint.input && value === "")
) {
return;
}
const input = { ...this.config.use_blueprint.input, [key]: value };
if (value === "" || value === undefined) {
delete input[key];
}
fireEvent(this, "value-changed", {
value: {
...this.config,
use_blueprint: {
...this.config.use_blueprint,
input,
},
},
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.padding {
padding: 16px;
}
.blueprint-picker-container {
padding: 16px;
}
p {
margin-bottom: 0;
}
ha-settings-row {
--paper-time-input-justify-content: flex-end;
border-top: 1px solid var(--divider-color);
}
:host(:not([narrow])) ha-settings-row paper-input {
width: 60%;
}
:host(:not([narrow])) ha-settings-row ha-selector {
width: 60%;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"blueprint-script-editor": HaBlueprintScriptEditor;
}
}

View File

@@ -38,6 +38,7 @@ import {
Action, Action,
deleteScript, deleteScript,
getScriptEditorInitData, getScriptEditorInitData,
ManualScriptConfig,
MODES, MODES,
MODES_MAX, MODES_MAX,
ScriptConfig, ScriptConfig,
@@ -55,6 +56,7 @@ import "../automation/action/ha-automation-action";
import { HaDeviceAction } from "../automation/action/types/ha-automation-action-device_id"; import { HaDeviceAction } from "../automation/action/types/ha-automation-action-device_id";
import "../ha-config-section"; import "../ha-config-section";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import "./blueprint-script-editor";
export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -236,60 +238,62 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
> >
</paper-input>` </paper-input>`
: ""} : ""}
<p> ${"use_blueprint" in this._config
${this.hass.localize( ? ""
"ui.panel.config.script.editor.modes.description", : html`<p>
"documentation_link", ${this.hass.localize(
html`<a "ui.panel.config.script.editor.modes.description",
href=${documentationUrl( "documentation_link",
this.hass, html`<a
"/integrations/script/#script-modes" href=${documentationUrl(
)} this.hass,
target="_blank" "/integrations/script/#script-modes"
rel="noreferrer" )}
>${this.hass.localize( target="_blank"
"ui.panel.config.script.editor.modes.documentation" rel="noreferrer"
)}</a >${this.hass.localize(
>` "ui.panel.config.script.editor.modes.documentation"
)} )}</a
</p> >`
<paper-dropdown-menu-light )}
.label=${this.hass.localize( </p>
"ui.panel.config.script.editor.modes.label" <paper-dropdown-menu-light
)} .label=${this.hass.localize(
no-animations "ui.panel.config.script.editor.modes.label"
> )}
<paper-listbox no-animations
slot="dropdown-content" >
.selected=${this._config.mode <paper-listbox
? MODES.indexOf(this._config.mode) slot="dropdown-content"
: 0} .selected=${this._config.mode
@iron-select=${this._modeChanged} ? MODES.indexOf(this._config.mode)
> : 0}
${MODES.map( @iron-select=${this._modeChanged}
(mode) => html` >
<paper-item .mode=${mode}> ${MODES.map(
${this.hass.localize( (mode) => html`
`ui.panel.config.script.editor.modes.${mode}` <paper-item .mode=${mode}>
) || mode} ${this.hass.localize(
</paper-item> `ui.panel.config.script.editor.modes.${mode}`
` ) || mode}
)} </paper-item>
</paper-listbox> `
</paper-dropdown-menu-light> )}
${this._config.mode && </paper-listbox>
MODES_MAX.includes(this._config.mode) </paper-dropdown-menu-light>
? html`<paper-input ${this._config.mode &&
.label=${this.hass.localize( MODES_MAX.includes(this._config.mode)
`ui.panel.config.script.editor.max.${this._config.mode}` ? html`<paper-input
)} .label=${this.hass.localize(
type="number" `ui.panel.config.script.editor.max.${this._config.mode}`
name="max" )}
.value=${this._config.max || "10"} type="number"
@value-changed=${this._valueChanged} name="max"
> .value=${this._config.max || "10"}
</paper-input>` @value-changed=${this._valueChanged}
: html``} >
</paper-input>`
: html``} `}
</div> </div>
${this.scriptEntityId ${this.scriptEntityId
? html` ? html`
@@ -323,37 +327,48 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
</ha-card> </ha-card>
</ha-config-section> </ha-config-section>
<ha-config-section vertical .isWide=${this.isWide}> ${"use_blueprint" in this._config
<span slot="header"> ? html`<blueprint-script-editor
${this.hass.localize( .hass=${this.hass}
"ui.panel.config.script.editor.sequence" .narrow=${this.narrow}
)} .isWide=${this.isWide}
</span> .config=${this._config}
<span slot="introduction"> @value-changed=${this._configChanged}
<p> ></blueprint-script-editor>`
${this.hass.localize( : html`<ha-config-section
"ui.panel.config.script.editor.sequence_sentence" vertical
)} .isWide=${this.isWide}
</p>
<a
href=${documentationUrl(
this.hass,
"/docs/scripts/"
)}
target="_blank"
rel="noreferrer"
> >
${this.hass.localize( <span slot="header">
"ui.panel.config.script.editor.link_available_actions" ${this.hass.localize(
)} "ui.panel.config.script.editor.sequence"
</a> )}
</span> </span>
<ha-automation-action <span slot="introduction">
.actions=${this._config.sequence} <p>
@value-changed=${this._sequenceChanged} ${this.hass.localize(
.hass=${this.hass} "ui.panel.config.script.editor.sequence_sentence"
></ha-automation-action> )}
</ha-config-section> </p>
<a
href=${documentationUrl(
this.hass,
"/docs/scripts/"
)}
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.script.editor.link_available_actions"
)}
</a>
</span>
<ha-automation-action
.actions=${this._config.sequence}
@value-changed=${this._sequenceChanged}
.hass=${this.hass}
></ha-automation-action>
</ha-config-section>`}
` `
: ""} : ""}
</div> </div>
@@ -427,7 +442,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
(!oldScript || oldScript !== this.scriptEntityId) (!oldScript || oldScript !== this.scriptEntityId)
) { ) {
this.hass this.hass
.callApi<ScriptConfig>( .callApi<ManualScriptConfig>(
"GET", "GET",
`config/script/config/${computeObjectId(this.scriptEntityId)}` `config/script/config/${computeObjectId(this.scriptEntityId)}`
) )
@@ -466,11 +481,16 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
) { ) {
const initData = getScriptEditorInitData(); const initData = getScriptEditorInitData();
this._dirty = !!initData; this._dirty = !!initData;
this._config = { const baseConfig: Partial<ScriptConfig> = {
alias: this.hass.localize("ui.panel.config.script.editor.default_name"), alias: this.hass.localize("ui.panel.config.script.editor.default_name"),
sequence: [{ ...HaDeviceAction.defaultConfig }],
...initData,
}; };
if (!initData || !("use_blueprint" in initData)) {
baseConfig.sequence = [{ ...HaDeviceAction.defaultConfig }];
}
this._config = {
...baseConfig,
...initData,
} as ScriptConfig;
} }
} }
@@ -548,6 +568,11 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
this._dirty = true; this._dirty = true;
} }
private _configChanged(ev) {
this._config = ev.detail.value;
this._dirty = true;
}
private _sequenceChanged(ev: CustomEvent): void { private _sequenceChanged(ev: CustomEvent): void {
this._config = { ...this._config!, sequence: ev.detail.value as Action[] }; this._config = { ...this._config!, sequence: ev.detail.value as Action[] };
this._errors = undefined; this._errors = undefined;
@@ -607,7 +632,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
) { ) {
return; return;
} }
// Wait for dialog to complate closing // Wait for dialog to complete closing
await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0));
} }
showScriptEditor({ showScriptEditor({
@@ -749,3 +774,9 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
} }
customElements.define("ha-script-editor", HaScriptEditor); customElements.define("ha-script-editor", HaScriptEditor);
declare global {
interface HTMLElementTagNameMap {
"ha-script-editor": HaScriptEditor;
}
}

View File

@@ -50,21 +50,21 @@ class HaPanelDevStatistics extends LitElement {
private _columns = memoizeOne( private _columns = memoizeOne(
(localize): DataTableColumnContainer => ({ (localize): DataTableColumnContainer => ({
state: { state: {
title: "Entity", title: "Name",
sortable: true, sortable: true,
filterable: true, filterable: true,
grows: true, grows: true,
template: (entityState, data: any) => template: (entityState, data: any) =>
html`${entityState html`${entityState
? computeStateName(entityState) ? computeStateName(entityState)
: data.statistic_id}`, : data.name || data.statistic_id}`,
}, },
statistic_id: { statistic_id: {
title: "Statistic id", title: "Statistic id",
sortable: true, sortable: true,
filterable: true, filterable: true,
hidden: this.narrow, hidden: this.narrow,
width: "30%", width: "20%",
}, },
unit_of_measurement: { unit_of_measurement: {
title: "Unit", title: "Unit",
@@ -72,6 +72,12 @@ class HaPanelDevStatistics extends LitElement {
filterable: true, filterable: true,
width: "10%", width: "10%",
}, },
source: {
title: "Source",
sortable: true,
filterable: true,
width: "10%",
},
issues: { issues: {
title: "Issue", title: "Issue",
sortable: true, sortable: true,
@@ -146,6 +152,7 @@ class HaPanelDevStatistics extends LitElement {
this._data.push({ this._data.push({
statistic_id: statisticId, statistic_id: statisticId,
unit_of_measurement: "", unit_of_measurement: "",
source: "",
state: this.hass.states[statisticId], state: this.hass.states[statisticId],
issues: issues[statisticId], issues: issues[statisticId],
}); });

View File

@@ -12,6 +12,7 @@ import {
} from "date-fns"; } from "date-fns";
import { css, html, LitElement, PropertyValues } from "lit"; import { css, html, LitElement, PropertyValues } from "lit";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import { extractSearchParam } from "../../common/url/search-params";
import { computeRTL } from "../../common/util/compute_rtl"; import { computeRTL } from "../../common/util/compute_rtl";
import "../../components/chart/state-history-charts"; import "../../components/chart/state-history-charts";
import "../../components/entity/ha-entity-picker"; import "../../components/entity/ha-entity-picker";
@@ -136,6 +137,8 @@ class HaPanelHistory extends LitElement {
[this.hass.localize("ui.components.date-range-picker.ranges.last_week")]: [this.hass.localize("ui.components.date-range-picker.ranges.last_week")]:
[addDays(weekStart, -7), addDays(weekEnd, -7)], [addDays(weekStart, -7), addDays(weekEnd, -7)],
}; };
this._entityId = extractSearchParam("entity_id") ?? "";
} }
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {

View File

@@ -15,7 +15,6 @@ import { formatTimeWithSeconds } from "../../common/datetime/format_time";
import { restoreScroll } from "../../common/decorators/restore-scroll"; import { restoreScroll } from "../../common/decorators/restore-scroll";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
import { domainIcon } from "../../common/entity/domain_icon";
import { computeRTL, emitRTLDirection } from "../../common/util/compute_rtl"; import { computeRTL, emitRTLDirection } from "../../common/util/compute_rtl";
import "../../components/entity/state-badge"; import "../../components/entity/state-badge";
import "../../components/ha-circular-progress"; import "../../components/ha-circular-progress";
@@ -151,12 +150,13 @@ class HaLogbook extends LitElement {
html` html`
<state-badge <state-badge
.hass=${this.hass} .hass=${this.hass}
.overrideIcon=${item.icon ?? .overrideIcon=${item.icon}
domainIcon(domain, stateObj, item.state)}
.overrideImage=${DOMAINS_WITH_DYNAMIC_PICTURE.has(domain) .overrideImage=${DOMAINS_WITH_DYNAMIC_PICTURE.has(domain)
? "" ? ""
: stateObj?.attributes.entity_picture_local || : stateObj?.attributes.entity_picture_local ||
stateObj?.attributes.entity_picture} stateObj?.attributes.entity_picture}
.stateObj=${stateObj}
.stateColor=${false}
></state-badge> ></state-badge>
` `
: ""} : ""}
@@ -222,6 +222,7 @@ class HaLogbook extends LitElement {
}?run_id=${ }?run_id=${
this.traceContexts[item.context_id!].run_id this.traceContexts[item.context_id!].run_id
}`} }`}
@click=${this._close}
>${this.hass.localize( >${this.hass.localize(
"ui.components.logbook.show_trace" "ui.components.logbook.show_trace"
)}</a )}</a
@@ -254,6 +255,10 @@ class HaLogbook extends LitElement {
}); });
} }
private _close(): void {
setTimeout(() => fireEvent(this, "closed"), 500);
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,

View File

@@ -33,6 +33,7 @@ import "../../layouts/ha-app-layout";
import { haStyle } from "../../resources/styles"; import { haStyle } from "../../resources/styles";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "./ha-logbook"; import "./ha-logbook";
import { extractSearchParam } from "../../common/url/search-params";
@customElement("ha-panel-logbook") @customElement("ha-panel-logbook")
export class HaPanelLogbook extends LitElement { export class HaPanelLogbook extends LitElement {
@@ -158,6 +159,8 @@ export class HaPanelLogbook extends LitElement {
[this.hass.localize("ui.components.date-range-picker.ranges.last_week")]: [this.hass.localize("ui.components.date-range-picker.ranges.last_week")]:
[addDays(weekStart, -7), addDays(weekEnd, -7)], [addDays(weekStart, -7), addDays(weekEnd, -7)],
}; };
this._entityId = extractSearchParam("entity_id") ?? "";
} }
protected updated(changedProps: PropertyValues<this>) { protected updated(changedProps: PropertyValues<this>) {

View File

@@ -8,17 +8,18 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, state, query } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { alarmPanelIcon } from "../../../common/entity/alarm_panel_icon"; import { alarmPanelIcon } from "../../../common/entity/alarm_panel_icon";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-label-badge"; import "../../../components/ha-chip";
import { import {
callAlarmAction, callAlarmAction,
FORMAT_NUMBER, FORMAT_NUMBER,
} from "../../../data/alarm_control_panel"; } from "../../../data/alarm_control_panel";
import { UNAVAILABLE } from "../../../data/entity";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { findEntities } from "../common/find-entities"; import { findEntities } from "../common/find-entities";
import { createEntityNotFoundWarning } from "../components/hui-warning"; import { createEntityNotFoundWarning } from "../components/hui-warning";
@@ -144,19 +145,24 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
`; `;
} }
const stateLabel = this._stateDisplay(stateObj.state);
return html` return html`
<ha-card <ha-card>
.header=${this._config.name || <h1 class="card-header">
stateObj.attributes.friendly_name || ${this._config.name ||
this._stateDisplay(stateObj.state)} stateObj.attributes.friendly_name ||
> stateLabel}
<ha-label-badge <ha-chip
class=${classMap({ [stateObj.state]: true })} hasIcon
.label=${this._stateIconLabel(stateObj.state)} class=${classMap({ [stateObj.state]: true })}
@click=${this._handleMoreInfo} @click=${this._handleMoreInfo}
> >
<ha-svg-icon .path=${alarmPanelIcon(stateObj.state)}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${alarmPanelIcon(stateObj.state)}>
</ha-label-badge> </ha-svg-icon>
${stateLabel}
</ha-chip>
</h1>
<div id="armActions" class="actions"> <div id="armActions" class="actions">
${(stateObj.state === "disarmed" ${(stateObj.state === "disarmed"
? this._config.states! ? this._config.states!
@@ -215,23 +221,16 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
`; `;
} }
private _stateIconLabel(entityState: string): string {
const stateLabel = entityState.split("_").pop();
return stateLabel === "disarmed" ||
stateLabel === "triggered" ||
!stateLabel
? ""
: this._stateDisplay(entityState);
}
private _actionDisplay(entityState: string): string { private _actionDisplay(entityState: string): string {
return this.hass!.localize(`ui.card.alarm_control_panel.${entityState}`); return this.hass!.localize(`ui.card.alarm_control_panel.${entityState}`);
} }
private _stateDisplay(entityState: string): string { private _stateDisplay(entityState: string): string {
return this.hass!.localize( return entityState === UNAVAILABLE
`component.alarm_control_panel.state._.${entityState}` ? this.hass!.localize("state.default.unavailable")
); : this.hass!.localize(
`component.alarm_control_panel.state._.${entityState}`
) || entityState;
} }
private _handlePadClick(e: MouseEvent): void { private _handlePadClick(e: MouseEvent): void {
@@ -273,15 +272,20 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
--alarm-state-color: var(--alarm-color-armed); --alarm-state-color: var(--alarm-color-armed);
} }
ha-label-badge { ha-chip {
--ha-label-badge-color: var(--alarm-state-color); --ha-chip-background-color: var(--alarm-state-color);
--label-badge-text-color: var(--alarm-state-color); --ha-chip-text-color: var(--text-primary-color);
--label-badge-background-color: var(--card-background-color); line-height: initial;
color: var(--alarm-state-color); }
position: absolute;
right: 12px; .card-header {
top: 8px; display: flex;
cursor: pointer; justify-content: space-between;
align-items: center;
}
.unavailable {
--alarm-state-color: var(--state-unavailable-color);
} }
.disarmed { .disarmed {

View File

@@ -171,6 +171,7 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
} }
ha-markdown { ha-markdown {
padding: 0 16px 16px; padding: 0 16px 16px;
word-wrap: break-word;
} }
ha-markdown.no-header { ha-markdown.no-header {
padding-top: 16px; padding-top: 16px;

View File

@@ -7,14 +7,12 @@ import {
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDisplay } from "../../../common/entity/compute_state_display"; import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/ha-card"; import "../../../components/ha-card";
import { UNAVAILABLE_STATES } from "../../../data/entity";
import { ActionHandlerEvent } from "../../../data/lovelace"; import { ActionHandlerEvent } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive"; import { actionHandler } from "../common/directives/action-handler-directive";
@@ -135,9 +133,9 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
</div> </div>
`; `;
} else if (this._config.show_name) { } else if (this._config.show_name) {
footer = html`<div class="footer">${name}</div>`; footer = html`<div class="footer single">${name}</div>`;
} else if (this._config.show_state) { } else if (this._config.show_state) {
footer = html`<div class="footer state">${entityState}</div>`; footer = html`<div class="footer single">${entityState}</div>`;
} }
return html` return html`
@@ -163,9 +161,6 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
? "0" ? "0"
: undefined : undefined
)} )}
class=${classMap({
clickable: !UNAVAILABLE_STATES.includes(stateObj.state),
})}
></hui-image> ></hui-image>
${footer} ${footer}
</ha-card> </ha-card>
@@ -182,7 +177,7 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
box-sizing: border-box; box-sizing: border-box;
} }
hui-image.clickable { hui-image {
cursor: pointer; cursor: pointer;
} }
@@ -212,8 +207,8 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
justify-content: space-between; justify-content: space-between;
} }
.state { .single {
text-align: right; text-align: center;
} }
`; `;
} }

View File

@@ -78,7 +78,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
} }
const configEntities = config.entities const configEntities = config.entities
? processConfigEntities(config.entities) ? processConfigEntities(config.entities, false)
: []; : [];
this._entities = []; this._entities = [];

View File

@@ -87,7 +87,14 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
} }
public getCardSize(): number { public getCardSize(): number {
return this._config?.show_forecast !== false ? 5 : 2; let cardSize = 0;
if (this._config?.show_current !== false) {
cardSize += 2;
}
if (this._config?.show_forecast !== false) {
cardSize += 3;
}
return cardSize;
} }
public setConfig(config: WeatherForecastCardConfig): void { public setConfig(config: WeatherForecastCardConfig): void {
@@ -168,6 +175,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
stateObj.attributes.forecast?.length stateObj.attributes.forecast?.length
? stateObj.attributes.forecast.slice(0, this._veryVeryNarrow ? 3 : 5) ? stateObj.attributes.forecast.slice(0, this._veryVeryNarrow ? 3 : 5)
: undefined; : undefined;
const weather = !forecast || this._config?.show_current !== false;
let hourly: boolean | undefined; let hourly: boolean | undefined;
let dayNight: boolean | undefined; let dayNight: boolean | undefined;
@@ -202,74 +210,81 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
hasAction(this._config.tap_action) ? "0" : undefined hasAction(this._config.tap_action) ? "0" : undefined
)} )}
> >
<div class="content"> ${weather
<div class="icon-image"> ? html`
${weatherStateIcon || <div class="content">
html` <div class="icon-image">
<ha-state-icon ${weatherStateIcon ||
class="weather-icon" html`
.state=${stateObj} <ha-state-icon
></ha-state-icon> class="weather-icon"
`} .state=${stateObj}
</div> ></ha-state-icon>
<div class="info"> `}
<div class="name-state"> </div>
<div class="state"> <div class="info">
${computeStateDisplay( <div class="name-state">
this.hass.localize, <div class="state">
stateObj, ${computeStateDisplay(
this.hass.locale this.hass.localize,
)} stateObj,
</div> this.hass.locale
<div class="name" .title=${name}>${name}</div> )}
</div> </div>
<div class="temp-attribute"> <div class="name" .title=${name}>${name}</div>
<div class="temp"> </div>
${formatNumber( <div class="temp-attribute">
stateObj.attributes.temperature, <div class="temp">
this.hass.locale ${formatNumber(
)}&nbsp;<span>${getWeatherUnit(this.hass, "temperature")}</span> stateObj.attributes.temperature,
</div> this.hass.locale
<div class="attribute"> )}&nbsp;<span
${this._config.secondary_info_attribute !== undefined >${getWeatherUnit(this.hass, "temperature")}</span
? html` >
${this._config.secondary_info_attribute in </div>
weatherAttrIcons <div class="attribute">
${this._config.secondary_info_attribute !== undefined
? html` ? html`
<ha-svg-icon ${this._config.secondary_info_attribute in
class="attr-icon" weatherAttrIcons
.path=${weatherAttrIcons[ ? html`
this._config.secondary_info_attribute <ha-svg-icon
]} class="attr-icon"
></ha-svg-icon> .path=${weatherAttrIcons[
this._config.secondary_info_attribute
]}
></ha-svg-icon>
`
: this.hass!.localize(
`ui.card.weather.attributes.${this._config.secondary_info_attribute}`
)}
${this._config.secondary_info_attribute ===
"wind_speed"
? getWind(
this.hass,
stateObj.attributes.wind_speed,
stateObj.attributes.wind_bearing
)
: html`
${formatNumber(
stateObj.attributes[
this._config.secondary_info_attribute
],
this.hass.locale
)}
${getWeatherUnit(
this.hass,
this._config.secondary_info_attribute
)}
`}
` `
: this.hass!.localize( : getSecondaryWeatherAttribute(this.hass, stateObj)}
`ui.card.weather.attributes.${this._config.secondary_info_attribute}` </div>
)} </div>
${this._config.secondary_info_attribute === "wind_speed" </div>
? getWind(
this.hass,
stateObj.attributes.wind_speed,
stateObj.attributes.wind_bearing
)
: html`
${formatNumber(
stateObj.attributes[
this._config.secondary_info_attribute
],
this.hass.locale
)}
${getWeatherUnit(
this.hass,
this._config.secondary_info_attribute
)}
`}
`
: getSecondaryWeatherAttribute(this.hass, stateObj)}
</div> </div>
</div> `
</div> : ""}
</div>
${forecast ${forecast
? html` ? html`
<div class="forecast"> <div class="forecast">

View File

@@ -387,6 +387,7 @@ export interface ThermostatCardConfig extends LovelaceCardConfig {
export interface WeatherForecastCardConfig extends LovelaceCardConfig { export interface WeatherForecastCardConfig extends LovelaceCardConfig {
entity: string; entity: string;
name?: string; name?: string;
show_current?: boolean;
show_forecast?: boolean; show_forecast?: boolean;
secondary_info_attribute?: string; secondary_info_attribute?: string;
theme?: string; theme?: string;

View File

@@ -3,6 +3,7 @@ import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import { splitByGroups } from "../../../common/entity/split_by_groups"; import { splitByGroups } from "../../../common/entity/split_by_groups";
import { stripPrefixFromEntityName } from "../../../common/entity/strip_prefix_from_entity_name";
import { stringCompare } from "../../../common/string/compare"; import { stringCompare } from "../../../common/string/compare";
import { LocalizeFunc } from "../../../common/translations/localize"; import { LocalizeFunc } from "../../../common/translations/localize";
import type { AreaRegistryEntry } from "../../../data/area_registry"; import type { AreaRegistryEntry } from "../../../data/area_registry";
@@ -15,11 +16,11 @@ import type { EntityRegistryEntry } from "../../../data/entity_registry";
import { domainToName } from "../../../data/integration"; import { domainToName } from "../../../data/integration";
import { LovelaceCardConfig, LovelaceViewConfig } from "../../../data/lovelace"; import { LovelaceCardConfig, LovelaceViewConfig } from "../../../data/lovelace";
import { SENSOR_DEVICE_CLASS_BATTERY } from "../../../data/sensor"; import { SENSOR_DEVICE_CLASS_BATTERY } from "../../../data/sensor";
import { computeUserInitials } from "../../../data/user";
import { import {
AlarmPanelCardConfig, AlarmPanelCardConfig,
EntitiesCardConfig, EntitiesCardConfig,
HumidifierCardConfig, HumidifierCardConfig,
LightCardConfig,
PictureEntityCardConfig, PictureEntityCardConfig,
ThermostatCardConfig, ThermostatCardConfig,
} from "../cards/types"; } from "../cards/types";
@@ -31,6 +32,8 @@ const HIDE_DOMAIN = new Set([
"device_tracker", "device_tracker",
"geo_location", "geo_location",
"persistent_notification", "persistent_notification",
"script",
"sun",
"zone", "zone",
]); ]);
@@ -83,8 +86,7 @@ const splitByAreas = (
export const computeCards = ( export const computeCards = (
states: Array<[string, HassEntity?]>, states: Array<[string, HassEntity?]>,
entityCardOptions: Partial<EntitiesCardConfig>, entityCardOptions: Partial<EntitiesCardConfig>
single = false
): LovelaceCardConfig[] => { ): LovelaceCardConfig[] => {
const cards: LovelaceCardConfig[] = []; const cards: LovelaceCardConfig[] = [];
@@ -92,7 +94,7 @@ export const computeCards = (
const entities: Array<string | LovelaceRowConfig> = []; const entities: Array<string | LovelaceRowConfig> = [];
const titlePrefix = entityCardOptions.title const titlePrefix = entityCardOptions.title
? `${entityCardOptions.title} ` ? `${entityCardOptions.title} `.toLowerCase()
: undefined; : undefined;
for (const [entityId, stateObj] of states) { for (const [entityId, stateObj] of states) {
@@ -122,12 +124,6 @@ export const computeCards = (
entity: entityId, entity: entityId,
}; };
cards.push(cardConfig); cards.push(cardConfig);
} else if (domain === "light" && single) {
const cardConfig: LightCardConfig = {
type: "light",
entity: entityId,
};
cards.push(cardConfig);
} else if (domain === "media_player") { } else if (domain === "media_player") {
const cardConfig = { const cardConfig = {
type: "media-control", type: "media-control",
@@ -153,16 +149,18 @@ export const computeCards = (
) { ) {
// Do nothing. // Do nothing.
} else { } else {
let name: string; let name: string | undefined;
const entityConf = const entityConf =
titlePrefix && titlePrefix &&
stateObj && stateObj &&
// eslint-disable-next-line no-cond-assign // eslint-disable-next-line no-cond-assign
(name = computeStateName(stateObj)) !== titlePrefix && (name = stripPrefixFromEntityName(
name.startsWith(titlePrefix) computeStateName(stateObj),
titlePrefix
))
? { ? {
entity: entityId, entity: entityId,
name: adjustName(name.substr(titlePrefix.length)), name,
} }
: entityId; : entityId;
@@ -181,15 +179,6 @@ export const computeCards = (
return cards; return cards;
}; };
const hasUpperCase = (str: string): boolean => str.toLowerCase() !== str;
const adjustName = (name: string): string =>
// If first word already has an upper case letter (e.g. from brand name)
// leave as-is, otherwise capitalize the first word.
hasUpperCase(name.substr(0, name.indexOf(" ")))
? name
: name[0].toUpperCase() + name.slice(1);
const computeDefaultViewStates = ( const computeDefaultViewStates = (
entities: HassEntities, entities: HassEntities,
entityEntries: EntityRegistryEntry[] entityEntries: EntityRegistryEntry[]
@@ -244,6 +233,62 @@ export const generateViewConfig = (
let cards: LovelaceCardConfig[] = []; let cards: LovelaceCardConfig[] = [];
if ("person" in ungroupedEntitites) {
const personCards: LovelaceCardConfig[] = [];
if (ungroupedEntitites.person.length === 1) {
cards.push({
type: "entities",
entities: ungroupedEntitites.person,
});
} else {
let backgroundColor: string | undefined;
let foregroundColor = "";
for (const personEntityId of ungroupedEntitites.person) {
const stateObj = entities[personEntityId];
let image = stateObj.attributes.entity_picture;
if (!image) {
if (backgroundColor === undefined) {
const computedStyle = getComputedStyle(document.body);
backgroundColor = encodeURIComponent(
computedStyle.getPropertyValue("--light-primary-color").trim()
);
foregroundColor = encodeURIComponent(
(
computedStyle.getPropertyValue("--text-light-primary-color") ||
computedStyle.getPropertyValue("--primary-text-color")
).trim()
);
}
const initials = computeUserInitials(
stateObj.attributes.friendly_name || ""
);
image = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 50 50' width='50' height='50' style='background-color:${backgroundColor}'%3E%3Cg%3E%3Ctext font-family='roboto' x='50%25' y='50%25' text-anchor='middle' stroke='${foregroundColor}' font-size='1.3em' dy='.3em'%3E${initials}%3C/text%3E%3C/g%3E%3C/svg%3E`;
}
personCards.push({
type: "picture-entity",
entity: personEntityId,
aspect_ratio: "1",
show_name: false,
image,
});
}
cards.push({
type: "grid",
square: true,
columns: 3,
cards: personCards,
});
}
delete ungroupedEntitites.person;
}
splitted.groups.forEach((groupEntity) => { splitted.groups.forEach((groupEntity) => {
cards = cards.concat( cards = cards.concat(
computeCards( computeCards(

View File

@@ -52,7 +52,7 @@ export function hasConfigOrEntitiesChanged(
const oldHass = changedProps.get("hass") as HomeAssistant; const oldHass = changedProps.get("hass") as HomeAssistant;
const entities = processConfigEntities(element._config!.entities); const entities = processConfigEntities(element._config!.entities, false);
return entities.some( return entities.some(
(entity) => (entity) =>

View File

@@ -5,7 +5,8 @@ import { EntityConfig, LovelaceRowConfig } from "../entity-rows/types";
export const processConfigEntities = < export const processConfigEntities = <
T extends EntityConfig | LovelaceRowConfig T extends EntityConfig | LovelaceRowConfig
>( >(
entities: Array<T | string> entities: Array<T | string>,
checkEntityId = true
): T[] => { ): T[] => {
if (!entities || !Array.isArray(entities)) { if (!entities || !Array.isArray(entities)) {
throw new Error("Entities need to be an array"); throw new Error("Entities need to be an array");
@@ -35,7 +36,7 @@ export const processConfigEntities = <
throw new Error(`Invalid entity specified at position ${index}.`); throw new Error(`Invalid entity specified at position ${index}.`);
} }
if (!isValidEntityId((config as EntityConfig).entity!)) { if (checkEntityId && !isValidEntityId((config as EntityConfig).entity!)) {
throw new Error( throw new Error(
`Invalid entity ID at position ${index}: ${ `Invalid entity ID at position ${index}: ${
(config as EntityConfig).entity (config as EntityConfig).entity

View File

@@ -267,6 +267,9 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
ha-button-toggle-group { ha-button-toggle-group {
padding-left: 8px; padding-left: 8px;
} }
mwc-button {
flex-shrink: 0;
}
`; `;
} }
} }

View File

@@ -10,10 +10,6 @@ import {
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { guard } from "lit/directives/guard"; import { guard } from "lit/directives/guard";
import type { SortableEvent } from "sortablejs"; import type { SortableEvent } from "sortablejs";
import Sortable, {
AutoScroll,
OnSpill,
} from "sortablejs/modular/sortable.core.esm";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/entity/ha-entity-picker"; import "../../../components/entity/ha-entity-picker";
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker"; import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
@@ -22,6 +18,8 @@ import { sortableStyles } from "../../../resources/ha-sortable-style";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { EntityConfig } from "../entity-rows/types"; import { EntityConfig } from "../entity-rows/types";
let Sortable;
@customElement("hui-entity-editor") @customElement("hui-entity-editor")
export class HuiEntityEditor extends LitElement { export class HuiEntityEditor extends LitElement {
@property({ attribute: false }) protected hass?: HomeAssistant; @property({ attribute: false }) protected hass?: HomeAssistant;
@@ -34,7 +32,7 @@ export class HuiEntityEditor extends LitElement {
@state() private _renderEmptySortable = false; @state() private _renderEmptySortable = false;
private _sortable?: Sortable; private _sortable?;
public connectedCallback() { public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
@@ -86,11 +84,6 @@ export class HuiEntityEditor extends LitElement {
`; `;
} }
protected firstUpdated(): void {
Sortable.mount(OnSpill);
Sortable.mount(new AutoScroll());
}
protected updated(changedProps: PropertyValues): void { protected updated(changedProps: PropertyValues): void {
super.updated(changedProps); super.updated(changedProps);
@@ -128,7 +121,17 @@ export class HuiEntityEditor extends LitElement {
this._renderEmptySortable = false; this._renderEmptySortable = false;
} }
private _createSortable() { private async _createSortable() {
if (!Sortable) {
const sortableImport = await import(
"sortablejs/modular/sortable.core.esm"
);
Sortable = sortableImport.Sortable;
Sortable.mount(sortableImport.OnSpill);
Sortable.mount(sortableImport.AutoScroll());
}
this._sortable = new Sortable(this.shadowRoot!.querySelector(".entities"), { this._sortable = new Sortable(this.shadowRoot!.querySelector(".entities"), {
animation: 150, animation: 150,
fallbackClass: "sortable-fallback", fallbackClass: "sortable-fallback",

View File

@@ -192,7 +192,7 @@ export class HuiImage extends LitElement {
: undefined, : undefined,
backgroundImage: backgroundImage:
useRatio && this._loadedImageSrc useRatio && this._loadedImageSrc
? `url(${this._loadedImageSrc})` ? `url("${this._loadedImageSrc}")`
: undefined, : undefined,
filter: filter:
this._loadState === LoadState.Loaded || this.cameraView === "live" this._loadState === LoadState.Loaded || this.cameraView === "live"

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