diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 26ee55e660..17e64a518c 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,5 +1,5 @@ # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.148.1/containers/python-3/.devcontainer/base.Dockerfile -FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.10 +FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.11 ENV \ DEBIAN_FRONTEND=noninteractive \ diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml index 0b22247453..76f0415496 100644 --- a/.github/workflows/nightly.yaml +++ b/.github/workflows/nightly.yaml @@ -6,7 +6,7 @@ on: - cron: "0 1 * * *" env: - PYTHON_VERSION: "3.10" + PYTHON_VERSION: "3.11" NODE_OPTIONS: --max_old_space_size=6144 permissions: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7d943eda97..a269b30074 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -6,7 +6,7 @@ on: - published env: - PYTHON_VERSION: "3.10" + PYTHON_VERSION: "3.11" NODE_OPTIONS: --max_old_space_size=6144 # Set default workflow permissions @@ -76,7 +76,7 @@ jobs: - name: Build wheels uses: home-assistant/wheels@2023.04.0 with: - abi: cp310 + abi: cp311 tag: musllinux_1_2 arch: amd64 wheels-key: ${{ secrets.WHEELS_KEY }} diff --git a/build-scripts/webpack.cjs b/build-scripts/webpack.cjs index 0ff76cc832..111dadfc36 100644 --- a/build-scripts/webpack.cjs +++ b/build-scripts/webpack.cjs @@ -167,6 +167,8 @@ const createWebpackConfig = ({ "lit/polyfill-support$": "lit/polyfill-support.js", "@lit-labs/virtualizer/layouts/grid": "@lit-labs/virtualizer/layouts/grid.js", + "@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver": + "@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js", }, }, output: { diff --git a/gallery/src/data/date-options.ts b/gallery/src/data/date-options.ts new file mode 100644 index 0000000000..9bb856a26e --- /dev/null +++ b/gallery/src/data/date-options.ts @@ -0,0 +1,24 @@ +import type { ControlSelectOption } from "../../../src/components/ha-control-select"; + +export const timeOptions: ControlSelectOption[] = [ + { + value: "now", + label: "Now", + }, + { + value: "00:15:30", + label: "12:15:30 AM", + }, + { + value: "06:15:30", + label: "06:15:30 AM", + }, + { + value: "12:15:30", + label: "12:15:30 PM", + }, + { + value: "18:15:30", + label: "06:15:30 PM", + }, +]; diff --git a/gallery/src/pages/date-time/date-time-numeric.markdown b/gallery/src/pages/date-time/date-time-numeric.markdown new file mode 100644 index 0000000000..3310f315a1 --- /dev/null +++ b/gallery/src/pages/date-time/date-time-numeric.markdown @@ -0,0 +1,7 @@ +--- +title: Date-Time Format (Numeric) +--- + +This pages lists all supported languages with their available date-time formats. + +Formatting function: `const formatDateTimeNumeric: (dateObj: Date, locale: FrontendLocaleData) => string` \ No newline at end of file diff --git a/gallery/src/pages/date-time/date-time-numeric.ts b/gallery/src/pages/date-time/date-time-numeric.ts new file mode 100644 index 0000000000..608b3fc152 --- /dev/null +++ b/gallery/src/pages/date-time/date-time-numeric.ts @@ -0,0 +1,136 @@ +import { html, css, LitElement } from "lit"; +import { customElement, state } from "lit/decorators"; +import "../../../../src/components/ha-card"; +import "../../../../src/components/ha-control-select"; +import { translationMetadata } from "../../../../src/resources/translations-metadata"; +import { formatDateTimeNumeric } from "../../../../src/common/datetime/format_date_time"; +import { timeOptions } from "../../data/date-options"; +import { demoConfig } from "../../../../src/fake_data/demo_config"; +import { + FrontendLocaleData, + NumberFormat, + TimeFormat, + DateFormat, + FirstWeekday, + TimeZone, +} from "../../../../src/data/translation"; + +@customElement("demo-date-time-date-time-numeric") +export class DemoDateTimeDateTimeNumeric extends LitElement { + @state() private selection?: string = "now"; + + @state() private date: Date = new Date(); + + handleValueChanged(e: CustomEvent) { + this.selection = e.detail.value as string; + this.date = new Date(); + if (this.selection !== "now") { + const [hours, minutes, seconds] = this.selection.split(":").map(Number); + this.date.setHours(hours); + this.date.setMinutes(minutes); + this.date.setSeconds(seconds); + } + } + + protected render() { + const defaultLocale: FrontendLocaleData = { + language: "en", + number_format: NumberFormat.language, + time_format: TimeFormat.language, + date_format: DateFormat.language, + first_weekday: FirstWeekday.language, + time_zone: TimeZone.local, + }; + return html` + + + +
+
Language
+
Default (lang)
+
12 Hours
+
24 Hours
+
+ ${Object.entries(translationMetadata.translations) + .filter(([key, _]) => key !== "test") + .map( + ([key, value]) => html` +
+
${value.nativeName}
+
+ ${formatDateTimeNumeric( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.language, + }, + demoConfig + )} +
+
+ ${formatDateTimeNumeric( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.am_pm, + }, + demoConfig + )} +
+
+ ${formatDateTimeNumeric( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.twenty_four, + }, + demoConfig + )} +
+
+ ` + )} +
+ `; + } + + static get styles() { + return css` + ha-control-select { + max-width: 800px; + margin: 12px auto; + } + .header { + font-weight: bold; + } + .center { + text-align: center; + } + .container { + max-width: 900px; + margin: 12px auto; + display: flex; + align-items: center; + justify-content: space-evenly; + } + + .container > div { + flex-grow: 1; + width: 20%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-date-time-date-time-numeric": DemoDateTimeDateTimeNumeric; + } +} diff --git a/gallery/src/pages/date-time/date-time-seconds.markdown b/gallery/src/pages/date-time/date-time-seconds.markdown new file mode 100644 index 0000000000..01cfa6c729 --- /dev/null +++ b/gallery/src/pages/date-time/date-time-seconds.markdown @@ -0,0 +1,7 @@ +--- +title: Date-Time Format (Seconds) +--- + +This pages lists all supported languages with their available date-time formats. + +Formatting function: `const formatDateTimeWithSeconds: (dateObj: Date, locale: FrontendLocaleData) => string` \ No newline at end of file diff --git a/gallery/src/pages/date-time/date-time-seconds.ts b/gallery/src/pages/date-time/date-time-seconds.ts new file mode 100644 index 0000000000..5f5b48f989 --- /dev/null +++ b/gallery/src/pages/date-time/date-time-seconds.ts @@ -0,0 +1,136 @@ +import { html, css, LitElement } from "lit"; +import { customElement, state } from "lit/decorators"; +import "../../../../src/components/ha-card"; +import "../../../../src/components/ha-control-select"; +import { translationMetadata } from "../../../../src/resources/translations-metadata"; +import { formatDateTimeWithSeconds } from "../../../../src/common/datetime/format_date_time"; +import { timeOptions } from "../../data/date-options"; +import { demoConfig } from "../../../../src/fake_data/demo_config"; +import { + FrontendLocaleData, + NumberFormat, + TimeFormat, + DateFormat, + FirstWeekday, + TimeZone, +} from "../../../../src/data/translation"; + +@customElement("demo-date-time-date-time-seconds") +export class DemoDateTimeDateTimeSeconds extends LitElement { + @state() private selection?: string = "now"; + + @state() private date: Date = new Date(); + + handleValueChanged(e: CustomEvent) { + this.selection = e.detail.value as string; + this.date = new Date(); + if (this.selection !== "now") { + const [hours, minutes, seconds] = this.selection.split(":").map(Number); + this.date.setHours(hours); + this.date.setMinutes(minutes); + this.date.setSeconds(seconds); + } + } + + protected render() { + const defaultLocale: FrontendLocaleData = { + language: "en", + number_format: NumberFormat.language, + time_format: TimeFormat.language, + date_format: DateFormat.language, + first_weekday: FirstWeekday.language, + time_zone: TimeZone.local, + }; + return html` + + + +
+
Language
+
Default (lang)
+
12 Hours
+
24 Hours
+
+ ${Object.entries(translationMetadata.translations) + .filter(([key, _]) => key !== "test") + .map( + ([key, value]) => html` +
+
${value.nativeName}
+
+ ${formatDateTimeWithSeconds( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.language, + }, + demoConfig + )} +
+
+ ${formatDateTimeWithSeconds( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.am_pm, + }, + demoConfig + )} +
+
+ ${formatDateTimeWithSeconds( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.twenty_four, + }, + demoConfig + )} +
+
+ ` + )} +
+ `; + } + + static get styles() { + return css` + ha-control-select { + max-width: 800px; + margin: 12px auto; + } + .header { + font-weight: bold; + } + .center { + text-align: center; + } + .container { + max-width: 900px; + margin: 12px auto; + display: flex; + align-items: center; + justify-content: space-evenly; + } + + .container > div { + flex-grow: 1; + width: 20%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-date-time-date-time-seconds": DemoDateTimeDateTimeSeconds; + } +} diff --git a/gallery/src/pages/date-time/date-time-short-year.markdown b/gallery/src/pages/date-time/date-time-short-year.markdown new file mode 100644 index 0000000000..19e77b55b9 --- /dev/null +++ b/gallery/src/pages/date-time/date-time-short-year.markdown @@ -0,0 +1,7 @@ +--- +title: Date-Time Format (Short w/ Year) +--- + +This pages lists all supported languages with their available date-time formats. + +Formatting function: `const formatShortDateTimeWithYear: (dateObj: Date, locale: FrontendLocaleData) => string` \ No newline at end of file diff --git a/gallery/src/pages/date-time/date-time-short-year.ts b/gallery/src/pages/date-time/date-time-short-year.ts new file mode 100644 index 0000000000..f132f2a047 --- /dev/null +++ b/gallery/src/pages/date-time/date-time-short-year.ts @@ -0,0 +1,136 @@ +import { html, css, LitElement } from "lit"; +import { customElement, state } from "lit/decorators"; +import "../../../../src/components/ha-card"; +import "../../../../src/components/ha-control-select"; +import { translationMetadata } from "../../../../src/resources/translations-metadata"; +import { formatShortDateTimeWithYear } from "../../../../src/common/datetime/format_date_time"; +import { timeOptions } from "../../data/date-options"; +import { demoConfig } from "../../../../src/fake_data/demo_config"; +import { + FrontendLocaleData, + NumberFormat, + TimeFormat, + DateFormat, + FirstWeekday, + TimeZone, +} from "../../../../src/data/translation"; + +@customElement("demo-date-time-date-time-short-year") +export class DemoDateTimeDateTimeShortYear extends LitElement { + @state() private selection?: string = "now"; + + @state() private date: Date = new Date(); + + handleValueChanged(e: CustomEvent) { + this.selection = e.detail.value as string; + this.date = new Date(); + if (this.selection !== "now") { + const [hours, minutes, seconds] = this.selection.split(":").map(Number); + this.date.setHours(hours); + this.date.setMinutes(minutes); + this.date.setSeconds(seconds); + } + } + + protected render() { + const defaultLocale: FrontendLocaleData = { + language: "en", + number_format: NumberFormat.language, + time_format: TimeFormat.language, + date_format: DateFormat.language, + first_weekday: FirstWeekday.language, + time_zone: TimeZone.local, + }; + return html` + + + +
+
Language
+
Default (lang)
+
12 Hours
+
24 Hours
+
+ ${Object.entries(translationMetadata.translations) + .filter(([key, _]) => key !== "test") + .map( + ([key, value]) => html` +
+
${value.nativeName}
+
+ ${formatShortDateTimeWithYear( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.language, + }, + demoConfig + )} +
+
+ ${formatShortDateTimeWithYear( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.am_pm, + }, + demoConfig + )} +
+
+ ${formatShortDateTimeWithYear( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.twenty_four, + }, + demoConfig + )} +
+
+ ` + )} +
+ `; + } + + static get styles() { + return css` + ha-control-select { + max-width: 800px; + margin: 12px auto; + } + .header { + font-weight: bold; + } + .center { + text-align: center; + } + .container { + max-width: 900px; + margin: 12px auto; + display: flex; + align-items: center; + justify-content: space-evenly; + } + + .container > div { + flex-grow: 1; + width: 20%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-date-time-date-time-short-year": DemoDateTimeDateTimeShortYear; + } +} diff --git a/gallery/src/pages/date-time/date-time-short.markdown b/gallery/src/pages/date-time/date-time-short.markdown new file mode 100644 index 0000000000..3564a7afa0 --- /dev/null +++ b/gallery/src/pages/date-time/date-time-short.markdown @@ -0,0 +1,7 @@ +--- +title: Date-Time Format (Short) +--- + +This pages lists all supported languages with their available date-time formats. + +Formatting function: `const formatShortDateTime: (dateObj: Date, locale: FrontendLocaleData) => string` \ No newline at end of file diff --git a/gallery/src/pages/date-time/date-time-short.ts b/gallery/src/pages/date-time/date-time-short.ts new file mode 100644 index 0000000000..21f7eb1294 --- /dev/null +++ b/gallery/src/pages/date-time/date-time-short.ts @@ -0,0 +1,136 @@ +import { html, css, LitElement } from "lit"; +import { customElement, state } from "lit/decorators"; +import "../../../../src/components/ha-card"; +import "../../../../src/components/ha-control-select"; +import { translationMetadata } from "../../../../src/resources/translations-metadata"; +import { formatShortDateTime } from "../../../../src/common/datetime/format_date_time"; +import { timeOptions } from "../../data/date-options"; +import { demoConfig } from "../../../../src/fake_data/demo_config"; +import { + FrontendLocaleData, + NumberFormat, + TimeFormat, + DateFormat, + FirstWeekday, + TimeZone, +} from "../../../../src/data/translation"; + +@customElement("demo-date-time-date-time-short") +export class DemoDateTimeDateTimeShort extends LitElement { + @state() private selection?: string = "now"; + + @state() private date: Date = new Date(); + + handleValueChanged(e: CustomEvent) { + this.selection = e.detail.value as string; + this.date = new Date(); + if (this.selection !== "now") { + const [hours, minutes, seconds] = this.selection.split(":").map(Number); + this.date.setHours(hours); + this.date.setMinutes(minutes); + this.date.setSeconds(seconds); + } + } + + protected render() { + const defaultLocale: FrontendLocaleData = { + language: "en", + number_format: NumberFormat.language, + time_format: TimeFormat.language, + date_format: DateFormat.language, + first_weekday: FirstWeekday.language, + time_zone: TimeZone.local, + }; + return html` + + + +
+
Language
+
Default (lang)
+
12 Hours
+
24 Hours
+
+ ${Object.entries(translationMetadata.translations) + .filter(([key, _]) => key !== "test") + .map( + ([key, value]) => html` +
+
${value.nativeName}
+
+ ${formatShortDateTime( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.language, + }, + demoConfig + )} +
+
+ ${formatShortDateTime( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.am_pm, + }, + demoConfig + )} +
+
+ ${formatShortDateTime( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.twenty_four, + }, + demoConfig + )} +
+
+ ` + )} +
+ `; + } + + static get styles() { + return css` + ha-control-select { + max-width: 800px; + margin: 12px auto; + } + .header { + font-weight: bold; + } + .center { + text-align: center; + } + .container { + max-width: 900px; + margin: 12px auto; + display: flex; + align-items: center; + justify-content: space-evenly; + } + + .container > div { + flex-grow: 1; + width: 20%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-date-time-date-time-short": DemoDateTimeDateTimeShort; + } +} diff --git a/gallery/src/pages/date-time/date-time.markdown b/gallery/src/pages/date-time/date-time.markdown new file mode 100644 index 0000000000..cef6195ab1 --- /dev/null +++ b/gallery/src/pages/date-time/date-time.markdown @@ -0,0 +1,7 @@ +--- +title: Date-Time Format +--- + +This pages lists all supported languages with their available date-time formats. + +Formatting function: `const formatDateTime: (dateObj: Date, locale: FrontendLocaleData) => string` \ No newline at end of file diff --git a/gallery/src/pages/date-time/date-time.ts b/gallery/src/pages/date-time/date-time.ts new file mode 100644 index 0000000000..4bb4b75865 --- /dev/null +++ b/gallery/src/pages/date-time/date-time.ts @@ -0,0 +1,136 @@ +import { html, css, LitElement } from "lit"; +import { customElement, state } from "lit/decorators"; +import "../../../../src/components/ha-card"; +import "../../../../src/components/ha-control-select"; +import { translationMetadata } from "../../../../src/resources/translations-metadata"; +import { formatDateTime } from "../../../../src/common/datetime/format_date_time"; +import { timeOptions } from "../../data/date-options"; +import { demoConfig } from "../../../../src/fake_data/demo_config"; +import { + FrontendLocaleData, + NumberFormat, + TimeFormat, + DateFormat, + FirstWeekday, + TimeZone, +} from "../../../../src/data/translation"; + +@customElement("demo-date-time-date-time") +export class DemoDateTimeDateTime extends LitElement { + @state() private selection?: string = "now"; + + @state() private date: Date = new Date(); + + handleValueChanged(e: CustomEvent) { + this.selection = e.detail.value as string; + this.date = new Date(); + if (this.selection !== "now") { + const [hours, minutes, seconds] = this.selection.split(":").map(Number); + this.date.setHours(hours); + this.date.setMinutes(minutes); + this.date.setSeconds(seconds); + } + } + + protected render() { + const defaultLocale: FrontendLocaleData = { + language: "en", + number_format: NumberFormat.language, + time_format: TimeFormat.language, + date_format: DateFormat.language, + first_weekday: FirstWeekday.language, + time_zone: TimeZone.local, + }; + return html` + + + +
+
Language
+
Default (lang)
+
12 Hours
+
24 Hours
+
+ ${Object.entries(translationMetadata.translations) + .filter(([key, _]) => key !== "test") + .map( + ([key, value]) => html` +
+
${value.nativeName}
+
+ ${formatDateTime( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.language, + }, + demoConfig + )} +
+
+ ${formatDateTime( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.am_pm, + }, + demoConfig + )} +
+
+ ${formatDateTime( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.twenty_four, + }, + demoConfig + )} +
+
+ ` + )} +
+ `; + } + + static get styles() { + return css` + ha-control-select { + max-width: 800px; + margin: 12px auto; + } + .header { + font-weight: bold; + } + .center { + text-align: center; + } + .container { + max-width: 900px; + margin: 12px auto; + display: flex; + align-items: center; + justify-content: space-evenly; + } + + .container > div { + flex-grow: 1; + width: 20%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-date-time-date-time": DemoDateTimeDateTime; + } +} diff --git a/gallery/src/pages/date-time/date.markdown b/gallery/src/pages/date-time/date.markdown index 5eb64bb19e..599921e1c7 100644 --- a/gallery/src/pages/date-time/date.markdown +++ b/gallery/src/pages/date-time/date.markdown @@ -1,5 +1,5 @@ --- -title: (Numeric) Date Formatting +title: Date Format (Numeric) --- This pages lists all supported languages with their available (numeric) date formats. diff --git a/gallery/src/pages/date-time/time-seconds.markdown b/gallery/src/pages/date-time/time-seconds.markdown new file mode 100644 index 0000000000..23136c29f4 --- /dev/null +++ b/gallery/src/pages/date-time/time-seconds.markdown @@ -0,0 +1,7 @@ +--- +title: Time Format (Seconds) +--- + +This pages lists all supported languages with their available time formats. + +Formatting function: `const formatTimeWithSeconds: (dateObj: Date, locale: FrontendLocaleData) => string` \ No newline at end of file diff --git a/gallery/src/pages/date-time/time-seconds.ts b/gallery/src/pages/date-time/time-seconds.ts new file mode 100644 index 0000000000..761bc6ed58 --- /dev/null +++ b/gallery/src/pages/date-time/time-seconds.ts @@ -0,0 +1,135 @@ +import { html, css, LitElement } from "lit"; +import { customElement, state } from "lit/decorators"; +import "../../../../src/components/ha-card"; +import { translationMetadata } from "../../../../src/resources/translations-metadata"; +import { formatTimeWithSeconds } from "../../../../src/common/datetime/format_time"; +import { timeOptions } from "../../data/date-options"; +import { demoConfig } from "../../../../src/fake_data/demo_config"; +import { + FrontendLocaleData, + NumberFormat, + TimeFormat, + DateFormat, + FirstWeekday, + TimeZone, +} from "../../../../src/data/translation"; + +@customElement("demo-date-time-time-seconds") +export class DemoDateTimeTimeSeconds extends LitElement { + @state() private selection?: string = "now"; + + @state() private date: Date = new Date(); + + handleValueChanged(e: CustomEvent) { + this.selection = e.detail.value as string; + this.date = new Date(); + if (this.selection !== "now") { + const [hours, minutes, seconds] = this.selection.split(":").map(Number); + this.date.setHours(hours); + this.date.setMinutes(minutes); + this.date.setSeconds(seconds); + } + } + + protected render() { + const defaultLocale: FrontendLocaleData = { + language: "en", + number_format: NumberFormat.language, + time_format: TimeFormat.language, + date_format: DateFormat.language, + first_weekday: FirstWeekday.language, + time_zone: TimeZone.local, + }; + return html` + + + +
+
Language
+
Default (lang)
+
12 Hours
+
24 Hours
+
+ ${Object.entries(translationMetadata.translations) + .filter(([key, _]) => key !== "test") + .map( + ([key, value]) => html` +
+
${value.nativeName}
+
+ ${formatTimeWithSeconds( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.language, + }, + demoConfig + )} +
+
+ ${formatTimeWithSeconds( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.am_pm, + }, + demoConfig + )} +
+
+ ${formatTimeWithSeconds( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.twenty_four, + }, + demoConfig + )} +
+
+ ` + )} +
+ `; + } + + static get styles() { + return css` + ha-control-select { + max-width: 800px; + margin: 12px auto; + } + .header { + font-weight: bold; + } + .center { + text-align: center; + } + .container { + max-width: 600px; + margin: 12px auto; + display: flex; + align-items: center; + justify-content: space-evenly; + } + + .container > div { + flex-grow: 1; + width: 20%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-date-time-time-seconds": DemoDateTimeTimeSeconds; + } +} diff --git a/gallery/src/pages/date-time/time-weekday.markdown b/gallery/src/pages/date-time/time-weekday.markdown new file mode 100644 index 0000000000..637be6afe3 --- /dev/null +++ b/gallery/src/pages/date-time/time-weekday.markdown @@ -0,0 +1,7 @@ +--- +title: Time Format (Weekday) +--- + +This pages lists all supported languages with their available time formats. + +Formatting function: `const formatTimeWeekday: (dateObj: Date, locale: FrontendLocaleData) => string` \ No newline at end of file diff --git a/gallery/src/pages/date-time/time-weekday.ts b/gallery/src/pages/date-time/time-weekday.ts new file mode 100644 index 0000000000..8ed5c951f5 --- /dev/null +++ b/gallery/src/pages/date-time/time-weekday.ts @@ -0,0 +1,135 @@ +import { html, css, LitElement } from "lit"; +import { customElement, state } from "lit/decorators"; +import "../../../../src/components/ha-card"; +import { translationMetadata } from "../../../../src/resources/translations-metadata"; +import { formatTimeWeekday } from "../../../../src/common/datetime/format_time"; +import { timeOptions } from "../../data/date-options"; +import { demoConfig } from "../../../../src/fake_data/demo_config"; +import { + FrontendLocaleData, + NumberFormat, + TimeFormat, + DateFormat, + FirstWeekday, + TimeZone, +} from "../../../../src/data/translation"; + +@customElement("demo-date-time-time-weekday") +export class DemoDateTimeTimeWeekday extends LitElement { + @state() private selection?: string = "now"; + + @state() private date: Date = new Date(); + + handleValueChanged(e: CustomEvent) { + this.selection = e.detail.value as string; + this.date = new Date(); + if (this.selection !== "now") { + const [hours, minutes, seconds] = this.selection.split(":").map(Number); + this.date.setHours(hours); + this.date.setMinutes(minutes); + this.date.setSeconds(seconds); + } + } + + protected render() { + const defaultLocale: FrontendLocaleData = { + language: "en", + number_format: NumberFormat.language, + time_format: TimeFormat.language, + date_format: DateFormat.language, + first_weekday: FirstWeekday.language, + time_zone: TimeZone.local, + }; + return html` + + + +
+
Language
+
Default (lang)
+
12 Hours
+
24 Hours
+
+ ${Object.entries(translationMetadata.translations) + .filter(([key, _]) => key !== "test") + .map( + ([key, value]) => html` +
+
${value.nativeName}
+
+ ${formatTimeWeekday( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.language, + }, + demoConfig + )} +
+
+ ${formatTimeWeekday( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.am_pm, + }, + demoConfig + )} +
+
+ ${formatTimeWeekday( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.twenty_four, + }, + demoConfig + )} +
+
+ ` + )} +
+ `; + } + + static get styles() { + return css` + ha-control-select { + max-width: 800px; + margin: 12px auto; + } + .header { + font-weight: bold; + } + .center { + text-align: center; + } + .container { + max-width: 800px; + margin: 12px auto; + display: flex; + align-items: center; + justify-content: space-evenly; + } + + .container > div { + flex-grow: 1; + width: 20%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-date-time-time-weekday": DemoDateTimeTimeWeekday; + } +} diff --git a/gallery/src/pages/date-time/time.markdown b/gallery/src/pages/date-time/time.markdown new file mode 100644 index 0000000000..df90d8931a --- /dev/null +++ b/gallery/src/pages/date-time/time.markdown @@ -0,0 +1,7 @@ +--- +title: Time Format +--- + +This pages lists all supported languages with their available time formats. + +Formatting function: `const formatTime: (dateObj: Date, locale: FrontendLocaleData) => string` \ No newline at end of file diff --git a/gallery/src/pages/date-time/time.ts b/gallery/src/pages/date-time/time.ts new file mode 100644 index 0000000000..df7b101653 --- /dev/null +++ b/gallery/src/pages/date-time/time.ts @@ -0,0 +1,136 @@ +import { html, css, LitElement } from "lit"; +import { customElement, state } from "lit/decorators"; +import "../../../../src/components/ha-card"; +import "../../../../src/components/ha-control-select"; +import { translationMetadata } from "../../../../src/resources/translations-metadata"; +import { formatTime } from "../../../../src/common/datetime/format_time"; +import { timeOptions } from "../../data/date-options"; +import { demoConfig } from "../../../../src/fake_data/demo_config"; +import { + FrontendLocaleData, + NumberFormat, + TimeFormat, + DateFormat, + FirstWeekday, + TimeZone, +} from "../../../../src/data/translation"; + +@customElement("demo-date-time-time") +export class DemoDateTimeTime extends LitElement { + @state() private selection?: string = "now"; + + @state() private date: Date = new Date(); + + handleValueChanged(e: CustomEvent) { + this.selection = e.detail.value as string; + this.date = new Date(); + if (this.selection !== "now") { + const [hours, minutes, seconds] = this.selection.split(":").map(Number); + this.date.setHours(hours); + this.date.setMinutes(minutes); + this.date.setSeconds(seconds); + } + } + + protected render() { + const defaultLocale: FrontendLocaleData = { + language: "en", + number_format: NumberFormat.language, + time_format: TimeFormat.language, + date_format: DateFormat.language, + first_weekday: FirstWeekday.language, + time_zone: TimeZone.local, + }; + return html` + + + +
+
Language
+
Default (lang)
+
12 Hours
+
24 Hours
+
+ ${Object.entries(translationMetadata.translations) + .filter(([key, _]) => key !== "test") + .map( + ([key, value]) => html` +
+
${value.nativeName}
+
+ ${formatTime( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.language, + }, + demoConfig + )} +
+
+ ${formatTime( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.am_pm, + }, + demoConfig + )} +
+
+ ${formatTime( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.twenty_four, + }, + demoConfig + )} +
+
+ ` + )} +
+ `; + } + + static get styles() { + return css` + ha-control-select { + max-width: 800px; + margin: 12px auto; + } + .header { + font-weight: bold; + } + .center { + text-align: center; + } + .container { + max-width: 600px; + margin: 12px auto; + display: flex; + align-items: center; + justify-content: space-evenly; + } + + .container > div { + flex-grow: 1; + width: 20%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-date-time-time": DemoDateTimeTime; + } +} diff --git a/gallery/src/pages/misc/entity-state.ts b/gallery/src/pages/misc/entity-state.ts index aa0905d9b2..c35c9fae1e 100644 --- a/gallery/src/pages/misc/entity-state.ts +++ b/gallery/src/pages/misc/entity-state.ts @@ -135,6 +135,9 @@ const ENTITIES: HassEntity[] = [ createEntity("climate.fan_only", "fan_only"), createEntity("climate.auto_idle", "auto", undefined, { hvac_action: "idle" }), createEntity("climate.auto_off", "auto", undefined, { hvac_action: "off" }), + createEntity("climate.auto_preheating", "auto", undefined, { + hvac_action: "preheating", + }), createEntity("climate.auto_heating", "auto", undefined, { hvac_action: "heating", }), diff --git a/package.json b/package.json index 43c1f40927..587d38410a 100644 --- a/package.json +++ b/package.json @@ -49,9 +49,9 @@ "@fullcalendar/list": "6.1.8", "@fullcalendar/timegrid": "6.1.8", "@lezer/highlight": "1.1.6", - "@lit-labs/context": "0.3.2", + "@lit-labs/context": "0.3.3", "@lit-labs/motion": "1.0.3", - "@lit-labs/virtualizer": "2.0.2", + "@lit-labs/virtualizer": "2.0.3", "@lrnwebcomponents/simple-tooltip": "7.0.2", "@material/chips": "=14.0.0-canary.53b3cad2f.0", "@material/data-table": "=14.0.0-canary.53b3cad2f.0", @@ -93,8 +93,8 @@ "@polymer/paper-toast": "3.0.1", "@polymer/polymer": "3.5.1", "@thomasloven/round-slider": "0.6.0", - "@vaadin/combo-box": "24.1.0", - "@vaadin/vaadin-themable-mixin": "24.1.0", + "@vaadin/combo-box": "24.1.1", + "@vaadin/vaadin-themable-mixin": "24.1.1", "@vibrant/color": "3.2.1-alpha.1", "@vibrant/core": "3.2.1-alpha.1", "@vibrant/quantizer-mmcq": "3.2.1-alpha.1", @@ -156,9 +156,9 @@ "@babel/preset-env": "7.22.5", "@babel/preset-typescript": "7.22.5", "@koa/cors": "4.0.0", - "@octokit/auth-oauth-device": "5.0.0", - "@octokit/plugin-retry": "5.0.3", - "@octokit/rest": "19.0.11", + "@octokit/auth-oauth-device": "5.0.2", + "@octokit/plugin-retry": "5.0.4", + "@octokit/rest": "19.0.13", "@open-wc/dev-server-hmr": "0.1.4", "@rollup/plugin-babel": "6.0.3", "@rollup/plugin-commonjs": "25.0.1", @@ -189,7 +189,7 @@ "babel-plugin-template-html-minifier": "4.1.0", "chai": "4.3.7", "del": "7.0.0", - "eslint": "8.42.0", + "eslint": "8.43.0", "eslint-config-airbnb-base": "15.0.0", "eslint-config-airbnb-typescript": "17.0.0", "eslint-config-prettier": "8.8.0", @@ -215,7 +215,7 @@ "instant-mocha": "1.5.1", "jszip": "3.10.1", "lint-staged": "13.2.2", - "lit-analyzer": "1.2.1", + "lit-analyzer": "2.0.0-pre.3", "lodash.template": "4.5.0", "magic-string": "0.30.0", "map-stream": "0.0.7", @@ -235,8 +235,8 @@ "systemjs": "6.14.1", "tar": "6.1.15", "terser-webpack-plugin": "5.3.9", - "ts-lit-plugin": "1.2.1", - "typescript": "4.9.5", + "ts-lit-plugin": "2.0.0-pre.1", + "typescript": "5.1.3", "vinyl-buffer": "1.0.1", "vinyl-source-stream": "2.0.0", "webpack": "5.87.0", diff --git a/pyproject.toml b/pyproject.toml index 099766e91b..8cac9f3594 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ readme = "README.md" authors = [ {name = "The Home Assistant Authors", email = "hello@home-assistant.io"} ] -requires-python = ">=3.4.0" +requires-python = ">=3.10.0" [project.urls] "Homepage" = "https://github.com/home-assistant/frontend" diff --git a/script/bootstrap b/script/bootstrap index f18b75d2b8..73e44806f9 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -8,9 +8,9 @@ cd "$(dirname "$0")/.." # Install/upgrade node when inside devcontainer if [[ -n "$DEVCONTAINER" ]]; then - nodeCurrent=$(nvm version default || echo "") + nodeCurrent=$(nvm version default || :) nodeLatest=$(nvm version-remote "$(cat .nvmrc)") - if [[ -z "$nodeCurrent" ]]; then + if [[ -z "$nodeCurrent" || "$nodeCurrent" == "N/A" ]]; then nvm install elif [[ "$nodeCurrent" != "$nodeLatest" ]]; then nvm install --reinstall-packages-from="$nodeCurrent" --default diff --git a/src/common/datetime/format_date_time.ts b/src/common/datetime/format_date_time.ts index d9b7a3a862..52700b4a23 100644 --- a/src/common/datetime/format_date_time.ts +++ b/src/common/datetime/format_date_time.ts @@ -15,20 +15,15 @@ export const formatDateTime = ( const formatDateTimeMem = memoizeOne( (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat( - locale.language === "en" && !useAmPm(locale) - ? "en-u-hc-h23" - : locale.language, - { - year: "numeric", - month: "long", - day: "numeric", - hour: useAmPm(locale) ? "numeric" : "2-digit", - minute: "2-digit", - hour12: useAmPm(locale), - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - } - ) + new Intl.DateTimeFormat(locale.language, { + year: "numeric", + month: "long", + day: "numeric", + hour: useAmPm(locale) ? "numeric" : "2-digit", + minute: "2-digit", + hourCycle: useAmPm(locale) ? "h12" : "h23", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) ); // Aug 9, 2021, 8:23 AM @@ -40,20 +35,15 @@ export const formatShortDateTimeWithYear = ( const formatShortDateTimeWithYearMem = memoizeOne( (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat( - locale.language === "en" && !useAmPm(locale) - ? "en-u-hc-h23" - : locale.language, - { - year: "numeric", - month: "short", - day: "numeric", - hour: useAmPm(locale) ? "numeric" : "2-digit", - minute: "2-digit", - hour12: useAmPm(locale), - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - } - ) + new Intl.DateTimeFormat(locale.language, { + year: "numeric", + month: "short", + day: "numeric", + hour: useAmPm(locale) ? "numeric" : "2-digit", + minute: "2-digit", + hourCycle: useAmPm(locale) ? "h12" : "h23", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) ); // Aug 9, 8:23 AM @@ -65,19 +55,14 @@ export const formatShortDateTime = ( const formatShortDateTimeMem = memoizeOne( (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat( - locale.language === "en" && !useAmPm(locale) - ? "en-u-hc-h23" - : locale.language, - { - month: "short", - day: "numeric", - hour: useAmPm(locale) ? "numeric" : "2-digit", - minute: "2-digit", - hour12: useAmPm(locale), - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - } - ) + new Intl.DateTimeFormat(locale.language, { + month: "short", + day: "numeric", + hour: useAmPm(locale) ? "numeric" : "2-digit", + minute: "2-digit", + hourCycle: useAmPm(locale) ? "h12" : "h23", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) ); // August 9, 2021, 8:23:15 AM @@ -89,21 +74,16 @@ export const formatDateTimeWithSeconds = ( const formatDateTimeWithSecondsMem = memoizeOne( (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat( - locale.language === "en" && !useAmPm(locale) - ? "en-u-hc-h23" - : locale.language, - { - year: "numeric", - month: "long", - day: "numeric", - hour: useAmPm(locale) ? "numeric" : "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: useAmPm(locale), - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - } - ) + new Intl.DateTimeFormat(locale.language, { + year: "numeric", + month: "long", + day: "numeric", + hour: useAmPm(locale) ? "numeric" : "2-digit", + minute: "2-digit", + second: "2-digit", + hourCycle: useAmPm(locale) ? "h12" : "h23", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) ); // 9/8/2021, 8:23 AM diff --git a/src/common/datetime/format_time.ts b/src/common/datetime/format_time.ts index 41827b4f8d..948eb553fe 100644 --- a/src/common/datetime/format_time.ts +++ b/src/common/datetime/format_time.ts @@ -13,17 +13,12 @@ export const formatTime = ( const formatTimeMem = memoizeOne( (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat( - locale.language === "en" && !useAmPm(locale) - ? "en-u-hc-h23" - : locale.language, - { - hour: "numeric", - minute: "2-digit", - hour12: useAmPm(locale), - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - } - ) + new Intl.DateTimeFormat(locale.language, { + hour: "numeric", + minute: "2-digit", + hourCycle: useAmPm(locale) ? "h12" : "h23", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) ); // 9:15:24 PM || 21:15:24 @@ -35,18 +30,13 @@ export const formatTimeWithSeconds = ( const formatTimeWithSecondsMem = memoizeOne( (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat( - locale.language === "en" && !useAmPm(locale) - ? "en-u-hc-h23" - : locale.language, - { - hour: useAmPm(locale) ? "numeric" : "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: useAmPm(locale), - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - } - ) + new Intl.DateTimeFormat(locale.language, { + hour: useAmPm(locale) ? "numeric" : "2-digit", + minute: "2-digit", + second: "2-digit", + hourCycle: useAmPm(locale) ? "h12" : "h23", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) ); // Tuesday 7:00 PM || Tuesday 19:00 @@ -58,18 +48,13 @@ export const formatTimeWeekday = ( const formatTimeWeekdayMem = memoizeOne( (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat( - locale.language === "en" && !useAmPm(locale) - ? "en-u-hc-h23" - : locale.language, - { - weekday: "long", - hour: useAmPm(locale) ? "numeric" : "2-digit", - minute: "2-digit", - hour12: useAmPm(locale), - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - } - ) + new Intl.DateTimeFormat(locale.language, { + weekday: "long", + hour: useAmPm(locale) ? "numeric" : "2-digit", + minute: "2-digit", + hourCycle: useAmPm(locale) ? "h12" : "h23", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) ); // 21:15 diff --git a/src/common/datetime/use_am_pm.ts b/src/common/datetime/use_am_pm.ts index 97bf2911b8..4510fee522 100644 --- a/src/common/datetime/use_am_pm.ts +++ b/src/common/datetime/use_am_pm.ts @@ -8,8 +8,10 @@ export const useAmPm = memoizeOne((locale: FrontendLocaleData): boolean => { ) { const testLanguage = locale.time_format === TimeFormat.language ? locale.language : undefined; - const test = new Date().toLocaleString(testLanguage); - return test.includes("AM") || test.includes("PM"); + const test = new Date("January 1, 2023 22:00:00").toLocaleString( + testLanguage + ); + return test.includes("10"); } return locale.time_format === TimeFormat.am_pm; diff --git a/src/common/entity/get_states.ts b/src/common/entity/get_states.ts index 5909dd2938..4ceaf82d26 100644 --- a/src/common/entity/get_states.ts +++ b/src/common/entity/get_states.ts @@ -102,7 +102,15 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = { frontend_stream_type: ["hls", "web_rtc"], }, climate: { - hvac_action: ["off", "idle", "heating", "cooling", "drying", "fan"], + hvac_action: [ + "off", + "idle", + "preheating", + "heating", + "cooling", + "drying", + "fan", + ], }, cover: { device_class: [ diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index 1e34d204fb..b90d57fb62 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -30,6 +30,8 @@ class StateHistoryChartLine extends LitElement { @property({ type: Boolean }) public showNames = true; + @property({ attribute: false }) public startTime!: Date; + @property({ attribute: false }) public endTime!: Date; @property({ type: Number }) public paddingYAxis = 0; @@ -57,7 +59,12 @@ class StateHistoryChartLine extends LitElement { } public willUpdate(changedProps: PropertyValues) { - if (!this.hasUpdated || changedProps.has("showNames")) { + if ( + !this.hasUpdated || + changedProps.has("showNames") || + changedProps.has("startTime") || + changedProps.has("endTime") + ) { this._chartOptions = { parsing: false, animation: false, @@ -74,6 +81,7 @@ class StateHistoryChartLine extends LitElement { config: this.hass.config, }, }, + suggestedMin: this.startTime, suggestedMax: this.endTime, ticks: { maxRotation: 0, @@ -146,6 +154,8 @@ class StateHistoryChartLine extends LitElement { } if ( changedProps.has("data") || + changedProps.has("startTime") || + changedProps.has("endTime") || this._chartTime < new Date(this.endTime.getTime() - MIN_TIME_BETWEEN_UPDATES) ) { diff --git a/src/components/chart/state-history-charts.ts b/src/components/chart/state-history-charts.ts index 0e143284e4..0ce32a583e 100644 --- a/src/components/chart/state-history-charts.ts +++ b/src/components/chart/state-history-charts.ts @@ -52,8 +52,12 @@ export class StateHistoryCharts extends LitElement { @property({ attribute: false }) public endTime?: Date; + @property({ attribute: false }) public startTime?: Date; + @property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false; + @property() public hoursToShow?: number; + @property({ type: Boolean }) public showNames = true; @property({ type: Boolean }) public isLoadingData = false; @@ -95,13 +99,24 @@ export class StateHistoryCharts extends LitElement { this._computedEndTime = this.upToNow || !this.endTime || this.endTime > now ? now : this.endTime; - this._computedStartTime = new Date( - this.historyData.timeline.reduce( - (minTime, stateInfo) => - Math.min(minTime, new Date(stateInfo.data[0].last_changed).getTime()), - new Date().getTime() - ) - ); + if (this.startTime) { + this._computedStartTime = this.startTime; + } else if (this.hoursToShow) { + this._computedStartTime = new Date( + new Date().getTime() - 60 * 60 * this.hoursToShow * 1000 + ); + } else { + this._computedStartTime = new Date( + this.historyData.timeline.reduce( + (minTime, stateInfo) => + Math.min( + minTime, + new Date(stateInfo.data[0].last_changed).getTime() + ), + new Date().getTime() + ) + ); + } const combinedItems = this.historyData.timeline.length ? (this.virtualize @@ -142,6 +157,7 @@ export class StateHistoryCharts extends LitElement { .data=${item.data} .identifier=${item.identifier} .showNames=${this.showNames} + .startTime=${this._computedStartTime} .endTime=${this._computedEndTime} .paddingYAxis=${this._maxYWidth} .names=${this.names} diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index 5fc1bfff80..fb837d15b2 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -26,6 +26,10 @@ import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { ValueChangedEvent, HomeAssistant } from "../../types"; import "../ha-combo-box"; import type { HaComboBox } from "../ha-combo-box"; +import { + fuzzyFilterSort, + ScorableTextItem, +} from "../../common/string/filter/sequence-matching"; interface Device { name: string; @@ -33,6 +37,8 @@ interface Device { id: string; } +type ScorableDevice = ScorableTextItem & Device; + export type HaDevicePickerDeviceFilterFunc = ( device: DeviceRegistryEntry ) => boolean; @@ -119,13 +125,14 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { deviceFilter: this["deviceFilter"], entityFilter: this["entityFilter"], excludeDevices: this["excludeDevices"] - ): Device[] => { + ): ScorableDevice[] => { if (!devices.length) { return [ { id: "no_devices", area: "", name: this.hass.localize("ui.components.device-picker.no_devices"), + strings: [], }, ]; } @@ -235,6 +242,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { device.area_id && areaLookup[device.area_id] ? areaLookup[device.area_id].name : this.hass.localize("ui.components.device-picker.no_area"), + strings: [device.name || ""], })); if (!outputDevices.length) { return [ @@ -242,6 +250,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { id: "no_devices", area: "", name: this.hass.localize("ui.components.device-picker.no_match"), + strings: [], }, ]; } @@ -284,7 +293,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { (this._init && changedProps.has("_opened") && this._opened) ) { this._init = true; - (this.comboBox as any).items = this._getDevices( + const devices = this._getDevices( this.devices!, this.areas!, this.entities!, @@ -295,6 +304,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { this.entityFilter, this.excludeDevices ); + this.comboBox.items = devices; + this.comboBox.filteredItems = devices; } } @@ -314,6 +325,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { item-label-path="name" @opened-changed=${this._openedChanged} @value-changed=${this._deviceChanged} + @filter-changed=${this._filterChanged} > `; } @@ -322,6 +334,14 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { return this.value || ""; } + private _filterChanged(ev: CustomEvent): void { + const target = ev.target as HaComboBox; + const filterString = ev.detail.value.toLowerCase(); + target.filteredItems = filterString.length + ? fuzzyFilterSort(filterString, target.items || []) + : target.items; + } + private _deviceChanged(ev: ValueChangedEvent) { ev.stopPropagation(); let newValue = ev.detail.value; diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index c06d2cd5a9..115e0795aa 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -7,15 +7,19 @@ import memoizeOne from "memoize-one"; import { fireEvent } from "../../common/dom/fire_event"; import { computeDomain } from "../../common/entity/compute_domain"; import { computeStateName } from "../../common/entity/compute_state_name"; -import { caseInsensitiveStringCompare } from "../../common/string/compare"; +import { + fuzzyFilterSort, + ScorableTextItem, +} from "../../common/string/filter/sequence-matching"; import { ValueChangedEvent, HomeAssistant } from "../../types"; import "../ha-combo-box"; import type { HaComboBox } from "../ha-combo-box"; import "../ha-icon-button"; import "../ha-svg-icon"; import "./state-badge"; +import { caseInsensitiveStringCompare } from "../../common/string/compare"; -interface HassEntityWithCachedName extends HassEntity { +interface HassEntityWithCachedName extends HassEntity, ScorableTextItem { friendly_name: string; } @@ -159,6 +163,7 @@ export class HaEntityPicker extends LitElement { ), icon: "mdi:magnify", }, + strings: [], }, ]; } @@ -169,10 +174,14 @@ export class HaEntityPicker extends LitElement { ); return entityIds - .map((key) => ({ - ...hass!.states[key], - friendly_name: computeStateName(hass!.states[key]) || key, - })) + .map((key) => { + const friendly_name = computeStateName(hass!.states[key]) || key; + return { + ...hass!.states[key], + friendly_name, + strings: [key, friendly_name], + }; + }) .sort((entityA, entityB) => caseInsensitiveStringCompare( entityA.friendly_name, @@ -201,10 +210,14 @@ export class HaEntityPicker extends LitElement { } states = entityIds - .map((key) => ({ - ...hass!.states[key], - friendly_name: computeStateName(hass!.states[key]) || key, - })) + .map((key) => { + const friendly_name = computeStateName(hass!.states[key]) || key; + return { + ...hass!.states[key], + friendly_name, + strings: [key, friendly_name], + }; + }) .sort((entityA, entityB) => caseInsensitiveStringCompare( entityA.friendly_name, @@ -260,6 +273,7 @@ export class HaEntityPicker extends LitElement { ), icon: "mdi:magnify", }, + strings: [], }, ]; } @@ -293,7 +307,7 @@ export class HaEntityPicker extends LitElement { this.excludeEntities ); if (this._initedStates) { - (this.comboBox as any).filteredItems = this._states; + this.comboBox.filteredItems = this._states; } this._initedStates = true; } @@ -340,12 +354,11 @@ export class HaEntityPicker extends LitElement { } private _filterChanged(ev: CustomEvent): void { + const target = ev.target as HaComboBox; const filterString = ev.detail.value.toLowerCase(); - (this.comboBox as any).filteredItems = this._states.filter( - (entityState) => - entityState.entity_id.toLowerCase().includes(filterString) || - computeStateName(entityState).toLowerCase().includes(filterString) - ); + target.filteredItems = filterString.length + ? fuzzyFilterSort(filterString, this._states) + : this._states; } private _setValue(value: string) { diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts index 9c06dd8015..1ea409aafe 100644 --- a/src/components/ha-area-picker.ts +++ b/src/components/ha-area-picker.ts @@ -7,6 +7,10 @@ import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; import { fireEvent } from "../common/dom/fire_event"; import { computeDomain } from "../common/entity/compute_domain"; +import { + fuzzyFilterSort, + ScorableTextItem, +} from "../common/string/filter/sequence-matching"; import { AreaRegistryEntry, createAreaRegistryEntry, @@ -28,6 +32,8 @@ import type { HaComboBox } from "./ha-combo-box"; import "./ha-icon-button"; import "./ha-svg-icon"; +type ScorableAreaRegistryEntry = ScorableTextItem & AreaRegistryEntry; + const rowRenderer: ComboBoxLitRenderer = ( item ) => html` ({ + ...area, + strings: [area.area_id, ...area.aliases, area.name], + })); + this.comboBox.items = areas; + this.comboBox.filteredItems = areas; } } @@ -345,8 +354,9 @@ export class HaAreaPicker extends LitElement { return; } - const filteredItems = this.comboBox.items?.filter((item) => - item.name.toLowerCase().includes(filter!.toLowerCase()) + const filteredItems = fuzzyFilterSort( + filter, + this.comboBox?.items || [] ); if (!this.noAdd && filteredItems?.length === 0) { this._suggestion = filter; @@ -409,7 +419,7 @@ export class HaAreaPicker extends LitElement { name, }); const areas = [...Object.values(this.hass.areas), area]; - (this.comboBox as any).filteredItems = this._getAreas( + this.comboBox.filteredItems = this._getAreas( areas, Object.values(this.hass.devices)!, Object.values(this.hass.entities)!, diff --git a/src/components/ha-hs-color-picker.ts b/src/components/ha-hs-color-picker.ts index 1533e2b6c4..ba5d038526 100644 --- a/src/components/ha-hs-color-picker.ts +++ b/src/components/ha-hs-color-picker.ts @@ -186,9 +186,8 @@ class HaHsColorPicker extends LitElement { } if (changedProps.has("value")) { if ( - this.value !== undefined && - (this._localValue?.[0] !== this.value[0] || - this._localValue?.[1] !== this.value[1]) + this._localValue?.[0] !== this.value?.[0] || + this._localValue?.[1] !== this.value?.[1] ) { this._resetPosition(); } @@ -243,7 +242,11 @@ class HaHsColorPicker extends LitElement { } private _resetPosition() { - if (this.value === undefined) return; + if (this.value === undefined) { + this._cursorPosition = undefined; + this._localValue = undefined; + return; + } this._cursorPosition = this._getCoordsFromValue(this.value); this._localValue = this.value; } @@ -384,6 +387,7 @@ class HaHsColorPicker extends LitElement { canvas { width: 100%; height: 100%; + object-fit: contain; border-radius: 50%; cursor: pointer; } diff --git a/src/components/ha-icon-button-group.ts b/src/components/ha-icon-button-group.ts new file mode 100644 index 0000000000..cd739082ab --- /dev/null +++ b/src/components/ha-icon-button-group.ts @@ -0,0 +1,38 @@ +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement } from "lit/decorators"; + +@customElement("ha-icon-button-group") +export class HaIconButtonGroup extends LitElement { + protected render(): TemplateResult { + return html``; + } + + static get styles(): CSSResultGroup { + return css` + :host { + position: relative; + display: flex; + flex-direction: row; + align-items: center; + height: 56px; + border-radius: 28px; + background-color: rgba(139, 145, 151, 0.1); + box-sizing: border-box; + width: auto; + padding: 4px; + gap: 4px; + } + ::slotted(.separator) { + background-color: rgba(var(--rgb-primary-text-color), 0.15); + width: 1px; + height: 40px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-icon-button-group": HaIconButtonGroup; + } +} diff --git a/src/components/ha-icon-button-toggle.ts b/src/components/ha-icon-button-toggle.ts new file mode 100644 index 0000000000..56d0b37c82 --- /dev/null +++ b/src/components/ha-icon-button-toggle.ts @@ -0,0 +1,52 @@ +import { css, CSSResultGroup } from "lit"; +import { customElement, property } from "lit/decorators"; +import { HaIconButton } from "./ha-icon-button"; + +@customElement("ha-icon-button-toggle") +export class HaIconButtonToggle extends HaIconButton { + @property({ type: Boolean, reflect: true }) selected = false; + + static get styles(): CSSResultGroup { + return css` + :host { + position: relative; + } + mwc-icon-button { + position: relative; + transition: color 180ms ease-in-out; + } + mwc-icon-button::before { + opacity: 0; + transition: opacity 180ms ease-in-out; + background-color: var(--primary-text-color); + border-radius: 20px; + height: 40px; + width: 40px; + content: ""; + position: absolute; + top: -10px; + left: -10px; + bottom: -10px; + right: -10px; + margin: auto; + box-sizing: border-box; + } + :host([border-only]) mwc-icon-button::before { + background-color: transparent; + border: 2px solid var(--primary-text-color); + } + :host([selected]) mwc-icon-button { + color: var(--primary-background-color); + } + :host([selected]) mwc-icon-button::before { + opacity: 1; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-icon-button-toggle": HaIconButtonToggle; + } +} diff --git a/src/components/ha-selector/ha-selector-select.ts b/src/components/ha-selector/ha-selector-select.ts index f4b07b55aa..e8bd3ac4b4 100644 --- a/src/components/ha-selector/ha-selector-select.ts +++ b/src/components/ha-selector/ha-selector-select.ts @@ -4,6 +4,7 @@ import { css, html, LitElement } from "lit"; import { customElement, property, query } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; import { stopPropagation } from "../../common/dom/stop_propagation"; +import { ensureArray } from "../../common/array/ensure-array"; import type { SelectOption, SelectSelector } from "../../data/selector"; import type { HomeAssistant } from "../../types"; import "../ha-checkbox"; @@ -40,7 +41,7 @@ export class HaSelectSelector extends LitElement { protected render() { const options = - this.selector.select?.options.map((option) => + this.selector.select?.options?.map((option) => typeof option === "object" ? (option as SelectOption) : ({ value: option, label: option } as SelectOption) @@ -77,7 +78,8 @@ export class HaSelectSelector extends LitElement { ${this._renderHelper()} `; } - + const value = + !this.value || this.value === "" ? [] : ensureArray(this.value); return html`
${this.label} @@ -85,7 +87,7 @@ export class HaSelectSelector extends LitElement { (item: SelectOption) => html` !option.disabled && !value?.includes(option.value) @@ -231,19 +233,19 @@ export class HaSelectSelector extends LitElement { const value: string = ev.target.value; const checked = ev.target.checked; + const oldValue = + !this.value || this.value === "" ? [] : ensureArray(this.value); + if (checked) { - if (!this.value) { - newValue = [value]; - } else if (this.value.includes(value)) { + if (oldValue.includes(value)) { return; - } else { - newValue = [...this.value, value]; } + newValue = [...oldValue, value]; } else { - if (!this.value?.includes(value)) { + if (!oldValue?.includes(value)) { return; } - newValue = (this.value as string[]).filter((v) => v !== value); + newValue = oldValue.filter((v) => v !== value); } fireEvent(this, "value-changed", { @@ -252,7 +254,7 @@ export class HaSelectSelector extends LitElement { } private async _removeItem(ev) { - const value: string[] = [...(this.value! as string[])]; + const value: string[] = [...ensureArray(this.value!)]; value.splice(ev.target.idx, 1); fireEvent(this, "value-changed", { @@ -277,7 +279,10 @@ export class HaSelectSelector extends LitElement { return; } - if (newValue !== undefined && this.value?.includes(newValue)) { + const currentValue = + !this.value || this.value === "" ? [] : ensureArray(this.value); + + if (newValue !== undefined && currentValue.includes(newValue)) { return; } @@ -286,9 +291,6 @@ export class HaSelectSelector extends LitElement { this.comboBox.setInputValue(""); }, 0); - const currentValue = - !this.value || this.value === "" ? [] : (this.value as string[]); - fireEvent(this, "value-changed", { value: [...currentValue, newValue], }); diff --git a/src/components/ha-selector/ha-selector-theme.ts b/src/components/ha-selector/ha-selector-theme.ts index eccf3c5b23..b00d523413 100644 --- a/src/components/ha-selector/ha-selector-theme.ts +++ b/src/components/ha-selector/ha-selector-theme.ts @@ -24,6 +24,7 @@ export class HaThemeSelector extends LitElement { .hass=${this.hass} .value=${this.value} .label=${this.label} + .includeDefault=${this.selector.theme?.include_default} .disabled=${this.disabled} .required=${this.required} > diff --git a/src/components/ha-temp-color-picker.ts b/src/components/ha-temp-color-picker.ts index 9070a46e28..b2f2b26dfc 100644 --- a/src/components/ha-temp-color-picker.ts +++ b/src/components/ha-temp-color-picker.ts @@ -139,7 +139,7 @@ class HaTempColorPicker extends LitElement { this.setAttribute("aria-valuemax", this.max.toString()); } if (changedProps.has("value")) { - if (this.value != null && this._localValue !== this.value) { + if (this._localValue !== this.value) { this._resetPosition(); } } @@ -197,7 +197,11 @@ class HaTempColorPicker extends LitElement { } private _resetPosition() { - if (this.value === undefined) return; + if (this.value === undefined) { + this._cursorPosition = undefined; + this._localValue = undefined; + return; + } const [, y] = this._getCoordsFromValue(this.value); const currentX = this._cursorPosition?.[0] ?? 0; const x = @@ -391,6 +395,7 @@ class HaTempColorPicker extends LitElement { canvas { width: 100%; height: 100%; + object-fit: contain; border-radius: 50%; transition: box-shadow 180ms ease-in-out; cursor: pointer; diff --git a/src/components/ha-textfield.ts b/src/components/ha-textfield.ts index e411469252..c18b49c2d4 100644 --- a/src/components/ha-textfield.ts +++ b/src/components/ha-textfield.ts @@ -99,6 +99,10 @@ export class HaTextField extends TextFieldBase { direction: var(--direction); } + .mdc-text-field__icon--trailing { + padding: var(--textfield-icon-trailing-padding, 12px); + } + .mdc-floating-label:not(.mdc-floating-label--float-above) { text-overflow: ellipsis; width: inherit; diff --git a/src/components/ha-theme-picker.ts b/src/components/ha-theme-picker.ts index 47aadd7e62..8835a5b186 100644 --- a/src/components/ha-theme-picker.ts +++ b/src/components/ha-theme-picker.ts @@ -1,17 +1,28 @@ import "@material/mwc-list/mwc-list-item"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + nothing, + LitElement, + TemplateResult, +} from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; import { stopPropagation } from "../common/dom/stop_propagation"; import { HomeAssistant } from "../types"; import "./ha-select"; +const DEFAULT_THEME = "default"; + @customElement("ha-theme-picker") export class HaThemePicker extends LitElement { @property() public value?: string; @property() public label?: string; + @property() includeDefault?: boolean = false; + @property({ attribute: false }) public hass?: HomeAssistant; @property({ type: Boolean, reflect: true }) public disabled = false; @@ -36,6 +47,13 @@ export class HaThemePicker extends LitElement { "ui.components.theme-picker.no_theme" )} + ${this.includeDefault + ? html`${this.hass!.localize( + "ui.components.theme-picker.default" + )}` + : nothing} ${Object.keys(this.hass!.themes.themes) .sort() .map( diff --git a/src/data/automation_i18n.ts b/src/data/automation_i18n.ts index b0ab6bb9f4..854d8501f7 100644 --- a/src/data/automation_i18n.ts +++ b/src/data/automation_i18n.ts @@ -24,6 +24,9 @@ import { EntityRegistryEntry } from "./entity_registry"; import "../resources/intl-polyfill"; import { FrontendLocaleData } from "./translation"; +const triggerTranslationBaseKey = + "ui.panel.config.automation.editor.triggers.type"; + const describeDuration = (forTime: number | string | ForDict) => { let duration: string | null; if (typeof forTime === "number") { @@ -90,29 +93,30 @@ export const describeTrigger = ( // Event Trigger if (trigger.platform === "event" && trigger.event_type) { - let eventTypes = ""; + const eventTypes: string[] = []; if (Array.isArray(trigger.event_type)) { - for (const [index, state] of trigger.event_type.entries()) { - eventTypes += `${index > 0 ? "," : ""} ${ - trigger.event_type.length > 1 && - index === trigger.event_type.length - 1 - ? "or" - : "" - } ${state}`; + for (const state of trigger.event_type.values()) { + eventTypes.push(state); } } else { - eventTypes = trigger.event_type.toString(); + eventTypes.push(trigger.event_type); } - return `When ${eventTypes} event is fired`; + const eventTypesString = disjunctionFormatter.format(eventTypes); + return hass.localize( + `${triggerTranslationBaseKey}.event.description.full`, + { eventTypes: eventTypesString } + ); } // Home Assistant Trigger if (trigger.platform === "homeassistant" && trigger.event) { - return `When Home Assistant is ${ - trigger.event === "start" ? "started" : "shutdown" - }`; + return hass.localize( + trigger.event === "start" + ? `${triggerTranslationBaseKey}.homeassistant.description.started` + : `${triggerTranslationBaseKey}.homeassistant.description.shutdown` + ); } // Numeric State Trigger @@ -157,7 +161,7 @@ export const describeTrigger = ( // State Trigger if (trigger.platform === "state") { let base = "When"; - let entities = ""; + const entities: string[] = []; const states = hass.states; if (trigger.attribute) { @@ -173,25 +177,22 @@ export const describeTrigger = ( } if (Array.isArray(trigger.entity_id)) { - for (const [index, entity] of trigger.entity_id.entries()) { + for (const entity of trigger.entity_id.values()) { if (states[entity]) { - entities += `${index > 0 ? "," : ""} ${ - trigger.entity_id.length > 1 && - index === trigger.entity_id.length - 1 - ? "or" - : "" - } ${computeStateName(states[entity]) || entity}`; + entities.push(computeStateName(states[entity]) || entity); } } } else if (trigger.entity_id) { - entities = states[trigger.entity_id] - ? computeStateName(states[trigger.entity_id]) - : trigger.entity_id; + entities.push( + states[trigger.entity_id] + ? computeStateName(states[trigger.entity_id]) + : trigger.entity_id + ); } - if (!entities) { + if (entities.length === 0) { // no entity_id or empty array - entities = "something"; + entities.push("something"); } base += ` ${entities} changes`; @@ -208,13 +209,9 @@ export const describeTrigger = ( base += " from any state"; } } else if (Array.isArray(trigger.from)) { - let from = ""; - for (const [index, state] of trigger.from.entries()) { - from += `${index > 0 ? "," : ""} ${ - trigger.from.length > 1 && index === trigger.from.length - 1 - ? "or" - : "" - } '${ + const from: string[] = []; + for (const state of trigger.from.values()) { + from.push( trigger.attribute ? computeAttributeValueDisplay( hass.localize, @@ -224,7 +221,7 @@ export const describeTrigger = ( hass.entities, trigger.attribute, state - ) + ).toString() : computeStateDisplay( hass.localize, stateObj, @@ -233,13 +230,14 @@ export const describeTrigger = ( hass.entities, state ) - }'`; + ); } - if (from) { - base += ` from ${from}`; + if (from.length !== 0) { + const fromString = disjunctionFormatter.format(from); + base += ` from ${fromString}`; } } else { - base += ` from '${ + base += ` from ${ trigger.attribute ? computeAttributeValueDisplay( hass.localize, @@ -258,7 +256,7 @@ export const describeTrigger = ( hass.entities, trigger.from.toString() ).toString() - }'`; + }`; } } @@ -268,11 +266,9 @@ export const describeTrigger = ( base += " to any state"; } } else if (Array.isArray(trigger.to)) { - let to = ""; - for (const [index, state] of trigger.to.entries()) { - to += `${index > 0 ? "," : ""} ${ - trigger.to.length > 1 && index === trigger.to.length - 1 ? "or" : "" - } '${ + const to: string[] = []; + for (const state of trigger.to.values()) { + to.push( trigger.attribute ? computeAttributeValueDisplay( hass.localize, @@ -291,13 +287,14 @@ export const describeTrigger = ( hass.entities, state ).toString() - }'`; + ); } - if (to) { - base += ` to ${to}`; + if (to.length !== 0) { + const toString = disjunctionFormatter.format(to); + base += ` to ${toString}`; } } else { - base += ` to '${ + base += ` to ${ trigger.attribute ? computeAttributeValueDisplay( hass.localize, @@ -315,8 +312,8 @@ export const describeTrigger = ( hass.config, hass.entities, trigger.to.toString() - ).toString() - }'`; + ) + }`; } } @@ -340,29 +337,28 @@ export const describeTrigger = ( // Sun Trigger if (trigger.platform === "sun" && trigger.event) { - let base = `When the sun ${trigger.event === "sunset" ? "sets" : "rises"}`; - + let duration = ""; if (trigger.offset) { - let duration = ""; - - if (trigger.offset) { - if (typeof trigger.offset === "number") { - duration = ` offset by ${secondsToDuration(trigger.offset)!}`; - } else if (typeof trigger.offset === "string") { - duration = ` offset by ${trigger.offset}`; - } else { - duration = ` offset by ${JSON.stringify(trigger.offset)}`; - } + if (typeof trigger.offset === "number") { + duration = secondsToDuration(trigger.offset)!; + } else if (typeof trigger.offset === "string") { + duration = trigger.offset; + } else { + duration = JSON.stringify(trigger.offset); } - base += duration; } - return base; + return hass.localize( + trigger.event === "sunset" + ? `${triggerTranslationBaseKey}.sun.description.sets` + : `${triggerTranslationBaseKey}.sun.description.rises`, + { hasDuration: duration !== "", duration: duration } + ); } // Tag Trigger if (trigger.platform === "tag") { - return "When a tag is scanned"; + return hass.localize(`${triggerTranslationBaseKey}.tag.description.full`); } // Time Trigger @@ -375,10 +371,9 @@ export const describeTrigger = ( : localizeTimeString(at, hass.locale, hass.config) ); - const last = result.splice(-1, 1)[0]; - return `When the time is equal to ${ - result.length ? `${result.join(", ")} or ` : "" - }${last}`; + return hass.localize(`${triggerTranslationBaseKey}.time.description.full`, { + time: disjunctionFormatter.format(result), + }); } // Time Pattern Trigger @@ -501,9 +496,9 @@ export const describeTrigger = ( const states = hass.states; if (Array.isArray(trigger.entity_id)) { - for (const [entity] of trigger.entity_id.entries()) { + for (const entity of trigger.entity_id.values()) { if (states[entity]) { - entities.push(`${computeStateName(states[entity]) || entity}`); + entities.push(computeStateName(states[entity]) || entity); } } } else { @@ -515,9 +510,9 @@ export const describeTrigger = ( } if (Array.isArray(trigger.zone)) { - for (const [zone] of trigger.zone.entries()) { + for (const zone of trigger.zone.values()) { if (states[zone]) { - zones.push(`${computeStateName(states[zone]) || zone}`); + zones.push(computeStateName(states[zone]) || zone); } } } else { @@ -537,67 +532,62 @@ export const describeTrigger = ( // Geo Location Trigger if (trigger.platform === "geo_location" && trigger.source && trigger.zone) { - let sources = ""; - let zones = ""; - let zonesPlural = false; + const sources: string[] = []; + const zones: string[] = []; const states = hass.states; if (Array.isArray(trigger.source)) { - for (const [index, source] of trigger.source.entries()) { - sources += `${index > 0 ? "," : ""} ${ - trigger.source.length > 1 && index === trigger.source.length - 1 - ? "or" - : "" - } ${source}`; + for (const source of trigger.source.values()) { + sources.push(source); } } else { - sources = trigger.source; + sources.push(trigger.source); } if (Array.isArray(trigger.zone)) { - if (trigger.zone.length > 1) { - zonesPlural = true; - } - - for (const [index, zone] of trigger.zone.entries()) { + for (const zone of trigger.zone.values()) { if (states[zone]) { - zones += `${index > 0 ? "," : ""} ${ - trigger.zone.length > 1 && index === trigger.zone.length - 1 - ? "or" - : "" - } ${computeStateName(states[zone]) || zone}`; + zones.push(computeStateName(states[zone]) || zone); } } } else { - zones = states[trigger.zone] - ? computeStateName(states[trigger.zone]) - : trigger.zone; + zones.push( + states[trigger.zone] + ? computeStateName(states[trigger.zone]) + : trigger.zone + ); } - return `When ${sources} ${trigger.event}s ${zones} ${ - zonesPlural ? "zones" : "zone" + const sourcesString = disjunctionFormatter.format(sources); + const zonesString = disjunctionFormatter.format(zones); + return `When ${sourcesString} ${trigger.event}s ${zonesString} ${ + zones.length > 1 ? "zones" : "zone" }`; } + // MQTT Trigger if (trigger.platform === "mqtt") { - return "When an MQTT message has been received"; + return hass.localize(`${triggerTranslationBaseKey}.mqtt.description.full`); } // Template Trigger if (trigger.platform === "template") { - let base = "When a template triggers"; + let duration = ""; if (trigger.for) { - const duration = describeDuration(trigger.for); - if (duration) { - base += ` for ${duration}`; - } + duration = describeDuration(trigger.for) ?? ""; } - return base; + + return hass.localize( + `${triggerTranslationBaseKey}.template.description.full`, + { hasDuration: duration !== "", duration: duration } + ); } // Webhook Trigger if (trigger.platform === "webhook") { - return "When a Webhook payload has been received"; + return hass.localize( + `${triggerTranslationBaseKey}.webhook.description.full` + ); } // Persistent Notification Trigger @@ -640,6 +630,10 @@ export const describeCondition = ( return condition.alias; } + const conjunctionFormatter = new Intl.ListFormat("en", { + style: "long", + type: "conjunction", + }); const disjunctionFormatter = new Intl.ListFormat("en", { style: "long", type: "disjunction", @@ -714,21 +708,20 @@ export const describeCondition = ( } if (Array.isArray(condition.entity_id)) { - let entities = ""; - for (const [index, entity] of condition.entity_id.entries()) { + const entities: string[] = []; + for (const entity of condition.entity_id.values()) { if (hass.states[entity]) { - entities += `${index > 0 ? "," : ""} ${ - condition.entity_id.length > 1 && - index === condition.entity_id.length - 1 - ? condition.match === "any" - ? "or" - : "and" - : "" - } ${computeStateName(hass.states[entity]) || entity}`; + entities.push(computeStateName(hass.states[entity]) || entity); } } - if (entities) { - base += ` ${entities} ${condition.entity_id.length > 1 ? "are" : "is"}`; + if (entities.length !== 0) { + const entitiesString = + condition.match === "any" + ? disjunctionFormatter.format(entities) + : conjunctionFormatter.format(entities); + base += ` ${entitiesString} ${ + condition.entity_id.length > 1 ? "are" : "is" + }`; } else { // no entity_id or empty array base += " an entity"; @@ -741,7 +734,7 @@ export const describeCondition = ( } is`; } - let states = ""; + const states: string[] = []; const stateObj = hass.states[ Array.isArray(condition.entity_id) @@ -749,12 +742,8 @@ export const describeCondition = ( : condition.entity_id ]; if (Array.isArray(condition.state)) { - for (const [index, state] of condition.state.entries()) { - states += `${index > 0 ? "," : ""} ${ - condition.state.length > 1 && index === condition.state.length - 1 - ? "or" - : "" - } '${ + for (const state of condition.state.values()) { + states.push( condition.attribute ? computeAttributeValueDisplay( hass.localize, @@ -764,7 +753,7 @@ export const describeCondition = ( hass.entities, condition.attribute, state - ) + ).toString() : computeStateDisplay( hass.localize, stateObj, @@ -773,10 +762,10 @@ export const describeCondition = ( hass.entities, state ) - }'`; + ); } } else if (condition.state !== "") { - states = `'${ + states.push( condition.attribute ? computeAttributeValueDisplay( hass.localize, @@ -794,15 +783,16 @@ export const describeCondition = ( hass.config, hass.entities, condition.state.toString() - ).toString() - }'`; + ) + ); } - if (!states) { - states = "a state"; + if (states.length === 0) { + states.push("a state"); } - base += ` ${states}`; + const statesString = disjunctionFormatter.format(states); + base += ` ${statesString}`; if (condition.for) { const duration = describeDuration(condition.for); @@ -891,17 +881,7 @@ export const describeCondition = ( `ui.panel.config.automation.editor.conditions.type.time.weekdays.${d}` ) ); - const last = localizedDays.pop(); - - result += " day is " + localizedDays.join(", "); - - if (localizedDays.length) { - if (localizedDays.length > 1) { - result += ","; - } - result += " or "; - } - result += last; + result += " day is " + disjunctionFormatter.format(localizedDays); } return result; @@ -953,9 +933,9 @@ export const describeCondition = ( const states = hass.states; if (Array.isArray(condition.entity_id)) { - for (const [entity] of condition.entity_id.entries()) { + for (const entity of condition.entity_id.values()) { if (states[entity]) { - entities.push(`${computeStateName(states[entity]) || entity}`); + entities.push(computeStateName(states[entity]) || entity); } } } else { @@ -967,9 +947,9 @@ export const describeCondition = ( } if (Array.isArray(condition.zone)) { - for (const [zone] of condition.zone.entries()) { + for (const zone of condition.zone.values()) { if (states[zone]) { - zones.push(`${computeStateName(states[zone]) || zone}`); + zones.push(computeStateName(states[zone]) || zone); } } } else { diff --git a/src/data/climate.ts b/src/data/climate.ts index b72f845cbf..705f83fe15 100644 --- a/src/data/climate.ts +++ b/src/data/climate.ts @@ -16,6 +16,7 @@ export const CLIMATE_PRESET_NONE = "none"; export type HvacAction = | "off" + | "preheating" | "heating" | "cooling" | "drying" @@ -77,6 +78,7 @@ export const HVAC_ACTION_TO_MODE: Record = { cooling: "cool", drying: "dry", fan: "fan_only", + preheating: "heat", heating: "heat", idle: "off", off: "off", diff --git a/src/data/light.ts b/src/data/light.ts index 42a2507731..ed42fdeb5f 100644 --- a/src/data/light.ts +++ b/src/data/light.ts @@ -159,3 +159,5 @@ export const computeDefaultFavoriteColors = ( return colors; }; + +export const formatTempColor = (value: number) => `${value} K`; diff --git a/src/data/lock.ts b/src/data/lock.ts new file mode 100644 index 0000000000..a6c9914559 --- /dev/null +++ b/src/data/lock.ts @@ -0,0 +1,20 @@ +import { + HassEntityAttributeBase, + HassEntityBase, +} from "home-assistant-js-websocket"; + +export const FORMAT_TEXT = "text"; +export const FORMAT_NUMBER = "number"; + +export const enum LockEntityFeature { + OPEN = 1, +} + +interface LockEntityAttributes extends HassEntityAttributeBase { + code_format?: string; + changed_by?: string | null; +} + +export interface LockEntity extends HassEntityBase { + attributes: LockEntityAttributes; +} diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts index 432cb6167b..091ced7c7b 100644 --- a/src/data/lovelace.ts +++ b/src/data/lovelace.ts @@ -152,6 +152,12 @@ export interface MoreInfoActionConfig extends BaseActionConfig { action: "more-info"; } +export interface AssistActionConfig extends BaseActionConfig { + action: "assist"; + pipeline_id?: string; + start_listening?: boolean; +} + export interface NoActionConfig extends BaseActionConfig { action: "none"; } @@ -180,6 +186,7 @@ export type ActionConfig = | NavigateActionConfig | UrlActionConfig | MoreInfoActionConfig + | AssistActionConfig | NoActionConfig | CustomActionConfig; diff --git a/src/data/script.ts b/src/data/script.ts index a1288158db..b679b0a22f 100644 --- a/src/data/script.ts +++ b/src/data/script.ts @@ -33,6 +33,7 @@ export const isMaxMode = arrayLiteralIncludes(MODES_MAX); export const baseActionStruct = object({ alias: optional(string()), + continue_on_error: optional(boolean()), enabled: optional(boolean()), }); @@ -99,6 +100,7 @@ export interface BlueprintScriptConfig extends ManualScriptConfig { interface BaseAction { alias?: string; + continue_on_error?: boolean; enabled?: boolean; } @@ -230,14 +232,10 @@ interface UnknownAction extends BaseAction { [key: string]: unknown; } -export type Action = +export type NonConditionAction = | EventAction | DeviceAction | ServiceAction - | Condition - | ShorthandAndCondition - | ShorthandOrCondition - | ShorthandNotCondition | DelayAction | SceneAction | WaitAction @@ -251,6 +249,13 @@ export type Action = | ParallelAction | UnknownAction; +export type Action = + | NonConditionAction + | Condition + | ShorthandAndCondition + | ShorthandOrCondition + | ShorthandNotCondition; + export interface ActionTypes { delay: DelayAction; wait_template: WaitAction; diff --git a/src/data/selector.ts b/src/data/selector.ts index 6523ede4f6..acbac41a43 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -345,8 +345,7 @@ export interface TemplateSelector { } export interface ThemeSelector { - // eslint-disable-next-line @typescript-eslint/ban-types - theme: {} | null; + theme: { include_default?: boolean } | null; } export interface TimeSelector { // eslint-disable-next-line @typescript-eslint/ban-types diff --git a/src/dialogs/more-info/components/alarm_control_panel/dialog-enter-code.ts b/src/dialogs/enter-code/dialog-enter-code.ts similarity index 91% rename from src/dialogs/more-info/components/alarm_control_panel/dialog-enter-code.ts rename to src/dialogs/enter-code/dialog-enter-code.ts index acacb74666..b4cfc63016 100644 --- a/src/dialogs/more-info/components/alarm_control_panel/dialog-enter-code.ts +++ b/src/dialogs/enter-code/dialog-enter-code.ts @@ -1,14 +1,15 @@ import { mdiCheck, mdiClose } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; -import { fireEvent } from "../../../../common/dom/fire_event"; -import "../../../../components/ha-button"; -import "../../../../components/ha-control-button"; -import { createCloseHeading } from "../../../../components/ha-dialog"; -import "../../../../components/ha-textfield"; -import type { HaTextField } from "../../../../components/ha-textfield"; -import { HomeAssistant } from "../../../../types"; -import { HassDialog } from "../../../make-dialog-manager"; +import { ifDefined } from "lit/directives/if-defined"; +import { fireEvent } from "../../common/dom/fire_event"; +import "../../components/ha-button"; +import "../../components/ha-control-button"; +import { createCloseHeading } from "../../components/ha-dialog"; +import "../../components/ha-textfield"; +import type { HaTextField } from "../../components/ha-textfield"; +import { HomeAssistant } from "../../types"; +import { HassDialog } from "../make-dialog-manager"; import { EnterCodeDialogParams } from "./show-enter-code-dialog"; const BUTTONS = [ @@ -72,7 +73,8 @@ export class DialogEnterCode } private _inputValueChange(e) { - const val = (e.currentTarget! as any).value; + const field = e.currentTarget as HaTextField; + const val = field.value; this._showClearButton = !!val; } @@ -97,6 +99,7 @@ export class DialogEnterCode id="code" .label=${this.hass.localize("ui.dialogs.enter_code.input_label")} type="password" + pattern=${ifDefined(this._dialogParams.codePattern)} input-mode="text" > diff --git a/src/dialogs/more-info/components/alarm_control_panel/show-enter-code-dialog.ts b/src/dialogs/enter-code/show-enter-code-dialog.ts similarity index 91% rename from src/dialogs/more-info/components/alarm_control_panel/show-enter-code-dialog.ts rename to src/dialogs/enter-code/show-enter-code-dialog.ts index 802ae23fb1..6356c20364 100644 --- a/src/dialogs/more-info/components/alarm_control_panel/show-enter-code-dialog.ts +++ b/src/dialogs/enter-code/show-enter-code-dialog.ts @@ -1,7 +1,8 @@ -import { fireEvent } from "../../../../common/dom/fire_event"; +import { fireEvent } from "../../common/dom/fire_event"; export interface EnterCodeDialogParams { codeFormat: "text" | "number"; + codePattern?: string; submitText?: string; cancelText?: string; title?: string; diff --git a/src/dialogs/more-info/components/alarm_control_panel/ha-more-info-alarm_control_panel-modes.ts b/src/dialogs/more-info/components/alarm_control_panel/ha-more-info-alarm_control_panel-modes.ts index ea8881c5b5..22f028621b 100644 --- a/src/dialogs/more-info/components/alarm_control_panel/ha-more-info-alarm_control_panel-modes.ts +++ b/src/dialogs/more-info/components/alarm_control_panel/ha-more-info-alarm_control_panel-modes.ts @@ -14,7 +14,7 @@ import { } from "../../../../data/alarm_control_panel"; import { UNAVAILABLE } from "../../../../data/entity"; import { HomeAssistant } from "../../../../types"; -import { showEnterCodeDialogDialog } from "./show-enter-code-dialog"; +import { showEnterCodeDialogDialog } from "../../../enter-code/show-enter-code-dialog"; @customElement("ha-more-info-alarm_control_panel-modes") export class HaMoreInfoAlarmControlPanelModes extends LitElement { diff --git a/src/dialogs/more-info/components/lights/dialog-light-color-favorite.ts b/src/dialogs/more-info/components/lights/dialog-light-color-favorite.ts index 3c72999412..410ba70c04 100644 --- a/src/dialogs/more-info/components/lights/dialog-light-color-favorite.ts +++ b/src/dialogs/more-info/components/lights/dialog-light-color-favorite.ts @@ -1,16 +1,28 @@ import { mdiClose } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/ha-button"; import "../../../../components/ha-dialog"; import "../../../../components/ha-dialog-header"; -import { EntityRegistryEntry } from "../../../../data/entity_registry"; -import { LightColor } from "../../../../data/light"; +import "../../../../components/ha-icon-button-toggle"; +import type { EntityRegistryEntry } from "../../../../data/entity_registry"; +import { + formatTempColor, + LightColor, + LightColorMode, + LightEntity, + lightSupportsColor, + lightSupportsColorMode, +} from "../../../../data/light"; import { haStyleDialog } from "../../../../resources/styles"; -import { HomeAssistant } from "../../../../types"; -import "./light-color-picker"; -import { LightColorFavoriteDialogParams } from "./show-dialog-light-color-favorite"; +import type { HomeAssistant } from "../../../../types"; +import "./light-color-rgb-picker"; +import "./light-color-temp-picker"; +import type { LightColorFavoriteDialogParams } from "./show-dialog-light-color-favorite"; + +export type LightPickerMode = "color_temp" | "color"; @customElement("dialog-light-color-favorite") class DialogLightColorFavorite extends LitElement { @@ -22,11 +34,26 @@ class DialogLightColorFavorite extends LitElement { @state() _color?: LightColor; + @state() private _mode?: LightPickerMode; + + @state() private _modes: LightPickerMode[] = []; + + @state() private _currentValue?: string; + + private _colorHovered(ev: CustomEvent) { + if (ev.detail && "color_temp_kelvin" in ev.detail) { + this._currentValue = formatTempColor(ev.detail.color_temp_kelvin); + } else { + this._currentValue = undefined; + } + } + public async showDialog( dialogParams: LightColorFavoriteDialogParams ): Promise { this._entry = dialogParams.entry; this._dialogParams = dialogParams; + this._updateModes(dialogParams.defaultMode); await this.updateComplete; } @@ -37,10 +64,43 @@ class DialogLightColorFavorite extends LitElement { fireEvent(this, "dialog-closed", { dialog: this.localName }); } + private _updateModes(defaultMode?: LightPickerMode) { + const supportsTemp = lightSupportsColorMode( + this.stateObj!, + LightColorMode.COLOR_TEMP + ); + + const supportsColor = lightSupportsColor(this.stateObj!); + + const modes: LightPickerMode[] = []; + if (supportsColor) { + modes.push("color"); + } + if (supportsTemp) { + modes.push("color_temp"); + } + + this._modes = modes; + this._mode = + defaultMode ?? + (this.stateObj!.attributes.color_mode + ? this.stateObj!.attributes.color_mode === LightColorMode.COLOR_TEMP + ? LightColorMode.COLOR_TEMP + : "color" + : this._modes[0]); + } + private _colorChanged(ev: CustomEvent) { this._color = ev.detail; } + get stateObj() { + return ( + this._entry && + (this.hass.states[this._entry.entity_id] as LightEntity | undefined) + ); + } + private async _cancel() { this._dialogParams?.cancel?.(); this.closeDialog(); @@ -55,8 +115,16 @@ class DialogLightColorFavorite extends LitElement { this.closeDialog(); } + private _modeChanged(ev): void { + const newMode = ev.currentTarget.mode; + if (newMode === this._mode) { + return; + } + this._mode = newMode; + } + protected render() { - if (!this._entry) { + if (!this._entry || !this.stateObj) { return nothing; } @@ -76,13 +144,58 @@ class DialogLightColorFavorite extends LitElement { > ${this._dialogParams?.title} - - +
+ ${this._currentValue} + ${this._modes.length > 1 + ? html` +
+ ${this._modes.map( + (value) => + html` + + + + ` + )} +
+ ` + : nothing} +
+ +
+ ${this._mode === "color_temp" + ? html` + + + ` + : nothing} + ${this._mode === "color" + ? html` + + + ` + : nothing} +
${this.hass.localize("ui.common.cancel")} @@ -101,16 +214,10 @@ class DialogLightColorFavorite extends LitElement { --dialog-content-padding: 0; } - light-color-picker { - display: flex; - flex-direction: column; - flex: 1; - } - @media all and (max-width: 450px), all and (max-height: 500px) { ha-dialog { --dialog-surface-margin-top: 100px; - --mdc-dialog-min-height: calc(100% - 100px); + --mdc-dialog-min-height: auto; --mdc-dialog-max-height: calc(100% - 100px); --ha-dialog-border-radius: var( --ha-dialog-bottom-sheet-border-radius, @@ -118,6 +225,54 @@ class DialogLightColorFavorite extends LitElement { ); } } + + .content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 24px; + flex: 1; + } + .modes { + display: flex; + flex-direction: row; + justify-content: flex-end; + padding: 0 24px; + } + .wheel { + width: 30px; + height: 30px; + flex: none; + border-radius: 15px; + } + .wheel.color { + background-image: url("/static/images/color_wheel.png"); + background-size: cover; + } + .wheel.color_temp { + background: linear-gradient( + 0, + rgb(166, 209, 255) 0%, + white 50%, + rgb(255, 160, 0) 100% + ); + } + .value { + pointer-events: none; + position: absolute; + top: 0; + left: 0; + right: 0; + margin: auto; + font-style: normal; + font-weight: 500; + font-size: 16px; + height: 48px; + line-height: 48px; + letter-spacing: 0.1px; + text-align: center; + } `, ]; } diff --git a/src/dialogs/more-info/components/lights/ha-more-info-light-favorite-colors.ts b/src/dialogs/more-info/components/lights/ha-more-info-light-favorite-colors.ts index c8c3c8cc1b..ad00a5b105 100644 --- a/src/dialogs/more-info/components/lights/ha-more-info-light-favorite-colors.ts +++ b/src/dialogs/more-info/components/lights/ha-more-info-light-favorite-colors.ts @@ -30,10 +30,16 @@ import { } from "../../../../resources/sortable.ondemand"; import { HomeAssistant } from "../../../../types"; import { showConfirmationDialog } from "../../../generic/show-dialog-box"; +import type { LightPickerMode } from "./dialog-light-color-favorite"; import "./ha-favorite-color-button"; -import type { LightPickerMode } from "./light-color-picker"; import { showLightColorFavoriteDialog } from "./show-dialog-light-color-favorite"; +declare global { + interface HASSDomEvents { + "favorite-color-edit-started"; + } +} + @customElement("ha-more-info-light-favorite-colors") export class HaMoreInfoLightFavoriteColors extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -147,8 +153,8 @@ export class HaMoreInfoLightFavoriteColors extends LitElement { private _edit = async (index) => { // Make sure the current favorite color is set + fireEvent(this, "favorite-color-edit-started"); await this._apply(index); - const defaultMode: LightPickerMode = "color_temp_kelvin" in this._favoriteColors[index] ? "color_temp" diff --git a/src/dialogs/more-info/components/lights/ha-more-info-view-light-color-picker.ts b/src/dialogs/more-info/components/lights/ha-more-info-view-light-color-picker.ts deleted file mode 100644 index b556443f5e..0000000000 --- a/src/dialogs/more-info/components/lights/ha-more-info-view-light-color-picker.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; -import { customElement, property } from "lit/decorators"; -import { HomeAssistant } from "../../../../types"; -import "./light-color-picker"; -import { LightColorPickerViewParams } from "./show-view-light-color-picker"; - -@customElement("ha-more-info-view-light-color-picker") -class MoreInfoViewLightColorPicker extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public params?: LightColorPickerViewParams; - - protected render() { - if (!this.params) { - return nothing; - } - - return html` - - - `; - } - - static get styles(): CSSResultGroup { - return [ - css` - :host { - position: relative; - display: flex; - flex-direction: column; - flex: 1; - } - light-color-picker { - display: flex; - flex-direction: column; - flex: 1; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ha-more-info-view-light-color-picker": MoreInfoViewLightColorPicker; - } -} diff --git a/src/dialogs/more-info/components/lights/light-color-picker.ts b/src/dialogs/more-info/components/lights/light-color-rgb-picker.ts similarity index 59% rename from src/dialogs/more-info/components/lights/light-color-picker.ts rename to src/dialogs/more-info/components/lights/light-color-rgb-picker.ts index 720cd2a5bb..cf50ae5c04 100644 --- a/src/dialogs/more-info/components/lights/light-color-picker.ts +++ b/src/dialogs/more-info/components/lights/light-color-rgb-picker.ts @@ -23,21 +23,18 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import { throttle } from "../../../../common/util/throttle"; import "../../../../components/ha-button-toggle-group"; import "../../../../components/ha-hs-color-picker"; +import "../../../../components/ha-icon"; import "../../../../components/ha-icon-button-prev"; import "../../../../components/ha-labeled-slider"; import "../../../../components/ha-temp-color-picker"; import { - LightColor, getLightCurrentModeRgbColor, + LightColor, LightColorMode, LightEntity, - lightSupportsColor, lightSupportsColorMode, } from "../../../../data/light"; import { HomeAssistant } from "../../../../types"; -import "../../../../components/ha-icon"; - -export type LightPickerMode = "color_temp" | "color"; declare global { interface HASSDomEvents { @@ -45,13 +42,11 @@ declare global { } } -@customElement("light-color-picker") -class LightColorPicker extends LitElement { +@customElement("light-color-rgb-picker") +class LightRgbColorPicker extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public entityId!: string; - - @property() public defaultMode?: LightPickerMode; + @property({ attribute: false }) public stateObj!: LightEntity; @state() private _cwSliderValue?: number; @@ -65,16 +60,6 @@ class LightColorPicker extends LitElement { @state() private _hsPickerValue?: [number, number]; - @state() private _ctPickerValue?: number; - - @state() private _mode?: LightPickerMode; - - @state() private _modes: LightPickerMode[] = []; - - get stateObj() { - return this.hass.states[this.entityId] as LightEntity | undefined; - } - protected render() { if (!this.stateObj) { return nothing; @@ -100,135 +85,89 @@ class LightColorPicker extends LitElement { : ""; return html` - ${this._modes.length > 1 +
+ + + + +
+ ${supportsRgbw || supportsRgbww + ? html`` + : nothing} + ${supportsRgbw ? html` - - ${this._modes.map( - (value) => - html`` - )} - + + ` + : nothing} + ${supportsRgbww + ? html` + + ` : nothing} -
- ${this._mode === LightColorMode.COLOR_TEMP - ? html` -

- ${this._ctPickerValue ? `${this._ctPickerValue} K` : nothing} -

- - - ` - : nothing} - ${this._mode === "color" - ? html` -
- - - - -
- ${supportsRgbw || supportsRgbww - ? html`` - : nothing} - ${supportsRgbw - ? html` - - ` - : nothing} - ${supportsRgbww - ? html` - - - ` - : nothing} - ` - : nothing} -
`; } public _updateSliderValues() { const stateObj = this.stateObj; - if (stateObj?.state === "on") { + if (stateObj.state === "on") { this._brightnessAdjusted = undefined; if ( stateObj.attributes.color_mode === LightColorMode.RGB && @@ -242,10 +181,6 @@ class LightColorPicker extends LitElement { this._brightnessAdjusted = maxVal; } } - this._ctPickerValue = - stateObj.attributes.color_mode === LightColorMode.COLOR_TEMP - ? stateObj.attributes.color_temp_kelvin - : undefined; this._wvSliderValue = stateObj.attributes.color_mode === LightColorMode.RGBW && @@ -273,8 +208,7 @@ class LightColorPicker extends LitElement { ? rgb2hs(currentRgbColor.slice(0, 3) as [number, number, number]) : undefined; } else { - this._hsPickerValue = [0, 0]; - this._ctPickerValue = undefined; + this._hsPickerValue = undefined; this._wvSliderValue = undefined; this._cwSliderValue = undefined; this._wwSliderValue = undefined; @@ -288,43 +222,9 @@ class LightColorPicker extends LitElement { return; } - if (changedProps.has("entityId")) { - const supportsTemp = lightSupportsColorMode( - this.stateObj!, - LightColorMode.COLOR_TEMP - ); - - const supportsColor = lightSupportsColor(this.stateObj!); - - const modes: LightPickerMode[] = []; - if (supportsColor) { - modes.push("color"); - } - if (supportsTemp) { - modes.push("color_temp"); - } - - this._modes = modes; - this._mode = - this.defaultMode ?? - (this.stateObj!.attributes.color_mode - ? this.stateObj!.attributes.color_mode === LightColorMode.COLOR_TEMP - ? LightColorMode.COLOR_TEMP - : "color" - : this._modes[0]); - } - this._updateSliderValues(); } - private _handleTabChanged(ev: CustomEvent): void { - const newMode = this._modes[ev.detail.index]; - if (newMode === this._mode) { - return; - } - this._mode = newMode; - } - private _hsColorCursorMoved(ev: CustomEvent) { if (!ev.detail.value) { return; @@ -404,40 +304,6 @@ class LightColorPicker extends LitElement { this._updateColor(); } - private _ctColorCursorMoved(ev: CustomEvent) { - const ct = ev.detail.value; - - if (isNaN(ct) || this._ctPickerValue === ct) { - return; - } - - this._ctPickerValue = ct; - - this._throttleUpdateColorTemp(); - } - - private _throttleUpdateColorTemp = throttle(() => { - this._updateColorTemp(); - }, 500); - - private _ctColorChanged(ev: CustomEvent) { - const ct = ev.detail.value; - - if (isNaN(ct) || this._ctPickerValue === ct) { - return; - } - - this._ctPickerValue = ct; - - this._updateColorTemp(); - } - - private _updateColorTemp() { - const color_temp_kelvin = this._ctPickerValue!; - - this._applyColor({ color_temp_kelvin }); - } - private _wvSliderChanged(ev: CustomEvent) { const target = ev.target as any; let wv = Number(target.value); @@ -574,19 +440,12 @@ class LightColorPicker extends LitElement { display: flex; flex-direction: column; } - .content { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 24px; - flex: 1; - } .native-color-picker { position: absolute; top: 0; right: 0; + z-index: 1; } .native-color-picker ha-svg-icon { @@ -639,37 +498,18 @@ class LightColorPicker extends LitElement { .color-container { position: relative; - max-width: 300px; - min-width: 200px; - margin: 0 0 44px 0; - padding-top: 44px; } ha-hs-color-picker { - width: 100%; - } - - ha-temp-color-picker { - max-width: 300px; - min-width: 200px; - margin: 20px 0 44px 0; + height: 45vh; + max-height: 320px; + min-height: 200px; } ha-labeled-slider { width: 100%; } - .color-temp-value { - font-style: normal; - font-weight: 500; - font-size: 16px; - height: 24px; - line-height: 24px; - letter-spacing: 0.1px; - margin: 0; - direction: ltr; - } - hr { border-color: var(--divider-color); border-bottom: none; @@ -682,6 +522,6 @@ class LightColorPicker extends LitElement { declare global { interface HTMLElementTagNameMap { - "light-color-picker": LightColorPicker; + "light-color-rgb-picker": LightRgbColorPicker; } } diff --git a/src/dialogs/more-info/components/lights/light-color-temp-picker.ts b/src/dialogs/more-info/components/lights/light-color-temp-picker.ts new file mode 100644 index 0000000000..4543c0dc88 --- /dev/null +++ b/src/dialogs/more-info/components/lights/light-color-temp-picker.ts @@ -0,0 +1,146 @@ +import { + css, + CSSResultGroup, + html, + LitElement, + nothing, + PropertyValues, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { throttle } from "../../../../common/util/throttle"; +import "../../../../components/ha-temp-color-picker"; +import { + LightColor, + LightColorMode, + LightEntity, +} from "../../../../data/light"; +import { HomeAssistant } from "../../../../types"; + +declare global { + interface HASSDomEvents { + "color-changed": LightColor; + "color-hovered": LightColor | undefined; + } +} + +@customElement("light-color-temp-picker") +class LightColorTempPicker extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj!: LightEntity; + + @state() private _ctPickerValue?: number; + + protected render() { + if (!this.stateObj) { + return nothing; + } + + return html` + + + `; + } + + public _updateSliderValues() { + const stateObj = this.stateObj; + + if (stateObj.state === "on") { + this._ctPickerValue = + stateObj.attributes.color_mode === LightColorMode.COLOR_TEMP + ? stateObj.attributes.color_temp_kelvin + : undefined; + } else { + this._ctPickerValue = undefined; + } + } + + public willUpdate(changedProps: PropertyValues) { + super.willUpdate(changedProps); + + if (!changedProps.has("stateObj")) { + return; + } + + this._updateSliderValues(); + } + + private _ctColorCursorMoved(ev: CustomEvent) { + const ct = ev.detail.value; + + if (isNaN(ct) || this._ctPickerValue === ct) { + return; + } + + this._ctPickerValue = ct; + + fireEvent(this, "color-hovered", { + color_temp_kelvin: ct, + }); + + this._throttleUpdateColorTemp(); + } + + private _throttleUpdateColorTemp = throttle(() => { + this._updateColorTemp(); + }, 500); + + private _ctColorChanged(ev: CustomEvent) { + const ct = ev.detail.value; + + fireEvent(this, "color-hovered", undefined); + + if (isNaN(ct) || this._ctPickerValue === ct) { + return; + } + + this._ctPickerValue = ct; + + this._updateColorTemp(); + } + + private _updateColorTemp() { + const color_temp_kelvin = this._ctPickerValue!; + + this._applyColor({ color_temp_kelvin }); + } + + private _applyColor(color: LightColor, params?: Record) { + fireEvent(this, "color-changed", color); + this.hass.callService("light", "turn_on", { + entity_id: this.stateObj!.entity_id, + ...color, + ...params, + }); + } + + static get styles(): CSSResultGroup { + return [ + css` + :host { + display: flex; + flex-direction: column; + } + + ha-temp-color-picker { + height: 45vh; + max-height: 320px; + min-height: 200px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "light-color-temp-picker": LightColorTempPicker; + } +} diff --git a/src/dialogs/more-info/components/lights/show-dialog-light-color-favorite.ts b/src/dialogs/more-info/components/lights/show-dialog-light-color-favorite.ts index 28f0af8622..d8bc865093 100644 --- a/src/dialogs/more-info/components/lights/show-dialog-light-color-favorite.ts +++ b/src/dialogs/more-info/components/lights/show-dialog-light-color-favorite.ts @@ -1,7 +1,7 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import { ExtEntityRegistryEntry } from "../../../../data/entity_registry"; import { LightColor } from "../../../../data/light"; -import type { LightPickerMode } from "./light-color-picker"; +import type { LightPickerMode } from "./dialog-light-color-favorite"; export interface LightColorFavoriteDialogParams { entry: ExtEntityRegistryEntry; diff --git a/src/dialogs/more-info/components/lights/show-view-light-color-picker.ts b/src/dialogs/more-info/components/lights/show-view-light-color-picker.ts deleted file mode 100644 index 5a2bd40fa1..0000000000 --- a/src/dialogs/more-info/components/lights/show-view-light-color-picker.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { fireEvent } from "../../../../common/dom/fire_event"; -import type { LightPickerMode } from "./light-color-picker"; - -export interface LightColorPickerViewParams { - entityId: string; - defaultMode: LightPickerMode; -} - -export const loadLightColorPickerView = () => - import("./ha-more-info-view-light-color-picker"); - -export const showLightColorPickerView = ( - element: HTMLElement, - title: string, - params: LightColorPickerViewParams -): void => { - fireEvent(element, "show-child-view", { - viewTag: "ha-more-info-view-light-color-picker", - viewImport: loadLightColorPickerView, - viewTitle: title, - viewParams: params, - }); -}; diff --git a/src/dialogs/more-info/components/lock/ha-more-info-lock-toggle.ts b/src/dialogs/more-info/components/lock/ha-more-info-lock-toggle.ts new file mode 100644 index 0000000000..f8a005002c --- /dev/null +++ b/src/dialogs/more-info/components/lock/ha-more-info-lock-toggle.ts @@ -0,0 +1,199 @@ +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; +import { domainIcon } from "../../../../common/entity/domain_icon"; +import { stateColorCss } from "../../../../common/entity/state_color"; +import "../../../../components/ha-control-button"; +import "../../../../components/ha-control-switch"; +import { UNAVAILABLE, UNKNOWN } from "../../../../data/entity"; +import { forwardHaptic } from "../../../../data/haptics"; +import { LockEntity } from "../../../../data/lock"; +import { HomeAssistant } from "../../../../types"; +import { showEnterCodeDialogDialog } from "../../../enter-code/show-enter-code-dialog"; + +@customElement("ha-more-info-lock-toggle") +export class HaMoreInfoLockToggle extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj!: LockEntity; + + @state() private _isOn = false; + + public willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); + if (changedProps.has("stateObj")) { + this._isOn = + this.stateObj.state === "locked" || this.stateObj.state === "locking"; + } + } + + private _valueChanged(ev) { + const checked = ev.target.checked as boolean; + + if (checked) { + this._turnOn(); + } else { + this._turnOff(); + } + } + + private async _turnOn() { + this._isOn = true; + try { + await this._callService(true); + } catch (err) { + this._isOn = false; + } + } + + private async _turnOff() { + this._isOn = false; + try { + await this._callService(false); + } catch (err) { + this._isOn = true; + } + } + + private async _callService(turnOn: boolean): Promise { + if (!this.hass || !this.stateObj) { + return; + } + forwardHaptic("light"); + + let code: string | undefined; + + if (this.stateObj.attributes.code_format) { + const response = await showEnterCodeDialogDialog(this, { + codeFormat: "text", + codePattern: this.stateObj.attributes.code_format, + title: this.hass.localize( + `ui.dialogs.more_info_control.lock.${turnOn ? "lock" : "unlock"}` + ), + submitText: this.hass.localize( + `ui.dialogs.more_info_control.lock.${turnOn ? "lock" : "unlock"}` + ), + }); + if (response == null) { + throw new Error("cancel"); + } + code = response; + } + + await this.hass.callService("lock", turnOn ? "lock" : "unlock", { + entity_id: this.stateObj.entity_id, + code, + }); + } + + protected render(): TemplateResult { + const locking = this.stateObj.state === "locking"; + const unlocking = this.stateObj.state === "unlocking"; + + const color = stateColorCss(this.stateObj); + + const onIcon = domainIcon( + "lock", + this.stateObj, + locking ? "locking" : "locked" + ); + + const offIcon = domainIcon( + "lock", + this.stateObj, + unlocking ? "unlocking" : "unlocked" + ); + + if (this.stateObj.state === UNKNOWN) { + return html` +
+ + + + + + +
+ `; + } + + return html` + + + `; + } + + static get styles(): CSSResultGroup { + return css` + ha-control-switch { + height: 45vh; + max-height: 320px; + min-height: 200px; + --control-switch-thickness: 100px; + --control-switch-border-radius: 24px; + --control-switch-padding: 6px; + --mdc-icon-size: 24px; + } + .buttons { + display: flex; + flex-direction: column; + width: 100px; + height: 45vh; + max-height: 320px; + min-height: 200px; + padding: 6px; + box-sizing: border-box; + } + ha-control-button { + flex: 1; + width: 100%; + --control-button-border-radius: 18px; + --mdc-icon-size: 24px; + } + ha-control-button.active { + --control-button-icon-color: white; + --control-button-background-color: var(--color); + --control-button-background-opacity: 1; + } + ha-control-button:not(:last-child) { + margin-bottom: 6px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-more-info-lock-toggle": HaMoreInfoLockToggle; + } +} diff --git a/src/dialogs/more-info/const.ts b/src/dialogs/more-info/const.ts index 8675c499e1..79af1e13d3 100644 --- a/src/dialogs/more-info/const.ts +++ b/src/dialogs/more-info/const.ts @@ -22,6 +22,7 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [ "fan", "input_boolean", "light", + "lock", "siren", "switch", ]; diff --git a/src/dialogs/more-info/controls/more-info-alarm_control_panel.ts b/src/dialogs/more-info/controls/more-info-alarm_control_panel.ts index 7fadaa5776..7f73407ae1 100644 --- a/src/dialogs/more-info/controls/more-info-alarm_control_panel.ts +++ b/src/dialogs/more-info/controls/more-info-alarm_control_panel.ts @@ -7,8 +7,8 @@ import { stateColorCss } from "../../../common/entity/state_color"; import "../../../components/ha-outlined-button"; import { AlarmControlPanelEntity } from "../../../data/alarm_control_panel"; import type { HomeAssistant } from "../../../types"; +import { showEnterCodeDialogDialog } from "../../enter-code/show-enter-code-dialog"; import "../components/alarm_control_panel/ha-more-info-alarm_control_panel-modes"; -import { showEnterCodeDialogDialog } from "../components/alarm_control_panel/show-enter-code-dialog"; import { moreInfoControlStyle } from "../components/ha-more-info-control-style"; import "../components/ha-more-info-state-header"; diff --git a/src/dialogs/more-info/controls/more-info-cover.ts b/src/dialogs/more-info/controls/more-info-cover.ts index 7fd9d9a8ac..3ea0907245 100644 --- a/src/dialogs/more-info/controls/more-info-cover.ts +++ b/src/dialogs/more-info/controls/more-info-cover.ts @@ -11,6 +11,8 @@ import { customElement, property, state } from "lit/decorators"; import { computeStateDisplay } from "../../../common/entity/compute_state_display"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-attributes"; +import "../../../components/ha-icon-button-group"; +import "../../../components/ha-icon-button-toggle"; import { computeCoverPositionStateDisplay, CoverEntity, @@ -24,6 +26,8 @@ import "../components/cover/ha-more-info-cover-toggle"; import { moreInfoControlStyle } from "../components/ha-more-info-control-style"; import "../components/ha-more-info-state-header"; +type Mode = "position" | "button"; + @customElement("more-info-cover") class MoreInfoCover extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -34,10 +38,10 @@ class MoreInfoCover extends LitElement { @state() private _liveTilt?: number; - @state() private _mode?: "position" | "button"; + @state() private _mode?: Mode; - private _toggleMode() { - this._mode = this._mode === "position" ? "button" : "position"; + private _setMode(ev) { + this._mode = ev.currentTarget.mode; } private _positionSliderMoved(ev) { @@ -192,19 +196,26 @@ class MoreInfoCover extends LitElement { (supportsPosition || supportsTiltPosition) && (supportsOpenClose || supportsTilt) ? html` -
- + -
+ .selected=${this._mode === "position"} + .path=${mdiMenu} + .mode=${"position"} + @click=${this._setMode} + > + + ` : nothing } diff --git a/src/dialogs/more-info/controls/more-info-light.ts b/src/dialogs/more-info/controls/more-info-light.ts index 45597b228d..730175c8dd 100644 --- a/src/dialogs/more-info/controls/more-info-light.ts +++ b/src/dialogs/more-info/controls/more-info-light.ts @@ -1,5 +1,6 @@ import "@material/mwc-list/mwc-list-item"; import { + mdiBrightness6, mdiCreation, mdiFileWordBox, mdiLightbulb, @@ -24,6 +25,8 @@ import { supportsFeature } from "../../../common/entity/supports-feature"; import { blankBeforePercent } from "../../../common/translations/blank_before_percent"; import "../../../components/ha-attributes"; import "../../../components/ha-button-menu"; +import "../../../components/ha-icon-button-group"; +import "../../../components/ha-icon-button-toggle"; import "../../../components/ha-outlined-button"; import "../../../components/ha-outlined-icon-button"; import "../../../components/ha-select"; @@ -31,6 +34,7 @@ import { UNAVAILABLE } from "../../../data/entity"; import { ExtEntityRegistryEntry } from "../../../data/entity_registry"; import { forwardHaptic } from "../../../data/haptics"; import { + formatTempColor, LightColorMode, LightEntity, LightEntityFeature, @@ -46,7 +50,10 @@ import "../components/ha-more-info-toggle"; import "../components/lights/ha-favorite-color-button"; import "../components/lights/ha-more-info-light-brightness"; import "../components/lights/ha-more-info-light-favorite-colors"; -import { showLightColorPickerView } from "../components/lights/show-view-light-color-picker"; +import "../components/lights/light-color-rgb-picker"; +import "../components/lights/light-color-temp-picker"; + +type MainControl = "brightness" | "color_temp" | "color"; @customElement("more-info-light") class MoreInfoLight extends LitElement { @@ -62,12 +69,24 @@ class MoreInfoLight extends LitElement { @state() private _selectedBrightness?: number; + @state() private _colorTempPreview?: number; + + @state() private _mainControl: MainControl = "brightness"; + private _brightnessChanged(ev) { const value = (ev.detail as any).value; if (isNaN(value)) return; this._selectedBrightness = value; } + private _tempColorHovered(ev: CustomEvent) { + if (ev.detail && "color_temp_kelvin" in ev.detail) { + this._colorTempPreview = ev.detail.color_temp_kelvin; + } else { + this._colorTempPreview = undefined; + } + } + protected updated(changedProps: PropertyValues): void { if (changedProps.has("stateObj")) { this._selectedBrightness = this.stateObj?.attributes.brightness @@ -77,6 +96,28 @@ class MoreInfoLight extends LitElement { } } + private _setMainControl(ev: any) { + ev.stopPropagation(); + this._mainControl = ev.currentTarget.control; + } + + private _resetMainControl(ev: any) { + ev.stopPropagation(); + this._mainControl = "brightness"; + } + + private get _stateOverride() { + if (this._colorTempPreview) { + return formatTempColor(this._colorTempPreview); + } + if (this._selectedBrightness) { + return `${Math.round(this._selectedBrightness)}${blankBeforePercent( + this.hass!.locale + )}%`; + } + return undefined; + } + protected render() { if (!this.hass || !this.stateObj) { return nothing; @@ -106,47 +147,60 @@ class MoreInfoLight extends LitElement { (this.entry.options?.light?.favorite_colors == null || this.entry.options.light.favorite_colors.length > 0); - const stateOverride = this._selectedBrightness - ? `${Math.round(this._selectedBrightness)}${blankBeforePercent( - this.hass!.locale - )}%` - : undefined; - return html`
- ${supportsBrightness + ${!supportsBrightness ? html` - - - ` - : html` - `} + ` + : nothing} ${supportsColorTemp || supportsColor || supportsBrightness ? html` -
+ ${supportsBrightness && this._mainControl === "brightness" + ? html` + + + ` + : nothing} + ${supportsColor && this._mainControl === "color" + ? html` + + + ` + : nothing} + ${supportsColorTemp && this._mainControl === "color_temp" + ? html` + + + ` + : nothing} + ${supportsBrightness ? html` ` : nothing} + ${supportsColor || supportsColorTemp + ? html` +
+ + + + ` + : nothing} ${supportsColor ? html` - - + ` : nothing} ${supportsColorTemp ? html` - - + ` : nothing} ${supportsWhite ? html` +
` : nothing} -
+ ${this.entry && lightSupportsFavoriteColors(this.stateObj) && (this.editMode || hasFavoriteColors) @@ -216,6 +281,7 @@ class MoreInfoLight extends LitElement { .stateObj=${this.stateObj} .entry=${this.entry} .editMode=${this.editMode} + @favorite-color-edit-started=${this._resetMainControl} > ` @@ -291,19 +357,6 @@ class MoreInfoLight extends LitElement { }); }; - private _showLightColorPickerView = (ev) => { - showLightColorPickerView( - this, - this.hass.localize( - "ui.dialogs.more_info_control.light.color_picker.title" - ), - { - entityId: this.stateObj!.entity_id, - defaultMode: ev.currentTarget.mode, - } - ); - }; - private _setWhite = () => { this.hass.callService("light", "turn_on", { entity_id: this.stateObj!.entity_id, @@ -347,9 +400,6 @@ class MoreInfoLight extends LitElement { flex: none; border-radius: 15px; } - ha-icon-button[disabled] .wheel { - filter: grayscale(1) opacity(0.5); - } .wheel.color { background-image: url("/static/images/color_wheel.png"); background-size: cover; @@ -362,6 +412,9 @@ class MoreInfoLight extends LitElement { rgb(255, 160, 0) 100% ); } + *[disabled] .wheel { + filter: grayscale(1) opacity(0.5); + } .buttons { flex-wrap: wrap; max-width: 250px; diff --git a/src/dialogs/more-info/controls/more-info-lock.ts b/src/dialogs/more-info/controls/more-info-lock.ts index 33afb0e93f..5fa6ec5df3 100644 --- a/src/dialogs/more-info/controls/more-info-lock.ts +++ b/src/dialogs/more-info/controls/more-info-lock.ts @@ -1,43 +1,156 @@ -import "@material/mwc-button"; -import type { HassEntity } from "home-assistant-js-websocket"; -import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, query } from "lit/decorators"; +import "@material/web/iconbutton/outlined-icon-button"; +import { mdiDoorOpen, mdiLock, mdiLockOff } from "@mdi/js"; +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; +import { domainIcon } from "../../../common/entity/domain_icon"; +import { stateColorCss } from "../../../common/entity/state_color"; +import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-attributes"; -import "../../../components/ha-textfield"; -import type { HaTextField } from "../../../components/ha-textfield"; +import { UNAVAILABLE } from "../../../data/entity"; +import { LockEntity, LockEntityFeature } from "../../../data/lock"; import type { HomeAssistant } from "../../../types"; +import { showEnterCodeDialogDialog } from "../../enter-code/show-enter-code-dialog"; +import { moreInfoControlStyle } from "../components/ha-more-info-control-style"; +import "../components/ha-more-info-state-header"; +import "../components/lock/ha-more-info-lock-toggle"; @customElement("more-info-lock") class MoreInfoLock extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public stateObj?: LockEntity; - @query("ha-textfield") private _textfield?: HaTextField; + private async _open() { + this._callService("open"); + } + + private async _lock() { + this._callService("lock"); + } + + private async _unlock() { + this._callService("unlock"); + } + + private async _callService(service: "open" | "lock" | "unlock") { + let code: string | undefined; + + if (this.stateObj!.attributes.code_format) { + const response = await showEnterCodeDialogDialog(this, { + codeFormat: "text", + codePattern: this.stateObj!.attributes.code_format, + title: this.hass.localize( + `ui.dialogs.more_info_control.lock.${service}` + ), + submitText: this.hass.localize( + `ui.dialogs.more_info_control.lock.${service}` + ), + }); + if (!response) { + return; + } + code = response; + } + + this.hass.callService("lock", service, { + entity_id: this.stateObj!.entity_id, + code, + }); + } protected render() { if (!this.hass || !this.stateObj) { return nothing; } + + const supportsOpen = supportsFeature(this.stateObj, LockEntityFeature.OPEN); + + const color = stateColorCss(this.stateObj); + const style = { + "--icon-color": color, + }; + + const isJammed = this.stateObj.state === "jammed"; + return html` - ${this.stateObj.attributes.code_format - ? html`
- - ${this.stateObj.state === "locked" - ? html`${this.hass.localize("ui.card.lock.unlock")}` - : html`${this.hass.localize("ui.card.lock.lock")}`} -
` - : ""} + +
+ ${ + this.stateObj.state === "jammed" + ? html` +
+ +
+ +
+
+ ` + : html` + + + ` + } + ${ + supportsOpen || isJammed + ? html` +
+ ${supportsOpen + ? html` + + + + ` + : nothing} + ${isJammed + ? html` + + + + + + + ` + : nothing} +
+ ` + : nothing + } +
+
{ + private _pipelinePromise?: Promise; + + public async showDialog(params?: VoiceCommandDialogParams): Promise { + if (params?.pipeline_id) { + this._pipelineId = params?.pipeline_id; + } + this._conversation = [ { who: "hass", @@ -92,6 +99,11 @@ export class HaVoiceCommandDialog extends LitElement { this._opened = true; await this.updateComplete; this._scrollMessagesBottom(); + + await this._pipelinePromise; + if (params?.start_listening && this._pipeline?.stt_engine) { + this._toggleListening(); + } } public async closeDialog(): Promise { @@ -230,7 +242,7 @@ export class HaVoiceCommandDialog extends LitElement {
import("./ha-voice-command-dialog"); +export interface VoiceCommandDialogParams { + pipeline_id?: string; + start_listening?: boolean; +} + export const showVoiceCommandDialog = ( element: HTMLElement, - hass: HomeAssistant + hass: HomeAssistant, + dialogParams?: VoiceCommandDialogParams ): void => { if (hass.auth.external?.config.hasAssist) { hass.auth.external!.fireMessage({ type: "assist/show", + payload: { + pipeline_id: dialogParams?.pipeline_id, + start_listening: dialogParams?.start_listening, + }, }); return; } fireEvent(element, "show-dialog", { dialogTag: "ha-voice-command-dialog", dialogImport: loadVoiceCommandDialog, - dialogParams: {}, + dialogParams, }); }; diff --git a/src/entrypoints/core.ts b/src/entrypoints/core.ts index 74cfd72a5f..cc92ddd5bc 100644 --- a/src/entrypoints/core.ts +++ b/src/entrypoints/core.ts @@ -133,7 +133,15 @@ window.hassConnection.then(({ conn }) => { }); window.addEventListener("error", (e) => { - if (!__DEV__ && e.message === "ResizeObserver loop limit exceeded") { + if ( + !__DEV__ && + typeof e.message === "string" && + (e.message.includes("ResizeObserver loop limit exceeded") || + e.message.includes( + "ResizeObserver loop completed with undelivered notifications" + )) + ) { + e.preventDefault(); e.stopImmediatePropagation(); e.stopPropagation(); return; diff --git a/src/external_app/external_auth.ts b/src/external_app/external_auth.ts index 26b1f1f892..c1f0674278 100644 --- a/src/external_app/external_auth.ts +++ b/src/external_app/external_auth.ts @@ -131,7 +131,7 @@ export class ExternalAuth extends Auth { export const createExternalAuth = async (hassUrl: string) => { const auth = new ExternalAuth(hassUrl); if ( - (window.externalApp && window.externalApp.externalBus) || + window.externalApp?.externalBus || (window.webkit && window.webkit.messageHandlers.externalBus) ) { auth.external = new ExternalMessaging(); diff --git a/src/panels/config/automation/action/ha-automation-action-row.ts b/src/panels/config/automation/action/ha-automation-action-row.ts index 0819a14287..242fd3173f 100644 --- a/src/panels/config/automation/action/ha-automation-action-row.ts +++ b/src/panels/config/automation/action/ha-automation-action-row.ts @@ -1,6 +1,7 @@ import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import "@material/mwc-list/mwc-list-item"; import { + mdiAlertCircleCheck, mdiCheck, mdiContentDuplicate, mdiContentCopy, @@ -14,7 +15,14 @@ import { mdiStopCircleOutline, } from "@mdi/js"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; +import { + css, + CSSResultGroup, + html, + LitElement, + nothing, + PropertyValues, +} from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; @@ -34,7 +42,11 @@ import { subscribeEntityRegistry, } from "../../../../data/entity_registry"; import { Clipboard } from "../../../../data/automation"; -import { Action, getActionType } from "../../../../data/script"; +import { + Action, + NonConditionAction, + getActionType, +} from "../../../../data/script"; import { describeAction } from "../../../../data/script_i18n"; import { callExecuteScript } from "../../../../data/service"; import { @@ -184,6 +196,17 @@ export default class HaAutomationActionRow extends LitElement { + ${type !== "condition" && + (this.action as NonConditionAction).continue_on_error === true + ? html`
+ + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.continue_on_error" + )} + +
` + : nothing} ${this.hideMenu ? "" : html` diff --git a/src/panels/config/automation/action/types/ha-automation-action-service.ts b/src/panels/config/automation/action/types/ha-automation-action-service.ts index e59c1800dd..af4a525406 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-service.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-service.ts @@ -1,7 +1,10 @@ import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { assert } from "superstruct"; import { fireEvent } from "../../../../../common/dom/fire_event"; +import { computeDomain } from "../../../../../common/entity/compute_domain"; +import { computeObjectId } from "../../../../../common/entity/compute_object_id"; import { hasTemplate } from "../../../../../common/string/has-template"; import "../../../../../components/ha-service-control"; import { ServiceAction, serviceActionStruct } from "../../../../../data/script"; @@ -20,6 +23,26 @@ export class HaServiceAction extends LitElement implements ActionElement { @state() private _action!: ServiceAction; + private _fields = memoizeOne( + ( + serviceDomains: HomeAssistant["services"], + domainService: string | undefined + ): { fields: any } => { + if (!domainService) { + return { fields: {} }; + } + const domain = computeDomain(domainService); + const service = computeObjectId(domainService); + if (!(domain in serviceDomains)) { + return { fields: {} }; + } + if (!(service in serviceDomains[domain])) { + return { fields: {} }; + } + return { fields: serviceDomains[domain][service].fields }; + } + ); + public static get defaultConfig() { return { service: "", data: {} }; } @@ -34,7 +57,28 @@ export class HaServiceAction extends LitElement implements ActionElement { fireEvent(this, "ui-mode-not-available", err); return; } - if (this.action && hasTemplate(this.action)) { + + const fields = this._fields( + this.hass.services, + this.action?.service + ).fields; + if ( + this.action && + (Object.entries(this.action).some( + ([key, val]) => key !== "data" && hasTemplate(val) + ) || + (this.action.data && + Object.entries(this.action.data).some(([key, val]) => { + const field = fields[key]; + if ( + field?.selector && + ("template" in field.selector || "object" in field.selector) + ) { + return false; + } + return hasTemplate(val); + }))) + ) { fireEvent( this, "ui-mode-not-available", diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts b/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts index 3eb72d48d2..8935668618 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts @@ -1,9 +1,11 @@ import "@material/mwc-list/mwc-list-item"; +import memoizeOne from "memoize-one"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; import { ensureArray } from "../../../../../common/array/ensure-array"; +import type { SchemaUnion } from "../../../../../components/ha-form/types"; import "../../../../../components/ha-select"; import type { AutomationConfig, @@ -30,6 +32,22 @@ export class HaTriggerCondition extends LitElement { }; } + private _schema = memoizeOne( + (triggers: Trigger[]) => + [ + { + name: "id", + selector: { + select: { + multiple: true, + options: triggers.map((trigger) => trigger.id!), + }, + }, + required: true, + }, + ] as const + ); + connectedCallback() { super.connectedCallback(); const details = { callback: (config) => this._automationUpdated(config) }; @@ -45,30 +63,33 @@ export class HaTriggerCondition extends LitElement { } protected render() { - const { id } = this.condition; - if (!this._triggers.length) { return this.hass.localize( "ui.panel.config.automation.editor.conditions.type.trigger.no_triggers" ); } - return html` - ${this._triggers.map( - (trigger) => - html` - ${trigger.id} - ` - )} - `; + + const schema = this._schema(this._triggers); + + return html` + + `; } + private _computeLabelCallback = ( + schema: SchemaUnion> + ): string => + this.hass.localize( + `ui.panel.config.automation.editor.conditions.type.trigger.${schema.name}` + ); + private _automationUpdated(config?: AutomationConfig) { const seenIds = new Set(); this._triggers = config?.trigger @@ -78,18 +99,24 @@ export class HaTriggerCondition extends LitElement { : []; } - private _triggerPicked(ev) { + private _valueChanged(ev: CustomEvent): void { ev.stopPropagation(); - if (!ev.target.value) { - return; + const newValue = ev.detail.value; + + if (typeof newValue.id === "string") { + if (!this._triggers.some((trigger) => trigger.id === newValue.id)) { + newValue.id = ""; + } + } else if (Array.isArray(newValue.id)) { + newValue.id = newValue.id.filter((id) => + this._triggers.some((trigger) => trigger.id === id) + ); + if (!newValue.id.length) { + newValue.id = ""; + } } - const newTrigger = ev.target.value; - if (this.condition.id === newTrigger) { - return; - } - fireEvent(this, "value-changed", { - value: { ...this.condition, id: newTrigger }, - }); + + fireEvent(this, "value-changed", { value: newValue }); } } diff --git a/src/panels/config/automation/dialog-new-automation.ts b/src/panels/config/automation/dialog-new-automation.ts index 0c160900b9..735d6aa9de 100644 --- a/src/panels/config/automation/dialog-new-automation.ts +++ b/src/panels/config/automation/dialog-new-automation.ts @@ -18,8 +18,10 @@ import "../../../components/ha-icon-next"; import "../../../components/ha-list-item"; import "../../../components/ha-tip"; import { showAutomationEditor } from "../../../data/automation"; +import { showScriptEditor } from "../../../data/script"; import { Blueprint, + BlueprintDomain, Blueprints, BlueprintSourceType, fetchBlueprints, @@ -29,6 +31,7 @@ import { HassDialog } from "../../../dialogs/make-dialog-manager"; import { haStyle, haStyleDialog } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; +import type { NewAutomationDialogParams } from "./show-dialog-new-automation"; const SOURCE_TYPE_ICONS: Record = { local: mdiFile, @@ -42,11 +45,15 @@ class DialogNewAutomation extends LitElement implements HassDialog { @state() private _opened = false; + @state() private _mode: BlueprintDomain = "automation"; + @state() public blueprints?: Blueprints; - public showDialog(): void { + public showDialog(params: NewAutomationDialogParams): void { this._opened = true; - fetchBlueprints(this.hass!, "automation").then((blueprints) => { + this._mode = params?.mode || "automation"; + + fetchBlueprints(this.hass!, this._mode).then((blueprints) => { this.blueprints = blueprints; }); } @@ -92,14 +99,14 @@ class DialogNewAutomation extends LitElement implements HassDialog { @closed=${this.closeDialog} .heading=${createCloseHeading( this.hass, - this.hass.localize("ui.panel.config.automation.dialog_new.header") + this.hass.localize(`ui.panel.config.${this._mode}.dialog_new.header`) )} > ${this.hass.localize( - "ui.panel.config.automation.dialog_new.create_empty" + `ui.panel.config.${this._mode}.dialog_new.create_empty` )} ${this.hass.localize( - "ui.panel.config.automation.dialog_new.create_empty_description" + `ui.panel.config.${this._mode}.dialog_new.create_empty_description` )} @@ -139,11 +146,11 @@ class DialogNewAutomation extends LitElement implements HassDialog { ${blueprint.author ? this.hass.localize( - `ui.panel.config.automation.dialog_new.blueprint_source.author`, + `ui.panel.config.${this._mode}.dialog_new.blueprint_source.author`, { author: blueprint.author } ) : this.hass.localize( - `ui.panel.config.automation.dialog_new.blueprint_source.${blueprint.sourceType}` + `ui.panel.config.${this._mode}.dialog_new.blueprint_source.${blueprint.sourceType}` )} @@ -161,11 +168,11 @@ class DialogNewAutomation extends LitElement implements HassDialog { ${this.hass.localize( - "ui.panel.config.automation.dialog_new.create_blueprint" + `ui.panel.config.${this._mode}.dialog_new.create_blueprint` )} ${this.hass.localize( - "ui.panel.config.automation.dialog_new.create_blueprint_description" + `ui.panel.config.${this._mode}.dialog_new.create_blueprint_description` )} @@ -180,7 +187,7 @@ class DialogNewAutomation extends LitElement implements HassDialog { rel="noreferrer noopener" > ${this.hass.localize( - "ui.panel.config.automation.dialog_new.discover_blueprint_tip" + `ui.panel.config.${this._mode}.dialog_new.discover_blueprint_tip` )} @@ -196,7 +203,11 @@ class DialogNewAutomation extends LitElement implements HassDialog { } const path = (ev.currentTarget! as any).path; this.closeDialog(); - showAutomationEditor({ use_blueprint: { path } }); + if (this._mode === "script") { + showScriptEditor({ use_blueprint: { path } }); + } else { + showAutomationEditor({ use_blueprint: { path } }); + } } private async _blank(ev) { @@ -204,7 +215,11 @@ class DialogNewAutomation extends LitElement implements HassDialog { return; } this.closeDialog(); - showAutomationEditor(); + if (this._mode === "script") { + showScriptEditor(); + } else { + showAutomationEditor(); + } } static get styles(): CSSResultGroup { diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index cdf33e2b50..eb1003aa0b 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -486,7 +486,7 @@ class HaAutomationPicker extends LitElement { private _createNew() { if (isComponentLoaded(this.hass, "blueprint")) { - showNewAutomationDialog(this); + showNewAutomationDialog(this, { mode: "automation" }); } else { navigate("/config/automation/edit/new"); } diff --git a/src/panels/config/automation/show-dialog-new-automation.ts b/src/panels/config/automation/show-dialog-new-automation.ts index 0a618c178b..1425842e80 100644 --- a/src/panels/config/automation/show-dialog-new-automation.ts +++ b/src/panels/config/automation/show-dialog-new-automation.ts @@ -1,11 +1,18 @@ import { fireEvent } from "../../../common/dom/fire_event"; +export interface NewAutomationDialogParams { + mode: "script" | "automation"; +} + export const loadNewAutomationDialog = () => import("./dialog-new-automation"); -export const showNewAutomationDialog = (element: HTMLElement): void => { +export const showNewAutomationDialog = ( + element: HTMLElement, + newAutomationDialogParams: NewAutomationDialogParams +): void => { fireEvent(element, "show-dialog", { dialogTag: "ha-dialog-new-automation", dialogImport: loadNewAutomationDialog, - dialogParams: {}, + dialogParams: newAutomationDialogParams, }); }; diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index 7db2fcd797..73f9104dbf 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -160,14 +160,16 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { return nothing; } + const useBlueprint = "use_blueprint" in this._config; + const schema = this._schema( !!this.scriptId, - "use_blueprint" in this._config, + useBlueprint, this._config.mode ); const data = { - mode: MODES[0], + ...(!this._config.mode && !useBlueprint && { mode: MODES[0] }), icon: undefined, max: this._config.mode && isMaxMode(this._config.mode) ? 10 : undefined, ...this._config, @@ -332,7 +334,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
- ${"use_blueprint" in this._config + ${useBlueprint ? html` - - - - - + + + `; } @@ -312,6 +314,14 @@ class HaScriptPicker extends LitElement { } } + private _createNew() { + if (isComponentLoaded(this.hass, "blueprint")) { + showNewAutomationDialog(this, { mode: "script" }); + } else { + navigate("/config/script/edit/new"); + } + } + private _runScript = async (script: any) => { const entry = this.entityRegistry.find( (e) => e.entity_id === script.entity_id diff --git a/src/panels/config/storage/dialog-mount-view.ts b/src/panels/config/storage/dialog-mount-view.ts index 624d4889a6..52592a9b67 100644 --- a/src/panels/config/storage/dialog-mount-view.ts +++ b/src/panels/config/storage/dialog-mount-view.ts @@ -1,8 +1,10 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; +import { mdiHelpCircle } from "@mdi/js"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-form/ha-form"; +import "../../../components/ha-icon-button"; import { extractApiErrorMessage } from "../../../data/hassio/common"; import { createSupervisorMount, @@ -17,6 +19,8 @@ import { HomeAssistant } from "../../../types"; import { MountViewDialogParams } from "./show-dialog-view-mount"; import { LocalizeFunc } from "../../../common/translations/localize"; import type { SchemaUnion } from "../../../components/ha-form/types"; +import { documentationUrl } from "../../../util/documentation-url"; +import { computeRTLDirection } from "../../../common/util/compute_rtl"; const mountSchema = memoizeOne( ( @@ -158,6 +162,33 @@ class ViewMountDialog extends LitElement { )} @closed=${this.closeDialog} > + + ${this._existing + ? this.hass.localize( + "ui.panel.config.storage.network_mounts.update_title" + ) + : this.hass.localize( + "ui.panel.config.storage.network_mounts.add_title" + )} + + + + + ${this._error ? html`${this._error}` : nothing} @@ -274,6 +305,9 @@ class ViewMountDialog extends LitElement { haStyle, haStyleDialog, css` + ha-icon-button { + color: var(--primary-text-color); + } .delete-btn { --mdc-theme-primary: var(--error-color); } diff --git a/src/panels/developer-tools/service/developer-tools-service.ts b/src/panels/developer-tools/service/developer-tools-service.ts index 8e45387ed2..a169b0fb27 100644 --- a/src/panels/developer-tools/service/developer-tools-service.ts +++ b/src/panels/developer-tools/service/developer-tools-service.ts @@ -346,7 +346,27 @@ class HaPanelDevService extends LitElement { } private _checkUiSupported() { - if (this._serviceData && hasTemplate(this._serviceData)) { + const fields = this._fields( + this.hass.services, + this._serviceData?.service + ).fields; + if ( + this._serviceData && + (Object.entries(this._serviceData).some( + ([key, val]) => key !== "data" && hasTemplate(val) + ) || + (this._serviceData.data && + Object.entries(this._serviceData.data).some(([key, val]) => { + const field = fields.find((f) => f.key === key); + if ( + field?.selector && + ("template" in field.selector || "object" in field.selector) + ) { + return false; + } + return hasTemplate(val); + }))) + ) { this._yamlMode = true; this._uiAvailable = false; } else { diff --git a/src/panels/energy/strategies/energy-strategy.ts b/src/panels/energy/strategies/energy-strategy.ts index de5d6aca44..42b0c6678a 100644 --- a/src/panels/energy/strategies/energy-strategy.ts +++ b/src/panels/energy/strategies/energy-strategy.ts @@ -141,6 +141,11 @@ export class EnergyStrategy { view_layout: { position: "sidebar" }, collection_key: "energy_dashboard", }); + view.cards!.push({ + type: "energy-self-sufficiency-gauge", + view_layout: { position: "sidebar" }, + collection_key: "energy_dashboard", + }); } // Only include if we have a grid diff --git a/src/panels/history/ha-panel-history.ts b/src/panels/history/ha-panel-history.ts index b88861d44c..bb3c224750 100644 --- a/src/panels/history/ha-panel-history.ts +++ b/src/panels/history/ha-panel-history.ts @@ -190,6 +190,7 @@ class HaPanelHistory extends SubscribeMixin(LitElement) { diff --git a/src/panels/lovelace/cards/energy/hui-energy-self-sufficiency-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-self-sufficiency-gauge-card.ts new file mode 100644 index 0000000000..d1b3221f6f --- /dev/null +++ b/src/panels/lovelace/cards/energy/hui-energy-self-sufficiency-gauge-card.ts @@ -0,0 +1,257 @@ +import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; +import { mdiInformation } from "@mdi/js"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; +import "../../../../components/ha-card"; +import "../../../../components/ha-gauge"; +import "../../../../components/ha-svg-icon"; +import { + EnergyData, + energySourcesByType, + getEnergyDataCollection, +} from "../../../../data/energy"; +import { calculateStatisticsSumGrowth } from "../../../../data/recorder"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; +import type { HomeAssistant } from "../../../../types"; +import type { LovelaceCard } from "../../types"; +import { severityMap } from "../hui-gauge-card"; +import type { EnergySelfSufficiencyGaugeCardConfig } from "../types"; + +@customElement("hui-energy-self-sufficiency-gauge-card") +class HuiEnergySelfSufficiencyGaugeCard + extends SubscribeMixin(LitElement) + implements LovelaceCard +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: EnergySelfSufficiencyGaugeCardConfig; + + @state() private _data?: EnergyData; + + protected hassSubscribeRequiredHostProps = ["_config"]; + + public hassSubscribe(): UnsubscribeFunc[] { + return [ + getEnergyDataCollection(this.hass!, { + key: this._config?.collection_key, + }).subscribe((data) => { + this._data = data; + }), + ]; + } + + public getCardSize(): number { + return 4; + } + + public setConfig(config: EnergySelfSufficiencyGaugeCardConfig): void { + this._config = config; + } + + protected render() { + if (!this._config || !this.hass) { + return nothing; + } + + if (!this._data) { + return html`${this.hass.localize( + "ui.panel.lovelace.cards.energy.loading" + )}`; + } + + const prefs = this._data.prefs; + const types = energySourcesByType(prefs); + + // The strategy only includes this card if we have a grid. + const hasConsumption = true; + + const hasSolarProduction = types.solar !== undefined; + const hasBattery = types.battery !== undefined; + const hasReturnToGrid = hasConsumption && types.grid![0].flow_to.length > 0; + + const totalFromGrid = + calculateStatisticsSumGrowth( + this._data.stats, + types.grid![0].flow_from.map((flow) => flow.stat_energy_from) + ) ?? 0; + + let totalSolarProduction: number | null = null; + + if (hasSolarProduction) { + totalSolarProduction = + calculateStatisticsSumGrowth( + this._data.stats, + types.solar!.map((source) => source.stat_energy_from) + ) || 0; + } + + let totalBatteryIn: number | null = null; + let totalBatteryOut: number | null = null; + + if (hasBattery) { + totalBatteryIn = + calculateStatisticsSumGrowth( + this._data.stats, + types.battery!.map((source) => source.stat_energy_to) + ) || 0; + totalBatteryOut = + calculateStatisticsSumGrowth( + this._data.stats, + types.battery!.map((source) => source.stat_energy_from) + ) || 0; + } + + let returnedToGrid: number | null = null; + + if (hasReturnToGrid) { + returnedToGrid = + calculateStatisticsSumGrowth( + this._data.stats, + types.grid![0].flow_to.map((flow) => flow.stat_energy_to) + ) || 0; + } + + let solarConsumption: number | null = null; + if (hasSolarProduction) { + solarConsumption = + (totalSolarProduction || 0) - + (returnedToGrid || 0) - + (totalBatteryIn || 0); + } + + let batteryFromGrid: null | number = null; + let batteryToGrid: null | number = null; + if (solarConsumption !== null && solarConsumption < 0) { + // What we returned to the grid and what went in to the battery is more than produced, + // so we have used grid energy to fill the battery + // or returned battery energy to the grid + if (hasBattery) { + batteryFromGrid = solarConsumption * -1; + if (batteryFromGrid > totalFromGrid) { + batteryToGrid = Math.min(0, batteryFromGrid - totalFromGrid); + batteryFromGrid = totalFromGrid; + } + } + solarConsumption = 0; + } + + let batteryConsumption: number | null = null; + if (hasBattery) { + batteryConsumption = (totalBatteryOut || 0) - (batteryToGrid || 0); + } + + const gridConsumption = Math.max(0, totalFromGrid - (batteryFromGrid || 0)); + + const totalHomeConsumption = Math.max( + 0, + gridConsumption + (solarConsumption || 0) + (batteryConsumption || 0) + ); + + let value: number | undefined; + if ( + totalFromGrid !== null && + totalHomeConsumption !== null && + totalHomeConsumption > 0 + ) { + value = (1 - totalFromGrid / totalHomeConsumption) * 100; + } + + return html` + + ${value !== undefined + ? html` + + + + ${this.hass.localize( + "ui.panel.lovelace.cards.energy.self_sufficiency_gauge.card_indicates_self_sufficiency_quota" + )} + + + +
+ ${this.hass.localize( + "ui.panel.lovelace.cards.energy.self_sufficiency_gauge.self_sufficiency_quota" + )} +
+ ` + : this.hass.localize( + "ui.panel.lovelace.cards.energy.self_sufficiency_gauge.self_sufficiency_could_not_calc" + )} +
+ `; + } + + private _computeSeverity(numberValue: number): string { + if (numberValue > 75) { + return severityMap.green; + } + if (numberValue < 50) { + return severityMap.yellow; + } + return severityMap.normal; + } + + static get styles(): CSSResultGroup { + return css` + ha-card { + height: 100%; + overflow: hidden; + padding: 16px; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + box-sizing: border-box; + } + + ha-gauge { + width: 100%; + max-width: 250px; + direction: ltr; + } + + .name { + text-align: center; + line-height: initial; + color: var(--primary-text-color); + width: 100%; + font-size: 15px; + margin-top: 8px; + } + + ha-svg-icon { + position: absolute; + right: 4px; + top: 4px; + color: var(--secondary-text-color); + } + simple-tooltip > span { + font-size: 12px; + line-height: 12px; + } + simple-tooltip { + width: 80%; + max-width: 250px; + top: 8px !important; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-energy-self-sufficiency-gauge-card": HuiEnergySelfSufficiencyGaugeCard; + } +} 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 0365164fb7..4f5e6cbfdf 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 @@ -68,10 +68,11 @@ class HuiEnergySolarGaugeCard return nothing; } - const totalSolarProduction = calculateStatisticsSumGrowth( - this._data.stats, - types.solar.map((source) => source.stat_energy_from) - ); + const totalSolarProduction = + calculateStatisticsSumGrowth( + this._data.stats, + types.solar.map((source) => source.stat_energy_from) + ) || 0; const productionReturnedToGrid = calculateStatisticsSumGrowth( this._data.stats, diff --git a/src/panels/lovelace/cards/hui-history-graph-card.ts b/src/panels/lovelace/cards/hui-history-graph-card.ts index 64e7809e18..5219027fc9 100644 --- a/src/panels/lovelace/cards/hui-history-graph-card.ts +++ b/src/panels/lovelace/cards/hui-history-graph-card.ts @@ -205,6 +205,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { .historyData=${this._stateHistory} .names=${this._names} up-to-now + .hoursToShow=${this._hoursToShow} .showNames=${this._config.show_names !== undefined ? this._config.show_names : true} diff --git a/src/panels/lovelace/cards/tile/badges/tile-badge-climate.ts b/src/panels/lovelace/cards/tile/badges/tile-badge-climate.ts index 81e7346983..03c959eec6 100644 --- a/src/panels/lovelace/cards/tile/badges/tile-badge-climate.ts +++ b/src/panels/lovelace/cards/tile/badges/tile-badge-climate.ts @@ -2,6 +2,7 @@ import { mdiClockOutline, mdiFan, mdiFire, + mdiHeatWave, mdiPower, mdiSnowflake, mdiWaterPercent, @@ -21,6 +22,7 @@ export const CLIMATE_HVAC_ACTION_ICONS: Record = { heating: mdiFire, idle: mdiClockOutline, off: mdiPower, + preheating: mdiHeatWave, }; export const CLIMATE_HVAC_ACTION_MODE: Record = { @@ -30,6 +32,7 @@ export const CLIMATE_HVAC_ACTION_MODE: Record = { heating: "heat", idle: "off", off: "off", + preheating: "heat", }; export const computeClimateBadge: ComputeBadgeFunction = (stateObj) => { diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 0e65d9a46a..fac076ad93 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -159,6 +159,13 @@ export interface EnergySolarGaugeCardConfig extends LovelaceCardConfig { collection_key?: string; } +export interface EnergySelfSufficiencyGaugeCardConfig + extends LovelaceCardConfig { + type: "energy-self-sufficiency-gauge"; + title?: string; + collection_key?: string; +} + export interface EnergyGridGaugeCardConfig extends LovelaceCardConfig { type: "energy-grid-result-gauge"; title?: string; diff --git a/src/panels/lovelace/common/handle-action.ts b/src/panels/lovelace/common/handle-action.ts index 7ea895d723..f782d6ad53 100644 --- a/src/panels/lovelace/common/handle-action.ts +++ b/src/panels/lovelace/common/handle-action.ts @@ -4,6 +4,7 @@ import { forwardHaptic } from "../../../data/haptics"; import { domainToName } from "../../../data/integration"; import { ActionConfig } from "../../../data/lovelace"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; +import { showVoiceCommandDialog } from "../../../dialogs/voice-command-dialog/show-ha-voice-command-dialog"; import { HomeAssistant } from "../../../types"; import { showToast } from "../../../util/toast"; import { toggleEntity } from "./entity/toggle-entity"; @@ -155,6 +156,13 @@ export const handleAction = async ( forwardHaptic("light"); break; } + case "assist": { + showVoiceCommandDialog(node, hass, { + start_listening: actionConfig.start_listening, + pipeline_id: actionConfig.pipeline_id, + }); + break; + } case "fire-dom-event": { fireEvent(node, "ll-custom", actionConfig); } diff --git a/src/panels/lovelace/components/hui-action-editor.ts b/src/panels/lovelace/components/hui-action-editor.ts index 4077ae9a09..20d5e0f597 100644 --- a/src/panels/lovelace/components/hui-action-editor.ts +++ b/src/panels/lovelace/components/hui-action-editor.ts @@ -3,6 +3,8 @@ import { customElement, property } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; import { stopPropagation } from "../../../common/dom/stop_propagation"; +import "../../../components/ha-assist-pipeline-picker"; +import { HaFormSchema, SchemaUnion } from "../../../components/ha-form/types"; import "../../../components/ha-help-tooltip"; import "../../../components/ha-navigation-picker"; import "../../../components/ha-service-control"; @@ -24,9 +26,31 @@ const DEFAULT_ACTIONS: UiAction[] = [ "navigate", "url", "call-service", + "assist", "none", ]; +const ASSIST_SCHEMA = [ + { + type: "grid", + name: "", + schema: [ + { + name: "pipeline_id", + selector: { + assist_pipeline: {}, + }, + }, + { + name: "start_listening", + selector: { + boolean: {}, + }, + }, + ], + }, +] as const satisfies readonly HaFormSchema[]; + @customElement("hui-action-editor") export class HuiActionEditor extends LitElement { @property() public config?: ActionConfig; @@ -101,7 +125,7 @@ export class HuiActionEditor extends LitElement { ? html` ` - : ""} + : nothing}
${this.config?.action === "navigate" ? html` @@ -114,7 +138,7 @@ export class HuiActionEditor extends LitElement { @value-changed=${this._navigateValueChanged} > ` - : ""} + : nothing} ${this.config?.action === "url" ? html` ` - : ""} + : nothing} ${this.config?.action === "call-service" ? html` ` - : ""} + : nothing} + ${this.config?.action === "assist" + ? html` + + + ` + : nothing} `; } @@ -182,7 +218,7 @@ export class HuiActionEditor extends LitElement { return; } const target = ev.target! as EditorTarget; - const value = ev.target.value; + const value = ev.target.value ?? ev.target.checked; if (this[`_${target.configValue}`] === value) { return; } @@ -193,6 +229,21 @@ export class HuiActionEditor extends LitElement { } } + private _formValueChanged(ev): void { + ev.stopPropagation(); + const value = ev.detail.value; + + fireEvent(this, "value-changed", { + value: value, + }); + } + + private _computeFormLabel(schema: SchemaUnion) { + return this.hass?.localize( + `ui.panel.lovelace.editor.action-editor.${schema.name}` + ); + } + private _serviceValueChanged(ev: CustomEvent) { ev.stopPropagation(); const value = { @@ -240,17 +291,25 @@ export class HuiActionEditor extends LitElement { width: 100%; } ha-service-control, - ha-navigation-picker { + ha-navigation-picker, + ha-form { display: block; } ha-textfield, ha-service-control, - ha-navigation-picker { + ha-navigation-picker, + ha-form { margin-top: 8px; } ha-service-control { --service-control-padding: 0; } + ha-formfield { + display: flex; + height: 56px; + align-items: center; + --mdc-typography-body2-font-size: 1em; + } `; } } diff --git a/src/panels/lovelace/create-element/create-card-element.ts b/src/panels/lovelace/create-element/create-card-element.ts index 98390dc9bf..3ecd7b4a16 100644 --- a/src/panels/lovelace/create-element/create-card-element.ts +++ b/src/panels/lovelace/create-element/create-card-element.ts @@ -53,6 +53,8 @@ const LAZY_LOAD_TYPES = { import("../cards/energy/hui-energy-grid-neutrality-gauge-card"), "energy-solar-consumed-gauge": () => import("../cards/energy/hui-energy-solar-consumed-gauge-card"), + "energy-self-sufficiency-gauge": () => + import("../cards/energy/hui-energy-self-sufficiency-gauge-card"), "energy-solar-graph": () => import("../cards/energy/hui-energy-solar-graph-card"), "energy-sources-table": () => diff --git a/src/panels/lovelace/editor/structs/action-struct.ts b/src/panels/lovelace/editor/structs/action-struct.ts index e8a6ec82be..512e45aa84 100644 --- a/src/panels/lovelace/editor/structs/action-struct.ts +++ b/src/panels/lovelace/editor/structs/action-struct.ts @@ -51,6 +51,12 @@ const actionConfigStructNavigate = object({ confirmation: optional(actionConfigStructConfirmation), }); +const actionConfigStructAssist = type({ + action: literal("assist"), + pipeline_id: optional(string()), + start_listening: optional(boolean()), +}); + const actionConfigStructCustom = type({ action: literal("fire-dom-event"), }); @@ -63,6 +69,7 @@ export const actionConfigStructType = object({ "call-service", "url", "navigate", + "assist", ]), confirmation: optional(actionConfigStructConfirmation), }); @@ -82,6 +89,9 @@ export const actionConfigStruct = dynamic((value) => { case "url": { return actionConfigStructUrl; } + case "assist": { + return actionConfigStructAssist; + } } } diff --git a/src/panels/lovelace/tile-features/hui-alarm-modes-tile-feature.ts b/src/panels/lovelace/tile-features/hui-alarm-modes-tile-feature.ts index d6641b5d38..1675e1b913 100644 --- a/src/panels/lovelace/tile-features/hui-alarm-modes-tile-feature.ts +++ b/src/panels/lovelace/tile-features/hui-alarm-modes-tile-feature.ts @@ -18,10 +18,10 @@ import { ALARM_MODES, } from "../../../data/alarm_control_panel"; import { UNAVAILABLE } from "../../../data/entity"; -import { showEnterCodeDialogDialog } from "../../../dialogs/more-info/components/alarm_control_panel/show-enter-code-dialog"; import { HomeAssistant } from "../../../types"; import { LovelaceTileFeature, LovelaceTileFeatureEditor } from "../types"; import { AlarmModesFileFeatureConfig } from "./types"; +import { showEnterCodeDialogDialog } from "../../../dialogs/enter-code/show-enter-code-dialog"; export const supportsAlarmModesTileFeature = (stateObj: HassEntity) => { const domain = computeDomain(stateObj.entity_id); diff --git a/src/panels/profile/ha-long-lived-access-token-dialog.ts b/src/panels/profile/ha-long-lived-access-token-dialog.ts index c16cb7b332..7841e4918a 100644 --- a/src/panels/profile/ha-long-lived-access-token-dialog.ts +++ b/src/panels/profile/ha-long-lived-access-token-dialog.ts @@ -1,4 +1,5 @@ import "@material/mwc-button"; +import { mdiContentCopy } from "@mdi/js"; import { css, CSSResultGroup, @@ -11,9 +12,13 @@ import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; import { createCloseHeading } from "../../components/ha-dialog"; import "../../components/ha-textfield"; +import "../../components/ha-icon-button"; import { haStyleDialog } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; import { LongLivedAccessTokenDialogParams } from "./show-long-lived-access-token-dialog"; +import type { HaTextField } from "../../components/ha-textfield"; +import { copyToClipboard } from "../../common/util/copy-clipboard"; +import { showToast } from "../../util/toast"; const QR_LOGO_URL = "/static/icons/favicon-192x192.png"; @@ -55,8 +60,15 @@ export class HaLongLivedAccessTokenDialog extends LitElement { "ui.panel.profile.long_lived_access_tokens.prompt_copy_token" )} type="text" + iconTrailing readOnly - > + > + +
${this._qrCode ? this._qrCode @@ -71,6 +83,14 @@ export class HaLongLivedAccessTokenDialog extends LitElement { `; } + private async _copyToken(ev): Promise { + const textField = ev.target.parentElement as HaTextField; + await copyToClipboard(textField.value); + showToast(this, { + message: this.hass.localize("ui.common.copied_clipboard"), + }); + } + private async _generateQR() { const qrcode = await import("qrcode"); const canvas = await qrcode.toCanvas(this._params!.token, { @@ -111,6 +131,17 @@ export class HaLongLivedAccessTokenDialog extends LitElement { } ha-textfield { display: block; + --textfield-icon-trailing-padding: 0; + } + ha-textfield > ha-icon-button { + position: relative; + right: -8px; + --mdc-icon-button-size: 36px; + --mdc-icon-size: 20px; + color: var(--secondary-text-color); + inset-inline-start: initial; + inset-inline-end: -8px; + direction: var(--direction); } `, ]; diff --git a/src/resources/resize-observer.polyfill.ts b/src/resources/resize-observer.polyfill.ts index dc5eec0273..5023714d60 100644 --- a/src/resources/resize-observer.polyfill.ts +++ b/src/resources/resize-observer.polyfill.ts @@ -5,7 +5,7 @@ export const loadPolyfillIfNeeded = async () => { } catch (e) { window.ResizeObserver = ( await import( - "@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js" + "@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver" ) ).default; } diff --git a/src/translations/en.json b/src/translations/en.json old mode 100755 new mode 100644 index 310e681af0..8e46f03ee8 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -396,7 +396,8 @@ }, "theme-picker": { "theme": "Theme", - "no_theme": "No theme" + "no_theme": "No theme", + "default": "[%key:ui::panel::profile::themes::default%]" }, "language-picker": { "language": "Language", @@ -929,8 +930,8 @@ "light": { "edit_mode": "Edit favorite colors", "toggle": "Toggle", - "change_color": "Change color", - "change_color_temp": "Change color temperature", + "color": "Color", + "color_temp": "Temperature", "set_white": "Set white", "select_effect": "Select effect", "brightness": "Brightness", @@ -978,6 +979,11 @@ "disarm_action": "Disarm", "arm_title": "Arm", "arm_action": "Arm" + }, + "lock": { + "open": "Open", + "lock": "Lock", + "unlock": "Unlock" } }, "entity_registry": { @@ -2286,7 +2292,10 @@ "event_data": "Event data", "context_users": "Limit to events triggered by", "context_user_picked": "User firing event", - "context_user_pick": "Select user" + "context_user_pick": "Select user", + "description": { + "full": "When {eventTypes} event is fired" + } }, "geo_location": { "label": "Geolocation", @@ -2308,12 +2317,19 @@ "label": "Home Assistant", "event": "Event:", "start": "Start", - "shutdown": "Shutdown" + "shutdown": "Shutdown", + "description": { + "started": "When Home Assistant is started", + "shutdown": "When Home Assistant is shutdown" + } }, "mqtt": { "label": "MQTT", "topic": "Topic", - "payload": "Payload (optional)" + "payload": "Payload (optional)", + "description": { + "full": "When an MQTT message has been received" + } }, "numeric_state": { "label": "Numeric state", @@ -2341,22 +2357,35 @@ "event": "[%key:ui::panel::config::automation::editor::triggers::type::homeassistant::event%]", "sunrise": "Sunrise", "sunset": "Sunset", - "offset": "Offset (optional)" + "offset": "Offset (optional)", + "description": { + "sets": "When the sun sets{hasDuration, select, \n true { offset by {duration}} \n other {}\n }", + "rises": "When the sun rises{hasDuration, select, \n true { offset by {duration}} \n other {}\n }" + } }, "tag": { - "label": "Tag" + "label": "Tag", + "description": { + "full": "When a tag is scanned" + } }, "template": { "label": "Template", "value_template": "Value template", - "for": "For" + "for": "For", + "description": { + "full": "When a template triggers{hasDuration, select, \n true { for {duration}} \n other {}\n }" + } }, "time": { "type_value": "Fixed time", "type_input": "Value of a date/time helper or timestamp-class sensor", "label": "Time", "at": "At time", - "mode": "Mode" + "mode": "Mode", + "description": { + "full": "When the time is equal to {time}" + } }, "time_pattern": { "label": "Time Pattern", @@ -2370,7 +2399,10 @@ "local_only": "Only accessible from the local network", "webhook_id": "Webhook ID", "webhook_id_helper": "Treat this ID like a password: keep it secret, and make it hard to guess.", - "webhook_settings": "Webhook Settings" + "webhook_settings": "Webhook Settings", + "description": { + "full": "When a Webhook payload has been received" + } }, "zone": { "label": "Zone", @@ -2507,6 +2539,7 @@ "delete_confirm_text": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm_text%]", "unsupported_action": "No visual editor support for action: {action}", "type_select": "Action type", + "continue_on_error": "Continue on error", "type": { "service": { "label": "Call service" @@ -2703,6 +2736,21 @@ "delete": "[%key:ui::common::delete%]", "duplicate": "[%key:ui::common::duplicate%]" }, + "dialog_new": { + "header": "Create script", + "create_empty": "Create new script", + "create_empty_description": "Start with an empty script from scratch", + "create_blueprint": "[%key:ui::panel::config::automation::dialog_new::create_blueprint%]", + "create_blueprint_description": "[%key:ui::panel::config::automation::dialog_new::create_blueprint_description%]", + "blueprint_source": { + "author": "[%key:ui::panel::config::automation::dialog_new::blueprint_source::author%]", + "local": "[%key:ui::panel::config::automation::dialog_new::blueprint_source::local%]", + "community": "[%key:ui::panel::config::automation::dialog_new::blueprint_source::community%]", + "homeassistant": "[%key:ui::panel::config::automation::dialog_new::blueprint_source::homeassistant%]" + }, + "discover_blueprint_tip": "[%key:ui::panel::config::automation::dialog_new::discover_blueprint_tip%]" + }, + "editor": { "alias": "Name", "icon": "Icon", @@ -3499,11 +3547,11 @@ }, "thread": { "other_networks": "Other networks", - "my_network": "My network", + "my_network": "Preferred network", "no_preferred_network": "You don't have a preferred network yet.", "add_open_thread_border_router": "Add an OpenThread border router", "reset_border_router": "Reset border router", - "add_to_my_network": "Add to my network", + "add_to_my_network": "Add to preferred network", "no_routers_otbr_network": "No border routers where found, maybe the border router is not configured correctly. You can try to reset it to the factory settings.", "add_dataset_from_tlv": "Add dataset from TLV", "add_dataset": "Add Thread dataset", @@ -4066,6 +4114,7 @@ "add_title": "Add network storage", "update_title": "Update network storage", "no_mounts": "No connected network storage", + "documentation": "Documentation", "not_supported": { "title": "The operating system does not support network storage", "supervised": "Network storage is not supported on this host", @@ -4229,6 +4278,11 @@ "not_produced_solar_energy": "You have not produced any solar energy", "self_consumed_solar_could_not_calc": "Self-consumed solar energy couldn't be calculated" }, + "self_sufficiency_gauge": { + "card_indicates_self_sufficiency_quota": "This card indicates how self-sufficient your home is.", + "self_sufficiency_quota": "Self-sufficiency quota", + "self_sufficiency_could_not_calc": "Self-sufficiency quota couldn't be calculated" + }, "grid_neutrality_gauge": { "energy_dependency": "This card indicates your net energy usage.", "color_explain": "If the needle is in the purple, you returned more energy to the grid than you consumed from it. If it's in the blue, you consumed more energy from the grid than you returned.", @@ -4416,12 +4470,15 @@ "action-editor": { "navigation_path": "Navigation Path", "url_path": "URL Path", + "start_listening": "Start listening", + "pipeline_id": "Assistant", "actions": { "default_action": "Default Action", "call-service": "Call Service", "more-info": "More Info", "toggle": "Toggle", "navigate": "Navigate", + "assist": "Assist", "url": "URL", "none": "No Action" } diff --git a/yarn.lock b/yarn.lock index 0f0d787f5f..2b607381f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1582,10 +1582,10 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:8.42.0": - version: 8.42.0 - resolution: "@eslint/js@npm:8.42.0" - checksum: 750558843ac458f7da666122083ee05306fc087ecc1e5b21e7e14e23885775af6c55bcc92283dff1862b7b0d8863ec676c0f18c7faf1219c722fe91a8ece56b6 +"@eslint/js@npm:8.43.0": + version: 8.43.0 + resolution: "@eslint/js@npm:8.43.0" + checksum: 580487a09c82ac169744d36e4af77bc4f582c9a37749d1e9481eb93626c8f3991b2390c6e4e69e5642e3b6e870912b839229a0e23594fae348156ea5a8ed7e2e languageName: node linkType: hard @@ -2062,13 +2062,13 @@ __metadata: languageName: node linkType: hard -"@lit-labs/context@npm:0.3.2": - version: 0.3.2 - resolution: "@lit-labs/context@npm:0.3.2" +"@lit-labs/context@npm:0.3.3": + version: 0.3.3 + resolution: "@lit-labs/context@npm:0.3.3" dependencies: "@lit/reactive-element": ^1.5.0 lit: ^2.7.0 - checksum: 55920366798a3337a455c627c0b6911c7b78dee94a783ad77edb9a9e237a2e48201d6cf869f3d0b805316e5c8e8fb817f52f663bc556dd40ca6e8b3168662daf + checksum: 3607a7f965f22072feeef8db791e37be45921d9168ea3f432e160cb1fb337de19b2b41a2cd8db5d4fd2675d704d567a24695b796d0b14590616e9232f27579d3 languageName: node linkType: hard @@ -2088,13 +2088,13 @@ __metadata: languageName: node linkType: hard -"@lit-labs/virtualizer@npm:2.0.2": - version: 2.0.2 - resolution: "@lit-labs/virtualizer@npm:2.0.2" +"@lit-labs/virtualizer@npm:2.0.3": + version: 2.0.3 + resolution: "@lit-labs/virtualizer@npm:2.0.3" dependencies: lit: ^2.7.0 tslib: ^2.0.3 - checksum: 419aedf72f2b08f8fda43d9729810d5c7f34f470484bd9dff2df49d42cc56e57fcdfd98f69dd84e582d07fd2f372b90077cf42e12c4464b2c04c83755eebb495 + checksum: 594b89aca53210a6c0127c331fd05b795074df41aba086b63cb13ad5990e6962b86ca8403fe3a673e3bf46735e2def75d5412afe582702346fbd92a3331d34e1 languageName: node linkType: hard @@ -3166,16 +3166,6 @@ __metadata: languageName: node linkType: hard -"@mrmlnc/readdir-enhanced@npm:^2.2.1": - version: 2.2.1 - resolution: "@mrmlnc/readdir-enhanced@npm:2.2.1" - dependencies: - call-me-maybe: ^1.0.1 - glob-to-regexp: ^0.3.0 - checksum: d3b82b29368821154ce8e10bef5ccdbfd070d3e9601643c99ea4607e56f3daeaa4e755dd6d2355da20762c695c1b0570543d9f84b48f70c211ec09c4aaada2e1 - languageName: node - linkType: hard - "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -3193,13 +3183,6 @@ __metadata: languageName: node linkType: hard -"@nodelib/fs.stat@npm:^1.1.2": - version: 1.1.3 - resolution: "@nodelib/fs.stat@npm:1.1.3" - checksum: 318deab369b518a34778cdaa0054dd28a4381c0c78e40bbd20252f67d084b1d7bf9295fea4423de2c19ac8e1a34f120add9125f481b2a710f7068bcac7e3e305 - languageName: node - linkType: hard - "@nodelib/fs.walk@npm:^1.2.3, @nodelib/fs.walk@npm:^1.2.8": version: 1.2.8 resolution: "@nodelib/fs.walk@npm:1.2.8" @@ -3219,15 +3202,15 @@ __metadata: languageName: node linkType: hard -"@octokit/auth-oauth-device@npm:5.0.0": - version: 5.0.0 - resolution: "@octokit/auth-oauth-device@npm:5.0.0" +"@octokit/auth-oauth-device@npm:5.0.2": + version: 5.0.2 + resolution: "@octokit/auth-oauth-device@npm:5.0.2" dependencies: - "@octokit/oauth-methods": ^2.0.0 - "@octokit/request": ^6.0.0 - "@octokit/types": ^9.0.0 + "@octokit/oauth-methods": ^3.0.1 + "@octokit/request": ^7.0.0 + "@octokit/types": ^10.0.0 universal-user-agent: ^6.0.0 - checksum: 7089bf13bc01629e501af88dc01c18b29b70dba87e26bd4eb2b7faf6baefe3ba9e4ed92f77d5b7a8a56afbbdb1a01b4b264c140c10c4ca6fbd28a7976fcfdc6e + checksum: b625a2d7604351e52df46d3fdad04d1eb2ec68f80bce065047691ea83044967ef1e7dd0a70e9f8aab818d8c5ecf7f2550d2aa029ffdba85e0ff8c0ce2e25736a languageName: node linkType: hard @@ -3264,6 +3247,17 @@ __metadata: languageName: node linkType: hard +"@octokit/endpoint@npm:^8.0.0": + version: 8.0.1 + resolution: "@octokit/endpoint@npm:8.0.1" + dependencies: + "@octokit/types": ^10.0.0 + is-plain-object: ^5.0.0 + universal-user-agent: ^6.0.0 + checksum: 0cff7c972d8304cb59c4cc28016c15bca05e6d7e9e2d9b00af88ce05bf9abdfdb17025f38080162a71ea15b21c740bcb5079361396f18a24bbe55134c504a581 + languageName: node + linkType: hard + "@octokit/graphql@npm:^5.0.0": version: 5.0.6 resolution: "@octokit/graphql@npm:5.0.6" @@ -3275,23 +3269,23 @@ __metadata: languageName: node linkType: hard -"@octokit/oauth-authorization-url@npm:^5.0.0": - version: 5.0.0 - resolution: "@octokit/oauth-authorization-url@npm:5.0.0" - checksum: bc457c4af9559e9e8f752e643fc9d116247f4e4246e69959d99b9e39196c93d7af53c1c8e3bd946bd0e4fc29f7ba27efe9bced8525ffa41fe45ef56a8281014b +"@octokit/oauth-authorization-url@npm:^6.0.2": + version: 6.0.2 + resolution: "@octokit/oauth-authorization-url@npm:6.0.2" + checksum: 0f11169a3eeb782cc08312c923de1a702b25ae033b972ba40380b6d72cb3f684543c8b6a5cf6f05936fdc6b8892070d4f7581138d8efc1b4c4a55ae6d7762327 languageName: node linkType: hard -"@octokit/oauth-methods@npm:^2.0.0": - version: 2.0.6 - resolution: "@octokit/oauth-methods@npm:2.0.6" +"@octokit/oauth-methods@npm:^3.0.1": + version: 3.0.1 + resolution: "@octokit/oauth-methods@npm:3.0.1" dependencies: - "@octokit/oauth-authorization-url": ^5.0.0 - "@octokit/request": ^6.2.3 - "@octokit/request-error": ^3.0.3 - "@octokit/types": ^9.0.0 + "@octokit/oauth-authorization-url": ^6.0.2 + "@octokit/request": ^7.0.0 + "@octokit/request-error": ^4.0.1 + "@octokit/types": ^10.0.0 btoa-lite: ^1.0.0 - checksum: 151b933d79d6fbf36fdfae8cdc868a3d43316352eaccf46cb8c420cfd238658275e41996d2d377177553bc0c637c3aefe8ca99c1ab7fd62054654b6119b7b1cc + checksum: ad327084f97d2f3be270d8957545dbd06c35df3e99d8e58702217beb7ac3574c361b49dfe28ba5d96b7f1911ac9c8e26ae07d6180a0598eef8b7fab4b0fe4ad5 languageName: node linkType: hard @@ -3334,20 +3328,20 @@ __metadata: languageName: node linkType: hard -"@octokit/plugin-retry@npm:5.0.3": - version: 5.0.3 - resolution: "@octokit/plugin-retry@npm:5.0.3" +"@octokit/plugin-retry@npm:5.0.4": + version: 5.0.4 + resolution: "@octokit/plugin-retry@npm:5.0.4" dependencies: "@octokit/request-error": ^4.0.1 - "@octokit/types": ^9.0.0 + "@octokit/types": ^10.0.0 bottleneck: ^2.15.3 peerDependencies: "@octokit/core": ">=3" - checksum: f98b90333e26a214f057557ee5b13a926e0472a47d103345c504f99e6c3d8564a8fa54bf2871eda8ef47a8f9c1dba84fb68e749ab7a1a749c0a86a3ae74bdfa7 + checksum: 0c5645613f7ff758ac126da11ba20b4d49e4067676e30808f5ee3ee471adbd2ccfdea2200adfa5a4663b207964b3d60987f4c5e8682fb275bf134b33f2ef5178 languageName: node linkType: hard -"@octokit/request-error@npm:^3.0.0, @octokit/request-error@npm:^3.0.3": +"@octokit/request-error@npm:^3.0.0": version: 3.0.3 resolution: "@octokit/request-error@npm:3.0.3" dependencies: @@ -3369,7 +3363,7 @@ __metadata: languageName: node linkType: hard -"@octokit/request@npm:^6.0.0, @octokit/request@npm:^6.2.3": +"@octokit/request@npm:^6.0.0": version: 6.2.8 resolution: "@octokit/request@npm:6.2.8" dependencies: @@ -3383,15 +3377,28 @@ __metadata: languageName: node linkType: hard -"@octokit/rest@npm:19.0.11": - version: 19.0.11 - resolution: "@octokit/rest@npm:19.0.11" +"@octokit/request@npm:^7.0.0": + version: 7.0.0 + resolution: "@octokit/request@npm:7.0.0" + dependencies: + "@octokit/endpoint": ^8.0.0 + "@octokit/request-error": ^4.0.1 + "@octokit/types": ^10.0.0 + is-plain-object: ^5.0.0 + universal-user-agent: ^6.0.0 + checksum: d3b8ac25c3702bb69c5b345f7a9f16b158209db7e244cc2d60dbcbfbaf1edec8252d78885de3607ee85eb86db7c1d2e07fa2515ba6e25cf2880440c0df5e918a + languageName: node + linkType: hard + +"@octokit/rest@npm:19.0.13": + version: 19.0.13 + resolution: "@octokit/rest@npm:19.0.13" dependencies: "@octokit/core": ^4.2.1 "@octokit/plugin-paginate-rest": ^6.1.2 "@octokit/plugin-request-log": ^1.0.4 "@octokit/plugin-rest-endpoint-methods": ^7.1.2 - checksum: 147518ad51d214ead88adc717b5fdc4f33317949d58c124f4069bdf07d2e6b49fa66861036b9e233aed71fcb88ff367a6da0357653484e466175ab4fb7183b3b + checksum: ca1553e3fe46efabffef60e68e4a228d4cc0f0d545daf7f019560f666d3e934c6f3a6402a42bbd786af4f3c0a6e69380776312f01b7d52998fe1bbdd1b068f69 languageName: node linkType: hard @@ -4785,126 +4792,126 @@ __metadata: languageName: node linkType: hard -"@vaadin/a11y-base@npm:~24.1.0": - version: 24.1.0 - resolution: "@vaadin/a11y-base@npm:24.1.0" +"@vaadin/a11y-base@npm:~24.1.1": + version: 24.1.1 + resolution: "@vaadin/a11y-base@npm:24.1.1" dependencies: "@open-wc/dedupe-mixin": ^1.3.0 "@polymer/polymer": ^3.0.0 - "@vaadin/component-base": ~24.1.0 + "@vaadin/component-base": ~24.1.1 lit: ^2.0.0 - checksum: f019f2c04473c60c3714ec3da65a129833e1fab4e2eefb4f88d4caa81eb45da756dce2cec8b222bd259d0b87bc67439a4aa88f636b90e6f704c037197bdc1492 + checksum: 1b83e3e44ebc8c395a5ba9a6bc92d42aeb6afce3bcd6a1c492bbc7eb166a228474bf7dc56fb6509d47618fdd87107c60e4ebc60d2aab3c72dcc1392465fd7ac1 languageName: node linkType: hard -"@vaadin/combo-box@npm:24.1.0": - version: 24.1.0 - resolution: "@vaadin/combo-box@npm:24.1.0" +"@vaadin/combo-box@npm:24.1.1": + version: 24.1.1 + resolution: "@vaadin/combo-box@npm:24.1.1" dependencies: "@open-wc/dedupe-mixin": ^1.3.0 "@polymer/polymer": ^3.0.0 - "@vaadin/a11y-base": ~24.1.0 - "@vaadin/component-base": ~24.1.0 - "@vaadin/field-base": ~24.1.0 - "@vaadin/input-container": ~24.1.0 - "@vaadin/item": ~24.1.0 - "@vaadin/lit-renderer": ~24.1.0 - "@vaadin/overlay": ~24.1.0 - "@vaadin/vaadin-lumo-styles": ~24.1.0 - "@vaadin/vaadin-material-styles": ~24.1.0 - "@vaadin/vaadin-themable-mixin": ~24.1.0 - checksum: cbdbfba535a16faa84a897d61565e91d1b2ec0dad87c1644e287c180fed13fcf3e2b0c436fb85ad7f394a4bb7aceb596b9070e3e171afe8167f2158908e71ea5 + "@vaadin/a11y-base": ~24.1.1 + "@vaadin/component-base": ~24.1.1 + "@vaadin/field-base": ~24.1.1 + "@vaadin/input-container": ~24.1.1 + "@vaadin/item": ~24.1.1 + "@vaadin/lit-renderer": ~24.1.1 + "@vaadin/overlay": ~24.1.1 + "@vaadin/vaadin-lumo-styles": ~24.1.1 + "@vaadin/vaadin-material-styles": ~24.1.1 + "@vaadin/vaadin-themable-mixin": ~24.1.1 + checksum: 0011a1271ebe41c6eaf5d5cf5f15c7cfdfab46534c70b4bdbcc0b1afdd6e83e0c7983d3927db6df4c7669269d7909a3d886586696b9d75d291a6a7876db22ce6 languageName: node linkType: hard -"@vaadin/component-base@npm:~24.1.0": - version: 24.1.0 - resolution: "@vaadin/component-base@npm:24.1.0" +"@vaadin/component-base@npm:~24.1.1": + version: 24.1.1 + resolution: "@vaadin/component-base@npm:24.1.1" dependencies: "@open-wc/dedupe-mixin": ^1.3.0 "@polymer/polymer": ^3.0.0 "@vaadin/vaadin-development-mode-detector": ^2.0.0 "@vaadin/vaadin-usage-statistics": ^2.1.0 lit: ^2.0.0 - checksum: 1df8786d8ef1b3a2a104101c8877464d6fca3ea70fa851b2f99bcf32bb83780ced05f524b6225e95f901edb8ec9379fe625ac18154367a3a684846004277badc + checksum: 34c698266897cf7c3c5f8b8798468f5035ae764b1743e6a93f5ea1921b3d29e642db51e4d6d71c6594ba03dee14fa0704b34ccb70b7f50ecbfd07677bde231ac languageName: node linkType: hard -"@vaadin/field-base@npm:~24.1.0": - version: 24.1.0 - resolution: "@vaadin/field-base@npm:24.1.0" +"@vaadin/field-base@npm:~24.1.1": + version: 24.1.1 + resolution: "@vaadin/field-base@npm:24.1.1" dependencies: "@open-wc/dedupe-mixin": ^1.3.0 "@polymer/polymer": ^3.0.0 - "@vaadin/a11y-base": ~24.1.0 - "@vaadin/component-base": ~24.1.0 + "@vaadin/a11y-base": ~24.1.1 + "@vaadin/component-base": ~24.1.1 lit: ^2.0.0 - checksum: 5e37ede91e05dd8eb9fa43749b89670904abfc30e522eebb4ec2225318cf72774dd4e94e49deb1b55daea719818803d90968c4d0f4b87132d24289345e728abd + checksum: b16d5579d4a5f43a62df431b7e7e3107bf5a8062ad681b6ce0c1c345dc56ce4f0ae4f0909e2e9ff0fb05d9f2f4b54e12f17b75a680c69b9f24f8f82b63c0b234 languageName: node linkType: hard -"@vaadin/icon@npm:~24.1.0": - version: 24.1.0 - resolution: "@vaadin/icon@npm:24.1.0" +"@vaadin/icon@npm:~24.1.1": + version: 24.1.1 + resolution: "@vaadin/icon@npm:24.1.1" dependencies: "@polymer/polymer": ^3.0.0 - "@vaadin/component-base": ~24.1.0 - "@vaadin/vaadin-lumo-styles": ~24.1.0 - "@vaadin/vaadin-themable-mixin": ~24.1.0 + "@vaadin/component-base": ~24.1.1 + "@vaadin/vaadin-lumo-styles": ~24.1.1 + "@vaadin/vaadin-themable-mixin": ~24.1.1 lit: ^2.0.0 - checksum: 60ee5e3056d175b032c1ad41b3b208b2289c2ef043e4f073e86f691ed135ea98a8780fe0624c9347858f5f0f44a0cc58c345d51eb3795fac47cdafd6cc1a8c59 + checksum: be3f8986e04f163791c0fdbc51c5d7c8074b12548f151b58a3f357ab639cc5c0c53b3375ded7936cd8c618df19dcfef4c616735b24e81da6ae1fca753d4c0774 languageName: node linkType: hard -"@vaadin/input-container@npm:~24.1.0": - version: 24.1.0 - resolution: "@vaadin/input-container@npm:24.1.0" +"@vaadin/input-container@npm:~24.1.1": + version: 24.1.1 + resolution: "@vaadin/input-container@npm:24.1.1" dependencies: "@polymer/polymer": ^3.0.0 - "@vaadin/component-base": ~24.1.0 - "@vaadin/vaadin-lumo-styles": ~24.1.0 - "@vaadin/vaadin-material-styles": ~24.1.0 - "@vaadin/vaadin-themable-mixin": ~24.1.0 - checksum: d222ec0df6c3e169341d8e1c4bc5b15da6f4324bb962537929b864df389f24474195c0088b1d06d88aaecc18f63938c5f5fa614d8fcda233c8ea5222fc31f183 + "@vaadin/component-base": ~24.1.1 + "@vaadin/vaadin-lumo-styles": ~24.1.1 + "@vaadin/vaadin-material-styles": ~24.1.1 + "@vaadin/vaadin-themable-mixin": ~24.1.1 + checksum: b8934fae0f5578b78f4ee05c506b59e66c247e3fdf6d42b1ba7d198d77af170907b1f3cd98ee5ce7bd540d0f5c1f4c08d8febdfa35f7231ed56d289b0eb7432b languageName: node linkType: hard -"@vaadin/item@npm:~24.1.0": - version: 24.1.0 - resolution: "@vaadin/item@npm:24.1.0" +"@vaadin/item@npm:~24.1.1": + version: 24.1.1 + resolution: "@vaadin/item@npm:24.1.1" dependencies: "@open-wc/dedupe-mixin": ^1.3.0 "@polymer/polymer": ^3.0.0 - "@vaadin/a11y-base": ~24.1.0 - "@vaadin/component-base": ~24.1.0 - "@vaadin/vaadin-lumo-styles": ~24.1.0 - "@vaadin/vaadin-material-styles": ~24.1.0 - "@vaadin/vaadin-themable-mixin": ~24.1.0 - checksum: 643f47f7e4ae74cffa3e891789c0689063d73552d81aa84dad66d3f415e624e734f13ae0b0123710984fa8390ea2df1f468d9415d7d914150695821045e09ea0 + "@vaadin/a11y-base": ~24.1.1 + "@vaadin/component-base": ~24.1.1 + "@vaadin/vaadin-lumo-styles": ~24.1.1 + "@vaadin/vaadin-material-styles": ~24.1.1 + "@vaadin/vaadin-themable-mixin": ~24.1.1 + checksum: a87529f5c0c385920d36173b670afbfaaa83d0c171cea048daf7986fbdfb7d82cfef651bc806043ca007a61c1f92b47bba489240b0917d15c75ad49fabab2153 languageName: node linkType: hard -"@vaadin/lit-renderer@npm:~24.1.0": - version: 24.1.0 - resolution: "@vaadin/lit-renderer@npm:24.1.0" +"@vaadin/lit-renderer@npm:~24.1.1": + version: 24.1.1 + resolution: "@vaadin/lit-renderer@npm:24.1.1" dependencies: lit: ^2.0.0 - checksum: 9f0940e0245f608dc613cb33ffdb4f88c275597f7b25fac04892d29ddfc752801fde118fca47bd8445a9d51bca203c339216186ca9b4941b0b6f07a52cc4fc9a + checksum: 17a22abce1654c9b6c86e8a113778d61d5780e45164d3362741b00f47e061cfd88521127802f16ce2ad3ba92ed1535829c8b154183cc6f4fbececdbbb70f4233 languageName: node linkType: hard -"@vaadin/overlay@npm:~24.1.0": - version: 24.1.0 - resolution: "@vaadin/overlay@npm:24.1.0" +"@vaadin/overlay@npm:~24.1.1": + version: 24.1.1 + resolution: "@vaadin/overlay@npm:24.1.1" dependencies: "@open-wc/dedupe-mixin": ^1.3.0 "@polymer/polymer": ^3.0.0 - "@vaadin/a11y-base": ~24.1.0 - "@vaadin/component-base": ~24.1.0 - "@vaadin/vaadin-lumo-styles": ~24.1.0 - "@vaadin/vaadin-material-styles": ~24.1.0 - "@vaadin/vaadin-themable-mixin": ~24.1.0 - checksum: 8362db034347e8186c4397de55fd51b69e645f621614298b68fa383e4957a6ea8290b0770b3d686217ce937a7a18d33ea0ea6844d3da4d3aa3a61d7498210b80 + "@vaadin/a11y-base": ~24.1.1 + "@vaadin/component-base": ~24.1.1 + "@vaadin/vaadin-lumo-styles": ~24.1.1 + "@vaadin/vaadin-material-styles": ~24.1.1 + "@vaadin/vaadin-themable-mixin": ~24.1.1 + checksum: d0def2106e4fff7d7c49931e9b917c68994f371a0246e076442e33d97ac7a25341d9794aee9e41c7c05c94111dacac74cb5648f1814fb45f11cf37ffe6850fa1 languageName: node linkType: hard @@ -4915,34 +4922,34 @@ __metadata: languageName: node linkType: hard -"@vaadin/vaadin-lumo-styles@npm:~24.1.0": - version: 24.1.0 - resolution: "@vaadin/vaadin-lumo-styles@npm:24.1.0" +"@vaadin/vaadin-lumo-styles@npm:~24.1.1": + version: 24.1.1 + resolution: "@vaadin/vaadin-lumo-styles@npm:24.1.1" dependencies: "@polymer/polymer": ^3.0.0 - "@vaadin/icon": ~24.1.0 - "@vaadin/vaadin-themable-mixin": ~24.1.0 - checksum: 68c55fadb2468048b3fe2ae14c8e5fdb90cb35a171c1a2dc7e33b369d8f79565b6e1c5a93a26dbc5e24b3f7b7e5634b87459fd5528e58bf045cc6c5717840703 + "@vaadin/icon": ~24.1.1 + "@vaadin/vaadin-themable-mixin": ~24.1.1 + checksum: ab344ce558de8f1075de6290517169bd3e95cf5038549b987ca7cfb14a9798ca12573e099958fecd518dd74f2bcfa76030dfc5d3b6e46b34dff10e4d675182c5 languageName: node linkType: hard -"@vaadin/vaadin-material-styles@npm:~24.1.0": - version: 24.1.0 - resolution: "@vaadin/vaadin-material-styles@npm:24.1.0" +"@vaadin/vaadin-material-styles@npm:~24.1.1": + version: 24.1.1 + resolution: "@vaadin/vaadin-material-styles@npm:24.1.1" dependencies: "@polymer/polymer": ^3.0.0 - "@vaadin/vaadin-themable-mixin": ~24.1.0 - checksum: 205e67f5a99dda6cdf1410a0786408d42b8ea48c18c26b3a89d9524fd651b89993db12f3ccfff6635d7981a557416aa8656696d42e6f58fe593d6845b88334ac + "@vaadin/vaadin-themable-mixin": ~24.1.1 + checksum: 601e345da8858a62804b1afde06a79f93e9b5c41fcc263bb692b6db167937e3dd1b754b502ceeaf4054586d3e04a68a167ba7bc2719ec4ad8ac10005795d9a6e languageName: node linkType: hard -"@vaadin/vaadin-themable-mixin@npm:24.1.0, @vaadin/vaadin-themable-mixin@npm:~24.1.0": - version: 24.1.0 - resolution: "@vaadin/vaadin-themable-mixin@npm:24.1.0" +"@vaadin/vaadin-themable-mixin@npm:24.1.1, @vaadin/vaadin-themable-mixin@npm:~24.1.1": + version: 24.1.1 + resolution: "@vaadin/vaadin-themable-mixin@npm:24.1.1" dependencies: "@open-wc/dedupe-mixin": ^1.3.0 lit: ^2.0.0 - checksum: 0abe57312bdda606b52ce93843e82628310e419cbfe4c8bd564c574f7883c8979861b1eb34982bab4a488a82a467dd80cd482e018154ce343310b2918146808d + checksum: 5066300dcf6c987e43bb9c2e16d75198188220dfbde0c76d3d875444200f05b4de70d50fd3d082c0ac0b5075953835d1499ed01d51236e06a56d5ca9e6d25e4c languageName: node linkType: hard @@ -5065,6 +5072,13 @@ __metadata: languageName: node linkType: hard +"@vscode/web-custom-data@npm:^0.4.2": + version: 0.4.6 + resolution: "@vscode/web-custom-data@npm:0.4.6" + checksum: 2d87f3e50dc6eeacdbbca224f1c8837154acd6e1588f733ff812423e72f0a14d819740bbb91732a9c09978faa0fcfe1be339a17fd0beb130e51784752cef4b4f + languageName: node + linkType: hard + "@vue/compiler-sfc@npm:2.7.14": version: 2.7.14 resolution: "@vue/compiler-sfc@npm:2.7.14" @@ -6472,13 +6486,6 @@ __metadata: languageName: node linkType: hard -"call-me-maybe@npm:^1.0.1": - version: 1.0.2 - resolution: "call-me-maybe@npm:1.0.2" - checksum: 42ff2d0bed5b207e3f0122589162eaaa47ba618f79ad2382fe0ba14d9e49fbf901099a6227440acc5946f86a4953e8aa2d242b330b0a5de4d090bb18f8935cae - languageName: node - linkType: hard - "callsites@npm:^3.0.0": version: 3.1.0 resolution: "callsites@npm:3.1.0" @@ -8144,14 +8151,14 @@ __metadata: languageName: node linkType: hard -"eslint@npm:8.42.0": - version: 8.42.0 - resolution: "eslint@npm:8.42.0" +"eslint@npm:8.43.0": + version: 8.43.0 + resolution: "eslint@npm:8.43.0" dependencies: "@eslint-community/eslint-utils": ^4.2.0 "@eslint-community/regexpp": ^4.4.0 "@eslint/eslintrc": ^2.0.3 - "@eslint/js": 8.42.0 + "@eslint/js": 8.43.0 "@humanwhocodes/config-array": ^0.11.10 "@humanwhocodes/module-importer": ^1.0.1 "@nodelib/fs.walk": ^1.2.8 @@ -8189,7 +8196,7 @@ __metadata: text-table: ^0.2.0 bin: eslint: bin/eslint.js - checksum: 07105397b5f2ff4064b983b8971e8c379ec04b1dfcc9d918976b3e00377189000161dac991d82ba14f8759e466091b8c71146f602930ca810c290ee3fcb3faf0 + checksum: 55654ce00b0d128822b57526e40473d0497c7c6be3886afdc0b41b6b0dfbd34d0eae8159911b18451b4db51a939a0e6d2e117e847ae419086884fc3d4fe23c7c languageName: node linkType: hard @@ -8494,20 +8501,6 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^2.2.6": - version: 2.2.7 - resolution: "fast-glob@npm:2.2.7" - dependencies: - "@mrmlnc/readdir-enhanced": ^2.2.1 - "@nodelib/fs.stat": ^1.1.2 - glob-parent: ^3.1.0 - is-glob: ^4.0.0 - merge2: ^1.2.3 - micromatch: ^3.1.10 - checksum: 304ccff1d437fcc44ae0168b0c3899054b92e0fd6af6ad7c3ccc82ab4ddd210b99c7c739d60ee3686da2aa165cd1a31810b31fd91f7c2a575d297342a9fc0534 - languageName: node - linkType: hard - "fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.2, fast-glob@npm:^3.2.9": version: 3.2.12 resolution: "fast-glob@npm:3.2.12" @@ -9141,13 +9134,6 @@ __metadata: languageName: node linkType: hard -"glob-to-regexp@npm:^0.3.0": - version: 0.3.0 - resolution: "glob-to-regexp@npm:0.3.0" - checksum: d34b3219d860042d508c4893b67617cd16e2668827e445ff39cff9f72ef70361d3dc24f429e003cdfb6607c75c9664b8eadc41d2eeb95690af0b0d3113c1b23b - languageName: node - linkType: hard - "glob-to-regexp@npm:^0.4.1": version: 0.4.1 resolution: "glob-to-regexp@npm:0.4.1" @@ -9633,9 +9619,9 @@ __metadata: "@fullcalendar/timegrid": 6.1.8 "@koa/cors": 4.0.0 "@lezer/highlight": 1.1.6 - "@lit-labs/context": 0.3.2 + "@lit-labs/context": 0.3.3 "@lit-labs/motion": 1.0.3 - "@lit-labs/virtualizer": 2.0.2 + "@lit-labs/virtualizer": 2.0.3 "@lrnwebcomponents/simple-tooltip": 7.0.2 "@material/chips": =14.0.0-canary.53b3cad2f.0 "@material/data-table": =14.0.0-canary.53b3cad2f.0 @@ -9665,9 +9651,9 @@ __metadata: "@material/web": =1.0.0-pre.10 "@mdi/js": 7.2.96 "@mdi/svg": 7.2.96 - "@octokit/auth-oauth-device": 5.0.0 - "@octokit/plugin-retry": 5.0.3 - "@octokit/rest": 19.0.11 + "@octokit/auth-oauth-device": 5.0.2 + "@octokit/plugin-retry": 5.0.4 + "@octokit/rest": 19.0.13 "@open-wc/dev-server-hmr": 0.1.4 "@polymer/app-layout": 3.1.0 "@polymer/iron-flex-layout": 3.0.1 @@ -9704,8 +9690,8 @@ __metadata: "@types/webspeechapi": 0.0.29 "@typescript-eslint/eslint-plugin": 5.59.11 "@typescript-eslint/parser": 5.59.11 - "@vaadin/combo-box": 24.1.0 - "@vaadin/vaadin-themable-mixin": 24.1.0 + "@vaadin/combo-box": 24.1.1 + "@vaadin/vaadin-themable-mixin": 24.1.1 "@vibrant/color": 3.2.1-alpha.1 "@vibrant/core": 3.2.1-alpha.1 "@vibrant/quantizer-mmcq": 3.2.1-alpha.1 @@ -9727,7 +9713,7 @@ __metadata: deep-clone-simple: 1.1.1 deep-freeze: 0.0.1 del: 7.0.0 - eslint: 8.42.0 + eslint: 8.43.0 eslint-config-airbnb-base: 15.0.0 eslint-config-airbnb-typescript: 17.0.0 eslint-config-prettier: 8.8.0 @@ -9763,7 +9749,7 @@ __metadata: leaflet-draw: 1.0.4 lint-staged: 13.2.2 lit: 2.7.5 - lit-analyzer: 1.2.1 + lit-analyzer: 2.0.0-pre.3 lodash.template: 4.5.0 magic-string: 0.30.0 map-stream: 0.0.7 @@ -9796,10 +9782,10 @@ __metadata: tar: 6.1.15 terser-webpack-plugin: 5.3.9 tinykeys: 2.1.0 - ts-lit-plugin: 1.2.1 + ts-lit-plugin: 2.0.0-pre.1 tsparticles-engine: 2.10.1 tsparticles-preset-links: 2.10.1 - typescript: 4.9.5 + typescript: 5.1.3 unfetch: 5.0.0 vinyl-buffer: 1.0.1 vinyl-source-stream: 2.0.0 @@ -11367,21 +11353,22 @@ __metadata: languageName: node linkType: hard -"lit-analyzer@npm:1.2.1": - version: 1.2.1 - resolution: "lit-analyzer@npm:1.2.1" +"lit-analyzer@npm:2.0.0-pre.3, lit-analyzer@npm:^2.0.0-pre.3": + version: 2.0.0-pre.3 + resolution: "lit-analyzer@npm:2.0.0-pre.3" dependencies: + "@vscode/web-custom-data": ^0.4.2 chalk: ^2.4.2 didyoumean2: 4.1.0 - fast-glob: ^2.2.6 + fast-glob: ^3.2.11 parse5: 5.1.0 - ts-simple-type: ~1.0.5 + ts-simple-type: ~2.0.0-next.0 vscode-css-languageservice: 4.3.0 vscode-html-languageservice: 3.1.0 - web-component-analyzer: ~1.1.1 + web-component-analyzer: ^2.0.0-next.5 bin: lit-analyzer: cli.js - checksum: b89646033b45262a863bf32d8bf177bfa4f22cde4e2c3f2cd006abdd68aeab434505f67c3c5ed213d8a5936d063ec2845efb15b0968ec9cf9e0863e53e6c118c + checksum: c6dcf657a0030342d183fcd9d550753bd5dd0692b478aff271085a5a0e7e08aff39cc6dde3d547ffca72897975ef07aac7de8a97e0060d2db70a85350412efae languageName: node linkType: hard @@ -11798,7 +11785,7 @@ __metadata: languageName: node linkType: hard -"merge2@npm:^1.2.3, merge2@npm:^1.3.0, merge2@npm:^1.4.1": +"merge2@npm:^1.3.0, merge2@npm:^1.4.1": version: 1.4.1 resolution: "merge2@npm:1.4.1" checksum: 7268db63ed5169466540b6fb947aec313200bcf6d40c5ab722c22e242f651994619bcd85601602972d3c85bd2cc45a358a4c61937e9f11a061919a1da569b0c2 @@ -15215,19 +15202,20 @@ __metadata: languageName: node linkType: hard -"ts-lit-plugin@npm:1.2.1": - version: 1.2.1 - resolution: "ts-lit-plugin@npm:1.2.1" +"ts-lit-plugin@npm:2.0.0-pre.1": + version: 2.0.0-pre.1 + resolution: "ts-lit-plugin@npm:2.0.0-pre.1" dependencies: - lit-analyzer: 1.2.1 - checksum: 3ba191d8924b18ba1aa1072db82cd10bca19f20693d98735dc1bbf3692ec759f2d4c728b789a1c1ade4d96e5ddf25e574fdba7c5e42b557c7e82da7a1ad298d7 + lit-analyzer: ^2.0.0-pre.3 + web-component-analyzer: ^2.0.0-next.5 + checksum: d9c8b3c0d8867e9564c7a3083accaf9f8b48b04c8f42e32c224806a7a606983abb3b7acb912e57d0d8f90ff650745bb1af18b1e379316b433838e37eddccfe1e languageName: node linkType: hard -"ts-simple-type@npm:~1.0.5": - version: 1.0.7 - resolution: "ts-simple-type@npm:1.0.7" - checksum: 3cffb45eab7ab7fd963e2765914c41488d9611dc0619334ac1cf01bc2b02cf9746adf12172d785894474c6c5f3cfbf8212e675e456362373a0c2a61441ad8572 +"ts-simple-type@npm:2.0.0-next.0, ts-simple-type@npm:~2.0.0-next.0": + version: 2.0.0-next.0 + resolution: "ts-simple-type@npm:2.0.0-next.0" + checksum: 025c5c1e4f2f7f2627300b2605a0346d5007f9c3d20d075807b01b3ae8179261e6be2d471f74948f3bae3208ca042203d97e80b984d6cb133396c5c4a3af5301 languageName: node linkType: hard @@ -15440,43 +15428,43 @@ __metadata: languageName: node linkType: hard -"typescript@npm:4.9.5": - version: 4.9.5 - resolution: "typescript@npm:4.9.5" +"typescript@npm:5.1.3": + version: 5.1.3 + resolution: "typescript@npm:5.1.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: ee000bc26848147ad423b581bd250075662a354d84f0e06eb76d3b892328d8d4440b7487b5a83e851b12b255f55d71835b008a66cbf8f255a11e4400159237db + checksum: d9d51862d98efa46534f2800a1071a613751b1585dc78884807d0c179bcd93d6e9d4012a508e276742f5f33c480adefc52ffcafaf9e0e00ab641a14cde9a31c7 languageName: node linkType: hard -"typescript@npm:^3.8.3": - version: 3.9.10 - resolution: "typescript@npm:3.9.10" +"typescript@npm:~4.4.3": + version: 4.4.4 + resolution: "typescript@npm:4.4.4" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 46c842e2cd4797b88b66ef06c9c41dd21da48b95787072ccf39d5f2aa3124361bc4c966aa1c7f709fae0509614d76751455b5231b12dbb72eb97a31369e1ff92 + checksum: 89ecb8436bb48ef5594d49289f5f89103071716b6e4844278f4fb3362856e31203e187a9c76d205c3f0b674d221a058fd28310dbcbcf5d95e9a57229bb5203f1 languageName: node linkType: hard -"typescript@patch:typescript@4.9.5#~builtin": - version: 4.9.5 - resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=289587" +"typescript@patch:typescript@5.1.3#~builtin": + version: 5.1.3 + resolution: "typescript@patch:typescript@npm%3A5.1.3#~builtin::version=5.1.3&hash=5da071" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 1f8f3b6aaea19f0f67cba79057674ba580438a7db55057eb89cc06950483c5d632115c14077f6663ea76fd09fce3c190e6414bb98582ec80aa5a4eaf345d5b68 + checksum: 6f0a9dca6bf4ce9dcaf4e282aade55ef4c56ecb5fb98d0a4a5c0113398815aea66d871b5611e83353e5953a19ed9ef103cf5a76ac0f276d550d1e7cd5344f61e languageName: node linkType: hard -"typescript@patch:typescript@^3.8.3#~builtin": - version: 3.9.10 - resolution: "typescript@patch:typescript@npm%3A3.9.10#~builtin::version=3.9.10&hash=3bd3d3" +"typescript@patch:typescript@~4.4.3#~builtin": + version: 4.4.4 + resolution: "typescript@patch:typescript@npm%3A4.4.4#~builtin::version=4.4.4&hash=bbeadb" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: dc7141ab555b23a8650a6787f98845fc11692063d02b75ff49433091b3af2fe3d773650dea18389d7c21f47d620fb3b110ea363dab4ab039417a6ccbbaf96fc2 + checksum: 3d1b04449662193544b81d055479d03b4c5dca95f1a82f8922596f089d894c9fefbe16639d1d9dfe26a7054419645530cef44001bc17aed1fe1eb3c237e9b3c7 languageName: node linkType: hard @@ -15978,18 +15966,18 @@ __metadata: languageName: node linkType: hard -"web-component-analyzer@npm:~1.1.1": - version: 1.1.7 - resolution: "web-component-analyzer@npm:1.1.7" +"web-component-analyzer@npm:^2.0.0-next.5": + version: 2.0.0-next.5 + resolution: "web-component-analyzer@npm:2.0.0-next.5" dependencies: fast-glob: ^3.2.2 - ts-simple-type: ~1.0.5 - typescript: ^3.8.3 + ts-simple-type: 2.0.0-next.0 + typescript: ~4.4.3 yargs: ^15.3.1 bin: wca: cli.js web-component-analyzer: cli.js - checksum: 6c36521b7b79d5547ffdbc359029651ad1d929df6e09f8adfbafb2a34c23199712b7080f08f941f056b6a989718c11eb9221171d97ad397ed8a20cf08dd78e4b + checksum: bc8eaf57884b81d378014376bcab92bce6b127971952831dd5bb06803b590cc99fb6fa17c7476c02ee014dfccfcee80e670d7fdc7aff4aae6aebc2e262055a65 languageName: node linkType: hard