Compare commits

...

100 Commits

Author SHA1 Message Date
Ludeeus 2a8a5319a2 Match clientID origin 2021-10-27 07:44:47 +00: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
Paulus Schoutsen f1a0623447 Bumped version to 20211020.0 2021-10-20 16:06:35 -07:00
Bram Kragten 41d02fdb72 Replace paper progress with mwc-linear-progess (#10339) 2021-10-20 15:56:20 -07:00
Bram Kragten 52d45d482c Change dark mode input fill color (#10341) 2021-10-20 15:55:40 -07:00
Bram Kragten a0fea94db2 Add support for no-state and entity-no-longer-available statistic… (#10345) 2021-10-20 15:55:09 -07:00
Bram Kragten c3975e48d9 Tweak icon picker a bit (#10319) 2021-10-20 21:03:18 +02:00
Bram Kragten f062e13921 Use svg icons for default panels (#10342) 2021-10-20 15:33:12 +02:00
Joakim Sørensen 08ca9c9064 Use secondary-text-color for trailing icon (#10340) 2021-10-20 12:51:41 +02:00
Bram Kragten 667fd39147 Convert default state icons (#10223)
* Convert default state icons

* update

* Update cast/src/launcher/layout/hc-cast.ts

Co-authored-by: Philip Allgaier <mail@spacegaier.de>

* Update ha-config-core.js

* Update

* Finish

* Add siren icon

* FIx

* Add curtain icons

Co-authored-by: Philip Allgaier <mail@spacegaier.de>
2021-10-20 11:10:16 +02:00
Joakim Sørensen b760e543b0 Fix overflow icon color in backup dialog (#10331)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-10-20 08:41:56 +00:00
Bram Kragten 760ead4860 Set default value when enabling optional value (#10247) 2021-10-19 21:45:41 -07:00
Bram Kragten 9a4cce74f0 Stack gas and solar sources (#10244) 2021-10-19 21:44:41 -07:00
Bram Kragten 7488eb782d Migrate all paper dialogs to mwc (#10333) 2021-10-19 13:56:49 -07:00
Joakim Sørensen b1e6935df9 Fix select options for add-on config (#10330) 2021-10-19 22:54:07 +02:00
Will Adler df53364d16 Correct grid neutrality card tooltip, make consistent with new colors (#10326) 2021-10-19 22:53:06 +02:00
Bram Kragten 777e6c4c72 Migrate all paper-radio elements to mwc-radio (#10327) 2021-10-19 13:42:30 -07:00
Bram Kragten e47a5effe6 Migrate all paper checkbox elements to mwc (#10329) 2021-10-19 13:31:24 -07:00
Joakim Sørensen 62d3f74513 Change unsupported reason container to software (#10325) 2021-10-19 18:37:38 +02:00
Joakim Sørensen 21e1fef0fb Use error for protection mode alert (#10315) 2021-10-19 18:37:22 +02:00
Philip Allgaier b3f8daa758 Fix ha-icon-button in ha-file-upload (#10328) 2021-10-19 16:24:11 +00:00
Will Adler 04f586721f Revise grid neutrality energy dashboard card, modify energy dashboard presentation to match (#10054)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-10-19 13:48:59 +02:00
uvjustin 8e22e41605 Use maxLiveSyncPlaybackRate in ha-hls-player (#10323) 2021-10-19 10:38:57 +02:00
Paul Bottein 2770d1f36b Icon Picker (#10161) 2021-10-18 22:45:21 +02:00
MartinT 403c042235 Add views dropdown and footer actions to the "move to view" dialog (#10172)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-10-18 20:27:00 +00:00
Philip Allgaier bdb3c04037 Ensure current active dark modes gets used for manually set themes (#10307) 2021-10-18 22:09:21 +02:00
Philip Allgaier f1cb21e7fc Fix formatting of weather extrema temperatures (#10306) 2021-10-18 22:07:16 +02:00
Allen Porter a8486eda9f Improve WebRTC stream error handling and cleanup (#10302) 2021-10-18 22:06:42 +02:00
Allen Porter d5b98d306d Remove element resize hook (#10300) 2021-10-18 22:05:38 +02:00
Joakim Sørensen bb2fe650ac Prevent mwc-list-item from opening up quick-bar (#10317) 2021-10-18 22:04:50 +02:00
Bram Kragten b576c3de40 Fix translation key energy distribution solar (#10316) 2021-10-18 14:11:41 +02:00
Allen Porter 84533b8843 Rename stream_type to frontend_stream_type (#10298) 2021-10-18 12:42:34 +02:00
Michael Irigoyen a8ff98b808 Update MDI to v6.3.95 (#10313) 2021-10-18 12:41:31 +02:00
Philip Allgaier f0062b1e67 Only render badge value if there is no icon and no image (#10310) 2021-10-18 01:39:13 +02:00
Bram Kragten 93f64de875 Fix icon buttons in Safari (#10293) 2021-10-16 23:03:26 +02:00
MartinT ec47e320d2 Unify default dashboard name (#10254)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-10-16 15:30:48 +00:00
Kyle Niewiada 816d5ee594 Fix energy onboarding add_solar_production button (#10275) (#10286) 2021-10-16 17:09:00 +02:00
Philip Allgaier 588f5bd6b7 Add additional binary device classes to inversion list (#10152) 2021-10-16 14:49:57 +02:00
Philip Allgaier 825ea93dba Add "capitalize" option to hui-timestamp-display (#10280) 2021-10-16 14:43:03 +02:00
Paulus Schoutsen a690a1d7bf ABC automation types + use MWC (#10287) 2021-10-16 14:41:23 +02:00
Paulus Schoutsen 9fe4c79782 Convert all warning classes to ha-alert (#10289) 2021-10-16 14:38:58 +02:00
Paulus Schoutsen 42613d6519 Disable ha-form while submitting entry flow (#10290) 2021-10-16 14:37:48 +02:00
Paulus Schoutsen 4b77910e4f Warn if iframe won't be able to load the website (#10217) 2021-10-15 09:03:51 +02: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
247 changed files with 6495 additions and 4867 deletions
@@ -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>
+32 -4
View File
@@ -22,17 +22,38 @@ const getMeta = () => {
const svg = fs.readFileSync(`${ICON_PATH}/${icon.name}.svg`, {
encoding,
});
return { path: svg.match(/ d="([^"]+)"/)[1], name: icon.name };
return {
path: svg.match(/ d="([^"]+)"/)[1],
name: icon.name,
tags: icon.tags,
};
});
};
const addRemovedMeta = (meta) => {
const file = fs.readFileSync(REMOVED_ICONS_PATH, { encoding });
const removed = JSON.parse(file);
const combinedMeta = [...meta, ...removed];
const removedMeta = removed.map((removeIcon) => ({
path: removeIcon.path,
name: removeIcon.name,
tags: [],
}));
const combinedMeta = [...meta, ...removedMeta];
return combinedMeta.sort((a, b) => a.name.localeCompare(b.name));
};
const homeAutomationTag = "Home Automation";
const orderMeta = (meta) => {
const homeAutomationMeta = meta.filter((icon) =>
icon.tags.includes(homeAutomationTag)
);
const otherMeta = meta.filter(
(icon) => !icon.tags.includes(homeAutomationTag)
);
return [...homeAutomationMeta, ...otherMeta];
};
const splitBySize = (meta) => {
const chunks = [];
const CHUNK_SIZE = 50000;
@@ -77,8 +98,10 @@ const findDifferentiator = (curString, prevString) => {
};
gulp.task("gen-icons-json", (done) => {
const meta = addRemovedMeta(getMeta());
const split = splitBySize(meta);
const meta = getMeta();
const metaAndRemoved = addRemovedMeta(meta);
const split = splitBySize(metaAndRemoved);
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
@@ -116,5 +139,10 @@ gulp.task("gen-icons-json", (done) => {
JSON.stringify({ version: package.version, parts })
);
fs.writeFileSync(
path.resolve(OUTPUT_DIR, "iconList.json"),
JSON.stringify(orderMeta(meta).map((icon) => icon.name))
);
done();
});
+5 -3
View File
@@ -1,4 +1,5 @@
import "@material/mwc-button/mwc-button";
import { mdiCast, mdiCastConnected } from "@mdi/js";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-listbox/paper-listbox";
import { Auth, Connection } from "home-assistant-js-websocket";
@@ -17,6 +18,7 @@ import {
import { atLeastVersion } from "../../../../src/common/config/version";
import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute";
import "../../../../src/components/ha-icon";
import "../../../../src/components/ha-svg-icon";
import {
getLegacyLovelaceCollection,
getLovelaceCollection,
@@ -73,7 +75,7 @@ class HcCast extends LitElement {
? html`
<p class="center-item">
<mwc-button raised @click=${this._handleLaunch}>
<ha-icon icon="hass:cast"></ha-icon>
<ha-svg-icon .path=${mdiCast}></ha-svg-icon>
Start Casting
</mwc-button>
</p>
@@ -111,7 +113,7 @@ class HcCast extends LitElement {
${this.castManager.status
? html`
<mwc-button @click=${this._handleLaunch}>
<ha-icon icon="hass:cast-connected"></ha-icon>
<ha-svg-icon .path=${mdiCastConnected}></ha-svg-icon>
Manage
</mwc-button>
`
@@ -233,7 +235,7 @@ class HcCast extends LitElement {
color: var(--secondary-text-color);
}
mwc-button ha-icon {
mwc-button ha-svg-icon {
margin-right: 8px;
height: 18px;
}
+8 -7
View File
@@ -1,4 +1,5 @@
import "@material/mwc-button";
import { mdiCastConnected, mdiCast } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import {
Auth,
@@ -19,7 +20,7 @@ import {
loadTokens,
saveTokens,
} from "../../../../src/common/auth/token_storage";
import "../../../../src/components/ha-icon";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/layouts/hass-loading-screen";
import { registerServiceWorker } from "../../../../src/util/register-service-worker";
import "./hc-layout";
@@ -127,11 +128,11 @@ export class HcConnect extends LitElement {
<div class="card-actions">
<mwc-button @click=${this._handleDemo}>
Show Demo
<ha-icon
.icon=${this.castManager.castState === "CONNECTED"
? "hass:cast-connected"
: "hass:cast"}
></ha-icon>
<ha-svg-icon
.path=${this.castManager.castState === "CONNECTED"
? mdiCastConnected
: mdiCast}
></ha-svg-icon>
</mwc-button>
<div class="spacer"></div>
<mwc-button @click=${this._handleConnect}>Authorize</mwc-button>
@@ -307,7 +308,7 @@ export class HcConnect extends LitElement {
color: darkred;
}
mwc-button ha-icon {
mwc-button ha-svg-icon {
margin-left: 8px;
}
+3 -2
View File
@@ -1,3 +1,4 @@
import { mdiTelevision } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import { CastManager } from "../../../src/cast/cast_manager";
@@ -27,7 +28,7 @@ class CastDemoRow extends LitElement implements LovelaceRow {
return html``;
}
return html`
<ha-icon icon="hademo:television"></ha-icon>
<ha-svg-icon .path=${mdiTelevision}></ha-svg-icon>
<div class="flex">
<div class="name">Show Chromecast interface</div>
<google-cast-launcher></google-cast-launcher>
@@ -72,7 +73,7 @@ class CastDemoRow extends LitElement implements LovelaceRow {
display: flex;
align-items: center;
}
ha-icon {
ha-svg-icon {
padding: 8px;
color: var(--paper-item-icon-color);
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -78,6 +78,18 @@ class HassioAddonConfig extends LitElement {
this.addon.translations.en?.configuration?.[entry.name].name ||
entry.name;
private _schema = memoizeOne((schema: HaFormSchema[]): HaFormSchema[] =>
// @ts-expect-error supervisor does not implement [string, string] for select.options[]
schema.map((entry) =>
entry.type === "select"
? {
...entry,
options: entry.options.map((option) => [option, option]),
}
: entry
)
);
private _filteredShchema = memoizeOne(
(options: Record<string, unknown>, schema: HaFormSchema[]) =>
schema.filter((entry) => entry.name in options || entry.required)
@@ -128,12 +140,14 @@ class HassioAddonConfig extends LitElement {
.data=${this._options!}
@value-changed=${this._configChanged}
.computeLabel=${this.computeLabel}
.schema=${this._showOptional
? this.addon.schema!
: this._filteredShchema(
this.addon.options,
this.addon.schema!
)}
.schema=${this._schema(
this._showOptional
? this.addon.schema!
: this._filteredShchema(
this.addon.options,
this.addon.schema!
)
)}
></ha-form>`
: html` <ha-yaml-editor
@value-changed=${this._configChanged}
+200 -197
View File
@@ -11,6 +11,12 @@ import {
mdiHomeAssistant,
mdiKey,
mdiNetwork,
mdiNumeric1,
mdiNumeric2,
mdiNumeric3,
mdiNumeric4,
mdiNumeric5,
mdiNumeric6,
mdiPound,
mdiShield,
} 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/ha-alert";
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-settings-row";
import "../../../../src/components/ha-svg-icon";
@@ -73,6 +79,15 @@ const STAGE_ICON = {
deprecated: mdiExclamationThick,
};
const RATING_ICON = {
1: mdiNumeric1,
2: mdiNumeric2,
3: mdiNumeric3,
4: mdiNumeric4,
5: mdiNumeric5,
6: mdiNumeric6,
};
@customElement("hassio-addon-info")
class HassioAddonInfo extends LitElement {
@property({ type: Boolean }) public narrow!: boolean;
@@ -180,24 +195,21 @@ class HassioAddonInfo extends LitElement {
: ""}
${!this.addon.protected
? html`
<ha-card class="warning">
<h1 class="card-header">${this.supervisor.localize(
"addon.dashboard.protection_mode.title"
)}
</h1>
<div class="card-content">
${this.supervisor.localize("addon.dashboard.protection_mode.content")}
</div>
<div class="card-actions protection-enable">
<mwc-button @click=${this._protectionToggled}>
${this.supervisor.localize(
<ha-alert
alert-type="error"
.title=${this.supervisor.localize(
"addon.dashboard.protection_mode.title"
)}
.actionText=${this.supervisor.localize(
"addon.dashboard.protection_mode.enable"
)}
</mwc-button>
</div>
</div>
</ha-card>
`
@alert-action-clicked=${this._protectionToggled}
>
${this.supervisor.localize(
"addon.dashboard.protection_mode.content"
)}
</ha-alert>
`
: ""}
<ha-card>
@@ -249,6 +261,163 @@ class HassioAddonInfo extends LitElement {
>`}
</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">
${this.addon.description}.<br />
${this.supervisor.localize(
@@ -269,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
? html`
<div
@@ -1178,34 +1181,31 @@ class HassioAddonInfo extends LitElement {
.description a {
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 {
--ha-label-badge-color: var(--label-badge-red, #df4c1e);
--ha-chip-background-color: var(--label-badge-red, #df4c1e);
}
.blue {
--ha-label-badge-color: var(--label-badge-blue, #039be5);
--ha-chip-background-color: var(--label-badge-blue, #039be5);
}
.green {
--ha-label-badge-color: var(--label-badge-green, #0da035);
--ha-chip-background-color: var(--label-badge-green, #0da035);
}
.yellow {
--ha-label-badge-color: var(--label-badge-yellow, #f4b400);
--ha-chip-background-color: var(--label-badge-yellow, #f4b400);
}
.security {
.capabilities {
margin-bottom: 16px;
}
.card-actions {
justify-content: space-between;
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 {
display: contents;
}
@@ -1245,6 +1245,9 @@ class HassioAddonInfo extends LitElement {
}
@media (max-width: 720px) {
ha-chip {
line-height: 36px;
}
.addon-options {
max-width: 100%;
}
@@ -141,6 +141,9 @@ class HassioBackupDialog
flex-shrink: 0;
display: block;
}
ha-icon-button {
color: var(--secondary-text-color);
}
`,
];
}
@@ -47,11 +47,6 @@ class HassioMarkdownDialog extends LitElement {
haStyleDialog,
hassioStyle,
css`
ha-paper-dialog {
min-width: 350px;
font-size: 14px;
border-radius: 2px;
}
app-toolbar {
margin: 0;
padding: 0 16px;
@@ -62,19 +57,6 @@ class HassioMarkdownDialog extends LitElement {
margin-left: 16px;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-paper-dialog {
max-height: 100%;
}
ha-paper-dialog::before {
content: "";
position: fixed;
z-index: -1;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
background-color: inherit;
}
app-toolbar {
color: var(--text-primary-color);
background-color: var(--primary-color);
-6
View File
@@ -113,12 +113,6 @@ export class HassioMain extends SupervisorBaseElement {
: this.hass.themes.default_theme);
themeSettings = this.hass.selectedTheme;
if (themeSettings?.dark === undefined) {
themeSettings = {
...this.hass.selectedTheme,
dark: this.hass.themes.darkMode,
};
}
} else {
themeName =
(this.hass.selectedTheme as unknown as string) ||
+27 -49
View File
@@ -31,29 +31,9 @@ import { documentationUrl } from "../../../src/util/documentation-url";
import "../components/supervisor-metric";
import { hassioStyle } from "../resources/hassio-style";
const UNSUPPORTED_REASON_URL = {
apparmor: "/more-info/unsupported/apparmor",
container: "/more-info/unsupported/container",
content_trust: "/more-info/unsupported/content_trust",
dbus: "/more-info/unsupported/dbus",
docker_configuration: "/more-info/unsupported/docker_configuration",
docker_version: "/more-info/unsupported/docker_version",
job_conditions: "/more-info/unsupported/job_conditions",
lxc: "/more-info/unsupported/lxc",
network_manager: "/more-info/unsupported/network_manager",
os_agent: "/more-info/unsupported/os_agent",
os: "/more-info/unsupported/os",
privileged: "/more-info/unsupported/privileged",
source_mods: "/more-info/unsupported/source_mods",
systemd: "/more-info/unsupported/systemd",
};
const UNSUPPORTED_REASON_URL = {};
const UNHEALTHY_REASON_URL = {
privileged: "/more-info/unsupported/privileged",
supervisor: "/more-info/unhealthy/supervisor",
setup: "/more-info/unhealthy/setup",
docker: "/more-info/unhealthy/docker",
untrusted: "/more-info/unhealthy/untrusted",
};
@customElement("hassio-supervisor-info")
@@ -425,20 +405,19 @@ class HassioSupervisorInfo extends LitElement {
${this.supervisor.resolution.unsupported.map(
(reason) => html`
<li>
${UNSUPPORTED_REASON_URL[reason]
? html`<a
href=${documentationUrl(
this.hass,
UNSUPPORTED_REASON_URL[reason]
)}
target="_blank"
rel="noreferrer"
>
${this.supervisor.localize(
`system.supervisor.unsupported_reason.${reason}`
) || reason}
</a>`
: reason}
<a
href=${documentationUrl(
this.hass,
UNSUPPORTED_REASON_URL[reason] ||
`/more-info/unsupported/${reason}`
)}
target="_blank"
rel="noreferrer"
>
${this.supervisor.localize(
`system.supervisor.unsupported_reason.${reason}`
) || reason}
</a>
</li>
`
)}
@@ -456,20 +435,19 @@ class HassioSupervisorInfo extends LitElement {
${this.supervisor.resolution.unhealthy.map(
(reason) => html`
<li>
${UNHEALTHY_REASON_URL[reason]
? html`<a
href=${documentationUrl(
this.hass,
UNHEALTHY_REASON_URL[reason]
)}
target="_blank"
rel="noreferrer"
>
${this.supervisor.localize(
`system.supervisor.unhealthy_reason.${reason}`
) || reason}
</a>`
: reason}
<a
href=${documentationUrl(
this.hass,
UNHEALTHY_REASON_URL[reason] ||
`/more-info/unhealthy/${reason}`
)}
target="_blank"
rel="noreferrer"
>
${this.supervisor.localize(
`system.supervisor.unhealthy_reason.${reason}`
) || reason}
</a>
</li>
`
)}
+44 -54
View File
@@ -22,23 +22,23 @@
"license": "Apache-2.0",
"dependencies": {
"@braintree/sanitize-url": "^5.0.2",
"@codemirror/commands": "^0.19.2",
"@codemirror/gutter": "^0.19.1",
"@codemirror/highlight": "^0.19.2",
"@codemirror/commands": "^0.19.5",
"@codemirror/gutter": "^0.19.3",
"@codemirror/highlight": "^0.19.6",
"@codemirror/history": "^0.19.0",
"@codemirror/legacy-modes": "^0.19.0",
"@codemirror/rectangular-selection": "^0.19.0",
"@codemirror/search": "^0.19.0",
"@codemirror/state": "^0.19.1",
"@codemirror/stream-parser": "^0.19.1",
"@codemirror/text": "^0.19.2",
"@codemirror/view": "^0.19.4",
"@formatjs/intl-datetimeformat": "^4.2.4",
"@formatjs/intl-getcanonicallocales": "^1.7.3",
"@formatjs/intl-locale": "^2.4.38",
"@formatjs/intl-numberformat": "^7.2.4",
"@formatjs/intl-pluralrules": "^4.1.4",
"@formatjs/intl-relativetimeformat": "^9.3.1",
"@codemirror/rectangular-selection": "^0.19.1",
"@codemirror/search": "^0.19.2",
"@codemirror/state": "^0.19.2",
"@codemirror/stream-parser": "^0.19.2",
"@codemirror/text": "^0.19.4",
"@codemirror/view": "^0.19.9",
"@formatjs/intl-datetimeformat": "^4.2.5",
"@formatjs/intl-getcanonicallocales": "^1.8.0",
"@formatjs/intl-locale": "^2.4.40",
"@formatjs/intl-numberformat": "^7.2.5",
"@formatjs/intl-pluralrules": "^4.1.5",
"@formatjs/intl-relativetimeformat": "^9.3.2",
"@formatjs/intl-utils": "^3.8.4",
"@fullcalendar/common": "5.9.0",
"@fullcalendar/core": "5.9.0",
@@ -46,48 +46,38 @@
"@fullcalendar/interaction": "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",
"@material/chips": "14.0.0-canary.353ca7e9f.0",
"@material/data-table": "14.0.0-canary.353ca7e9f.0",
"@material/mwc-button": "0.25.2",
"@material/mwc-checkbox": "0.25.2",
"@material/mwc-circular-progress": "0.25.2",
"@material/mwc-dialog": "0.25.2",
"@material/mwc-fab": "0.25.2",
"@material/mwc-formfield": "0.25.2",
"@material/mwc-icon-button": "0.25.2",
"@material/mwc-linear-progress": "0.25.2",
"@material/mwc-list": "0.25.2",
"@material/mwc-menu": "0.25.2",
"@material/mwc-radio": "0.25.2",
"@material/mwc-ripple": "0.25.2",
"@material/mwc-select": "0.25.2",
"@material/mwc-slider": "0.25.2",
"@material/mwc-switch": "0.25.2",
"@material/mwc-tab": "0.25.2",
"@material/mwc-tab-bar": "0.25.2",
"@material/mwc-textfield": "0.25.2",
"@material/top-app-bar": "14.0.0-canary.353ca7e9f.0",
"@mdi/js": "6.2.95",
"@mdi/svg": "6.2.95",
"@material/chips": "14.0.0-canary.261f2db59.0",
"@material/data-table": "14.0.0-canary.261f2db59.0",
"@material/mwc-button": "0.25.3",
"@material/mwc-checkbox": "0.25.3",
"@material/mwc-circular-progress": "0.25.3",
"@material/mwc-dialog": "0.25.3",
"@material/mwc-fab": "0.25.3",
"@material/mwc-formfield": "0.25.3",
"@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.3",
"@material/mwc-list": "0.25.3",
"@material/mwc-menu": "0.25.3",
"@material/mwc-radio": "0.25.3",
"@material/mwc-ripple": "0.25.3",
"@material/mwc-select": "0.25.3",
"@material/mwc-slider": "0.25.3",
"@material/mwc-switch": "0.25.3",
"@material/mwc-tab": "0.25.3",
"@material/mwc-tab-bar": "0.25.3",
"@material/mwc-textfield": "0.25.3",
"@material/top-app-bar": "14.0.0-canary.261f2db59.0",
"@mdi/js": "6.4.95",
"@mdi/svg": "6.4.95",
"@polymer/app-layout": "^3.1.0",
"@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1",
"@polymer/iron-input": "^3.0.1",
"@polymer/iron-overlay-behavior": "^3.0.3",
"@polymer/iron-resizable-behavior": "^3.0.1",
"@polymer/paper-checkbox": "^3.1.0",
"@polymer/paper-dialog": "^3.0.1",
"@polymer/paper-dialog-behavior": "^3.0.1",
"@polymer/paper-dialog-scrollable": "^3.0.1",
"@polymer/paper-dropdown-menu": "^3.2.0",
"@polymer/paper-input": "^3.2.1",
"@polymer/paper-item": "^3.0.1",
"@polymer/paper-listbox": "^3.0.1",
"@polymer/paper-menu-button": "^3.1.0",
"@polymer/paper-progress": "^3.0.1",
"@polymer/paper-radio-button": "^3.0.1",
"@polymer/paper-radio-group": "^3.0.1",
"@polymer/paper-ripple": "^3.0.2",
"@polymer/paper-slider": "^3.0.1",
"@polymer/paper-styles": "^3.0.1",
"@polymer/paper-tabs": "^3.1.0",
@@ -118,7 +108,7 @@
"js-yaml": "^4.1.0",
"leaflet": "^1.7.1",
"leaflet-draw": "^1.0.4",
"lit": "^2.0.0",
"lit": "^2.0.2",
"lit-vaadin-helpers": "^0.2.1",
"marked": "^3.0.2",
"memoize-one": "^5.2.1",
@@ -190,7 +180,7 @@
"eslint-import-resolver-webpack": "^0.13.1",
"eslint-plugin-disable": "^2.0.1",
"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-unused-imports": "^1.1.5",
"eslint-plugin-wc": "^1.3.2",
@@ -240,10 +230,10 @@
"resolutions": {
"@polymer/polymer": "patch:@polymer/polymer@3.4.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
"@webcomponents/webcomponentsjs": "^2.2.10",
"lit": "^2.0.0",
"lit-html": "2.0.0",
"lit-element": "3.0.0",
"@lit/reactive-element": "1.0.0"
"lit": "^2.0.2",
"lit-html": "2.0.1",
"lit-element": "3.0.1",
"@lit/reactive-element": "1.0.1"
},
"main": "src/home-assistant.js",
"husky": {
+1 -1
View File
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20211014.0",
version="20211026.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/frontend",
author="The Home Assistant Authors",
+33 -1
View File
@@ -7,9 +7,10 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import "./ha-password-manager-polyfill";
import { property, state } from "lit/decorators";
import "../components/ha-checkbox";
import "../components/ha-form/ha-form";
import "../components/ha-formfield";
import "../components/ha-markdown";
import "../components/ha-alert";
import { AuthProvider } from "../data/auth";
@@ -19,6 +20,7 @@ import {
} from "../data/data_entry_flow";
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";
@@ -41,6 +43,8 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
@state() private _submitting = false;
@state() private _storeToken = false;
willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
@@ -170,6 +174,9 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
}
private _renderStep(step: DataEntryFlowStep): TemplateResult {
const clientIdOrigin = this.clientId
? new URL(this.clientId).origin
: undefined;
switch (step.type) {
case "abort":
return html`
@@ -201,12 +208,29 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
.computeError=${this._computeErrorCallback(step)}
@value-changed=${this._stepDataChanged}
></ha-form>
${clientIdOrigin === window.location.origin && step.step_id !== "mfa"
? 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:
return html``;
}
}
private _storeTokenChanged(e: CustomEvent<HTMLInputElement>) {
this._storeToken = (e.currentTarget as HTMLInputElement).checked;
}
private async _providerChanged(newProvider?: AuthProvider) {
if (this._step && this._step.type === "form") {
fetch(`/auth/login_flow/${this._step.flow_id}`, {
@@ -274,6 +298,9 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
if (this.oauth2State) {
url += `&state=${encodeURIComponent(this.oauth2State)}`;
}
if (this._storeToken) {
url += `&storeToken=true`;
}
document.location.assign(url);
}
@@ -357,6 +384,11 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
margin: 24px 0 8px;
text-align: center;
}
/* Align with the rest of the form. */
.store-token {
margin-top: 10px;
margin-left: -16px;
}
`;
}
}
+8 -1
View File
@@ -30,6 +30,14 @@ export function askWrite() {
export function saveTokens(tokens: AuthData | null) {
tokenCache.tokens = tokens;
if (
!tokenCache.writeEnabled &&
new URLSearchParams(window.location.search).get("storeToken") === "true"
) {
tokenCache.writeEnabled = true;
}
if (tokenCache.writeEnabled) {
try {
storage.hassTokens = JSON.stringify(tokens);
@@ -45,7 +53,6 @@ export function enableWrite() {
saveTokens(tokenCache.tokens);
}
}
export function loadTokens() {
if (tokenCache.tokens === undefined) {
try {
+130 -72
View File
@@ -1,88 +1,146 @@
/** Constants to be used in the frontend. */
import {
mdiAccount,
mdiAirFilter,
mdiAlert,
mdiAngleAcute,
mdiAppleSafari,
mdiBell,
mdiBookmark,
mdiBrightness5,
mdiBullhorn,
mdiCalendar,
mdiCalendarClock,
mdiCash,
mdiClock,
mdiCloudUpload,
mdiCog,
mdiCommentAlert,
mdiCounter,
mdiCurrentAc,
mdiEye,
mdiFan,
mdiFlash,
mdiFlower,
mdiFormatListBulleted,
mdiFormTextbox,
mdiGasCylinder,
mdiGauge,
mdiGoogleAssistant,
mdiGoogleCirclesCommunities,
mdiHomeAssistant,
mdiHomeAutomation,
mdiImageFilterFrames,
mdiLightbulb,
mdiLightningBolt,
mdiMailbox,
mdiMapMarkerRadius,
mdiMolecule,
mdiMoleculeCo,
mdiMoleculeCo2,
mdiPalette,
mdiRayVertex,
mdiRemote,
mdiRobot,
mdiRobotVacuum,
mdiScriptText,
mdiSineWave,
mdiTextToSpeech,
mdiThermometer,
mdiThermostat,
mdiTimerOutline,
mdiToggleSwitchOutline,
mdiVideo,
mdiWaterPercent,
mdiWeatherCloudy,
mdiWhiteBalanceSunny,
mdiWifi,
} from "@mdi/js";
// Constants should be alphabetically sorted by name.
// Arrays with values should be alphabetically sorted if order doesn't matter.
// Each constant should have a description what it is supposed to be used for.
/** Icon to use when no icon specified for domain. */
export const DEFAULT_DOMAIN_ICON = "hass:bookmark";
export const DEFAULT_DOMAIN_ICON = mdiBookmark;
/** Icons for each domain */
export const FIXED_DOMAIN_ICONS = {
alert: "hass:alert",
alexa: "hass:amazon-alexa",
air_quality: "hass:air-filter",
automation: "hass:robot",
calendar: "hass:calendar",
camera: "hass:video",
climate: "hass:thermostat",
configurator: "hass:cog",
conversation: "hass:text-to-speech",
counter: "hass:counter",
device_tracker: "hass:account",
fan: "hass:fan",
google_assistant: "hass:google-assistant",
group: "hass:google-circles-communities",
homeassistant: "hass:home-assistant",
homekit: "hass:home-automation",
image_processing: "hass:image-filter-frames",
input_boolean: "hass:toggle-switch-outline",
input_datetime: "hass:calendar-clock",
input_number: "hass:ray-vertex",
input_select: "hass:format-list-bulleted",
input_text: "hass:form-textbox",
light: "hass:lightbulb",
mailbox: "hass:mailbox",
notify: "hass:comment-alert",
number: "hass:ray-vertex",
persistent_notification: "hass:bell",
person: "hass:account",
plant: "hass:flower",
proximity: "hass:apple-safari",
remote: "hass:remote",
scene: "hass:palette",
script: "hass:script-text",
select: "hass:format-list-bulleted",
sensor: "hass:eye",
simple_alarm: "hass:bell",
sun: "hass:white-balance-sunny",
switch: "hass:flash",
timer: "hass:timer-outline",
updater: "hass:cloud-upload",
vacuum: "hass:robot-vacuum",
water_heater: "hass:thermometer",
weather: "hass:weather-cloudy",
zone: "hass:map-marker-radius",
alert: mdiAlert,
air_quality: mdiAirFilter,
automation: mdiRobot,
calendar: mdiCalendar,
camera: mdiVideo,
climate: mdiThermostat,
configurator: mdiCog,
conversation: mdiTextToSpeech,
counter: mdiCounter,
device_tracker: mdiAccount,
fan: mdiFan,
google_assistant: mdiGoogleAssistant,
group: mdiGoogleCirclesCommunities,
homeassistant: mdiHomeAssistant,
homekit: mdiHomeAutomation,
image_processing: mdiImageFilterFrames,
input_boolean: mdiToggleSwitchOutline,
input_datetime: mdiCalendarClock,
input_number: mdiRayVertex,
input_select: mdiFormatListBulleted,
input_text: mdiFormTextbox,
light: mdiLightbulb,
mailbox: mdiMailbox,
notify: mdiCommentAlert,
number: mdiRayVertex,
persistent_notification: mdiBell,
person: mdiAccount,
plant: mdiFlower,
proximity: mdiAppleSafari,
remote: mdiRemote,
scene: mdiPalette,
script: mdiScriptText,
select: mdiFormatListBulleted,
sensor: mdiEye,
siren: mdiBullhorn,
simple_alarm: mdiBell,
sun: mdiWhiteBalanceSunny,
switch: mdiFlash,
timer: mdiTimerOutline,
updater: mdiCloudUpload,
vacuum: mdiRobotVacuum,
water_heater: mdiThermometer,
weather: mdiWeatherCloudy,
zone: mdiMapMarkerRadius,
};
export const FIXED_DEVICE_CLASS_ICONS = {
aqi: "hass:air-filter",
// battery: "hass:battery", => not included by design since `sensorIcon()` will dynamically determine the icon
carbon_dioxide: "mdi:molecule-co2",
carbon_monoxide: "mdi:molecule-co",
current: "hass:current-ac",
date: "hass:calendar",
energy: "hass:lightning-bolt",
gas: "hass:gas-cylinder",
humidity: "hass:water-percent",
illuminance: "hass:brightness-5",
monetary: "mdi:cash",
nitrogen_dioxide: "mdi:molecule",
nitrogen_monoxide: "mdi:molecule",
nitrous_oxide: "mdi:molecule",
ozone: "mdi:molecule",
pm1: "mdi:molecule",
pm10: "mdi:molecule",
pm25: "mdi:molecule",
power: "hass:flash",
power_factor: "hass:angle-acute",
pressure: "hass:gauge",
signal_strength: "hass:wifi",
sulphur_dioxide: "mdi:molecule",
temperature: "hass:thermometer",
timestamp: "hass:clock",
volatile_organic_compounds: "mdi:molecule",
voltage: "hass:sine-wave",
aqi: mdiAirFilter,
// battery: mdiBattery, => not included by design since `sensorIcon()` will dynamically determine the icon
carbon_dioxide: mdiMoleculeCo2,
carbon_monoxide: mdiMoleculeCo,
current: mdiCurrentAc,
date: mdiCalendar,
energy: mdiLightningBolt,
gas: mdiGasCylinder,
humidity: mdiWaterPercent,
illuminance: mdiBrightness5,
monetary: mdiCash,
nitrogen_dioxide: mdiMolecule,
nitrogen_monoxide: mdiMolecule,
nitrous_oxide: mdiMolecule,
ozone: mdiMolecule,
pm1: mdiMolecule,
pm10: mdiMolecule,
pm25: mdiMolecule,
power: mdiFlash,
power_factor: mdiAngleAcute,
pressure: mdiGauge,
signal_strength: mdiWifi,
sulphur_dioxide: mdiMolecule,
temperature: mdiThermometer,
timestamp: mdiClock,
volatile_organic_compounds: mdiMolecule,
voltage: mdiSineWave,
};
/** Domains that have a state card. */
+53 -46
View File
@@ -36,55 +36,62 @@ export const applyThemesOnElement = (
let cacheKey = selectedTheme;
let themeRules: Partial<ThemeVars> = {};
if (themeSettings) {
if (themeSettings.dark) {
cacheKey = `${cacheKey}__dark`;
themeRules = { ...darkStyles };
// If there is no explicitly desired dark mode provided, we automatically
// use the active one from hass.themes.
if (!themeSettings || themeSettings?.dark === undefined) {
themeSettings = {
...themeSettings,
dark: themes.darkMode,
};
}
if (themeSettings.dark) {
cacheKey = `${cacheKey}__dark`;
themeRules = { ...darkStyles };
}
if (selectedTheme === "default") {
// Determine the primary and accent colors from the current settings.
// Fallbacks are implicitly the HA default blue and orange or the
// derived "darkStyles" values, depending on the light vs dark mode.
const primaryColor = themeSettings.primaryColor;
const accentColor = themeSettings.accentColor;
if (themeSettings.dark && primaryColor) {
themeRules["app-header-background-color"] = hexBlend(
primaryColor,
"#121212",
8
);
}
if (selectedTheme === "default") {
// Determine the primary and accent colors from the current settings.
// Fallbacks are implicitly the HA default blue and orange or the
// derived "darkStyles" values, depending on the light vs dark mode.
const primaryColor = themeSettings.primaryColor;
const accentColor = themeSettings.accentColor;
if (primaryColor) {
cacheKey = `${cacheKey}__primary_${primaryColor}`;
const rgbPrimaryColor = hex2rgb(primaryColor);
const labPrimaryColor = rgb2lab(rgbPrimaryColor);
themeRules["primary-color"] = primaryColor;
const rgbLightPrimaryColor = lab2rgb(labBrighten(labPrimaryColor));
themeRules["light-primary-color"] = rgb2hex(rgbLightPrimaryColor);
themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor));
themeRules["text-primary-color"] =
rgbContrast(rgbPrimaryColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
themeRules["text-light-primary-color"] =
rgbContrast(rgbLightPrimaryColor, [33, 33, 33]) < 6
? "#fff"
: "#212121";
themeRules["state-icon-color"] = themeRules["dark-primary-color"];
}
if (accentColor) {
cacheKey = `${cacheKey}__accent_${accentColor}`;
themeRules["accent-color"] = accentColor;
const rgbAccentColor = hex2rgb(accentColor);
themeRules["text-accent-color"] =
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
}
if (themeSettings.dark && primaryColor) {
themeRules["app-header-background-color"] = hexBlend(
primaryColor,
"#121212",
8
);
}
if (primaryColor) {
cacheKey = `${cacheKey}__primary_${primaryColor}`;
const rgbPrimaryColor = hex2rgb(primaryColor);
const labPrimaryColor = rgb2lab(rgbPrimaryColor);
themeRules["primary-color"] = primaryColor;
const rgbLightPrimaryColor = lab2rgb(labBrighten(labPrimaryColor));
themeRules["light-primary-color"] = rgb2hex(rgbLightPrimaryColor);
themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor));
themeRules["text-primary-color"] =
rgbContrast(rgbPrimaryColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
themeRules["text-light-primary-color"] =
rgbContrast(rgbLightPrimaryColor, [33, 33, 33]) < 6
? "#fff"
: "#212121";
themeRules["state-icon-color"] = themeRules["dark-primary-color"];
}
if (accentColor) {
cacheKey = `${cacheKey}__accent_${accentColor}`;
themeRules["accent-color"] = accentColor;
const rgbAccentColor = hex2rgb(accentColor);
themeRules["text-accent-color"] =
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
}
// Nothing was changed
if (element._themes?.cacheKey === cacheKey) {
return;
}
// Nothing was changed
if (element._themes?.cacheKey === cacheKey) {
return;
}
}
+21 -9
View File
@@ -1,24 +1,36 @@
/** Return an icon representing a alarm panel state. */
import {
mdiShieldLock,
mdiShieldAirplane,
mdiShieldHome,
mdiShieldMoon,
mdiSecurity,
mdiShieldOutline,
mdiBellRing,
mdiShieldOff,
mdiShield,
} from "@mdi/js";
export const alarmPanelIcon = (state?: string) => {
switch (state) {
case "armed_away":
return "hass:shield-lock";
return mdiShieldLock;
case "armed_vacation":
return "hass:shield-airplane";
return mdiShieldAirplane;
case "armed_home":
return "hass:shield-home";
return mdiShieldHome;
case "armed_night":
return "hass:shield-moon";
return mdiShieldMoon;
case "armed_custom_bypass":
return "hass:security";
return mdiSecurity;
case "pending":
return "hass:shield-outline";
return mdiShieldOutline;
case "triggered":
return "hass:bell-ring";
return mdiBellRing;
case "disarmed":
return "hass:shield-off";
return mdiShieldOff;
default:
return "hass:shield";
return mdiShield;
}
};
+83 -26
View File
@@ -1,35 +1,92 @@
/** Return an icon representing a battery state. */
import {
mdiBattery,
mdiBattery10,
mdiBattery20,
mdiBattery30,
mdiBattery40,
mdiBattery50,
mdiBattery60,
mdiBattery70,
mdiBattery80,
mdiBattery90,
mdiBatteryAlert,
mdiBatteryAlertVariantOutline,
mdiBatteryCharging,
mdiBatteryCharging10,
mdiBatteryCharging20,
mdiBatteryCharging30,
mdiBatteryCharging40,
mdiBatteryCharging50,
mdiBatteryCharging60,
mdiBatteryCharging70,
mdiBatteryCharging80,
mdiBatteryCharging90,
mdiBatteryChargingOutline,
mdiBatteryUnknown,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
export const batteryIcon = (
const BATTERY_ICONS = {
10: mdiBattery10,
20: mdiBattery20,
30: mdiBattery30,
40: mdiBattery40,
50: mdiBattery50,
60: mdiBattery60,
70: mdiBattery70,
80: mdiBattery80,
90: mdiBattery90,
100: mdiBattery,
};
const BATTERY_CHARGING_ICONS = {
10: mdiBatteryCharging10,
20: mdiBatteryCharging20,
30: mdiBatteryCharging30,
40: mdiBatteryCharging40,
50: mdiBatteryCharging50,
60: mdiBatteryCharging60,
70: mdiBatteryCharging70,
80: mdiBatteryCharging80,
90: mdiBatteryCharging90,
100: mdiBatteryCharging,
};
export const batteryStateIcon = (
batteryState: HassEntity,
batteryChargingState?: HassEntity
) => {
const battery = Number(batteryState.state);
const battery_charging =
const battery = batteryState.state;
const batteryCharging =
batteryChargingState && batteryChargingState.state === "on";
let icon = "hass:battery";
if (isNaN(battery)) {
if (batteryState.state === "off") {
icon += "-full";
} else if (batteryState.state === "on") {
icon += "-alert";
} else {
icon += "-unknown";
}
return icon;
}
const batteryRound = Math.round(battery / 10) * 10;
if (battery_charging && battery > 10) {
icon += `-charging-${batteryRound}`;
} else if (battery_charging) {
icon += "-outline";
} else if (battery <= 5) {
icon += "-alert";
} else if (battery > 5 && battery < 95) {
icon += `-${batteryRound}`;
}
return icon;
return batteryIcon(battery, batteryCharging);
};
export const batteryIcon = (
batteryState: number | string,
batteryCharging?: boolean
) => {
const batteryValue = Number(batteryState);
if (isNaN(batteryValue)) {
if (batteryState === "off") {
return mdiBattery;
}
if (batteryState === "on") {
return mdiBatteryAlert;
}
return mdiBatteryUnknown;
}
const batteryRound = Math.round(batteryValue / 10) * 10;
if (batteryCharging && batteryValue >= 10) {
return BATTERY_CHARGING_ICONS[batteryRound];
}
if (batteryCharging) {
return mdiBatteryChargingOutline;
}
if (batteryValue <= 5) {
return mdiBatteryAlertVariantOutline;
}
return BATTERY_ICONS[batteryRound];
};
+68 -23
View File
@@ -1,3 +1,46 @@
import {
mdiAlertCircle,
mdiBattery,
mdiBatteryCharging,
mdiBatteryOutline,
mdiBrightness5,
mdiBrightness7,
mdiCheckboxMarkedCircle,
mdiCheckCircle,
mdiCropPortrait,
mdiDoorClosed,
mdiDoorOpen,
mdiFire,
mdiGarage,
mdiGarageOpen,
mdiHome,
mdiHomeOutline,
mdiLock,
mdiLockOpen,
mdiMusicNote,
mdiMusicNoteOff,
mdiPackage,
mdiPackageUp,
mdiPlay,
mdiPowerPlug,
mdiPowerPlugOff,
mdiRadioboxBlank,
mdiRun,
mdiServerNetwork,
mdiServerNetworkOff,
mdiSmoke,
mdiSnowflake,
mdiSquare,
mdiSquareOutline,
mdiStop,
mdiThermometer,
mdiVibrate,
mdiWalk,
mdiWater,
mdiWaterOff,
mdiWindowClosed,
mdiWindowOpen,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
/** Return an icon representing a binary sensor state. */
@@ -6,53 +49,55 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
const is_off = state === "off";
switch (stateObj?.attributes.device_class) {
case "battery":
return is_off ? "hass:battery" : "hass:battery-outline";
return is_off ? mdiBattery : mdiBatteryOutline;
case "battery_charging":
return is_off ? "hass:battery" : "hass:battery-charging";
return is_off ? mdiBattery : mdiBatteryCharging;
case "cold":
return is_off ? "hass:thermometer" : "hass:snowflake";
return is_off ? mdiThermometer : mdiSnowflake;
case "connectivity":
return is_off ? "hass:server-network-off" : "hass:server-network";
return is_off ? mdiServerNetworkOff : mdiServerNetwork;
case "door":
return is_off ? "hass:door-closed" : "hass:door-open";
return is_off ? mdiDoorClosed : mdiDoorOpen;
case "garage_door":
return is_off ? "hass:garage" : "hass:garage-open";
return is_off ? mdiGarage : mdiGarageOpen;
case "power":
return is_off ? "hass:power-plug-off" : "hass:power-plug";
return is_off ? mdiPowerPlugOff : mdiPowerPlug;
case "gas":
case "problem":
case "safety":
case "tamper":
return is_off ? "hass:check-circle" : "hass:alert-circle";
return is_off ? mdiCheckCircle : mdiAlertCircle;
case "smoke":
return is_off ? "hass:check-circle" : "hass:smoke";
return is_off ? mdiCheckCircle : mdiSmoke;
case "heat":
return is_off ? "hass:thermometer" : "hass:fire";
return is_off ? mdiThermometer : mdiFire;
case "light":
return is_off ? "hass:brightness-5" : "hass:brightness-7";
return is_off ? mdiBrightness5 : mdiBrightness7;
case "lock":
return is_off ? "hass:lock" : "hass:lock-open";
return is_off ? mdiLock : mdiLockOpen;
case "moisture":
return is_off ? "hass:water-off" : "hass:water";
return is_off ? mdiWaterOff : mdiWater;
case "motion":
return is_off ? "hass:walk" : "hass:run";
return is_off ? mdiWalk : mdiRun;
case "occupancy":
return is_off ? "hass:home-outline" : "hass:home";
return is_off ? mdiHomeOutline : mdiHome;
case "opening":
return is_off ? "hass:square" : "hass:square-outline";
return is_off ? mdiSquare : mdiSquareOutline;
case "plug":
return is_off ? "hass:power-plug-off" : "hass:power-plug";
return is_off ? mdiPowerPlugOff : mdiPowerPlug;
case "presence":
return is_off ? "hass:home-outline" : "hass:home";
return is_off ? mdiHomeOutline : mdiHome;
case "running":
return is_off ? mdiStop : mdiPlay;
case "sound":
return is_off ? "hass:music-note-off" : "hass:music-note";
return is_off ? mdiMusicNoteOff : mdiMusicNote;
case "update":
return is_off ? "mdi:package" : "mdi:package-up";
return is_off ? mdiPackage : mdiPackageUp;
case "vibration":
return is_off ? "hass:crop-portrait" : "hass:vibrate";
return is_off ? mdiCropPortrait : mdiVibrate;
case "window":
return is_off ? "hass:window-closed" : "hass:window-open";
return is_off ? mdiWindowClosed : mdiWindowOpen;
default:
return is_off ? "hass:radiobox-blank" : "hass:checkbox-marked-circle";
return is_off ? mdiRadioboxBlank : mdiCheckboxMarkedCircle;
}
};
+15 -13
View File
@@ -39,7 +39,7 @@ export const computeStateDisplay = (
const domain = computeStateDomain(stateObj);
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.
// Attributes aren't available, we have to use `state`.
try {
@@ -63,7 +63,7 @@ export const computeStateDisplay = (
}
}
return state;
} catch {
} catch (_e) {
// Formatting methods may throw error if date parsing doesn't go well,
// just return the state string in that case.
return state;
@@ -71,7 +71,17 @@ export const computeStateDisplay = (
} else {
// If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format.
let date: Date;
if (!stateObj.attributes.has_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(
stateObj.attributes.year,
stateObj.attributes.month - 1,
@@ -79,20 +89,12 @@ export const computeStateDisplay = (
);
return formatDate(date, locale);
}
if (!stateObj.attributes.has_date) {
if (stateObj.attributes.has_time) {
date = new Date();
date.setHours(stateObj.attributes.hour, stateObj.attributes.minute);
return formatTime(date, locale);
}
date = new Date(
stateObj.attributes.year,
stateObj.attributes.month - 1,
stateObj.attributes.day,
stateObj.attributes.hour,
stateObj.attributes.minute
);
return formatDateTime(date, locale);
return stateObj.state;
}
}
+66 -30
View File
@@ -1,4 +1,30 @@
/** Return an icon representing a cover state. */
import {
mdiArrowUpBox,
mdiArrowDownBox,
mdiGarage,
mdiGarageOpen,
mdiGateArrowRight,
mdiGate,
mdiGateOpen,
mdiDoorOpen,
mdiDoorClosed,
mdiCircle,
mdiWindowShutter,
mdiWindowShutterOpen,
mdiBlinds,
mdiBlindsOpen,
mdiWindowClosed,
mdiWindowOpen,
mdiArrowExpandHorizontal,
mdiArrowUp,
mdiArrowCollapseHorizontal,
mdiArrowDown,
mdiCircleSlice8,
mdiArrowSplitVertical,
mdiCurtains,
mdiCurtainsClosed,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
export const coverIcon = (state?: string, stateObj?: HassEntity): string => {
@@ -8,74 +34,84 @@ export const coverIcon = (state?: string, stateObj?: HassEntity): string => {
case "garage":
switch (state) {
case "opening":
return "hass:arrow-up-box";
return mdiArrowUpBox;
case "closing":
return "hass:arrow-down-box";
return mdiArrowDownBox;
case "closed":
return "hass:garage";
return mdiGarage;
default:
return "hass:garage-open";
return mdiGarageOpen;
}
case "gate":
switch (state) {
case "opening":
case "closing":
return "hass:gate-arrow-right";
return mdiGateArrowRight;
case "closed":
return "hass:gate";
return mdiGate;
default:
return "hass:gate-open";
return mdiGateOpen;
}
case "door":
return open ? "hass:door-open" : "hass:door-closed";
return open ? mdiDoorOpen : mdiDoorClosed;
case "damper":
return open ? "hass:circle" : "hass:circle-slice-8";
return open ? mdiCircle : mdiCircleSlice8;
case "shutter":
switch (state) {
case "opening":
return "hass:arrow-up-box";
return mdiArrowUpBox;
case "closing":
return "hass:arrow-down-box";
return mdiArrowDownBox;
case "closed":
return "hass:window-shutter";
return mdiWindowShutter;
default:
return "hass:window-shutter-open";
return mdiWindowShutterOpen;
}
case "curtain":
switch (state) {
case "opening":
return mdiArrowSplitVertical;
case "closing":
return mdiArrowCollapseHorizontal;
case "closed":
return mdiCurtainsClosed;
default:
return mdiCurtains;
}
case "blind":
case "curtain":
case "shade":
switch (state) {
case "opening":
return "hass:arrow-up-box";
return mdiArrowUpBox;
case "closing":
return "hass:arrow-down-box";
return mdiArrowDownBox;
case "closed":
return "hass:blinds";
return mdiBlinds;
default:
return "hass:blinds-open";
return mdiBlindsOpen;
}
case "window":
switch (state) {
case "opening":
return "hass:arrow-up-box";
return mdiArrowUpBox;
case "closing":
return "hass:arrow-down-box";
return mdiArrowDownBox;
case "closed":
return "hass:window-closed";
return mdiWindowClosed;
default:
return "hass:window-open";
return mdiWindowOpen;
}
}
switch (state) {
case "opening":
return "hass:arrow-up-box";
return mdiArrowUpBox;
case "closing":
return "hass:arrow-down-box";
return mdiArrowDownBox;
case "closed":
return "hass:window-closed";
return mdiWindowClosed;
default:
return "hass:window-open";
return mdiWindowOpen;
}
};
@@ -84,9 +120,9 @@ export const computeOpenIcon = (stateObj: HassEntity): string => {
case "awning":
case "door":
case "gate":
return "hass:arrow-expand-horizontal";
return mdiArrowExpandHorizontal;
default:
return "hass:arrow-up";
return mdiArrowUp;
}
};
@@ -95,8 +131,8 @@ export const computeCloseIcon = (stateObj: HassEntity): string => {
case "awning":
case "door":
case "gate":
return "hass:arrow-collapse-horizontal";
return mdiArrowCollapseHorizontal;
default:
return "hass:arrow-down";
return mdiArrowDown;
}
};
+30 -15
View File
@@ -1,3 +1,20 @@
import {
mdiAirHumidifierOff,
mdiAirHumidifier,
mdiLockOpen,
mdiLockAlert,
mdiLockClock,
mdiLock,
mdiCastConnected,
mdiCast,
mdiEmoticonDead,
mdiSleep,
mdiTimerSand,
mdiZWave,
mdiClock,
mdiCalendar,
mdiWeatherNight,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
/**
* Return the icon to be used for a domain.
@@ -28,36 +45,34 @@ export const domainIcon = (
return coverIcon(compareState, stateObj);
case "humidifier":
return state && state === "off"
? "hass:air-humidifier-off"
: "hass:air-humidifier";
return state && state === "off" ? mdiAirHumidifierOff : mdiAirHumidifier;
case "lock":
switch (compareState) {
case "unlocked":
return "hass:lock-open";
return mdiLockOpen;
case "jammed":
return "hass:lock-alert";
return mdiLockAlert;
case "locking":
case "unlocking":
return "hass:lock-clock";
return mdiLockClock;
default:
return "hass:lock";
return mdiLock;
}
case "media_player":
return compareState === "playing" ? "hass:cast-connected" : "hass:cast";
return compareState === "playing" ? mdiCastConnected : mdiCast;
case "zwave":
switch (compareState) {
case "dead":
return "hass:emoticon-dead";
return mdiEmoticonDead;
case "sleeping":
return "hass:sleep";
return mdiSleep;
case "initializing":
return "hass:timer-sand";
return mdiTimerSand;
default:
return "hass:z-wave";
return mdiZWave;
}
case "sensor": {
@@ -71,17 +86,17 @@ export const domainIcon = (
case "input_datetime":
if (!stateObj?.attributes.has_date) {
return "hass:clock";
return mdiClock;
}
if (!stateObj.attributes.has_time) {
return "hass:calendar";
return mdiCalendar;
}
break;
case "sun":
return stateObj?.state === "above_horizon"
? FIXED_DOMAIN_ICONS[domain]
: "hass:weather-night";
: mdiWeatherNight;
}
if (domain in FIXED_DOMAIN_ICONS) {
+5 -4
View File
@@ -1,8 +1,9 @@
/** Return an icon representing a sensor state. */
import { mdiBattery, mdiThermometer } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { FIXED_DEVICE_CLASS_ICONS, UNIT_C, UNIT_F } from "../const";
import { batteryIcon } from "./battery_icon";
import { SENSOR_DEVICE_CLASS_BATTERY } from "../../data/sensor";
import { FIXED_DEVICE_CLASS_ICONS, UNIT_C, UNIT_F } from "../const";
import { batteryStateIcon } from "./battery_icon";
export const sensorIcon = (stateObj?: HassEntity): string | undefined => {
const dclass = stateObj?.attributes.device_class;
@@ -12,12 +13,12 @@ export const sensorIcon = (stateObj?: HassEntity): string | undefined => {
}
if (dclass === SENSOR_DEVICE_CLASS_BATTERY) {
return stateObj ? batteryIcon(stateObj) : "hass:battery";
return stateObj ? batteryStateIcon(stateObj) : mdiBattery;
}
const unit = stateObj?.attributes.unit_of_measurement;
if (unit === UNIT_C || unit === UNIT_F) {
return "hass:thermometer";
return mdiThermometer;
}
return undefined;
@@ -4,13 +4,9 @@ import { DEFAULT_DOMAIN_ICON } from "../const";
import { computeDomain } from "./compute_domain";
import { domainIcon } from "./domain_icon";
export const stateIcon = (state?: HassEntity) => {
export const stateIconPath = (state?: HassEntity) => {
if (!state) {
return DEFAULT_DOMAIN_ICON;
}
if (state.attributes.icon) {
return state.attributes.icon;
}
return domainIcon(computeDomain(state.entity_id), state);
};
@@ -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;
+4 -4
View File
@@ -12,8 +12,8 @@ export const slugify = (value: string, delimiter = "_") => {
.replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters
.replace(/&/g, `${delimiter}and${delimiter}`) // Replace & with 'and'
.replace(/[^\w-]+/g, "") // Remove all non-word characters
.replace(/-/, delimiter) // Replace - with delimiter
.replace(new RegExp(`/${delimiter}${delimiter}+/`, "g"), delimiter) // Replace multiple delimiters with single delimiter
.replace(new RegExp(`/^${delimiter}+/`), "") // Trim delimiter from start of text
.replace(new RegExp(`/-+$/`), ""); // Trim delimiter from end of text
.replace(/-/g, delimiter) // Replace - with 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 end of text
};
+35 -35
View File
@@ -1,57 +1,57 @@
import { css } from "lit";
export const iconColorCSS = css`
ha-icon[data-domain="alert"][data-state="on"],
ha-icon[data-domain="automation"][data-state="on"],
ha-icon[data-domain="binary_sensor"][data-state="on"],
ha-icon[data-domain="calendar"][data-state="on"],
ha-icon[data-domain="camera"][data-state="streaming"],
ha-icon[data-domain="cover"][data-state="open"],
ha-icon[data-domain="fan"][data-state="on"],
ha-icon[data-domain="humidifier"][data-state="on"],
ha-icon[data-domain="light"][data-state="on"],
ha-icon[data-domain="input_boolean"][data-state="on"],
ha-icon[data-domain="lock"][data-state="unlocked"],
ha-icon[data-domain="media_player"][data-state="on"],
ha-icon[data-domain="media_player"][data-state="paused"],
ha-icon[data-domain="media_player"][data-state="playing"],
ha-icon[data-domain="script"][data-state="on"],
ha-icon[data-domain="sun"][data-state="above_horizon"],
ha-icon[data-domain="switch"][data-state="on"],
ha-icon[data-domain="timer"][data-state="active"],
ha-icon[data-domain="vacuum"][data-state="cleaning"],
ha-icon[data-domain="group"][data-state="on"],
ha-icon[data-domain="group"][data-state="home"],
ha-icon[data-domain="group"][data-state="open"],
ha-icon[data-domain="group"][data-state="locked"],
ha-icon[data-domain="group"][data-state="problem"] {
ha-state-icon[data-domain="alert"][data-state="on"],
ha-state-icon[data-domain="automation"][data-state="on"],
ha-state-icon[data-domain="binary_sensor"][data-state="on"],
ha-state-icon[data-domain="calendar"][data-state="on"],
ha-state-icon[data-domain="camera"][data-state="streaming"],
ha-state-icon[data-domain="cover"][data-state="open"],
ha-state-icon[data-domain="fan"][data-state="on"],
ha-state-icon[data-domain="humidifier"][data-state="on"],
ha-state-icon[data-domain="light"][data-state="on"],
ha-state-icon[data-domain="input_boolean"][data-state="on"],
ha-state-icon[data-domain="lock"][data-state="unlocked"],
ha-state-icon[data-domain="media_player"][data-state="on"],
ha-state-icon[data-domain="media_player"][data-state="paused"],
ha-state-icon[data-domain="media_player"][data-state="playing"],
ha-state-icon[data-domain="script"][data-state="on"],
ha-state-icon[data-domain="sun"][data-state="above_horizon"],
ha-state-icon[data-domain="switch"][data-state="on"],
ha-state-icon[data-domain="timer"][data-state="active"],
ha-state-icon[data-domain="vacuum"][data-state="cleaning"],
ha-state-icon[data-domain="group"][data-state="on"],
ha-state-icon[data-domain="group"][data-state="home"],
ha-state-icon[data-domain="group"][data-state="open"],
ha-state-icon[data-domain="group"][data-state="locked"],
ha-state-icon[data-domain="group"][data-state="problem"] {
color: var(--paper-item-icon-active-color, #fdd835);
}
ha-icon[data-domain="climate"][data-state="cooling"] {
ha-state-icon[data-domain="climate"][data-state="cooling"] {
color: var(--cool-color, var(--state-climate-cool-color));
}
ha-icon[data-domain="climate"][data-state="heating"] {
ha-state-icon[data-domain="climate"][data-state="heating"] {
color: var(--heat-color, var(--state-climate-heat-color));
}
ha-icon[data-domain="climate"][data-state="drying"] {
ha-state-icon[data-domain="climate"][data-state="drying"] {
color: var(--dry-color, var(--state-climate-dry-color));
}
ha-icon[data-domain="alarm_control_panel"] {
ha-state-icon[data-domain="alarm_control_panel"] {
color: var(--alarm-color-armed, var(--label-badge-red));
}
ha-icon[data-domain="alarm_control_panel"][data-state="disarmed"] {
ha-state-icon[data-domain="alarm_control_panel"][data-state="disarmed"] {
color: var(--alarm-color-disarmed, var(--label-badge-green));
}
ha-icon[data-domain="alarm_control_panel"][data-state="pending"],
ha-icon[data-domain="alarm_control_panel"][data-state="arming"] {
ha-state-icon[data-domain="alarm_control_panel"][data-state="pending"],
ha-state-icon[data-domain="alarm_control_panel"][data-state="arming"] {
color: var(--alarm-color-pending, var(--label-badge-yellow));
animation: pulse 1s infinite;
}
ha-icon[data-domain="alarm_control_panel"][data-state="triggered"] {
ha-state-icon[data-domain="alarm_control_panel"][data-state="triggered"] {
color: var(--alarm-color-triggered, var(--label-badge-red));
animation: pulse 1s infinite;
}
@@ -68,13 +68,13 @@ export const iconColorCSS = css`
}
}
ha-icon[data-domain="plant"][data-state="problem"],
ha-icon[data-domain="zwave"][data-state="dead"] {
ha-state-icon[data-domain="plant"][data-state="problem"],
ha-state-icon[data-domain="zwave"][data-state="dead"] {
color: var(--state-icon-error-color);
}
/* Color the icon if unavailable */
ha-icon[data-state="unavailable"] {
ha-state-icon[data-state="unavailable"] {
color: var(--state-unavailable-color);
}
`;
@@ -12,22 +12,19 @@ import { HomeAssistant } from "../../types";
import "./ha-chart-base";
import type { TimeLineData } from "./timeline-chart/const";
/** Binary sensor device classes for which the static colors for on/off need to be inverted.
* List the ones were "off" = good or normal state = should be rendered "green".
/** Binary sensor device classes for which the static colors for on/off are NOT inverted.
* List the ones were "on" = good or normal state => should be rendered "green".
* Note: It is now a "not inverted" list (compared to the past) since we now have more inverted ones.
*/
const BINARY_SENSOR_DEVICE_CLASS_COLOR_INVERTED = new Set([
"battery",
"door",
"garage_door",
"gas",
"lock",
"motion",
"opening",
"problem",
"safety",
"smoke",
"tamper",
"window",
const BINARY_SENSOR_DEVICE_CLASS_COLOR_NOT_INVERTED = new Set([
"battery_charging",
"connectivity",
"light",
"moving",
"plug",
"power",
"presence",
"update",
]);
const STATIC_STATE_COLORS = new Set([
@@ -48,7 +45,7 @@ const invertOnOff = (entityState?: HassEntity) =>
entityState &&
computeDomain(entityState.entity_id) === "binary_sensor" &&
"device_class" in entityState.attributes &&
BINARY_SENSOR_DEVICE_CLASS_COLOR_INVERTED.has(
!BINARY_SENSOR_DEVICE_CLASS_COLOR_NOT_INVERTED.has(
entityState.attributes.device_class!
);
+66 -37
View File
@@ -70,7 +70,7 @@ export interface DataTableSortColumnData {
export interface DataTableColumnData extends DataTableSortColumnData {
title: TemplateResult | string;
type?: "numeric" | "icon" | "icon-button";
type?: "numeric" | "icon" | "icon-button" | "overflow-menu";
template?: <T>(data: any, row: T) => TemplateResult | string;
width?: string;
maxWidth?: string;
@@ -281,15 +281,13 @@ export class HaDataTable extends LitElement {
}
const sorted = key === this._sortColumn;
const classes = {
"mdc-data-table__header-cell--numeric": Boolean(
column.type === "numeric"
),
"mdc-data-table__header-cell--icon": Boolean(
column.type === "icon"
),
"mdc-data-table__header-cell--icon-button": Boolean(
column.type === "icon-button"
),
"mdc-data-table__header-cell--numeric":
column.type === "numeric",
"mdc-data-table__header-cell--icon": column.type === "icon",
"mdc-data-table__header-cell--icon-button":
column.type === "icon-button",
"mdc-data-table__header-cell--overflow-menu":
column.type === "overflow-menu",
sortable: Boolean(column.sortable),
"not-sorted": Boolean(column.sortable && !sorted),
grows: Boolean(column.grows),
@@ -405,14 +403,14 @@ export class HaDataTable extends LitElement {
<div
role="cell"
class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--numeric": Boolean(
column.type === "numeric"
),
"mdc-data-table__cell--icon": Boolean(
column.type === "icon"
),
"mdc-data-table__cell--numeric":
column.type === "numeric",
"mdc-data-table__cell--icon":
column.type === "icon",
"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),
forceLTR: Boolean(column.forceLTR),
})}"
@@ -747,10 +745,16 @@ export class HaDataTable extends LitElement {
text-align: right;
}
.mdc-data-table__cell--icon:first-child ha-icon {
.mdc-data-table__cell--icon:first-child ha-icon,
.mdc-data-table__cell--icon:first-child ha-state-icon,
.mdc-data-table__cell--icon:first-child ha-svg-icon {
margin-left: 8px;
}
:host([dir="rtl"]) .mdc-data-table__cell--icon:first-child ha-icon {
:host([dir="rtl"]) .mdc-data-table__cell--icon:first-child ha-icon,
:host([dir="rtl"])
.mdc-data-table__cell--icon:first-child
ha-state-icon,
:host([dir="rtl"]) .mdc-data-table__cell--icon:first-child ha-svg-icon {
margin-left: auto;
margin-right: 8px;
}
@@ -763,40 +767,65 @@ export class HaDataTable extends LitElement {
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__cell--icon-button {
width: 56px;
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 {
color: var(--secondary-text-color);
text-overflow: clip;
}
.mdc-data-table__header-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__cell--icon-button:first-child,
.mdc-data-table__header-cell--icon-button:last-child,
.mdc-data-table__cell--icon-button:last-child {
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 {
color: var(--secondary-text-color);
}
@@ -1,89 +0,0 @@
/**
@license
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/*
Fixes issue with not using shadow dom properly in iron-overlay-behavior/icon-focusables-helper.js
*/
import { IronFocusablesHelper } from "@polymer/iron-overlay-behavior/iron-focusables-helper";
import { dom } from "@polymer/polymer/lib/legacy/polymer.dom";
export const HaIronFocusablesHelper = {
/**
* Returns a sorted array of tabbable nodes, including the root node.
* It searches the tabbable nodes in the light and shadow dom of the chidren,
* sorting the result by tabindex.
* @param {!Node} node
* @return {!Array<!HTMLElement>}
*/
getTabbableNodes: function (node) {
const result = [];
// If there is at least one element with tabindex > 0, we need to sort
// the final array by tabindex.
const needsSortByTabIndex = this._collectTabbableNodes(node, result);
if (needsSortByTabIndex) {
return IronFocusablesHelper._sortByTabIndex(result);
}
return result;
},
/**
* Searches for nodes that are tabbable and adds them to the `result` array.
* Returns if the `result` array needs to be sorted by tabindex.
* @param {!Node} node The starting point for the search; added to `result`
* if tabbable.
* @param {!Array<!HTMLElement>} result
* @return {boolean}
* @private
*/
_collectTabbableNodes: function (node, result) {
// If not an element or not visible, no need to explore children.
if (
node.nodeType !== Node.ELEMENT_NODE ||
!IronFocusablesHelper._isVisible(node)
) {
return false;
}
const element = /** @type {!HTMLElement} */ (node);
const tabIndex = IronFocusablesHelper._normalizedTabIndex(element);
let needsSort = tabIndex > 0;
if (tabIndex >= 0) {
result.push(element);
}
// In ShadowDOM v1, tab order is affected by the order of distrubution.
// E.g. getTabbableNodes(#root) in ShadowDOM v1 should return [#A, #B];
// in ShadowDOM v0 tab order is not affected by the distrubution order,
// in fact getTabbableNodes(#root) returns [#B, #A].
// <div id="root">
// <!-- shadow -->
// <slot name="a">
// <slot name="b">
// <!-- /shadow -->
// <input id="A" slot="a">
// <input id="B" slot="b" tabindex="1">
// </div>
// TODO(valdrin) support ShadowDOM v1 when upgrading to Polymer v2.0.
let children;
if (element.localName === "content" || element.localName === "slot") {
children = dom(element).getDistributedNodes();
} else {
// /////////////////////////
// Use shadow root if possible, will check for distributed nodes.
// THIS IS THE CHANGED LINE
children = dom(element.shadowRoot || element.root || element).children;
// /////////////////////////
}
for (let i = 0; i < children.length; i++) {
// Ensure method is always invoked to collect tabbable children.
needsSort = this._collectTabbableNodes(children[i], result) || needsSort;
}
return needsSort;
},
};
-31
View File
@@ -1,31 +0,0 @@
import "@polymer/paper-dialog/paper-dialog";
import type { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class";
import type { Constructor } from "../../types";
import { HaIronFocusablesHelper } from "./ha-iron-focusables-helper";
const paperDialogClass = customElements.get(
"paper-dialog"
) as Constructor<PaperDialogElement>;
// behavior that will override existing iron-overlay-behavior and call the fixed implementation
const haTabFixBehaviorImpl = {
get _focusableNodes() {
return HaIronFocusablesHelper.getTabbableNodes(this);
},
};
// paper-dialog that uses the haTabFixBehaviorImpl behavior
// export class HaPaperDialog extends paperDialogClass {}
// @ts-ignore
export class HaPaperDialog
extends mixinBehaviors([haTabFixBehaviorImpl], paperDialogClass)
implements PaperDialogElement {}
declare global {
interface HTMLElementTagNameMap {
"ha-paper-dialog": HaPaperDialog;
}
}
// @ts-ignore
customElements.define("ha-paper-dialog", HaPaperDialog);
+14 -5
View File
@@ -1,7 +1,7 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { batteryIcon } from "../../common/entity/battery_icon";
import "../ha-icon";
import { batteryStateIcon } from "../../common/entity/battery_icon";
import "../ha-svg-icon";
@customElement("ha-battery-icon")
export class HaBatteryIcon extends LitElement {
@@ -11,9 +11,18 @@ export class HaBatteryIcon extends LitElement {
protected render() {
return html`
<ha-icon
.icon=${batteryIcon(this.batteryStateObj, this.batteryChargingStateObj)}
></ha-icon>
<ha-svg-icon
.path=${batteryStateIcon(
this.batteryStateObj,
this.batteryChargingStateObj
)}
></ha-svg-icon>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-battery-icon": HaBatteryIcon;
}
}
+24 -17
View File
@@ -14,19 +14,18 @@ import secondsToDuration from "../../common/datetime/seconds_to_duration";
import { computeStateDisplay } from "../../common/entity/compute_state_display";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { stateIcon } from "../../common/entity/state_icon";
import { formatNumber } from "../../common/number/format_number";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { timerTimeRemaining } from "../../data/timer";
import { HomeAssistant } from "../../types";
import "../ha-label-badge";
import "../ha-icon";
import "../ha-state-icon";
@customElement("ha-state-label-badge")
export class HaStateLabelBadge extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public state?: HassEntity;
@property({ attribute: false }) public state?: HassEntity;
@property() public name?: string;
@@ -69,16 +68,23 @@ export class HaStateLabelBadge extends LitElement {
`;
}
// Rendering priority inside badge:
// 1. Icon directly defined in badge config
// 2. Image directly defined in badge config
// 3. Image taken from entity picture
// 4. Icon determined via entity state
// 5. Value string as fallback
const domain = computeStateDomain(entityState);
const value = this._computeValue(domain, entityState);
const icon = this.icon ? this.icon : this._computeIcon(domain, entityState);
const showIcon = this.icon || this._computeShowIcon(domain, entityState);
const image = this.icon
? ""
: this.image
? this.image
: entityState.attributes.entity_picture_local ||
entityState.attributes.entity_picture;
const value =
!image && !showIcon ? this._computeValue(domain, entityState) : undefined;
return html`
<ha-label-badge
@@ -95,8 +101,13 @@ export class HaStateLabelBadge extends LitElement {
)}
.description=${this.name ?? computeStateName(entityState)}
>
${!image && icon ? html`<ha-icon .icon=${icon}></ha-icon>` : ""}
${value && (this.icon || !this.image)
${!image && showIcon
? html`<ha-state-icon
.icon=${this.icon}
.state=${entityState}
></ha-state-icon>`
: ""}
${value && !image && !showIcon
? html`<span class=${value && value.length > 4 ? "big" : ""}
>${value}</span
>`
@@ -144,9 +155,9 @@ export class HaStateLabelBadge extends LitElement {
}
}
private _computeIcon(domain: string, entityState: HassEntity) {
private _computeShowIcon(domain: string, entityState: HassEntity): boolean {
if (entityState.state === UNAVAILABLE) {
return null;
return false;
}
switch (domain) {
case "alarm_control_panel":
@@ -156,17 +167,13 @@ export class HaStateLabelBadge extends LitElement {
case "person":
case "scene":
case "sun":
return stateIcon(entityState);
return true;
case "timer":
return entityState.state === "active"
? "hass:timer-outline"
: "hass:timer-off-outline";
return true;
case "sensor":
return entityState.attributes.device_class === "moon__phase"
? stateIcon(entityState)
: null;
return entityState.attributes.device_class === "moon__phase";
default:
return null;
return false;
}
}
+10 -2
View File
@@ -102,7 +102,12 @@ export class HaStatisticPicker extends LitElement {
</style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<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="">
${item.name}
<span secondary
@@ -153,7 +158,10 @@ export class HaStatisticPicker extends LitElement {
const entityState = this.hass.states[meta.statistic_id];
if (!entityState) {
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;
}
+15 -16
View File
@@ -1,3 +1,4 @@
import { mdiAlert } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import {
css,
@@ -12,10 +13,9 @@ import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { computeActiveState } from "../../common/entity/compute_active_state";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { stateIcon } from "../../common/entity/state_icon";
import { iconColorCSS } from "../../common/style/icon_color_css";
import type { HomeAssistant } from "../../types";
import "../ha-icon";
import "../ha-state-icon";
export class StateBadge extends LitElement {
public hass?: HomeAssistant;
@@ -39,7 +39,7 @@ export class StateBadge extends LitElement {
// We either need a `stateObj` or one override
if (!stateObj && !this.overrideIcon && !this.overrideImage) {
return html`<div class="missing">
<ha-icon icon="hass:alert"></ha-icon>
<ha-svg-icon .path=${mdiAlert}></ha-svg-icon>
</div>`;
}
@@ -49,18 +49,17 @@ export class StateBadge extends LitElement {
const domain = stateObj ? computeStateDomain(stateObj) : undefined;
return html`
<ha-icon
style=${styleMap(this._iconStyle)}
data-domain=${ifDefined(
this.stateColor || (domain === "light" && this.stateColor !== false)
? domain
: undefined
)}
data-state=${stateObj ? computeActiveState(stateObj) : ""}
.icon=${this.overrideIcon || (stateObj ? stateIcon(stateObj) : "")}
></ha-icon>
`;
return html`<ha-state-icon
style=${styleMap(this._iconStyle)}
data-domain=${ifDefined(
this.stateColor || (domain === "light" && this.stateColor !== false)
? domain
: undefined
)}
data-state=${stateObj ? computeActiveState(stateObj) : ""}
.icon=${this.overrideIcon}
.state=${stateObj}
></ha-state-icon>`;
}
public willUpdate(changedProps: PropertyValues) {
@@ -154,7 +153,7 @@ export class StateBadge extends LitElement {
:host([icon]:focus) {
background: var(--divider-color);
}
ha-icon {
ha-state-icon {
transition: color 0.3s ease-in-out, filter 0.3s ease-in-out;
}
.missing {
+2
View File
@@ -53,6 +53,7 @@ class StateInfo extends LitElement {
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.last_changed}
capitalize
></ha-relative-time>
</div>
<div class="row">
@@ -64,6 +65,7 @@ class StateInfo extends LitElement {
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.last_updated}
capitalize
></ha-relative-time>
</div>
</div>
+4 -4
View File
@@ -52,7 +52,7 @@ class HaCameraStream extends LitElement {
this.stateObj &&
(changedProps.get("stateObj") as CameraEntity | undefined)?.entity_id !==
this.stateObj.entity_id &&
this.stateObj!.attributes.stream_type === STREAM_TYPE_HLS
this.stateObj!.attributes.frontend_stream_type === STREAM_TYPE_HLS
) {
this._forceMJPEG = undefined;
this._url = undefined;
@@ -84,7 +84,7 @@ class HaCameraStream extends LitElement {
.alt=${`Preview of the ${computeStateName(this.stateObj)} camera.`}
/>`;
}
if (this.stateObj.attributes.stream_type === STREAM_TYPE_HLS && true) {
if (this.stateObj.attributes.frontend_stream_type === STREAM_TYPE_HLS) {
return this._url
? html` <ha-hls-player
autoplay
@@ -97,7 +97,7 @@ class HaCameraStream extends LitElement {
></ha-hls-player>`
: html``;
}
if (this.stateObj.attributes.stream_type === STREAM_TYPE_WEB_RTC) {
if (this.stateObj.attributes.frontend_stream_type === STREAM_TYPE_WEB_RTC) {
return html` <ha-web-rtc-player
autoplay
playsinline
@@ -123,7 +123,7 @@ class HaCameraStream extends LitElement {
return true;
}
if (
this.stateObj!.attributes.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
+2 -3
View File
@@ -14,7 +14,6 @@ import { computeCloseIcon, computeOpenIcon } from "../common/entity/cover_icon";
import { UNAVAILABLE } from "../data/entity";
import type { HomeAssistant } from "../types";
import CoverEntity from "../util/cover-model";
import "./ha-icon";
import "./ha-icon-button";
@customElement("ha-cover-controls")
@@ -49,8 +48,8 @@ class HaCoverControls extends LitElement {
)}
@click=${this._onOpenTap}
.disabled=${this._computeOpenDisabled()}
.path=${computeOpenIcon(this.stateObj)}
>
<ha-icon .icon=${computeOpenIcon(this.stateObj)}></ha-icon>
</ha-icon-button>
<ha-icon-button
class=${classMap({
@@ -72,8 +71,8 @@ class HaCoverControls extends LitElement {
)}
@click=${this._onCloseTap}
.disabled=${this._computeClosedDisabled()}
.path=${computeCloseIcon(this.stateObj)}
>
<ha-icon .icon=${computeCloseIcon(this.stateObj)}></ha-icon>
</ha-icon-button>
</div>
`;
+7 -1
View File
@@ -24,7 +24,7 @@ export const createCloseHeading = (
// @ts-expect-error
export class HaDialog extends Dialog {
public scrollToPos(x: number, y: number) {
this.contentElement.scrollTo(x, y);
this.contentElement?.scrollTo(x, y);
}
protected renderHeading() {
@@ -44,6 +44,12 @@ export class HaDialog extends Dialog {
justify-content: var(--justify-action-buttons, flex-end);
padding-bottom: max(env(safe-area-inset-bottom), 8px);
}
.mdc-dialog__actions span:nth-child(1) {
flex: var(--secondary-action-button-flex, unset);
}
.mdc-dialog__actions span:nth-child(2) {
flex: var(--primary-action-button-flex, unset);
}
.mdc-dialog__container {
align-items: var(--vertial-align-dialog, center);
}
+14 -9
View File
@@ -84,15 +84,20 @@ export class HaFileUpload extends LitElement {
${this.value}
</iron-input>
${this.value
? html`<ha-icon-button
slot="suffix"
@click=${this._clearValue}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>`
: html`<ha-icon-button slot="suffix">
.path=${this.icon} ></ha-icon-button
>`}
? html`
<ha-icon-button
slot="suffix"
@click=${this._clearValue}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
`
: html`
<ha-icon-button
slot="suffix"
.path=${this.icon}
></ha-icon-button>
`}
</paper-input-container>
</label>
`}
+1 -1
View File
@@ -20,7 +20,7 @@ export class HaFormBoolean extends LitElement implements HaFormElement {
@property({ type: Boolean }) public disabled = false;
@query("paper-checkbox", true) private _input?: HTMLElement;
@query("ha-checkbox", true) private _input?: HTMLElement;
public focus() {
if (this._input) {
@@ -43,7 +43,7 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
@state() private _opened = false;
@query("paper-menu-button", true) private _input?: HTMLElement;
@query("ha-button-menu") private _input?: HTMLElement;
public focus(): void {
if (this._input) {
+1
View File
@@ -52,6 +52,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
return html`
<mwc-select
fixedMenuPosition
naturalMenuWidth
.label=${this.label}
.value=${this.data}
.disabled=${this.disabled}
+1 -6
View File
@@ -8,7 +8,6 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { nextRender } from "../common/util/render-status";
import { getExternalConfig } from "../external_app/external_config";
import type { HomeAssistant } from "../types";
@@ -65,7 +64,6 @@ class HaHLSPlayer extends LitElement {
.muted=${this.muted}
?playsinline=${this.playsInline}
?controls=${this.controls}
@loadeddata=${this._elementResized}
></video>
`;
}
@@ -191,6 +189,7 @@ class HaHLSPlayer extends LitElement {
fragLoadingTimeOut: 30000,
manifestLoadingTimeOut: 30000,
levelLoadingTimeOut: 30000,
maxLiveSyncPlaybackRate: 2,
});
this._hlsPolyfillInstance = hls;
hls.attachMedia(videoEl);
@@ -206,10 +205,6 @@ class HaHLSPlayer extends LitElement {
});
}
private _elementResized() {
fireEvent(this, "iron-resize");
}
private _cleanUp() {
if (this._hlsPolyfillInstance) {
this._hlsPolyfillInstance.destroy();
+3 -5
View File
@@ -28,8 +28,9 @@ export class HaIconButton extends LitElement {
.title=${this.hideTitle ? "" : this.label}
.disabled=${this.disabled}
>
${this.path ? html`<ha-svg-icon .path=${this.path}></ha-svg-icon>` : ""}
<slot></slot>
${this.path
? html`<ha-svg-icon .path=${this.path}></ha-svg-icon>`
: html`<slot></slot>`}
</mwc-icon-button>
`;
}
@@ -47,9 +48,6 @@ export class HaIconButton extends LitElement {
--mdc-theme-on-primary: currentColor;
--mdc-theme-text-disabled-on-light: var(--disabled-text-color);
}
ha-icon {
--ha-icon-display: inline;
}
`;
}
}
-63
View File
@@ -1,63 +0,0 @@
import "@polymer/paper-input/paper-input";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-icon";
@customElement("ha-icon-input")
export class HaIconInput extends LitElement {
@property() public value?: string;
@property() public label?: string;
@property() public placeholder?: string;
@property({ attribute: "error-message" }) public errorMessage?: string;
@property({ type: Boolean }) public disabled = false;
protected render(): TemplateResult {
return html`
<paper-input
.value=${this.value}
.label=${this.label}
.placeholder=${this.placeholder}
@value-changed=${this._valueChanged}
.disabled=${this.disabled}
auto-validate
.errorMessage=${this.errorMessage}
pattern="^\\S+:\\S+$"
>
${this.value || this.placeholder
? html`
<ha-icon .icon=${this.value || this.placeholder} slot="suffix">
</ha-icon>
`
: ""}
</paper-input>
`;
}
private _valueChanged(ev: CustomEvent) {
this.value = ev.detail.value;
fireEvent(
this,
"value-changed",
{ value: ev.detail.value },
{
bubbles: false,
composed: false,
}
);
}
static get styles() {
return css`
ha-icon {
position: absolute;
bottom: 2px;
right: 0;
}
`;
}
}
+119
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;
}
}
+176
View File
@@ -0,0 +1,176 @@
import { mdiCheck, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import { css, html, LitElement, TemplateResult } from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { PolymerChangedEvent } from "../polymer-types";
import "./ha-icon";
import "./ha-icon-button";
let mdiIconList: string[] = [];
// eslint-disable-next-line lit/prefer-static-styles
const rowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style>
paper-icon-item {
padding: 0;
margin: -8px;
}
#content {
display: flex;
align-items: center;
}
ha-svg-icon {
padding-left: 2px;
color: var(--secondary-text-color);
}
:host(:not([selected])) ha-svg-icon {
display: none;
}
:host([selected]) paper-icon-item {
margin-left: 0;
}
</style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-icon-item>
<ha-icon .icon=${item} slot="item-icon"></ha-icon>
<paper-item-body>${item}</paper-item-body>
</paper-icon-item>`;
@customElement("ha-icon-picker")
export class HaIconPicker extends LitElement {
@property() public value?: string;
@property() public label?: string;
@property() public placeholder?: string;
@property() public fallbackPath?: string;
@property({ attribute: "error-message" }) public errorMessage?: string;
@property({ type: Boolean }) public disabled = false;
@state() private _opened = false;
@query("vaadin-combo-box-light", true) private comboBox!: HTMLElement;
protected render(): TemplateResult {
return html`
<vaadin-combo-box-light
item-value-path="icon"
item-label-path="icon"
.value=${this._value}
allow-custom-value
.filteredItems=${mdiIconList}
${comboBoxRenderer(rowRenderer)}
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
@filter-changed=${this._filterChanged}
>
<paper-input
.label=${this.label}
.placeholder=${this.placeholder}
.disabled=${this.disabled}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
>
${this._value || this.placeholder
? html`
<ha-icon .icon=${this._value || this.placeholder} slot="prefix">
</ha-icon>
`
: this.fallbackPath
? html`<ha-svg-icon
.path=${this.fallbackPath}
slot="prefix"
></ha-svg-icon>`
: ""}
<ha-icon-button
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
slot="suffix"
class="toggle-button"
></ha-icon-button>
</paper-input>
</vaadin-combo-box-light>
`;
}
private async _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
if (this._opened && !mdiIconList.length) {
const iconList = await import("../../build/mdi/iconList.json");
mdiIconList = iconList.default.map((icon) => `mdi:${icon}`);
(this.comboBox as any).filteredItems = mdiIconList;
}
}
private _valueChanged(ev: PolymerChangedEvent<string>) {
this._setValue(ev.detail.value);
}
private _setValue(value: string) {
this.value = value;
fireEvent(
this,
"value-changed",
{ value },
{
bubbles: false,
composed: false,
}
);
}
private _filterChanged(ev: CustomEvent): void {
const filterString = ev.detail.value.toLowerCase();
const characterCount = filterString.length;
if (characterCount >= 2) {
const filteredItems = mdiIconList.filter((icon) =>
icon.includes(filterString)
);
if (filteredItems.length > 0) {
(this.comboBox as any).filteredItems = filteredItems;
} else {
(this.comboBox as any).filteredItems = [filterString];
}
} else {
(this.comboBox as any).filteredItems = mdiIconList;
}
}
private get _value() {
return this.value || "";
}
static get styles() {
return css`
ha-icon,
ha-svg-icon {
position: relative;
bottom: 2px;
}
*[slot="prefix"] {
margin-right: 8px;
}
paper-input > ha-icon-button {
--mdc-icon-button-size: 24px;
padding: 2px;
color: var(--secondary-text-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-icon-picker": HaIconPicker;
}
}
-20
View File
@@ -52,18 +52,6 @@ const mdiDeprecatedIcons: DeprecatedIcon = {
newName: "cast-variant",
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: {
removeIn: "2021.12",
},
@@ -77,14 +65,6 @@ const mdiDeprecatedIcons: DeprecatedIcon = {
newName: "cross-bolnisi",
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": {
newName: "boom-gate-arrow-down",
removeIn: "2021.12",
+9 -7
View File
@@ -1,26 +1,27 @@
import { mdiStar } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import {
css,
CSSResultGroup,
html,
nothing,
LitElement,
nothing,
TemplateResult,
} from "lit";
import { customElement, state, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import {
Adapter,
NetworkConfig,
IPv6ConfiguredAddress,
IPv4ConfiguredAddress,
IPv6ConfiguredAddress,
NetworkConfig,
} from "../data/network";
import { fireEvent } from "../common/dom/fire_event";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
import "./ha-checkbox";
import type { HaCheckbox } from "./ha-checkbox";
import "./ha-settings-row";
import "./ha-icon";
import "./ha-svg-icon";
const format_addresses = (
addresses: IPv6ConfiguredAddress[] | IPv4ConfiguredAddress[]
@@ -92,7 +93,8 @@ export class HaNetwork extends LitElement {
<span slot="heading">
Adapter: ${adapter.name}
${adapter.default
? html`<ha-icon .icon="hass:star"></ha-icon> (Default)`
? html`<ha-svg-icon .path=${mdiStar}></ha-svg-icon>
(Default)`
: ""}
</span>
<span slot="description">
@@ -16,7 +16,7 @@ export class HaBooleanSelector extends LitElement {
@property({ type: Boolean }) public disabled = false;
protected render() {
return html` <ha-formfield alignEnd spaceBetween .label=${this.label}>
return html`<ha-formfield alignEnd spaceBetween .label=${this.label}>
<ha-switch
.checked=${this.value}
@change=${this._handleChange}
+36 -18
View File
@@ -25,6 +25,11 @@ import "./ha-settings-row";
import "./ha-yaml-editor";
import type { HaYamlEditor } from "./ha-yaml-editor";
const showOptionalToggle = (field) =>
field.selector &&
!field.required &&
!("boolean" in field.selector && field.default);
interface ExtHassService extends Omit<HassService, "fields"> {
fields: {
key: string;
@@ -186,7 +191,7 @@ export class HaServiceControl extends LitElement {
const hasOptional = Boolean(
!shouldRenderServiceDataYaml &&
serviceData?.fields.some((field) => field.selector && !field.required)
serviceData?.fields.some((field) => showOptionalToggle(field))
);
return html`<ha-service-picker
@@ -253,14 +258,15 @@ export class HaServiceControl extends LitElement {
.defaultValue=${this._value?.data}
@value-changed=${this._dataChanged}
></ha-yaml-editor>`
: serviceData?.fields.map((dataField) =>
dataField.selector &&
(!dataField.advanced ||
this.showAdvanced ||
(this._value?.data &&
this._value.data[dataField.key] !== undefined))
: serviceData?.fields.map((dataField) => {
const showOptional = showOptionalToggle(dataField);
return dataField.selector &&
(!dataField.advanced ||
this.showAdvanced ||
(this._value?.data &&
this._value.data[dataField.key] !== undefined))
? html`<ha-settings-row .narrow=${this.narrow}>
${dataField.required
${!showOptional
? hasOptional
? html`<div slot="prefix" class="checkbox-spacer"></div>`
: ""
@@ -273,9 +279,9 @@ export class HaServiceControl extends LitElement {
slot="prefix"
></ha-checkbox>`}
<span slot="heading">${dataField.name || dataField.key}</span>
<span slot="description">${dataField?.description}</span
><ha-selector
.disabled=${!dataField.required &&
<span slot="description">${dataField?.description}</span>
<ha-selector
.disabled=${showOptional &&
!this._checkedKeys.has(dataField.key) &&
(!this._value?.data ||
this._value.data[dataField.key] === undefined)}
@@ -287,23 +293,35 @@ export class HaServiceControl extends LitElement {
this._value.data[dataField.key] !== undefined
? this._value.data[dataField.key]
: dataField.default}
></ha-selector
></ha-settings-row>`
: ""
)} `;
></ha-selector>
</ha-settings-row>`
: "";
})}`;
}
private _checkboxChanged(ev) {
const checked = ev.currentTarget.checked;
const key = ev.currentTarget.key;
let data;
if (checked) {
this._checkedKeys.add(key);
const defaultValue = this._getServiceInfo(
this._value?.service,
this.hass.services
)?.fields.find((field) => field.key === key)?.default;
if (defaultValue) {
data = {
...this._value?.data,
[key]: defaultValue,
};
}
} else {
this._checkedKeys.delete(key);
const data = { ...this._value?.data };
data = { ...this._value?.data };
delete data[key];
}
if (data) {
fireEvent(this, "value-changed", {
value: {
...this._value,
+91 -60
View File
@@ -1,11 +1,21 @@
import "@material/mwc-button/mwc-button";
import {
mdiBell,
mdiCalendar,
mdiCart,
mdiCellphoneCog,
mdiChartBox,
mdiClose,
mdiCog,
mdiFormatListBulletedType,
mdiHammer,
mdiHomeAssistant,
mdiLightningBolt,
mdiMenu,
mdiMenuOpen,
mdiPlayBoxMultiple,
mdiPlus,
mdiTooltipAccount,
mdiViewDashboard,
} from "@mdi/js";
import "@polymer/paper-item/paper-icon-item";
@@ -62,6 +72,20 @@ const SORT_VALUE_URL_PATHS = {
config: 11,
};
const PANEL_ICONS = {
calendar: mdiCalendar,
config: mdiCog,
"developer-tools": mdiHammer,
energy: mdiLightningBolt,
hassio: mdiHomeAssistant,
history: mdiChartBox,
logbook: mdiFormatListBulletedType,
lovelace: mdiViewDashboard,
map: mdiTooltipAccount,
"media-browser": mdiPlayBoxMultiple,
"shopping-list": mdiCart,
};
const panelSorter = (
reverseSort: string[],
defaultPanel: string,
@@ -348,6 +372,60 @@ class HaSidebar extends LitElement {
`;
}
private _renderPanels(panels: PanelInfo[]) {
return panels.map((panel) =>
this._renderPanel(
panel.url_path,
panel.url_path === this.hass.defaultPanel
? panel.title || this.hass.localize("panel.states")
: this.hass.localize(`panel.${panel.title}`) || panel.title,
panel.icon,
panel.url_path === this.hass.defaultPanel && !panel.icon
? PANEL_ICONS.lovelace
: panel.url_path in PANEL_ICONS
? PANEL_ICONS[panel.url_path]
: undefined
)
);
}
private _renderPanel(
urlPath: string,
title: string | null,
icon?: string | null,
iconPath?: string | null
) {
return html`
<a
aria-role="option"
href=${`/${urlPath}`}
data-panel=${urlPath}
tabindex="-1"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
${iconPath
? html`<ha-svg-icon
slot="item-icon"
.path=${iconPath}
></ha-svg-icon>`
: html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`}
<span class="item-text">${title}</span>
</paper-icon-item>
${this.editMode
? html`<ha-icon-button
.label=${this.hass.localize("ui.sidebar.hide_panel")}
.path=${mdiClose}
class="hide-panel"
.panel=${urlPath}
@click=${this._hidePanel}
></ha-icon-button>`
: ""}
</a>
`;
}
private _renderPanelsEdit(beforeSpacer: PanelInfo[]) {
// prettier-ignore
return html`<div id="sortable">
@@ -360,7 +438,7 @@ class HaSidebar extends LitElement {
}
private _renderHiddenPanels() {
return html` ${this._hiddenPanels.length
return html`${this._hiddenPanels.length
? html`${this._hiddenPanels.map((url) => {
const panel = this.hass.panels[url];
if (!panel) {
@@ -371,12 +449,17 @@ class HaSidebar extends LitElement {
class="hidden-panel"
.panel=${url}
>
<ha-icon
slot="item-icon"
.icon=${panel.url_path === this.hass.defaultPanel
? "mdi:view-dashboard"
: panel.icon}
></ha-icon>
${panel.url_path === this.hass.defaultPanel && !panel.icon
? html`<ha-svg-icon
slot="item-icon"
.path=${PANEL_ICONS.lovelace}
></ha-svg-icon>`
: panel.url_path in PANEL_ICONS
? html`<ha-svg-icon
slot="item-icon"
.path=${PANEL_ICONS[panel.url_path]}
></ha-svg-icon>`
: html`<ha-icon slot="item-icon" .icon=${panel.icon}></ha-icon>`}
<span class="item-text"
>${panel.url_path === this.hass.defaultPanel
? this.hass.localize("panel.states")
@@ -412,7 +495,7 @@ class HaSidebar extends LitElement {
}
}
return html` <div
return html`<div
class="notifications-container"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
@@ -682,58 +765,6 @@ class HaSidebar extends LitElement {
fireEvent(this, "hass-toggle-menu");
}
private _renderPanels(panels: PanelInfo[]) {
return panels.map((panel) =>
this._renderPanel(
panel.url_path,
panel.url_path === this.hass.defaultPanel
? panel.title || this.hass.localize("panel.states")
: this.hass.localize(`panel.${panel.title}`) || panel.title,
panel.icon,
panel.url_path === this.hass.defaultPanel && !panel.icon
? mdiViewDashboard
: undefined
)
);
}
private _renderPanel(
urlPath: string,
title: string | null,
icon?: string | null,
iconPath?: string | null
) {
return html`
<a
aria-role="option"
href=${`/${urlPath}`}
data-panel=${urlPath}
tabindex="-1"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
${iconPath
? html`<ha-svg-icon
slot="item-icon"
.path=${iconPath}
></ha-svg-icon>`
: html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`}
<span class="item-text">${title}</span>
</paper-icon-item>
${this.editMode
? html`<ha-icon-button
.label=${this.hass.localize("ui.sidebar.hide_panel")}
.path=${mdiClose}
class="hide-panel"
.panel=${urlPath}
@click=${this._hidePanel}
></ha-icon-button>`
: ""}
</a>
`;
}
static get styles(): CSSResultGroup {
return [
haStyleScrollbar,
+27
View File
@@ -0,0 +1,27 @@
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { stateIconPath } from "../common/entity/state_icon_path";
import "./ha-icon";
import "./ha-svg-icon";
@customElement("ha-state-icon")
export class HaStateIcon extends LitElement {
@property({ attribute: false }) public state?: HassEntity;
@property() public icon?: string;
protected render(): TemplateResult {
if (this.icon || this.state?.attributes.icon) {
return html`<ha-icon
.icon=${this.icon || this.state?.attributes.icon}
></ha-icon>`;
}
return html`<ha-svg-icon .path=${stateIconPath(this.state)}></ha-svg-icon>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-state-icon": HaStateIcon;
}
}
+10 -11
View File
@@ -10,6 +10,7 @@ import {
} from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import {
HassEntity,
HassServiceTarget,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
@@ -20,7 +21,6 @@ import { fireEvent } from "../common/dom/fire_event";
import { ensureArray } from "../common/ensure-array";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import { stateIcon } from "../common/entity/state_icon";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
@@ -41,15 +41,14 @@ import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./entity/ha-entity-picker";
import type { HaEntityPickerEntityFilterFunc } from "./entity/ha-entity-picker";
import "./ha-area-picker";
import "./ha-icon";
import "./ha-icon-button";
import "./ha-svg-icon";
@customElement("ha-target-picker")
export class HaTargetPicker extends SubscribeMixin(LitElement) {
@property() public hass!: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public value?: HassServiceTarget;
@property({ attribute: false }) public value?: HassServiceTarget;
@property() public label?: string;
@@ -147,7 +146,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
"entity_id",
entity_id,
entity ? computeStateName(entity) : entity_id,
entity ? stateIcon(entity) : undefined
entity
);
})
: ""}
@@ -230,7 +229,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
type: string,
id: string,
name: string,
icon?: string,
entityState?: HassEntity,
iconPath?: string
) {
return html`
@@ -245,11 +244,11 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
.path=${iconPath}
></ha-svg-icon>`
: ""}
${icon
? html`<ha-icon
${entityState
? html`<ha-state-icon
class="mdc-chip__icon mdc-chip__icon--leading"
.icon=${icon}
></ha-icon>`
.state=${entityState}
></ha-state-icon>`
: ""}
<span role="gridcell">
<span role="button" tabindex="0" class="mdc-chip__primary-action">
@@ -559,7 +558,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
width: 16px;
height: 16px;
--mdc-icon-size: 14px;
color: var(--card-background-color);
color: var(--secondary-text-color);
}
.mdc-chip__icon--leading {
display: flex;
+25 -1
View File
@@ -39,6 +39,10 @@ class HaWebRtcPlayer extends LitElement {
// don't cache this, as we remove it on disconnects
@query("#remote-stream") private _videoEl!: HTMLVideoElement;
private _peerConnection?: RTCPeerConnection;
private _remoteStream?: MediaStream;
protected render(): TemplateResult {
if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
@@ -71,6 +75,7 @@ class HaWebRtcPlayer extends LitElement {
private async _startWebRtc(): Promise<void> {
this._error = undefined;
const peerConnection = new RTCPeerConnection();
// Some cameras (such as nest) require a data channel to establish a stream
// however, not used by any integrations.
@@ -93,6 +98,7 @@ class HaWebRtcPlayer extends LitElement {
);
} catch (err: any) {
this._error = "Failed to start WebRTC stream: " + err.message;
peerConnection.close();
return;
}
@@ -102,21 +108,39 @@ class HaWebRtcPlayer extends LitElement {
remoteStream.addTrack(event.track);
this._videoEl.srcObject = remoteStream;
});
this._remoteStream = remoteStream;
// Initiate the stream with the remote device
const remoteDesc = new RTCSessionDescription({
type: "answer",
sdp: webRtcAnswer.answer,
});
await peerConnection.setRemoteDescription(remoteDesc);
try {
await peerConnection.setRemoteDescription(remoteDesc);
} catch (err: any) {
this._error = "Failed to connect WebRTC stream: " + err.message;
peerConnection.close();
return;
}
this._peerConnection = peerConnection;
}
private _cleanUp() {
if (this._remoteStream) {
this._remoteStream.getTracks().forEach((track) => {
track.stop();
});
this._remoteStream = undefined;
}
if (this._videoEl) {
const videoEl = this._videoEl;
videoEl.removeAttribute("src");
videoEl.load();
}
if (this._peerConnection) {
this._peerConnection.close();
this._peerConnection = undefined;
}
}
static get styles(): CSSResultGroup {
+4 -4
View File
@@ -194,10 +194,10 @@ export interface NumericStateCondition extends BaseCondition {
export interface SunCondition extends BaseCondition {
condition: "sun";
after_offset: number;
before_offset: number;
after: "sunrise" | "sunset";
before: "sunrise" | "sunset";
after_offset?: number;
before_offset?: number;
after?: "sunrise" | "sunset";
before?: "sunrise" | "sunset";
}
export interface ZoneCondition extends BaseCondition {
+1 -1
View File
@@ -17,7 +17,7 @@ interface CameraEntityAttributes extends HassEntityAttributeBase {
access_token: string;
brand: string;
motion_detection: boolean;
stream_type: string;
frontend_stream_type: string;
}
export interface CameraEntity extends HassEntityBase {
+35
View File
@@ -62,6 +62,8 @@ export type CloudStatus = CloudStatusNotLoggedIn | CloudStatusLoggedIn;
export interface SubscriptionInfo {
human_description: string;
provider: string;
plan_renewal_date?: number;
}
export interface CloudWebhook {
@@ -76,6 +78,39 @@ export interface ThingTalkConversion {
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) =>
hass.callWS<CloudStatus>({ type: "cloud/status" });
+14
View File
@@ -74,15 +74,29 @@ export interface StatisticValue {
export interface StatisticsMetaData {
unit_of_measurement: string;
statistic_id: string;
source: string;
name?: string | null;
}
export type StatisticsValidationResult =
| StatisticsValidationResultNoState
| StatisticsValidationResultEntityNotRecorded
| StatisticsValidationResultEntityNoLongerRecorded
| StatisticsValidationResultUnsupportedStateClass
| StatisticsValidationResultUnitsChanged
| StatisticsValidationResultUnsupportedUnitMetadata
| StatisticsValidationResultUnsupportedUnitState;
export interface StatisticsValidationResultNoState {
type: "no_state";
data: { statistic_id: string };
}
export interface StatisticsValidationResultEntityNoLongerRecorded {
type: "entity_no_longer_recorded";
data: { statistic_id: string };
}
export interface StatisticsValidationResultEntityNotRecorded {
type: "entity_not_recorded";
data: { statistic_id: string };
+55 -49
View File
@@ -2,6 +2,7 @@ import { HassEntity } from "home-assistant-js-websocket";
import { BINARY_STATE_OFF, BINARY_STATE_ON } from "../common/const";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { LocalizeFunc } from "../common/translations/localize";
import { HomeAssistant } from "../types";
import { UNAVAILABLE_STATES } from "./entity";
@@ -35,9 +36,11 @@ export const getLogbookDataForContext = async (
hass: HomeAssistant,
startDate: string,
contextId?: string
): Promise<LogbookEntry[]> =>
addLogbookMessage(
): Promise<LogbookEntry[]> => {
const localize = await hass.loadBackendTranslation("device_class");
return addLogbookMessage(
hass,
localize,
await getLogbookDataFromServer(
hass,
startDate,
@@ -47,6 +50,7 @@ export const getLogbookDataForContext = async (
contextId
)
);
};
export const getLogbookData = async (
hass: HomeAssistant,
@@ -54,9 +58,11 @@ export const getLogbookData = async (
endDate: string,
entityId?: string,
entity_matches_only?: boolean
): Promise<LogbookEntry[]> =>
addLogbookMessage(
): Promise<LogbookEntry[]> => {
const localize = await hass.loadBackendTranslation("device_class");
return addLogbookMessage(
hass,
localize,
await getLogbookDataCache(
hass,
startDate,
@@ -65,9 +71,11 @@ export const getLogbookData = async (
entity_matches_only
)
);
};
export const addLogbookMessage = (
hass: HomeAssistant,
localize: LocalizeFunc,
logbookData: LogbookEntry[]
): LogbookEntry[] => {
for (const entry of logbookData) {
@@ -75,6 +83,7 @@ export const addLogbookMessage = (
if (entry.state && stateObj) {
entry.message = getLogbookMessage(
hass,
localize,
entry.state,
stateObj,
computeDomain(entry.entity_id!)
@@ -157,6 +166,7 @@ export const clearLogbookCache = (startDate: string, endDate: string) => {
export const getLogbookMessage = (
hass: HomeAssistant,
localize: LocalizeFunc,
state: string,
stateObj: HassEntity,
domain: string
@@ -165,21 +175,17 @@ export const getLogbookMessage = (
case "device_tracker":
case "person":
if (state === "not_home") {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_away`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_away`);
}
if (state === "home") {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_at_home`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_at_home`);
}
return hass.localize(
`${LOGBOOK_LOCALIZE_PATH}.was_at_state`,
"state",
state
);
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_at_state`, "state", state);
case "sun":
return state === "above_horizon"
? hass.localize(`${LOGBOOK_LOCALIZE_PATH}.rose`)
: hass.localize(`${LOGBOOK_LOCALIZE_PATH}.set`);
? localize(`${LOGBOOK_LOCALIZE_PATH}.rose`)
: localize(`${LOGBOOK_LOCALIZE_PATH}.set`);
case "binary_sensor": {
const isOn = state === BINARY_STATE_ON;
@@ -189,19 +195,19 @@ export const getLogbookMessage = (
switch (device_class) {
case "battery":
if (isOn) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_low`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_low`);
}
if (isOff) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_normal`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_normal`);
}
break;
case "connectivity":
if (isOn) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_connected`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_connected`);
}
if (isOff) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_disconnected`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_disconnected`);
}
break;
@@ -210,46 +216,46 @@ export const getLogbookMessage = (
case "opening":
case "window":
if (isOn) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_opened`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_opened`);
}
if (isOff) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_closed`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_closed`);
}
break;
case "lock":
if (isOn) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_unlocked`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_unlocked`);
}
if (isOff) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_locked`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_locked`);
}
break;
case "plug":
if (isOn) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_plugged_in`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_plugged_in`);
}
if (isOff) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_unplugged`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_unplugged`);
}
break;
case "presence":
if (isOn) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_at_home`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_at_home`);
}
if (isOff) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_away`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_away`);
}
break;
case "safety":
if (isOn) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_unsafe`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_unsafe`);
}
if (isOff) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_safe`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_safe`);
}
break;
@@ -265,27 +271,27 @@ export const getLogbookMessage = (
case "sound":
case "vibration":
if (isOn) {
return hass.localize(
`${LOGBOOK_LOCALIZE_PATH}.detected_device_class`,
"device_class",
device_class
);
return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_device_class`, {
device_class: localize(
`component.binary_sensor.device_class.${device_class}`
),
});
}
if (isOff) {
return hass.localize(
`${LOGBOOK_LOCALIZE_PATH}.cleared_device_class`,
"device_class",
device_class
);
return localize(`${LOGBOOK_LOCALIZE_PATH}.cleared_device_class`, {
device_class: localize(
`component.binary_sensor.device_class.${device_class}`
),
});
}
break;
case "tamper":
if (isOn) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.detected_tampering`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_tampering`);
}
if (isOff) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.cleared_tampering`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.cleared_tampering`);
}
break;
}
@@ -296,43 +302,43 @@ export const getLogbookMessage = (
case "cover":
switch (state) {
case "open":
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_opened`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_opened`);
case "opening":
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.is_opening`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_opening`);
case "closing":
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.is_closing`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_closing`);
case "closed":
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_closed`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_closed`);
}
break;
case "lock":
if (state === "unlocked") {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_unlocked`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_unlocked`);
}
if (state === "locked") {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_locked`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_locked`);
}
break;
}
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) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.turned_off`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.turned_off`);
}
if (UNAVAILABLE_STATES.includes(state)) {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.became_unavailable`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.became_unavailable`);
}
return hass.localize(
`${LOGBOOK_LOCALIZE_PATH}.changed_to_state`,
"state",
stateObj
? computeStateDisplay(hass.localize, stateObj, hass.locale, state)
? computeStateDisplay(localize, stateObj, hass.locale, state)
: state
);
};
+15 -8
View File
@@ -10,8 +10,15 @@ import {
mdiImage,
mdiMovie,
mdiMusic,
mdiPause,
mdiPlay,
mdiPlaylistMusic,
mdiPlayPause,
mdiPodcast,
mdiPower,
mdiSkipNext,
mdiSkipPrevious,
mdiStop,
mdiTelevisionClassic,
mdiVideo,
mdiWeb,
@@ -246,7 +253,7 @@ export const computeMediaControls = (
return supportsFeature(stateObj, SUPPORT_TURN_ON)
? [
{
icon: "hass:power",
icon: mdiPower,
action: "turn_on",
},
]
@@ -257,7 +264,7 @@ export const computeMediaControls = (
if (supportsFeature(stateObj, SUPPORT_TURN_OFF)) {
buttons.push({
icon: "hass:power",
icon: mdiPower,
action: "turn_off",
});
}
@@ -267,7 +274,7 @@ export const computeMediaControls = (
supportsFeature(stateObj, SUPPORT_PREVIOUS_TRACK)
) {
buttons.push({
icon: "hass:skip-previous",
icon: mdiSkipPrevious,
action: "media_previous_track",
});
}
@@ -285,12 +292,12 @@ export const computeMediaControls = (
buttons.push({
icon:
state === "on"
? "hass:play-pause"
? mdiPlayPause
: state !== "playing"
? "hass:play"
? mdiPlay
: supportsFeature(stateObj, SUPPORT_PAUSE)
? "hass:pause"
: "hass:stop",
? mdiPause
: mdiStop,
action:
state !== "playing"
? "media_play"
@@ -305,7 +312,7 @@ export const computeMediaControls = (
supportsFeature(stateObj, SUPPORT_NEXT_TRACK)
) {
buttons.push({
icon: "hass:skip-next",
icon: mdiSkipNext,
action: "media_next_track",
});
}
+4 -2
View File
@@ -21,7 +21,9 @@ export interface ScriptEntity extends HassEntityBase {
};
}
export interface ScriptConfig {
export type ScriptConfig = ManualScriptConfig | BlueprintScriptConfig;
export interface ManualScriptConfig {
alias: string;
sequence: Action | Action[];
icon?: string;
@@ -29,7 +31,7 @@ export interface ScriptConfig {
max?: number;
}
export interface BlueprintScriptConfig extends ScriptConfig {
export interface BlueprintScriptConfig extends ManualScriptConfig {
use_blueprint: { path: string; input?: BlueprintInput };
}
+2 -1
View File
@@ -37,7 +37,8 @@ export type TranslationCategory =
| "options"
| "device_automation"
| "mfa_setup"
| "system_health";
| "system_health"
| "device_class";
export const fetchTranslationPreferences = (hass: HomeAssistant) =>
fetchFrontendUserData(hass.connection, "language");
+8 -13
View File
@@ -1,4 +1,5 @@
import {
mdiAlertCircleOutline,
mdiGauge,
mdiWaterPercent,
mdiWeatherFog,
@@ -12,7 +13,6 @@ import {
import { css, html, svg, SVGTemplateResult, TemplateResult } from "lit";
import { styleMap } from "lit/directives/style-map";
import { formatNumber } from "../common/number/format_number";
import "../components/ha-icon";
import "../components/ha-svg-icon";
import type { HomeAssistant } from "../types";
@@ -57,7 +57,7 @@ export const weatherSVGs = new Set<string>([
]);
export const weatherIcons = {
exceptional: "hass:alert-circle-outline",
exceptional: mdiAlertCircleOutline,
};
export const weatherAttrIcons = {
@@ -245,17 +245,9 @@ const getWeatherExtrema = (
const unit = getWeatherUnit(hass!, "temperature");
return html`
${tempHigh
? `
${tempHigh} ${unit}
`
: ""}
${tempHigh ? `${formatNumber(tempHigh, hass.locale)} ${unit}` : ""}
${tempLow && tempHigh ? " / " : ""}
${tempLow
? `
${tempLow} ${unit}
`
: ""}
${tempLow ? `${formatNumber(tempLow, hass.locale)} ${unit}` : ""}
`;
};
@@ -441,7 +433,10 @@ export const getWeatherStateIcon = (
if (state in weatherIcons) {
return html`
<ha-icon class="weather-icon" .icon=${weatherIcons[state]}></ha-icon>
<ha-svg-icon
class="weather-icon"
.path=${weatherIcons[state]}
></ha-svg-icon>
`;
}
+3
View File
@@ -84,6 +84,9 @@ export interface ZWaveJSNodeStatus {
ready: boolean;
status: number;
is_secure: boolean | string;
is_routing: boolean | null;
zwave_plus_version: number | null;
highest_security_class: SecurityClass | null;
}
export interface ZwaveJSNodeMetadata {
@@ -1,6 +1,5 @@
import "@material/mwc-button";
import { mdiClose } from "@mdi/js";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
@@ -48,6 +48,7 @@ class StepFlowForm extends LitElement {
: ""}
<ha-form
.data=${stepData}
.disabled=${this._loading}
@value-changed=${this._stepDataChanged}
.schema=${step.data_schema}
.error=${step.errors}
-78
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;
}
}
@@ -1,5 +1,3 @@
import "@polymer/paper-checkbox/paper-checkbox";
import type { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
import {
css,
CSSResultGroup,
@@ -12,6 +10,7 @@ import { property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-camera-stream";
import { HaCheckbox } from "../../../components/ha-checkbox";
import {
CameraEntity,
CameraPreferences,
@@ -25,7 +24,7 @@ import type { HomeAssistant } from "../../../types";
class MoreInfoCamera extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public stateObj?: CameraEntity;
@property({ attribute: false }) public stateObj?: CameraEntity;
@state() private _cameraPrefs?: CameraPreferences;
@@ -55,12 +54,14 @@ class MoreInfoCamera extends LitElement {
></ha-camera-stream>
${this._cameraPrefs
? html`
<paper-checkbox
.checked=${this._cameraPrefs.preload_stream}
@change=${this._handleCheckboxChanged}
>
Preload stream
</paper-checkbox>
<ha-formfield>
<ha-checkbox
.checked=${this._cameraPrefs.preload_stream}
@change=${this._handleCheckboxChanged}
>
Preload stream
</ha-checkbox>
</ha-formfield>
`
: undefined}
`;
@@ -86,7 +87,7 @@ class MoreInfoCamera extends LitElement {
supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM) &&
// The stream component for HLS streams supports a server-side pre-load
// option that client initiated WebRTC streams do not
this.stateObj!.attributes.stream_type === STREAM_TYPE_HLS
this.stateObj!.attributes.frontend_stream_type === STREAM_TYPE_HLS
) {
// Fetch in background while we set up the video.
this._fetchCameraPrefs();
@@ -101,7 +102,7 @@ class MoreInfoCamera extends LitElement {
}
private async _handleCheckboxChanged(ev) {
const checkbox = ev.currentTarget as PaperCheckboxElement;
const checkbox = ev.currentTarget as HaCheckbox;
try {
this._cameraPrefs = await updateCameraPrefs(
this.hass!,
@@ -122,7 +123,7 @@ class MoreInfoCamera extends LitElement {
display: block;
position: relative;
}
paper-checkbox {
ha-checkbox {
position: absolute;
top: 0;
right: 0;
@@ -135,3 +136,9 @@ class MoreInfoCamera extends LitElement {
}
customElements.define("more-info-camera", MoreInfoCamera);
declare global {
interface HTMLElementTagNameMap {
"more-info-camera": MoreInfoCamera;
}
}
@@ -1,5 +1,7 @@
import "@material/mwc-button/mwc-button";
import {
mdiLoginVariant,
mdiMusicNote,
mdiPlayBoxMultiple,
mdiSend,
mdiVolumeHigh,
@@ -15,8 +17,8 @@ import { customElement, property, query } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import "../../../components/ha-paper-dropdown-menu";
import "../../../components/ha-slider";
import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog";
@@ -62,8 +64,8 @@ class MoreInfoMediaPlayer extends LitElement {
<ha-icon-button
action=${control.action}
@click=${this._handleClick}
.path=${control.icon}
>
<ha-icon .icon=${control.icon}></ha-icon>
</ha-icon-button>
`
)}
@@ -130,7 +132,10 @@ class MoreInfoMediaPlayer extends LitElement {
stateObj.attributes.source_list?.length
? html`
<div class="source-input">
<ha-icon class="source-input" icon="hass:login-variant"></ha-icon>
<ha-svg-icon
class="source-input"
.path=${mdiLoginVariant}
></ha-svg-icon>
<ha-paper-dropdown-menu
.label=${this.hass.localize("ui.card.media_player.source")}
>
@@ -155,7 +160,7 @@ class MoreInfoMediaPlayer extends LitElement {
stateObj.attributes.sound_mode_list?.length
? html`
<div class="sound-input">
<ha-icon icon="hass:music-note"></ha-icon>
<ha-svg-icon .path=${mdiMusicNote}></ha-svg-icon>
<ha-paper-dropdown-menu
dynamic-align
label-float
@@ -228,8 +233,8 @@ class MoreInfoMediaPlayer extends LitElement {
justify-content: space-between;
}
.source-input ha-icon,
.sound-input ha-icon {
.source-input ha-svg-icon,
.sound-input ha-svg-icon {
padding: 7px;
margin-top: 24px;
}
@@ -1,4 +1,5 @@
import {
mdiFan,
mdiHomeMapMarker,
mdiMapMarker,
mdiPause,
@@ -195,7 +196,7 @@ class MoreInfoVacuum extends LitElement {
style="justify-content: center; align-self: center; padding-top: 1.3em"
>
<span>
<ha-icon icon="hass:fan"></ha-icon>
<ha-svg-icon .path=${mdiFan}></ha-svg-icon>
${stateObj.attributes.fan_speed}
</span>
</div>
+59 -8
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 { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event";
import { throttle } from "../../common/util/throttle";
import "../../components/chart/state-history-charts";
import { getRecentWithCache } from "../../data/cached-history";
import { HistoryResult } from "../../data/history";
import { HomeAssistant } from "../../types";
declare global {
interface HASSDomEvents {
closed: undefined;
}
}
@customElement("ha-more-info-history")
export class MoreInfoHistory extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -24,14 +31,26 @@ export class MoreInfoHistory extends LitElement {
return html``;
}
const href = "/history?entity_id=" + this.entityId;
return html`${isComponentLoaded(this.hass, "history")
? html`<state-history-charts
up-to-now
.hass=${this.hass}
.historyData=${this._stateHistory}
.isLoadingData=${!this._stateHistory}
></state-history-charts>`
: ""} `;
? html` <div class="header">
<div class="title">
${this.hass.localize("ui.dialogs.more_info_control.history")}
</div>
<a href=${href} @click=${this._close}
>${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 {
@@ -78,6 +97,38 @@ export class MoreInfoHistory extends LitElement {
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 {
+39 -7
View File
@@ -1,16 +1,16 @@
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { throttle } from "../../common/util/throttle";
import "../../components/ha-circular-progress";
import { fetchUsers } from "../../data/user";
import { getLogbookData, LogbookEntry } from "../../data/logbook";
import { loadTraceContexts, TraceContexts } from "../../data/trace";
import { fetchUsers } from "../../data/user";
import "../../panels/logbook/ha-logbook";
import { haStyle } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { closeDialog } from "../make-dialog-manager";
@customElement("ha-more-info-logbook")
export class MoreInfoLogbook extends LitElement {
@@ -44,6 +44,8 @@ export class MoreInfoLogbook extends LitElement {
return html``;
}
const href = "/logbook?entity_id=" + this.entityId;
return html`
${isComponentLoaded(this.hass, "logbook")
? this._error
@@ -61,6 +63,16 @@ export class MoreInfoLogbook extends LitElement {
`
: this._logbookEntries.length
? 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
narrow
no-icon
@@ -81,11 +93,6 @@ export class MoreInfoLogbook extends LitElement {
protected firstUpdated(): void {
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 {
@@ -182,6 +189,10 @@ export class MoreInfoLogbook extends LitElement {
this._userIdToName = userIdToName;
}
private _close(): void {
setTimeout(() => fireEvent(this, "closed"), 500);
}
static get styles() {
return [
haStyle,
@@ -203,6 +214,27 @@ export class MoreInfoLogbook extends LitElement {
display: flex;
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);
}
`,
];
}
+6 -2
View File
@@ -386,10 +386,14 @@ export class QuickBar extends LitElement {
private _generateEntityItems(): EntityItem[] {
return Object.keys(this.hass.states)
.map((entityId) => {
const entityState = this.hass.states[entityId];
const entityItem = {
primaryText: computeStateName(this.hass.states[entityId]),
primaryText: computeStateName(entityState),
altText: entityId,
icon: domainIcon(computeDomain(entityId), this.hass.states[entityId]),
icon: entityState.attributes.icon,
iconPath: entityState.attributes.icon
? undefined
: domainIcon(computeDomain(entityId), entityState),
action: () => fireEvent(this, "hass-more-info", { entityId }),
};
@@ -1,7 +1,5 @@
/* eslint-disable lit/prefer-static-styles */
import { mdiMicrophone } from "@mdi/js";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import type { PaperDialogScrollableElement } from "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import {
@@ -17,7 +15,6 @@ import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event";
import { SpeechRecognition } from "../../common/dom/speech-recognition";
import { uid } from "../../common/util/uid";
import "../../components/dialog/ha-paper-dialog";
import "../../components/ha-icon-button";
import {
AgentInfo,
@@ -27,6 +24,9 @@ import {
} from "../../data/conversation";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../../components/ha-dialog";
import type { HaDialog } from "../../components/ha-dialog";
import "@material/mwc-button/mwc-button";
interface Message {
who: string;
@@ -56,7 +56,7 @@ export class HaVoiceCommandDialog extends LitElement {
@state() private _agentInfo?: AgentInfo;
@query("#messages", true) private messages!: PaperDialogScrollableElement;
@query("ha-dialog", true) private _dialog!: HaDialog;
private recognition!: SpeechRecognition;
@@ -70,74 +70,42 @@ export class HaVoiceCommandDialog extends LitElement {
this._agentInfo = await getAgentInfo(this.hass);
}
public async closeDialog(): Promise<void> {
this._opened = false;
if (this.recognition) {
this.recognition.abort();
}
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
// CSS custom property mixins only work in render https://github.com/Polymer/lit-element/issues/633
if (!this._opened) {
return html``;
}
return html`
<style>
paper-dialog-scrollable {
--paper-dialog-scrollable: {
-webkit-overflow-scrolling: auto;
max-height: 50vh !important;
}
}
paper-dialog-scrollable.can-scroll {
--paper-dialog-scrollable: {
-webkit-overflow-scrolling: touch;
max-height: 50vh !important;
}
}
@media all and (max-width: 450px), all and (max-height: 500px) {
paper-dialog-scrollable {
--paper-dialog-scrollable: {
-webkit-overflow-scrolling: auto;
max-height: calc(100vh - 175px) !important;
}
}
paper-dialog-scrollable.can-scroll {
--paper-dialog-scrollable: {
-webkit-overflow-scrolling: touch;
max-height: calc(100vh - 175px) !important;
}
}
}
</style>
<ha-paper-dialog
with-backdrop
.opened=${this._opened}
@opened-changed=${this._openedChanged}
>
${this._agentInfo && this._agentInfo.onboarding
? html`
<div class="onboarding">
${this._agentInfo.onboarding.text}
<div class="side-by-side" @click=${this._completeOnboarding}>
<a
class="button"
href=${this._agentInfo.onboarding.url}
target="_blank"
rel="noreferrer"
><mwc-button unelevated
>${this.hass.localize("ui.common.yes")}!</mwc-button
></a
>
<mwc-button outlined
>${this.hass.localize("ui.common.no")}</mwc-button
>
<ha-dialog open @closed=${this.closeDialog}>
<div>
${this._agentInfo && this._agentInfo.onboarding
? html`
<div class="onboarding">
${this._agentInfo.onboarding.text}
<div class="side-by-side" @click=${this._completeOnboarding}>
<a
class="button"
href=${this._agentInfo.onboarding.url}
target="_blank"
rel="noreferrer"
><mwc-button unelevated
>${this.hass.localize("ui.common.yes")}!</mwc-button
></a
>
<mwc-button outlined
>${this.hass.localize("ui.common.no")}</mwc-button
>
</div>
</div>
</div>
`
: ""}
<paper-dialog-scrollable
id="messages"
class=${classMap({
"top-border": Boolean(
this._agentInfo && this._agentInfo.onboarding
),
})}
>
`
: ""}
${this._conversation.map(
(message) => html`
<div class=${this._computeMessageClasses(message)}>
@@ -157,8 +125,8 @@ export class HaVoiceCommandDialog extends LitElement {
</div>
`
: ""}
</paper-dialog-scrollable>
<div class="input">
</div>
<div class="input" slot="primaryAction">
<paper-input
@keyup=${this._handleKeyUp}
.label=${this.hass.localize(
@@ -200,7 +168,7 @@ export class HaVoiceCommandDialog extends LitElement {
`
: ""}
</div>
</ha-paper-dialog>
</ha-dialog>
`;
}
@@ -346,18 +314,7 @@ export class HaVoiceCommandDialog extends LitElement {
}
private _scrollMessagesBottom() {
this.messages.scrollTarget.scrollTop =
this.messages.scrollTarget.scrollHeight;
if (this.messages.scrollTarget.scrollTop === 0) {
fireEvent(this.messages, "iron-resize");
}
}
private _openedChanged(ev: CustomEvent) {
this._opened = ev.detail.value;
if (!this._opened && this.recognition) {
this.recognition.abort();
}
this._dialog.scrollToPos(0, 99999);
}
private _computeMessageClasses(message: Message) {
@@ -368,10 +325,6 @@ export class HaVoiceCommandDialog extends LitElement {
return [
haStyleDialog,
css`
:host {
z-index: 103;
}
ha-icon-button {
color: var(--secondary-text-color);
}
@@ -380,13 +333,12 @@ export class HaVoiceCommandDialog extends LitElement {
color: var(--primary-color);
}
.input {
margin: 0 0 16px 0;
ha-dialog {
--primary-action-button-flex: 1;
--secondary-action-button-flex: 0;
--mdc-dialog-max-width: 450px;
}
ha-paper-dialog {
width: 450px;
}
a.button {
text-decoration: none;
}
@@ -394,16 +346,7 @@ export class HaVoiceCommandDialog extends LitElement {
width: 100%;
}
.onboarding {
padding: 0 24px;
}
paper-dialog-scrollable.top-border::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: var(--divider-color);
border-bottom: 1px solid var(--divider-color);
}
.side-by-side {
display: flex;
-6
View File
@@ -91,12 +91,6 @@ class SupervisorErrorScreen extends LitElement {
: this.hass.themes.default_theme);
themeSettings = this.hass.selectedTheme;
if (themeName === "default" && themeSettings?.dark === undefined) {
themeSettings = {
...this.hass.selectedTheme,
dark: this.hass.themes.darkMode,
};
}
} else {
themeName =
(this.hass.selectedTheme as unknown as string) ||
-25
View File
@@ -1,25 +0,0 @@
import { PaperDialogBehavior } from "@polymer/paper-dialog-behavior/paper-dialog-behavior";
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class";
import { dedupingMixin } from "@polymer/polymer/lib/utils/mixin";
import { EventsMixin } from "./events-mixin";
/**
* @polymerMixin
* @appliesMixin EventsMixin
* @appliesMixin PaperDialogBehavior
*/
export default dedupingMixin(
(superClass) =>
class extends mixinBehaviors(
[EventsMixin, PaperDialogBehavior],
superClass
) {
static get properties() {
return {
withBackdrop: {
type: Boolean,
value: true,
},
};
}
}
);
+6 -3
View File
@@ -1,6 +1,6 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "../components/ha-icon";
import "../components/ha-svg-icon";
@customElement("action-badge")
class ActionBadge extends LitElement {
@@ -15,9 +15,12 @@ class ActionBadge extends LitElement {
protected render(): TemplateResult {
return html`
<div class="icon">
<ha-icon .icon=${this.icon}></ha-icon>
<ha-svg-icon .path=${this.icon}></ha-svg-icon>
${this.badgeIcon
? html` <ha-icon class="badge" .icon=${this.badgeIcon}></ha-icon> `
? html`<ha-svg-icon
class="badge"
.path=${this.badgeIcon}
></ha-svg-icon>`
: ""}
</div>
<div class="title">${this.title}</div>
+5 -2
View File
@@ -1,6 +1,6 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "../components/ha-icon";
import "../components/ha-svg-icon";
import { brandsUrl } from "../util/brands-url";
@customElement("integration-badge")
@@ -27,7 +27,10 @@ class IntegrationBadge extends LitElement {
referrerpolicy="no-referrer"
/>
${this.badgeIcon
? html` <ha-icon class="badge" .icon=${this.badgeIcon}></ha-icon> `
? html`<ha-svg-icon
class="badge"
.path=${this.badgeIcon}
></ha-svg-icon>`
: ""}
</div>
<div class="title">${this.title}</div>
+48 -30
View File
@@ -1,8 +1,6 @@
import "@material/mwc-button/mwc-button";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import "@polymer/paper-radio-button/paper-radio-button";
import "@polymer/paper-radio-group/paper-radio-group";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -21,6 +19,9 @@ import { SYMBOL_TO_ISO } from "../data/currency";
import { onboardCoreConfigStep } from "../data/onboarding";
import type { PolymerChangedEvent } from "../polymer-types";
import type { HomeAssistant } from "../types";
import "../components/ha-radio";
import "../components/ha-formfield";
import type { HaRadio } from "../components/ha-radio";
const amsterdam: [number, number] = [52.3731339, 4.8903147];
const mql = matchMedia("(prefers-color-scheme: dark)");
@@ -135,32 +136,44 @@ class OnboardingCoreConfig extends LitElement {
"ui.panel.config.core.section.core.core_config.unit_system"
)}
</div>
<paper-radio-group
class="flex"
.selected=${this._unitSystemValue}
@selected-changed=${this._unitSystemChanged}
>
<paper-radio-button name="metric" .disabled=${this._working}>
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system_metric"
)}
<div class="secondary">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.metric_example"
<div class="radio-group">
<ha-formfield
.label=${html`${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system_metric"
)}
</div>
</paper-radio-button>
<paper-radio-button name="imperial" .disabled=${this._working}>
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system_imperial"
)}
<div class="secondary">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.imperial_example"
<div class="secondary">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.metric_example"
)}
</div>`}
>
<ha-radio
name="unit_system"
value="metric"
.checked=${this._unitSystemValue === "metric"}
@change=${this._unitSystemChanged}
.disabled=${this._working}
></ha-radio>
</ha-formfield>
<ha-formfield
.label=${html`${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system_imperial"
)}
</div>
</paper-radio-button>
</paper-radio-group>
<div class="secondary">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.imperial_example"
)}
</div>`}
>
<ha-radio
name="unit_system"
value="imperial"
.checked=${this._unitSystemValue === "imperial"}
@change=${this._unitSystemChanged}
.disabled=${this._working}
></ha-radio>
</ha-formfield>
</div>
</div>
<div class="row">
@@ -281,10 +294,8 @@ class OnboardingCoreConfig extends LitElement {
this._location = ev.detail.location;
}
private _unitSystemChanged(
ev: PolymerChangedEvent<ConfigUpdateValues["unit_system"]>
) {
this._unitSystem = ev.detail.value;
private _unitSystemChanged(ev: CustomEvent) {
this._unitSystem = (ev.target as HaRadio).value as "metric" | "imperial";
}
private async _detect() {
@@ -363,6 +374,13 @@ class OnboardingCoreConfig extends LitElement {
.row > * {
margin: 0 8px;
}
.radio-group {
display: flex;
flex-direction: column;
flex: 1;
}
.footer {
margin-top: 16px;
text-align: right;
+3 -2
View File
@@ -8,6 +8,7 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { mdiCheck, mdiDotsHorizontal } from "@mdi/js";
import { fireEvent } from "../common/dom/fire_event";
import { stringCompare } from "../common/string/compare";
import { isComponentLoaded } from "../common/config/is_component_loaded";
@@ -79,7 +80,7 @@ class OnboardingIntegrations extends LitElement {
<integration-badge
.domain=${entry.domain}
.title=${title}
badgeIcon="hass:check"
.badgeIcon=${mdiCheck}
.darkOptimizedIcon=${this.hass.selectedTheme?.dark}
></integration-badge>
`,
@@ -120,7 +121,7 @@ class OnboardingIntegrations extends LitElement {
title=${this.onboardingLocalize(
"ui.panel.page-onboarding.integration.more_integrations"
)}
icon="hass:dots-horizontal"
.icon=${mdiDotsHorizontal}
></action-badge>
</button>
</div>
+4
View File
@@ -371,6 +371,10 @@ export class HAFullCalendar extends LitElement {
);
--fc-theme-standard-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 {
@@ -1,5 +1,4 @@
import "@material/mwc-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property, state } from "lit/decorators";
@@ -1,17 +1,19 @@
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import { mdiArrowDown, mdiArrowUp, mdiDotsVertical } from "@mdi/js";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import type { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox";
import "@material/mwc-select";
import type { Select } from "@material/mwc-select";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stringCompare } from "../../../../common/string/compare";
import { handleStructError } from "../../../../common/structs/handle-errors";
import { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-card";
import "../../../../components/ha-alert";
import "../../../../components/ha-icon-button";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import type { Action } from "../../../../data/script";
@@ -42,7 +44,8 @@ const OPTIONS = [
"device_id",
];
const getType = (action: Action) => OPTIONS.find((option) => option in action);
const getType = (action: Action | undefined) =>
action ? OPTIONS.find((option) => option in action) : undefined;
declare global {
// for fire event
@@ -97,6 +100,19 @@ export default class HaAutomationActionRow extends LitElement {
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string][] =>
OPTIONS.map(
(action) =>
[
action,
localize(
`ui.panel.config.automation.editor.actions.type.${action}.label`
),
] as [string, string]
).sort((a, b) => stringCompare(a[1], b[1]))
);
protected updated(changedProperties: PropertyValues) {
if (!changedProperties.has("action")) {
return;
@@ -171,9 +187,12 @@ export default class HaAutomationActionRow extends LitElement {
</ha-button-menu>
</div>
${this._warnings
? html`<div class="warning">
${this.hass.localize("ui.errors.config.editor_not_supported")}:
<br />
? html`<ha-alert
alert-type="warning"
.title=${this.hass.localize(
"ui.errors.config.editor_not_supported"
)}
>
${this._warnings!.length > 0 && this._warnings![0] !== undefined
? html` <ul>
${this._warnings!.map(
@@ -182,7 +201,7 @@ export default class HaAutomationActionRow extends LitElement {
</ul>`
: ""}
${this.hass.localize("ui.errors.config.edit_in_yaml_supported")}
</div>`
</ha-alert>`
: ""}
${yamlMode
? html`
@@ -206,28 +225,21 @@ export default class HaAutomationActionRow extends LitElement {
></ha-yaml-editor>
`
: html`
<paper-dropdown-menu-light
<mwc-select
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type_select"
)}
no-animations
.value=${getType(this.action)}
naturalMenuWidth
@selected=${this._typeChanged}
>
<paper-listbox
slot="dropdown-content"
.selected=${selected}
@iron-select=${this._typeChanged}
>
${OPTIONS.map(
(opt) => html`
<paper-item .action=${opt}>
${this.hass.localize(
`ui.panel.config.automation.editor.actions.type.${opt}.label`
)}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
${this._processedTypes(this.hass.localize).map(
([opt, label]) => html`
<mwc-list-item .value=${opt}>${label}</mwc-list-item>
`
)}
</mwc-select>
<div @ui-mode-not-available=${this._handleUiModeNotAvailable}>
${dynamicElement(`ha-automation-action-${type}`, {
hass: this.hass,
@@ -287,8 +299,7 @@ export default class HaAutomationActionRow extends LitElement {
}
private _typeChanged(ev: CustomEvent) {
const type = ((ev.target as PaperListboxElement)?.selectedItem as any)
?.action;
const type = (ev.target as Select).value;
if (!type) {
return;
@@ -40,6 +40,9 @@ export class HaDelayAction extends LitElement implements ActionElement {
protected render() {
return html`<ha-duration-input
.label=${this.hass.localize(
`ui.panel.config.automation.editor.actions.type.delay.delay`
)}
.data=${this._timeData}
enableMillisecond
@value-changed=${this._valueChanged}
@@ -1,11 +1,12 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import type { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox";
import { CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "@material/mwc-select";
import type { Select } from "@material/mwc-select";
import memoizeOne from "memoize-one";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stringCompare } from "../../../../common/string/compare";
import { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-card";
import "../../../../components/ha-yaml-editor";
import type { Condition } from "../../../../data/automation";
@@ -45,6 +46,19 @@ export default class HaAutomationConditionEditor extends LitElement {
@property() public yamlMode = false;
private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string][] =>
OPTIONS.map(
(condition) =>
[
condition,
localize(
`ui.panel.config.automation.editor.conditions.type.${condition}.label`
),
] as [string, string]
).sort((a, b) => stringCompare(a[1], b[1]))
);
protected render() {
const selected = OPTIONS.indexOf(this.condition.condition);
const yamlMode = this.yamlMode || selected === -1;
@@ -71,28 +85,21 @@ export default class HaAutomationConditionEditor extends LitElement {
></ha-yaml-editor>
`
: html`
<paper-dropdown-menu-light
<mwc-select
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type_select"
)}
no-animations
.value=${this.condition.condition}
naturalMenuWidth
@selected=${this._typeChanged}
>
<paper-listbox
slot="dropdown-content"
.selected=${selected}
@iron-select=${this._typeChanged}
>
${OPTIONS.map(
(opt) => html`
<paper-item .condition=${opt}>
${this.hass.localize(
`ui.panel.config.automation.editor.conditions.type.${opt}.label`
)}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
${this._processedTypes(this.hass.localize).map(
([opt, label]) => html`
<mwc-list-item .value=${opt}>${label}</mwc-list-item>
`
)}
</mwc-select>
<div>
${dynamicElement(
`ha-automation-condition-${this.condition.condition}`,
@@ -104,8 +111,7 @@ export default class HaAutomationConditionEditor extends LitElement {
}
private _typeChanged(ev: CustomEvent) {
const type = ((ev.target as PaperListboxElement)?.selectedItem as any)
?.condition;
const type = (ev.target as Select).value;
if (!type) {
return;
@@ -1,10 +1,11 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { LogicalCondition } from "../../../../../data/automation";
import { Condition, LogicalCondition } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types";
import "../ha-automation-condition";
import { ConditionElement } from "../ha-automation-condition-row";
import { HaStateCondition } from "./ha-automation-condition-state";
@customElement("ha-automation-condition-logical")
export class HaLogicalCondition extends LitElement implements ConditionElement {
@@ -13,7 +14,14 @@ export class HaLogicalCondition extends LitElement implements ConditionElement {
@property() public condition!: LogicalCondition;
public static get defaultConfig() {
return { conditions: [{ condition: "state" }] };
return {
conditions: [
{
condition: "state",
...HaStateCondition.defaultConfig,
},
] as Condition[],
};
}
protected render() {
@@ -1,8 +1,5 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-radio-button/paper-radio-button";
import "@polymer/paper-radio-group/paper-radio-group";
import type { PaperRadioGroupElement } from "@polymer/paper-radio-group/paper-radio-group";
import { html, LitElement } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { SunCondition } from "../../../../../data/automation";
@@ -11,12 +8,15 @@ import {
ConditionElement,
handleChangeEvent,
} from "../ha-automation-condition-row";
import "../../../../../components/ha-radio";
import "../../../../../components/ha-formfield";
import type { HaRadio } from "../../../../../components/ha-radio";
@customElement("ha-automation-condition-sun")
export class HaSunCondition extends LitElement implements ConditionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public condition!: SunCondition;
@property({ attribute: false }) public condition!: SunCondition;
public static get defaultConfig() {
return {};
@@ -25,28 +25,35 @@ export class HaSunCondition extends LitElement implements ConditionElement {
protected render() {
const { after, after_offset, before, before_offset } = this.condition;
return html`
<label id="beforelabel">
<label>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.sun.before"
)}
</label>
<paper-radio-group
.selected=${before}
.name=${"before"}
aria-labelledby="beforelabel"
@paper-radio-group-changed=${this._radioGroupPicked}
>
<paper-radio-button name="sunrise">
${this.hass.localize(
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.sun.sunrise"
)}
</paper-radio-button>
<paper-radio-button name="sunset">
${this.hass.localize(
>
<ha-radio
name="before"
value="sunrise"
.checked=${before === "sunrise"}
@change=${this._radioGroupPicked}
></ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.sun.sunset"
)}
</paper-radio-button>
</paper-radio-group>
>
<ha-radio
name="before"
value="sunset"
.checked=${before === "sunset"}
@change=${this._radioGroupPicked}
></ha-radio>
</ha-formfield>
</label>
<paper-input
.label=${this.hass.localize(
@@ -57,28 +64,36 @@ export class HaSunCondition extends LitElement implements ConditionElement {
@value-changed=${this._valueChanged}
></paper-input>
<label id="afterlabel">
<label>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.sun.after"
)}
</label>
<paper-radio-group
.selected=${after}
.name=${"after"}
aria-labelledby="afterlabel"
@paper-radio-group-changed=${this._radioGroupPicked}
>
<paper-radio-button name="sunrise">
${this.hass.localize(
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.sun.sunrise"
)}
</paper-radio-button>
<paper-radio-button name="sunset">
${this.hass.localize(
>
<ha-radio
name="after"
value="sunrise"
.checked=${after === "sunrise"}
@change=${this._radioGroupPicked}
></ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.sun.sunset"
)}
</paper-radio-button>
</paper-radio-group>
>
<ha-radio
name="after"
value="sunset"
.checked=${after === "sunset"}
@change=${this._radioGroupPicked}
></ha-radio>
</ha-formfield>
</label>
<paper-input
.label=${this.hass.localize(
@@ -95,14 +110,27 @@ export class HaSunCondition extends LitElement implements ConditionElement {
handleChangeEvent(this, ev);
}
private _radioGroupPicked(ev) {
const key = ev.target.name;
private _radioGroupPicked(ev: CustomEvent) {
const key = (ev.target as HaRadio).name;
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: {
...this.condition,
[key]: (ev.target as PaperRadioGroupElement).selected,
[key]: (ev.target as HaRadio).value,
},
});
}
static styles = css`
label {
display: flex;
align-items: center;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-condition-sun": HaSunCondition;
}
}
@@ -464,7 +464,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
) {
return;
}
// Wait for dialog to complate closing
// Wait for dialog to complete closing
await new Promise((resolve) => setTimeout(resolve, 0));
}
showAutomationEditor({
@@ -4,12 +4,12 @@ import {
mdiInformationOutline,
mdiPencil,
mdiPencilOff,
mdiPlayCircleOutline,
mdiPlus,
} from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { 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-icon-button";
import "../../../components/ha-svg-icon";
import "../../../components/ha-icon-overflow-menu";
import {
AutomationEntity,
triggerAutomationActions,
@@ -135,7 +136,7 @@ class HaAutomationPicker extends LitElement {
template: (_info, automation: any) => html`
<mwc-button
.automation=${automation}
@click=${this._runActions}
@click=${this._triggerRunActions}
.disabled=${UNAVAILABLE_STATES.includes(automation.state)}
>
${this.hass.localize("ui.card.automation.trigger")}
@@ -143,78 +144,73 @@ class HaAutomationPicker extends LitElement {
`,
};
}
columns.info = {
columns.actions = {
title: "",
type: "icon-button",
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",
type: "overflow-menu",
template: (_info, automation: any) => html`
<a
href=${ifDefined(
automation.attributes.id
? `/config/automation/trace/${automation.attributes.id}`
: undefined
)}
<ha-icon-overflow-menu
.hass=${this.hass}
.narrow=${this.narrow}
.items=${[
// 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
.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>
`
: ""}
</ha-icon-overflow-menu>
`,
};
return columns;
@@ -285,9 +281,8 @@ class HaAutomationPicker extends LitElement {
this._filterValue = undefined;
}
private _showInfo(ev) {
ev.stopPropagation();
const entityId = ev.currentTarget.automation.entity_id;
private _showInfo(automation: AutomationEntity) {
const entityId = automation.entity_id;
fireEvent(this, "hass-more-info", { entityId });
}
@@ -311,9 +306,12 @@ class HaAutomationPicker extends LitElement {
});
}
private _runActions = (ev) => {
const entityId = ev.currentTarget.automation.entity_id;
triggerAutomationActions(this.hass, entityId);
private _triggerRunActions = (ev) => {
this._runActions(ev.currentTarget.automation);
};
private _runActions = (automation: AutomationEntity) => {
triggerAutomationActions(this.hass, automation.entity_id);
};
private _createNew() {
@@ -1,20 +1,18 @@
import "@material/mwc-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state, query } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/dialog/ha-paper-dialog";
import "../../../../components/ha-circular-progress";
import type { AutomationConfig } from "../../../../data/automation";
import { convertThingTalk } from "../../../../data/cloud";
import type { PolymerChangedEvent } from "../../../../polymer-types";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "./ha-thingtalk-placeholders";
import type { PlaceholderValues } from "./ha-thingtalk-placeholders";
import type { ThingtalkDialogParams } from "./show-dialog-thingtalk";
import "../../../../components/ha-dialog";
export interface Placeholder {
name: string;
@@ -38,8 +36,6 @@ class DialogThingtalk extends LitElement {
@state() private _submitting = false;
@state() private _opened = false;
@state() private _placeholders?: PlaceholderContainer;
@query("#input") private _input?: PaperInputElement;
@@ -51,7 +47,6 @@ class DialogThingtalk extends LitElement {
public async showDialog(params: ThingtalkDialogParams): Promise<void> {
this._params = params;
this._error = undefined;
this._opened = true;
if (params.input) {
this._value = params.input;
await this.updateComplete;
@@ -61,10 +56,10 @@ class DialogThingtalk extends LitElement {
public closeDialog() {
this._placeholders = undefined;
this._params = undefined;
if (this._input) {
this._input.value = null;
}
this._opened = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -77,26 +72,22 @@ class DialogThingtalk extends LitElement {
<ha-thingtalk-placeholders
.hass=${this.hass}
.placeholders=${this._placeholders}
.opened=${this._opened}
.skip=${this._skip}
@opened-changed=${this._openedChanged}
@closed=${this.closeDialog}
@placeholders-filled=${this._handlePlaceholders}
>
</ha-thingtalk-placeholders>
`;
}
return html`
<ha-paper-dialog
with-backdrop
.opened=${this._opened}
@opened-changed=${this._openedChanged}
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${this.hass.localize(
`ui.panel.config.automation.thingtalk.task_selection.header`
)}
>
<h2>
${this.hass.localize(
`ui.panel.config.automation.thingtalk.task_selection.header`
)}
</h2>
<paper-dialog-scrollable>
<div>
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
${this.hass.localize(
`ui.panel.config.automation.thingtalk.task_selection.introduction`
@@ -143,23 +134,25 @@ class DialogThingtalk extends LitElement {
class="attribution"
>Powered by Almond</a
>
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
<mwc-button class="left" @click=${this._skip}>
${this.hass.localize(`ui.common.skip`)}
</mwc-button>
<mwc-button @click=${this._generate} .disabled=${this._submitting}>
${this._submitting
? html`<ha-circular-progress
active
size="small"
title="Creating your automation..."
></ha-circular-progress>`
: ""}
${this.hass.localize(`ui.panel.config.automation.thingtalk.create`)}
</mwc-button>
</div>
</ha-paper-dialog>
<mwc-button class="left" @click=${this._skip} slot="secondaryAction">
${this.hass.localize(`ui.common.skip`)}
</mwc-button>
<mwc-button
@click=${this._generate}
.disabled=${this._submitting}
slot="primaryAction"
>
${this._submitting
? html`<ha-circular-progress
active
size="small"
title="Creating your automation..."
></ha-circular-progress>`
: ""}
${this.hass.localize(`ui.panel.config.automation.thingtalk.create`)}
</mwc-button>
</ha-dialog>
`;
}
@@ -234,12 +227,6 @@ class DialogThingtalk extends LitElement {
this.closeDialog();
};
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
if (!ev.detail.value) {
this.closeDialog();
}
}
private _handleKeyUp(ev: KeyboardEvent) {
if (ev.keyCode === 13) {
this._generate();
@@ -255,7 +242,7 @@ class DialogThingtalk extends LitElement {
haStyle,
haStyleDialog,
css`
ha-paper-dialog {
ha-dialog {
max-width: 500px;
}
mwc-button.left {
@@ -25,7 +25,6 @@ import {
import { subscribeEntityRegistry } from "../../../../data/entity_registry";
import { domainToName } from "../../../../data/integration";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../../../../polymer-types";
import { haStyleDialog } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import { Placeholder, PlaceholderContainer } from "./dialog-thingtalk";
@@ -124,18 +123,14 @@ export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) {
protected render(): TemplateResult {
return html`
<ha-paper-dialog
modal
with-backdrop
.opened=${this.opened}
@opened-changed=${this._openedChanged}
<ha-dialog
open
scrimClickAction
.heading=${this.hass.localize(
`ui.panel.config.automation.thingtalk.link_devices.header`
)}
>
<h2>
${this.hass.localize(
`ui.panel.config.automation.thingtalk.link_devices.header`
)}
</h2>
<paper-dialog-scrollable>
<div>
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
${Object.entries(this.placeholders).map(
([type, placeholders]) =>
@@ -243,16 +238,18 @@ export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) {
})}
`
)}
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
<mwc-button class="left" @click=${this.skip}>
${this.hass.localize(`ui.common.skip`)}
</mwc-button>
<mwc-button @click=${this._done} .disabled=${!this._isDone}>
${this.hass.localize(`ui.panel.config.automation.thingtalk.create`)}
</mwc-button>
</div>
</ha-paper-dialog>
<mwc-button @click=${this.skip} slot="secondaryAction">
${this.hass.localize(`ui.common.skip`)}
</mwc-button>
<mwc-button
@click=${this._done}
.disabled=${!this._isDone}
slot="primaryAction"
>
${this.hass.localize(`ui.panel.config.automation.thingtalk.create`)}
</mwc-button>
</ha-dialog>
`;
}
@@ -440,11 +437,6 @@ export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) {
this.requestUpdate("_placeholderValues");
}
});
fireEvent(
this.shadowRoot!.querySelector("ha-paper-dialog")! as HTMLElement,
"iron-resize"
);
}
private _entityPicked(ev: Event): void {
@@ -465,24 +457,16 @@ export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) {
fireEvent(this, "placeholders-filled", { value: this._placeholderValues });
}
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
// The opened-changed event doesn't leave the shadowdom so we re-dispatch it
this.dispatchEvent(new CustomEvent(ev.type, ev));
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-paper-dialog {
ha-dialog {
max-width: 500px;
}
mwc-button.left {
margin-right: auto;
}
paper-dialog-scrollable {
margin-top: 10px;
}
h3 {
margin: 10px 0 0 0;
font-weight: 500;
@@ -1,17 +1,19 @@
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import type { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox";
import "@material/mwc-select";
import type { Select } from "@material/mwc-select";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stringCompare } from "../../../../common/string/compare";
import { handleStructError } from "../../../../common/structs/handle-errors";
import { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-card";
import "../../../../components/ha-alert";
import "../../../../components/ha-icon-button";
import type { Trigger } from "../../../../data/automation";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
@@ -85,6 +87,19 @@ export default class HaAutomationTriggerRow extends LitElement {
@state() private _yamlMode = false;
private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string][] =>
OPTIONS.map(
(action) =>
[
action,
localize(
`ui.panel.config.automation.editor.triggers.type.${action}.label`
),
] as [string, string]
).sort((a, b) => stringCompare(a[1], b[1]))
);
protected render() {
const selected = OPTIONS.indexOf(this.trigger.platform);
const yamlMode = this._yamlMode || selected === -1;
@@ -121,9 +136,12 @@ export default class HaAutomationTriggerRow extends LitElement {
</ha-button-menu>
</div>
${this._warnings
? html`<div class="warning">
${this.hass.localize("ui.errors.config.editor_not_supported")}:
<br />
? html`<ha-alert
alert-type="warning"
.title=${this.hass.localize(
"ui.errors.config.editor_not_supported"
)}
>
${this._warnings.length && this._warnings[0] !== undefined
? html` <ul>
${this._warnings.map(
@@ -132,7 +150,7 @@ export default class HaAutomationTriggerRow extends LitElement {
</ul>`
: ""}
${this.hass.localize("ui.errors.config.edit_in_yaml_supported")}
</div>`
</ha-alert>`
: ""}
${yamlMode
? html`
@@ -156,28 +174,21 @@ export default class HaAutomationTriggerRow extends LitElement {
></ha-yaml-editor>
`
: html`
<paper-dropdown-menu-light
<mwc-select
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type_select"
)}
no-animations
.value=${this.trigger.platform}
naturalMenuWidth
@selected=${this._typeChanged}
>
<paper-listbox
slot="dropdown-content"
.selected=${selected}
@iron-select=${this._typeChanged}
>
${OPTIONS.map(
(opt) => html`
<paper-item .platform=${opt}>
${this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.${opt}.label`
)}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
${this._processedTypes(this.hass.localize).map(
([opt, label]) => html`
<mwc-list-item .value=${opt}>${label}</mwc-list-item>
`
)}
</mwc-select>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.id"
@@ -233,8 +244,7 @@ export default class HaAutomationTriggerRow extends LitElement {
}
private _typeChanged(ev: CustomEvent) {
const type = ((ev.target as PaperListboxElement)?.selectedItem as any)
?.platform;
const type = (ev.target as Select).value;
if (!type) {
return;
@@ -1,10 +1,8 @@
import "@polymer/paper-radio-button/paper-radio-button";
import "@polymer/paper-radio-group/paper-radio-group";
import type { PaperRadioGroupElement } from "@polymer/paper-radio-group/paper-radio-group";
import { html, LitElement } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/entity/ha-entity-picker";
import type { HaRadio } from "../../../../../components/ha-radio";
import type { GeoLocationTrigger } from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
import { handleChangeEvent } from "../ha-automation-trigger-row";
@@ -12,16 +10,16 @@ import { handleChangeEvent } from "../ha-automation-trigger-row";
const includeDomains = ["zone"];
@customElement("ha-automation-trigger-geo_location")
export default class HaGeolocationTrigger extends LitElement {
export class HaGeolocationTrigger extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public trigger!: GeoLocationTrigger;
@property({ attribute: false }) public trigger!: GeoLocationTrigger;
public static get defaultConfig() {
return {
source: "",
zone: "",
event: "enter",
event: "enter" as GeoLocationTrigger["event"],
};
}
@@ -47,27 +45,35 @@ export default class HaGeolocationTrigger extends LitElement {
allow-custom-entity
.includeDomains=${includeDomains}
></ha-entity-picker>
<label id="eventlabel">
<label>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.geo_location.event"
)}
</label>
<paper-radio-group
.selected=${event}
aria-labelledby="eventlabel"
@paper-radio-group-changed=${this._radioGroupPicked}
>
<paper-radio-button name="enter">
${this.hass.localize(
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.geo_location.enter"
)}
</paper-radio-button>
<paper-radio-button name="leave">
${this.hass.localize(
>
<ha-radio
name="event"
value="enter"
.checked=${event === "enter"}
@change=${this._radioGroupPicked}
></ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.geo_location.leave"
)}
</paper-radio-button>
</paper-radio-group>
>
<ha-radio
name="event"
value="leave"
.checked=${event === "leave"}
@change=${this._radioGroupPicked}
></ha-radio>
</ha-formfield>
</label>
`;
}
@@ -87,10 +93,17 @@ export default class HaGeolocationTrigger extends LitElement {
fireEvent(this, "value-changed", {
value: {
...this.trigger,
event: (ev.target as PaperRadioGroupElement).selected,
event: (ev.target as HaRadio).value,
},
});
}
static styles = css`
label {
display: flex;
align-items: center;
}
`;
}
declare global {
@@ -1,21 +1,21 @@
import "@polymer/paper-radio-button/paper-radio-button";
import "@polymer/paper-radio-group/paper-radio-group";
import type { PaperRadioGroupElement } from "@polymer/paper-radio-group/paper-radio-group";
import { html, LitElement } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { HaRadio } from "../../../../../components/ha-radio";
import type { HassTrigger } from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-formfield";
import "../../../../../components/ha-radio";
@customElement("ha-automation-trigger-homeassistant")
export default class HaHassTrigger extends LitElement {
export class HaHassTrigger extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public trigger!: HassTrigger;
@property({ attribute: false }) public trigger!: HassTrigger;
public static get defaultConfig() {
return {
event: "start",
event: "start" as HassTrigger["event"],
};
}
@@ -26,23 +26,31 @@ export default class HaHassTrigger extends LitElement {
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.homeassistant.event"
)}
</label>
<paper-radio-group
.selected=${event}
aria-labelledby="eventlabel"
@paper-radio-group-changed=${this._radioGroupPicked}
>
<paper-radio-button name="start">
${this.hass.localize(
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.homeassistant.start"
)}
</paper-radio-button>
<paper-radio-button name="shutdown">
${this.hass.localize(
>
<ha-radio
name="event"
value="start"
.checked=${event === "start"}
@change=${this._radioGroupPicked}
></ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.homeassistant.shutdown"
)}
</paper-radio-button>
</paper-radio-group>
>
<ha-radio
name="event"
value="shutdown"
.checked=${event === "shutdown"}
@change=${this._radioGroupPicked}
></ha-radio>
</ha-formfield>
</label>
`;
}
@@ -51,10 +59,17 @@ export default class HaHassTrigger extends LitElement {
fireEvent(this, "value-changed", {
value: {
...this.trigger,
event: (ev.target as PaperRadioGroupElement).selected,
event: (ev.target as HaRadio).value,
},
});
}
static styles = css`
label {
display: flex;
align-items: center;
}
`;
}
declare global {

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