diff --git a/.yarn/patches/@material/mwc-icon-button/remove-icon.patch b/.yarn/patches/@material/mwc-icon-button/remove-icon.patch new file mode 100644 index 0000000000..b724248eb0 --- /dev/null +++ b/.yarn/patches/@material/mwc-icon-button/remove-icon.patch @@ -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()} +- ${this.icon} + \ No newline at end of file diff --git a/build-scripts/bundle.js b/build-scripts/bundle.js index 277fa54bee..c15e1ab1dc 100644 --- a/build-scripts/bundle.js +++ b/build-scripts/bundle.js @@ -210,6 +210,9 @@ module.exports.config = { publicPath: publicPath(latestBuild), isProdBuild, latestBuild, + defineOverlay: { + __DEMO__: true, + }, }; }, }; diff --git a/build-scripts/gulp/gen-icons-json.js b/build-scripts/gulp/gen-icons-json.js index 58bb4faaaa..7fce535c4d 100644 --- a/build-scripts/gulp/gen-icons-json.js +++ b/build-scripts/gulp/gen-icons-json.js @@ -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(); }); diff --git a/build-scripts/gulp/hassio.js b/build-scripts/gulp/hassio.js index 696e742a05..08370ad7c6 100644 --- a/build-scripts/gulp/hassio.js +++ b/build-scripts/gulp/hassio.js @@ -17,7 +17,6 @@ gulp.task( process.env.NODE_ENV = "development"; }, "clean-hassio", - "gen-icons-json", "gen-index-hassio-dev", "build-supervisor-translations", "copy-translations-supervisor", @@ -34,7 +33,6 @@ gulp.task( process.env.NODE_ENV = "production"; }, "clean-hassio", - "gen-icons-json", "build-supervisor-translations", "copy-translations-supervisor", "build-locale-data", diff --git a/build-scripts/gulp/webpack.js b/build-scripts/gulp/webpack.js index e257fd58a7..2d6e0a863f 100644 --- a/build-scripts/gulp/webpack.js +++ b/build-scripts/gulp/webpack.js @@ -173,6 +173,7 @@ gulp.task("webpack-dev-server-gallery", () => compiler: webpack(bothBuilds(createGalleryConfig, { isProdBuild: false })), contentBase: paths.gallery_output_root, port: 8100, + listenHost: "0.0.0.0", }) ); diff --git a/cast/src/launcher/layout/hc-cast.ts b/cast/src/launcher/layout/hc-cast.ts index 078850d4f8..9c8debb48b 100644 --- a/cast/src/launcher/layout/hc-cast.ts +++ b/cast/src/launcher/layout/hc-cast.ts @@ -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`

- + Start Casting

@@ -111,7 +113,7 @@ class HcCast extends LitElement { ${this.castManager.status ? html` - + Manage ` @@ -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; } diff --git a/cast/src/launcher/layout/hc-connect.ts b/cast/src/launcher/layout/hc-connect.ts index a3fd436e97..346b704d78 100644 --- a/cast/src/launcher/layout/hc-connect.ts +++ b/cast/src/launcher/layout/hc-connect.ts @@ -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 {
Show Demo - +
Authorize @@ -307,7 +308,7 @@ export class HcConnect extends LitElement { color: darkred; } - mwc-button ha-icon { + mwc-button ha-svg-icon { margin-left: 8px; } diff --git a/demo/src/custom-cards/cast-demo-row.ts b/demo/src/custom-cards/cast-demo-row.ts index 8fbecb4eb5..e400031878 100644 --- a/demo/src/custom-cards/cast-demo-row.ts +++ b/demo/src/custom-cards/cast-demo-row.ts @@ -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` - +
Show Chromecast interface
@@ -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); } diff --git a/demo/src/stubs/area_registry.ts b/demo/src/stubs/area_registry.ts new file mode 100644 index 0000000000..b7d8e5a34b --- /dev/null +++ b/demo/src/stubs/area_registry.ts @@ -0,0 +1,7 @@ +import { AreaRegistryEntry } from "../../../src/data/area_registry"; +import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; + +export const mockAreaRegistry = ( + hass: MockHomeAssistant, + data: AreaRegistryEntry[] = [] +) => hass.mockWS("config/area_registry/list", () => data); diff --git a/demo/src/stubs/device_registry.ts b/demo/src/stubs/device_registry.ts new file mode 100644 index 0000000000..28c47e4a96 --- /dev/null +++ b/demo/src/stubs/device_registry.ts @@ -0,0 +1,7 @@ +import { DeviceRegistryEntry } from "../../../src/data/device_registry"; +import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; + +export const mockDeviceRegistry = ( + hass: MockHomeAssistant, + data: DeviceRegistryEntry[] = [] +) => hass.mockWS("config/device_registry/list", () => data); diff --git a/demo/src/stubs/entity_registry.ts b/demo/src/stubs/entity_registry.ts new file mode 100644 index 0000000000..8f548629e7 --- /dev/null +++ b/demo/src/stubs/entity_registry.ts @@ -0,0 +1,7 @@ +import { EntityRegistryEntry } from "../../../src/data/entity_registry"; +import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; + +export const mockEntityRegistry = ( + hass: MockHomeAssistant, + data: EntityRegistryEntry[] = [] +) => hass.mockWS("config/entity_registry/list", () => data); diff --git a/demo/src/stubs/hassio_supervisor.ts b/demo/src/stubs/hassio_supervisor.ts new file mode 100644 index 0000000000..95c0d330d4 --- /dev/null +++ b/demo/src/stubs/hassio_supervisor.ts @@ -0,0 +1,59 @@ +import { HassioSupervisorInfo } from "../../../src/data/hassio/supervisor"; +import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; + +export const mockHassioSupervisor = (hass: MockHomeAssistant) => { + hass.config.components.push("hassio"); + hass.mockWS("supervisor/api", (msg) => { + if (msg.endpoint === "/supervisor/info") { + const data: HassioSupervisorInfo = { + version: "2021.10.dev0805", + version_latest: "2021.10.dev0806", + update_available: true, + channel: "dev", + arch: "aarch64", + supported: true, + healthy: true, + ip_address: "172.30.32.2", + wait_boot: 5, + timezone: "America/Los_Angeles", + logging: "info", + debug: false, + debug_block: false, + diagnostics: true, + addons: [ + { + name: "Visual Studio Code", + slug: "a0d7b954_vscode", + description: + "Fully featured VSCode experience, to edit your HA config in the browser, including auto-completion!", + state: "started", + version: "3.6.2", + version_latest: "3.6.2", + update_available: false, + repository: "a0d7b954", + icon: true, + logo: true, + }, + { + name: "Z-Wave JS", + slug: "core_zwave_js", + description: + "Control a ZWave network with Home Assistant Z-Wave JS", + state: "started", + version: "0.1.45", + version_latest: "0.1.45", + update_available: false, + repository: "core", + icon: true, + logo: true, + }, + ] as any, + addons_repositories: [ + "https://github.com/hassio-addons/repository", + ] as any, + }; + return data; + } + return Promise.reject(`${msg.method} ${msg.endpoint} is not implemented`); + }); +}; diff --git a/gallery/script/netlify_build_gallery b/gallery/script/netlify_build_gallery new file mode 100755 index 0000000000..173b77d73f --- /dev/null +++ b/gallery/script/netlify_build_gallery @@ -0,0 +1,35 @@ +#!/bin/bash + +TARGET_LABEL="Needs gallery preview" + +if [[ "$NETLIFY" != "true" ]]; then + echo "This script can only be run on Netlify" + exit 1 +fi + +function createStatus() { + state="$1" + description="$2" + target_url="$3" + curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token $GITHUB_TOKEN" \ + "https://api.github.com/repos/home-assistant/frontend/statuses/$COMMIT_REF" \ + -d '{"state": "'"${state}"'", "context": "Netlify/Gallery Preview Build", "description": "'"$description"'", "target_url": "'"$target_url"'"}' +} + + +if [[ "${PULL_REQUEST}" == "false" ]]; then + gulp build-gallery +else + if [[ "$(curl -sSLf -H "Accept: application/vnd.github.v3+json" -H "Authorization: token $GITHUB_TOKEN" \ + "https://api.github.com/repos/home-assistant/frontend/pulls/${REVIEW_ID}" | jq '.labels[].name' -r)" =~ "$TARGET_LABEL" ]]; then + createStatus "pending" "Building gallery preview" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID" + gulp build-gallery + if [ $? -eq 0 ]; then + createStatus "success" "Build complete" "$DEPLOY_URL" + else + createStatus "error" "Build failed" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID" + fi + else + createStatus "success" "Build was not requested by PR label" + fi +fi diff --git a/gallery/src/components/demo-black-white-row.ts b/gallery/src/components/demo-black-white-row.ts new file mode 100644 index 0000000000..a3b1233ffb --- /dev/null +++ b/gallery/src/components/demo-black-white-row.ts @@ -0,0 +1,143 @@ +import { Button } from "@material/mwc-button"; +import { html, LitElement, css, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; +import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element"; +import { fireEvent } from "../../../src/common/dom/fire_event"; + +@customElement("demo-black-white-row") +class DemoBlackWhiteRow extends LitElement { + @property() title!: string; + + @property() value!: any; + + @property() disabled = false; + + protected render(): TemplateResult { + return html` +
+
+ +
+ +
+
+ + Submit + +
+
+
+
+ +
+ +
+
+ + Submit + +
+
+
${JSON.stringify(this.value, undefined, 2)}
+
+
+ `; + } + + firstUpdated(changedProps) { + super.firstUpdated(changedProps); + applyThemesOnElement( + this.shadowRoot!.querySelector(".dark"), + { + default_theme: "default", + default_dark_theme: "default", + themes: {}, + darkMode: false, + }, + "default", + { dark: true } + ); + } + + handleSubmit(ev) { + const content = (ev.target as Button).closest(".content")!; + fireEvent(this, "submitted" as any, { + slot: content.classList.contains("light") ? "light" : "dark", + }); + } + + static styles = css` + .row { + display: flex; + } + .content { + padding: 50px 0; + background-color: var(--primary-background-color); + } + .light { + flex: 1; + padding-left: 50px; + padding-right: 50px; + box-sizing: border-box; + } + .light ha-card { + margin-left: auto; + } + .dark { + display: flex; + flex: 1; + padding-left: 50px; + box-sizing: border-box; + flex-wrap: wrap; + } + ha-card { + width: 400px; + } + pre { + width: 300px; + margin: 0 16px 0; + overflow: auto; + color: var(--primary-text-color); + } + .card-actions { + display: flex; + flex-direction: row-reverse; + border-top: none; + } + @media only screen and (max-width: 1500px) { + .light { + flex: initial; + } + } + @media only screen and (max-width: 1000px) { + .light, + .dark { + padding: 16px; + } + .row, + .dark { + flex-direction: column; + } + ha-card { + margin: 0 auto; + width: 100%; + max-width: 400px; + } + pre { + margin: 16px auto; + } + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "demo-black-white-row": DemoBlackWhiteRow; + } +} diff --git a/gallery/src/demos/demo-automation-editor-action.ts b/gallery/src/demos/demo-automation-editor-action.ts new file mode 100644 index 0000000000..1aea950b92 --- /dev/null +++ b/gallery/src/demos/demo-automation-editor-action.ts @@ -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` + + ${["light", "dark"].map( + (slot) => + html` + + ` + )} + + ` + )} + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-ha-automation-editor-action": DemoHaAutomationEditorAction; + } +} diff --git a/gallery/src/demos/demo-automation-editor-condition.ts b/gallery/src/demos/demo-automation-editor-condition.ts new file mode 100644 index 0000000000..4b1ebd8a13 --- /dev/null +++ b/gallery/src/demos/demo-automation-editor-condition.ts @@ -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` + + ${["light", "dark"].map( + (slot) => + html` + + ` + )} + + ` + )} + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-ha-automation-editor-condition": DemoHaAutomationEditorCondition; + } +} diff --git a/gallery/src/demos/demo-automation-editor-trigger.ts b/gallery/src/demos/demo-automation-editor-trigger.ts new file mode 100644 index 0000000000..0bc04b7435 --- /dev/null +++ b/gallery/src/demos/demo-automation-editor-trigger.ts @@ -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` + + ${["light", "dark"].map( + (slot) => + html` + + ` + )} + + ` + )} + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-ha-automation-editor-trigger": DemoHaAutomationEditorTrigger; + } +} diff --git a/gallery/src/demos/demo-ha-bar.ts b/gallery/src/demos/demo-ha-bar.ts new file mode 100644 index 0000000000..83f47f5dd6 --- /dev/null +++ b/gallery/src/demos/demo-ha-bar.ts @@ -0,0 +1,85 @@ +import { html, css, LitElement, TemplateResult } from "lit"; +import { customElement } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import "../../../src/components/ha-bar"; +import "../../../src/components/ha-card"; + +const bars: { + min?: number; + max?: number; + value: number; + warning?: number; + error?: number; +}[] = [ + { + value: 33, + }, + { + value: 150, + }, + { + min: -10, + value: 0, + }, + { + value: 80, + }, + { + value: 200, + max: 13, + }, + { + value: 4, + min: 13, + }, +]; + +@customElement("demo-ha-bar") +export class DemoHaBar extends LitElement { + protected render(): TemplateResult { + return html` + ${bars + .map((bar) => ({ min: 0, max: 100, warning: 70, error: 90, ...bar })) + .map( + (bar) => html` + +
+
Config: ${JSON.stringify(bar)}
+ bar.warning, + error: bar.value > bar.error, + })} + .min=${bar.min} + .max=${bar.max} + .value=${bar.value} + > + +
+
+ ` + )} + `; + } + + static get styles() { + return css` + ha-card { + max-width: 600px; + margin: 24px auto; + } + .warning { + --ha-bar-primary-color: var(--warning-color); + } + .error { + --ha-bar-primary-color: var(--error-color); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-ha-bar": DemoHaBar; + } +} diff --git a/gallery/src/demos/demo-ha-chip.ts b/gallery/src/demos/demo-ha-chip.ts new file mode 100644 index 0000000000..8344d4eb64 --- /dev/null +++ b/gallery/src/demos/demo-ha-chip.ts @@ -0,0 +1,61 @@ +import { mdiHomeAssistant } from "@mdi/js"; +import { css, html, LitElement, TemplateResult } from "lit"; +import { customElement } from "lit/decorators"; +import "../../../src/components/ha-card"; +import "../../../src/components/ha-chip"; +import "../../../src/components/ha-svg-icon"; + +const chips: { + icon?: string; + content?: string; +}[] = [ + {}, + { + icon: mdiHomeAssistant, + }, + { + content: "Content", + }, + { + icon: mdiHomeAssistant, + content: "Content", + }, +]; + +@customElement("demo-ha-chip") +export class DemoHaChip extends LitElement { + protected render(): TemplateResult { + return html` + +
+ ${chips.map( + (chip) => html` + + ${chip.icon + ? html` + ` + : ""} + ${chip.content} + + ` + )} +
+
+ `; + } + + static get styles() { + return css` + ha-card { + max-width: 600px; + margin: 24px auto; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-ha-chip": DemoHaChip; + } +} diff --git a/gallery/src/demos/demo-ha-form.ts b/gallery/src/demos/demo-ha-form.ts index 9f5f9a3d54..abc2cca571 100644 --- a/gallery/src/demos/demo-ha-form.ts +++ b/gallery/src/demos/demo-ha-form.ts @@ -1,23 +1,25 @@ /* eslint-disable lit/no-template-arrow */ -import { LitElement, TemplateResult, css, html } from "lit"; +import "@material/mwc-button"; +import { LitElement, TemplateResult, html } from "lit"; import { customElement } from "lit/decorators"; +import { computeInitialHaFormData } from "../../../src/components/ha-form/compute-initial-ha-form-data"; +import type { HaFormSchema } from "../../../src/components/ha-form/types"; import "../../../src/components/ha-form/ha-form"; -import "../../../src/components/ha-card"; -import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element"; -import type { HaFormSchema } from "../../../src/components/ha-form/ha-form"; +import "../components/demo-black-white-row"; const SCHEMAS: { title: string; translations?: Record; error?: Record; schema: HaFormSchema[]; + data?: Record; }[] = [ { title: "Authentication", translations: { username: "Username", password: "Password", - invalid_login: "Invalid login", + invalid_login: "Invalid username or password", }, error: { base: "invalid_login", @@ -57,6 +59,11 @@ const SCHEMAS: { optional: true, default: 10, }, + { + type: "float", + name: "float", + required: true, + }, { type: "string", name: "string", @@ -83,6 +90,80 @@ const SCHEMAS: { optional: true, default: ["default"], }, + { + type: "positive_time_period_dict", + name: "time", + required: true, + }, + ], + }, + { + title: "Numbers", + schema: [ + { + type: "integer", + name: "int", + required: true, + }, + { + type: "integer", + name: "int with default", + optional: true, + default: 10, + }, + { + type: "integer", + name: "int range required", + required: true, + default: 5, + valueMin: 0, + valueMax: 10, + }, + { + type: "integer", + name: "int range optional", + optional: true, + valueMin: 0, + valueMax: 10, + }, + ], + }, + { + title: "select", + schema: [ + { + type: "select", + options: [ + ["default", "Default"], + ["other", "Other"], + ], + name: "select", + required: true, + default: "default", + }, + { + type: "select", + options: [ + ["default", "Default"], + ["other", "Other"], + ], + name: "select optional", + optional: true, + }, + { + type: "select", + options: [ + ["default", "Default"], + ["other", "Other"], + ["uno", "mas"], + ["one", "more"], + ["and", "another_one"], + ["option", "1000"], + ], + name: "select many otions", + optional: true, + default: "default", + }, ], }, { @@ -95,7 +176,7 @@ const SCHEMAS: { other: "Other", }, name: "multi", - optional: true, + required: true, default: ["default"], }, { @@ -108,101 +189,90 @@ const SCHEMAS: { and: "another_one", option: "1000", }, - name: "multi", + name: "multi many otions", optional: true, default: ["default"], }, ], }, + { + title: "Field specific error", + data: { + new_password: "hello", + new_password_2: "bye", + }, + translations: { + new_password: "New Password", + new_password_2: "Re-type Password", + not_match: "The passwords do not match", + }, + error: { + new_password_2: "not_match", + }, + schema: [ + { + type: "string", + name: "new_password", + required: true, + }, + { + type: "string", + name: "new_password_2", + required: true, + }, + ], + }, ]; @customElement("demo-ha-form") class DemoHaForm extends LitElement { - private lightModeData: any = []; + private data = SCHEMAS.map( + ({ schema, data }) => data || computeInitialHaFormData(schema) + ); - private darkModeData: any = []; + private disabled = SCHEMAS.map(() => false); protected render(): TemplateResult { return html` ${SCHEMAS.map((info, idx) => { const translations = info.translations || {}; - const computeLabel = (schema) => - translations[schema.name] || schema.name; - const computeError = (error) => translations[error] || error; - - return [ - [this.lightModeData, "light"], - [this.darkModeData, "dark"], - ].map( - ([data, type]) => html` -
- -
- { - data[idx] = e.detail.value; - this.requestUpdate(); - }} - > -
-
-
${JSON.stringify(data[idx], undefined, 2)}
-
- ` - ); + return html` + { + this.disabled[idx] = true; + this.requestUpdate(); + setTimeout(() => { + this.disabled[idx] = false; + this.requestUpdate(); + }, 2000); + }} + > + ${["light", "dark"].map( + (slot) => html` + translations[error] || error} + .computeLabel=${(schema) => + translations[schema.name] || schema.name} + @value-changed=${(e) => { + this.data[idx] = e.detail.value; + this.requestUpdate(); + }} + > + ` + )} + + `; })} `; } - - firstUpdated(changedProps) { - super.firstUpdated(changedProps); - this.shadowRoot!.querySelectorAll("[data-type=dark]").forEach((el) => { - applyThemesOnElement( - el, - { - default_theme: "default", - default_dark_theme: "default", - themes: {}, - darkMode: false, - }, - "default", - { dark: true } - ); - }); - } - - static styles = css` - .row { - margin: 0 auto; - max-width: 800px; - display: flex; - padding: 50px; - background-color: var(--primary-background-color); - } - ha-card { - width: 100%; - max-width: 384px; - } - pre { - width: 400px; - margin: 0 16px; - overflow: auto; - color: var(--primary-text-color); - } - @media only screen and (max-width: 800px) { - .row { - flex-direction: column; - } - pre { - margin: 16px 0; - } - } - `; } declare global { diff --git a/gallery/src/demos/demo-ha-label-badge.ts b/gallery/src/demos/demo-ha-label-badge.ts new file mode 100644 index 0000000000..277c539040 --- /dev/null +++ b/gallery/src/demos/demo-ha-label-badge.ts @@ -0,0 +1,122 @@ +import { html, css, LitElement, TemplateResult } from "lit"; +import { customElement } from "lit/decorators"; +import "../../../src/components/ha-label-badge"; +import "../../../src/components/ha-card"; + +const colors = ["#03a9f4", "#ffa600", "#43a047"]; + +const badges: { + label?: string; + description?: string; + image?: string; +}[] = [ + { + label: "label", + }, + { + label: "label", + description: "Description", + }, + { + description: "Description", + }, + { + label: "label", + description: "Description", + image: "/images/living_room.png", + }, + { + description: "Description", + image: "/images/living_room.png", + }, + { + label: "label", + image: "/images/living_room.png", + }, + { + image: "/images/living_room.png", + }, + { + label: "big label", + }, + { + label: "big label", + description: "Description", + }, + { + label: "big label", + description: "Description", + image: "/images/living_room.png", + }, +]; + +@customElement("demo-ha-label-badge") +export class DemoHaLabelBadge extends LitElement { + protected render(): TemplateResult { + return html` + +
+ ${badges.map( + (badge) => html` + + + ` + )} +
+
+ +
+ ${badges.map( + (badge) => html` +
+ + +
${JSON.stringify(badge, null, 2)}
+
+ ` + )} +
+
+ `; + } + + static get styles() { + return css` + ha-card { + max-width: 600px; + margin: 24px auto; + } + pre { + margin-left: 16px; + background-color: var(--markdown-code-background-color); + padding: 8px; + } + .badge { + display: flex; + flex-direction: row; + margin-bottom: 16px; + align-items: center; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-ha-label-badge": DemoHaLabelBadge; + } +} diff --git a/gallery/src/demos/demo-ha-selector.ts b/gallery/src/demos/demo-ha-selector.ts new file mode 100644 index 0000000000..919a2c2419 --- /dev/null +++ b/gallery/src/demos/demo-ha-selector.ts @@ -0,0 +1,131 @@ +/* eslint-disable lit/no-template-arrow */ +import "@material/mwc-button"; +import { LitElement, TemplateResult, css, html } from "lit"; +import { customElement, state } from "lit/decorators"; +import "../../../src/components/ha-selector/ha-selector"; +import "../../../src/components/ha-settings-row"; +import { provideHass } from "../../../src/fake_data/provide_hass"; +import type { HomeAssistant } from "../../../src/types"; +import "../components/demo-black-white-row"; +import { BlueprintInput } from "../../../src/data/blueprint"; +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"; + +const SCHEMAS: { + name: string; + input: Record; +}[] = [ + { + name: "One of each", + input: { + entity: { name: "Entity", selector: { entity: {} } }, + device: { name: "Device", selector: { device: {} } }, + addon: { name: "Addon", selector: { addon: {} } }, + area: { name: "Area", selector: { area: {} } }, + target: { name: "Target", selector: { target: {} } }, + number_box: { + name: "Number Box", + selector: { + number: { + min: 0, + max: 10, + mode: "box", + }, + }, + }, + number_slider: { + name: "Number Slider", + selector: { + number: { + min: 0, + max: 10, + mode: "slider", + }, + }, + }, + boolean: { name: "Boolean", selector: { boolean: {} } }, + time: { name: "Time", selector: { time: {} } }, + action: { name: "Action", selector: { action: {} } }, + text: { name: "Text", selector: { text: { multiline: false } } }, + text_multiline: { + name: "Text multiline", + selector: { text: { multiline: true } }, + }, + object: { name: "Object", selector: { object: {} } }, + select: { + name: "Select", + selector: { select: { options: ["Option 1", "Option 2"] } }, + }, + }, + }, +]; + +@customElement("demo-ha-selector") +class DemoHaSelector extends LitElement { + @state() private hass!: HomeAssistant; + + private data = SCHEMAS.map(() => ({})); + + 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 { + return html` + ${SCHEMAS.map((info, idx) => { + const data = this.data[idx]; + const valueChanged = (ev) => { + this.data[idx] = { + ...data, + [ev.target.key]: ev.detail.value, + }; + this.requestUpdate(); + }; + return html` + + ${["light", "dark"].map((slot) => + Object.entries(info.input).map( + ([key, value]) => + html` + + ${value?.name || key} + ${value?.description} + + + ` + ) + )} + + `; + })} + `; + } + + static styles = css` + paper-input, + ha-selector { + width: 60; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "demo-ha-selector": DemoHaSelector; + } +} diff --git a/gallery/src/demos/demo-integration-card.ts b/gallery/src/demos/demo-integration-card.ts index 0b08a0aa3e..4da42f1279 100644 --- a/gallery/src/demos/demo-integration-card.ts +++ b/gallery/src/demos/demo-integration-card.ts @@ -187,6 +187,7 @@ const createEntityRegistryEntries = ( device_id: "mock-device-id", area_id: null, disabled_by: null, + entity_category: null, entity_id: "binary_sensor.updater", name: null, icon: null, @@ -211,6 +212,7 @@ const createDeviceRegistryEntries = ( area_id: null, name_by_user: null, disabled_by: null, + configuration_url: null, }, ]; diff --git a/gallery/src/ha-gallery.js b/gallery/src/ha-gallery.js index 9d09da0119..7f53dd717a 100644 --- a/gallery/src/ha-gallery.js +++ b/gallery/src/ha-gallery.js @@ -65,10 +65,11 @@ class HaGallery extends PolymerElement { + > + +
[[_withDefault(_demo, "Home Assistant Gallery")]]
diff --git a/hassio/src/addon-store/hassio-addon-store.ts b/hassio/src/addon-store/hassio-addon-store.ts index a1dbe3237f..4cc55898a1 100644 --- a/hassio/src/addon-store/hassio-addon-store.ts +++ b/hassio/src/addon-store/hassio-addon-store.ts @@ -1,4 +1,3 @@ -import "@material/mwc-icon-button/mwc-icon-button"; import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import "@material/mwc-list/mwc-list-item"; import { mdiDotsVertical } from "@mdi/js"; @@ -18,7 +17,7 @@ import { navigate } from "../../../src/common/navigate"; import "../../../src/common/search/search-input"; import { extractSearchParam } from "../../../src/common/url/search-params"; import "../../../src/components/ha-button-menu"; -import "../../../src/components/ha-svg-icon"; +import "../../../src/components/ha-icon-button"; import { HassioAddonInfo, HassioAddonRepository, @@ -92,9 +91,11 @@ class HassioAddonStore extends LitElement { slot="toolbar-icon" @action=${this._handleAction} > - - - + ${this.supervisor.localize("store.repositories")} @@ -113,6 +114,7 @@ class HassioAddonStore extends LitElement { : html` +
+ ${this.addon.stage !== "stable" + ? html` + + + ${this.supervisor.localize( + `addon.dashboard.capability.stages.${this.addon.stage}` + )} + ` + : ""} + + + + + + ${this.supervisor.localize( + "addon.dashboard.capability.label.rating" + )} + + ${this.addon.host_network + ? html` + + + ${this.supervisor.localize( + "addon.dashboard.capability.label.host" + )} + + ` + : ""} + ${this.addon.full_access + ? html` + + + ${this.supervisor.localize( + "addon.dashboard.capability.label.hardware" + )} + + ` + : ""} + ${this.addon.homeassistant_api + ? html` + + + ${this.supervisor.localize( + "addon.dashboard.capability.label.core" + )} + + ` + : ""} + ${this._computeHassioApi + ? html` + + + ${this.supervisor.localize( + `addon.dashboard.capability.role.${this.addon.hassio_role}` + ) || this.addon.hassio_role} + + ` + : ""} + ${this.addon.docker_api + ? html` + + + ${this.supervisor.localize( + "addon.dashboard.capability.label.docker" + )} + + ` + : ""} + ${this.addon.host_pid + ? html` + + + ${this.supervisor.localize( + "addon.dashboard.capability.label.host_pid" + )} + + ` + : ""} + ${this.addon.apparmor !== "default" + ? html` + + + ${this.supervisor.localize( + "addon.dashboard.capability.label.apparmor" + )} + + ` + : ""} + ${this.addon.auth_api + ? html` + + + ${this.supervisor.localize( + "addon.dashboard.capability.label.auth" + )} + + ` + : ""} + ${this.addon.ingress + ? html` + + + ${this.supervisor.localize( + "addon.dashboard.capability.label.ingress" + )} + + ` + : ""} +
+
${this.addon.description}.
${this.supervisor.localize( @@ -269,172 +438,6 @@ class HassioAddonInfo extends LitElement { /> ` : ""} -
- ${this.addon.stage !== "stable" - ? html` - - ` - : ""} - - - ${this.addon.rating} - - ${this.addon.host_network - ? html` - - - - ` - : ""} - ${this.addon.full_access - ? html` - - - - ` - : ""} - ${this.addon.homeassistant_api - ? html` - - - - ` - : ""} - ${this._computeHassioApi - ? html` - - - - ` - : ""} - ${this.addon.docker_api - ? html` - - - - ` - : ""} - ${this.addon.host_pid - ? html` - - - - ` - : ""} - ${this.addon.apparmor - ? html` - - - - ` - : ""} - ${this.addon.auth_api - ? html` - - - - ` - : ""} - ${this.addon.ingress - ? html` - - - - ` - : ""} -
- ${this.addon.version ? html`
- - - + ${this.supervisor?.localize("common.reload")} @@ -216,13 +220,15 @@ export class HassioBackups extends LitElement { ` : html` - - - + > ${this.supervisor.localize("backup.delete_selected")} @@ -368,7 +374,7 @@ export class HassioBackups extends LitElement { margin-right: -12px; } .header-btns > mwc-button, - .header-btns > mwc-icon-button { + .header-btns > ha-icon-button { margin: 8px; } `, diff --git a/hassio/src/components/hassio-upload-backup.ts b/hassio/src/components/hassio-upload-backup.ts index bbb6f43d7f..70b326bff9 100644 --- a/hassio/src/components/hassio-upload-backup.ts +++ b/hassio/src/components/hassio-upload-backup.ts @@ -1,4 +1,3 @@ -import "@material/mwc-icon-button/mwc-icon-button"; import { mdiFolderUpload } from "@mdi/js"; import "@polymer/paper-input/paper-input-container"; import { html, LitElement, TemplateResult } from "lit"; @@ -6,9 +5,8 @@ import { customElement, state } from "lit/decorators"; import { fireEvent } from "../../../src/common/dom/fire_event"; import "../../../src/components/ha-circular-progress"; import "../../../src/components/ha-file-upload"; -import "../../../src/components/ha-svg-icon"; -import { extractApiErrorMessage } from "../../../src/data/hassio/common"; import { HassioBackup, uploadBackup } from "../../../src/data/hassio/backup"; +import { extractApiErrorMessage } from "../../../src/data/hassio/common"; import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; import { HomeAssistant } from "../../../src/types"; @@ -31,6 +29,7 @@ export class HassioUploadBackup extends LitElement { public render(): TemplateResult { return html` Upload backup - - - +
${this._backup.name} - - - +
${this._restoringBackup @@ -110,9 +113,11 @@ class HassioBackupDialog @action=${this._handleMenuAction} @closed=${stopPropagation} > - - - + Download Backup Delete Backup ` @@ -126,9 +131,6 @@ class HassioBackupDialog haStyle, haStyleDialog, css` - ha-svg-icon { - color: var(--primary-text-color); - } ha-circular-progress { display: block; text-align: center; @@ -139,6 +141,9 @@ class HassioBackupDialog flex-shrink: 0; display: block; } + ha-icon-button { + color: var(--secondary-text-color); + } `, ]; } diff --git a/hassio/src/dialogs/hardware/dialog-hassio-hardware.ts b/hassio/src/dialogs/hardware/dialog-hassio-hardware.ts index a8d1481c84..b3e82c46d2 100755 --- a/hassio/src/dialogs/hardware/dialog-hassio-hardware.ts +++ b/hassio/src/dialogs/hardware/dialog-hassio-hardware.ts @@ -7,6 +7,7 @@ import "../../../../src/common/search/search-input"; import { stringCompare } from "../../../../src/common/string/compare"; import "../../../../src/components/ha-dialog"; import "../../../../src/components/ha-expansion-panel"; +import "../../../../src/components/ha-icon-button"; import { HassioHardwareInfo } from "../../../../src/data/hassio/hardware"; import { dump } from "../../../../src/resources/js-yaml-dump"; import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; @@ -70,10 +71,13 @@ class HassioHardwareDialog extends LitElement {

${this._dialogParams.supervisor.localize("dialog.hardware.title")}

- - - + ${this.supervisor.localize("dialog.network.title")} - - - + ${this._interfaces.length > 1 ? html` - - - + > ` ) @@ -234,7 +232,7 @@ class HassioRegistriesDialog extends LitElement { mwc-button { margin-left: 8px; } - mwc-icon-button { + ha-icon-button { color: var(--error-color); margin: -10px; } diff --git a/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts b/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts index 64eb324a20..03f1fc0395 100644 --- a/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts +++ b/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts @@ -1,5 +1,4 @@ import "@material/mwc-button/mwc-button"; -import "@material/mwc-icon-button/mwc-icon-button"; import { mdiDelete } from "@mdi/js"; import "@polymer/paper-input/paper-input"; import type { PaperInputElement } from "@polymer/paper-input/paper-input"; @@ -13,7 +12,7 @@ import { caseInsensitiveStringCompare } from "../../../../src/common/string/comp import "../../../../src/components/ha-alert"; import "../../../../src/components/ha-circular-progress"; import { createCloseHeading } from "../../../../src/components/ha-dialog"; -import "../../../../src/components/ha-svg-icon"; +import "../../../../src/components/ha-icon-button"; import { fetchHassioAddonsInfo, HassioAddonRepository, @@ -90,15 +89,14 @@ class HassioRepositoriesDialog extends LitElement {
${repo.maintainer}
${repo.url}
- - - + > ` ) diff --git a/hassio/src/dialogs/update/dialog-supervisor-update.ts b/hassio/src/dialogs/update/dialog-supervisor-update.ts index ca832ff776..cc4f80bb09 100644 --- a/hassio/src/dialogs/update/dialog-supervisor-update.ts +++ b/hassio/src/dialogs/update/dialog-supervisor-update.ts @@ -6,7 +6,6 @@ import "../../../../src/components/ha-alert"; import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-dialog"; import "../../../../src/components/ha-settings-row"; -import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-switch"; import { extractApiErrorMessage, diff --git a/hassio/src/hassio-main.ts b/hassio/src/hassio-main.ts index 93181aa106..8ded32bbf0 100644 --- a/hassio/src/hassio-main.ts +++ b/hassio/src/hassio-main.ts @@ -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) || diff --git a/hassio/src/ingress-view/hassio-ingress-view.ts b/hassio/src/ingress-view/hassio-ingress-view.ts index 91c338a3e5..9deb9bb45a 100644 --- a/hassio/src/ingress-view/hassio-ingress-view.ts +++ b/hassio/src/ingress-view/hassio-ingress-view.ts @@ -12,6 +12,7 @@ import { fireEvent } from "../../../src/common/dom/fire_event"; import { navigate } from "../../../src/common/navigate"; import { extractSearchParam } from "../../../src/common/url/search-params"; import { nextRender } from "../../../src/common/util/render-status"; +import "../../../src/components/ha-icon-button"; import { fetchHassioAddonInfo, HassioAddonDetails, @@ -72,12 +73,11 @@ class HassioIngressView extends LitElement { return html`${this.narrow || this.hass.dockedSidebar === "always_hidden" ? html`
- - - + >
${this._addon.name}
${iframe}` @@ -241,7 +241,7 @@ class HassioIngressView extends LitElement { flex-grow: 1; } - mwc-icon-button { + ha-icon-button { pointer-events: auto; } diff --git a/hassio/src/system/hassio-host-info.ts b/hassio/src/system/hassio-host-info.ts index 89aab52da9..64b7f90e8f 100644 --- a/hassio/src/system/hassio-host-info.ts +++ b/hassio/src/system/hassio-host-info.ts @@ -9,6 +9,7 @@ import { fireEvent } from "../../../src/common/dom/fire_event"; import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-card"; +import "../../../src/components/ha-icon-button"; import "../../../src/components/ha-settings-row"; import { extractApiErrorMessage, @@ -181,9 +182,11 @@ class HassioHostInfo extends LitElement { : ""} - - - + html`
  • - ${UNSUPPORTED_REASON_URL[reason] - ? html` - ${this.supervisor.localize( - `system.supervisor.unsupported_reason.${reason}` - ) || reason} - ` - : reason} + + ${this.supervisor.localize( + `system.supervisor.unsupported_reason.${reason}` + ) || reason} +
  • ` )} @@ -456,20 +435,19 @@ class HassioSupervisorInfo extends LitElement { ${this.supervisor.resolution.unhealthy.map( (reason) => html`
  • - ${UNHEALTHY_REASON_URL[reason] - ? html` - ${this.supervisor.localize( - `system.supervisor.unhealthy_reason.${reason}` - ) || reason} - ` - : reason} + + ${this.supervisor.localize( + `system.supervisor.unhealthy_reason.${reason}` + ) || reason} +
  • ` )} diff --git a/package.json b/package.json index 3a3dfe5645..c50c3c9ad6 100644 --- a/package.json +++ b/package.json @@ -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,45 +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": "13.0.0-canary.65125b3a6.0", - "@material/data-table": "13.0.0-canary.65125b3a6.0", - "@material/mwc-button": "0.25.1", - "@material/mwc-checkbox": "0.25.1", - "@material/mwc-circular-progress": "0.25.1", - "@material/mwc-dialog": "0.25.1", - "@material/mwc-fab": "0.25.1", - "@material/mwc-formfield": "0.25.1", - "@material/mwc-icon-button": "0.25.1", - "@material/mwc-linear-progress": "0.25.1", - "@material/mwc-list": "0.25.1", - "@material/mwc-menu": "0.25.1", - "@material/mwc-radio": "0.25.1", - "@material/mwc-ripple": "0.25.1", - "@material/mwc-switch": "0.25.1", - "@material/mwc-tab": "0.25.1", - "@material/mwc-tab-bar": "0.25.1", - "@material/top-app-bar": "13.0.0-canary.65125b3a6.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", @@ -115,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", @@ -187,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", @@ -237,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": { diff --git a/setup.py b/setup.py index 21bd283ba7..c5f9e949f6 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20211007.1", + version="20211026.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/frontend", author="The Home Assistant Authors", diff --git a/src/auth/ha-auth-flow.ts b/src/auth/ha-auth-flow.ts index d046ca6dae..ff0a31e0f9 100644 --- a/src/auth/ha-auth-flow.ts +++ b/src/auth/ha-auth-flow.ts @@ -7,16 +7,20 @@ 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"; import { DataEntryFlowStep, DataEntryFlowStepForm, } 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"; @@ -31,12 +35,44 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { @state() private _state: State = "loading"; - @state() private _stepData: any = {}; + @state() private _stepData?: Record; @state() private _step?: DataEntryFlowStep; @state() private _errorMessage?: string; + @state() private _submitting = false; + + @state() private _storeToken = false; + + willUpdate(changedProps: PropertyValues) { + super.willUpdate(changedProps); + + if (!changedProps.has("_step")) { + return; + } + + if (!this._step) { + this._stepData = undefined; + return; + } + + const oldStep = changedProps.get("_step") as HaAuthFlow["_step"]; + + if ( + !oldStep || + this._step.flow_id !== oldStep.flow_id || + (this._step.type === "form" && + oldStep.type === "form" && + this._step.step_id !== oldStep.step_id) + ) { + this._stepData = + this._step.type === "form" + ? computeInitialHaFormData(this._step.data_schema) + : undefined; + } + } + protected render() { return html`
    ${this._renderForm()}
    @@ -76,6 +112,24 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { if (changedProps.has("authProvider")) { this._providerChanged(this.authProvider); } + + if (!changedProps.has("_step") || this._step?.type !== "form") { + return; + } + + // 100ms to give all the form elements time to initialize. + setTimeout(() => { + const form = this.renderRoot.querySelector("ha-form"); + if (form) { + (form as any).focus(); + } + }, 100); + + setTimeout(() => { + this.renderRoot.querySelector( + "ha-password-manager-polyfill" + )!.boundingRect = this.getBoundingClientRect(); + }, 500); } private _renderForm(): TemplateResult { @@ -87,27 +141,33 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { return html` ${this._renderStep(this._step)}
    - ${this._step.type === "form" - ? this.localize("ui.panel.page-authorize.form.next") - : this.localize( - "ui.panel.page-authorize.form.start_over" - )} + ${this._step.type === "form" + ? this.localize("ui.panel.page-authorize.form.next") + : this.localize("ui.panel.page-authorize.form.start_over")} +
    `; case "error": return html` -
    + ${this.localize( "ui.panel.page-authorize.form.error", "error", this._errorMessage )} -
    + `; case "loading": - return html` ${this.localize("ui.panel.page-authorize.form.working")} `; + return html` + + ${this.localize("ui.panel.page-authorize.form.working")} + + `; default: return html``; } @@ -140,16 +200,34 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { .data=${this._stepData} .schema=${step.data_schema} .error=${step.errors} + .disabled=${this._submitting} .computeLabel=${this._computeLabelCallback(step)} .computeError=${this._computeErrorCallback(step)} @value-changed=${this._stepDataChanged} > + ${this.clientId === window.location.origin && step.step_id !== "mfa" + ? html` + + + + ` + : ""} `; default: return html``; } } + private _storeTokenChanged(e: CustomEvent) { + 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}`, { @@ -189,7 +267,8 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { return; } - await this._updateStep(data); + this._step = data; + this._state = "step"; } else { this._state = "error"; this._errorMessage = data.message; @@ -216,43 +295,13 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { if (this.oauth2State) { url += `&state=${encodeURIComponent(this.oauth2State)}`; } + if (this._storeToken) { + url += `&storeToken=true`; + } document.location.assign(url); } - private async _updateStep(step: DataEntryFlowStep) { - let stepData: any = null; - if ( - this._step && - (step.flow_id !== this._step.flow_id || - (step.type === "form" && - this._step.type === "form" && - step.step_id !== this._step.step_id)) - ) { - stepData = {}; - } - this._step = step; - this._state = "step"; - if (stepData != null) { - this._stepData = stepData; - } - - await this.updateComplete; - // 100ms to give all the form elements time to initialize. - setTimeout(() => { - const form = this.renderRoot.querySelector("ha-form"); - if (form) { - (form as any).focus(); - } - }, 100); - - setTimeout(() => { - this.renderRoot.querySelector( - "ha-password-manager-polyfill" - )!.boundingRect = this.getBoundingClientRect(); - }, 500); - } - private _stepDataChanged(ev: CustomEvent) { this._stepData = ev.detail.value; } @@ -297,9 +346,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { this._providerChanged(this.authProvider); return; } - this._state = "loading"; - // To avoid a jumping UI. - this.style.setProperty("min-height", `${this.offsetHeight}px`); + this._submitting = true; const postData = { ...this._stepData, client_id: this.clientId }; @@ -316,29 +363,28 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { this._redirect(newStep.result); return; } - await this._updateStep(newStep); + this._step = newStep; + this._state = "step"; } catch (err: any) { // eslint-disable-next-line no-console console.error("Error submitting step", err); this._state = "error"; this._errorMessage = this._unknownError(); } finally { - this.style.setProperty("min-height", ""); + this._submitting = false; } } static get styles(): CSSResultGroup { return css` - :host { - /* So we can set min-height to avoid jumping during loading */ - display: block; - } .action { margin: 24px 0 8px; text-align: center; } - .error { - color: red; + /* Align with the rest of the form. */ + .store-token { + margin-top: 10px; + margin-left: -16px; } `; } diff --git a/src/auth/ha-authorize.ts b/src/auth/ha-authorize.ts index 9608ad0642..acd98952c9 100644 --- a/src/auth/ha-authorize.ts +++ b/src/auth/ha-authorize.ts @@ -174,6 +174,10 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) { display: block; margin-top: 48px; } + ha-auth-flow { + display: block; + margin-top: 24px; + } `; } } diff --git a/src/auth/ha-password-manager-polyfill.ts b/src/auth/ha-password-manager-polyfill.ts index c0c46c0ab0..a0f2488b78 100644 --- a/src/auth/ha-password-manager-polyfill.ts +++ b/src/auth/ha-password-manager-polyfill.ts @@ -2,8 +2,8 @@ import { html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; -import { HaFormSchema } from "../components/ha-form/ha-form"; -import { DataEntryFlowStep } from "../data/data_entry_flow"; +import type { HaFormSchema } from "../components/ha-form/types"; +import type { DataEntryFlowStep } from "../data/data_entry_flow"; declare global { interface HTMLElementTagNameMap { diff --git a/src/common/auth/token_storage.ts b/src/common/auth/token_storage.ts index 6f8eea5d43..6b432a7029 100644 --- a/src/common/auth/token_storage.ts +++ b/src/common/auth/token_storage.ts @@ -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 { diff --git a/src/common/config/version.ts b/src/common/config/version.ts index ca9b8ae7af..ec89a3407e 100644 --- a/src/common/config/version.ts +++ b/src/common/config/version.ts @@ -4,6 +4,10 @@ export const atLeastVersion = ( minor: number, patch?: number ): boolean => { + if (__DEMO__) { + return true; + } + const [haMajor, haMinor, haPatch] = version.split(".", 3); return ( diff --git a/src/common/const.ts b/src/common/const.ts index dbc9ba4aac..6559ece953 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -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", - 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. */ diff --git a/src/common/dom/apply_themes_on_element.ts b/src/common/dom/apply_themes_on_element.ts index 721863e7f3..fca6e2262a 100644 --- a/src/common/dom/apply_themes_on_element.ts +++ b/src/common/dom/apply_themes_on_element.ts @@ -36,55 +36,62 @@ export const applyThemesOnElement = ( let cacheKey = selectedTheme; let themeRules: Partial = {}; - 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; } } diff --git a/src/common/entity/alarm_panel_icon.ts b/src/common/entity/alarm_panel_icon.ts index aa2590ed67..cdbb736b3a 100644 --- a/src/common/entity/alarm_panel_icon.ts +++ b/src/common/entity/alarm_panel_icon.ts @@ -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; } }; diff --git a/src/common/entity/battery_icon.ts b/src/common/entity/battery_icon.ts index f4b1784232..d73cdbe489 100644 --- a/src/common/entity/battery_icon.ts +++ b/src/common/entity/battery_icon.ts @@ -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]; }; diff --git a/src/common/entity/binary_sensor_icon.ts b/src/common/entity/binary_sensor_icon.ts index da18e0d01f..0f1dfaeaf2 100644 --- a/src/common/entity/binary_sensor_icon.ts +++ b/src/common/entity/binary_sensor_icon.ts @@ -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,52 +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": - return is_off ? "hass:check-circle" : "hass:alert-circle"; + case "tamper": + 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; } }; diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index fddce09d82..fd99a022da 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -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; } } diff --git a/src/common/entity/cover_icon.ts b/src/common/entity/cover_icon.ts index 011f8e7294..4308306890 100644 --- a/src/common/entity/cover_icon.ts +++ b/src/common/entity/cover_icon.ts @@ -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; } }; diff --git a/src/common/entity/domain_icon.ts b/src/common/entity/domain_icon.ts index 60f27c93aa..3816c925de 100644 --- a/src/common/entity/domain_icon.ts +++ b/src/common/entity/domain_icon.ts @@ -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) { diff --git a/src/common/entity/sensor_icon.ts b/src/common/entity/sensor_icon.ts index fa9bed83ce..9af29ceb3e 100644 --- a/src/common/entity/sensor_icon.ts +++ b/src/common/entity/sensor_icon.ts @@ -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; diff --git a/src/common/entity/state_icon.ts b/src/common/entity/state_icon_path.ts similarity index 74% rename from src/common/entity/state_icon.ts rename to src/common/entity/state_icon_path.ts index 86bfe50812..24d842caf6 100644 --- a/src/common/entity/state_icon.ts +++ b/src/common/entity/state_icon_path.ts @@ -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); }; diff --git a/src/common/entity/strip_prefix_from_entity_name.ts b/src/common/entity/strip_prefix_from_entity_name.ts new file mode 100644 index 0000000000..1efa3704d5 --- /dev/null +++ b/src/common/entity/strip_prefix_from_entity_name.ts @@ -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; diff --git a/src/common/search/search-input.ts b/src/common/search/search-input.ts index b7405a41cf..08084af98a 100644 --- a/src/common/search/search-input.ts +++ b/src/common/search/search-input.ts @@ -1,4 +1,3 @@ -import "@material/mwc-icon-button/mwc-icon-button"; import { mdiClose, mdiMagnify } from "@mdi/js"; import "@polymer/paper-input/paper-input"; import type { PaperInputElement } from "@polymer/paper-input/paper-input"; @@ -11,11 +10,15 @@ import { TemplateResult, } from "lit"; import { customElement, property, query } from "lit/decorators"; +import "../../components/ha-icon-button"; import "../../components/ha-svg-icon"; +import { HomeAssistant } from "../../types"; import { fireEvent } from "../dom/fire_event"; @customElement("search-input") class SearchInput extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @property() public filter?: string; @property({ type: Boolean, attribute: "no-label-float" }) @@ -50,13 +53,12 @@ class SearchInput extends LitElement { ${this.filter && html` - - - + .label=${this.hass.localize("ui.common.clear")} + .path=${mdiClose} + > `} `; @@ -90,10 +92,10 @@ class SearchInput extends LitElement { static get styles(): CSSResultGroup { return css` ha-svg-icon, - mwc-icon-button { + ha-icon-button { color: var(--primary-text-color); } - mwc-icon-button { + ha-icon-button { --mdc-icon-button-size: 24px; } ha-svg-icon.prefix { diff --git a/src/common/string/slugify.ts b/src/common/string/slugify.ts index dc4015ff68..e9901faa76 100644 --- a/src/common/string/slugify.ts +++ b/src/common/string/slugify.ts @@ -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 }; diff --git a/src/common/style/icon_color_css.ts b/src/common/style/icon_color_css.ts index b636ffa89c..a0905c0ea9 100644 --- a/src/common/style/icon_color_css.ts +++ b/src/common/style/icon_color_css.ts @@ -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); } `; diff --git a/src/components/chart/state-history-chart-timeline.ts b/src/components/chart/state-history-chart-timeline.ts index 6aa8c2951b..85de3ea9f7 100644 --- a/src/components/chart/state-history-chart-timeline.ts +++ b/src/components/chart/state-history-chart-timeline.ts @@ -12,21 +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", - "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([ @@ -47,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! ); diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index 6419406adf..11e0a50a30 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -30,6 +30,7 @@ import "../ha-checkbox"; import type { HaCheckbox } from "../ha-checkbox"; import "../ha-svg-icon"; import { filterData, sortData } from "./sort-filter"; +import { HomeAssistant } from "../../types"; declare global { // for fire event @@ -69,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?: (data: any, row: T) => TemplateResult | string; width?: string; maxWidth?: string; @@ -93,6 +94,8 @@ export interface SortableColumnContainer { @customElement("ha-data-table") export class HaDataTable extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @property({ type: Object }) public columns: DataTableColumnContainer = {}; @property({ type: Array }) public data: DataTableRowData[] = []; @@ -232,6 +235,7 @@ export class HaDataTable extends LitElement { ? html`
    ${this.value - ? html` - - ` + > ` : ""} ${areas.length > 0 ? html` - - - + > ` : ""}
    @@ -408,7 +404,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { .suffix { display: flex; } - mwc-icon-button { + ha-icon-button { --mdc-icon-button-size: 24px; padding: 0px 2px; color: var(--secondary-text-color); diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index daf9969af1..3b13c963e1 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -338,7 +338,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { static get styles(): CSSResultGroup { return css` - paper-input > mwc-icon-button { + paper-input > ha-icon-button { --mdc-icon-button-size: 24px; padding: 2px; color: var(--secondary-text-color); diff --git a/src/components/dialog/ha-iron-focusables-helper.js b/src/components/dialog/ha-iron-focusables-helper.js deleted file mode 100644 index 1f75e82c52..0000000000 --- a/src/components/dialog/ha-iron-focusables-helper.js +++ /dev/null @@ -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} - */ - 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} 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]. - //
    - // - // - // - // - // - // - //
    - // 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; - }, -}; diff --git a/src/components/dialog/ha-paper-dialog.ts b/src/components/dialog/ha-paper-dialog.ts deleted file mode 100644 index edaeff9315..0000000000 --- a/src/components/dialog/ha-paper-dialog.ts +++ /dev/null @@ -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; - -// 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); diff --git a/src/components/entity/ha-battery-icon.ts b/src/components/entity/ha-battery-icon.ts index 40233ec4a3..814f9d332e 100644 --- a/src/components/entity/ha-battery-icon.ts +++ b/src/components/entity/ha-battery-icon.ts @@ -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` - + `; } } + +declare global { + interface HTMLElementTagNameMap { + "ha-battery-icon": HaBatteryIcon; + } +} diff --git a/src/components/entity/ha-entity-attribute-picker.ts b/src/components/entity/ha-entity-attribute-picker.ts index 778a7c991c..a4e45e57ed 100644 --- a/src/components/entity/ha-entity-attribute-picker.ts +++ b/src/components/entity/ha-entity-attribute-picker.ts @@ -1,4 +1,3 @@ -import "@material/mwc-icon-button/mwc-icon-button"; import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; import "@polymer/paper-input/paper-input"; import "@polymer/paper-item/paper-item"; @@ -18,6 +17,7 @@ import { fireEvent } from "../../common/dom/fire_event"; import { PolymerChangedEvent } from "../../polymer-types"; import { HomeAssistant } from "../../types"; import { formatAttributeName } from "../../util/hass-attributes-util"; +import "../ha-icon-button"; import "../ha-svg-icon"; import "./state-badge"; @@ -114,31 +114,27 @@ class HaEntityAttributePicker extends LitElement {
    ${this.value ? html` - - - + > ` : ""} - - - + >
    @@ -178,7 +174,7 @@ class HaEntityAttributePicker extends LitElement { .suffix { display: flex; } - mwc-icon-button { + ha-icon-button { --mdc-icon-button-size: 24px; padding: 0px 2px; color: var(--secondary-text-color); diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index 40816b289e..016684e6c2 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -1,4 +1,3 @@ -import "@material/mwc-icon-button/mwc-icon-button"; import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; import "@polymer/paper-input/paper-input"; import "@polymer/paper-item/paper-icon-item"; @@ -21,6 +20,7 @@ import { computeDomain } from "../../common/entity/compute_domain"; import { computeStateName } from "../../common/entity/compute_state_name"; import { PolymerChangedEvent } from "../../polymer-types"; import { HomeAssistant } from "../../types"; +import "../ha-icon-button"; import "../ha-svg-icon"; import "./state-badge"; @@ -267,31 +267,27 @@ export class HaEntityPicker extends LitElement {
    ${this.value && !this.hideClearIcon ? html` - - - + > ` : ""} - - - + >
    @@ -340,7 +336,7 @@ export class HaEntityPicker extends LitElement { .suffix { display: flex; } - mwc-icon-button { + ha-icon-button { --mdc-icon-button-size: 24px; padding: 0px 2px; color: var(--secondary-text-color); diff --git a/src/components/entity/ha-entity-toggle.ts b/src/components/entity/ha-entity-toggle.ts index 5a3942d5d3..962af59b92 100644 --- a/src/components/entity/ha-entity-toggle.ts +++ b/src/components/entity/ha-entity-toggle.ts @@ -1,3 +1,4 @@ +import { mdiFlash, mdiFlashOff } from "@mdi/js"; import { HassEntity } from "home-assistant-js-websocket"; import { css, @@ -41,15 +42,15 @@ export class HaEntityToggle extends LitElement { if (this.stateObj.attributes.assumed_state) { return html` - ${!image && icon ? html`` : ""} - ${value && (this.icon || !this.image) + ${!image && showIcon + ? html`` + : ""} + ${value && !image && !showIcon ? html` 4 ? "big" : ""} >${value}` @@ -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; } } diff --git a/src/components/entity/ha-statistic-picker.ts b/src/components/entity/ha-statistic-picker.ts index 64f1b1cd19..7cc6be7542 100644 --- a/src/components/entity/ha-statistic-picker.ts +++ b/src/components/entity/ha-statistic-picker.ts @@ -1,4 +1,3 @@ -import "@material/mwc-icon-button/mwc-icon-button"; import { mdiCheck } from "@mdi/js"; import "@polymer/paper-input/paper-input"; import "@polymer/paper-item/paper-icon-item"; @@ -289,7 +288,7 @@ export class HaStatisticPicker extends LitElement { static get styles(): CSSResultGroup { return css` - paper-input > mwc-icon-button { + paper-input > ha-icon-button { --mdc-icon-button-size: 24px; padding: 2px; color: var(--secondary-text-color); diff --git a/src/components/entity/state-badge.ts b/src/components/entity/state-badge.ts index 2decae9151..404457b33d 100644 --- a/src/components/entity/state-badge.ts +++ b/src/components/entity/state-badge.ts @@ -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`
    - +
    `; } @@ -49,18 +49,17 @@ export class StateBadge extends LitElement { const domain = stateObj ? computeStateDomain(stateObj) : undefined; - return html` - - `; + return html``; } 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 { diff --git a/src/components/entity/state-info.ts b/src/components/entity/state-info.ts index cbc7b1c0f9..01f95f88bb 100644 --- a/src/components/entity/state-info.ts +++ b/src/components/entity/state-info.ts @@ -53,6 +53,7 @@ class StateInfo extends LitElement {
    @@ -64,6 +65,7 @@ class StateInfo extends LitElement {
    diff --git a/src/components/ha-alert.ts b/src/components/ha-alert.ts index ddd3c641e7..25c3975ac6 100644 --- a/src/components/ha-alert.ts +++ b/src/components/ha-alert.ts @@ -1,5 +1,4 @@ import "@material/mwc-button/mwc-button"; -import "@material/mwc-icon-button/mwc-icon-button"; import { mdiAlertCircleOutline, mdiAlertOutline, @@ -11,6 +10,7 @@ import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../common/dom/fire_event"; +import "./ha-icon-button"; import "./ha-svg-icon"; const ALERT_ICONS = { @@ -66,12 +66,11 @@ class HaAlert extends LitElement { .label=${this.actionText} >` : this.dismissable - ? html` - - ` + label="Dismiss alert" + .path=${mdiClose} + >` : ""}
    @@ -140,7 +139,7 @@ class HaAlert extends LitElement { mwc-button { --mdc-theme-primary: var(--primary-text-color); } - mwc-icon-button { + ha-icon-button { --mdc-icon-button-size: 36px; } .issue-type.info > .icon { diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts index 2b991869b3..b79abb66a4 100644 --- a/src/components/ha-area-picker.ts +++ b/src/components/ha-area-picker.ts @@ -1,4 +1,3 @@ -import "@material/mwc-icon-button/mwc-icon-button"; import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; import "@polymer/paper-input/paper-input"; import "@polymer/paper-item/paper-item"; @@ -42,6 +41,7 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { PolymerChangedEvent } from "../polymer-types"; import { HomeAssistant } from "../types"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; +import "./ha-icon-button"; import "./ha-svg-icon"; const rowRenderer: ComboBoxLitRenderer = ( @@ -362,28 +362,24 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { > ${this.value ? html` - - - + > ` : ""} - - - + > `; @@ -457,7 +453,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { static get styles(): CSSResultGroup { return css` - paper-input > mwc-icon-button { + paper-input > ha-icon-button { --mdc-icon-button-size: 24px; padding: 2px; color: var(--secondary-text-color); diff --git a/src/components/ha-button-related-filter-menu.ts b/src/components/ha-button-related-filter-menu.ts index 02db7f1e20..afbfff1966 100644 --- a/src/components/ha-button-related-filter-menu.ts +++ b/src/components/ha-button-related-filter-menu.ts @@ -1,4 +1,3 @@ -import "@material/mwc-icon-button"; import type { Corner } from "@material/mwc-menu"; import "@material/mwc-menu/mwc-menu-surface"; import { mdiFilterVariant } from "@mdi/js"; @@ -12,7 +11,7 @@ import type { HomeAssistant } from "../types"; import "./device/ha-device-picker"; import "./entity/ha-entity-picker"; import "./ha-area-picker"; -import "./ha-svg-icon"; +import "./ha-icon-button"; declare global { // for fire event @@ -55,9 +54,10 @@ export class HaRelatedFilterButtonMenu extends LitElement { protected render(): TemplateResult { return html` - - - + ${this.buttons.map((button) => button.iconPath - ? html` - - ` + >` : html` - ` - : this._url - ? html` - - ` - : ""} - `; + if (__DEMO__ || this._shouldRenderMJPEG) { + return html` `; + } + if (this.stateObj.attributes.frontend_stream_type === STREAM_TYPE_HLS) { + return this._url + ? html` ` + : html``; + } + if (this.stateObj.attributes.frontend_stream_type === STREAM_TYPE_WEB_RTC) { + return html` `; + } + return html``; } private get _shouldRenderMJPEG() { - return ( - this._forceMJPEG === this.stateObj!.entity_id || + if (this._forceMJPEG === this.stateObj!.entity_id) { + // Fallback when unable to fetch stream url + return true; + } + if ( !isComponentLoaded(this.hass!, "stream") || !supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM) - ); + ) { + // Steaming is not supported by the camera so fallback to MJPEG stream + return true; + } + if ( + this.stateObj!.attributes.frontend_stream_type === STREAM_TYPE_WEB_RTC && + typeof RTCPeerConnection === "undefined" + ) { + // Stream requires WebRTC but browser does not support, so fallback to + // MJPEG stream. + return true; + } + // Render stream + return false; } private async _getStreamUrl(): Promise { diff --git a/src/components/ha-climate-control.js b/src/components/ha-climate-control.js index 9962680dfd..39398a8cd6 100644 --- a/src/components/ha-climate-control.js +++ b/src/components/ha-climate-control.js @@ -3,6 +3,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; /* eslint-plugin-disable lit */ import { PolymerElement } from "@polymer/polymer/polymer-element"; import { EventsMixin } from "../mixins/events-mixin"; +import "./ha-icon"; import "./ha-icon-button"; /* @@ -40,16 +41,14 @@ class HaClimateControl extends EventsMixin(PolymerElement) {
    [[value]] [[units]]
    - + + +
    - + + +
    `; diff --git a/src/components/ha-combo-box.ts b/src/components/ha-combo-box.ts index 8abeaf24ba..afcd9c8911 100644 --- a/src/components/ha-combo-box.ts +++ b/src/components/ha-combo-box.ts @@ -1,4 +1,3 @@ -import "@material/mwc-icon-button/mwc-icon-button"; import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; import "@polymer/paper-input/paper-input"; import "@polymer/paper-item/paper-item"; @@ -11,7 +10,7 @@ import { customElement, property, query, state } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; import { PolymerChangedEvent } from "../polymer-types"; import { HomeAssistant } from "../types"; -import "./ha-svg-icon"; +import "./ha-icon-button"; // eslint-disable-next-line lit/prefer-static-styles const defaultRowRenderer: ComboBoxLitRenderer = (item) => html` + + + + + ${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` + + + ${this._value || this.placeholder + ? html` + + + ` + : this.fallbackPath + ? html`` + : ""} + + + + `; + } + + private async _openedChanged(ev: PolymerChangedEvent) { + 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) { + 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; + } +} diff --git a/src/components/ha-icon.ts b/src/components/ha-icon.ts index 2c829299b4..5a353e946e 100644 --- a/src/components/ha-icon.ts +++ b/src/components/ha-icon.ts @@ -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", diff --git a/src/components/ha-label-badge.ts b/src/components/ha-label-badge.ts index 7c1d633f43..a8bbf838a5 100644 --- a/src/components/ha-label-badge.ts +++ b/src/components/ha-label-badge.ts @@ -8,7 +8,6 @@ import { } from "lit"; import { property } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; -import "./ha-svg-icon"; class HaLabelBadge extends LitElement { @property() public label?: string; diff --git a/src/components/ha-menu-button.ts b/src/components/ha-menu-button.ts index 9737b2427d..294418195c 100644 --- a/src/components/ha-menu-button.ts +++ b/src/components/ha-menu-button.ts @@ -1,4 +1,3 @@ -import "@material/mwc-icon-button"; import { mdiMenu } from "@mdi/js"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; @@ -7,7 +6,7 @@ import { fireEvent } from "../common/dom/fire_event"; import { computeDomain } from "../common/entity/compute_domain"; import { subscribeNotifications } from "../data/persistent_notification"; import { HomeAssistant } from "../types"; -import "./ha-svg-icon"; +import "./ha-icon-button"; @customElement("ha-menu-button") class HaMenuButton extends LitElement { @@ -50,12 +49,11 @@ class HaMenuButton extends LitElement { (entityId) => computeDomain(entityId) === "configurator" )); return html` - - - + > ${hasNotifications ? html`
    ` : ""} `; } diff --git a/src/components/ha-network.ts b/src/components/ha-network.ts index 451e709176..6b7957573a 100644 --- a/src/components/ha-network.ts +++ b/src/components/ha-network.ts @@ -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 { Adapter: ${adapter.name} ${adapter.default - ? html` (Default)` + ? html` + (Default)` : ""} diff --git a/src/components/ha-picture-upload.ts b/src/components/ha-picture-upload.ts index 7e6977d3fc..809d6a2b23 100644 --- a/src/components/ha-picture-upload.ts +++ b/src/components/ha-picture-upload.ts @@ -1,4 +1,3 @@ -import "@material/mwc-icon-button/mwc-icon-button"; import { mdiImagePlus } from "@mdi/js"; import "@polymer/paper-input/paper-input-container"; import { html, LitElement, TemplateResult } from "lit"; @@ -13,7 +12,6 @@ import { import { HomeAssistant } from "../types"; import "./ha-circular-progress"; import "./ha-file-upload"; -import "./ha-svg-icon"; @customElement("ha-picture-upload") export class HaPictureUpload extends LitElement { @@ -34,6 +32,7 @@ export class HaPictureUpload extends LitElement { public render(): TemplateResult { return html` + return html` + field.selector && + !field.required && + !("boolean" in field.selector && field.default); + interface ExtHassService extends Omit { fields: { key: string; @@ -185,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` - - - + ` : ""} @@ -254,14 +258,15 @@ export class HaServiceControl extends LitElement { .defaultValue=${this._value?.data} @value-changed=${this._dataChanged} >` - : 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` - ${dataField.required + ${!showOptional ? hasOptional ? html`
    ` : "" @@ -274,9 +279,9 @@ export class HaServiceControl extends LitElement { slot="prefix" >`} ${dataField.name || dataField.key} - ${dataField?.description}${dataField?.description}
    + ` - : "" - )} `; + > + ` + : ""; + })}`; } 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, diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 099e9d23e4..502c0e7fe7 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -1,12 +1,21 @@ import "@material/mwc-button/mwc-button"; -import "@material/mwc-icon-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"; @@ -44,6 +53,7 @@ import { actionHandler } from "../panels/lovelace/common/directives/action-handl import { haStyleScrollbar } from "../resources/styles"; import type { HomeAssistant, PanelInfo } from "../types"; import "./ha-icon"; +import "./ha-icon-button"; import "./ha-menu-button"; import "./ha-svg-icon"; import "./user/ha-user-badge"; @@ -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, @@ -302,16 +326,13 @@ class HaSidebar extends LitElement { > ${!this.narrow ? html` - - - + > ` : ""} ${this.editMode @@ -351,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` + + + ${iconPath + ? html`` + : html``} + ${title} + + ${this.editMode + ? html`` + : ""} + + `; + } + private _renderPanelsEdit(beforeSpacer: PanelInfo[]) { // prettier-ignore return html`
    @@ -363,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) { @@ -374,21 +449,28 @@ class HaSidebar extends LitElement { class="hidden-panel" .panel=${url} > - + ${panel.url_path === this.hass.defaultPanel && !panel.icon + ? html`` + : panel.url_path in PANEL_ICONS + ? html`` + : html``} ${panel.url_path === this.hass.defaultPanel ? this.hass.localize("panel.states") : this.hass.localize(`panel.${panel.title}`) || panel.title} - - - + `; })} ${this._renderSpacer()}` @@ -413,7 +495,7 @@ class HaSidebar extends LitElement { } } - return html`
    - 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` - - - ${iconPath - ? html`` - : html``} - ${title} - - ${this.editMode - ? html` - - ` - : ""} - - `; - } - static get styles(): CSSResultGroup { return [ haStyleScrollbar, @@ -786,7 +816,7 @@ class HaSidebar extends LitElement { :host([rtl][expanded]) .menu { width: calc(256px + env(safe-area-inset-right)); } - .menu mwc-icon-button { + .menu ha-icon-button { color: var(--sidebar-icon-color); } .title { @@ -1021,7 +1051,7 @@ class HaSidebar extends LitElement { font-weight: 500; } - :host([rtl]) .menu mwc-icon-button { + :host([rtl]) .menu ha-icon-button { -webkit-transform: scaleX(-1); transform: scaleX(-1); } diff --git a/src/components/ha-state-icon.ts b/src/components/ha-state-icon.ts new file mode 100644 index 0000000000..eace7706bc --- /dev/null +++ b/src/components/ha-state-icon.ts @@ -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``; + } + return html``; + } +} +declare global { + interface HTMLElementTagNameMap { + "ha-state-icon": HaStateIcon; + } +} diff --git a/src/components/ha-tab.ts b/src/components/ha-tab.ts index 5b326f61fb..da172e1fab 100644 --- a/src/components/ha-tab.ts +++ b/src/components/ha-tab.ts @@ -96,6 +96,7 @@ export class HaTab extends LitElement { box-sizing: border-box; align-items: center; justify-content: center; + width: 100%; height: var(--header-height); cursor: pointer; position: relative; @@ -104,6 +105,9 @@ export class HaTab extends LitElement { .name { white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; } :host([active]) { @@ -120,6 +124,10 @@ export class HaTab extends LitElement { justify-content: center; overflow: hidden; } + + :host([narrow]) div { + padding: 0 4px; + } `; } } diff --git a/src/components/ha-target-picker.ts b/src/components/ha-target-picker.ts index 3e2e40260f..4edf45230d 100644 --- a/src/components/ha-target-picker.ts +++ b/src/components/ha-target-picker.ts @@ -1,7 +1,6 @@ // @ts-ignore import chipStyles from "@material/chips/dist/mdc.chips.min.css"; import "@material/mwc-button/mwc-button"; -import "@material/mwc-icon-button/mwc-icon-button"; import { mdiClose, mdiDevices, @@ -11,17 +10,17 @@ import { } from "@mdi/js"; import "@polymer/paper-tooltip/paper-tooltip"; import { + HassEntity, HassServiceTarget, UnsubscribeFunc, } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, unsafeCSS } from "lit"; -import { customElement, property, state, query } from "lit/decorators"; +import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { 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, @@ -42,14 +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} >` : ""} - ${icon - ? html`` + .state=${entityState} + >` : ""} @@ -259,17 +258,19 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { ${type === "entity_id" ? "" : html` - - - + > ${this.hass.localize( `ui.components.target-picker.expand_${type}` @@ -277,17 +278,17 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { > `} - - - + > ${this.hass.localize( `ui.components.target-picker.remove_${type}` @@ -543,13 +544,13 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { .mdc-chip:not(.add) { cursor: default; } - .mdc-chip mwc-icon-button { + .mdc-chip ha-icon-button { --mdc-icon-button-size: 24px; display: flex; align-items: center; outline: none; } - .mdc-chip mwc-icon-button ha-svg-icon { + .mdc-chip ha-icon-button ha-svg-icon { border-radius: 50%; background: var(--secondary-text-color); } @@ -557,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; diff --git a/src/components/ha-water_heater-control.js b/src/components/ha-water_heater-control.js index 9a3403b36d..f92034266e 100644 --- a/src/components/ha-water_heater-control.js +++ b/src/components/ha-water_heater-control.js @@ -3,6 +3,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; /* eslint-plugin-disable lit */ import { PolymerElement } from "@polymer/polymer/polymer-element"; import { EventsMixin } from "../mixins/events-mixin"; +import "./ha-icon"; import "./ha-icon-button"; /* @@ -40,16 +41,14 @@ class HaWaterHeaterControl extends EventsMixin(PolymerElement) {
    [[value]] [[units]]
    - + + +
    - + + +
    `; diff --git a/src/components/ha-web-rtc-player.ts b/src/components/ha-web-rtc-player.ts new file mode 100644 index 0000000000..b31b2c0a7d --- /dev/null +++ b/src/components/ha-web-rtc-player.ts @@ -0,0 +1,159 @@ +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state, query } from "lit/decorators"; +import { handleWebRtcOffer, WebRtcAnswer } from "../data/camera"; +import type { HomeAssistant } from "../types"; +import "./ha-alert"; + +/** + * A WebRTC stream is established by first sending an offer through a signal + * path via an integration. An answer is returned, then the rest of the stream + * is handled entirely client side. + */ +@customElement("ha-web-rtc-player") +class HaWebRtcPlayer extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public entityid!: string; + + @property({ type: Boolean, attribute: "controls" }) + public controls = false; + + @property({ type: Boolean, attribute: "muted" }) + public muted = false; + + @property({ type: Boolean, attribute: "autoplay" }) + public autoPlay = false; + + @property({ type: Boolean, attribute: "playsinline" }) + public playsInline = false; + + @state() private _error?: string; + + // 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`${this._error}`; + } + return html` + + `; + } + + public disconnectedCallback() { + super.disconnectedCallback(); + this._cleanUp(); + } + + protected updated(changedProperties: PropertyValues) { + if (!changedProperties.has("entityid")) { + return; + } + if (!this._videoEl) { + return; + } + this._startWebRtc(); + } + + private async _startWebRtc(): Promise { + 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. + peerConnection.createDataChannel("dataSendChannel"); + const offerOptions: RTCOfferOptions = { + offerToReceiveAudio: true, + offerToReceiveVideo: true, + }; + const offer: RTCSessionDescriptionInit = await peerConnection.createOffer( + offerOptions + ); + await peerConnection.setLocalDescription(offer); + + let webRtcAnswer: WebRtcAnswer; + try { + webRtcAnswer = await handleWebRtcOffer( + this.hass, + this.entityid, + offer.sdp! + ); + } catch (err: any) { + this._error = "Failed to start WebRTC stream: " + err.message; + peerConnection.close(); + return; + } + + // Setup callbacks to render remote stream once media tracks are discovered. + const remoteStream = new MediaStream(); + peerConnection.addEventListener("track", (event) => { + 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, + }); + 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 { + return css` + video { + display: block; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-web-rtc-player": HaWebRtcPlayer; + } +} diff --git a/src/components/media-player/ha-media-player-browse.ts b/src/components/media-player/ha-media-player-browse.ts index 0d9a21da5c..19672ca8de 100644 --- a/src/components/media-player/ha-media-player-browse.ts +++ b/src/components/media-player/ha-media-player-browse.ts @@ -15,10 +15,10 @@ import { } from "lit"; import { customElement, - property, - state, - query, eventOptions, + property, + query, + state, } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; @@ -45,6 +45,7 @@ import "../ha-button-menu"; import "../ha-card"; import "../ha-circular-progress"; import "../ha-fab"; +import "../ha-icon-button"; import "../ha-svg-icon"; declare global { @@ -225,14 +226,13 @@ export class HaMediaPlayerBrowse extends LitElement {
    ${this.dialog ? html` - - - + > ` : ""}
    @@ -281,7 +281,7 @@ export class HaMediaPlayerBrowse extends LitElement { ${child.can_play ? html` - - - + > ` : ""} @@ -339,7 +336,7 @@ export class HaMediaPlayerBrowse extends LitElement { )} slot="graphic" > - - - + > ${child.title} @@ -952,7 +944,7 @@ export class HaMediaPlayerBrowse extends LitElement { -webkit-line-clamp: 1; } - :host(:not([narrow])[scroll]) .header:not(.no-img) mwc-icon-button { + :host(:not([narrow])[scroll]) .header:not(.no-img) ha-icon-button { align-self: center; } diff --git a/src/components/paper-time-input.js b/src/components/paper-time-input.js index bc4f442ba7..99e9c8de8f 100644 --- a/src/components/paper-time-input.js +++ b/src/components/paper-time-input.js @@ -119,6 +119,7 @@ export class PaperTimeInput extends PolymerElement {
    - - - - + - - + .path=${mdiChevronDown} + >
    `; } catch (err: any) { diff --git a/src/components/user/ha-users-picker.ts b/src/components/user/ha-users-picker.ts index d0420b82dd..d9c4ff148a 100644 --- a/src/components/user/ha-users-picker.ts +++ b/src/components/user/ha-users-picker.ts @@ -7,6 +7,7 @@ import { fireEvent } from "../../common/dom/fire_event"; import { fetchUsers, User } from "../../data/user"; import type { PolymerChangedEvent } from "../../polymer-types"; import type { HomeAssistant } from "../../types"; +import "../ha-icon-button"; import "./ha-user-picker"; @customElement("ha-users-picker") @@ -46,7 +47,7 @@ class HaUsersPickerLight extends LitElement {
    - - - + + >
    ` ) )} `/api/camera_proxy_stream/${entity.entity_id}?token=${entity.attributes.access_token}`; @@ -78,6 +86,17 @@ export const fetchStreamUrl = async ( return stream; }; +export const handleWebRtcOffer = ( + hass: HomeAssistant, + entityId: string, + offer: string +) => + hass.callWS({ + type: "camera/web_rtc_offer", + entity_id: entityId, + offer: offer, + }); + export const fetchCameraPrefs = (hass: HomeAssistant, entityId: string) => hass.callWS({ type: "camera/get_prefs", diff --git a/src/data/cloud.ts b/src/data/cloud.ts index 0082c5ee35..a2ddc98c1b 100644 --- a/src/data/cloud.ts +++ b/src/data/cloud.ts @@ -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({ type: "cloud/status" }); diff --git a/src/data/data_entry_flow.ts b/src/data/data_entry_flow.ts index 8be5367632..d617b37020 100644 --- a/src/data/data_entry_flow.ts +++ b/src/data/data_entry_flow.ts @@ -1,5 +1,5 @@ import { Connection } from "home-assistant-js-websocket"; -import { HaFormSchema } from "../components/ha-form/ha-form"; +import type { HaFormSchema } from "../components/ha-form/types"; import { ConfigEntry } from "./config_entries"; export interface DataEntryFlowProgressedEvent { diff --git a/src/data/device_automation.ts b/src/data/device_automation.ts index 0131a451a2..b047f87f1e 100644 --- a/src/data/device_automation.ts +++ b/src/data/device_automation.ts @@ -1,5 +1,5 @@ import { computeStateName } from "../common/entity/compute_state_name"; -import { HaFormSchema } from "../components/ha-form/ha-form"; +import type { HaFormSchema } from "../components/ha-form/types"; import { HomeAssistant } from "../types"; import { BaseTrigger } from "./automation"; diff --git a/src/data/device_registry.ts b/src/data/device_registry.ts index 8e074b5e2c..5c039437e4 100644 --- a/src/data/device_registry.ts +++ b/src/data/device_registry.ts @@ -18,6 +18,7 @@ export interface DeviceRegistryEntry { name_by_user: string | null; entry_type: "service" | null; disabled_by: string | null; + configuration_url: string | null; } export interface DeviceEntityLookup { diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index 28f389922e..7072c44227 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -13,6 +13,7 @@ export interface EntityRegistryEntry { device_id: string | null; area_id: string | null; disabled_by: string | null; + entity_category: "config" | "diagnostic" | null; } export interface ExtEntityRegistryEntry extends EntityRegistryEntry { diff --git a/src/data/hassio/addon.ts b/src/data/hassio/addon.ts index c2e43ac046..93ac68c506 100644 --- a/src/data/hassio/addon.ts +++ b/src/data/hassio/addon.ts @@ -1,5 +1,5 @@ import { atLeastVersion } from "../../common/config/version"; -import { HaFormSchema } from "../../components/ha-form/ha-form"; +import type { HaFormSchema } from "../../components/ha-form/types"; import { HomeAssistant } from "../../types"; import { SupervisorArch } from "../supervisor/supervisor"; import { diff --git a/src/data/history.ts b/src/data/history.ts index a163ea4708..fed6abaf89 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -77,12 +77,24 @@ export interface StatisticsMetaData { } 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 }; diff --git a/src/data/logbook.ts b/src/data/logbook.ts index b67e602d6c..0ce969c3f4 100644 --- a/src/data/logbook.ts +++ b/src/data/logbook.ts @@ -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 => - addLogbookMessage( +): Promise => { + 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 => - addLogbookMessage( +): Promise => { + 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,18 +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 localize(`${LOGBOOK_LOCALIZE_PATH}.detected_tampering`); + } + if (isOff) { + return localize(`${LOGBOOK_LOCALIZE_PATH}.cleared_tampering`); } break; } @@ -287,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 ); }; diff --git a/src/data/media-player.ts b/src/data/media-player.ts index 143b7e1bf0..f0431b7f57 100644 --- a/src/data/media-player.ts +++ b/src/data/media-player.ts @@ -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", }); } diff --git a/src/data/script.ts b/src/data/script.ts index c6c78fc163..6b83157bde 100644 --- a/src/data/script.ts +++ b/src/data/script.ts @@ -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 }; } diff --git a/src/data/translation.ts b/src/data/translation.ts index 65f98a059d..aee999172a 100644 --- a/src/data/translation.ts +++ b/src/data/translation.ts @@ -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"); diff --git a/src/data/weather.ts b/src/data/weather.ts index d37175d727..17e91ccf6c 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -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([ ]); 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` - + `; } diff --git a/src/data/zha.ts b/src/data/zha.ts index b3b3944f1b..b8c99e0e24 100644 --- a/src/data/zha.ts +++ b/src/data/zha.ts @@ -1,5 +1,5 @@ import { HassEntity } from "home-assistant-js-websocket"; -import { HaFormSchema } from "../components/ha-form/ha-form"; +import type { HaFormSchema } from "../components/ha-form/types"; import { HomeAssistant } from "../types"; export interface ZHAEntityReference extends HassEntity { diff --git a/src/data/zwave_js.ts b/src/data/zwave_js.ts index c447665f40..fbd9113d5c 100644 --- a/src/data/zwave_js.ts +++ b/src/data/zwave_js.ts @@ -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 { diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 12821b4abe..e7fcb1572b 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -1,5 +1,5 @@ import "@material/mwc-button"; -import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; +import { mdiClose } from "@mdi/js"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, @@ -232,10 +232,10 @@ class DataEntryFlowDialog extends LitElement { "" : html` diff --git a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts index 45a914acf8..c1141d729a 100644 --- a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts @@ -1,6 +1,6 @@ import { TemplateResult } from "lit"; import { fireEvent } from "../../common/dom/fire_event"; -import { HaFormSchema } from "../../components/ha-form/ha-form"; +import type { HaFormSchema } from "../../components/ha-form/types"; import { DataEntryFlowStep, DataEntryFlowStepAbort, diff --git a/src/dialogs/config-flow/step-flow-form.ts b/src/dialogs/config-flow/step-flow-form.ts index 1c2d017826..a1969dc460 100644 --- a/src/dialogs/config-flow/step-flow-form.ts +++ b/src/dialogs/config-flow/step-flow-form.ts @@ -11,9 +11,11 @@ import { import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; import "../../components/ha-circular-progress"; +import { computeInitialHaFormData } from "../../components/ha-form/compute-initial-ha-form-data"; +import type { HaFormSchema } from "../../components/ha-form/types"; import "../../components/ha-form/ha-form"; -import type { HaFormSchema } from "../../components/ha-form/ha-form"; import "../../components/ha-markdown"; +import "../../components/ha-alert"; import type { DataEntryFlowStepForm } from "../../data/data_entry_flow"; import type { HomeAssistant } from "../../types"; import type { FlowConfig } from "./show-dialog-data-entry-flow"; @@ -37,26 +39,16 @@ class StepFlowForm extends LitElement { const step = this.step; const stepData = this._stepDataProcessed; - const allRequiredInfoFilledIn = - stepData === undefined - ? // If no data filled in, just check that any field is required - step.data_schema.find((field) => !field.optional) === undefined - : // If data is filled in, make sure all required fields are - stepData && - step.data_schema.every( - (field) => - field.optional || !["", undefined].includes(stepData![field.name]) - ); - return html`

    ${this.flowConfig.renderShowFormStepHeader(this.hass, this.step)}

    - ${this._errorMsg - ? html`
    ${this._errorMsg}
    ` - : ""} ${this.flowConfig.renderShowFormStepDescription(this.hass, this.step)} + ${this._errorMsg + ? html`${this._errorMsg}` + : ""} - ${this.hass.localize( + + ${this.hass.localize( `ui.panel.config.integrations.config_flow.${ this.step.last_step === false ? "next" : "submit" }` )} - - ${!allRequiredInfoFilledIn - ? html` - ${this.hass.localize( - "ui.panel.config.integrations.config_flow.not_all_required_fields" - )} - - ` - : html``}
    `} @@ -113,25 +93,35 @@ class StepFlowForm extends LitElement { return this._stepData; } - const data = {}; - this.step.data_schema.forEach((field) => { - if (field.description?.suggested_value) { - data[field.name] = field.description.suggested_value; - } else if ("default" in field) { - data[field.name] = field.default; - } - }); - - this._stepData = data; - return data; + this._stepData = computeInitialHaFormData(this.step.data_schema); + return this._stepData; } private async _submitStep(): Promise { + const stepData = this._stepData || {}; + + const allRequiredInfoFilledIn = + stepData === undefined + ? // If no data filled in, just check that any field is required + this.step.data_schema.find((field) => !field.optional) === undefined + : // If data is filled in, make sure all required fields are + stepData && + this.step.data_schema.every( + (field) => + field.optional || !["", undefined].includes(stepData![field.name]) + ); + + if (!allRequiredInfoFilledIn) { + this._errorMsg = this.hass.localize( + "ui.panel.config.integrations.config_flow.not_all_required_fields" + ); + return; + } + this._loading = true; this._errorMsg = undefined; const flowId = this.step.flow_id; - const stepData = this._stepData || {}; const toSendData = {}; Object.keys(stepData).forEach((key) => { @@ -188,6 +178,12 @@ class StepFlowForm extends LitElement { .submit-spinner { margin-right: 16px; } + + ha-alert, + ha-form { + margin-top: 24px; + display: block; + } `, ]; } diff --git a/src/dialogs/config-flow/step-flow-pick-handler.ts b/src/dialogs/config-flow/step-flow-pick-handler.ts index e444729a64..e15a2d1316 100644 --- a/src/dialogs/config-flow/step-flow-pick-handler.ts +++ b/src/dialogs/config-flow/step-flow-pick-handler.ts @@ -75,6 +75,7 @@ class StepFlowPickHandler extends LitElement { return html`

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

    -
    - ${this.hass.localize("ui.auth_store.ask")} -
    -
    - - ${this.hass.localize("ui.auth_store.decline")} - - - ${this.hass.localize("ui.auth_store.confirm")} - -
    - - `; - } - - 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; - } -} diff --git a/src/dialogs/more-info/controls/more-info-camera.ts b/src/dialogs/more-info/controls/more-info-camera.ts index ce3b02140f..6d265e5b8d 100644 --- a/src/dialogs/more-info/controls/more-info-camera.ts +++ b/src/dialogs/more-info/controls/more-info-camera.ts @@ -1,5 +1,3 @@ -import "@polymer/paper-checkbox/paper-checkbox"; -import type { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox"; import { css, CSSResultGroup, @@ -12,11 +10,13 @@ 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, CAMERA_SUPPORT_STREAM, fetchCameraPrefs, + STREAM_TYPE_HLS, updateCameraPrefs, } from "../../../data/camera"; import type { HomeAssistant } from "../../../types"; @@ -24,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; @@ -54,12 +54,14 @@ class MoreInfoCamera extends LitElement { > ${this._cameraPrefs ? html` - - Preload stream - + + + Preload stream + + ` : undefined} `; @@ -82,7 +84,10 @@ class MoreInfoCamera extends LitElement { if ( curEntityId && isComponentLoaded(this.hass!, "stream") && - supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM) + 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.frontend_stream_type === STREAM_TYPE_HLS ) { // Fetch in background while we set up the video. this._fetchCameraPrefs(); @@ -97,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!, @@ -118,7 +123,7 @@ class MoreInfoCamera extends LitElement { display: block; position: relative; } - paper-checkbox { + ha-checkbox { position: absolute; top: 0; right: 0; @@ -131,3 +136,9 @@ class MoreInfoCamera extends LitElement { } customElements.define("more-info-camera", MoreInfoCamera); + +declare global { + interface HTMLElementTagNameMap { + "more-info-camera": MoreInfoCamera; + } +} diff --git a/src/dialogs/more-info/controls/more-info-fan.js b/src/dialogs/more-info/controls/more-info-fan.js index 5ef80331c1..3bfa8ec2aa 100644 --- a/src/dialogs/more-info/controls/more-info-fan.js +++ b/src/dialogs/more-info/controls/more-info-fan.js @@ -7,6 +7,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element"; import { attributeClassNames } from "../../../common/entity/attribute_class_names"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-attributes"; +import "../../../components/ha-icon"; import "../../../components/ha-icon-button"; import "../../../components/ha-labeled-slider"; import "../../../components/ha-paper-dropdown-menu"; @@ -97,17 +98,19 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
    [[localize('ui.card.fan.direction')]]
    + > + + + > + +
    diff --git a/src/dialogs/more-info/controls/more-info-light.ts b/src/dialogs/more-info/controls/more-info-light.ts index d3dfb713dc..023b4cf0a8 100644 --- a/src/dialogs/more-info/controls/more-info-light.ts +++ b/src/dialogs/more-info/controls/more-info-light.ts @@ -1,3 +1,4 @@ +import { mdiPalette } from "@mdi/js"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; import { @@ -142,7 +143,7 @@ class MoreInfoLight extends LitElement { > diff --git a/src/dialogs/more-info/controls/more-info-media_player.ts b/src/dialogs/more-info/controls/more-info-media_player.ts index 89bb143d3f..8f27cdb034 100644 --- a/src/dialogs/more-info/controls/more-info-media_player.ts +++ b/src/dialogs/more-info/controls/more-info-media_player.ts @@ -1,6 +1,14 @@ import "@material/mwc-button/mwc-button"; -import "@material/mwc-icon-button"; -import { mdiPlayBoxMultiple } from "@mdi/js"; +import { + mdiLoginVariant, + mdiMusicNote, + mdiPlayBoxMultiple, + mdiSend, + mdiVolumeHigh, + mdiVolumeMinus, + mdiVolumeOff, + mdiVolumePlus, +} from "@mdi/js"; import "@polymer/paper-input/paper-input"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; @@ -9,11 +17,10 @@ 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 "../../../components/ha-svg-icon"; import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog"; import { UNAVAILABLE, UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity"; import { @@ -56,21 +63,22 @@ class MoreInfoMediaPlayer extends LitElement { (control) => html` + .path=${control.icon} + > + ` )} ${supportsFeature(stateObj, SUPPORT_BROWSE_MEDIA) ? html` - + > ` : ""} @@ -83,9 +91,9 @@ class MoreInfoMediaPlayer extends LitElement { ${supportsFeature(stateObj, SUPPORT_VOLUME_MUTE) ? html` ` @@ -94,12 +102,12 @@ class MoreInfoMediaPlayer extends LitElement { ? html` ` @@ -124,7 +132,10 @@ class MoreInfoMediaPlayer extends LitElement { stateObj.attributes.source_list?.length ? html`
    - + @@ -149,7 +160,7 @@ class MoreInfoMediaPlayer extends LitElement { stateObj.attributes.sound_mode_list?.length ? html`
    - + @@ -222,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; } diff --git a/src/dialogs/more-info/controls/more-info-vacuum.ts b/src/dialogs/more-info/controls/more-info-vacuum.ts index 887a69cfb1..73a3c12eb0 100644 --- a/src/dialogs/more-info/controls/more-info-vacuum.ts +++ b/src/dialogs/more-info/controls/more-info-vacuum.ts @@ -1,3 +1,13 @@ +import { + mdiFan, + mdiHomeMapMarker, + mdiMapMarker, + mdiPause, + mdiPlay, + mdiPlayPause, + mdiStop, + mdiTargetVariant, +} from "@mdi/js"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; @@ -32,13 +42,13 @@ interface VacuumCommand { const VACUUM_COMMANDS: VacuumCommand[] = [ { translationKey: "start", - icon: "hass:play", + icon: mdiPlay, serviceName: "start", isVisible: (stateObj) => supportsFeature(stateObj, VACUUM_SUPPORT_START), }, { translationKey: "pause", - icon: "hass:pause", + icon: mdiPause, serviceName: "pause", isVisible: (stateObj) => // We need also to check if Start is supported because if not we show play-pause @@ -47,7 +57,7 @@ const VACUUM_COMMANDS: VacuumCommand[] = [ }, { translationKey: "start_pause", - icon: "hass:play-pause", + icon: mdiPlayPause, serviceName: "start_pause", isVisible: (stateObj) => // If start is supported, we don't show this button @@ -56,26 +66,26 @@ const VACUUM_COMMANDS: VacuumCommand[] = [ }, { translationKey: "stop", - icon: "hass:stop", + icon: mdiStop, serviceName: "stop", isVisible: (stateObj) => supportsFeature(stateObj, VACUUM_SUPPORT_STOP), }, { translationKey: "clean_spot", - icon: "hass:target-variant", + icon: mdiTargetVariant, serviceName: "clean_spot", isVisible: (stateObj) => supportsFeature(stateObj, VACUUM_SUPPORT_CLEAN_SPOT), }, { translationKey: "locate", - icon: "hass:map-marker", + icon: mdiMapMarker, serviceName: "locate", isVisible: (stateObj) => supportsFeature(stateObj, VACUUM_SUPPORT_LOCATE), }, { translationKey: "return_home", - icon: "hass:home-map-marker", + icon: mdiHomeMapMarker, serviceName: "return_to_base", isVisible: (stateObj) => supportsFeature(stateObj, VACUUM_SUPPORT_RETURN_HOME), @@ -144,10 +154,10 @@ class MoreInfoVacuum extends LitElement { (item) => html`
    - + ${stateObj.attributes.fan_speed}
    diff --git a/src/dialogs/more-info/ha-more-info-dialog.ts b/src/dialogs/more-info/ha-more-info-dialog.ts index e74420c9da..207dbeafed 100644 --- a/src/dialogs/more-info/ha-more-info-dialog.ts +++ b/src/dialogs/more-info/ha-more-info-dialog.ts @@ -1,5 +1,4 @@ import "@material/mwc-button"; -import "@material/mwc-icon-button"; import "@material/mwc-tab"; import "@material/mwc-tab-bar"; import { mdiClose, mdiCog, mdiPencil } from "@mdi/js"; @@ -17,7 +16,7 @@ import { computeStateName } from "../../common/entity/compute_state_name"; import { navigate } from "../../common/navigate"; import "../../components/ha-dialog"; import "../../components/ha-header-bar"; -import "../../components/ha-svg-icon"; +import "../../components/ha-icon-button"; import { removeEntityRegistryEntry } from "../../data/entity_registry"; import { CONTINUOUS_DOMAINS } from "../../data/logbook"; import { showEntityEditorDialog } from "../../panels/config/entities/show-dialog-entity-editor"; @@ -112,15 +111,14 @@ export class MoreInfoDialog extends LitElement { >
    - - - + .path=${mdiClose} + >
    ${this.hass.user!.is_admin ? html` - - - + > ` : ""} ${this.shouldShowEditIcon(domain, stateObj) ? html` - - - + > ` : ""} diff --git a/src/dialogs/more-info/ha-more-info-history.ts b/src/dialogs/more-info/ha-more-info-history.ts index 199e8e9490..b00a4ba695 100644 --- a/src/dialogs/more-info/ha-more-info-history.ts +++ b/src/dialogs/more-info/ha-more-info-history.ts @@ -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`` - : ""} `; + ? html`
    +
    + ${this.hass.localize("ui.dialogs.more_info_control.history")} +
    + ${this.hass.localize( + "ui.dialogs.more_info_control.show_more" + )} +
    + ` + : ""}`; } 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 { diff --git a/src/dialogs/more-info/ha-more-info-logbook.ts b/src/dialogs/more-info/ha-more-info-logbook.ts index 3f83804ff2..5932f1502d 100644 --- a/src/dialogs/more-info/ha-more-info-logbook.ts +++ b/src/dialogs/more-info/ha-more-info-logbook.ts @@ -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` +
    +
    + ${this.hass.localize("ui.dialogs.more_info_control.logbook")} +
    + ${this.hass.localize( + "ui.dialogs.more_info_control.show_more" + )} +
    { - 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); + } `, ]; } diff --git a/src/dialogs/notifications/notification-drawer.js b/src/dialogs/notifications/notification-drawer.js index f5bd40d926..303c2aaae6 100644 --- a/src/dialogs/notifications/notification-drawer.js +++ b/src/dialogs/notifications/notification-drawer.js @@ -64,7 +64,6 @@ export class HuiNotificationDrawer extends EventsMixin(
    [[localize('ui.notification_drawer.title')]]
    diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts index c0f9caf20b..80afe58dd7 100644 --- a/src/dialogs/quick-bar/ha-quick-bar.ts +++ b/src/dialogs/quick-bar/ha-quick-bar.ts @@ -35,6 +35,7 @@ import "../../components/ha-chip"; import "../../components/ha-circular-progress"; import "../../components/ha-dialog"; import "../../components/ha-header-bar"; +import "../../components/ha-icon-button"; import { domainToName } from "../../data/integration"; import { getPanelNameTranslationKey } from "../../data/panel"; import { PageNavigation } from "../../layouts/hass-tabs-subpage"; @@ -160,13 +161,12 @@ export class QuickBar extends LitElement { >`} ${this._search && html` - - - + .label=${this.hass!.localize("ui.common.clear")} + .path=${mdiClose} + > `} ${!items @@ -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 }), }; @@ -613,7 +617,7 @@ export class QuickBar extends LitElement { color: var(--primary-text-color); } - paper-input mwc-icon-button { + paper-input ha-icon-button { --mdc-icon-button-size: 24px; color: var(--primary-text-color); } diff --git a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts index 29c2a166ad..4c8fa20780 100644 --- a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts +++ b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts @@ -1,6 +1,5 @@ /* eslint-disable lit/prefer-static-styles */ -import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; -import type { PaperDialogScrollableElement } from "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; +import { mdiMicrophone } from "@mdi/js"; import "@polymer/paper-input/paper-input"; import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import { @@ -16,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, @@ -26,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; @@ -55,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; @@ -69,74 +70,42 @@ export class HaVoiceCommandDialog extends LitElement { this._agentInfo = await getAgentInfo(this.hass); } + public async closeDialog(): Promise { + 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` - - - ${this._agentInfo && this._agentInfo.onboarding - ? html` -
    - ${this._agentInfo.onboarding.text} -
    - ${this.hass.localize("ui.common.yes")}! - ${this.hass.localize("ui.common.no")} + +
    + ${this._agentInfo && this._agentInfo.onboarding + ? html` +
    + ${this._agentInfo.onboarding.text} +
    + ${this.hass.localize("ui.common.yes")}! + ${this.hass.localize("ui.common.no")} +
    -
    - ` - : ""} - + ` + : ""} ${this._conversation.map( (message) => html`
    @@ -156,8 +125,8 @@ export class HaVoiceCommandDialog extends LitElement {
    ` : ""} -
    -
    +
    +
    @@ -199,7 +168,7 @@ export class HaVoiceCommandDialog extends LitElement { ` : ""}
    - +
    `; } @@ -345,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) { @@ -367,10 +325,6 @@ export class HaVoiceCommandDialog extends LitElement { return [ haStyleDialog, css` - :host { - z-index: 103; - } - ha-icon-button { color: var(--secondary-text-color); } @@ -379,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; } @@ -393,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; diff --git a/src/entrypoints/authorize.ts b/src/entrypoints/authorize.ts index 6150ebb7e0..36954a3fcf 100644 --- a/src/entrypoints/authorize.ts +++ b/src/entrypoints/authorize.ts @@ -4,6 +4,7 @@ import "../auth/ha-authorize"; import "../resources/ha-style"; import "../resources/roboto"; import "../resources/safari-14-attachshadow-patch"; +import "../resources/array.flat.polyfill"; /* polyfill for paper-dropdown */ setTimeout( diff --git a/src/entrypoints/onboarding.ts b/src/entrypoints/onboarding.ts index 9c085ed82f..44a67890ab 100644 --- a/src/entrypoints/onboarding.ts +++ b/src/entrypoints/onboarding.ts @@ -4,6 +4,7 @@ import "../onboarding/ha-onboarding"; import "../resources/ha-style"; import "../resources/roboto"; import "../resources/safari-14-attachshadow-patch"; +import "../resources/array.flat.polyfill"; declare global { interface Window { diff --git a/src/layouts/hass-tabs-subpage-data-table.ts b/src/layouts/hass-tabs-subpage-data-table.ts index bbdc54dca5..b92f35e2e9 100644 --- a/src/layouts/hass-tabs-subpage-data-table.ts +++ b/src/layouts/hass-tabs-subpage-data-table.ts @@ -157,6 +157,7 @@ export class HaTabsSubpageDataTable extends LitElement { : hiddenLabel; const headerToolbar = html` a { + overflow: hidden; + max-width: 45%; } #tabbar.bottom-bar { diff --git a/src/layouts/supervisor-error-screen.ts b/src/layouts/supervisor-error-screen.ts index f59ae95538..cc9a9be2ad 100644 --- a/src/layouts/supervisor-error-screen.ts +++ b/src/layouts/supervisor-error-screen.ts @@ -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) || diff --git a/src/mixins/dialog-mixin.js b/src/mixins/dialog-mixin.js deleted file mode 100644 index cc8103935a..0000000000 --- a/src/mixins/dialog-mixin.js +++ /dev/null @@ -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, - }, - }; - } - } -); diff --git a/src/onboarding/action-badge.ts b/src/onboarding/action-badge.ts index e97c9c5bd8..c53f57ca52 100644 --- a/src/onboarding/action-badge.ts +++ b/src/onboarding/action-badge.ts @@ -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`
    - + ${this.badgeIcon - ? html` ` + ? html`` : ""}
    ${this.title}
    diff --git a/src/onboarding/integration-badge.ts b/src/onboarding/integration-badge.ts index c7a18f07ca..62e48d1b00 100644 --- a/src/onboarding/integration-badge.ts +++ b/src/onboarding/integration-badge.ts @@ -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` ` + ? html`` : ""}
    ${this.title}
    diff --git a/src/onboarding/onboarding-core-config.ts b/src/onboarding/onboarding-core-config.ts index 83a1e06281..d0f61cfbbc 100644 --- a/src/onboarding/onboarding-core-config.ts +++ b/src/onboarding/onboarding-core-config.ts @@ -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" )}
    - - - ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.unit_system_metric" - )} -
    - ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.metric_example" +
    + - - - ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.unit_system_imperial" - )} -
    - ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.imperial_example" +
    + ${this.hass.localize( + "ui.panel.config.core.section.core.core_config.metric_example" + )} +
    `} + > + + + - - +
    + ${this.hass.localize( + "ui.panel.config.core.section.core.core_config.imperial_example" + )} +
    `} + > + +
    +
    @@ -281,10 +294,8 @@ class OnboardingCoreConfig extends LitElement { this._location = ev.detail.location; } - private _unitSystemChanged( - ev: PolymerChangedEvent - ) { - 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; diff --git a/src/onboarding/onboarding-integrations.ts b/src/onboarding/onboarding-integrations.ts index a914d6855d..9b0370bb9b 100644 --- a/src/onboarding/onboarding-integrations.ts +++ b/src/onboarding/onboarding-integrations.ts @@ -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 { `, @@ -120,7 +121,7 @@ class OnboardingIntegrations extends LitElement { title=${this.onboardingLocalize( "ui.panel.page-onboarding.integration.more_integrations" )} - icon="hass:dots-horizontal" + .icon=${mdiDotsHorizontal} >
    diff --git a/src/panels/calendar/ha-full-calendar.ts b/src/panels/calendar/ha-full-calendar.ts index 33e6d35480..3516a14f06 100644 --- a/src/panels/calendar/ha-full-calendar.ts +++ b/src/panels/calendar/ha-full-calendar.ts @@ -11,7 +11,14 @@ import listPlugin from "@fullcalendar/list"; // @ts-ignore import listStyle from "@fullcalendar/list/main.css"; import "@material/mwc-button"; -import { mdiViewAgenda, mdiViewDay, mdiViewModule, mdiViewWeek } from "@mdi/js"; +import { + mdiChevronLeft, + mdiChevronRight, + mdiViewAgenda, + mdiViewDay, + mdiViewModule, + mdiViewWeek, +} from "@mdi/js"; import { css, CSSResultGroup, @@ -118,15 +125,15 @@ export class HAFullCalendar extends LitElement { )} @@ -144,15 +151,15 @@ export class HAFullCalendar extends LitElement {

    ${this.calendar.view.title}

    @@ -364,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 { diff --git a/src/panels/calendar/ha-panel-calendar.ts b/src/panels/calendar/ha-panel-calendar.ts index 91cad5b3a6..ac3862380c 100644 --- a/src/panels/calendar/ha-panel-calendar.ts +++ b/src/panels/calendar/ha-panel-calendar.ts @@ -1,5 +1,6 @@ import "@material/mwc-checkbox"; import "@material/mwc-formfield"; +import { mdiRefresh } from "@mdi/js"; import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-toolbar/app-toolbar"; import { @@ -15,6 +16,7 @@ import { styleMap } from "lit/directives/style-map"; import { LocalStorage } from "../../common/decorators/local-storage"; import { HASSDomEvent } from "../../common/dom/fire_event"; import "../../components/ha-card"; +import "../../components/ha-icon-button"; import "../../components/ha-menu-button"; import { Calendar, @@ -66,7 +68,8 @@ class PanelCalendar extends LitElement { >
    ${this.hass.localize("panel.calendar")}
    diff --git a/src/panels/config/areas/dialog-area-registry-detail.ts b/src/panels/config/areas/dialog-area-registry-detail.ts index 5daf53b5e0..217a330d49 100644 --- a/src/panels/config/areas/dialog-area-registry-detail.ts +++ b/src/panels/config/areas/dialog-area-registry-detail.ts @@ -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"; diff --git a/src/panels/config/areas/ha-config-area-page.ts b/src/panels/config/areas/ha-config-area-page.ts index 3dca119277..ece1334bce 100644 --- a/src/panels/config/areas/ha-config-area-page.ts +++ b/src/panels/config/areas/ha-config-area-page.ts @@ -1,4 +1,5 @@ import "@material/mwc-button"; +import { mdiCog } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; @@ -6,6 +7,7 @@ import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { computeStateName } from "../../../common/entity/compute_state_name"; import "../../../components/ha-card"; +import "../../../components/ha-icon-button"; import { AreaRegistryEntry, deleteAreaRegistryEntry, @@ -136,9 +138,10 @@ class HaConfigAreaPage extends LitElement {
    diff --git a/src/panels/config/areas/ha-config-areas-dashboard.ts b/src/panels/config/areas/ha-config-areas-dashboard.ts index 92bdbe225b..a2d27e63b7 100644 --- a/src/panels/config/areas/ha-config-areas-dashboard.ts +++ b/src/panels/config/areas/ha-config-areas-dashboard.ts @@ -1,4 +1,4 @@ -import { mdiPlus } from "@mdi/js"; +import { mdiHelpCircle, mdiPlus } from "@mdi/js"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item-body"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; @@ -147,7 +147,8 @@ export class HaConfigAreasDashboard extends LitElement { > OPTIONS.find((option) => option in action); +const getType = (action: Action | undefined) => + action ? OPTIONS.find((option) => option in action) : undefined; declare global { // for fire event @@ -98,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; @@ -124,41 +139,32 @@ export default class HaAutomationActionRow extends LitElement {
    ${this.index !== 0 ? html` - - - + > ` : ""} ${this.index !== this.totalActions - 1 ? html` - - - + > ` : ""} - - + .label=${this.hass.localize("ui.common.menu")} + .path=${mdiDotsVertical} + > ${yamlMode ? this.hass.localize( @@ -181,9 +187,12 @@ export default class HaAutomationActionRow extends LitElement {
    ${this._warnings - ? html`
    - ${this.hass.localize("ui.errors.config.editor_not_supported")}: -
    + ? html` ${this._warnings!.length > 0 && this._warnings![0] !== undefined ? html`
      ${this._warnings!.map( @@ -192,7 +201,7 @@ export default class HaAutomationActionRow extends LitElement {
    ` : ""} ${this.hass.localize("ui.errors.config.edit_in_yaml_supported")} -
    ` + ` : ""} ${yamlMode ? html` @@ -216,28 +225,21 @@ export default class HaAutomationActionRow extends LitElement { > ` : html` - - - ${OPTIONS.map( - (opt) => html` - - ${this.hass.localize( - `ui.panel.config.automation.editor.actions.type.${opt}.label` - )} - - ` - )} - - + ${this._processedTypes(this.hass.localize).map( + ([opt, label]) => html` + ${label} + ` + )} + +
    ${dynamicElement(`ha-automation-action-${type}`, { hass: this.hass, @@ -297,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; diff --git a/src/panels/config/automation/action/types/ha-automation-action-choose.ts b/src/panels/config/automation/action/types/ha-automation-action-choose.ts index 584d217b90..f342512745 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-choose.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-choose.ts @@ -5,6 +5,7 @@ import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; import { ensureArray } from "../../../../../common/ensure-array"; +import "../../../../../components/ha-icon-button"; import { Condition } from "../../../../../data/automation"; import { Action, ChooseAction } from "../../../../../data/script"; import { haStyle } from "../../../../../resources/styles"; @@ -28,15 +29,14 @@ export class HaChooseAction extends LitElement implements ActionElement { return html` ${(action.choose ? ensureArray(action.choose) : []).map( (option, idx) => html` - - - + .path=${mdiDelete} + >

    ${this.hass.localize( @@ -161,7 +161,7 @@ export class HaChooseAction extends LitElement implements ActionElement { display: block; text-align: center; } - mwc-icon-button { + ha-icon-button { position: absolute; right: 0; padding: 4px; diff --git a/src/panels/config/automation/action/types/ha-automation-action-delay.ts b/src/panels/config/automation/action/types/ha-automation-action-delay.ts index e03a533be6..488494e65e 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-delay.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-delay.ts @@ -40,6 +40,9 @@ export class HaDelayAction extends LitElement implements ActionElement { protected render() { return html` + 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 { > ` : html` - - - ${OPTIONS.map( - (opt) => html` - - ${this.hass.localize( - `ui.panel.config.automation.editor.conditions.type.${opt}.label` - )} - - ` - )} - - + ${this._processedTypes(this.hass.localize).map( + ([opt, label]) => html` + ${label} + ` + )} + +
    ${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; diff --git a/src/panels/config/automation/condition/ha-automation-condition-row.ts b/src/panels/config/automation/condition/ha-automation-condition-row.ts index 8015055358..325949c3b5 100644 --- a/src/panels/config/automation/condition/ha-automation-condition-row.ts +++ b/src/panels/config/automation/condition/ha-automation-condition-row.ts @@ -60,12 +60,12 @@ export default class HaAutomationConditionRow extends LitElement {
    - + .label=${this.hass.localize("ui.common.menu")} + .path=${mdiDotsVertical} + > + ${this._yamlMode ? this.hass.localize( diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-logical.ts b/src/panels/config/automation/condition/types/ha-automation-condition-logical.ts index 47fbba8de6..35e9685463 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-logical.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-logical.ts @@ -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() { diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-sun.ts b/src/panels/config/automation/condition/types/ha-automation-condition-sun.ts index bda9399539..0cbf5a339a 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-sun.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-sun.ts @@ -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=${this.hass.localize("ui.common.menu")} + .path=${mdiDotsVertical} + > setTimeout(resolve, 0)); } showAutomationEditor({ diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index 21eedd5009..22a7a152cd 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -1,16 +1,15 @@ -import "@material/mwc-icon-button"; import { mdiHelpCircle, mdiHistory, 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"; @@ -21,7 +20,9 @@ import { DataTableColumnContainer } from "../../../components/data-table/ha-data import "../../../components/entity/ha-entity-toggle"; 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` ${this.hass.localize("ui.card.automation.trigger")} @@ -143,83 +144,73 @@ class HaAutomationPicker extends LitElement { `, }; } - columns.info = { + columns.actions = { title: "", - type: "icon-button", - template: (_info, automation) => html` - - - - `, - }; - columns.trace = { - title: "", - type: "icon-button", + type: "overflow-menu", template: (_info, automation: any) => html` - 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)" > - - - - - ${!automation.attributes.id - ? html` - - ${this.hass.localize( - "ui.panel.config.automation.picker.dev_only_editable" - )} - - ` - : ""} - `, - }; - columns.edit = { - title: "", - type: "icon-button", - template: (_info, automation: any) => html` - - - - - - ${!automation.attributes.id - ? html` - - ${this.hass.localize( - "ui.panel.config.automation.picker.only_editable" - )} - - ` - : ""} + `, }; return columns; @@ -244,9 +235,12 @@ class HaAutomationPicker extends LitElement { @clear-filter=${this._clearFilter} hasFab > - - - + { - 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() { diff --git a/src/panels/config/automation/ha-automation-trace.ts b/src/panels/config/automation/ha-automation-trace.ts index c4066847cb..63b37c0efd 100644 --- a/src/panels/config/automation/ha-automation-trace.ts +++ b/src/panels/config/automation/ha-automation-trace.ts @@ -11,7 +11,18 @@ import { classMap } from "lit/directives/class-map"; import { repeat } from "lit/directives/repeat"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_time"; +import "../../../components/ha-icon-button"; +import "../../../components/trace/ha-trace-blueprint-config"; +import "../../../components/trace/ha-trace-config"; +import "../../../components/trace/ha-trace-logbook"; +import "../../../components/trace/ha-trace-path-details"; +import "../../../components/trace/ha-trace-timeline"; import "../../../components/trace/hat-script-graph"; +import type { + HatScriptGraph, + NodeInfo, +} from "../../../components/trace/hat-script-graph"; +import { traceTabStyles } from "../../../components/trace/trace-tab-styles"; import { AutomationEntity } from "../../../data/automation"; import { getLogbookDataForContext, LogbookEntry } from "../../../data/logbook"; import { @@ -24,16 +35,6 @@ import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant, Route } from "../../../types"; import { configSections } from "../ha-panel-config"; -import "../../../components/trace/ha-trace-blueprint-config"; -import "../../../components/trace/ha-trace-config"; -import "../../../components/trace/ha-trace-logbook"; -import "../../../components/trace/ha-trace-path-details"; -import "../../../components/trace/ha-trace-timeline"; -import { traceTabStyles } from "../../../components/trace/trace-tab-styles"; -import type { - HatScriptGraph, - NodeInfo, -} from "../../../components/trace/hat-script-graph"; @customElement("ha-automation-trace") export class HaAutomationTrace extends LitElement { @@ -90,16 +91,19 @@ export class HaAutomationTrace extends LitElement { } const actionButtons = html` - - - - + - - + > `; return html` @@ -122,23 +126,28 @@ export class HaAutomationTrace extends LitElement { class="linkButton" href="/config/automation/edit/${this.automationId}" > - - - +
    ` : ""} ${this._traces && this._traces.length > 0 ? html`
    - - - + > - - - + >
    ` : ""} diff --git a/src/panels/config/automation/thingtalk/dialog-thingtalk.ts b/src/panels/config/automation/thingtalk/dialog-thingtalk.ts index 3c6752ad46..a19ccbed10 100644 --- a/src/panels/config/automation/thingtalk/dialog-thingtalk.ts +++ b/src/panels/config/automation/thingtalk/dialog-thingtalk.ts @@ -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 { 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 { `; } return html` - -

    - ${this.hass.localize( - `ui.panel.config.automation.thingtalk.task_selection.header` - )} -

    - +
    ${this._error ? html`
    ${this._error}
    ` : ""} ${this.hass.localize( `ui.panel.config.automation.thingtalk.task_selection.introduction` @@ -143,23 +134,25 @@ class DialogThingtalk extends LitElement { class="attribution" >Powered by Almond - -
    - - ${this.hass.localize(`ui.common.skip`)} - - - ${this._submitting - ? html`` - : ""} - ${this.hass.localize(`ui.panel.config.automation.thingtalk.create`)} -
    - + + ${this.hass.localize(`ui.common.skip`)} + + + ${this._submitting + ? html`` + : ""} + ${this.hass.localize(`ui.panel.config.automation.thingtalk.create`)} + + `; } @@ -234,12 +227,6 @@ class DialogThingtalk extends LitElement { this.closeDialog(); }; - private _openedChanged(ev: PolymerChangedEvent): 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 { diff --git a/src/panels/config/automation/thingtalk/ha-thingtalk-placeholders.ts b/src/panels/config/automation/thingtalk/ha-thingtalk-placeholders.ts index 0f62bb0f39..9731a96394 100644 --- a/src/panels/config/automation/thingtalk/ha-thingtalk-placeholders.ts +++ b/src/panels/config/automation/thingtalk/ha-thingtalk-placeholders.ts @@ -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` - -

    - ${this.hass.localize( - `ui.panel.config.automation.thingtalk.link_devices.header` - )} -

    - +
    ${this._error ? html`
    ${this._error}
    ` : ""} ${Object.entries(this.placeholders).map( ([type, placeholders]) => @@ -243,16 +238,18 @@ export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) { })} ` )} - -
    - - ${this.hass.localize(`ui.common.skip`)} - - - ${this.hass.localize(`ui.panel.config.automation.thingtalk.create`)} -
    - + + ${this.hass.localize(`ui.common.skip`)} + + + ${this.hass.localize(`ui.panel.config.automation.thingtalk.create`)} + + `; } @@ -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): 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; diff --git a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts index 6444b6aaef..3e8a6bee37 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts @@ -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; @@ -94,12 +109,11 @@ export default class HaAutomationTriggerRow extends LitElement {
    - + .label=${this.hass.localize("ui.common.menu")} + .path=${mdiDotsVertical} + > ${yamlMode ? this.hass.localize( @@ -122,9 +136,12 @@ export default class HaAutomationTriggerRow extends LitElement {
    ${this._warnings - ? html`
    - ${this.hass.localize("ui.errors.config.editor_not_supported")}: -
    + ? html` ${this._warnings.length && this._warnings[0] !== undefined ? html`
      ${this._warnings.map( @@ -133,7 +150,7 @@ export default class HaAutomationTriggerRow extends LitElement {
    ` : ""} ${this.hass.localize("ui.errors.config.edit_in_yaml_supported")} -
    ` + ` : ""} ${yamlMode ? html` @@ -157,28 +174,21 @@ export default class HaAutomationTriggerRow extends LitElement { > ` : html` - - - ${OPTIONS.map( - (opt) => html` - - ${this.hass.localize( - `ui.panel.config.automation.editor.triggers.type.${opt}.label` - )} - - ` - )} - - + ${this._processedTypes(this.hass.localize).map( + ([opt, label]) => html` + ${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 { diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-homeassistant.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-homeassistant.ts index f4464a4b36..c660b8f132 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-homeassistant.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-homeassistant.ts @@ -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" )} - - - - ${this.hass.localize( + - - ${this.hass.localize( + > + + + - + > + + + `; } @@ -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 { diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts index 7a1327e2ef..218bb8273b 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts @@ -12,7 +12,7 @@ import { handleChangeEvent } from "../ha-automation-trigger-row"; import "../../../../../components/ha-duration-input"; @customElement("ha-automation-trigger-numeric_state") -export default class HaNumericStateTrigger extends LitElement { +export class HaNumericStateTrigger extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property() public trigger!: NumericStateTrigger; diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-sun.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-sun.ts index d940a3e1cc..b996cbdcbf 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-sun.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-sun.ts @@ -1,10 +1,10 @@ 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 "../../../../../components/ha-radio"; +import "../../../../../components/ha-formfield"; +import type { HaRadio } from "../../../../../components/ha-radio"; import type { SunTrigger } from "../../../../../data/automation"; import type { HomeAssistant } from "../../../../../types"; import { @@ -16,38 +16,47 @@ import { export class HaSunTrigger extends LitElement implements TriggerElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public trigger!: SunTrigger; + @property({ attribute: false }) public trigger!: SunTrigger; public static get defaultConfig() { return { - event: "sunrise", + event: "sunrise" as SunTrigger["event"], + offset: 0, }; } protected render() { const { offset, event } = this.trigger; return html` - - `; } @@ -97,10 +106,17 @@ export class HaZoneTrigger 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 { diff --git a/src/panels/config/blueprint/dialog-import-blueprint.ts b/src/panels/config/blueprint/dialog-import-blueprint.ts index e9d9a4f0ea..ad2cb1eb6c 100644 --- a/src/panels/config/blueprint/dialog-import-blueprint.ts +++ b/src/panels/config/blueprint/dialog-import-blueprint.ts @@ -1,5 +1,4 @@ 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 { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; diff --git a/src/panels/config/blueprint/ha-blueprint-overview.ts b/src/panels/config/blueprint/ha-blueprint-overview.ts index e5bfb0c997..35123dceb0 100644 --- a/src/panels/config/blueprint/ha-blueprint-overview.ts +++ b/src/panels/config/blueprint/ha-blueprint-overview.ts @@ -1,4 +1,3 @@ -import "@material/mwc-icon-button"; import { mdiDelete, mdiDownload, @@ -22,6 +21,7 @@ import { extractSearchParam } from "../../../common/url/search-params"; import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; import "../../../components/entity/ha-entity-toggle"; import "../../../components/ha-fab"; +import "../../../components/ha-icon-button"; import "../../../components/ha-svg-icon"; import { showAutomationEditor } from "../../../data/automation"; import { @@ -29,6 +29,7 @@ import { Blueprints, deleteBlueprint, } from "../../../data/blueprint"; +import { showScriptEditor } from "../../../data/script"; import { showAlertDialog, showConfirmationDialog, @@ -52,6 +53,12 @@ const createNewFunctions = { use_blueprint: { path: blueprintMeta.path }, }); }, + script: (blueprintMeta: BlueprintMetaDataPath) => { + showScriptEditor({ + alias: blueprintMeta.name, + use_blueprint: { path: blueprintMeta.path }, + }); + }, }; @customElement("ha-blueprint-overview") @@ -62,27 +69,38 @@ class HaBlueprintOverview extends LitElement { @property({ type: Boolean }) public narrow!: boolean; - @property() public route!: Route; + @property({ attribute: false }) public route!: Route; - @property() public blueprints!: Blueprints; + @property({ attribute: false }) public blueprints!: Record< + string, + Blueprints + >; - private _processedBlueprints = memoizeOne((blueprints: Blueprints) => { - const result = Object.entries(blueprints).map(([path, blueprint]) => { - if ("error" in blueprint) { - return { - name: blueprint.error, - error: true, - path, - }; - } - return { - ...blueprint.metadata, - error: false, - path, - }; - }); - return result; - }); + private _processedBlueprints = memoizeOne( + (blueprints: Record) => { + const result: any[] = []; + Object.entries(blueprints).forEach(([type, typeBlueprints]) => + Object.entries(typeBlueprints).forEach(([path, blueprint]) => { + if ("error" in blueprint) { + result.push({ + name: blueprint.error, + type, + error: true, + path, + }); + } else { + result.push({ + ...blueprint.metadata, + type, + error: false, + path, + }); + } + }) + ); + return result; + } + ); private _columns = memoizeOne( (narrow, _language): DataTableColumnContainer => ({ @@ -102,6 +120,20 @@ class HaBlueprintOverview extends LitElement { ` : undefined, }, + type: { + title: this.hass.localize( + "ui.panel.config.blueprint.overview.headers.type" + ), + template: (type: string) => + html`${this.hass.localize( + `ui.panel.config.blueprint.overview.types.${type}` + )}`, + sortable: true, + filterable: true, + hidden: narrow, + direction: "asc", + width: "10%", + }, path: { title: this.hass.localize( "ui.panel.config.blueprint.overview.headers.file_name" @@ -114,29 +146,27 @@ class HaBlueprintOverview extends LitElement { }, create: { title: "", + width: narrow ? undefined : "20%", type: narrow ? "icon-button" : undefined, template: (_, blueprint: any) => blueprint.error ? "" : narrow - ? html` - - ` + ` : html` ${this.hass.localize( - "ui.panel.config.blueprint.overview.use_blueprint" + `ui.panel.config.blueprint.overview.create_${blueprint.domain}` )} `, }, @@ -146,7 +176,7 @@ class HaBlueprintOverview extends LitElement { template: (_, blueprint: any) => blueprint.error ? "" - : html``, + >`, }, delete: { title: "", @@ -164,14 +194,14 @@ class HaBlueprintOverview extends LitElement { template: (_, blueprint: any) => blueprint.error ? "" - : html` `, + >`, }, }) ); @@ -220,9 +250,12 @@ class HaBlueprintOverview extends LitElement {
    `} > - - - + = {}; protected routerOptions: RouterOptions = { defaultPage: "dashboard", @@ -41,7 +41,11 @@ class HaConfigBlueprint extends HassRouterPage { }; private async _getBlueprints() { - this.blueprints = await fetchBlueprints(this.hass, "automation"); + const [automation, script] = await Promise.all([ + fetchBlueprints(this.hass, "automation"), + fetchBlueprints(this.hass, "script"), + ]); + this.blueprints = { automation, script }; } protected firstUpdated(changedProps) { diff --git a/src/panels/config/cloud/account/cloud-account.js b/src/panels/config/cloud/account/cloud-account.js deleted file mode 100644 index 9670794dfc..0000000000 --- a/src/panels/config/cloud/account/cloud-account.js +++ /dev/null @@ -1,246 +0,0 @@ -import "@material/mwc-button"; -import "@polymer/paper-item/paper-item-body"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { formatDateTime } from "../../../../common/datetime/format_date_time"; -import { computeRTLDirection } from "../../../../common/util/compute_rtl"; -import "../../../../components/buttons/ha-call-api-button"; -import "../../../../components/ha-card"; -import { fetchCloudSubscriptionInfo } from "../../../../data/cloud"; -import "../../../../layouts/hass-subpage"; -import { EventsMixin } from "../../../../mixins/events-mixin"; -import LocalizeMixin from "../../../../mixins/localize-mixin"; -import "../../../../styles/polymer-ha-style"; -import "../../ha-config-section"; -import "./cloud-alexa-pref"; -import "./cloud-google-pref"; -import "./cloud-remote-pref"; -import "./cloud-tts-pref"; -import "./cloud-webhooks"; - -/* - * @appliesMixin EventsMixin - * @appliesMixin LocalizeMixin - */ -class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) { - static get template() { - return html` - - -
    - - Home Assistant Cloud -
    -

    - [[localize('ui.panel.config.cloud.account.thank_you_note')]] -

    -
    - - - - - - -
    - - [[localize('ui.panel.config.cloud.account.manage_account')]] - - [[localize('ui.panel.config.cloud.account.sign_out')]] -
    -
    -
    - - - [[localize('ui.panel.config.cloud.account.integrations')]] -
    -

    - [[localize('ui.panel.config.cloud.account.integrations_introduction')]] -

    -

    - [[localize('ui.panel.config.cloud.account.integrations_introduction2')]] - - [[localize('ui.panel.config.cloud.account.integrations_link_all_features')]]. -

    -
    - - - - - - - - - - -
    -
    -
    - `; - } - - static get properties() { - return { - hass: Object, - isWide: Boolean, - narrow: Boolean, - cloudStatus: Object, - _subscription: { - type: Object, - value: null, - }, - _rtlDirection: { - type: Boolean, - computed: "_computeRTLDirection(hass)", - }, - }; - } - - ready() { - super.ready(); - this._fetchSubscriptionInfo(); - } - - _computeConnectionStatus(status) { - return status === "connected" - ? this.hass.localize("ui.panel.config.cloud.account.connected") - : status === "disconnected" - ? this.hass.localize("ui.panel.config.cloud.account.not_connected") - : this.hass.localize("ui.panel.config.cloud.account.connecting"); - } - - async _fetchSubscriptionInfo() { - this._subscription = await fetchCloudSubscriptionInfo(this.hass); - if ( - this._subscription.provider && - this.cloudStatus && - this.cloudStatus.cloud !== "connected" - ) { - this.fire("ha-refresh-cloud-status"); - } - } - - handleLogout() { - this.hass - .callApi("post", "cloud/logout") - .then(() => this.fire("ha-refresh-cloud-status")); - } - - _formatSubscription(subInfo) { - if (subInfo === null) { - return this.hass.localize( - "ui.panel.config.cloud.account.fetching_subscription" - ); - } - - let description = subInfo.human_description; - - if (subInfo.plan_renewal_date) { - description = description.replace( - "{periodEnd}", - formatDateTime( - new Date(subInfo.plan_renewal_date * 1000), - this.hass.locale - ) - ); - } - - return description; - } - - _computeRTLDirection(hass) { - return computeRTLDirection(hass); - } -} - -customElements.define("cloud-account", CloudAccount); diff --git a/src/panels/config/cloud/account/cloud-account.ts b/src/panels/config/cloud/account/cloud-account.ts new file mode 100644 index 0000000000..8e98482979 --- /dev/null +++ b/src/panels/config/cloud/account/cloud-account.ts @@ -0,0 +1,268 @@ +import "@material/mwc-button"; +import "@polymer/paper-item/paper-item-body"; +import { LitElement, css, html, PropertyValues } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { formatDateTime } from "../../../../common/datetime/format_date_time"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { computeRTLDirection } from "../../../../common/util/compute_rtl"; +import "../../../../components/buttons/ha-call-api-button"; +import "../../../../components/ha-card"; +import { + cloudLogout, + CloudStatusLoggedIn, + fetchCloudSubscriptionInfo, + SubscriptionInfo, +} from "../../../../data/cloud"; +import "../../../../layouts/hass-subpage"; +import { HomeAssistant } from "../../../../types"; +import "../../ha-config-section"; +import "./cloud-alexa-pref"; +import "./cloud-google-pref"; +import "./cloud-remote-pref"; +import "./cloud-tts-pref"; +import "./cloud-webhooks"; + +@customElement("cloud-account") +export class CloudAccount extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public isWide = false; + + @property({ type: Boolean }) public narrow = false; + + @property({ attribute: false }) public cloudStatus!: CloudStatusLoggedIn; + + @state() private _subscription?: SubscriptionInfo; + + @state() private _rtlDirection: "rtl" | "ltr" = "rtl"; + + protected render() { + return html` + +
    + + Home Assistant Cloud +
    +

    + ${this.hass.localize( + "ui.panel.config.cloud.account.thank_you_note" + )} +

    +
    + + + + + + +
    + + + ${this.hass.localize( + "ui.panel.config.cloud.account.manage_account" + )} + + + ${this.hass.localize( + "ui.panel.config.cloud.account.sign_out" + )} +
    +
    +
    + + + ${this.hass.localize( + "ui.panel.config.cloud.account.integrations" + )} +
    +

    + ${this.hass.localize( + "ui.panel.config.cloud.account.integrations_introduction" + )} +

    +

    + ${this.hass.localize( + "ui.panel.config.cloud.account.integrations_introduction2" + )} + + ${this.hass.localize( + "ui.panel.config.cloud.account.integrations_link_all_features" + )}. +

    +
    + + + + + + + + + + +
    +
    +
    + `; + } + + firstUpdated() { + this._fetchSubscriptionInfo(); + } + + protected updated(changedProps: PropertyValues) { + if (changedProps.has("hass")) { + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + if (!oldHass || oldHass.locale !== this.hass.locale) { + this._rtlDirection = computeRTLDirection(this.hass); + } + } + } + + private async _fetchSubscriptionInfo() { + this._subscription = await fetchCloudSubscriptionInfo(this.hass); + if ( + this._subscription.provider && + this.cloudStatus && + this.cloudStatus.cloud !== "connected" + ) { + fireEvent(this, "ha-refresh-cloud-status"); + } + } + + private async _handleLogout() { + await cloudLogout(this.hass); + fireEvent(this, "ha-refresh-cloud-status"); + } + + _computeRTLDirection(hass) { + return computeRTLDirection(hass); + } + + static get styles() { + return css` + [slot="introduction"] { + margin: -1em 0; + } + [slot="introduction"] a { + color: var(--primary-color); + } + .content { + padding-bottom: 24px; + } + .account-row { + display: flex; + padding: 0 16px; + } + .card-actions { + display: flex; + justify-content: space-between; + } + .card-actions a { + text-decoration: none; + } + mwc-button { + align-self: center; + } + .wrap { + white-space: normal; + } + .status { + text-transform: capitalize; + padding: 16px; + } + a { + color: var(--primary-color); + } + `; + } +} + +customElements.define("cloud-account", CloudAccount); + +declare global { + interface HTMLElementTagNameMap { + "cloud-account": CloudAccount; + } +} diff --git a/src/panels/config/cloud/alexa/cloud-alexa.ts b/src/panels/config/cloud/alexa/cloud-alexa.ts index a375552df0..90f43f83da 100644 --- a/src/panels/config/cloud/alexa/cloud-alexa.ts +++ b/src/panels/config/cloud/alexa/cloud-alexa.ts @@ -116,25 +116,22 @@ class CloudAlexa extends LitElement { ? exposedCards : notExposedCards; - const iconButton = html` - - `; + .label=${this.hass!.localize("ui.panel.config.cloud.alexa.expose")} + .path=${config.should_expose !== null + ? isExposed + ? mdiCheckboxMarked + : mdiCloseBox + : isDomainExposed + ? mdiCheckboxMultipleMarked + : mdiCloseBoxMultiple} + >`; target.push(html` diff --git a/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts b/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts index a91261914e..0616c0edd9 100644 --- a/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts +++ b/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts @@ -2,8 +2,7 @@ import "@material/mwc-button"; import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { formatDateTime } from "../../../../common/datetime/format_date_time"; -import "../../../../components/dialog/ha-paper-dialog"; -import type { HaPaperDialog } from "../../../../components/dialog/ha-paper-dialog"; +import { fireEvent } from "../../../../common/dom/fire_event"; import { haStyle } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; import type { CloudCertificateParams as CloudCertificateDialogParams } from "./show-dialog-cloud-certificate"; @@ -15,11 +14,13 @@ class DialogCloudCertificate extends LitElement { @property() private _params?: CloudCertificateDialogParams; - public async showDialog(params: CloudCertificateDialogParams) { + public showDialog(params: CloudCertificateDialogParams) { this._params = params; - // Wait till dialog is rendered. - await this.updateComplete; - this._dialog.open(); + } + + public closeDialog() { + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); } protected render() { @@ -29,12 +30,12 @@ class DialogCloudCertificate extends LitElement { const { certificateInfo } = this._params; return html` - -

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

    +

    ${this.hass!.localize( @@ -56,31 +57,21 @@ class DialogCloudCertificate extends LitElement {

    -
    - ${this.hass!.localize( - "ui.panel.config.cloud.dialog_certificate.close" - )} -
    -
    + + ${this.hass!.localize( + "ui.panel.config.cloud.dialog_certificate.close" + )} + + `; } - private get _dialog(): HaPaperDialog { - return this.shadowRoot!.querySelector("ha-paper-dialog")!; - } - - private _closeDialog() { - this._dialog.close(); - } - static get styles(): CSSResultGroup { return [ haStyle, css` - ha-paper-dialog { - width: 535px; + ha-dialog { + --mdc-dialog-max-width: 535px; } .break-word { overflow-wrap: break-word; diff --git a/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts b/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts index ce44207e7c..b018af2959 100644 --- a/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts +++ b/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts @@ -1,11 +1,9 @@ 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 } from "lit"; import { state } from "lit/decorators"; -import "../../../../components/dialog/ha-paper-dialog"; -import type { HaPaperDialog } from "../../../../components/dialog/ha-paper-dialog"; +import { fireEvent } from "../../../../common/dom/fire_event"; import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../../../resources/styles"; import { HomeAssistant } from "../../../../types"; @@ -19,11 +17,13 @@ export class DialogManageCloudhook extends LitElement { @state() private _params?: WebhookDialogParams; - public async showDialog(params: WebhookDialogParams) { + public showDialog(params: WebhookDialogParams) { this._params = params; - // Wait till dialog is rendered. - await this.updateComplete; - this._dialog.open(); + } + + public closeDialog() { + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); } protected render() { @@ -39,14 +39,14 @@ export class DialogManageCloudhook extends LitElement { ) : documentationUrl(this.hass!, `/integrations/${webhook.domain}/`); return html` - -

    - ${this.hass!.localize( - "ui.panel.config.cloud.dialog_cloudhook.webhook_for", - "name", - webhook.name - )} -

    +

    ${this.hass!.localize( @@ -79,36 +79,29 @@ export class DialogManageCloudhook extends LitElement {

    -
    - - ${this.hass!.localize( - "ui.panel.config.cloud.dialog_cloudhook.view_documentation" - )} - - ${this.hass!.localize( - "ui.panel.config.cloud.dialog_cloudhook.close" - )} -
    -
    + + + ${this.hass!.localize( + "ui.panel.config.cloud.dialog_cloudhook.view_documentation" + )} + + + + ${this.hass!.localize("ui.panel.config.cloud.dialog_cloudhook.close")} + + `; } - private get _dialog(): HaPaperDialog { - return this.shadowRoot!.querySelector("ha-paper-dialog")!; - } - private get _paperInput(): PaperInputElement { return this.shadowRoot!.querySelector("paper-input")!; } - private _closeDialog() { - this._dialog.close(); - } - private async _disableWebhook() { showConfirmationDialog(this, { text: this.hass!.localize( @@ -118,7 +111,7 @@ export class DialogManageCloudhook extends LitElement { confirmText: this.hass!.localize("ui.common.disable"), confirm: () => { this._params!.disableHook(); - this._closeDialog(); + this.closeDialog(); }, }); } @@ -147,7 +140,7 @@ export class DialogManageCloudhook extends LitElement { return [ haStyle, css` - ha-paper-dialog { + ha-dialog { width: 650px; } paper-input { @@ -156,7 +149,7 @@ export class DialogManageCloudhook extends LitElement { button.link { color: var(--primary-color); } - .paper-dialog-buttons a { + a { text-decoration: none; } `, diff --git a/src/panels/config/cloud/forgot-password/cloud-forgot-password.js b/src/panels/config/cloud/forgot-password/cloud-forgot-password.js deleted file mode 100644 index 46b45c9022..0000000000 --- a/src/panels/config/cloud/forgot-password/cloud-forgot-password.js +++ /dev/null @@ -1,152 +0,0 @@ -import "@polymer/paper-input/paper-input"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "../../../../components/buttons/ha-progress-button"; -import "../../../../components/ha-card"; -import "../../../../layouts/hass-subpage"; -import { EventsMixin } from "../../../../mixins/events-mixin"; -import LocalizeMixin from "../../../../mixins/localize-mixin"; -import "../../../../styles/polymer-ha-style"; - -/* - * @appliesMixin EventsMixin - * @appliesMixin LocalizeMixin - */ -class CloudForgotPassword extends LocalizeMixin(EventsMixin(PolymerElement)) { - static get template() { - return html` - - -
    - -
    -

    - [[localize('ui.panel.config.cloud.forgot_password.instructions')]] -

    -
    [[_error]]
    - -
    -
    - [[localize('ui.panel.config.cloud.forgot_password.send_reset_email')]] -
    -
    -
    -
    - `; - } - - static get properties() { - return { - hass: Object, - narrow: Boolean, - email: { - type: String, - notify: true, - observer: "_emailChanged", - }, - _requestInProgress: { - type: Boolean, - value: false, - }, - _error: { - type: String, - value: "", - }, - }; - } - - _emailChanged() { - this._error = ""; - this.$.email.invalid = false; - } - - _keyDown(ev) { - // validate on enter - if (ev.keyCode === 13) { - this._handleEmailPasswordReset(); - ev.preventDefault(); - } - } - - _handleEmailPasswordReset() { - if (!this.email || !this.email.includes("@")) { - this.$.email.invalid = true; - } - - if (this.$.email.invalid) return; - - this._requestInProgress = true; - - this.hass - .callApi("post", "cloud/forgot_password", { - email: this.email, - }) - .then( - () => { - this._requestInProgress = false; - this.fire("cloud-done", { - flashMessage: this.hass.localize( - "ui.panel.config.cloud.forgot_password.check_your_email" - ), - }); - }, - (err) => - this.setProperties({ - _requestInProgress: false, - _error: - err && err.body && err.body.message - ? err.body.message - : "Unknown error", - }) - ); - } -} - -customElements.define("cloud-forgot-password", CloudForgotPassword); diff --git a/src/panels/config/cloud/forgot-password/cloud-forgot-password.ts b/src/panels/config/cloud/forgot-password/cloud-forgot-password.ts new file mode 100644 index 0000000000..a6b34b9054 --- /dev/null +++ b/src/panels/config/cloud/forgot-password/cloud-forgot-password.ts @@ -0,0 +1,156 @@ +import "@material/mwc-textfield/mwc-textfield"; +import type { TextField } from "@material/mwc-textfield/mwc-textfield"; +import { css, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/buttons/ha-progress-button"; +import "../../../../components/ha-alert"; +import "../../../../components/ha-card"; +import { cloudForgotPassword } from "../../../../data/cloud"; +import "../../../../layouts/hass-subpage"; +import { haStyle } from "../../../../resources/styles"; +import { HomeAssistant } from "../../../../types"; + +@customElement("cloud-forgot-password") +export class CloudForgotPassword extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public narrow = false; + + @property() public email?: string; + + @state() public _requestInProgress = false; + + @state() private _error?: string; + + @query("#email", true) private _emailField!: TextField; + + protected render(): TemplateResult { + return html` + +
    + +
    +

    + ${this.hass.localize( + "ui.panel.config.cloud.forgot_password.instructions" + )} +

    + ${this._error + ? html`${this._error}` + : ""} + +
    +
    + + ${this.hass.localize( + "ui.panel.config.cloud.forgot_password.send_reset_email" + )} + +
    +
    +
    +
    + `; + } + + private _keyDown(ev: KeyboardEvent) { + if (ev.key === "Enter") { + this._handleEmailPasswordReset(); + } + } + + private async _handleEmailPasswordReset() { + const emailField = this._emailField; + + const email = emailField.value; + + if (!emailField.reportValidity()) { + emailField.focus(); + return; + } + + this._requestInProgress = true; + + try { + await cloudForgotPassword(this.hass, email); + // @ts-ignore + fireEvent(this, "email-changed", { value: email }); + this._requestInProgress = false; + // @ts-ignore + fireEvent(this, "cloud-done", { + flashMessage: this.hass.localize( + "ui.panel.config.cloud.forgot_password.check_your_email" + ), + }); + } catch (err: any) { + this._requestInProgress = false; + this._error = + err && err.body && err.body.message + ? err.body.message + : "Unknown error"; + } + } + + static get styles() { + return [ + haStyle, + css` + .content { + padding-bottom: 24px; + } + ha-card { + max-width: 600px; + margin: 0 auto; + margin-top: 24px; + } + h1 { + margin: 0; + } + mwc-textfield { + width: 100%; + } + .card-actions { + display: flex; + justify-content: space-between; + align-items: center; + } + .card-actions a { + color: var(--primary-text-color); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "cloud-forgot-password": CloudForgotPassword; + } +} diff --git a/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts b/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts index 17414d4abc..51ffa4360a 100644 --- a/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts +++ b/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts @@ -122,25 +122,22 @@ class CloudGoogleAssistant extends LitElement { ? exposedCards : notExposedCards; - const iconButton = html` - - `; + .label=${this.hass!.localize("ui.panel.config.cloud.google.expose")} + .path=${config.should_expose !== null + ? isExposed + ? mdiCheckboxMarked + : mdiCloseBox + : isDomainExposed + ? mdiCheckboxMultipleMarked + : mdiCloseBoxMultiple} + >`; target.push(html` diff --git a/src/panels/config/cloud/login/cloud-login.js b/src/panels/config/cloud/login/cloud-login.js deleted file mode 100644 index b40f08c95c..0000000000 --- a/src/panels/config/cloud/login/cloud-login.js +++ /dev/null @@ -1,332 +0,0 @@ -import "@material/mwc-button"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-item/paper-item-body"; -import "@polymer/paper-ripple/paper-ripple"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { computeRTL } from "../../../../common/util/compute_rtl"; -import "../../../../components/buttons/ha-progress-button"; -import "../../../../components/ha-card"; -import "../../../../components/ha-icon-button"; -import "../../../../components/ha-icon-next"; -import "../../../../layouts/hass-subpage"; -import { EventsMixin } from "../../../../mixins/events-mixin"; -import LocalizeMixin from "../../../../mixins/localize-mixin"; -import NavigateMixin from "../../../../mixins/navigate-mixin"; -import "../../../../styles/polymer-ha-style"; -import "../../ha-config-section"; - -/* - * @appliesMixin NavigateMixin - * @appliesMixin EventsMixin - * @appliesMixin LocalizeMixin - */ -class CloudLogin extends LocalizeMixin( - NavigateMixin(EventsMixin(PolymerElement)) -) { - static get template() { - return html` - - -
    - - Home Assistant Cloud -
    -

    [[localize('ui.panel.config.cloud.login.introduction')]]

    -

    - [[localize('ui.panel.config.cloud.login.introduction2')]] - - Nabu Casa, Inc[[localize('ui.panel.config.cloud.login.introduction2a')]] -

    -

    [[localize('ui.panel.config.cloud.login.introduction3')]]

    -

    - - [[localize('ui.panel.config.cloud.login.learn_more_link')]] - -

    -
    - - -
    - [[flashMessage]] - [[localize('ui.panel.config.cloud.login.dismiss')]] - -
    -
    - - - -
    - [[localize('ui.panel.config.cloud.login.sign_in')]] -
    -
    - - - - - [[localize('ui.panel.config.cloud.login.start_trial')]] -
    - [[localize('ui.panel.config.cloud.login.trial_info')]] -
    -
    - -
    -
    -
    -
    -
    - `; - } - - static get properties() { - return { - hass: Object, - isWide: Boolean, - narrow: Boolean, - email: { - type: String, - notify: true, - }, - _password: { - type: String, - value: "", - }, - _requestInProgress: { - type: Boolean, - value: false, - }, - flashMessage: { - type: String, - notify: true, - }, - rtl: { - type: Boolean, - reflectToAttribute: true, - computed: "_computeRTL(hass)", - }, - _error: String, - }; - } - - static get observers() { - return ["_inputChanged(email, _password)"]; - } - - connectedCallback() { - super.connectedCallback(); - if (this.flashMessage) { - // Wait for DOM to be drawn - requestAnimationFrame(() => - requestAnimationFrame(() => this.$.flashRipple.simulatedRipple()) - ); - } - } - - _inputChanged() { - this.$.email.invalid = false; - this.$.password.invalid = false; - this._error = false; - } - - _keyDown(ev) { - // validate on enter - if (ev.keyCode === 13) { - this._handleLogin(); - ev.preventDefault(); - } - } - - _handleLogin() { - let invalid = false; - - if (!this.email || !this.email.includes("@")) { - this.$.email.invalid = true; - this.$.email.focus(); - invalid = true; - } - - if (this._password.length < 8) { - this.$.password.invalid = true; - - if (!invalid) { - invalid = true; - this.$.password.focus(); - } - } - - if (invalid) return; - - this._requestInProgress = true; - - this.hass - .callApi("post", "cloud/login", { - email: this.email, - password: this._password, - }) - .then( - () => { - this.fire("ha-refresh-cloud-status"); - this.setProperties({ - email: "", - _password: "", - }); - }, - (err) => { - // Do this before setProperties because changing it clears errors. - this._password = ""; - - const errCode = err && err.body && err.body.code; - if (errCode === "PasswordChangeRequired") { - alert( - "[[localize('ui.panel.config.cloud.login.alert_password_change_required')]]" - ); - this.navigate("/config/cloud/forgot-password"); - return; - } - - const props = { - _requestInProgress: false, - _error: - err && err.body && err.body.message - ? err.body.message - : "Unknown error", - }; - - if (errCode === "UserNotConfirmed") { - props._error = - "[[localize('ui.panel.config.cloud.login.alert_email_confirm_necessary')]]"; - } - - this.setProperties(props); - this.$.email.focus(); - } - ); - } - - _handleRegister() { - this.flashMessage = ""; - this.navigate("/config/cloud/register"); - } - - _handleForgotPassword() { - this.flashMessage = ""; - this.navigate("/config/cloud/forgot-password"); - } - - _dismissFlash() { - // give some time to let the ripple finish. - setTimeout(() => { - this.flashMessage = ""; - }, 200); - } - - _computeRTL(hass) { - return computeRTL(hass); - } -} - -customElements.define("cloud-login", CloudLogin); diff --git a/src/panels/config/cloud/login/cloud-login.ts b/src/panels/config/cloud/login/cloud-login.ts new file mode 100644 index 0000000000..c058985a60 --- /dev/null +++ b/src/panels/config/cloud/login/cloud-login.ts @@ -0,0 +1,310 @@ +import "@material/mwc-button"; +import "@material/mwc-textfield/mwc-textfield"; +import type { TextField } from "@material/mwc-textfield/mwc-textfield"; +import "@polymer/paper-input/paper-input"; +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-item/paper-item-body"; +import { css, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { navigate } from "../../../../common/navigate"; +import "../../../../components/buttons/ha-progress-button"; +import "../../../../components/ha-alert"; +import "../../../../components/ha-card"; +import "../../../../components/ha-icon-next"; +import { cloudLogin } from "../../../../data/cloud"; +import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box"; +import "../../../../layouts/hass-subpage"; +import { haStyle } from "../../../../resources/styles"; +import "../../../../styles/polymer-ha-style"; +import { HomeAssistant } from "../../../../types"; +import "../../ha-config-section"; + +@customElement("cloud-login") +export class CloudLogin extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public isWide = false; + + @property({ type: Boolean }) public narrow = false; + + @property() public email?: string; + + @property() public flashMessage?: string; + + @state() private _password?: string; + + @state() private _requestInProgress = false; + + @state() private _error?: string; + + @query("#email", true) private _emailField!: TextField; + + @query("#password", true) private _passwordField!: TextField; + + protected render(): TemplateResult { + return html` + +
    + + Home Assistant Cloud +
    +

    + ${this.hass.localize( + "ui.panel.config.cloud.login.introduction" + )} +

    +

    + ${this.hass.localize( + "ui.panel.config.cloud.login.introduction2" + )} + + Nabu Casa, Inc${this.hass.localize( + "ui.panel.config.cloud.login.introduction2a" + )} +

    +

    + ${this.hass.localize( + "ui.panel.config.cloud.login.introduction3" + )} +

    +

    + + ${this.hass.localize( + "ui.panel.config.cloud.login.learn_more_link" + )} + +

    +
    + + ${this.flashMessage + ? html` + ${this.flashMessage} + ` + : ""} + + + +
    + ${this.hass.localize( + "ui.panel.config.cloud.login.sign_in" + )} +
    +
    + + + + + ${this.hass.localize( + "ui.panel.config.cloud.login.start_trial" + )} +
    + ${this.hass.localize( + "ui.panel.config.cloud.login.trial_info" + )} +
    +
    + +
    +
    +
    +
    +
    + `; + } + + private _keyDown(ev: KeyboardEvent) { + if (ev.key === "Enter") { + this._handleLogin(); + } + } + + private async _handleLogin() { + const emailField = this._emailField; + const passwordField = this._passwordField; + + const email = emailField.value; + const password = passwordField.value; + + if (!emailField.reportValidity()) { + passwordField.reportValidity(); + emailField.focus(); + return; + } + + if (!passwordField.reportValidity()) { + passwordField.focus(); + return; + } + + this._requestInProgress = true; + + try { + await cloudLogin(this.hass, email, password); + fireEvent(this, "ha-refresh-cloud-status"); + this.email = ""; + this._password = ""; + } catch (err: any) { + const errCode = err && err.body && err.body.code; + if (errCode === "PasswordChangeRequired") { + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.cloud.login.alert_password_change_required" + ), + }); + navigate("/config/cloud/forgot-password"); + return; + } + + this._password = ""; + this._requestInProgress = false; + + if (errCode === "UserNotConfirmed") { + this._error = this.hass.localize( + "ui.panel.config.cloud.login.alert_email_confirm_necessary" + ); + } else { + this._error = + err && err.body && err.body.message + ? err.body.message + : "Unknown error"; + } + + emailField.focus(); + } + } + + private _handleRegister() { + this._dismissFlash(); + // @ts-ignore + fireEvent(this, "email-changed", { value: this._emailField.value }); + navigate("/config/cloud/register"); + } + + private _handleForgotPassword() { + this._dismissFlash(); + // @ts-ignore + fireEvent(this, "email-changed", { value: this._emailField.value }); + navigate("/config/cloud/forgot-password"); + } + + private _dismissFlash() { + // @ts-ignore + fireEvent(this, "flash-message-changed", { value: "" }); + } + + static get styles() { + return [ + haStyle, + css` + .content { + padding-bottom: 24px; + } + [slot="introduction"] { + margin: -1em 0; + } + [slot="introduction"] a { + color: var(--primary-color); + } + paper-item { + cursor: pointer; + } + ha-card { + overflow: hidden; + } + ha-card .card-header { + margin-bottom: -8px; + } + h1 { + margin: 0; + } + .card-actions { + display: flex; + justify-content: space-between; + align-items: center; + } + .login-form { + display: flex; + flex-direction: column; + } + .pwd-forgot-link { + color: var(--secondary-text-color) !important; + text-align: right !important; + align-self: flex-end; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "cloud-login": CloudLogin; + } +} diff --git a/src/panels/config/cloud/register/cloud-register.js b/src/panels/config/cloud/register/cloud-register.js deleted file mode 100644 index b07a19d2f5..0000000000 --- a/src/panels/config/cloud/register/cloud-register.js +++ /dev/null @@ -1,229 +0,0 @@ -import "@polymer/paper-input/paper-input"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "../../../../components/buttons/ha-progress-button"; -import "../../../../components/ha-card"; -import "../../../../layouts/hass-subpage"; -import { EventsMixin } from "../../../../mixins/events-mixin"; -import LocalizeMixin from "../../../../mixins/localize-mixin"; -import "../../../../styles/polymer-ha-style"; -import { documentationUrl } from "../../../../util/documentation-url"; -import "../../ha-config-section"; - -/* - * @appliesMixin EventsMixin - * @appliesMixin LocalizeMixin - */ -class CloudRegister extends LocalizeMixin(EventsMixin(PolymerElement)) { - static get template() { - return html` - - -
    - - [[localize('ui.panel.config.cloud.register.headline')]] -
    -

    - [[localize('ui.panel.config.cloud.register.information')]] -

    -

    - [[localize('ui.panel.config.cloud.register.information2')]] -

    -
      -
    • [[localize('ui.panel.config.cloud.register.feature_remote_control')]]
    • -
    • [[localize('ui.panel.config.cloud.register.feature_google_home')]]
    • -
    • [[localize('ui.panel.config.cloud.register.feature_amazon_alexa')]]
    • -
    • [[localize('ui.panel.config.cloud.register.feature_webhook_apps')]]
    • -
    -

    - [[localize('ui.panel.config.cloud.register.information3')]] Nabu Casa, Inc[[localize('ui.panel.config.cloud.register.information3a')]] -

    - -

    - [[localize('ui.panel.config.cloud.register.information4')]] -

    -

    -
    - - -
    -
    -
    [[_error]]
    -
    - - -
    -
    - [[localize('ui.panel.config.cloud.register.start_trial')]] - -
    -
    -
    -
    -
    -`; - } - - static get properties() { - return { - hass: Object, - isWide: Boolean, - narrow: Boolean, - email: { - type: String, - notify: true, - }, - - _requestInProgress: { - type: Boolean, - value: false, - }, - _password: { - type: String, - value: "", - }, - _error: { - type: String, - value: "", - }, - }; - } - - static get observers() { - return ["_inputChanged(email, _password)"]; - } - - _inputChanged() { - this._error = ""; - this.$.email.invalid = false; - this.$.password.invalid = false; - } - - _keyDown(ev) { - // validate on enter - if (ev.keyCode === 13) { - this._handleRegister(); - ev.preventDefault(); - } - } - - _computeDocumentationUrlTos(hass) { - return documentationUrl(hass, "/tos/"); - } - - _computeDocumentationUrlPrivacy(hass) { - return documentationUrl(hass, "/privacy/"); - } - - _handleRegister() { - let invalid = false; - - if (!this.email || !this.email.includes("@")) { - this.$.email.invalid = true; - this.$.email.focus(); - invalid = true; - } - - if (this._password.length < 8) { - this.$.password.invalid = true; - - if (!invalid) { - invalid = true; - this.$.password.focus(); - } - } - - if (invalid) return; - - this._requestInProgress = true; - - this.hass - .callApi("post", "cloud/register", { - email: this.email, - password: this._password, - }) - .then( - () => this._verificationEmailSent(), - (err) => { - // Do this before setProperties because changing it clears errors. - this._password = ""; - - this.setProperties({ - _requestInProgress: false, - _error: - err && err.body && err.body.message - ? err.body.message - : "Unknown error", - }); - } - ); - } - - _handleResendVerifyEmail() { - if (!this.email) { - this.$.email.invalid = true; - return; - } - - this.hass - .callApi("post", "cloud/resend_confirm", { - email: this.email, - }) - .then( - () => this._verificationEmailSent(), - (err) => - this.setProperties({ - _error: - err && err.body && err.body.message - ? err.body.message - : "Unknown error", - }) - ); - } - - _verificationEmailSent() { - this.setProperties({ - _requestInProgress: false, - _password: "", - }); - this.fire("cloud-done", { - flashMessage: this.hass.localize( - "ui.panel.config.cloud.register.account_created" - ), - }); - } -} - -customElements.define("cloud-register", CloudRegister); diff --git a/src/panels/config/cloud/register/cloud-register.ts b/src/panels/config/cloud/register/cloud-register.ts new file mode 100644 index 0000000000..10e539b05b --- /dev/null +++ b/src/panels/config/cloud/register/cloud-register.ts @@ -0,0 +1,293 @@ +import "@material/mwc-textfield/mwc-textfield"; +import type { TextField } from "@material/mwc-textfield/mwc-textfield"; +import { css, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/buttons/ha-progress-button"; +import "../../../../components/ha-alert"; +import "../../../../components/ha-card"; +import { cloudRegister, cloudResendVerification } from "../../../../data/cloud"; +import "../../../../layouts/hass-subpage"; +import { haStyle } from "../../../../resources/styles"; +import { HomeAssistant } from "../../../../types"; +import "../../ha-config-section"; + +@customElement("cloud-register") +export class CloudRegister extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public isWide = false; + + @property({ type: Boolean }) public narrow = false; + + @property() public email?: string; + + @state() private _requestInProgress = false; + + @state() private _password = ""; + + @state() private _error?: string; + + @query("#email", true) private _emailField!: TextField; + + @query("#password", true) private _passwordField!: TextField; + + protected render(): TemplateResult { + return html` + +
    + + ${this.hass.localize( + "ui.panel.config.cloud.register.headline" + )} +
    +

    + ${this.hass.localize( + "ui.panel.config.cloud.register.information" + )} +

    +

    + ${this.hass.localize( + "ui.panel.config.cloud.register.information2" + )} +

    +
      +
    • + ${this.hass.localize( + "ui.panel.config.cloud.register.feature_remote_control" + )} +
    • +
    • + ${this.hass.localize( + "ui.panel.config.cloud.register.feature_google_home" + )} +
    • +
    • + ${this.hass.localize( + "ui.panel.config.cloud.register.feature_amazon_alexa" + )} +
    • +
    • + ${this.hass.localize( + "ui.panel.config.cloud.register.feature_webhook_apps" + )} +
    • +
    +

    + ${this.hass.localize( + "ui.panel.config.cloud.register.information3" + )} + Nabu Casa, Inc + ${this.hass.localize( + "ui.panel.config.cloud.register.information3a" + )} +

    +

    + ${this.hass.localize( + "ui.panel.config.cloud.register.information4" + )} +

    + +
    +
    + ${this._error + ? html`${this._error}` + : ""} + + +
    +
    + ${this.hass.localize( + "ui.panel.config.cloud.register.start_trial" + )} + +
    +
    +
    +
    +
    + `; + } + + private _keyDown(ev: KeyboardEvent) { + if (ev.key === "Enter") { + this._handleRegister(); + } + } + + private async _handleRegister() { + const emailField = this._emailField; + const passwordField = this._passwordField; + + const email = emailField.value; + const password = passwordField.value; + + if (!emailField.reportValidity()) { + passwordField.reportValidity(); + emailField.focus(); + return; + } + + if (!passwordField.reportValidity()) { + passwordField.focus(); + return; + } + + this._requestInProgress = true; + + try { + await cloudRegister(this.hass, email, password); + this._verificationEmailSent(email); + } catch (err: any) { + this._password = ""; + this._requestInProgress = false; + this._error = + err && err.body && err.body.message + ? err.body.message + : "Unknown error"; + } + } + + private async _handleResendVerifyEmail() { + const emailField = this._emailField; + + const email = emailField.value; + + if (!emailField.reportValidity()) { + emailField.focus(); + return; + } + + try { + await cloudResendVerification(this.hass, email); + this._verificationEmailSent(email); + } catch (err: any) { + this._error = + err && err.body && err.body.message + ? err.body.message + : "Unknown error"; + } + } + + private _verificationEmailSent(email: string) { + this._requestInProgress = false; + this._password = ""; + // @ts-ignore + fireEvent(this, "email-changed", { value: email }); + // @ts-ignore + fireEvent(this, "cloud-done", { + flashMessage: this.hass.localize( + "ui.panel.config.cloud.register.account_created" + ), + }); + } + + static get styles() { + return [ + haStyle, + css` + [slot="introduction"] { + margin: -1em 0; + } + [slot="introduction"] a { + color: var(--primary-color); + } + a { + color: var(--primary-color); + } + paper-item { + cursor: pointer; + } + h1 { + margin: 0; + } + .register-form { + display: flex; + flex-direction: column; + } + .card-actions { + display: flex; + justify-content: space-between; + align-items: center; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "cloud-register": CloudRegister; + } +} diff --git a/src/panels/config/core/ha-config-core-form.ts b/src/panels/config/core/ha-config-core-form.ts index 4e48098672..495b963121 100644 --- a/src/panels/config/core/ha-config-core-form.ts +++ b/src/panels/config/core/ha-config-core-form.ts @@ -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"; @@ -16,6 +14,9 @@ import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core"; import { SYMBOL_TO_ISO } from "../../../data/currency"; import type { PolymerChangedEvent } from "../../../polymer-types"; import type { HomeAssistant } from "../../../types"; +import "../../../components/ha-formfield"; +import "../../../components/ha-radio"; +import type { HaRadio } from "../../../components/ha-radio"; @customElement("ha-config-core-form") class ConfigCoreForm extends LitElement { @@ -120,32 +121,44 @@ class ConfigCoreForm extends LitElement { "ui.panel.config.core.section.core.core_config.unit_system" )}
    - - - ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.unit_system_metric" - )} -
    - ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.metric_example" +
    + - - - ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.unit_system_imperial" - )} -
    - ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.imperial_example" +
    + ${this.hass.localize( + "ui.panel.config.core.section.core.core_config.metric_example" + )} +
    `} + > + + + - - +
    + ${this.hass.localize( + "ui.panel.config.core.section.core.core_config.imperial_example" + )} +
    `} + > + +
    +
    @@ -258,10 +271,8 @@ class ConfigCoreForm extends LitElement { this._location = ev.detail.location; } - private _unitSystemChanged( - ev: PolymerChangedEvent - ) { - this._unitSystem = ev.detail.value; + private _unitSystemChanged(ev: CustomEvent) { + this._unitSystem = (ev.target as HaRadio).value as "metric" | "imperial"; } private async _save() { @@ -307,6 +318,12 @@ class ConfigCoreForm extends LitElement { margin: 0 8px; } + .radio-group { + display: flex; + flex-direction: column; + flex: 1; + } + .card-actions { text-align: right; } diff --git a/src/panels/config/core/ha-config-core.js b/src/panels/config/core/ha-config-core.js index 5b41675898..4e01b9d6af 100644 --- a/src/panels/config/core/ha-config-core.js +++ b/src/panels/config/core/ha-config-core.js @@ -3,7 +3,6 @@ import "@polymer/app-layout/app-toolbar/app-toolbar"; import { html } from "@polymer/polymer/lib/utils/html-tag"; /* eslint-plugin-disable lit */ import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "../../../components/ha-icon-button"; import "../../../layouts/hass-tabs-subpage"; import LocalizeMixin from "../../../mixins/localize-mixin"; import "../../../styles/polymer-ha-style"; diff --git a/src/panels/config/core/ha-config-name-form.ts b/src/panels/config/core/ha-config-name-form.ts index 3edc3be6d8..7dc7752720 100644 --- a/src/panels/config/core/ha-config-name-form.ts +++ b/src/panels/config/core/ha-config-name-form.ts @@ -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, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import "../../../components/ha-card"; diff --git a/src/panels/config/customize/ha-config-customize.ts b/src/panels/config/customize/ha-config-customize.ts index 6d6c378b59..03fd6f17e9 100644 --- a/src/panels/config/customize/ha-config-customize.ts +++ b/src/panels/config/customize/ha-config-customize.ts @@ -25,7 +25,7 @@ class HaConfigCustomize extends LitElement { protected render(): TemplateResult { return html`
    - + + + `; } diff --git a/src/panels/config/customize/ha-form-customize.js b/src/panels/config/customize/ha-form-customize.js index 5e22d6b5d9..cd3278abeb 100644 --- a/src/panels/config/customize/ha-form-customize.js +++ b/src/panels/config/customize/ha-form-customize.js @@ -180,10 +180,16 @@ export class HaFormCustomize extends LocalizeMixin(PolymerElement) { this.newAttributes ); attrs.forEach((attr) => { - if (attr.closed || attr.secondary || !attr.attribute || !attr.value) + if ( + attr.closed || + attr.secondary || + !attr.attribute || + attr.value === null || + attr.value === undefined + ) return; const value = attr.type === "json" ? JSON.parse(attr.value) : attr.value; - if (!value) return; + if (value === null || value === undefined) return; data[attr.attribute] = value; }); diff --git a/src/panels/config/customize/types/ha-customize-boolean.js b/src/panels/config/customize/types/ha-customize-boolean.js index bf6aa5c7db..803e6d8d21 100644 --- a/src/panels/config/customize/types/ha-customize-boolean.js +++ b/src/panels/config/customize/types/ha-customize-boolean.js @@ -1,14 +1,20 @@ -import "@polymer/paper-checkbox/paper-checkbox"; import { html } from "@polymer/polymer/lib/utils/html-tag"; /* eslint-plugin-disable lit */ import { PolymerElement } from "@polymer/polymer/polymer-element"; +import "../../../../components/ha-checkbox"; +import "../../../../components/ha-formfield"; class HaCustomizeBoolean extends PolymerElement { static get template() { return html` - - [[item.description]] - + + + `; } @@ -20,5 +26,9 @@ class HaCustomizeBoolean extends PolymerElement { }, }; } + + checkedChanged(ev) { + this.item.value = ev.target.checked; + } } customElements.define("ha-customize-boolean", HaCustomizeBoolean); diff --git a/src/panels/config/dashboard/ha-config-dashboard.ts b/src/panels/config/dashboard/ha-config-dashboard.ts index f81527cce0..b7428354e4 100644 --- a/src/panels/config/dashboard/ha-config-dashboard.ts +++ b/src/panels/config/dashboard/ha-config-dashboard.ts @@ -7,7 +7,6 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import "../../../components/ha-card"; import "../../../components/ha-icon-next"; import "../../../components/ha-menu-button"; -import "../../../components/ha-svg-icon"; import { CloudStatus } from "../../../data/cloud"; import "../../../layouts/ha-app-layout"; import { haStyle } from "../../../resources/styles"; diff --git a/src/panels/config/devices/device-detail/ha-device-entities-card.ts b/src/panels/config/devices/device-detail/ha-device-entities-card.ts index 5bb169256b..0c57f498ce 100644 --- a/src/panels/config/devices/device-detail/ha-device-entities-card.ts +++ b/src/panels/config/devices/device-detail/ha-device-entities-card.ts @@ -15,16 +15,23 @@ import { domainIcon } from "../../../../common/entity/domain_icon"; import "../../../../components/entity/state-badge"; import "../../../../components/ha-card"; import "../../../../components/ha-icon"; -import { HomeAssistant } from "../../../../types"; -import { HuiErrorCard } from "../../../lovelace/cards/hui-error-card"; +import type { LovelaceRowConfig } from "../../../lovelace/entity-rows/types"; +import type { HomeAssistant } from "../../../../types"; +import type { HuiErrorCard } from "../../../lovelace/cards/hui-error-card"; import { createRowElement } from "../../../lovelace/create-element/create-row-element"; import { addEntitiesToLovelaceView } from "../../../lovelace/editor/add-entities-to-view"; import { LovelaceRow } from "../../../lovelace/entity-rows/types"; import { showEntityEditorDialog } from "../../entities/show-dialog-entity-editor"; import { EntityRegistryStateEntry } from "../ha-config-device-page"; +import { computeStateName } from "../../../../common/entity/compute_state_name"; +import { stripPrefixFromEntityName } from "../../../../common/entity/strip_prefix_from_entity_name"; @customElement("ha-device-entities-card") export class HaDeviceEntitiesCard extends LitElement { + @property() public header!: string; + + @property() public deviceName!: string; + @property({ attribute: false }) public hass!: HomeAssistant; @property() public entities!: EntityRegistryStateEntry[]; @@ -47,11 +54,7 @@ export class HaDeviceEntitiesCard extends LitElement { const disabledEntities: EntityRegistryStateEntry[] = []; this._entityRows = []; return html` - + ${this.entities.length ? html`
    @@ -102,14 +105,8 @@ export class HaDeviceEntitiesCard extends LitElement {
    ` : html` -
    - -
    - ${this.hass.localize( - "ui.panel.config.devices.entities.none" - )} -
    -
    +
    + ${this.hass.localize("ui.panel.config.devices.entities.none")}
    `} @@ -121,9 +118,21 @@ export class HaDeviceEntitiesCard extends LitElement { } private _renderEntity(entry: EntityRegistryStateEntry): TemplateResult { - const element = createRowElement({ entity: entry.entity_id }); + const config: LovelaceRowConfig = { + entity: entry.entity_id, + }; + + const element = createRowElement(config); if (this.hass) { element.hass = this.hass; + const state = this.hass.states[entry.entity_id]; + const name = stripPrefixFromEntityName( + computeStateName(state), + `${this.deviceName} `.toLowerCase() + ); + if (name) { + config.name = name; + } } // @ts-ignore element.entry = entry; @@ -133,11 +142,15 @@ export class HaDeviceEntitiesCard extends LitElement { private _renderEntry(entry: EntityRegistryStateEntry): TemplateResult { return html` - - + + .path=${domainIcon(computeDomain(entry.entity_id))} + >
    ${entry.stateName || entry.entity_id}
    @@ -168,7 +181,8 @@ export class HaDeviceEntitiesCard extends LitElement { this.hass, this.entities .filter((entity) => !entity.disabled_by) - .map((entity) => entity.entity_id) + .map((entity) => entity.entity_id), + this.deviceName ); } @@ -190,6 +204,9 @@ export class HaDeviceEntitiesCard extends LitElement { .disabled-entry { color: var(--secondary-text-color); } + #entities { + margin-top: -24px; /* match the spacing between card title and content of the device info card above it */ + } #entities > * { margin: 8px 16px 8px 8px; } @@ -198,12 +215,16 @@ export class HaDeviceEntitiesCard extends LitElement { } paper-icon-item { min-height: 40px; - padding: 0 8px; + padding: 0 16px; cursor: pointer; + --paper-item-icon-width: 48px; } .name { font-size: 14px; } + .empty { + text-align: center; + } button.show-more { color: var(--primary-color); text-align: left; diff --git a/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts b/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts index ffac1d04f3..b69484180e 100644 --- a/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts +++ b/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts @@ -18,6 +18,7 @@ import { nodeStatus, ZWaveJSNodeStatus, ZWaveJSNodeIdentifiers, + SecurityClass, } from "../../../../../../data/zwave_js"; import { haStyle } from "../../../../../../resources/styles"; import { HomeAssistant } from "../../../../../../types"; @@ -117,13 +118,33 @@ export class HaDeviceInfoZWaveJS extends LitElement { : this.hass.localize("ui.common.no")}
    - ${this.hass.localize("ui.panel.config.zwave_js.device_info.is_secure")}: - ${this._node.is_secure === true - ? this.hass.localize("ui.common.yes") + ${this.hass.localize( + "ui.panel.config.zwave_js.device_info.highest_security" + )}: + ${this._node.highest_security_class !== null + ? this.hass.localize( + `ui.panel.config.zwave_js.security_classes.${ + SecurityClass[this._node.highest_security_class] + }.title` + ) : this._node.is_secure === false - ? this.hass.localize("ui.common.no") + ? this.hass.localize( + "ui.panel.config.zwave_js.security_classes.none.title" + ) : this.hass.localize("ui.panel.config.zwave_js.device_info.unknown")}
    +
    + ${this.hass.localize( + "ui.panel.config.zwave_js.device_info.zwave_plus" + )}: + ${this._node.zwave_plus_version + ? this.hass.localize( + "ui.panel.config.zwave_js.device_info.zwave_plus_version", + "version", + this._node.zwave_plus_version + ) + : this.hass.localize("ui.common.no")} +
    `; } diff --git a/src/panels/config/devices/device-registry-detail/dialog-device-registry-detail.ts b/src/panels/config/devices/device-registry-detail/dialog-device-registry-detail.ts index 13f930437c..8ad148eef4 100644 --- a/src/panels/config/devices/device-registry-detail/dialog-device-registry-detail.ts +++ b/src/panels/config/devices/device-registry-detail/dialog-device-registry-detail.ts @@ -25,7 +25,7 @@ class DialogDeviceRegistryDetail extends LitElement { @state() private _params?: DeviceRegistryDetailDialogParams; - @state() private _areaId?: string | null; + @property() public _areaId?: string | null; @state() private _disabledBy!: string | null; diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 25206b1ca9..5a2d375d53 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -1,3 +1,4 @@ +import { mdiPencil, mdiPlusCircle, mdiOpenInNew } from "@mdi/js"; import "@polymer/paper-tooltip/paper-tooltip"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -7,9 +8,13 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { stringCompare } from "../../../common/string/compare"; +import { groupBy } from "../../../common/util/group-by"; import { slugify } from "../../../common/string/slugify"; import "../../../components/entity/ha-battery-icon"; +import "../../../components/ha-icon-button"; import "../../../components/ha-icon-next"; +import "../../../components/ha-alert"; +import "../../../components/ha-svg-icon"; import { AreaRegistryEntry } from "../../../data/area_registry"; import { ConfigEntry, @@ -47,6 +52,7 @@ import { loadDeviceRegistryDetailDialog, showDeviceRegistryDetailDialog, } from "./device-registry-detail/show-dialog-device-registry-detail"; +import { computeDomain } from "../../../common/entity/compute_domain"; export interface EntityRegistryStateEntry extends EntityRegistryEntry { stateName?: string | null; @@ -110,6 +116,30 @@ export class HaConfigDevicePage extends LitElement { ) ); + private _entitiesByCategory = memoizeOne( + (entities: EntityRegistryEntry[]) => { + const result = groupBy(entities, (entry) => + entry.entity_category + ? entry.entity_category + : ["sensor", "binary_sensor"].includes(computeDomain(entry.entity_id)) + ? "sensor" + : "control" + ) as Record< + | "control" + | "sensor" + | NonNullable, + EntityRegistryStateEntry[] + >; + for (const key of ["control", "sensor", "diagnostic", "config"]) { + if (!(key in result)) { + result[key] = []; + } + } + + return result; + } + ); + private _computeArea = memoizeOne( (areas, device): AreaRegistryEntry | undefined => { if (!areas || !device || !device.area_id) { @@ -155,8 +185,10 @@ export class HaConfigDevicePage extends LitElement { `; } + const deviceName = computeDeviceName(device, this.hass); const integrations = this._integrations(device, this.entries); const entities = this._entities(this.deviceId, this.entities); + const entitiesByCategory = this._entitiesByCategory(entities); const batteryEntity = this._batteryEntity(entities); const batteryChargingEntity = this._batteryChargingEntity(entities); const batteryState = batteryEntity @@ -169,6 +201,13 @@ export class HaConfigDevicePage extends LitElement { : undefined; const area = this._computeArea(this.areas, device); + const configurationUrlIsHomeAssistant = + device.configuration_url?.startsWith("homeassistant://") || false; + + const configurationUrl = configurationUrlIsHomeAssistant + ? device.configuration_url!.replace("homeassistant://", "/") + : device.configuration_url; + return html` - ${computeDeviceName(device, this.hass)} - + ${deviceName} ` : "" @@ -202,7 +242,7 @@ export class HaConfigDevicePage extends LitElement { : html`
    -

    ${computeDeviceName(device, this.hass)}

    +

    ${deviceName}

    ${area ? html`
    ` @@ -266,17 +309,15 @@ export class HaConfigDevicePage extends LitElement { ${ device.disabled_by ? html` -
    -

    - ${this.hass.localize( - "ui.panel.config.devices.enabled_cause", - "cause", - this.hass.localize( - `ui.panel.config.devices.disabled_by.${device.disabled_by}` - ) - )} -

    -
    + + ${this.hass.localize( + "ui.panel.config.devices.enabled_cause", + "cause", + this.hass.localize( + `ui.panel.config.devices.disabled_by.${device.disabled_by}` + ) + )} + ${device.disabled_by === "user" ? html`
    +
    + ${["control", "sensor", "config", "diagnostic"].map((category) => + // Make sure we render controls if no other cards will be rendered + entitiesByCategory[category].length > 0 || + (entities.length === 0 && category === "control") ? html` ` - : html`` - } + : "" + )}
    -
    +
    ${ isComponentLoaded(this.hass, "automation") ? html` @@ -315,14 +390,14 @@ export class HaConfigDevicePage extends LitElement {

    ${this._related?.automation?.length @@ -362,7 +437,7 @@ export class HaConfigDevicePage extends LitElement { : ""; }) : html` - +
    ${this.hass.localize( "ui.panel.config.devices.add_prompt", "name", @@ -370,92 +445,82 @@ export class HaConfigDevicePage extends LitElement { "ui.panel.config.devices.automation.automations" ) )} - +
    `} ` : "" } -
    -
    ${ isComponentLoaded(this.hass, "scene") && entities.length ? html` -

    - ${this.hass.localize( - "ui.panel.config.devices.scene.scenes" - )} +

    + ${this.hass.localize( + "ui.panel.config.devices.scene.scenes" + )} - -

    + + - ${ - this._related?.scene?.length - ? this._related.scene.map((scene) => { - const entityState = this.hass.states[scene]; - return entityState - ? html` -
    - - - - ${computeStateName(entityState)} - - - - - ${!entityState.attributes.id - ? html` - - ${this.hass.localize( - "ui.panel.config.devices.cant_edit" - )} - - ` - : ""} -
    - ` - : ""; - }) - : html` - - ${this.hass.localize( - "ui.panel.config.devices.add_prompt", - "name", - this.hass.localize( - "ui.panel.config.devices.scene.scenes" - ) - )} - - ` - } -
    + ${this._related?.scene?.length + ? this._related.scene.map((scene) => { + const entityState = this.hass.states[scene]; + return entityState + ? html` +
    + + + + ${computeStateName(entityState)} + + + + + ${!entityState.attributes.id + ? html` + + ${this.hass.localize( + "ui.panel.config.devices.cant_edit" + )} + + ` + : ""} +
    + ` + : ""; + }) + : html` +
    + ${this.hass.localize( + "ui.panel.config.devices.add_prompt", + "name", + this.hass.localize( + "ui.panel.config.devices.scene.scenes" + ) + )} +
    + `} ` : "" @@ -471,14 +536,14 @@ export class HaConfigDevicePage extends LitElement { ${this._related?.script?.length @@ -500,7 +565,7 @@ export class HaConfigDevicePage extends LitElement { : ""; }) : html` - +
    ${this.hass.localize( "ui.panel.config.devices.add_prompt", "name", @@ -508,14 +573,14 @@ export class HaConfigDevicePage extends LitElement { "ui.panel.config.devices.script.scripts" ) )} - +
    `} ` : "" }
    -
    +
    `; } @@ -895,22 +960,18 @@ export class HaConfigDevicePage extends LitElement { font-size: var(--paper-font-body1_-_font-size); } - paper-item.no-link { - cursor: default; - } - a { text-decoration: none; color: var(--primary-color); } - ha-card { - padding-bottom: 8px; - } - ha-card a { color: var(--primary-text-color); } + + ha-svg-icon[slot="trailingIcon"] { + display: block; + } `, ]; } diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index 97502831d2..c8f12b685f 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -17,6 +17,7 @@ import { } from "../../../components/data-table/ha-data-table"; import "../../../components/entity/ha-battery-icon"; import "../../../components/ha-button-menu"; +import "../../../components/ha-icon-button"; import { AreaRegistryEntry } from "../../../data/area_registry"; import { ConfigEntry } from "../../../data/config_entries"; import { @@ -407,17 +408,13 @@ export class HaConfigDeviceDashboard extends LitElement { ` : html``} - - - + .path=${mdiFilterVariant} + >
    Battery systems +

    + ${this.hass.localize( + "ui.panel.config.energy.battery.battery_systems" + )} +

    ${batterySources.map((source) => { const fromEntityState = this.hass.states[source.stat_energy_from]; const toEntityState = this.hass.states[source.stat_energy_to]; @@ -101,19 +106,23 @@ export class EnergyBatterySettings extends LitElement { : source.stat_energy_to}
    - - - - - - + +
    `; })}
    Add battery system${this.hass.localize( + "ui.panel.config.energy.battery.add_battery_system" + )}
    @@ -154,7 +163,7 @@ export class EnergyBatterySettings extends LitElement { if ( !(await showConfirmationDialog(this, { - title: "Are you sure you want to delete this source?", + title: this.hass.localize("ui.panel.config.energy.delete_source"), })) ) { return; diff --git a/src/panels/config/energy/components/ha-energy-device-settings.ts b/src/panels/config/energy/components/ha-energy-device-settings.ts index ad277fd0e4..fe75d061f6 100644 --- a/src/panels/config/energy/components/ha-energy-device-settings.ts +++ b/src/panels/config/energy/components/ha-energy-device-settings.ts @@ -4,8 +4,9 @@ import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; import { computeStateName } from "../../../../common/entity/compute_state_name"; -import { stateIcon } from "../../../../common/entity/state_icon"; import "../../../../components/ha-card"; +import "../../../../components/ha-icon-button"; +import "../../../../components/ha-state-icon"; import { DeviceConsumptionEnergyPreference, EnergyPreferences, @@ -13,8 +14,8 @@ import { saveEnergyPreferences, } from "../../../../data/energy"; import { - showConfirmationDialog, showAlertDialog, + showConfirmationDialog, } from "../../../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../../../resources/styles"; import { HomeAssistant } from "../../../../types"; @@ -69,26 +70,36 @@ export class EnergyDeviceSettings extends LitElement { > ` )} -

    Devices

    +

    + ${this.hass.localize( + "ui.panel.config.energy.device_consumption.devices" + )} +

    ${this.preferences.device_consumption.map((device) => { const entityState = this.hass.states[device.stat_consumption]; return html`
    - + ${entityState ? computeStateName(entityState) : device.stat_consumption} - - - +
    `; })}
    - Add device + ${this.hass.localize( + "ui.panel.config.energy.device_consumption.add_device" + )}
    @@ -113,7 +124,7 @@ export class EnergyDeviceSettings extends LitElement { if ( !(await showConfirmationDialog(this, { - title: "Are you sure you want to delete this device?", + title: this.hass.localize("ui.panel.config.energy.delete_source"), })) ) { return; diff --git a/src/panels/config/energy/components/ha-energy-gas-settings.ts b/src/panels/config/energy/components/ha-energy-gas-settings.ts index 9a92798b2b..3f92d1a023 100644 --- a/src/panels/config/energy/components/ha-energy-gas-settings.ts +++ b/src/panels/config/energy/components/ha-energy-gas-settings.ts @@ -5,17 +5,18 @@ import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; import { computeStateName } from "../../../../common/entity/compute_state_name"; import "../../../../components/ha-card"; +import "../../../../components/ha-icon-button"; import { EnergyPreferences, - saveEnergyPreferences, - GasSourceTypeEnergyPreference, EnergyPreferencesValidation, EnergyValidationIssue, + GasSourceTypeEnergyPreference, getEnergyGasUnitCategory, + saveEnergyPreferences, } from "../../../../data/energy"; import { - showConfirmationDialog, showAlertDialog, + showConfirmationDialog, } from "../../../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../../../resources/styles"; import { HomeAssistant } from "../../../../types"; @@ -75,7 +76,9 @@ export class EnergyGasSettings extends LitElement { > ` )} -

    Gas consumption

    +

    + ${this.hass.localize("ui.panel.config.energy.gas.gas_consumption")} +

    ${gasSources.map((source) => { const entityState = this.hass.states[source.stat_energy_from]; return html` @@ -90,18 +93,24 @@ export class EnergyGasSettings extends LitElement { ? computeStateName(entityState) : source.stat_energy_from} - - - - - - + +
    `; })}
    - Add gas source + ${this.hass.localize( + "ui.panel.config.energy.gas.add_gas_source" + )}
    @@ -143,7 +152,7 @@ export class EnergyGasSettings extends LitElement { if ( !(await showConfirmationDialog(this, { - title: "Are you sure you want to delete this source?", + title: this.hass.localize("ui.panel.config.energy.delete_source"), })) ) { return; diff --git a/src/panels/config/energy/components/ha-energy-grid-settings.ts b/src/panels/config/energy/components/ha-energy-grid-settings.ts index 6f31cd8394..d0b3df4e76 100644 --- a/src/panels/config/energy/components/ha-energy-grid-settings.ts +++ b/src/panels/config/energy/components/ha-energy-grid-settings.ts @@ -11,6 +11,7 @@ import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; import { computeStateName } from "../../../../common/entity/compute_state_name"; import "../../../../components/ha-card"; +import "../../../../components/ha-icon-button"; import { ConfigEntry, deleteConfigEntry, @@ -109,7 +110,11 @@ export class EnergyGridSettings extends LitElement { ` : ""} -

    Grid consumption

    +

    + ${this.hass.localize( + "ui.panel.config.energy.grid.grid_consumption" + )} +

    ${gridSource.flow_from.map((flow) => { const entityState = this.hass.states[flow.stat_energy_from]; return html` @@ -126,23 +131,29 @@ export class EnergyGridSettings extends LitElement { ? computeStateName(entityState) : flow.stat_energy_from} - - - - - - + + `; })}
    Add consumption${this.hass.localize( + "ui.panel.config.energy.grid.add_consumption" + )}
    -

    Return to grid

    +

    + ${this.hass.localize("ui.panel.config.energy.grid.return_to_grid")} +

    ${gridSource.flow_to.map((flow) => { const entityState = this.hass.states[flow.stat_energy_to]; return html` @@ -159,21 +170,31 @@ export class EnergyGridSettings extends LitElement { ? computeStateName(entityState) : flow.stat_energy_to} - - - - - - + + `; })}
    - Add return + ${this.hass.localize( + "ui.panel.config.energy.grid.add_return" + )}
    -

    Grid carbon footprint

    +

    + ${this.hass.localize( + "ui.panel.config.energy.grid.grid_carbon_footprint" + )} +

    ${this._configEntries?.map( (entry) => html`
    ${entry.title} - - - + - - - +
    ` )} ${this._configEntries?.length === 0 @@ -207,7 +227,9 @@ export class EnergyGridSettings extends LitElement { })} /> - Add CO2 signal integration + ${this.hass.localize( + "ui.panel.config.energy.grid.add_co2_signal" + )} ` @@ -236,8 +258,7 @@ export class EnergyGridSettings extends LitElement { const entryId = ev.currentTarget.closest(".row").entry.entry_id; if ( !(await showConfirmationDialog(this, { - title: - "Are you sure you want to delete this integration? It will remove the entities it provides", + title: this.hass.localize("ui.panel.config.energy.delete_integration"), })) ) { return; @@ -374,7 +395,7 @@ export class EnergyGridSettings extends LitElement { if ( !(await showConfirmationDialog(this, { - title: "Are you sure you want to delete this source?", + title: this.hass.localize("ui.panel.config.energy.delete_source"), })) ) { return; @@ -404,7 +425,7 @@ export class EnergyGridSettings extends LitElement { if ( !(await showConfirmationDialog(this, { - title: "Are you sure you want to delete this source?", + title: this.hass.localize("ui.panel.config.energy.delete_source"), })) ) { return; diff --git a/src/panels/config/energy/components/ha-energy-solar-settings.ts b/src/panels/config/energy/components/ha-energy-solar-settings.ts index eaec9ded9a..eca1055649 100644 --- a/src/panels/config/energy/components/ha-energy-solar-settings.ts +++ b/src/panels/config/energy/components/ha-energy-solar-settings.ts @@ -5,6 +5,7 @@ import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; import { computeStateName } from "../../../../common/entity/compute_state_name"; import "../../../../components/ha-card"; +import "../../../../components/ha-icon-button"; import { EnergyInfo, EnergyPreferences, @@ -81,7 +82,11 @@ export class EnergySolarSettings extends LitElement { ` )} -

    Solar production

    +

    + ${this.hass.localize( + "ui.panel.config.energy.solar.solar_production" + )} +

    ${solarSources.map((source) => { const entityState = this.hass.states[source.stat_energy_from]; return html` @@ -98,14 +103,16 @@ export class EnergySolarSettings extends LitElement { > ${this.info ? html` - - - + ` : ""} - - - + `; })} @@ -114,7 +121,9 @@ export class EnergySolarSettings extends LitElement {
    - Add solar production + ${this.hass.localize( + "ui.panel.config.energy.solar.add_solar_production" + )}
    ` @@ -159,7 +168,7 @@ export class EnergySolarSettings extends LitElement { if ( !(await showConfirmationDialog(this, { - title: "Are you sure you want to delete this source?", + title: this.hass.localize("ui.panel.config.energy.delete_source"), })) ) { return; diff --git a/src/panels/config/energy/components/styles.ts b/src/panels/config/energy/components/styles.ts index 91ef2109cd..5aab3c4dbc 100644 --- a/src/panels/config/energy/components/styles.ts +++ b/src/panels/config/energy/components/styles.ts @@ -31,7 +31,7 @@ export const energyCardStyles = css` .row .content { flex-grow: 1; } - mwc-icon-button { + ha-icon-button { color: var(--secondary-text-color); } `; diff --git a/src/panels/config/energy/dialogs/dialog-energy-battery-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-battery-settings.ts index 61f4841168..12ad6a67cc 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-battery-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-battery-settings.ts @@ -58,7 +58,9 @@ export class DialogEnergyBatterySettings .path=${mdiBatteryHigh} style="--mdc-icon-size: 32px;" > - Configure battery system`} + ${this.hass.localize( + "ui.panel.config.energy.battery.dialog.header" + )}`} @closed=${this.closeDialog} > ${this._error ? html`

    ${this._error}

    ` : ""} @@ -68,7 +70,9 @@ export class DialogEnergyBatterySettings .includeUnitOfMeasurement=${energyUnits} .includeDeviceClasses=${energyDeviceClasses} .value=${this._source.stat_energy_to} - .label=${`Energy going in to the battery (kWh)`} + .label=${this.hass.localize( + "ui.panel.config.energy.battery.dialog.energy_into_battery" + )} entities-only @value-changed=${this._statisticToChanged} > @@ -78,7 +82,9 @@ export class DialogEnergyBatterySettings .includeUnitOfMeasurement=${energyUnits} .includeDeviceClasses=${energyDeviceClasses} .value=${this._source.stat_energy_from} - .label=${`Energy coming out of the battery (kWh)`} + .label=${this.hass.localize( + "ui.panel.config.energy.battery.dialog.energy_out_of_battery" + )} entities-only @value-changed=${this._statisticFromChanged} > diff --git a/src/panels/config/energy/dialogs/dialog-energy-device-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-device-settings.ts index 758874ff42..4a7002a8e3 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-device-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-device-settings.ts @@ -55,7 +55,9 @@ export class DialogEnergyDeviceSettings .path=${mdiDevices} style="--mdc-icon-size: 32px;" > - Add a device`} + ${this.hass.localize( + "ui.panel.config.energy.device_consumption.dialog.header" + )}`} @closed=${this.closeDialog} > ${this._error ? html`

    ${this._error}

    ` : ""} @@ -69,7 +71,9 @@ export class DialogEnergyDeviceSettings .hass=${this.hass} .includeUnitOfMeasurement=${energyUnits} .includeDeviceClasses=${energyDeviceClasses} - .label=${`Device consumption energy (kWh)`} + .label=${this.hass.localize( + "ui.panel.config.energy.device_consumption.dialog.device_consumption_energy" + )} entities-only @value-changed=${this._statisticChanged} > diff --git a/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts index 595ec1c942..e4a4bf98ce 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts @@ -82,7 +82,7 @@ export class DialogEnergyGasSettings .path=${mdiFire} style="--mdc-icon-size: 32px;" > - Configure Gas consumption`} + ${this.hass.localize("ui.panel.config.energy.gas.dialog.header")}`} @closed=${this.closeDialog} > ${this._error ? html`

    ${this._error}

    ` : ""} @@ -95,9 +95,13 @@ export class DialogEnergyGasSettings ? ENERGY_GAS_ENERGY_UNITS : ENERGY_GAS_VOLUME_UNITS} .value=${this._source.stat_energy_from} - .label=${`Gas usage (${ + .label=${`${this.hass.localize( + "ui.panel.config.energy.gas.dialog.gas_usage" + )} (${ this._params.unit === undefined - ? "m³ or kWh" + ? this.hass.localize( + "ui.panel.config.energy.gas.dialog.m3_or_kWh" + ) : this._params.unit === "energy" ? "kWh" : "m³" diff --git a/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts index d4cdd03676..bcb5786097 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts @@ -72,7 +72,7 @@ export class DialogEnergySolarSettings .path=${mdiSolarPower} style="--mdc-icon-size: 32px;" > - Configure solar panels`} + ${this.hass.localize("ui.panel.config.energy.solar.dialog.header")}`} @closed=${this.closeDialog} > ${this._error ? html`

    ${this._error}

    ` : ""} @@ -82,18 +82,29 @@ export class DialogEnergySolarSettings .includeUnitOfMeasurement=${energyUnits} .includeDeviceClasses=${energyDeviceClasses} .value=${this._source.stat_energy_from} - .label=${`Solar production energy (kWh)`} + .label=${this.hass.localize( + "ui.panel.config.energy.solar.dialog.solar_production_energy" + )} entities-only @value-changed=${this._statisticChanged} > -

    Solar production forecast

    +

    + ${this.hass.localize( + "ui.panel.config.energy.solar.dialog.solar_production_forecast" + )} +

    - Adding solar production forecast information will allow you to quickly - see your expected production for today. + ${this.hass.localize( + "ui.panel.config.energy.solar.dialog.solar_production_forecast_description" + )}

    - + - + ` )} - Add forecast + ${this.hass.localize( + "ui.panel.config.energy.solar.dialog.add_forecast" + )} ` : ""} diff --git a/src/panels/config/energy/ha-config-energy.ts b/src/panels/config/energy/ha-config-energy.ts index e9c21120c8..b93c93c749 100644 --- a/src/panels/config/energy/ha-config-energy.ts +++ b/src/panels/config/energy/ha-config-energy.ts @@ -1,6 +1,5 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; -import "../../../components/ha-svg-icon"; import { EnergyPreferencesValidation, getEnergyPreferenceValidation, @@ -79,8 +78,7 @@ class HaConfigEnergy extends LitElement { .tabs=${configSections.experiences} > - After setting up a new device, it can take up to 2 hours for new data - to arrive in your energy dashboard. + ${this.hass.localize("ui.panel.config.energy.new_device_info")}
    - - - + > ${stateObj ? computeStateName(stateObj) : entry?.name || entityId} ${stateObj ? html` - - - + > ` : ""} diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index b7936933e6..96fab20b01 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -15,7 +15,7 @@ import { computeDomain } from "../../../common/entity/compute_domain"; import { domainIcon } from "../../../common/entity/domain_icon"; import "../../../components/ha-area-picker"; import "../../../components/ha-expansion-panel"; -import "../../../components/ha-icon-input"; +import "../../../components/ha-icon-picker"; import "../../../components/ha-switch"; import type { HaSwitch } from "../../../components/ha-switch"; import { @@ -133,20 +133,16 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { .placeholder=${this.entry.original_name} .disabled=${this._submitting} > - + > html` - + template: (_, entry: any) => html` + `, }, name: { @@ -249,18 +253,18 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { tabindex="0" style="display:inline-block; position: relative;" > - + ? mdiCancel + : mdiPencilOff} + > ${entity.restored ? this.hass.localize( @@ -371,9 +375,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { result.push({ ...entry, - icon: entity - ? stateIcon(entity) - : domainIcon(computeDomain(entry.entity_id)), + entity, name: computeEntityRegistryName(this.hass!, entry) || this.hass.localize("state.default.unavailable"), @@ -540,32 +542,35 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { > ` : html` - + .path=${mdiUndo} + .label=${this.hass.localize("ui.common.enable")} + > ${this.hass.localize( "ui.panel.config.entities.picker.enable_selected.button" )} - + .path=${mdiCancel} + .label=${this.hass.localize("ui.common.disable")} + > ${this.hass.localize( "ui.panel.config.entities.picker.disable_selected.button" )} - + .path=${mdiDelete} + .label=${this.hass.localize("ui.common.remove")} + > ${this.hass.localize( "ui.panel.config.entities.picker.remove_selected.button" @@ -575,17 +580,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
    ` : html` - - - + .path=${mdiFilterVariant} + > mwc-button, - .header-btns > mwc-icon-button { + .header-btns > ha-icon-button { margin: 8px; } ha-button-menu { diff --git a/src/panels/config/helpers/dialog-helper-detail.ts b/src/panels/config/helpers/dialog-helper-detail.ts index af6bece8df..9ae73825f4 100644 --- a/src/panels/config/helpers/dialog-helper-detail.ts +++ b/src/panels/config/helpers/dialog-helper-detail.ts @@ -121,10 +121,10 @@ export class DialogHelperDetail extends LitElement { .platform=${platform} dialogInitialFocus > - + .path=${domainIcon(platform)} + > ${this.hass.localize( `ui.panel.config.helpers.types.${platform}` diff --git a/src/panels/config/helpers/forms/ha-counter-form.ts b/src/panels/config/helpers/forms/ha-counter-form.ts index 6b7ae3be8d..badebfd38c 100644 --- a/src/panels/config/helpers/forms/ha-counter-form.ts +++ b/src/panels/config/helpers/forms/ha-counter-form.ts @@ -2,7 +2,7 @@ import "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; -import "../../../../components/ha-icon-input"; +import "../../../../components/ha-icon-picker"; import "../../../../components/ha-switch"; import type { HaSwitch } from "../../../../components/ha-switch"; import { Counter } from "../../../../data/counter"; @@ -81,14 +81,14 @@ class HaCounterForm extends LitElement { .invalid=${nameInvalid} dialogInitialFocus >
    - + > - + > `; } diff --git a/src/panels/config/helpers/forms/ha-input_datetime-form.ts b/src/panels/config/helpers/forms/ha-input_datetime-form.ts index d28277078c..8f9f89c084 100644 --- a/src/panels/config/helpers/forms/ha-input_datetime-form.ts +++ b/src/panels/config/helpers/forms/ha-input_datetime-form.ts @@ -1,13 +1,14 @@ import "@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 { fireEvent } from "../../../../common/dom/fire_event"; -import "../../../../components/ha-icon-input"; +import "../../../../components/ha-icon-picker"; import { InputDateTime } from "../../../../data/input_datetime"; import { haStyle } from "../../../../resources/styles"; import { HomeAssistant } from "../../../../types"; +import "../../../../components/ha-formfield"; +import "../../../../components/ha-radio"; +import type { HaRadio } from "../../../../components/ha-radio"; @customElement("ha-input_datetime-form") class HaInputDateTimeForm extends LitElement { @@ -70,43 +71,60 @@ class HaInputDateTimeForm extends LitElement { .invalid=${nameInvalid} dialogInitialFocus > - + >
    ${this.hass.localize("ui.dialogs.helper_settings.input_datetime.mode")}:
    - - - ${this.hass.localize( - "ui.dialogs.helper_settings.input_datetime.date" - )} - - - ${this.hass.localize( - "ui.dialogs.helper_settings.input_datetime.time" - )} - - - ${this.hass.localize( - "ui.dialogs.helper_settings.input_datetime.datetime" - )} - - + + + + + + + + `; } private _modeChanged(ev: CustomEvent) { - const mode = ev.detail.value; + const mode = (ev.target as HaRadio).value; fireEvent(this, "value-changed", { value: { ...this._item, diff --git a/src/panels/config/helpers/forms/ha-input_number-form.ts b/src/panels/config/helpers/forms/ha-input_number-form.ts index a87ea0cde0..9d6960062a 100644 --- a/src/panels/config/helpers/forms/ha-input_number-form.ts +++ b/src/panels/config/helpers/forms/ha-input_number-form.ts @@ -1,13 +1,14 @@ import "@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 { fireEvent } from "../../../../common/dom/fire_event"; -import "../../../../components/ha-icon-input"; +import "../../../../components/ha-icon-picker"; import { InputNumber } from "../../../../data/input_number"; import { haStyle } from "../../../../resources/styles"; import { HomeAssistant } from "../../../../types"; +import "../../../../components/ha-formfield"; +import "../../../../components/ha-radio"; +import type { HaRadio } from "../../../../components/ha-radio"; @customElement("ha-input_number-form") class HaInputNumberForm extends LitElement { @@ -85,14 +86,14 @@ class HaInputNumberForm extends LitElement { .invalid=${nameInvalid} dialogInitialFocus > - + > - - ${this.hass.localize( - "ui.dialogs.helper_settings.input_number.slider" - )} - - - ${this.hass.localize( - "ui.dialogs.helper_settings.input_number.box" - )} - - + + + + + - + > ${this.hass!.localize( "ui.dialogs.helper_settings.input_select.options" )}: @@ -89,11 +90,11 @@ class HaInputSelectForm extends LitElement { ${option} ` diff --git a/src/panels/config/helpers/forms/ha-input_text-form.ts b/src/panels/config/helpers/forms/ha-input_text-form.ts index 3c951bc8f3..7c912ca3df 100644 --- a/src/panels/config/helpers/forms/ha-input_text-form.ts +++ b/src/panels/config/helpers/forms/ha-input_text-form.ts @@ -1,13 +1,14 @@ import "@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 { fireEvent } from "../../../../common/dom/fire_event"; -import "../../../../components/ha-icon-input"; +import "../../../../components/ha-icon-picker"; +import type { HaRadio } from "../../../../components/ha-radio"; import { InputText } from "../../../../data/input_text"; import { haStyle } from "../../../../resources/styles"; import { HomeAssistant } from "../../../../types"; +import "../../../../components/ha-formfield"; +import "../../../../components/ha-radio"; @customElement("ha-input_text-form") class HaInputTextForm extends LitElement { @@ -76,14 +77,14 @@ class HaInputTextForm extends LitElement { .invalid=${nameInvalid} dialogInitialFocus > - + > ${this.hass.userData?.showAdvanced ? html` - - ${this.hass.localize( - "ui.dialogs.helper_settings.input_text.text" - )} - - - ${this.hass.localize( - "ui.dialogs.helper_settings.input_text.password" - )} - - + + + + + - + > html` - - `, + template: (icon, helper: any) => + icon + ? html` ` + : html``, }, name: { title: this.hass.localize( @@ -93,7 +96,7 @@ export class HaConfigHelpers extends LitElement { tabindex="0" style="display:inline-block; position: relative;" > - + ${this.hass.localize( "ui.panel.config.entities.picker.status.readonly" diff --git a/src/panels/config/info/system-health-card.ts b/src/panels/config/info/system-health-card.ts index 28c139609a..5f825ac37a 100644 --- a/src/panels/config/info/system-health-card.ts +++ b/src/panels/config/info/system-health-card.ts @@ -1,5 +1,4 @@ import "@material/mwc-button/mwc-button"; -import "@material/mwc-icon-button"; import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import "@material/mwc-list/mwc-list-item"; import { mdiContentCopy } from "@mdi/js"; @@ -12,7 +11,7 @@ import { copyToClipboard } from "../../../common/util/copy-clipboard"; import "../../../components/ha-button-menu"; import "../../../components/ha-card"; import "../../../components/ha-circular-progress"; -import "../../../components/ha-svg-icon"; +import "../../../components/ha-icon-button"; import { domainToName } from "../../../data/integration"; import { subscribeSystemHealthInfo, @@ -150,9 +149,11 @@ class SystemHealthCard extends LitElement { slot="toolbar-icon" @action=${this._copyInfo} > - - - + ${this.hass.localize("ui.panel.config.info.copy_raw")} diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts index 55e691d524..a3dbdff879 100644 --- a/src/panels/config/integrations/ha-config-integrations.ts +++ b/src/panels/config/integrations/ha-config-integrations.ts @@ -1,4 +1,3 @@ -import "@material/mwc-icon-button"; import { ActionDetail } from "@material/mwc-list"; import "@material/mwc-list/mwc-list-item"; import { mdiFilterVariant, mdiPlus } from "@mdi/js"; @@ -25,6 +24,7 @@ import { nextRender } from "../../../common/util/render-status"; import "../../../components/ha-button-menu"; import "../../../components/ha-checkbox"; import "../../../components/ha-fab"; +import "../../../components/ha-icon-button"; import "../../../components/ha-svg-icon"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { ConfigEntry, getConfigEntries } from "../../../data/config_entries"; @@ -290,13 +290,12 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { slot=${ifDefined(this.narrow ? "toolbar-icon" : undefined)} @action=${this._handleMenuAction} > - - - + ${this.hass.localize( @@ -326,6 +325,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { ? html`
    ` @@ -291,13 +298,11 @@ export class HaIntegrationCard extends LitElement { : ""} - - - + .label=${this.hass.localize("ui.common.menu")} + .path=${mdiDotsVertical} + > ${this.hass.localize( "ui.panel.config.integrations.config_entry.rename" diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-network-dashboard.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-network-dashboard.ts index 46ba6fbd7d..7dc5478939 100644 --- a/src/panels/config/integrations/integration-panels/ozw/ozw-network-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/ozw/ozw-network-dashboard.ts @@ -6,7 +6,6 @@ import { classMap } from "lit/directives/class-map"; import { navigate } from "../../../../../common/navigate"; import "../../../../../components/buttons/ha-call-service-button"; import "../../../../../components/ha-card"; -import "../../../../../components/ha-icon-next"; import { fetchOZWNetworkStatistics, fetchOZWNetworkStatus, diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-network-nodes.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-network-nodes.ts index ce9ec48b4b..8547058884 100644 --- a/src/panels/config/integrations/integration-panels/ozw/ozw-network-nodes.ts +++ b/src/panels/config/integrations/integration-panels/ozw/ozw-network-nodes.ts @@ -11,7 +11,6 @@ import { RowClickedEvent, } from "../../../../../components/data-table/ha-data-table"; import "../../../../../components/ha-card"; -import "../../../../../components/ha-icon-next"; import { fetchOZWNodes, OZWDevice } from "../../../../../data/ozw"; import "../../../../../layouts/hass-tabs-subpage"; import "../../../../../layouts/hass-tabs-subpage-data-table"; diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-node-config.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-node-config.ts index 336d2ee893..7c29bf2cbc 100644 --- a/src/panels/config/integrations/integration-panels/ozw/ozw-node-config.ts +++ b/src/panels/config/integrations/integration-panels/ozw/ozw-node-config.ts @@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators"; import { navigate } from "../../../../../common/navigate"; import "../../../../../components/buttons/ha-call-service-button"; import "../../../../../components/ha-card"; -import "../../../../../components/ha-icon-next"; import { fetchOZWNodeConfig, fetchOZWNodeMetadata, diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-node-dashboard.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-node-dashboard.ts index a5c6fc76ca..ec45071c16 100644 --- a/src/panels/config/integrations/integration-panels/ozw/ozw-node-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/ozw/ozw-node-dashboard.ts @@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators"; import { navigate } from "../../../../../common/navigate"; import "../../../../../components/buttons/ha-call-service-button"; import "../../../../../components/ha-card"; -import "../../../../../components/ha-icon-next"; import { fetchOZWNodeMetadata, fetchOZWNodeStatus, diff --git a/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-children.ts b/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-children.ts index 255579d4ad..032ee83778 100644 --- a/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-children.ts +++ b/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-children.ts @@ -101,6 +101,7 @@ class DialogZHADeviceChildren extends LitElement { active >` : html` -
    +
    diff --git a/src/panels/config/integrations/integration-panels/zha/zha-cluster-commands.ts b/src/panels/config/integrations/integration-panels/zha/zha-cluster-commands.ts index de1a2781c5..d00e672d5f 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-cluster-commands.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-cluster-commands.ts @@ -1,3 +1,4 @@ +import { mdiHelpCircle } from "@mdi/js"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-input/paper-input"; import "@polymer/paper-item/paper-item"; @@ -72,7 +73,8 @@ export class ZHAClusterCommands extends LitElement {
    diff --git a/src/panels/config/integrations/integration-panels/zha/zha-clusters-data-table.ts b/src/panels/config/integrations/integration-panels/zha/zha-clusters-data-table.ts index e29bcbe10d..f4f0e5fdbf 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-clusters-data-table.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-clusters-data-table.ts @@ -76,6 +76,7 @@ export class ZHAClustersDataTable extends LitElement { protected render(): TemplateResult { return html` diff --git a/src/panels/config/integrations/integration-panels/zha/zha-device-binding.ts b/src/panels/config/integrations/integration-panels/zha/zha-device-binding.ts index cc5623422d..024e35db24 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-device-binding.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-device-binding.ts @@ -1,4 +1,5 @@ import "@material/mwc-button/mwc-button"; +import { mdiHelpCircle } from "@mdi/js"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; @@ -52,7 +53,8 @@ export class ZHADeviceBindingControl extends LitElement { diff --git a/src/panels/config/integrations/integration-panels/zha/zha-device-endpoint-data-table.ts b/src/panels/config/integrations/integration-panels/zha/zha-device-endpoint-data-table.ts index eb2237a716..bebc5826a8 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-device-endpoint-data-table.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-device-endpoint-data-table.ts @@ -137,6 +137,7 @@ export class ZHADeviceEndpointDataTable extends LitElement { protected render(): TemplateResult { return html` diff --git a/src/panels/config/integrations/integration-panels/zha/zha-group-page.ts b/src/panels/config/integrations/integration-panels/zha/zha-group-page.ts index af438db7f3..3eeb04d18b 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-group-page.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-group-page.ts @@ -1,4 +1,5 @@ import "@material/mwc-button"; +import { mdiDelete } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { customElement, property, state, query } from "lit/decorators"; import { HASSDomEvent } from "../../../../../common/dom/fire_event"; @@ -101,8 +102,9 @@ export class ZHAGroupPage extends LitElement { >
    @@ -153,7 +155,7 @@ export class ZHAGroupPage extends LitElement { > -
    +
    -
    +
    ${!this.narrow ? html`[[localize('ui.panel.config.zwave.node_management.header')]] - + + +
    [[localize('ui.panel.config.zwave.node_management.introduction')]] @@ -309,12 +307,16 @@ class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) { >
    - - [[localize('ui.panel.config.zwave.node_management.exclude_entity')]] - + + + - -

    OpenZwave internal logfile

    - + +
    [[_ozwLog]]
    - - +
    + `; } @@ -56,7 +54,6 @@ class ZwaveLogDialog extends EventsMixin(PolymerElement) { this._opened = true; this._dialogClosedCallback = dialogClosedCallback; this._numLogLines = _numLogLines; - setTimeout(() => this.$.pwaDialog.center(), 0); if (_tail) { this.setProperties({ _intervalId: setInterval(() => { @@ -66,6 +63,14 @@ class ZwaveLogDialog extends EventsMixin(PolymerElement) { } } + closeDialog() { + clearInterval(this._intervalId); + this._opened = false; + const closedEvent = true; + this._dialogClosedCallback({ closedEvent }); + this._dialogClosedCallback = null; + } + async _refreshLog() { const info = await this.hass.callApi( "GET", @@ -73,16 +78,6 @@ class ZwaveLogDialog extends EventsMixin(PolymerElement) { ); this.setProperties({ _ozwLog: info }); } - - _dialogClosed(ev) { - if (ev.target.nodeName === "ZWAVE-LOG-DIALOG") { - clearInterval(this._intervalId); - this._opened = false; - const closedEvent = true; - this._dialogClosedCallback({ closedEvent }); - this._dialogClosedCallback = null; - } - } } customElements.define("zwave-log-dialog", ZwaveLogDialog); diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-network.ts b/src/panels/config/integrations/integration-panels/zwave/zwave-network.ts index 9a346e7551..f49090c2ad 100644 --- a/src/panels/config/integrations/integration-panels/zwave/zwave-network.ts +++ b/src/panels/config/integrations/integration-panels/zwave/zwave-network.ts @@ -1,3 +1,4 @@ +import { mdiCheckboxMarkedCircle, mdiClose, mdiHelpCircle } from "@mdi/js"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -5,7 +6,7 @@ import "../../../../../components/buttons/ha-call-api-button"; import "../../../../../components/buttons/ha-call-service-button"; import "../../../../../components/ha-card"; import "../../../../../components/ha-circular-progress"; -import "../../../../../components/ha-icon"; +import "../../../../../components/ha-svg-icon"; import "../../../../../components/ha-icon-button"; import "../../../../../components/ha-service-description"; import { @@ -55,7 +56,8 @@ export class ZwaveNetwork extends LitElement {
    @@ -79,7 +81,7 @@ export class ZwaveNetwork extends LitElement {
    ${this._networkStatus.state === ZWAVE_NETWORK_STATE_STOPPED ? html` - + ${this.hass!.localize( "ui.panel.config.zwave.network_status.network_stopped" )} @@ -98,7 +100,9 @@ export class ZwaveNetwork extends LitElement { ` : this._networkStatus.state === ZWAVE_NETWORK_STATE_AWAKED ? html` - + ${this.hass!.localize( "ui.panel.config.zwave.network_status.network_started" )}
    @@ -250,7 +254,7 @@ export class ZwaveNetwork extends LitElement { padding: 24px; } - .network-status ha-icon { + .network-status ha-svg-icon { display: block; margin: 0px auto 16px; width: 48px; diff --git a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-add-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-add-node.ts index 088ebf5f86..b33c340cc5 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-add-node.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-add-node.ts @@ -202,12 +202,12 @@ class DialogZWaveJSAddNode extends LitElement { (securityClass) => html`${this.hass.localize( - `ui.panel.config.zwave_js.add_node.security_classes.${SecurityClass[securityClass]}.title` + `ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.title` )}
    ${this.hass.localize( - `ui.panel.config.zwave_js.add_node.security_classes.${SecurityClass[securityClass]}.description` + `ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.description` )}
    `} > diff --git a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-node.ts index de8d541293..632c19286b 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-node.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-node.ts @@ -1,6 +1,5 @@ import "../../../../../components/ha-circular-progress"; import "@material/mwc-button/mwc-button"; -import "@material/mwc-linear-progress/mwc-linear-progress"; import { mdiStethoscope, mdiCheckCircle, mdiCloseCircle } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts index e24de84726..dbd163508f 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts @@ -1,10 +1,10 @@ import "@material/mwc-button/mwc-button"; -import "@material/mwc-icon-button/mwc-icon-button"; import { mdiAlertCircle, mdiCheckCircle, mdiCircle, mdiRefresh } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import "../../../../../components/ha-card"; +import "../../../../../components/ha-icon-button"; import "../../../../../components/ha-icon-next"; import "../../../../../components/ha-svg-icon"; import { getSignedPath } from "../../../../../data/auth"; @@ -83,9 +83,12 @@ class ZWaveJSConfigDashboard extends LitElement { .route=${this.route} .tabs=${configTabs} > - - - +
    ${this.hass.localize("ui.panel.config.zwave_js.dashboard.header")} diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-logs.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-logs.ts index 8df84d6576..42f9c88675 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-logs.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-logs.ts @@ -1,9 +1,11 @@ +import { mdiDownload } from "@mdi/js"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-listbox/paper-listbox"; -import { mdiDownload } from "@mdi/js"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultArray, html, LitElement } from "lit"; -import { customElement, property, state, query } from "lit/decorators"; +import { customElement, property, query, state } from "lit/decorators"; +import { capitalizeFirstLetter } from "../../../../../common/string/capitalize-first-letter"; +import "../../../../../components/ha-icon-button"; import { fetchZWaveJSLogConfig, setZWaveJSLogLevel, @@ -16,7 +18,6 @@ import { haStyle } from "../../../../../resources/styles"; import { HomeAssistant, Route } from "../../../../../types"; import { fileDownload } from "../../../../../util/file_download"; import { configTabs } from "./zwave_js-config-router"; -import { capitalizeFirstLetter } from "../../../../../common/string/capitalize-first-letter"; @customElement("zwave_js-logs") class ZWaveJSLogs extends SubscribeMixin(LitElement) { @@ -99,14 +100,13 @@ class ZWaveJSLogs extends SubscribeMixin(LitElement) { ` : ""}
    - - - + .path=${mdiDownload} + >
    diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts index 993b3cc7a9..9b2e0a4bdf 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts @@ -1,5 +1,4 @@ import "@material/mwc-button/mwc-button"; -import "@material/mwc-icon-button/mwc-icon-button"; import { mdiCheckCircle, mdiCircle, diff --git a/src/panels/config/logs/dialog-system-log-detail.ts b/src/panels/config/logs/dialog-system-log-detail.ts index 074a87eafb..b8abf3474d 100644 --- a/src/panels/config/logs/dialog-system-log-detail.ts +++ b/src/panels/config/logs/dialog-system-log-detail.ts @@ -1,12 +1,13 @@ -import "@material/mwc-icon-button/mwc-icon-button"; -import { mdiClose, mdiContentCopy, mdiPackageVariant } from "@mdi/js"; +import { mdiClose, mdiContentCopy } from "@mdi/js"; import "@polymer/paper-tooltip/paper-tooltip"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { property, state } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; import { copyToClipboard } from "../../../common/util/copy-clipboard"; +import "../../../components/ha-alert"; import "../../../components/ha-dialog"; import "../../../components/ha-header-bar"; +import "../../../components/ha-icon-button"; import "../../../components/ha-svg-icon"; import { domainToName, @@ -64,15 +65,18 @@ class DialogSystemLogDetail extends LitElement { const showDocumentation = this._manifest && (this._manifest.is_built_in || - // Custom components with our offical docs should not link to our docs + // Custom components with our official docs should not link to our docs !this._manifest.documentation.includes("://www.home-assistant.io")); return html` - - - + ${this.hass.localize( "ui.panel.config.logs.details", @@ -84,17 +88,20 @@ class DialogSystemLogDetail extends LitElement { >` )} - - - + ${this.isCustomIntegration - ? html`
    - + ? html` ${this.hass.localize( "ui.panel.config.logs.error_from_custom_integration" )} -
    ` + ` : ""}

    @@ -208,9 +215,9 @@ class DialogSystemLogDetail extends LitElement { margin-bottom: 0; font-family: var(--code-font-family, monospace); } - .custom { - padding: 8px 16px; - background-color: var(--warning-color); + ha-alert { + display: block; + margin: -4px 0; } .contents { padding: 16px; diff --git a/src/panels/config/logs/error-log-card.ts b/src/panels/config/logs/error-log-card.ts index b096fb93ad..bd9c7f9e7d 100644 --- a/src/panels/config/logs/error-log-card.ts +++ b/src/panels/config/logs/error-log-card.ts @@ -1,4 +1,5 @@ import "@material/mwc-button"; +import { mdiRefresh } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { property, state } from "lit/decorators"; import "../../../components/ha-icon-button"; @@ -17,8 +18,9 @@ class ErrorLogCard extends LitElement { ? html`

    ${this._errorHTML}
    diff --git a/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts b/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts index 486a6c8c55..6e3aa60a52 100644 --- a/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts +++ b/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts @@ -5,7 +5,7 @@ import { slugify } from "../../../../common/string/slugify"; import { computeRTLDirection } from "../../../../common/util/compute_rtl"; import { createCloseHeading } from "../../../../components/ha-dialog"; import "../../../../components/ha-formfield"; -import "../../../../components/ha-icon-input"; +import "../../../../components/ha-icon-picker"; import "../../../../components/ha-switch"; import type { HaSwitch } from "../../../../components/ha-switch"; import { @@ -118,13 +118,13 @@ export class DialogLovelaceDashboardDetail extends LitElement { )} dialogInitialFocus > - + > ${!this._params.dashboard && this.hass.userData?.showAdvanced ? html` + .path=${mdiCheckCircleOutline} + > ${this.hass.localize( `ui.panel.config.lovelace.dashboards.default_dashboard` @@ -127,7 +132,7 @@ export class HaConfigLovelaceDashboards extends LitElement { width: "100px", template: (requireAdmin: boolean) => requireAdmin - ? html` ` + ? html` ` : html` - `, }; columns.show_in_sidebar = { @@ -137,7 +142,9 @@ export class HaConfigLovelaceDashboards extends LitElement { type: "icon", width: "121px", template: (sidebar) => - sidebar ? html` ` : html` - `, + sidebar + ? html` ` + : html` - `, }; } @@ -149,9 +156,12 @@ export class HaConfigLovelaceDashboards extends LitElement { narrow ? html` ` : html` diff --git a/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts b/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts index b5a7f2859d..75748a23b6 100644 --- a/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts +++ b/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts @@ -11,7 +11,6 @@ import { RowClickedEvent, } from "../../../../components/data-table/ha-data-table"; import "../../../../components/ha-fab"; -import "../../../../components/ha-icon"; import "../../../../components/ha-svg-icon"; import { createResource, diff --git a/src/panels/config/scene/ha-scene-dashboard.ts b/src/panels/config/scene/ha-scene-dashboard.ts index a3399fc85a..039611f15a 100644 --- a/src/panels/config/scene/ha-scene-dashboard.ts +++ b/src/panels/config/scene/ha-scene-dashboard.ts @@ -1,4 +1,3 @@ -import "@material/mwc-icon-button"; import { mdiHelpCircle, mdiInformationOutline, @@ -14,11 +13,11 @@ import { ifDefined } from "lit/directives/if-defined"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; import { computeStateName } from "../../../common/entity/compute_state_name"; -import { stateIcon } from "../../../common/entity/state_icon"; import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; import "../../../components/ha-button-related-filter-menu"; import "../../../components/ha-fab"; -import "../../../components/ha-icon"; +import "../../../components/ha-icon-button"; +import "../../../components/ha-state-icon"; import "../../../components/ha-svg-icon"; import { forwardHaptic } from "../../../data/haptics"; import { activateScene, SceneEntity } from "../../../data/scene"; @@ -60,7 +59,6 @@ class HaSceneDashboard extends LitElement { ).map((scene) => ({ ...scene, name: computeStateName(scene), - icon: stateIcon(scene), })); } ); @@ -72,21 +70,21 @@ class HaSceneDashboard extends LitElement { type: "icon-button", template: (_toggle, scene) => html` - - - + > `, }, icon: { title: "", type: "icon", - template: (icon) => html` `, + template: (_, scene) => + html` `, }, name: { title: this.hass.localize("ui.panel.config.scene.picker.headers.name"), @@ -99,15 +97,14 @@ class HaSceneDashboard extends LitElement { title: "", type: "icon-button", template: (_info, scene) => html` - - - + .path=${mdiInformationOutline} + > `, }, edit: { @@ -121,16 +118,13 @@ class HaSceneDashboard extends LitElement { : undefined )} > - - - + .path=${scene.attributes.id ? mdiPencil : mdiPencilOff} + > ${!scene.attributes.id ? html` @@ -164,9 +158,12 @@ class HaSceneDashboard extends LitElement { @clear-filter=${this._clearFilter} hasFab > - - - + - - + .label=${this.hass.localize("ui.common.menu")} + .path=${mdiDotsVertical} + > - - +
    @@ -311,8 +310,8 @@ export class HaSceneEditor extends SubscribeMixin(

    ${device.name} + ${this.hass.localize( + "ui.panel.config.automation.editor.blueprint.header" + )} + +
    + ${this._blueprints + ? Object.keys(this._blueprints).length + ? html` + + ` + : this.hass.localize( + "ui.panel.config.automation.editor.blueprint.no_blueprints" + ) + : html``} +
    + + ${this.config.use_blueprint.path + ? blueprint && "error" in blueprint + ? html`

    + There is an error in this Blueprint: ${blueprint.error} +

    ` + : html`${blueprint?.metadata.description + ? html`` + : ""} + ${blueprint?.metadata?.input && + Object.keys(blueprint.metadata.input).length + ? Object.entries(blueprint.metadata.input).map( + ([key, value]) => + html` + ${value?.name || key} + ${value?.description} + ${value?.selector + ? html`` + : html``} + ` + ) + : html`

    + ${this.hass.localize( + "ui.panel.config.automation.editor.blueprint.no_inputs" + )} +

    `}` + : ""} +
    + `; + } + + private async _getBlueprints() { + this._blueprints = await fetchBlueprints(this.hass, "script"); + } + + private _blueprintChanged(ev) { + ev.stopPropagation(); + if (this.config.use_blueprint.path === ev.detail.value) { + return; + } + fireEvent(this, "value-changed", { + value: { + ...this.config, + use_blueprint: { + path: ev.detail.value, + }, + }, + }); + } + + private _inputChanged(ev) { + ev.stopPropagation(); + const target = ev.target as any; + const key = target.key; + const value = ev.detail.value; + if ( + (this.config.use_blueprint.input && + this.config.use_blueprint.input[key] === value) || + (!this.config.use_blueprint.input && value === "") + ) { + return; + } + const input = { ...this.config.use_blueprint.input, [key]: value }; + + if (value === "" || value === undefined) { + delete input[key]; + } + + fireEvent(this, "value-changed", { + value: { + ...this.config, + use_blueprint: { + ...this.config.use_blueprint, + input, + }, + }, + }); + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + css` + .padding { + padding: 16px; + } + .blueprint-picker-container { + padding: 16px; + } + p { + margin-bottom: 0; + } + ha-settings-row { + --paper-time-input-justify-content: flex-end; + border-top: 1px solid var(--divider-color); + } + :host(:not([narrow])) ha-settings-row paper-input { + width: 60%; + } + :host(:not([narrow])) ha-settings-row ha-selector { + width: 60%; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "blueprint-script-editor": HaBlueprintScriptEditor; + } +} diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index 315f2dc2f3..2a7eb0b167 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -30,7 +30,7 @@ import "../../../components/ha-button-menu"; import "../../../components/ha-card"; import "../../../components/ha-fab"; import "../../../components/ha-icon-button"; -import "../../../components/ha-icon-input"; +import "../../../components/ha-icon-picker"; import "../../../components/ha-svg-icon"; import "../../../components/ha-yaml-editor"; import type { HaYamlEditor } from "../../../components/ha-yaml-editor"; @@ -38,6 +38,7 @@ import { Action, deleteScript, getScriptEditorInitData, + ManualScriptConfig, MODES, MODES_MAX, ScriptConfig, @@ -55,6 +56,7 @@ import "../automation/action/ha-automation-action"; import { HaDeviceAction } from "../automation/action/types/ha-automation-action-device_id"; import "../ha-config-section"; import { configSections } from "../ha-panel-config"; +import "./blueprint-script-editor"; export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @@ -96,12 +98,11 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { @action=${this._handleMenuAction} activatable > - - + .label=${this.hass.localize("ui.common.menu")} + .path=${mdiDotsVertical} + >
    - - + ${!this.scriptEntityId ? html` ` : ""} -

    - ${this.hass.localize( - "ui.panel.config.script.editor.modes.description", - "documentation_link", - html`${this.hass.localize( - "ui.panel.config.script.editor.modes.documentation" - )}` - )} -

    - - - ${MODES.map( - (mode) => html` - - ${this.hass.localize( - `ui.panel.config.script.editor.modes.${mode}` - ) || mode} - - ` - )} - - - ${this._config.mode && - MODES_MAX.includes(this._config.mode) - ? html` - ` - : html``} + ${"use_blueprint" in this._config + ? "" + : html`

    + ${this.hass.localize( + "ui.panel.config.script.editor.modes.description", + "documentation_link", + html`${this.hass.localize( + "ui.panel.config.script.editor.modes.documentation" + )}` + )} +

    + + + ${MODES.map( + (mode) => html` + + ${this.hass.localize( + `ui.panel.config.script.editor.modes.${mode}` + ) || mode} + + ` + )} + + + ${this._config.mode && + MODES_MAX.includes(this._config.mode) + ? html` + ` + : html``} `}

    ${this.scriptEntityId ? html` @@ -324,37 +327,48 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { - - - ${this.hass.localize( - "ui.panel.config.script.editor.sequence" - )} - - -

    - ${this.hass.localize( - "ui.panel.config.script.editor.sequence_sentence" - )} -

    - ` + : html` - ${this.hass.localize( - "ui.panel.config.script.editor.link_available_actions" - )} - -
    - -
    + + ${this.hass.localize( + "ui.panel.config.script.editor.sequence" + )} + + +

    + ${this.hass.localize( + "ui.panel.config.script.editor.sequence_sentence" + )} +

    + + ${this.hass.localize( + "ui.panel.config.script.editor.link_available_actions" + )} + +
    + + `} ` : ""}
    @@ -428,7 +442,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { (!oldScript || oldScript !== this.scriptEntityId) ) { this.hass - .callApi( + .callApi( "GET", `config/script/config/${computeObjectId(this.scriptEntityId)}` ) @@ -467,11 +481,16 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { ) { const initData = getScriptEditorInitData(); this._dirty = !!initData; - this._config = { + const baseConfig: Partial = { alias: this.hass.localize("ui.panel.config.script.editor.default_name"), - sequence: [{ ...HaDeviceAction.defaultConfig }], - ...initData, }; + if (!initData || !("use_blueprint" in initData)) { + baseConfig.sequence = [{ ...HaDeviceAction.defaultConfig }]; + } + this._config = { + ...baseConfig, + ...initData, + } as ScriptConfig; } } @@ -549,6 +568,11 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { this._dirty = true; } + private _configChanged(ev) { + this._config = ev.detail.value; + this._dirty = true; + } + private _sequenceChanged(ev: CustomEvent): void { this._config = { ...this._config!, sequence: ev.detail.value as Action[] }; this._errors = undefined; @@ -608,7 +632,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { ) { return; } - // Wait for dialog to complate closing + // Wait for dialog to complete closing await new Promise((resolve) => setTimeout(resolve, 0)); } showScriptEditor({ @@ -750,3 +774,9 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { } customElements.define("ha-script-editor", HaScriptEditor); + +declare global { + interface HTMLElementTagNameMap { + "ha-script-editor": HaScriptEditor; + } +} diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts index c29c647d4d..f2d68cca59 100644 --- a/src/panels/config/script/ha-script-picker.ts +++ b/src/panels/config/script/ha-script-picker.ts @@ -1,4 +1,3 @@ -import "@material/mwc-icon-button"; import { mdiHelpCircle, mdiHistory, @@ -14,11 +13,11 @@ import memoizeOne from "memoize-one"; import { formatDateTime } from "../../../common/datetime/format_date_time"; import { fireEvent } from "../../../common/dom/fire_event"; import { computeStateName } from "../../../common/entity/compute_state_name"; -import { stateIcon } from "../../../common/entity/state_icon"; import { computeRTL } from "../../../common/util/compute_rtl"; import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; import "../../../components/ha-button-related-filter-menu"; import "../../../components/ha-fab"; +import "../../../components/ha-icon-button"; import "../../../components/ha-svg-icon"; import { triggerScript } from "../../../data/script"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; @@ -61,7 +60,6 @@ class HaScriptPicker extends LitElement { ).map((script) => ({ ...script, name: computeStateName(script), - icon: stateIcon(script), last_triggered: script.attributes.last_triggered || undefined, })); } @@ -74,21 +72,21 @@ class HaScriptPicker extends LitElement { type: "icon-button", template: (_toggle, script) => html` - - - + .path=${mdiPlay} + > `, }, icon: { title: "", type: "icon", - template: (icon) => html` `, + template: (_icon, script) => + html` `, }, name: { title: this.hass.localize("ui.panel.config.script.picker.headers.name"), @@ -128,13 +126,14 @@ class HaScriptPicker extends LitElement { title: "", type: "icon-button", template: (_info, script) => html` - - - + .label=${this.hass.localize( + "ui.panel.config.script.picker.show_info" + )} + .path=${mdiInformationOutline} + > `, }; columns.trace = { @@ -142,13 +141,12 @@ class HaScriptPicker extends LitElement { type: "icon-button", template: (_info, script: any) => html` - - - + .path=${mdiHistory} + > `, }; @@ -157,13 +155,12 @@ class HaScriptPicker extends LitElement { type: "icon-button", template: (_info, script: any) => html` - - - + .path=${mdiPencil} + > `, }; @@ -188,9 +185,12 @@ class HaScriptPicker extends LitElement { @clear-filter=${this._clearFilter} hasFab > - - - + - - - + - - + .path=${mdiDownload} + > `; return html` @@ -120,23 +122,24 @@ export class HaScriptTrace extends LitElement { class="linkButton" href="/config/script/edit/${this.scriptEntityId}" > - - - +
    ` : ""} ${this._traces && this._traces.length > 0 ? html`
    - - - + .path=${mdiRayEndArrow} + > - - - + .path=${mdiRayStartArrow} + >
    ` : ""} diff --git a/src/panels/config/tags/ha-config-tags.ts b/src/panels/config/tags/ha-config-tags.ts index 09d5096d16..d0c6b780c0 100644 --- a/src/panels/config/tags/ha-config-tags.ts +++ b/src/panels/config/tags/ha-config-tags.ts @@ -1,4 +1,3 @@ -import "@material/mwc-icon-button"; import { mdiCog, mdiContentDuplicate, @@ -12,6 +11,7 @@ import memoizeOne from "memoize-one"; import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; import "../../../components/ha-card"; import "../../../components/ha-fab"; +import "../../../components/ha-icon-button"; import "../../../components/ha-relative-time"; import { showAutomationEditor, TagTrigger } from "../../../data/automation"; import { @@ -107,36 +107,33 @@ export class HaConfigTags extends SubscribeMixin(LitElement) { columns.write = { title: "", type: "icon-button", - template: (_write, tag: any) => html` html` - - `, + .label=${this.hass.localize("ui.panel.config.tag.write")} + .path=${mdiContentDuplicate} + >`, }; } columns.automation = { title: "", type: "icon-button", - template: (_automation, tag: any) => html` html` - - `, + .label=${this.hass.localize("ui.panel.config.tag.create_automation")} + .path=${mdiRobot} + >`, }; columns.edit = { title: "", type: "icon-button", - template: (_settings, tag: any) => html` html` - - `, + .label=${this.hass.localize("ui.panel.config.tag.edit")} + .path=${mdiCog} + >`, }; return columns; } @@ -193,9 +190,12 @@ export class HaConfigTags extends SubscribeMixin(LitElement) { .noDataText=${this.hass.localize("ui.panel.config.tag.no_tags")} hasFab > - - - + - is_active ? html` ` : "", + is_active + ? html` ` + : "", }, system_generated: { title: this.hass.localize( @@ -100,7 +102,9 @@ export class HaConfigUsers extends LitElement { filterable: true, width: "160px", template: (generated) => - generated ? html` ` : "", + generated + ? html` ` + : "", }, }; diff --git a/src/panels/config/zone/ha-config-zone.ts b/src/panels/config/zone/ha-config-zone.ts index a8975b8cb7..5533dcff03 100644 --- a/src/panels/config/zone/ha-config-zone.ts +++ b/src/panels/config/zone/ha-config-zone.ts @@ -1,4 +1,3 @@ -import "@material/mwc-icon-button"; import { mdiPencil, mdiPencilOff, mdiPlus } from "@mdi/js"; import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-item-body"; @@ -13,7 +12,7 @@ import { PropertyValues, TemplateResult, } from "lit"; -import { customElement, property, state, query } from "lit/decorators"; +import { customElement, property, query, state } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; import memoizeOne from "memoize-one"; import { computeStateDomain } from "../../../common/entity/compute_state_domain"; @@ -21,6 +20,7 @@ import { navigate } from "../../../common/navigate"; import { stringCompare } from "../../../common/string/compare"; import "../../../components/ha-card"; import "../../../components/ha-fab"; +import "../../../components/ha-icon-button"; import "../../../components/ha-svg-icon"; import "../../../components/map/ha-locations-editor"; import type { @@ -124,7 +124,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { this._storageItems === undefined || this._stateItems === undefined ) { - return html` `; + return html``; } const hass = this.hass; const listBox = @@ -151,15 +151,17 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { .entry=${entry} > - ${entry.name} + ${entry.name} ${!this.narrow ? html` - - - + .path=${mdiPencil} + .label=${hass.localize( + "ui.panel.config.zone.edit_zone" + )} + > ` : ""} @@ -181,7 +183,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { stateObject.entity_id}
    - - - + .path=${stateObject.entity_id === "zone.home" && + this.narrow && + this._canEditCore + ? mdiPencil + : mdiPencilOff} + .label=${hass.localize( + "ui.panel.config.zone.edit_zone" + )} + > ${stateObject.entity_id === "zone.home" - ? this.hass.localize( + ? hass.localize( `ui.panel.config.zone.${ this.narrow ? "edit_home_zone_narrow" : "edit_home_zone" }` ) - : this.hass.localize( + : hass.localize( "ui.panel.config.zone.configured_in_yaml" )} @@ -488,10 +490,10 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { overflow: hidden; } ha-icon, - mwc-icon-button:not([disabled]) { + ha-icon-button:not([disabled]) { color: var(--secondary-text-color); } - mwc-icon-button { + ha-icon-button { --mdc-theme-text-disabled-on-light: var(--disabled-text-color); } .empty { @@ -520,6 +522,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { paper-icon-item { padding-top: 4px; padding-bottom: 4px; + cursor: pointer; } .overflow paper-icon-item:last-child { margin-bottom: 80px; diff --git a/src/panels/developer-tools/ha-panel-developer-tools.ts b/src/panels/developer-tools/ha-panel-developer-tools.ts index 1a218e9a24..a3c1c7a897 100644 --- a/src/panels/developer-tools/ha-panel-developer-tools.ts +++ b/src/panels/developer-tools/ha-panel-developer-tools.ts @@ -4,7 +4,6 @@ import "@polymer/paper-tabs/paper-tab"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { navigate } from "../../common/navigate"; -import "../../components/ha-icon-button"; import "../../components/ha-menu-button"; import "../../components/ha-tabs"; import "../../layouts/ha-app-layout"; diff --git a/src/panels/developer-tools/service/developer-tools-service.ts b/src/panels/developer-tools/service/developer-tools-service.ts index a560d70693..77b85734b9 100644 --- a/src/panels/developer-tools/service/developer-tools-service.ts +++ b/src/panels/developer-tools/service/developer-tools-service.ts @@ -14,6 +14,7 @@ import { HaProgressButton } from "../../../components/buttons/ha-progress-button import "../../../components/entity/ha-entity-picker"; import "../../../components/ha-card"; import "../../../components/ha-expansion-panel"; +import "../../../components/ha-icon-button"; import "../../../components/ha-service-control"; import "../../../components/ha-service-picker"; import "../../../components/ha-yaml-editor"; @@ -185,12 +186,11 @@ class HaPanelDevService extends LitElement { target="_blank" rel="noreferrer" > - - - + ` : ""}
    ` diff --git a/src/panels/developer-tools/state/developer-tools-state.js b/src/panels/developer-tools/state/developer-tools-state.js index 201619bb3b..9c52a70ac8 100644 --- a/src/panels/developer-tools/state/developer-tools-state.js +++ b/src/panels/developer-tools/state/developer-tools-state.js @@ -4,7 +4,6 @@ import { mdiInformationOutline, mdiRefresh, } from "@mdi/js"; -import "@polymer/paper-checkbox/paper-checkbox"; import "@polymer/paper-input/paper-input"; import { html } from "@polymer/polymer/lib/utils/html-tag"; /* eslint-plugin-disable lit */ @@ -16,7 +15,9 @@ import { escapeRegExp } from "../../../common/string/escape_regexp"; import { copyToClipboard } from "../../../common/util/copy-clipboard"; import "../../../components/entity/ha-entity-picker"; import "../../../components/ha-code-editor"; +import "../../../components/ha-icon-button"; import "../../../components/ha-svg-icon"; +import "../../../components/ha-checkbox"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { EventsMixin } from "../../../mixins/events-mixin"; import LocalizeMixin from "../../../mixins/localize-mixin"; @@ -67,6 +68,15 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) { ); } + th.attributes { + position: relative; + } + + th.attributes ha-checkbox { + position: absolute; + bottom: -8px; + } + :host([rtl]) .entities th { text-align: right; } @@ -165,11 +175,11 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) { raised >[[localize('ui.panel.developer-tools.tabs.states.set_state')]]
    - + path="[[refreshIcon()]]" + >
    @@ -196,12 +206,13 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) { [[localize('ui.panel.developer-tools.tabs.states.entity')]] [[localize('ui.panel.developer-tools.tabs.states.state')]] - + [[localize('ui.panel.developer-tools.tabs.states.attributes')]] - + reducedTouchTarget + > @@ -544,6 +555,7 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) { } saveAttributeCheckboxState(ev) { + this._showAttributes = ev.target.checked; try { localStorage.setItem("devToolsShowAttributes", ev.target.checked); } catch (e) { diff --git a/src/panels/developer-tools/statistics/developer-tools-statistics.ts b/src/panels/developer-tools/statistics/developer-tools-statistics.ts index 66fbd5354b..71963bfe6e 100644 --- a/src/panels/developer-tools/statistics/developer-tools-statistics.ts +++ b/src/panels/developer-tools/statistics/developer-tools-statistics.ts @@ -8,18 +8,24 @@ import { computeStateName } from "../../../common/entity/compute_state_name"; import "../../../components/data-table/ha-data-table"; import type { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; import { + clearStatistics, getStatisticIds, StatisticsMetaData, StatisticsValidationResult, validateStatistics, } from "../../../data/history"; -import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { showFixStatisticsUnitsChangedDialog } from "./show-dialog-statistics-fix-units-changed"; import { showFixStatisticsUnsupportedUnitMetadataDialog } from "./show-dialog-statistics-fix-unsupported-unit-meta"; const FIX_ISSUES_ORDER = { + no_state: 0, + entity_no_longer_recorded: 1, entity_not_recorded: 1, unsupported_unit_state: 2, unsupported_state_class: 3, @@ -103,7 +109,7 @@ class HaPanelDevStatistics extends LitElement {
    Do you want to permanently remove + the long term statistics of ${issue.data.statistic_id} from your + database?`, + confirm: async () => { + await clearStatistics(this.hass, [issue.data.statistic_id]); + this._validateStatistics(); + }, + }); + break; case "entity_not_recorded": showAlertDialog(this, { title: "Entity not recorded", @@ -172,6 +192,24 @@ class HaPanelDevStatistics extends LitElement { for more information.`, }); break; + case "entity_no_longer_recorded": + showAlertDialog(this, { + title: "Entity no longer recorded", + text: html`We have generated statistics for this entity in the past, + but state changes of this entity are no longer recorded, therefore, + we can not track long term statistics for it anymore. +

    You probably excluded this entity, or have just included + some entities.

    See the + + recorder documentation + for more information.`, + }); + break; case "unsupported_state_class": showAlertDialog(this, { title: "Unsupported state class", diff --git a/src/panels/developer-tools/template/developer-tools-template.ts b/src/panels/developer-tools/template/developer-tools-template.ts index 9ad752a4b9..952815a91f 100644 --- a/src/panels/developer-tools/template/developer-tools-template.ts +++ b/src/panels/developer-tools/template/developer-tools-template.ts @@ -25,7 +25,7 @@ The temperature is {{ my_test_json.temperature }} {{ my_test_json.unit }}. {% if is_state("sun.sun", "above_horizon") -%} The sun rose {{ relative_time(states.sun.sun.last_changed) }} ago. {%- else -%} - The sun will rise at {{ as_timestamp(strptime(state_attr("sun.sun", "next_rising"), "")) | timestamp_local }}. + The sun will rise at {{ as_timestamp(state_attr("sun.sun", "next_rising")) | timestamp_local }}. {%- endif %} For loop example getting entity values in the weather domain: diff --git a/src/panels/energy/cards/energy-setup-wizard-card.ts b/src/panels/energy/cards/energy-setup-wizard-card.ts index 248a80c10d..57a1ee52e7 100644 --- a/src/panels/energy/cards/energy-setup-wizard-card.ts +++ b/src/panels/energy/cards/energy-setup-wizard-card.ts @@ -1,7 +1,12 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; -import { EnergyPreferences, saveEnergyPreferences } from "../../../data/energy"; +import { + EnergyInfo, + EnergyPreferences, + getEnergyInfo, + saveEnergyPreferences, +} from "../../../data/energy"; import { LovelaceCardConfig } from "../../../data/lovelace"; import { HomeAssistant } from "../../../types"; import { LovelaceCard, Lovelace } from "../../lovelace/types"; @@ -20,6 +25,8 @@ export class EnergySetupWizard extends LitElement implements LovelaceCard { @property({ attribute: false }) public lovelace?: Lovelace; + @state() private _info?: EnergyInfo; + @state() private _step = 0; @state() private _preferences: EnergyPreferences = { @@ -39,6 +46,7 @@ export class EnergySetupWizard extends LitElement implements LovelaceCard { protected firstUpdated() { this.hass.loadFragmentTranslation("config"); + this._fetchconfig(); } protected render(): TemplateResult { @@ -54,6 +62,7 @@ export class EnergySetupWizard extends LitElement implements LovelaceCard { ? html`` : this._step === 2 @@ -90,6 +99,10 @@ export class EnergySetupWizard extends LitElement implements LovelaceCard { `; } + private async _fetchconfig() { + this._info = await getEnergyInfo(this.hass); + } + private _prefsChanged(ev: CustomEvent) { this._preferences = ev.detail.value; } diff --git a/src/panels/energy/ha-panel-energy.ts b/src/panels/energy/ha-panel-energy.ts index 5f825fede5..fe5c64e361 100644 --- a/src/panels/energy/ha-panel-energy.ts +++ b/src/panels/energy/ha-panel-energy.ts @@ -122,9 +122,6 @@ class PanelEnergy extends LitElement { return [ haStyle, css` - mwc-icon-button { - color: var(--text-primary-color); - } app-toolbar { display: flex; justify-content: space-between; diff --git a/src/panels/energy/strategies/energy-strategy.ts b/src/panels/energy/strategies/energy-strategy.ts index dd4c738d80..41827e062b 100644 --- a/src/panels/energy/strategies/energy-strategy.ts +++ b/src/panels/energy/strategies/energy-strategy.ts @@ -63,7 +63,7 @@ export class EnergyStrategy { // Only include if we have a grid source. if (hasGrid) { view.cards!.push({ - title: "Energy usage", + title: hass.localize("ui.panel.energy.cards.energy_usage_graph_title"), type: "energy-usage-graph", collection_key: "energy_dashboard", }); @@ -72,7 +72,7 @@ export class EnergyStrategy { // Only include if we have a solar source. if (hasSolar) { view.cards!.push({ - title: "Solar production", + title: hass.localize("ui.panel.energy.cards.energy_solar_graph_title"), type: "energy-solar-graph", collection_key: "energy_dashboard", }); @@ -81,7 +81,7 @@ export class EnergyStrategy { // Only include if we have a gas source. if (hasGas) { view.cards!.push({ - title: "Gas consumption", + title: hass.localize("ui.panel.energy.cards.energy_gas_graph_title"), type: "energy-gas-graph", collection_key: "energy_dashboard", }); @@ -90,7 +90,7 @@ export class EnergyStrategy { // Only include if we have a grid. if (hasGrid) { view.cards!.push({ - title: "Energy distribution", + title: hass.localize("ui.panel.energy.cards.energy_distribution_title"), type: "energy-distribution", view_layout: { position: "sidebar" }, collection_key: "energy_dashboard", @@ -99,7 +99,9 @@ export class EnergyStrategy { if (hasGrid || hasSolar) { view.cards!.push({ - title: "Sources", + title: hass.localize( + "ui.panel.energy.cards.energy_sources_table_title" + ), type: "energy-sources-table", collection_key: "energy_dashboard", }); @@ -135,7 +137,9 @@ export class EnergyStrategy { // Only include if we have at least 1 device in the config. if (prefs.device_consumption.length) { view.cards!.push({ - title: "Monitor individual devices", + title: hass.localize( + "ui.panel.energy.cards.energy_devices_graph_title" + ), type: "energy-devices-graph", collection_key: "energy_dashboard", }); diff --git a/src/panels/history/ha-panel-history.ts b/src/panels/history/ha-panel-history.ts index c1f7ae5fd6..1e37f7a7f7 100644 --- a/src/panels/history/ha-panel-history.ts +++ b/src/panels/history/ha-panel-history.ts @@ -12,12 +12,14 @@ import { } from "date-fns"; import { css, html, LitElement, PropertyValues } from "lit"; import { property, state } from "lit/decorators"; +import { extractSearchParam } from "../../common/url/search-params"; import { computeRTL } from "../../common/util/compute_rtl"; import "../../components/chart/state-history-charts"; import "../../components/entity/ha-entity-picker"; import "../../components/ha-circular-progress"; import "../../components/ha-date-range-picker"; import type { DateRangePickerRanges } from "../../components/ha-date-range-picker"; +import "../../components/ha-icon-button"; import "../../components/ha-menu-button"; import { computeHistory, fetchDate } from "../../data/history"; import "../../layouts/ha-app-layout"; @@ -65,12 +67,12 @@ class HaPanelHistory extends LitElement { .narrow=${this.narrow} >
    ${this.hass.localize("panel.history")}
    - - - + .path=${mdiRefresh} + .label=${this.hass.localize("ui.common.refresh")} + > @@ -135,6 +137,8 @@ class HaPanelHistory extends LitElement { [this.hass.localize("ui.components.date-range-picker.ranges.last_week")]: [addDays(weekStart, -7), addDays(weekEnd, -7)], }; + + this._entityId = extractSearchParam("entity_id") ?? ""; } protected updated(changedProps: PropertyValues) { diff --git a/src/panels/iframe/ha-panel-iframe.js b/src/panels/iframe/ha-panel-iframe.js deleted file mode 100644 index 8dcc0e4670..0000000000 --- a/src/panels/iframe/ha-panel-iframe.js +++ /dev/null @@ -1,44 +0,0 @@ -import "@polymer/app-layout/app-toolbar/app-toolbar"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "../../components/ha-menu-button"; -import "../../styles/polymer-ha-style"; - -class HaPanelIframe extends PolymerElement { - static get template() { - return html` - - - -
    [[panel.title]]
    -
    - - - `; - } - - static get properties() { - return { - hass: Object, - narrow: Boolean, - panel: Object, - }; - } -} - -customElements.define("ha-panel-iframe", HaPanelIframe); diff --git a/src/panels/iframe/ha-panel-iframe.ts b/src/panels/iframe/ha-panel-iframe.ts new file mode 100644 index 0000000000..14aec1fdcd --- /dev/null +++ b/src/panels/iframe/ha-panel-iframe.ts @@ -0,0 +1,63 @@ +import { html, css, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import "../../layouts/hass-error-screen"; +import "../../layouts/hass-subpage"; +import { HomeAssistant, PanelInfo } from "../../types"; + +@customElement("ha-panel-iframe") +class HaPanelIframe extends LitElement { + @property() hass!: HomeAssistant; + + @property({ type: Boolean }) narrow!: boolean; + + @property() panel!: PanelInfo<{ url: string }>; + + render() { + if ( + location.protocol === "https:" && + new URL(this.panel.config.url, location.toString()).protocol !== "https:" + ) { + return html` + + `; + } + + return html` + + + + `; + } + + static styles = css` + iframe { + border: 0; + width: 100%; + position: absolute; + height: 100%; + background-color: var(--primary-background-color); + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-panel-iframe": HaPanelIframe; + } +} diff --git a/src/panels/logbook/ha-logbook.ts b/src/panels/logbook/ha-logbook.ts index 2ac78f9515..87d3d311ac 100644 --- a/src/panels/logbook/ha-logbook.ts +++ b/src/panels/logbook/ha-logbook.ts @@ -222,6 +222,7 @@ class HaLogbook extends LitElement { }?run_id=${ this.traceContexts[item.context_id!].run_id }`} + @click=${this._close} >${this.hass.localize( "ui.components.logbook.show_trace" )} fireEvent(this, "closed"), 500); + } + static get styles(): CSSResultGroup { return [ haStyle, diff --git a/src/panels/logbook/ha-panel-logbook.ts b/src/panels/logbook/ha-panel-logbook.ts index 8b7987f8d1..ed9fb3e690 100644 --- a/src/panels/logbook/ha-panel-logbook.ts +++ b/src/panels/logbook/ha-panel-logbook.ts @@ -1,5 +1,4 @@ import { mdiRefresh } from "@mdi/js"; -import "@material/mwc-icon-button"; import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-toolbar/app-toolbar"; import { css, html, LitElement, PropertyValues } from "lit"; @@ -20,6 +19,7 @@ import "../../components/entity/ha-entity-picker"; import "../../components/ha-circular-progress"; import "../../components/ha-date-range-picker"; import type { DateRangePickerRanges } from "../../components/ha-date-range-picker"; +import "../../components/ha-icon-button"; import "../../components/ha-menu-button"; import { clearLogbookCache, @@ -33,6 +33,7 @@ import "../../layouts/ha-app-layout"; import { haStyle } from "../../resources/styles"; import { HomeAssistant } from "../../types"; import "./ha-logbook"; +import { extractSearchParam } from "../../common/url/search-params"; @customElement("ha-panel-logbook") export class HaPanelLogbook extends LitElement { @@ -82,12 +83,12 @@ export class HaPanelLogbook extends LitElement { .narrow=${this.narrow} >
    ${this.hass.localize("panel.logbook")}
    - - - + > @@ -158,6 +159,8 @@ export class HaPanelLogbook extends LitElement { [this.hass.localize("ui.components.date-range-picker.ranges.last_week")]: [addDays(weekStart, -7), addDays(weekEnd, -7)], }; + + this._entityId = extractSearchParam("entity_id") ?? ""; } protected updated(changedProps: PropertyValues) { diff --git a/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts index a64d0864e6..ee15dc7b3f 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts @@ -59,7 +59,9 @@ class HuiEnergyCarbonGaugeCard } if (!this._data) { - return html`Loading...`; + return html`${this.hass.localize( + "ui.panel.lovelace.cards.energy.loading" + )}`; } if (!this._data.co2SignalEntity) { @@ -129,9 +131,9 @@ class HuiEnergyCarbonGaugeCard - This card indicates how much of the energy consumed by your - home was generated using non-fossil fuels like solar, wind and - nuclear. The higher, the better! + ${this.hass.localize( + "ui.panel.lovelace.cards.energy.carbon_consumed_gauge.card_indicates_energy_used" + )} -
    Non-fossil energy consumed
    +
    + ${this.hass.localize( + "ui.panel.lovelace.cards.energy.carbon_consumed_gauge.non_fossil_energy_consumed" + )} +
    ` - : html`Consumed non-fossil energy couldn't be calculated`} + : html`${this.hass.localize( + "ui.panel.lovelace.cards.energy.carbon_consumed_gauge.non_fossil_energy_not_calculated" + )}`} `; } diff --git a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts index 5d24f900d5..5006808340 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts @@ -170,7 +170,9 @@ export class HuiEnergyDevicesGraphCard const datasets: ChartDataset<"bar", ParsedDataType<"bar">[]>[] = [ { - label: "Energy usage", + label: this.hass.localize( + "ui.panel.lovelace.cards.energy.energy_devices_graph.energy_usage" + ), borderColor, backgroundColor, data, diff --git a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts index 3a71b8d80b..7c2cf2c2d2 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts @@ -70,7 +70,9 @@ class HuiEnergyDistrubutionCard } if (!this._data) { - return html`Loading…`; + return html`${this.hass.localize( + "ui.panel.lovelace.cards.energy.loading" + )}`; } const prefs = this._data.prefs; @@ -267,7 +269,11 @@ class HuiEnergyDistrubutionCard ${lowCarbonEnergy === undefined ? html`
    ` : html`
    ` @@ -659,7 +685,11 @@ class HuiEnergyDistrubutionCard ? html`
    Go to the energy dashboard + ${this.hass.localize( + "ui.panel.lovelace.cards.energy.energy_distribution.go_to_energy_dashboard" + )} +
    ` diff --git a/src/panels/lovelace/cards/energy/hui-energy-gas-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-gas-graph-card.ts index 4175f55bee..8f31da9fc1 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-gas-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-gas-graph-card.ts @@ -110,8 +110,10 @@ export class HuiEnergyGasGraphCard ${!this._chartData.datasets.length ? html`
    ${isToday(this._start) - ? "There is no data to show. It can take up to 2 hours for new data to arrive after you configure your energy dashboard." - : "There is no data for this period."} + ? this.hass.localize("ui.panel.lovelace.cards.energy.no_data") + : this.hass.localize( + "ui.panel.lovelace.cards.energy.no_data_period" + )}
    ` : ""}
    @@ -173,6 +175,7 @@ export class HuiEnergyGasGraphCard offset: true, }, y: { + stacked: true, type: "linear", title: { display: true, @@ -304,6 +307,7 @@ export class HuiEnergyGasGraphCard borderColor, backgroundColor: borderColor + "7F", data: gasConsumptionData, + stack: "gas", }); } diff --git a/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts index 1f7e9d3f94..cc26d82a0d 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts @@ -18,12 +18,10 @@ import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../../../types"; import type { LovelaceCard } from "../../types"; import type { EnergyGridGaugeCardConfig } from "../types"; -import { severityMap } from "../hui-gauge-card"; const LEVELS: LevelDefinition[] = [ - { level: -1, stroke: severityMap.red }, - { level: -0.2, stroke: severityMap.yellow }, - { level: 0, stroke: severityMap.green }, + { level: -1, stroke: "var(--energy-grid-consumption-color)" }, + { level: 0, stroke: "var(--energy-grid-return-color)" }, ]; @customElement("hui-energy-grid-neutrality-gauge-card") @@ -61,7 +59,9 @@ class HuiEnergyGridGaugeCard } if (!this._data) { - return html`Loading...`; + return html`${this.hass.localize( + "ui.panel.lovelace.cards.energy.loading" + )}`; } const prefs = this._data.prefs; @@ -102,11 +102,13 @@ class HuiEnergyGridGaugeCard - This card represents your energy dependency. + ${this.hass.localize( + "ui.panel.lovelace.cards.energy.grid_neutrality_gauge.energy_dependency" + )}

    - If it's green, it means you produced more energy than that you - consumed from the grid. If it's in the red, it means that you - relied on the grid for part of your home's energy consumption. + ${this.hass.localize( + "ui.panel.lovelace.cards.energy.grid_neutrality_gauge.color_explain" + )}
    @@ -126,11 +128,17 @@ class HuiEnergyGridGaugeCard >
    ${returnedToGrid! >= consumedFromGrid! - ? "Net returned to the grid" - : "Net consumed from the grid"} + ? this.hass.localize( + "ui.panel.lovelace.cards.energy.grid_neutrality_gauge.net_returned_grid" + ) + : this.hass.localize( + "ui.panel.lovelace.cards.energy.grid_neutrality_gauge.net_consumed_grid" + )}
    ` - : "Grid neutrality could not be calculated"} + : this.hass.localize( + "ui.panel.lovelace.cards.energy.grid_neutrality_gauge.grid_neutrality_not_calculated" + )} `; } diff --git a/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts index 6f54631658..578ae00fd5 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts @@ -54,7 +54,9 @@ class HuiEnergySolarGaugeCard } if (!this._data) { - return html`Loading...`; + return html`${this.hass.localize( + "ui.panel.lovelace.cards.energy.loading" + )}`; } const prefs = this._data.prefs; @@ -91,13 +93,13 @@ class HuiEnergySolarGaugeCard - This card indicates how much of the solar energy you produced - was used by your home instead of being returned to the grid. + ${this.hass.localize( + "ui.panel.lovelace.cards.energy.solar_consumed_gauge.card_indicates_solar_energy_used" + )}

    - If this number is typically very low, indicating excess solar - production, you might want to consider charging a home battery - or electric car from your solar panels at times of high solar - production. + ${this.hass.localize( + "ui.panel.lovelace.cards.energy.solar_consumed_gauge.card_indicates_solar_energy_used_charge_home_bat" + )}
    -
    Self-consumed solar energy
    +
    + ${this.hass.localize( + "ui.panel.lovelace.cards.energy.solar_consumed_gauge.self_consumed_solar_energy" + )} +
    ` : totalSolarProduction === 0 - ? "You have not produced any solar energy" - : "Self-consumed solar energy couldn't be calculated"} + ? this.hass.localize( + "ui.panel.lovelace.cards.energy.solar_consumed_gauge.not_produced_solar_energy" + ) + : this.hass.localize( + "ui.panel.lovelace.cards.energy.solar_consumed_gauge.self_consumed_solar_could_not_calc" + )} `; } diff --git a/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts index fb595bf124..de57958829 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts @@ -108,8 +108,10 @@ export class HuiEnergySolarGraphCard ${!this._chartData.datasets.length ? html`
    ${isToday(this._start) - ? "There is no data to show. It can take up to 2 hours for new data to arrive after you configure your energy dashboard." - : "There is no data for this period."} + ? this.hass.localize("ui.panel.lovelace.cards.energy.no_data") + : this.hass.localize( + "ui.panel.lovelace.cards.energy.no_data_period" + )}
    ` : ""} @@ -166,6 +168,7 @@ export class HuiEnergySolarGraphCard offset: true, }, y: { + stacked: true, type: "linear", title: { display: true, @@ -306,12 +309,16 @@ export class HuiEnergySolarGraphCard if (solarProductionData.length) { data.push({ - label: `Production ${ - entity ? computeStateName(entity) : source.stat_energy_from - }`, + label: this.hass.localize( + "ui.panel.lovelace.cards.energy.energy_solar_graph.production", + { + name: entity ? computeStateName(entity) : source.stat_energy_from, + } + ), borderColor, backgroundColor: borderColor + "7F", data: solarProductionData, + stack: "solar", }); } @@ -361,9 +368,14 @@ export class HuiEnergySolarGraphCard if (solarForecastData.length) { data.push({ type: "line", - label: `Forecast ${ - entity ? computeStateName(entity) : source.stat_energy_from - }`, + label: this.hass.localize( + "ui.panel.lovelace.cards.energy.energy_solar_graph.forecast", + { + name: entity + ? computeStateName(entity) + : source.stat_energy_from, + } + ), fill: false, stepped: false, borderColor: computedStyles.getPropertyValue( diff --git a/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts b/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts index e91ecd9fee..bd6a0f3756 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts @@ -69,7 +69,9 @@ export class HuiEnergySourcesTableCard } if (!this._data) { - return html`Loading...`; + return html`${this.hass.localize( + "ui.panel.lovelace.cards.energy.loading" + )}`; } let totalGrid = 0; @@ -134,14 +136,18 @@ export class HuiEnergySourcesTableCard role="columnheader" scope="col" > - Source + ${this.hass.localize( + "ui.panel.lovelace.cards.energy.energy_sources_table.source" + )} - Energy + ${this.hass.localize( + "ui.panel.lovelace.cards.energy.energy_sources_table.energy" + )} ${showCosts ? html` - Cost + ${this.hass.localize( + "ui.panel.lovelace.cards.energy.energy_sources_table.cost" + )} ` : ""} @@ -290,7 +298,9 @@ export class HuiEnergySourcesTableCard ? html` - Battery total + ${this.hass.localize( + "ui.panel.lovelace.cards.energy.energy_sources_table.battery_total" + )} - Grid total + + ${this.hass.localize( + "ui.panel.lovelace.cards.energy.energy_sources_table.grid_total" + )} + @@ -524,7 +538,9 @@ export class HuiEnergySourcesTableCard ? html` - Total costs + ${this.hass.localize( + "ui.panel.lovelace.cards.energy.energy_sources_table.total_costs" + )} dataset.data.length) ? html`
    ${isToday(this._start) - ? "There is no data to show. It can take up to 2 hours for new data to arrive after you configure your energy dashboard." - : "There is no data for this period."} + ? this.hass.localize("ui.panel.lovelace.cards.energy.no_data") + : this.hass.localize( + "ui.panel.lovelace.cards.energy.no_data_period" + )}
    ` : ""} @@ -204,16 +205,16 @@ export class HuiEnergyUsageGraphCard } return [ totalConsumed - ? `Total consumed: ${formatNumber( - totalConsumed, - locale - )} kWh` + ? this.hass.localize( + "ui.panel.lovelace.cards.energy.energy_usage_graph.total_consumed", + { num: formatNumber(totalConsumed, locale) } + ) : "", totalReturned - ? `Total returned: ${formatNumber( - totalReturned, - locale - )} kWh` + ? this.hass.localize( + "ui.panel.lovelace.cards.energyenergy_usage_graph.total_returned", + { num: formatNumber(totalReturned, locale) } + ) : "", ].filter(Boolean); }, @@ -344,15 +345,17 @@ export class HuiEnergyUsageGraphCard .trim(), }; const labels = { - used_grid: "Combined from grid", - used_solar: "Consumed solar", - used_battery: "Consumed battery", + used_grid: this.hass.localize( + "ui.panel.lovelace.cards.energy.energy_usage_graph.combined_from_grid" + ), + used_solar: this.hass.localize( + "ui.panel.lovelace.cards.energy.energy_usage_graph.consumed_solar" + ), + used_battery: this.hass.localize( + "ui.panel.lovelace.cards.energy.energy_usage_graph.consumed_battery" + ), }; - const backgroundColor = computedStyles - .getPropertyValue("--card-background-color") - .trim(); - Object.entries(statistics).forEach(([key, statIds]) => { const sum = [ "solar", @@ -507,7 +510,7 @@ export class HuiEnergyUsageGraphCard ? Object.keys(combinedData).length : idx + 1, borderColor, - backgroundColor: hexBlend(borderColor, backgroundColor, 50), + backgroundColor: borderColor + "7F", stack: "stack", data: [], }); diff --git a/src/panels/lovelace/cards/hui-alarm-panel-card.ts b/src/panels/lovelace/cards/hui-alarm-panel-card.ts index 8565b47cdd..370add6330 100644 --- a/src/panels/lovelace/cards/hui-alarm-panel-card.ts +++ b/src/panels/lovelace/cards/hui-alarm-panel-card.ts @@ -8,17 +8,18 @@ import { PropertyValues, TemplateResult, } from "lit"; -import { customElement, property, state, query } from "lit/decorators"; +import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { fireEvent } from "../../../common/dom/fire_event"; import { alarmPanelIcon } from "../../../common/entity/alarm_panel_icon"; import "../../../components/ha-card"; -import "../../../components/ha-label-badge"; +import "../../../components/ha-chip"; import { callAlarmAction, FORMAT_NUMBER, } from "../../../data/alarm_control_panel"; +import { UNAVAILABLE } from "../../../data/entity"; import type { HomeAssistant } from "../../../types"; import { findEntities } from "../common/find-entities"; import { createEntityNotFoundWarning } from "../components/hui-warning"; @@ -144,19 +145,24 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard { `; } + const stateLabel = this._stateDisplay(stateObj.state); + return html` - - - - + +

    + ${this._config.name || + stateObj.attributes.friendly_name || + stateLabel} + + + + ${stateLabel} + +

    ${(stateObj.state === "disarmed" ? this._config.states! @@ -215,23 +221,16 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard { `; } - private _stateIconLabel(entityState: string): string { - const stateLabel = entityState.split("_").pop(); - return stateLabel === "disarmed" || - stateLabel === "triggered" || - !stateLabel - ? "" - : this._stateDisplay(entityState); - } - private _actionDisplay(entityState: string): string { return this.hass!.localize(`ui.card.alarm_control_panel.${entityState}`); } private _stateDisplay(entityState: string): string { - return this.hass!.localize( - `component.alarm_control_panel.state._.${entityState}` - ); + return entityState === UNAVAILABLE + ? this.hass!.localize("state.default.unavailable") + : this.hass!.localize( + `component.alarm_control_panel.state._.${entityState}` + ) || entityState; } private _handlePadClick(e: MouseEvent): void { @@ -273,15 +272,20 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard { --alarm-state-color: var(--alarm-color-armed); } - ha-label-badge { - --ha-label-badge-color: var(--alarm-state-color); - --label-badge-text-color: var(--alarm-state-color); - --label-badge-background-color: var(--card-background-color); - color: var(--alarm-state-color); - position: absolute; - right: 12px; - top: 8px; - cursor: pointer; + ha-chip { + --ha-chip-background-color: var(--alarm-state-color); + --ha-chip-text-color: var(--text-primary-color); + line-height: initial; + } + + .card-header { + display: flex; + justify-content: space-between; + align-items: center; + } + + .unavailable { + --alarm-state-color: var(--state-unavailable-color); } .disarmed { diff --git a/src/panels/lovelace/cards/hui-button-card.ts b/src/panels/lovelace/cards/hui-button-card.ts index 6893279f1a..a490f9277c 100644 --- a/src/panels/lovelace/cards/hui-button-card.ts +++ b/src/panels/lovelace/cards/hui-button-card.ts @@ -26,7 +26,6 @@ import { computeDomain } from "../../../common/entity/compute_domain"; 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 { isValidEntityId } from "../../../common/entity/valid_entity_id"; import { iconColorCSS } from "../../../common/style/icon_color_css"; import "../../../components/ha-card"; @@ -167,7 +166,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard { > ${this._config.show_icon ? html` - + > ` : ""} ${this._config.show_name @@ -270,18 +269,18 @@ export class HuiButtonCard extends LitElement implements LovelaceCard { outline: none; } - ha-icon { + ha-state-icon { width: 40%; height: auto; color: var(--paper-item-icon-color, #44739e); --mdc-icon-size: 100%; } - ha-icon + span { + ha-state-icon + span { margin-top: 8px; } - ha-icon, + ha-state-icon, span { outline: none; } diff --git a/src/panels/lovelace/cards/hui-calendar-card.ts b/src/panels/lovelace/cards/hui-calendar-card.ts index 84436c04b4..8b868dc255 100644 --- a/src/panels/lovelace/cards/hui-calendar-card.ts +++ b/src/panels/lovelace/cards/hui-calendar-card.ts @@ -12,7 +12,6 @@ import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_elemen import { HASSDomEvent } from "../../../common/dom/fire_event"; import { debounce } from "../../../common/util/debounce"; import "../../../components/ha-card"; -import "../../../components/ha-icon"; import { Calendar, fetchCalendarEvents } from "../../../data/calendar"; import type { CalendarEvent, diff --git a/src/panels/lovelace/cards/hui-entity-card.ts b/src/panels/lovelace/cards/hui-entity-card.ts index d63d721a05..6e3e3d4728 100644 --- a/src/panels/lovelace/cards/hui-entity-card.ts +++ b/src/panels/lovelace/cards/hui-entity-card.ts @@ -14,7 +14,6 @@ import { computeActiveState } from "../../../common/entity/compute_active_state" 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 { isValidEntityId } from "../../../common/entity/valid_entity_id"; import { formatNumber } from "../../../common/number/format_number"; import { iconColorCSS } from "../../../common/style/icon_color_css"; @@ -122,8 +121,9 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
    ${name}
    - + >
    diff --git a/src/panels/lovelace/cards/hui-glance-card.ts b/src/panels/lovelace/cards/hui-glance-card.ts index fd1528c329..445bd5ff7a 100644 --- a/src/panels/lovelace/cards/hui-glance-card.ts +++ b/src/panels/lovelace/cards/hui-glance-card.ts @@ -321,6 +321,7 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard { .hass=${this.hass} .ts=${new Date(stateObj.state)} .format=${entityConf.format} + capitalize > ` : entityConf.show_last_changed diff --git a/src/panels/lovelace/cards/hui-humidifier-card.ts b/src/panels/lovelace/cards/hui-humidifier-card.ts index b1337abf0a..9baf03d160 100644 --- a/src/panels/lovelace/cards/hui-humidifier-card.ts +++ b/src/panels/lovelace/cards/hui-humidifier-card.ts @@ -1,3 +1,4 @@ +import { mdiDotsVertical } from "@mdi/js"; import "@thomasloven/round-slider"; import { HassEntity } from "home-assistant-js-websocket"; import { @@ -158,7 +159,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard { return html` + ${this.hass!.localize( + "ui.panel.lovelace.cards.iframe.error_secure_context", + { + target_protocol, + context_protocol: location.protocol, + } + )} + + `; + } + return html`
    - - - + >
    @@ -131,7 +132,6 @@ export class HuiLightCard extends LitElement implements LovelaceCard { "state-on": stateObj.state === "on", "state-unavailable": stateObj.state === UNAVAILABLE, })}" - .icon=${this._config.icon || stateIcon(stateObj)} .disabled=${UNAVAILABLE_STATES.includes(stateObj.state)} style=${styleMap({ filter: this._computeBrightness(stateObj), @@ -143,7 +143,12 @@ export class HuiLightCard extends LitElement implements LovelaceCard { hasDoubleClick: hasAction(this._config!.double_tap_action), })} tabindex="0" - > + > + +
    diff --git a/src/panels/lovelace/cards/hui-map-card.ts b/src/panels/lovelace/cards/hui-map-card.ts index d660dae680..9efb669f41 100644 --- a/src/panels/lovelace/cards/hui-map-card.ts +++ b/src/panels/lovelace/cards/hui-map-card.ts @@ -137,13 +137,14 @@ class HuiMapCard extends LitElement implements LovelaceCard { .paths=${this._getHistoryPaths(this._config, this._history)} .darkMode=${this._config.dark_mode} > - - - + >
    `; @@ -365,7 +366,7 @@ class HuiMapCard extends LitElement implements LovelaceCard { background: inherit; } - mwc-icon-button { + ha-icon-button { position: absolute; top: 75px; left: 3px; diff --git a/src/panels/lovelace/cards/hui-markdown-card.ts b/src/panels/lovelace/cards/hui-markdown-card.ts index 6f8cc9fde9..0f91a15b34 100644 --- a/src/panels/lovelace/cards/hui-markdown-card.ts +++ b/src/panels/lovelace/cards/hui-markdown-card.ts @@ -171,6 +171,7 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard { } ha-markdown { padding: 0 16px 16px; + word-wrap: break-word; } ha-markdown.no-header { padding-top: 16px; diff --git a/src/panels/lovelace/cards/hui-media-control-card.ts b/src/panels/lovelace/cards/hui-media-control-card.ts index 7a223f7bb0..28f279019d 100644 --- a/src/panels/lovelace/cards/hui-media-control-card.ts +++ b/src/panels/lovelace/cards/hui-media-control-card.ts @@ -1,7 +1,6 @@ -import "@material/mwc-icon-button"; -import { mdiPlayBoxMultiple } from "@mdi/js"; -import "@polymer/paper-progress/paper-progress"; -import type { PaperProgressElement } from "@polymer/paper-progress/paper-progress"; +import "@material/mwc-linear-progress/mwc-linear-progress"; +import type { LinearProgress } from "@material/mwc-linear-progress/mwc-linear-progress"; +import { mdiDotsVertical, mdiPlayBoxMultiple } from "@mdi/js"; import { css, CSSResultGroup, @@ -10,20 +9,18 @@ import { PropertyValues, TemplateResult, } from "lit"; -import { customElement, property, state, query } from "lit/decorators"; +import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { styleMap } from "lit/directives/style-map"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { fireEvent } from "../../../common/dom/fire_event"; import { computeStateName } from "../../../common/entity/compute_state_name"; -import { stateIcon } from "../../../common/entity/state_icon"; import { supportsFeature } from "../../../common/entity/supports-feature"; import { extractColors } from "../../../common/image/extract_color"; import { debounce } from "../../../common/util/debounce"; import "../../../components/ha-card"; -import "../../../components/ha-icon"; import "../../../components/ha-icon-button"; -import "../../../components/ha-svg-icon"; +import "../../../components/ha-state-icon"; import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog"; import { UNAVAILABLE_STATES } from "../../../data/entity"; import { @@ -84,7 +81,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { @state() private _cardHeight = 0; - @query("paper-progress") private _progressBar?: PaperProgressElement; + @query("mwc-linear-progress") private _progressBar?: LinearProgress; @state() private _marqueeActive = false; @@ -229,7 +226,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { >
    - +
    ${this._config!.name || computeStateName(this.hass!.states[this._config!.entity])} @@ -237,7 +234,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
    @@ -271,27 +268,26 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { ${controls!.map( (control) => html` + > + ` )} ${supportsFeature(stateObj, SUPPORT_BROWSE_MEDIA) ? html` - + > ` : ""}
    @@ -300,17 +296,18 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { ${!this._showProgressBar ? "" : html` - + > + `}
    ` @@ -512,8 +509,10 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { } private _updateProgressBar(): void { - if (this._progressBar) { - this._progressBar.value = getCurrentProgress(this._stateObj!); + if (this._progressBar && this._stateObj?.attributes.media_duration) { + this._progressBar.progress = + getCurrentProgress(this._stateObj) / + this._stateObj!.attributes.media_duration; } } @@ -528,12 +527,10 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { return; } - const progressWidth = ( - this.shadowRoot!.querySelector("paper-progress") as HTMLElement - ).offsetWidth; + const progressWidth = (this._progressBar as HTMLElement).offsetWidth; const percent = e.offsetX / progressWidth; - const position = (e.currentTarget! as any).max * percent; + const position = this._stateObj!.attributes.media_duration! * percent; this.hass!.callService("media_player", "media_seek", { entity_id: this._config!.entity, @@ -692,7 +689,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { --mdc-icon-size: 40px; } - mwc-icon-button.browse-media { + ha-icon-button.browse-media { position: absolute; right: 4px; --mdc-icon-size: 24px; @@ -709,7 +706,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { align-items: center; } - .icon-name ha-icon { + .icon-name ha-state-icon { padding-right: 8px; } @@ -734,12 +731,10 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { padding-top: 16px; } - paper-progress { + mwc-linear-progress { width: 100%; - height: var(--paper-progress-height, 4px); margin-top: 4px; - border-radius: calc(var(--paper-progress-height, 4px) / 2); - --paper-progress-container-color: rgba(200, 200, 200, 0.5); + --mdc-linear-progress-buffer-color: rgba(200, 200, 200, 0.5); } .no-image .controls { diff --git a/src/panels/lovelace/cards/hui-picture-glance-card.ts b/src/panels/lovelace/cards/hui-picture-glance-card.ts index 4a29f5679f..c46d669a9f 100644 --- a/src/panels/lovelace/cards/hui-picture-glance-card.ts +++ b/src/panels/lovelace/cards/hui-picture-glance-card.ts @@ -14,9 +14,9 @@ import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_elemen import { computeDomain } from "../../../common/entity/compute_domain"; import { computeStateDisplay } from "../../../common/entity/compute_state_display"; import { computeStateName } from "../../../common/entity/compute_state_name"; -import { stateIcon } from "../../../common/entity/state_icon"; import "../../../components/ha-card"; import "../../../components/ha-icon-button"; +import "../../../components/ha-state-icon"; import { ActionHandlerEvent } from "../../../data/lovelace"; import { HomeAssistant } from "../../../types"; import { actionHandler } from "../common/directives/action-handler-directive"; @@ -252,13 +252,18 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard { class=${classMap({ "state-on": !STATES_OFF.has(stateObj.state), })} - .icon=${entityConf.icon || stateIcon(stateObj)} title=${`${computeStateName(stateObj)} : ${computeStateDisplay( this.hass!.localize, stateObj, this.hass!.locale )}`} - > + > + + + ${this._config!.show_state !== true && entityConf.show_state !== true ? html`
    ` : html` diff --git a/src/panels/lovelace/cards/hui-plant-status-card.ts b/src/panels/lovelace/cards/hui-plant-status-card.ts index ed3dd31f25..010388ccf0 100644 --- a/src/panels/lovelace/cards/hui-plant-status-card.ts +++ b/src/panels/lovelace/cards/hui-plant-status-card.ts @@ -1,3 +1,9 @@ +import { + mdiEmoticonPoop, + mdiThermometer, + mdiWater, + mdiWhiteBalanceSunny, +} from "@mdi/js"; import { HassEntity } from "home-assistant-js-websocket"; import { css, @@ -10,9 +16,10 @@ import { import { customElement, property, state } from "lit/decorators"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { fireEvent } from "../../../common/dom/fire_event"; +import { batteryIcon } from "../../../common/entity/battery_icon"; import { computeStateName } from "../../../common/entity/compute_state_name"; import "../../../components/ha-card"; -import "../../../components/ha-icon"; +import "../../../components/ha-svg-icon"; import { HomeAssistant } from "../../../types"; import { actionHandler } from "../common/directives/action-handler-directive"; import { findEntities } from "../common/find-entities"; @@ -21,12 +28,12 @@ import { createEntityNotFoundWarning } from "../components/hui-warning"; import { LovelaceCard, LovelaceCardEditor } from "../types"; import { PlantAttributeTarget, PlantStatusCardConfig } from "./types"; -const SENSORS = { - moisture: "hass:water", - temperature: "hass:thermometer", - brightness: "hass:white-balance-sunny", - conductivity: "hass:emoticon-poop", - battery: "hass:battery", +const SENSOR_ICONS = { + moisture: mdiWater, + temperature: mdiThermometer, + brightness: mdiWhiteBalanceSunny, + conductivity: mdiEmoticonPoop, + battery: undefined, }; @customElement("hui-plant-status-card") @@ -132,9 +139,9 @@ class HuiPlantStatusCard extends LitElement implements LovelaceCard { .value=${item} >
    - +
    key in stateObj.attributes); + return Object.keys(SENSOR_ICONS).filter( + (key) => key in stateObj.attributes + ); } private computeIcon(attr: string, batLvl: number): string { - const icon = SENSORS[attr]; if (attr === "battery") { - if (batLvl <= 5) { - return `${icon}-alert`; - } - if (batLvl < 95) { - return `${icon}-${Math.round(batLvl / 10 - 0.01) * 10}`; - } + return batteryIcon(batLvl); } - return icon; + return SENSOR_ICONS[attr]; } private _handleMoreInfo(ev: Event): void { diff --git a/src/panels/lovelace/cards/hui-shopping-list-card.ts b/src/panels/lovelace/cards/hui-shopping-list-card.ts index 25ed95dd0e..7063c86a67 100644 --- a/src/panels/lovelace/cards/hui-shopping-list-card.ts +++ b/src/panels/lovelace/cards/hui-shopping-list-card.ts @@ -1,5 +1,4 @@ import { mdiDrag, mdiNotificationClearAll, mdiPlus, mdiSort } from "@mdi/js"; -import "@polymer/paper-checkbox/paper-checkbox"; import { PaperInputElement } from "@polymer/paper-input/paper-input"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { @@ -16,7 +15,8 @@ import { guard } from "lit/directives/guard"; import { repeat } from "lit/directives/repeat"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import "../../../components/ha-card"; -import "../../../components/ha-icon"; +import "../../../components/ha-svg-icon"; +import "../../../components/ha-checkbox"; import { addItem, clearItems, @@ -178,12 +178,12 @@ class HuiShoppingListCard (item) => html`
    - + @change=${this._completeItem} + > html`
    - + @change=${this._completeItem} + > - - - + >
    @@ -414,10 +424,11 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { + .path=${modeIcons[mode]} + > + `; } diff --git a/src/panels/lovelace/cards/hui-weather-forecast-card.ts b/src/panels/lovelace/cards/hui-weather-forecast-card.ts index 7e6acae221..0d599a383a 100644 --- a/src/panels/lovelace/cards/hui-weather-forecast-card.ts +++ b/src/panels/lovelace/cards/hui-weather-forecast-card.ts @@ -12,12 +12,11 @@ import { formatTime } from "../../../common/datetime/format_time"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { computeStateDisplay } from "../../../common/entity/compute_state_display"; import { computeStateName } from "../../../common/entity/compute_state_name"; -import { stateIcon } from "../../../common/entity/state_icon"; import { isValidEntityId } from "../../../common/entity/valid_entity_id"; import { formatNumber } from "../../../common/number/format_number"; import { debounce } from "../../../common/util/debounce"; import "../../../components/ha-card"; -import "../../../components/ha-icon"; +import "../../../components/ha-svg-icon"; import { UNAVAILABLE } from "../../../data/entity"; import { ActionHandlerEvent } from "../../../data/lovelace"; import { @@ -88,7 +87,14 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { } public getCardSize(): number { - return this._config?.show_forecast !== false ? 5 : 2; + let cardSize = 0; + if (this._config?.show_current !== false) { + cardSize += 2; + } + if (this._config?.show_forecast !== false) { + cardSize += 3; + } + return cardSize; } public setConfig(config: WeatherForecastCardConfig): void { @@ -169,6 +175,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { stateObj.attributes.forecast?.length ? stateObj.attributes.forecast.slice(0, this._veryVeryNarrow ? 3 : 5) : undefined; + const weather = !forecast || this._config?.show_current !== false; let hourly: boolean | undefined; let dayNight: boolean | undefined; @@ -203,74 +210,81 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { hasAction(this._config.tap_action) ? "0" : undefined )} > -
    -
    - ${weatherStateIcon || - html` - - `} -
    -
    -
    -
    - ${computeStateDisplay( - this.hass.localize, - stateObj, - this.hass.locale - )} -
    -
    ${name}
    -
    -
    -
    - ${formatNumber( - stateObj.attributes.temperature, - this.hass.locale - )} ${getWeatherUnit(this.hass, "temperature")} -
    -
    - ${this._config.secondary_info_attribute !== undefined - ? html` - ${this._config.secondary_info_attribute in - weatherAttrIcons + ${weather + ? html` +
    +
    + ${weatherStateIcon || + html` + + `} +
    +
    +
    +
    + ${computeStateDisplay( + this.hass.localize, + stateObj, + this.hass.locale + )} +
    +
    ${name}
    +
    +
    +
    + ${formatNumber( + stateObj.attributes.temperature, + this.hass.locale + )} ${getWeatherUnit(this.hass, "temperature")} +
    +
    + ${this._config.secondary_info_attribute !== undefined ? html` - + ${this._config.secondary_info_attribute in + weatherAttrIcons + ? html` + + ` + : this.hass!.localize( + `ui.card.weather.attributes.${this._config.secondary_info_attribute}` + )} + ${this._config.secondary_info_attribute === + "wind_speed" + ? getWind( + this.hass, + stateObj.attributes.wind_speed, + stateObj.attributes.wind_bearing + ) + : html` + ${formatNumber( + stateObj.attributes[ + this._config.secondary_info_attribute + ], + this.hass.locale + )} + ${getWeatherUnit( + this.hass, + this._config.secondary_info_attribute + )} + `} ` - : this.hass!.localize( - `ui.card.weather.attributes.${this._config.secondary_info_attribute}` - )} - ${this._config.secondary_info_attribute === "wind_speed" - ? getWind( - this.hass, - stateObj.attributes.wind_speed, - stateObj.attributes.wind_bearing - ) - : html` - ${formatNumber( - stateObj.attributes[ - this._config.secondary_info_attribute - ], - this.hass.locale - )} - ${getWeatherUnit( - this.hass, - this._config.secondary_info_attribute - )} - `} - ` - : getSecondaryWeatherAttribute(this.hass, stateObj)} + : getSecondaryWeatherAttribute(this.hass, stateObj)} +
    +
    +
    -
    -
    -
    + ` + : ""} ${forecast ? html`
    diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index ac4c4d6289..80a70b4378 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -387,6 +387,7 @@ export interface ThermostatCardConfig extends LovelaceCardConfig { export interface WeatherForecastCardConfig extends LovelaceCardConfig { entity: string; name?: string; + show_current?: boolean; show_forecast?: boolean; secondary_info_attribute?: string; theme?: string; diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts index 7ed2709528..2c7b5d757e 100644 --- a/src/panels/lovelace/common/generate-lovelace-config.ts +++ b/src/panels/lovelace/common/generate-lovelace-config.ts @@ -3,6 +3,7 @@ import { computeDomain } from "../../../common/entity/compute_domain"; import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { splitByGroups } from "../../../common/entity/split_by_groups"; +import { stripPrefixFromEntityName } from "../../../common/entity/strip_prefix_from_entity_name"; import { stringCompare } from "../../../common/string/compare"; import { LocalizeFunc } from "../../../common/translations/localize"; import type { AreaRegistryEntry } from "../../../data/area_registry"; @@ -19,7 +20,6 @@ import { AlarmPanelCardConfig, EntitiesCardConfig, HumidifierCardConfig, - LightCardConfig, PictureEntityCardConfig, ThermostatCardConfig, } from "../cards/types"; @@ -83,8 +83,7 @@ const splitByAreas = ( export const computeCards = ( states: Array<[string, HassEntity?]>, - entityCardOptions: Partial, - single = false + entityCardOptions: Partial ): LovelaceCardConfig[] => { const cards: LovelaceCardConfig[] = []; @@ -92,7 +91,7 @@ export const computeCards = ( const entities: Array = []; const titlePrefix = entityCardOptions.title - ? `${entityCardOptions.title} ` + ? `${entityCardOptions.title} `.toLowerCase() : undefined; for (const [entityId, stateObj] of states) { @@ -122,12 +121,6 @@ export const computeCards = ( entity: entityId, }; cards.push(cardConfig); - } else if (domain === "light" && single) { - const cardConfig: LightCardConfig = { - type: "light", - entity: entityId, - }; - cards.push(cardConfig); } else if (domain === "media_player") { const cardConfig = { type: "media-control", @@ -153,16 +146,18 @@ export const computeCards = ( ) { // Do nothing. } else { - let name: string; + let name: string | undefined; const entityConf = titlePrefix && stateObj && // eslint-disable-next-line no-cond-assign - (name = computeStateName(stateObj)) !== titlePrefix && - name.startsWith(titlePrefix) + (name = stripPrefixFromEntityName( + computeStateName(stateObj), + titlePrefix + )) ? { entity: entityId, - name: adjustName(name.substr(titlePrefix.length)), + name, } : entityId; @@ -181,14 +176,6 @@ export const computeCards = ( return cards; }; -const hasUpperCase = (str: string): boolean => str.toLowerCase() !== str; - -const adjustName = (name: string): string => - // If first word already has an upper case letter (e.g. from brand name) - // leave as-is, otherwise capitalize the first word. - hasUpperCase(name.substr(0, name.indexOf(" "))) - ? name - : name[0].toUpperCase() + name.slice(1); const computeDefaultViewStates = ( entities: HassEntities, entityEntries: EntityRegistryEntry[] @@ -196,7 +183,9 @@ const computeDefaultViewStates = ( const states = {}; const hiddenEntities = new Set( entityEntries - .filter((entry) => HIDE_PLATFORM.has(entry.platform)) + .filter( + (entry) => entry.entity_category || HIDE_PLATFORM.has(entry.platform) + ) .map((entry) => entry.entity_id) ); diff --git a/src/panels/lovelace/components/hui-card-options.ts b/src/panels/lovelace/components/hui-card-options.ts index 63290da1de..aef797e278 100644 --- a/src/panels/lovelace/components/hui-card-options.ts +++ b/src/panels/lovelace/components/hui-card-options.ts @@ -1,5 +1,4 @@ import "@material/mwc-button"; -import "@material/mwc-icon-button"; import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import "@material/mwc-list/mwc-list-item"; import { mdiArrowDown, mdiArrowUp, mdiDotsVertical } from "@mdi/js"; @@ -14,6 +13,7 @@ import { import { customElement, property, queryAssignedNodes } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-button-menu"; +import "../../../components/ha-icon-button"; import { saveConfig } from "../../../data/lovelace"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { HomeAssistant } from "../../../types"; @@ -60,36 +60,34 @@ export class HuiCardOptions extends LitElement { >
    - - - - + + > - - - - + .path=${mdiDotsVertical} + > ${this.hass!.localize( "ui.panel.lovelace.editor.edit_card.move" @@ -137,11 +135,11 @@ export class HuiCardOptions extends LitElement { align-items: center; } - mwc-icon-button { + ha-icon-button { color: var(--primary-text-color); } - mwc-icon-button.move-arrow[disabled] { + ha-icon-button.move-arrow[disabled] { color: var(--disabled-text-color); } diff --git a/src/panels/lovelace/components/hui-energy-period-selector.ts b/src/panels/lovelace/components/hui-energy-period-selector.ts index d2515d7dd9..0ac5bab267 100644 --- a/src/panels/lovelace/components/hui-energy-period-selector.ts +++ b/src/panels/lovelace/components/hui-energy-period-selector.ts @@ -1,21 +1,22 @@ +import "@material/mwc-button/mwc-button"; import { mdiChevronLeft, mdiChevronRight } from "@mdi/js"; import { - endOfToday, addDays, - endOfDay, - startOfToday, - endOfWeek, - endOfMonth, - startOfDay, - startOfWeek, - startOfMonth, addMonths, addWeeks, - startOfYear, addYears, + differenceInDays, + endOfDay, + endOfMonth, + endOfToday, + endOfWeek, endOfYear, isWithinInterval, - differenceInDays, + startOfDay, + startOfMonth, + startOfToday, + startOfWeek, + startOfYear, } from "date-fns"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; @@ -26,21 +27,12 @@ import { formatDateShort, formatDateYear, } from "../../../common/datetime/format_date"; +import { toggleAttribute } from "../../../common/dom/toggle_attribute"; +import "../../../components/ha-button-toggle-group"; +import "../../../components/ha-icon-button"; import { EnergyData, getEnergyDataCollection } from "../../../data/energy"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { HomeAssistant, ToggleButton } from "../../../types"; -import "@material/mwc-icon-button/mwc-icon-button"; -import "../../../components/ha-svg-icon"; -import "@material/mwc-button/mwc-button"; -import "../../../components/ha-button-toggle-group"; -import { toggleAttribute } from "../../../common/dom/toggle_attribute"; - -const viewButtons: ToggleButton[] = [ - { label: "Day", value: "day" }, - { label: "Week", value: "week" }, - { label: "Month", value: "month" }, - { label: "Year", value: "year" }, -]; @customElement("hui-energy-period-selector") export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { @@ -72,6 +64,33 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { return html``; } + const viewButtons: ToggleButton[] = [ + { + label: this.hass.localize( + "ui.panel.lovelace.components.energy_period_selector.day" + ), + value: "day", + }, + { + label: this.hass.localize( + "ui.panel.lovelace.components.energy_period_selector.week" + ), + value: "week", + }, + { + label: this.hass.localize( + "ui.panel.lovelace.components.energy_period_selector.month" + ), + value: "month", + }, + { + label: this.hass.localize( + "ui.panel.lovelace.components.energy_period_selector.year" + ), + value: "year", + }, + ]; + return html`
    @@ -88,14 +107,24 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { this._endDate || new Date(), this.hass.locale )}`} - - - - - - + + - Today + ${this.hass.localize( + "ui.panel.lovelace.components.energy_period_selector.today" + )}
    @@ -232,7 +261,7 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { --mdc-button-disabled-ink-color: var(--disabled-text-color); --mdc-icon-button-ripple-opacity: 0.2; } - mwc-icon-button { + ha-icon-button { --mdc-icon-button-size: 28px; } ha-button-toggle-group { diff --git a/src/panels/lovelace/components/hui-entity-editor.ts b/src/panels/lovelace/components/hui-entity-editor.ts index 0393d59ea9..ff81d8ec67 100644 --- a/src/panels/lovelace/components/hui-entity-editor.ts +++ b/src/panels/lovelace/components/hui-entity-editor.ts @@ -10,10 +10,6 @@ import { import { customElement, property, state } from "lit/decorators"; import { guard } from "lit/directives/guard"; import type { SortableEvent } from "sortablejs"; -import Sortable, { - AutoScroll, - OnSpill, -} from "sortablejs/modular/sortable.core.esm"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/entity/ha-entity-picker"; import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker"; @@ -22,6 +18,8 @@ import { sortableStyles } from "../../../resources/ha-sortable-style"; import { HomeAssistant } from "../../../types"; import { EntityConfig } from "../entity-rows/types"; +let Sortable; + @customElement("hui-entity-editor") export class HuiEntityEditor extends LitElement { @property({ attribute: false }) protected hass?: HomeAssistant; @@ -34,7 +32,7 @@ export class HuiEntityEditor extends LitElement { @state() private _renderEmptySortable = false; - private _sortable?: Sortable; + private _sortable?; public connectedCallback() { super.connectedCallback(); @@ -86,11 +84,6 @@ export class HuiEntityEditor extends LitElement { `; } - protected firstUpdated(): void { - Sortable.mount(OnSpill); - Sortable.mount(new AutoScroll()); - } - protected updated(changedProps: PropertyValues): void { super.updated(changedProps); @@ -128,7 +121,17 @@ export class HuiEntityEditor extends LitElement { this._renderEmptySortable = false; } - private _createSortable() { + private async _createSortable() { + if (!Sortable) { + const sortableImport = await import( + "sortablejs/modular/sortable.core.esm" + ); + + Sortable = sortableImport.Sortable; + Sortable.mount(sortableImport.OnSpill); + Sortable.mount(sortableImport.AutoScroll()); + } + this._sortable = new Sortable(this.shadowRoot!.querySelector(".entities"), { animation: 150, fallbackClass: "sortable-fallback", diff --git a/src/panels/lovelace/components/hui-generic-entity-row.ts b/src/panels/lovelace/components/hui-generic-entity-row.ts index b1554bfcdd..88094f7dd9 100644 --- a/src/panels/lovelace/components/hui-generic-entity-row.ts +++ b/src/panels/lovelace/components/hui-generic-entity-row.ts @@ -15,7 +15,6 @@ import { computeDomain } from "../../../common/entity/compute_domain"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeRTL } from "../../../common/util/compute_rtl"; import "../../../components/entity/state-badge"; -import "../../../components/ha-icon"; import "../../../components/ha-relative-time"; import { ActionHandlerEvent } from "../../../data/lovelace"; import { HomeAssistant } from "../../../types"; diff --git a/src/panels/lovelace/components/hui-input-list-editor.ts b/src/panels/lovelace/components/hui-input-list-editor.ts index 5cead88e75..fe53e72c11 100644 --- a/src/panels/lovelace/components/hui-input-list-editor.ts +++ b/src/panels/lovelace/components/hui-input-list-editor.ts @@ -1,3 +1,4 @@ +import { mdiClose } from "@mdi/js"; import "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; @@ -33,9 +34,10 @@ export class HuiInputListEditor extends LitElement { >Clear diff --git a/src/panels/lovelace/components/hui-timestamp-display.ts b/src/panels/lovelace/components/hui-timestamp-display.ts index 211ff97268..2ffbba65ce 100644 --- a/src/panels/lovelace/components/hui-timestamp-display.ts +++ b/src/panels/lovelace/components/hui-timestamp-display.ts @@ -4,6 +4,7 @@ import { formatDate } from "../../../common/datetime/format_date"; import { formatDateTime } from "../../../common/datetime/format_date_time"; import { formatTime } from "../../../common/datetime/format_time"; import { relativeTime } from "../../../common/datetime/relative_time"; +import { capitalizeFirstLetter } from "../../../common/string/capitalize-first-letter"; import { FrontendLocaleData } from "../../../data/translation"; import { HomeAssistant } from "../../../types"; import { TimestampRenderingFormat } from "./types"; @@ -25,6 +26,8 @@ class HuiTimestampDisplay extends LitElement { @property() public format?: TimestampRenderingFormat; + @property({ type: Boolean }) public capitalize = false; + @state() private _relative?: string; private _connected?: boolean; @@ -105,6 +108,10 @@ class HuiTimestampDisplay extends LitElement { this._format === "relative" ? relativeTime(this.ts, this.hass!.locale) : relativeTime(new Date(), this.hass!.locale, this.ts, false); + + this._relative = this.capitalize + ? capitalizeFirstLetter(this._relative) + : this._relative; } } } diff --git a/src/panels/lovelace/components/hui-views-list.ts b/src/panels/lovelace/components/hui-views-list.ts deleted file mode 100644 index a04e3f0cba..0000000000 --- a/src/panels/lovelace/components/hui-views-list.ts +++ /dev/null @@ -1,93 +0,0 @@ -import "@polymer/paper-item/paper-icon-item"; -import "@polymer/paper-listbox/paper-listbox"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, state } from "lit/decorators"; -import { fireEvent } from "../../../common/dom/fire_event"; -import { toggleAttribute } from "../../../common/dom/toggle_attribute"; -import "../../../components/ha-icon"; -import { LovelaceConfig } from "../../../data/lovelace"; - -declare global { - interface HASSDomEvents { - "view-selected": { - view: number; - }; - } -} - -@customElement("hui-views-list") -class HuiViewsList extends LitElement { - @state() private lovelaceConfig?: LovelaceConfig | undefined; - - @state() private selected?: number | undefined; - - protected render(): TemplateResult { - if (!this.lovelaceConfig) { - return html``; - } - - return html` - - ${this.lovelaceConfig.views.map( - (view, index) => html` - - ${view.icon - ? html` - - ` - : ""} - ${view.title || view.path || "Unnamed view"} - - ` - )} - - `; - } - - protected updated(changedProps) { - super.updated(changedProps); - toggleAttribute( - this, - "hide-icons", - this.lovelaceConfig - ? !this.lovelaceConfig.views.some((view) => view.icon) - : true - ); - } - - private async _handlePickView(ev: Event) { - const view = Number((ev.currentTarget as any).getAttribute("data-index")); - fireEvent(this, "view-selected", { view }); - } - - static get styles(): CSSResultGroup { - return css` - paper-listbox { - padding-top: 0; - } - - paper-listbox ha-icon { - padding: 12px; - color: var(--secondary-text-color); - } - - paper-icon-item { - cursor: pointer; - } - - paper-icon-item[disabled] { - cursor: initial; - } - - :host([hide-icons]) paper-icon-item { - --paper-item-icon-width: 0px; - } - `; - } -} - -declare global { - interface HTMLElementTagNameMap { - "hui-views-list": HuiViewsList; - } -} diff --git a/src/panels/lovelace/editor/add-entities-to-view.ts b/src/panels/lovelace/editor/add-entities-to-view.ts index cd6ccb873a..42ad5ac06d 100644 --- a/src/panels/lovelace/editor/add-entities-to-view.ts +++ b/src/panels/lovelace/editor/add-entities-to-view.ts @@ -13,8 +13,10 @@ import { showSelectViewDialog } from "./select-view/show-select-view-dialog"; export const addEntitiesToLovelaceView = async ( element: HTMLElement, hass: HomeAssistant, - entities: string[] + entities: string[], + cardTitle?: string ) => { + hass.loadFragmentTranslation("lovelace"); const dashboards = await fetchDashboards(hass); const storageDashs = dashboards.filter( @@ -30,6 +32,7 @@ export const addEntitiesToLovelaceView = async ( showSuggestCardDialog(element, { entities, yaml: true, + cardTitle, }); return; } @@ -68,6 +71,7 @@ export const addEntitiesToLovelaceView = async ( showSuggestCardDialog(element, { entities, yaml: true, + cardTitle, }); } else { // all storage dashboards are generated @@ -87,6 +91,7 @@ export const addEntitiesToLovelaceView = async ( if (!storageDashs.length && lovelaceConfig.views.length === 1) { showSuggestCardDialog(element, { + cardTitle, lovelaceConfig: lovelaceConfig!, saveConfig: async (newConfig: LovelaceConfig): Promise => { try { @@ -107,9 +112,11 @@ export const addEntitiesToLovelaceView = async ( lovelaceConfig, urlPath, allowDashboardChange: true, + actionLabel: hass.localize("ui.common.next"), dashboards, viewSelectedCallback: (newUrlPath, selectedDashConfig, viewIndex) => { showSuggestCardDialog(element, { + cardTitle, lovelaceConfig: selectedDashConfig, saveConfig: async (newConfig: LovelaceConfig): Promise => { try { diff --git a/src/panels/lovelace/editor/card-editor/hui-card-picker.ts b/src/panels/lovelace/editor/card-editor/hui-card-picker.ts index 882cc7266b..a3c9f8ec85 100644 --- a/src/panels/lovelace/editor/card-editor/hui-card-picker.ts +++ b/src/panels/lovelace/editor/card-editor/hui-card-picker.ts @@ -98,6 +98,7 @@ export class HuiCardPicker extends LitElement { return html` { this._params = params; this._GUImode = true; @@ -92,6 +95,9 @@ export class HuiDialogEditCard } public closeDialog(): boolean { + this._isEscapeEnabled = true; + window.removeEventListener("dialog-closed", this._enableEscapeKeyClose); + window.removeEventListener("hass-more-info", this._disableEscapeKeyClose); if (this._dirty) { this._confirmCancel(); return false; @@ -124,6 +130,16 @@ export class HuiDialogEditCard } } + private _enableEscapeKeyClose = (ev: any) => { + if (ev.detail.dialog === "ha-more-info-dialog") { + this._isEscapeEnabled = true; + } + }; + + private _disableEscapeKeyClose = () => { + this._isEscapeEnabled = false; + }; + protected render(): TemplateResult { if (!this._params) { return html``; @@ -156,6 +172,7 @@ export class HuiDialogEditCard - - - + ` : ""} @@ -281,6 +296,8 @@ export class HuiDialogEditCard } private _opened() { + window.addEventListener("dialog-closed", this._enableEscapeKeyClose); + window.addEventListener("hass-more-info", this._disableEscapeKeyClose); this._cardEditorEl?.focusYamlEditor(); } diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts index 7116d09d05..142e82b234 100755 --- a/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts +++ b/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts @@ -1,9 +1,7 @@ -import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; import deepFreeze from "deep-freeze"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; -import "../../../../components/dialog/ha-paper-dialog"; import "../../../../components/ha-yaml-editor"; import type { HaYamlEditor } from "../../../../components/ha-yaml-editor"; import { LovelaceCardConfig } from "../../../../data/lovelace"; @@ -37,8 +35,9 @@ export class HuiDialogSuggestCard extends LitElement { entityId, this.hass.states[entityId], ]), - {}, - true + { + title: params.cardTitle, + } ); if (!Object.isFrozen(this._cardConfig)) { this._cardConfig = deepFreeze(this._cardConfig); diff --git a/src/panels/lovelace/editor/card-editor/hui-entity-picker-table.ts b/src/panels/lovelace/editor/card-editor/hui-entity-picker-table.ts index 3a5791e8e3..b08cdbc539 100644 --- a/src/panels/lovelace/editor/card-editor/hui-entity-picker-table.ts +++ b/src/panels/lovelace/editor/card-editor/hui-entity-picker-table.ts @@ -28,6 +28,7 @@ export class HuiEntityPickerTable extends LitElement { protected render(): TemplateResult { return html` void; diff --git a/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts index 2105938beb..c843f2b52b 100644 --- a/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts @@ -1,3 +1,4 @@ +import { mdiClose } from "@mdi/js"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; @@ -6,7 +7,7 @@ import { customElement, property, state } from "lit/decorators"; import { array, assert, assign, object, optional, string } from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/entity/ha-entity-picker"; -import "../../../../components/ha-icon"; +import "../../../../components/ha-svg-icon"; import { HomeAssistant } from "../../../../types"; import { AlarmPanelCardConfig } from "../../cards/types"; import "../../components/hui-theme-select-editor"; @@ -73,11 +74,11 @@ export class HuiAlarmPanelCardEditor return html`
    html`
    ${entityState} - + >
    ` )} @@ -144,7 +145,7 @@ export class HuiAlarmPanelCardEditor .states:hover > .deleteState { visibility: visible; } - ha-icon { + ha-svg-icon { padding-top: 12px; } `, diff --git a/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts index 48439e3d3e..417c4f327e 100644 --- a/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts @@ -3,10 +3,9 @@ import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { assert, boolean, object, optional, string, assign } from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; -import { stateIcon } from "../../../../common/entity/state_icon"; import { computeRTLDirection } from "../../../../common/util/compute_rtl"; import "../../../../components/ha-formfield"; -import "../../../../components/ha-icon-input"; +import "../../../../components/ha-icon-picker"; import "../../../../components/ha-switch"; import { ActionConfig } from "../../../../data/lovelace"; import { HomeAssistant } from "../../../../types"; @@ -19,6 +18,8 @@ import { actionConfigStruct } from "../structs/action-struct"; import { EditorTarget } from "../types"; import { configElementStyle } from "./config-elements-style"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; +import { computeDomain } from "../../../../common/entity/compute_domain"; +import { domainIcon } from "../../../../common/entity/domain_icon"; const cardConfigStruct = assign( baseLovelaceCardConfig, @@ -107,6 +108,7 @@ export class HuiButtonCardEditor } const dir = computeRTLDirection(this.hass!); + const entityState = this.hass.states[this._entity]; return html`
    @@ -133,18 +135,22 @@ export class HuiButtonCardEditor .configValue=${"name"} @value-changed=${this._valueChanged} > - + >
    diff --git a/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts index af18dd6edd..845743d300 100644 --- a/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts @@ -3,9 +3,10 @@ import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { assert, assign, boolean, object, optional, string } from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; -import { stateIcon } from "../../../../common/entity/state_icon"; +import { computeDomain } from "../../../../common/entity/compute_domain"; +import { domainIcon } from "../../../../common/entity/domain_icon"; import "../../../../components/entity/ha-entity-attribute-picker"; -import "../../../../components/ha-icon-input"; +import "../../../../components/ha-icon-picker"; import { HomeAssistant } from "../../../../types"; import { EntityCardConfig } from "../../cards/types"; import "../../components/hui-action-editor"; @@ -77,6 +78,7 @@ export class HuiEntityCardEditor if (!this.hass || !this._config) { return html``; } + const entityState = this.hass.states[this._entity]; return html`
    @@ -103,18 +105,22 @@ export class HuiEntityCardEditor .configValue=${"name"} @value-changed=${this._valueChanged} > - + >
    @@ -83,15 +84,20 @@ export class HuiGenericEntityRowEditor .configValue=${"name"} @value-changed=${this._valueChanged} > - + >
    - + >
    - + > - - - + > - - - + > - - - + >
    -
    -
    +
    +
    - + + + +
    @@ -143,7 +204,20 @@ export class HuiWeatherForecastCardEditor return; } if (target.configValue) { - if (target.value === "") { + if (target.configValue.startsWith("show_")) { + this._config = { ...this._config }; + if (target.configValue === "show_both") { + /* delete since true is default */ + delete this._config.show_current; + delete this._config.show_forecast; + } else if (target.configValue === "show_current") { + delete this._config.show_current; + this._config.show_forecast = false; + } else if (target.configValue === "show_forecast") { + delete this._config.show_forecast; + this._config.show_current = false; + } + } else if (target.value === "") { this._config = { ...this._config }; delete this._config[target.configValue!]; } else { diff --git a/src/panels/lovelace/editor/header-footer-editor/hui-header-footer-editor.ts b/src/panels/lovelace/editor/header-footer-editor/hui-header-footer-editor.ts index 5ee72ba281..76b7032a8f 100644 --- a/src/panels/lovelace/editor/header-footer-editor/hui-header-footer-editor.ts +++ b/src/panels/lovelace/editor/header-footer-editor/hui-header-footer-editor.ts @@ -1,11 +1,10 @@ -import "@material/mwc-icon-button/mwc-icon-button"; import { mdiClose, mdiPencil, mdiPlus } from "@mdi/js"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; -import "../../../../components/ha-svg-icon"; +import "../../../../components/ha-icon-button"; import type { LovelaceConfig } from "../../../../data/lovelace"; import type { HomeAssistant } from "../../../../types"; import type { LovelaceHeaderFooterConfig } from "../../header-footer/types"; @@ -38,35 +37,32 @@ export class HuiHeaderFooterEditor extends LitElement {
    ${!this.config?.type ? html` - - - + > ` : html` - - - - + - - + > `}
    `; @@ -118,7 +114,7 @@ export class HuiHeaderFooterEditor extends LitElement { align-items: center; } - mwc-icon-button, + ha-icon-button, .header-footer-icon { --mdc-icon-button-size: 36px; color: var(--secondary-text-color); diff --git a/src/panels/lovelace/editor/hui-dialog-save-config.ts b/src/panels/lovelace/editor/hui-dialog-save-config.ts index fe2e6fad39..5b2a00fc09 100644 --- a/src/panels/lovelace/editor/hui-dialog-save-config.ts +++ b/src/panels/lovelace/editor/hui-dialog-save-config.ts @@ -1,5 +1,4 @@ import "@material/mwc-button"; -import "@material/mwc-icon-button/mwc-icon-button"; import { mdiHelpCircle } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -8,7 +7,7 @@ import { computeRTLDirection } from "../../../common/util/compute_rtl"; import "../../../components/ha-circular-progress"; import "../../../components/ha-dialog"; import "../../../components/ha-formfield"; -import "../../../components/ha-svg-icon"; +import "../../../components/ha-icon-button"; import "../../../components/ha-switch"; import "../../../components/ha-yaml-editor"; import type { LovelaceConfig } from "../../../data/lovelace"; @@ -67,9 +66,10 @@ export class HuiSaveConfig extends LitElement implements HassDialog { rel="noreferrer" dir=${computeRTLDirection(this.hass!)} > - - - + `} >
    @@ -197,21 +197,6 @@ export class HuiSaveConfig extends LitElement implements HassDialog { return [ haStyleDialog, css` - @media all and (max-width: 450px), all and (max-height: 500px) { - /* overrule the ha-style-dialog max-height on small screens */ - ha-paper-dialog { - max-height: 100%; - height: 100%; - } - } - @media all and (min-width: 660px) { - ha-paper-dialog { - width: 650px; - } - } - ha-paper-dialog { - max-width: 650px; - } ha-switch { padding-bottom: 16px; } diff --git a/src/panels/lovelace/editor/hui-element-editor.ts b/src/panels/lovelace/editor/hui-element-editor.ts index 68e0d1875c..3fa6aa3d87 100644 --- a/src/panels/lovelace/editor/hui-element-editor.ts +++ b/src/panels/lovelace/editor/hui-element-editor.ts @@ -15,6 +15,7 @@ import { computeRTL } from "../../../common/util/compute_rtl"; import { deepEqual } from "../../../common/util/deep-equal"; import "../../../components/ha-circular-progress"; import "../../../components/ha-code-editor"; +import "../../../components/ha-alert"; import type { HaCodeEditor } from "../../../components/ha-code-editor"; import type { LovelaceCardConfig, @@ -229,9 +230,12 @@ export abstract class HuiElementEditor extends LitElement { : ""} ${this.hasWarning ? html` -
    - ${this.hass.localize("ui.errors.config.editor_not_supported")}: -
    + ${this._warnings!.length > 0 && this._warnings![0] !== undefined ? html`
      ${this._warnings!.map( @@ -240,7 +244,7 @@ export abstract class HuiElementEditor extends LitElement {
    ` : ""} ${this.hass.localize("ui.errors.config.edit_in_yaml_supported")} -
    + ` : ""}
    diff --git a/src/panels/lovelace/editor/hui-entities-card-row-editor.ts b/src/panels/lovelace/editor/hui-entities-card-row-editor.ts index 9095c6b1b1..e9e8f5d92c 100644 --- a/src/panels/lovelace/editor/hui-entities-card-row-editor.ts +++ b/src/panels/lovelace/editor/hui-entities-card-row-editor.ts @@ -1,4 +1,3 @@ -import "@material/mwc-icon-button"; import { mdiClose, mdiDrag, mdiPencil } from "@mdi/js"; import { css, @@ -11,18 +10,17 @@ import { import { customElement, property, state } from "lit/decorators"; import { guard } from "lit/directives/guard"; import type { SortableEvent } from "sortablejs"; -import Sortable, { - AutoScroll, - OnSpill, -} from "sortablejs/modular/sortable.core.esm"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/entity/ha-entity-picker"; import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker"; +import "../../../components/ha-icon-button"; import "../../../components/ha-svg-icon"; import { sortableStyles } from "../../../resources/ha-sortable-style"; import { HomeAssistant } from "../../../types"; import { EntityConfig, LovelaceRowConfig } from "../entity-rows/types"; +let Sortable; + declare global { interface HASSDomEvents { "entities-changed": { @@ -43,7 +41,7 @@ export class HuiEntitiesCardRowEditor extends LitElement { @state() private _renderEmptySortable = false; - private _sortable?: Sortable; + private _sortable?; public connectedCallback() { super.connectedCallback(); @@ -104,26 +102,24 @@ export class HuiEntitiesCardRowEditor extends LitElement { @value-changed=${this._valueChanged} > `} - - - - + - - + >
    ` ) @@ -136,11 +132,6 @@ export class HuiEntitiesCardRowEditor extends LitElement { `; } - protected firstUpdated(): void { - Sortable.mount(OnSpill); - Sortable.mount(new AutoScroll()); - } - protected updated(changedProps: PropertyValues): void { super.updated(changedProps); @@ -178,7 +169,17 @@ export class HuiEntitiesCardRowEditor extends LitElement { this._renderEmptySortable = false; } - private _createSortable() { + private async _createSortable() { + if (!Sortable) { + const sortableImport = await import( + "sortablejs/modular/sortable.core.esm" + ); + + Sortable = sortableImport.Sortable; + Sortable.mount(sortableImport.OnSpill); + Sortable.mount(sortableImport.AutoScroll()); + } + this._sortable = new Sortable(this.shadowRoot!.querySelector(".entities"), { animation: 150, fallbackClass: "sortable-fallback", diff --git a/src/panels/lovelace/editor/hui-sub-element-editor.ts b/src/panels/lovelace/editor/hui-sub-element-editor.ts index e243aee060..eeae816645 100644 --- a/src/panels/lovelace/editor/hui-sub-element-editor.ts +++ b/src/panels/lovelace/editor/hui-sub-element-editor.ts @@ -1,10 +1,9 @@ import "@material/mwc-button"; -import "@material/mwc-icon-button"; import { mdiArrowLeft } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state, query } from "lit/decorators"; import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event"; -import "../../../components/ha-svg-icon"; +import "../../../components/ha-icon-button"; import type { HomeAssistant } from "../../../types"; import type { LovelaceRowConfig } from "../entity-rows/types"; import type { LovelaceHeaderFooterConfig } from "../header-footer/types"; @@ -37,9 +36,11 @@ export class HuiSubElementEditor extends LitElement { return html`
    - - - + ${this.hass.localize( `ui.panel.lovelace.editor.sub-element-editor.types.${this.config?.type}` diff --git a/src/panels/lovelace/editor/lovelace-editor/hui-dialog-edit-lovelace.ts b/src/panels/lovelace/editor/lovelace-editor/hui-dialog-edit-lovelace.ts index 7465d3d757..606d822967 100644 --- a/src/panels/lovelace/editor/lovelace-editor/hui-dialog-edit-lovelace.ts +++ b/src/panels/lovelace/editor/lovelace-editor/hui-dialog-edit-lovelace.ts @@ -1,16 +1,14 @@ import "@material/mwc-button"; -import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; -import "../../../../components/dialog/ha-paper-dialog"; -import type { HaPaperDialog } from "../../../../components/dialog/ha-paper-dialog"; import "../../../../components/ha-circular-progress"; import type { LovelaceConfig } from "../../../../data/lovelace"; import { haStyleDialog } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; import type { Lovelace } from "../../types"; import "./hui-lovelace-editor"; +import "../../../../components/ha-dialog"; @customElement("hui-dialog-edit-lovelace") export class HuiDialogEditLovelace extends LitElement { @@ -20,44 +18,31 @@ export class HuiDialogEditLovelace extends LitElement { private _config?: LovelaceConfig; - private _saving: boolean; + private _saving = false; - public constructor() { - super(); - this._saving = false; - } - - public async showDialog(lovelace: Lovelace): Promise { + public showDialog(lovelace: Lovelace): void { this._lovelace = lovelace; - if (this._dialog == null) { - await this.updateComplete; - } - const { views, ...lovelaceConfig } = this._lovelace!.config; this._config = lovelaceConfig as LovelaceConfig; - - this._dialog.open(); } public closeDialog(): void { this._config = undefined; - this._dialog.close(); fireEvent(this, "dialog-closed", { dialog: this.localName }); } - private get _dialog(): HaPaperDialog { - return this.shadowRoot!.querySelector("ha-paper-dialog")!; - } - protected render(): TemplateResult { + if (!this._config) { + return html``; + } return html` - -

    - ${this.hass!.localize( - "ui.panel.lovelace.editor.edit_lovelace.header" - )} -

    - + +
    ${this.hass!.localize( "ui.panel.lovelace.editor.edit_lovelace.explanation" )} @@ -65,27 +50,26 @@ export class HuiDialogEditLovelace extends LitElement { .hass=${this.hass} .config=${this._config} @lovelace-config-changed=${this._ConfigChanged} - > -
    - ${this.hass!.localize("ui.common.cancel")} - - ${this._saving - ? html`` - : ""} - ${this.hass!.localize("ui.common.save")} + >
    - + + ${this.hass!.localize("ui.common.cancel")} + + + ${this._saving + ? html`` + : ""} + ${this.hass!.localize("ui.common.save")} + `; } @@ -128,26 +112,7 @@ export class HuiDialogEditLovelace extends LitElement { } static get styles(): CSSResultGroup { - return [ - haStyleDialog, - css` - @media all and (max-width: 450px), all and (max-height: 500px) { - /* overrule the ha-style-dialog max-height on small screens */ - ha-paper-dialog { - max-height: 100%; - height: 100%; - } - } - @media all and (min-width: 660px) { - ha-paper-dialog { - width: 650px; - } - } - ha-paper-dialog { - max-width: 650px; - } - `, - ]; + return haStyleDialog; } } diff --git a/src/panels/lovelace/editor/select-view/hui-dialog-select-view.ts b/src/panels/lovelace/editor/select-view/hui-dialog-select-view.ts index 2ea6f32c6b..ff555d8e89 100644 --- a/src/panels/lovelace/editor/select-view/hui-dialog-select-view.ts +++ b/src/panels/lovelace/editor/select-view/hui-dialog-select-view.ts @@ -1,9 +1,12 @@ +import "@material/mwc-list/mwc-list"; +import "@material/mwc-list/mwc-radio-list-item"; import "@polymer/paper-item/paper-item"; +import "@polymer/paper-listbox/paper-listbox"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; -import "../../../../components/dialog/ha-paper-dialog"; import { createCloseHeading } from "../../../../components/ha-dialog"; +import "../../../../components/ha-icon"; import "../../../../components/ha-paper-dropdown-menu"; import { fetchConfig, @@ -13,9 +16,16 @@ import { } from "../../../../data/lovelace"; import { haStyleDialog } from "../../../../resources/styles"; import { HomeAssistant } from "../../../../types"; -import "../../components/hui-views-list"; import type { SelectViewDialogParams } from "./show-select-view-dialog"; +declare global { + interface HASSDomEvents { + "view-selected": { + view: number; + }; + } +} + @customElement("hui-dialog-select-view") export class HuiDialogSelectView extends LitElement { public hass!: HomeAssistant; @@ -28,6 +38,8 @@ export class HuiDialogSelectView extends LitElement { @state() private _config?: LovelaceConfig; + @state() private _selectedViewIdx = 0; + public showDialog(params: SelectViewDialogParams): void { this._config = params.lovelaceConfig; this._urlPath = params.urlPath; @@ -50,7 +62,6 @@ export class HuiDialogSelectView extends LitElement { ` : ""} ${this._config - ? html` - ` + ? this._config.views.length > 1 + ? html` + + ${this._config.views.map( + (view, idx) => html` + icon) + ? "icon" + : null} + @click=${this._viewChanged} + .value=${idx.toString()} + .selected=${this._selectedViewIdx === idx} + > + ${view.title} + + + ` + )} + + ` + : "" : html`
    No config found.
    `} + + ${this.hass!.localize("ui.common.cancel")} + + + ${this._params.actionLabel || this.hass!.localize("ui.common.move")} +
    `; } @@ -118,6 +151,7 @@ export class HuiDialogSelectView extends LitElement { urlPath = null; } this._urlPath = urlPath; + this._selectedViewIdx = 0; try { this._config = await fetchConfig(this.hass.connection, urlPath, false); } catch (err: any) { @@ -125,9 +159,21 @@ export class HuiDialogSelectView extends LitElement { } } - private _selectView(e: CustomEvent): void { - const view: number = e.detail.view; - this._params!.viewSelectedCallback(this._urlPath!, this._config!, view); + private _viewChanged(e) { + const view = Number(e.target.value); + + if (!isNaN(view)) { + this._selectedViewIdx = view; + } + } + + private _selectView(): void { + fireEvent(this, "view-selected", { view: this._selectedViewIdx }); + this._params!.viewSelectedCallback( + this._urlPath!, + this._config!, + this._selectedViewIdx + ); this.closeDialog(); } diff --git a/src/panels/lovelace/editor/select-view/show-select-view-dialog.ts b/src/panels/lovelace/editor/select-view/show-select-view-dialog.ts index 05d393c850..4287711de0 100644 --- a/src/panels/lovelace/editor/select-view/show-select-view-dialog.ts +++ b/src/panels/lovelace/editor/select-view/show-select-view-dialog.ts @@ -7,6 +7,7 @@ export interface SelectViewDialogParams { dashboards?: LovelaceDashboard[]; urlPath?: string | null; header?: string; + actionLabel?: string; viewSelectedCallback: ( urlPath: string | null, config: LovelaceConfig, diff --git a/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts b/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts index 9f76adeadc..b261b3c549 100644 --- a/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts +++ b/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts @@ -8,7 +8,6 @@ import { navigate } from "../../../../common/navigate"; import "../../../../components/ha-circular-progress"; import "../../../../components/ha-dialog"; import "../../../../components/ha-alert"; -import "../../../../components/ha-icon-button"; import type { LovelaceBadgeConfig, LovelaceCardConfig, diff --git a/src/panels/lovelace/editor/view-editor/hui-view-editor.ts b/src/panels/lovelace/editor/view-editor/hui-view-editor.ts index f290c8e9c2..40da6d5d4e 100644 --- a/src/panels/lovelace/editor/view-editor/hui-view-editor.ts +++ b/src/panels/lovelace/editor/view-editor/hui-view-editor.ts @@ -4,7 +4,7 @@ import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; import { slugify } from "../../../../common/string/slugify"; import "../../../../components/ha-formfield"; -import "../../../../components/ha-icon-input"; +import "../../../../components/ha-icon-picker"; import "../../../../components/ha-switch"; import { LovelaceViewConfig } from "../../../../data/lovelace"; import { HomeAssistant } from "../../../../types"; @@ -94,7 +94,7 @@ export class HuiViewEditor extends LitElement { @value-changed=${this._valueChanged} @blur=${this._handleTitleBlur} > - + > ` @@ -117,7 +130,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow { supportsFeature(stateObj, SUPPORT_PAUSE))) ? html` ` @@ -126,7 +139,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow { supportsFeature(stateObj, SUPPORT_NEXT_TRACK) ? html` ` @@ -148,7 +161,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow { !UNAVAILABLE_STATES.includes(entityState) ? html` ` @@ -161,7 +174,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow { !UNAVAILABLE_STATES.includes(entityState) ? html` ` @@ -177,9 +190,9 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow { ${supportsFeature(stateObj, SUPPORT_VOLUME_MUTE) ? html` ` @@ -200,11 +213,11 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow { supportsFeature(stateObj, SUPPORT_VOLUME_BUTTONS) ? html` ` @@ -238,12 +251,12 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow { private _computeControlIcon(stateObj: HassEntity): string { return stateObj.state === "on" - ? "hass:play-pause" + ? mdiPlayPause : stateObj.state !== "playing" - ? "hass:play" + ? mdiPlay : supportsFeature(stateObj, SUPPORT_PAUSE) - ? "hass:pause" - : "hass:stop"; + ? mdiPause + : mdiStop; } private _togglePower(): void { diff --git a/src/panels/lovelace/entity-rows/hui-number-entity-row.ts b/src/panels/lovelace/entity-rows/hui-number-entity-row.ts index 339244c1b1..65b306137a 100644 --- a/src/panels/lovelace/entity-rows/hui-number-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-number-entity-row.ts @@ -81,7 +81,11 @@ class HuiNumberEntityRow extends LitElement implements LovelaceRow { return html` - ${stateObj.attributes.mode === "slider" + ${stateObj.attributes.mode === "slider" || + (stateObj.attributes.mode === "auto" && + (Number(stateObj.attributes.max) - Number(stateObj.attributes.min)) / + Number(stateObj.attributes.step) <= + 256) ? html`
    ` : computeStateDisplay( diff --git a/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts b/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts index 2f5c1fd115..8d06737195 100644 --- a/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts @@ -13,7 +13,6 @@ import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const"; import { computeDomain } from "../../../common/entity/compute_domain"; import { computeStateDisplay } from "../../../common/entity/compute_state_display"; import { computeStateName } from "../../../common/entity/compute_state_name"; -import { stateIcon } from "../../../common/entity/state_icon"; import { formatNumber } from "../../../common/number/format_number"; import "../../../components/entity/state-badge"; import { UNAVAILABLE_STATES } from "../../../data/entity"; @@ -89,7 +88,10 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow { > ${weatherStateIcon || html` - + `}
    ${this.hass!.localize( diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index 5ab8afac4e..61445901d7 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -47,6 +47,7 @@ import { debounce } from "../../common/util/debounce"; import { afterNextRender } from "../../common/util/render-status"; import "../../components/ha-button-menu"; import "../../components/ha-icon"; +import "../../components/ha-icon-button"; import "../../components/ha-icon-button-arrow-next"; import "../../components/ha-icon-button-arrow-prev"; import "../../components/ha-menu-button"; @@ -118,30 +119,24 @@ class HUIRoot extends LitElement { ${this._editMode ? html` - - - + >
    ${this.config.title || this.hass!.localize("ui.panel.lovelace.editor.header")} - - - + >
    - - - + .path=${mdiHelpCircle} + > - - - + .path=${mdiDotsVertical} + > ${__DEMO__ /* No unused entities available in the demo */ ? "" : html` @@ -278,28 +268,23 @@ class HUIRoot extends LitElement { ${!this.narrow && this._conversation(this.hass.config.components) ? html` - - - + > ` : ""} - - - + .path=${mdiDotsVertical} + > ${this.narrow && this._conversation(this.hass.config.components) ? html` @@ -448,9 +433,6 @@ class HUIRoot extends LitElement { ? html` - - + .path=${mdiPlus} + > ` : ""} @@ -735,7 +713,8 @@ class HUIRoot extends LitElement { }); } - private _moveViewLeft() { + private _moveViewLeft(ev) { + ev.stopPropagation(); if (this._curView === 0) { return; } @@ -746,7 +725,8 @@ class HUIRoot extends LitElement { lovelace.saveConfig(swapView(lovelace.config, oldIndex, newIndex)); } - private _moveViewRight() { + private _moveViewRight(ev) { + ev.stopPropagation(); if ((this._curView! as number) + 1 === this.lovelace!.config.views.length) { return; } diff --git a/src/panels/lovelace/special-rows/hui-attribute-row.ts b/src/panels/lovelace/special-rows/hui-attribute-row.ts index 540c7bf1ef..99e4a57907 100644 --- a/src/panels/lovelace/special-rows/hui-attribute-row.ts +++ b/src/panels/lovelace/special-rows/hui-attribute-row.ts @@ -70,6 +70,7 @@ class HuiAttributeRow extends LitElement implements LovelaceRow { .hass=${this.hass} .ts=${date} .format=${this._config.format} + capitalize >` : typeof attribute === "number" ? formatNumber(attribute, this.hass.locale) diff --git a/src/panels/lovelace/special-rows/hui-button-row.ts b/src/panels/lovelace/special-rows/hui-button-row.ts index 4292af6600..7cbbf9bb0a 100644 --- a/src/panels/lovelace/special-rows/hui-button-row.ts +++ b/src/panels/lovelace/special-rows/hui-button-row.ts @@ -4,14 +4,13 @@ import { customElement, state } from "lit/decorators"; import { DOMAINS_TOGGLE } from "../../../common/const"; import { computeDomain } from "../../../common/entity/compute_domain"; import { computeStateName } from "../../../common/entity/compute_state_name"; -import { stateIcon } from "../../../common/entity/state_icon"; -import "../../../components/ha-icon"; import { ActionHandlerEvent } from "../../../data/lovelace"; import { HomeAssistant } from "../../../types"; import { actionHandler } from "../common/directives/action-handler-directive"; import { handleAction } from "../common/handle-action"; import { hasAction } from "../common/has-action"; import { ButtonRowConfig, LovelaceRow } from "../entity-rows/types"; +import "../../../components/ha-state-icon"; @customElement("hui-button-row") export class HuiButtonRow extends LitElement implements LovelaceRow { @@ -54,11 +53,8 @@ export class HuiButtonRow extends LitElement implements LovelaceRow { this._config.name ?? (stateObj ? computeStateName(stateObj) : ""); return html` - - + +
    ${name}
    - -

    - [[localize('ui.panel.mailbox.playback_title')]] +
    - + + + +
    +
    +
    + +
    -

    -
    -
    - -
    -
    + `; } @@ -161,13 +139,8 @@ class HaDialogShowAudioMessage extends LocalizeMixin(PolymerElement) { }); } - _openedChanged(ev) { - // Closed dialog by clicking on the overlay - // Check against dialogClosedCallback to make sure we didn't change - // programmatically - if (!ev.detail.value) { - this._dialogDone(); - } + closeDialog() { + this._dialogDone(); } _showLoading(displayed) { diff --git a/src/panels/map/ha-panel-map.ts b/src/panels/map/ha-panel-map.ts index d391668c9d..89b9351473 100644 --- a/src/panels/map/ha-panel-map.ts +++ b/src/panels/map/ha-panel-map.ts @@ -1,16 +1,15 @@ import { mdiPencil } from "@mdi/js"; -import "@material/mwc-icon-button"; import "@polymer/app-layout/app-toolbar/app-toolbar"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { property } from "lit/decorators"; import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { navigate } from "../../common/navigate"; -import "../../components/ha-svg-icon"; import "../../components/ha-menu-button"; -import "../../layouts/ha-app-layout"; -import { HomeAssistant } from "../../types"; +import "../../components/ha-icon-button"; import "../../components/map/ha-map"; +import "../../layouts/ha-app-layout"; import { haStyle } from "../../resources/styles"; +import { HomeAssistant } from "../../types"; class HaPanelMap extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -30,9 +29,11 @@ class HaPanelMap extends LitElement { >
    ${this.hass.localize("panel.map")}
    ${!__DEMO__ && this.hass.user?.is_admin - ? html`` + ? html` ` : ""} diff --git a/src/panels/media-browser/ha-panel-media-browser.ts b/src/panels/media-browser/ha-panel-media-browser.ts index ebf1bfe90c..5b1cc9b0dc 100644 --- a/src/panels/media-browser/ha-panel-media-browser.ts +++ b/src/panels/media-browser/ha-panel-media-browser.ts @@ -1,4 +1,3 @@ -import "@material/mwc-icon-button"; import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-toolbar/app-toolbar"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; diff --git a/src/panels/profile/ha-change-password-card.ts b/src/panels/profile/ha-change-password-card.ts index 2bbcd54caf..4ba20e8478 100644 --- a/src/panels/profile/ha-change-password-card.ts +++ b/src/panels/profile/ha-change-password-card.ts @@ -1,5 +1,4 @@ import "@material/mwc-button"; -import "@polymer/paper-dialog/paper-dialog"; import "@polymer/paper-input/paper-input"; import { css, diff --git a/src/panels/profile/ha-long-lived-access-tokens-card.ts b/src/panels/profile/ha-long-lived-access-tokens-card.ts index dd331c6949..3fe4755b23 100644 --- a/src/panels/profile/ha-long-lived-access-tokens-card.ts +++ b/src/panels/profile/ha-long-lived-access-tokens-card.ts @@ -1,5 +1,4 @@ import "@material/mwc-button/mwc-button"; -import "@material/mwc-icon-button/mwc-icon-button"; import { mdiDelete } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; @@ -8,7 +7,7 @@ import { relativeTime } from "../../common/datetime/relative_time"; import { fireEvent } from "../../common/dom/fire_event"; import "../../components/ha-card"; import "../../components/ha-settings-row"; -import "../../components/ha-svg-icon"; +import "../../components/ha-icon-button"; import { RefreshToken } from "../../data/refresh_token"; import { showAlertDialog, @@ -72,14 +71,13 @@ class HaLongLivedTokens extends LitElement { relativeTime(new Date(token.created_at), this.hass.locale) )}
    - - - + > ` )}
    @@ -171,7 +169,7 @@ class HaLongLivedTokens extends LitElement { mwc-button { --mdc-theme-primary: var(--primary-color); } - mwc-icon-button { + ha-icon-button { color: var(--primary-text-color); } `, diff --git a/src/panels/profile/ha-pick-dashboard-row.ts b/src/panels/profile/ha-pick-dashboard-row.ts index 482ce1969b..544c7296cc 100644 --- a/src/panels/profile/ha-pick-dashboard-row.ts +++ b/src/panels/profile/ha-pick-dashboard-row.ts @@ -43,7 +43,11 @@ class HaPickDashboardRow extends LitElement { @iron-select=${this._dashboardChanged} attr-for-selected="url-path" > - default + ${this.hass.localize( + "ui.panel.profile.dashboard.default_dashboard_label" + )} ${this._dashboards.map((dashboard) => { if (!this.hass.user!.is_admin && dashboard.require_admin) { return ""; diff --git a/src/panels/profile/ha-refresh-tokens-card.ts b/src/panels/profile/ha-refresh-tokens-card.ts index 12f801978e..12f08d2bf0 100644 --- a/src/panels/profile/ha-refresh-tokens-card.ts +++ b/src/panels/profile/ha-refresh-tokens-card.ts @@ -1,4 +1,3 @@ -import "@material/mwc-icon-button/mwc-icon-button"; import { mdiDelete } from "@mdi/js"; import "@polymer/paper-tooltip/paper-tooltip"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; @@ -8,7 +7,7 @@ import { relativeTime } from "../../common/datetime/relative_time"; import { fireEvent } from "../../common/dom/fire_event"; import "../../components/ha-card"; import "../../components/ha-settings-row"; -import "../../components/ha-svg-icon"; +import "../../components/ha-icon-button"; import { RefreshToken } from "../../data/refresh_token"; import { showAlertDialog, @@ -93,14 +92,13 @@ class HaRefreshTokens extends LitElement { )} ` : ""} - - - + >
    ` ) @@ -144,7 +142,7 @@ class HaRefreshTokens extends LitElement { ha-settings-row { padding: 0; } - mwc-icon-button { + ha-icon-button { color: var(--primary-text-color); } `, diff --git a/src/panels/shopping-list/ha-panel-shopping-list.ts b/src/panels/shopping-list/ha-panel-shopping-list.ts index f46d52dcd2..a852182225 100644 --- a/src/panels/shopping-list/ha-panel-shopping-list.ts +++ b/src/panels/shopping-list/ha-panel-shopping-list.ts @@ -12,6 +12,7 @@ import { import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; +import "../../components/ha-icon-button"; import "../../components/ha-menu-button"; import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog"; import "../../layouts/ha-app-layout"; @@ -60,14 +61,13 @@ class PanelShoppingList extends LitElement {
    ${this.hass.localize("panel.shopping_list")}
    ${this._conversation(this.hass.config.components) ? html` - - - + > ` : ""} diff --git a/src/resources/compatibility.ts b/src/resources/compatibility.ts index d11f3ba97a..03a0b23fec 100644 --- a/src/resources/compatibility.ts +++ b/src/resources/compatibility.ts @@ -13,6 +13,7 @@ import "@formatjs/intl-relativetimeformat/polyfill"; import "@formatjs/intl-relativetimeformat/locale-data/en"; import "@formatjs/intl-datetimeformat/polyfill"; import "@formatjs/intl-datetimeformat/locale-data/en"; +import "@formatjs/intl-datetimeformat/add-all-tz"; // To use comlink under ES5 import "proxy-polyfill"; diff --git a/src/resources/ha-style.ts b/src/resources/ha-style.ts index a570a73c22..dffa7a2663 100644 --- a/src/resources/ha-style.ts +++ b/src/resources/ha-style.ts @@ -59,7 +59,7 @@ documentContainer.innerHTML = ` /* states */ --state-icon-color: #44739e; - /* an active state is anything that would require attention */ + /* an active state is anything that would require attention */ --state-icon-active-color: #FDD835; /* an error state is anything that would be considered an error */ /* --state-icon-error-color: #db4437; derived from error-color */ @@ -84,8 +84,8 @@ documentContainer.innerHTML = ` --state-climate-idle-color: #8a8a8a; /* energy */ - --energy-grid-consumption-color: #126a9a; - --energy-grid-return-color: #673ab7; + --energy-grid-consumption-color: #488fc2; + --energy-grid-return-color: #8353d1; --energy-solar-color: #ff9800; --energy-non-fossil-color: #0f9d58; --energy-battery-out-color: #4db6ac; @@ -112,6 +112,20 @@ documentContainer.innerHTML = ` --rgb-text-primary-color: 255, 255, 255; --rgb-card-background-color: 255, 255, 255; + /* input components */ + --input-idle-line-color: rgba(0, 0, 0, 0.42); + --input-hover-line-color: rgba(0, 0, 0, 0.87); + --input-disabled-line-color: rgba(0, 0, 0, 0.06); + --input-outlined-idle-border-color: rgba(0, 0, 0, 0.38); + --input-outlined-hover-border-color: rgba(0, 0, 0, 0.87); + --input-outlined-disabled-border-color: rgba(0, 0, 0, 0.06); + --input-fill-color: rgb(245, 245, 245); + --input-disabled-fill-color: rgb(250, 250, 250); + --input-ink-color: rgba(0, 0, 0, 0.87); + --input-label-ink-color: rgba(0, 0, 0, 0.6); + --input-disabled-ink-color: rgba(0, 0, 0, 0.37); + --input-dropdown-icon-color: rgba(0, 0, 0, 0.54); + /* Vaadin typography */ --material-h6-font-size: 1.25rem; --material-small-font-size: 0.875rem; @@ -122,25 +136,6 @@ documentContainer.innerHTML = ` .map(([key, value]) => `--${key}: ${value};`) .join("")} } - - /* - prevent clipping of positioned elements in a small scrollable - force smooth scrolling if can scroll - use non-shady selectors so this only targets iOS 9 - conditional mixin set in ha-style-dialog does not work with shadyCSS - */ - paper-dialog-scrollable:not(.can-scroll) > .scrollable { - -webkit-overflow-scrolling: auto !important; - } - - paper-dialog-scrollable.can-scroll > .scrollable { - -webkit-overflow-scrolling: touch !important; - } - - /* for paper-dialog */ - iron-overlay-backdrop { - backdrop-filter: var(--dialog-backdrop-filter, none); - } `; diff --git a/src/resources/markdown_worker.ts b/src/resources/markdown_worker.ts index b026f47dcc..9b38ad78a8 100644 --- a/src/resources/markdown_worker.ts +++ b/src/resources/markdown_worker.ts @@ -11,6 +11,28 @@ interface WhiteList { let whiteListNormal: WhiteList | undefined; let whiteListSvg: WhiteList | undefined; +// Override the default `onTagAttr` behavior to only render +// our markdown checkboxes. +// Returning undefined causes the default measure to be taken +// in the xss library. +const onTagAttr = ( + tag: string, + name: string, + value: string +): string | undefined => { + if (tag === "input") { + if ( + (name === "type" && value === "checkbox") || + name === "checked" || + name === "disabled" + ) { + return undefined; + } + return ""; + } + return undefined; +}; + const renderMarkdown = ( content: string, markedOptions: marked.MarkedOptions, @@ -22,6 +44,7 @@ const renderMarkdown = ( if (!whiteListNormal) { whiteListNormal = { ...(getDefaultWhiteList() as WhiteList), + input: ["type", "disabled", "checked"], "ha-icon": ["icon"], "ha-svg-icon": ["path"], }; @@ -45,6 +68,7 @@ const renderMarkdown = ( return filterXSS(marked(content, markedOptions), { whiteList, + onTagAttr, }); }; diff --git a/src/resources/styles.ts b/src/resources/styles.ts index 6f7cb696ab..f9c7013fd5 100644 --- a/src/resources/styles.ts +++ b/src/resources/styles.ts @@ -13,6 +13,20 @@ export const darkStyles = { "switch-unchecked-track-color": "#9b9b9b", "divider-color": "rgba(225, 225, 225, .12)", "mdc-ripple-color": "#AAAAAA", + + "input-idle-line-color": "rgba(255, 255, 255, 0.42)", + "input-hover-line-color": "rgba(255, 255, 255, 0.87)", + "input-disabled-line-color": "rgba(255, 255, 255, 0.06)", + "input-outlined-idle-border-color": "rgba(255, 255, 255, 0.38)", + "input-outlined-hover-border-color": "rgba(255, 255, 255, 0.87)", + "input-outlined-disabled-border-color": "rgba(255, 255, 255, 0.06)", + "input-fill-color": "rgba(255, 255, 255, 0.05)", + "input-disabled-fill-color": "rgba(255, 255, 255, 0.02)", + "input-ink-color": "rgba(255, 255, 255, 0.87)", + "input-label-ink-color": "rgba(255, 255, 255, 0.6)", + "input-disabled-ink-color": "rgba(255, 255, 255, 0.37)", + "input-dropdown-icon-color": "rgba(255, 255, 255, 0.54)", + "codemirror-keyword": "#C792EA", "codemirror-operator": "#89DDFF", "codemirror-variable": "#f07178", @@ -31,7 +45,7 @@ export const darkStyles = { "codemirror-property": "#C792EA", "codemirror-qualifier": "#DECB6B", "codemirror-type": "#DECB6B", - "energy-grid-return-color": "#b39bdb", + "energy-grid-return-color": "#a280db", }; export const derivedStyles = { @@ -69,6 +83,8 @@ export const derivedStyles = { "paper-slider-container-color": "var(--slider-track-color)", "data-table-background-color": "var(--card-background-color)", "markdown-code-background-color": "var(--primary-background-color)", + + // https://github.com/material-components/material-web/blob/master/docs/theming.md "mdc-theme-primary": "var(--primary-color)", "mdc-theme-secondary": "var(--accent-color)", "mdc-theme-background": "var(--primary-background-color)", @@ -80,6 +96,7 @@ export const derivedStyles = { "mdc-theme-text-primary-on-background": "var(--primary-text-color)", "mdc-theme-text-secondary-on-background": "var(--secondary-text-color)", "mdc-theme-text-icon-on-background": "var(--secondary-text-color)", + "mdc-theme-error": "var(--error-color)", "app-header-text-color": "var(--text-primary-color)", "app-header-background-color": "var(--primary-color)", "mdc-checkbox-unchecked-color": "rgba(var(--rgb-primary-text-color), 0.54)", @@ -90,6 +107,38 @@ export const derivedStyles = { "mdc-button-disabled-ink-color": "var(--disabled-text-color)", "mdc-button-outline-color": "var(--divider-color)", "mdc-dialog-scroll-divider-color": "var(--divider-color)", + + "mdc-text-field-idle-line-color": "var(--input-idle-line-color)", + "mdc-text-field-hover-line-color": "var(--input-hover-line-color)", + "mdc-text-field-disabled-line-color": "var(--input-disabled-line-color)", + "mdc-text-field-outlined-idle-border-color": + "var(--input-outlined-idle-border-color)", + "mdc-text-field-outlined-hover-border-color": + "var(--input-outlined-hover-border-color)", + "mdc-text-field-outlined-disabled-border-color": + "var(--input-outlined-disabled-border-color)", + "mdc-text-field-fill-color": "var(--input-fill-color)", + "mdc-text-field-disabled-fill-color": "var(--input-disabled-fill-color)", + "mdc-text-field-ink-color": "var(--input-ink-color)", + "mdc-text-field-label-ink-color": "var(--input-label-ink-color)", + "mdc-text-field-disabled-ink-color": "var(--input-disabled-ink-color)", + + "mdc-select-idle-line-color": "var(--input-idle-line-color)", + "mdc-select-hover-line-color": "var(--input-hover-line-color)", + "mdc-select-outlined-idle-border-color": + "var(--input-outlined-idle-border-color)", + "mdc-select-outlined-hover-border-color": + "var(--input-outlined-hover-border-color)", + "mdc-select-outlined-disabled-border-color": + "var(--input-outlined-disabled-border-color)", + "mdc-select-fill-color": "var(--input-fill-color)", + "mdc-select-disabled-fill-color": "var(--input-disabled-fill-color)", + "mdc-select-ink-color": "var(--input-ink-color)", + "mdc-select-label-ink-color": "var(--input-label-ink-color)", + "mdc-select-disabled-ink-color": "var(--input-disabled-ink-color)", + "mdc-select-dropdown-icon-color": "var(--input-dropdown-icon-color)", + "mdc-select-disabled-dropdown-icon-color": "var(--input-disabled-ink-color)", + "chip-background-color": "rgba(var(--rgb-primary-text-color), 0.15)", // Vaadin "material-body-text-color": "var(--primary-text-color)", @@ -253,57 +302,6 @@ export const haStyle = css` `; export const haStyleDialog = css` - /* prevent clipping of positioned elements */ - paper-dialog-scrollable { - --paper-dialog-scrollable: { - -webkit-overflow-scrolling: auto; - } - } - - /* force smooth scrolling for iOS 10 */ - paper-dialog-scrollable.can-scroll { - --paper-dialog-scrollable: { - -webkit-overflow-scrolling: touch; - } - } - - .paper-dialog-buttons { - align-items: flex-end; - padding: 8px; - padding-bottom: max(env(safe-area-inset-bottom), 8px); - } - - @media all and (min-width: 450px) and (min-height: 500px) { - ha-paper-dialog { - min-width: 400px; - } - } - - @media all and (max-width: 450px), all and (max-height: 500px) { - paper-dialog, - ha-paper-dialog { - margin: 0; - width: calc( - 100% - env(safe-area-inset-right) - env(safe-area-inset-left) - ) !important; - min-width: calc( - 100% - env(safe-area-inset-right) - env(safe-area-inset-left) - ) !important; - max-width: calc( - 100% - env(safe-area-inset-right) - env(safe-area-inset-left) - ) !important; - max-height: calc(100% - var(--header-height)); - - position: fixed !important; - bottom: 0px; - left: env(safe-area-inset-left); - right: env(safe-area-inset-right); - overflow: scroll; - border-bottom-left-radius: 0px; - border-bottom-right-radius: 0px; - } - } - /* mwc-dialog (ha-dialog) styles */ ha-dialog { --mdc-dialog-min-width: 400px; diff --git a/src/state-summary/state-card-display.ts b/src/state-summary/state-card-display.ts index 7bd0e49c6c..1e8a856975 100755 --- a/src/state-summary/state-card-display.ts +++ b/src/state-summary/state-card-display.ts @@ -47,6 +47,7 @@ export class StateCardDisplay extends LitElement { .hass=${this.hass} .ts=${new Date(this.stateObj.state)} format="datetime" + capitalize >` : computeStateDisplay( this.hass!.localize, diff --git a/src/state-summary/state-card-number.js b/src/state-summary/state-card-number.js index 44ffac69b8..24a3e64e0c 100644 --- a/src/state-summary/state-card-number.js +++ b/src/state-summary/state-card-number.js @@ -6,6 +6,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; /* eslint-plugin-disable lit */ import { PolymerElement } from "@polymer/polymer/polymer-element"; import "../components/entity/state-info"; +import "../components/ha-slider"; class StateCardNumber extends mixinBehaviors( [IronResizableBehavior], @@ -15,6 +16,9 @@ class StateCardNumber extends mixinBehaviors( return html`