Merge branch 'dev' into persistent_notification_trigger

This commit is contained in:
J. Nick Koston 2023-06-21 09:58:42 +02:00
commit 1e5c35c158
No known key found for this signature in database
96 changed files with 3614 additions and 1128 deletions

View File

@ -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 \

View File

@ -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:

View File

@ -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 }}

View File

@ -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: {

View File

@ -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",
},
];

View File

@ -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`

View File

@ -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`
<ha-control-select
.value=${this.selection}
.options=${timeOptions}
@value-changed=${this.handleValueChanged}
>
</ha-control-select>
<mwc-list>
<div class="container header">
<div>Language</div>
<div class="center">Default (lang)</div>
<div class="center">12 Hours</div>
<div class="center">24 Hours</div>
</div>
${Object.entries(translationMetadata.translations)
.filter(([key, _]) => key !== "test")
.map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatDateTimeNumeric(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTimeNumeric(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTimeNumeric(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list>
`;
}
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;
}
}

View File

@ -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`

View File

@ -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`
<ha-control-select
.value=${this.selection}
.options=${timeOptions}
@value-changed=${this.handleValueChanged}
>
</ha-control-select>
<mwc-list>
<div class="container header">
<div>Language</div>
<div class="center">Default (lang)</div>
<div class="center">12 Hours</div>
<div class="center">24 Hours</div>
</div>
${Object.entries(translationMetadata.translations)
.filter(([key, _]) => key !== "test")
.map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatDateTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list>
`;
}
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;
}
}

View File

@ -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`

View File

@ -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`
<ha-control-select
.value=${this.selection}
.options=${timeOptions}
@value-changed=${this.handleValueChanged}
>
</ha-control-select>
<mwc-list>
<div class="container header">
<div>Language</div>
<div class="center">Default (lang)</div>
<div class="center">12 Hours</div>
<div class="center">24 Hours</div>
</div>
${Object.entries(translationMetadata.translations)
.filter(([key, _]) => key !== "test")
.map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatShortDateTimeWithYear(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
</div>
<div class="center">
${formatShortDateTimeWithYear(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatShortDateTimeWithYear(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list>
`;
}
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;
}
}

View File

@ -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`

View File

@ -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`
<ha-control-select
.value=${this.selection}
.options=${timeOptions}
@value-changed=${this.handleValueChanged}
>
</ha-control-select>
<mwc-list>
<div class="container header">
<div>Language</div>
<div class="center">Default (lang)</div>
<div class="center">12 Hours</div>
<div class="center">24 Hours</div>
</div>
${Object.entries(translationMetadata.translations)
.filter(([key, _]) => key !== "test")
.map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatShortDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
</div>
<div class="center">
${formatShortDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatShortDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list>
`;
}
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;
}
}

View File

@ -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`

View File

@ -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`
<ha-control-select
.value=${this.selection}
.options=${timeOptions}
@value-changed=${this.handleValueChanged}
>
</ha-control-select>
<mwc-list>
<div class="container header">
<div>Language</div>
<div class="center">Default (lang)</div>
<div class="center">12 Hours</div>
<div class="center">24 Hours</div>
</div>
${Object.entries(translationMetadata.translations)
.filter(([key, _]) => key !== "test")
.map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list>
`;
}
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;
}
}

View File

@ -1,5 +1,5 @@
---
title: (Numeric) Date Formatting
title: Date Format (Numeric)
---
This pages lists all supported languages with their available (numeric) date formats.

View File

@ -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`

View File

@ -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`
<ha-control-select
.value=${this.selection}
.options=${timeOptions}
@value-changed=${this.handleValueChanged}
>
</ha-control-select>
<mwc-list>
<div class="container header">
<div>Language</div>
<div class="center">Default (lang)</div>
<div class="center">12 Hours</div>
<div class="center">24 Hours</div>
</div>
${Object.entries(translationMetadata.translations)
.filter(([key, _]) => key !== "test")
.map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
</div>
<div class="center">
${formatTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list>
`;
}
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;
}
}

View File

@ -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`

View File

@ -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`
<ha-control-select
.value=${this.selection}
.options=${timeOptions}
@value-changed=${this.handleValueChanged}
>
</ha-control-select>
<mwc-list>
<div class="container header">
<div>Language</div>
<div class="center">Default (lang)</div>
<div class="center">12 Hours</div>
<div class="center">24 Hours</div>
</div>
${Object.entries(translationMetadata.translations)
.filter(([key, _]) => key !== "test")
.map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatTimeWeekday(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
</div>
<div class="center">
${formatTimeWeekday(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatTimeWeekday(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list>
`;
}
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;
}
}

View File

@ -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`

View File

@ -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`
<ha-control-select
.value=${this.selection}
.options=${timeOptions}
@value-changed=${this.handleValueChanged}
>
</ha-control-select>
<mwc-list>
<div class="container header">
<div>Language</div>
<div class="center">Default (lang)</div>
<div class="center">12 Hours</div>
<div class="center">24 Hours</div>
</div>
${Object.entries(translationMetadata.translations)
.filter(([key, _]) => key !== "test")
.map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
</div>
<div class="center">
${formatTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list>
`;
}
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;
}
}

View File

@ -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",
}),

View File

@ -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",

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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: [

View File

@ -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)
) {

View File

@ -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}

View File

@ -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}
></ha-combo-box>
`;
}
@ -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<ScorableDevice>(filterString, target.items || [])
: target.items;
}
private _deviceChanged(ev: ValueChangedEvent<string>) {
ev.stopPropagation();
let newValue = ev.detail.value;

View File

@ -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<HassEntityWithCachedName>(filterString, this._states)
: this._states;
}
private _setValue(value: string) {

View File

@ -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<AreaRegistryEntry> = (
item
) => html`<mwc-list-item
@ -306,9 +312,12 @@ export class HaAreaPicker extends LitElement {
this.entityFilter,
this.noAdd,
this.excludeAreas
);
(this.comboBox as any).items = areas;
(this.comboBox as any).filteredItems = areas;
).map((area) => ({
...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<ScorableAreaRegistryEntry>(
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)!,

View File

@ -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;
}

View File

@ -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`<slot></slot>`;
}
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;
}
}

View File

@ -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;
}
}

View File

@ -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`
<div>
${this.label}
@ -85,7 +87,7 @@ export class HaSelectSelector extends LitElement {
(item: SelectOption) => html`
<ha-formfield .label=${item.label}>
<ha-checkbox
.checked=${this.value?.includes(item.value)}
.checked=${value.includes(item.value)}
.value=${item.value}
.disabled=${item.disabled || this.disabled}
@change=${this._checkboxChanged}
@ -100,7 +102,7 @@ export class HaSelectSelector extends LitElement {
if (this.selector.select?.multiple) {
const value =
!this.value || this.value === "" ? [] : (this.value as string[]);
!this.value || this.value === "" ? [] : ensureArray(this.value);
const optionItems = options.filter(
(option) => !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],
});

View File

@ -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}
></ha-theme-picker>

View File

@ -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;

View File

@ -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;

View File

@ -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"
)}</mwc-list-item
>
${this.includeDefault
? html`<mwc-list-item .value=${DEFAULT_THEME}
>${this.hass!.localize(
"ui.components.theme-picker.default"
)}</mwc-list-item
>`
: nothing}
${Object.keys(this.hass!.themes.themes)
.sort()
.map(

View File

@ -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 {

View File

@ -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<HvacAction, HvacMode> = {
cooling: "cool",
drying: "dry",
fan: "fan_only",
preheating: "heat",
heating: "heat",
idle: "off",
off: "off",

View File

@ -159,3 +159,5 @@ export const computeDefaultFavoriteColors = (
return colors;
};
export const formatTempColor = (value: number) => `${value} K`;

20
src/data/lock.ts Normal file
View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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"
></ha-textfield>
<ha-button slot="secondaryAction" dialogAction="cancel">

View File

@ -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;

View File

@ -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 {

View File

@ -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<HASSDomEvents["color-hovered"]>) {
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<void> {
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 {
></ha-icon-button>
<span slot="title">${this._dialogParams?.title}</span>
</ha-dialog-header>
<light-color-picker
.hass=${this.hass}
entityId=${this._entry.entity_id}
.defaultMode=${this._dialogParams?.defaultMode}
@color-changed=${this._colorChanged}
>
</light-color-picker>
<div class="header">
<span class="value">${this._currentValue}</span>
${this._modes.length > 1
? html`
<div class="modes">
${this._modes.map(
(value) =>
html`
<ha-icon-button-toggle
border-only
.selected=${value === this._mode}
.label=${this.hass.localize(
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
)}
.mode=${value}
@click=${this._modeChanged}
>
<span
class="wheel ${classMap({ [value]: true })}"
></span>
</ha-icon-button-toggle>
`
)}
</div>
`
: nothing}
</div>
<div class="content">
${this._mode === "color_temp"
? html`
<light-color-temp-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
@color-hovered=${this._colorHovered}
>
</light-color-temp-picker>
`
: nothing}
${this._mode === "color"
? html`
<light-color-rgb-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
@color-hovered=${this._colorHovered}
>
</light-color-rgb-picker>
`
: nothing}
</div>
<ha-button slot="secondaryAction" dialogAction="cancel">
${this.hass.localize("ui.common.cancel")}
</ha-button>
@ -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;
}
`,
];
}

View File

@ -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"

View File

@ -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`
<light-color-picker
.hass=${this.hass}
.entityId=${this.params.entityId}
.defaultMode=${this.params.defaultMode}
>
</light-color-picker>
`;
}
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;
}
}

View File

@ -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
<div class="color-container">
<label class="native-color-picker">
<input
type="color"
.value=${hexValue ?? ""}
@input=${this._nativeColorChanged}
/>
<ha-svg-icon .path=${mdiEyedropper}></ha-svg-icon>
</label>
<ha-hs-color-picker
@value-changed=${this._hsColorChanged}
@cursor-moved=${this._hsColorCursorMoved}
.value=${this._hsPickerValue}
.colorBrightness=${this._colorBrightnessSliderValue != null
? (this._colorBrightnessSliderValue * 255) / 100
: undefined}
.wv=${this._wvSliderValue != null
? (this._wvSliderValue * 255) / 100
: undefined}
.ww=${this._wwSliderValue != null
? (this._wwSliderValue * 255) / 100
: undefined}
.cw=${this._cwSliderValue != null
? (this._cwSliderValue * 255) / 100
: undefined}
.minKelvin=${this.stateObj.attributes.min_color_temp_kelvin}
.maxKelvin=${this.stateObj.attributes.max_color_temp_kelvin}
>
</ha-hs-color-picker>
</div>
${supportsRgbw || supportsRgbww
? html`<ha-labeled-slider
.caption=${this.hass.localize("ui.card.light.color_brightness")}
icon="hass:brightness-7"
max="100"
.value=${this._colorBrightnessSliderValue}
@change=${this._colorBrightnessSliderChanged}
pin
></ha-labeled-slider>`
: nothing}
${supportsRgbw
? html`
<mwc-tab-bar
.activeIndex=${this._mode ? this._modes.indexOf(this._mode) : 0}
@MDCTabBar:activated=${this._handleTabChanged}
>
${this._modes.map(
(value) =>
html`<mwc-tab
.label=${this.hass.localize(
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
)}
></mwc-tab>`
)}
</mwc-tab-bar>
<ha-labeled-slider
.caption=${this.hass.localize("ui.card.light.white_value")}
icon="hass:file-word-box"
max="100"
.name=${"wv"}
.value=${this._wvSliderValue}
@change=${this._wvSliderChanged}
pin
></ha-labeled-slider>
`
: nothing}
${supportsRgbww
? html`
<ha-labeled-slider
.caption=${this.hass.localize("ui.card.light.cold_white_value")}
icon="hass:file-word-box-outline"
max="100"
.name=${"cw"}
.value=${this._cwSliderValue}
@change=${this._wvSliderChanged}
pin
></ha-labeled-slider>
<ha-labeled-slider
.caption=${this.hass.localize("ui.card.light.warm_white_value")}
icon="hass:file-word-box"
max="100"
.name=${"ww"}
.value=${this._wwSliderValue}
@change=${this._wvSliderChanged}
pin
></ha-labeled-slider>
`
: nothing}
<div class="content">
${this._mode === LightColorMode.COLOR_TEMP
? html`
<p class="color-temp-value">
${this._ctPickerValue ? `${this._ctPickerValue} K` : nothing}
</p>
<ha-temp-color-picker
@value-changed=${this._ctColorChanged}
@cursor-moved=${this._ctColorCursorMoved}
.min=${this.stateObj.attributes.min_color_temp_kelvin!}
.max=${this.stateObj.attributes.max_color_temp_kelvin!}
.value=${this._ctPickerValue}
>
</ha-temp-color-picker>
`
: nothing}
${this._mode === "color"
? html`
<div class="color-container">
<label class="native-color-picker">
<input
type="color"
.value=${hexValue ?? ""}
@input=${this._nativeColorChanged}
/>
<ha-svg-icon .path=${mdiEyedropper}></ha-svg-icon>
</label>
<ha-hs-color-picker
@value-changed=${this._hsColorChanged}
@cursor-moved=${this._hsColorCursorMoved}
.value=${this._hsPickerValue}
.colorBrightness=${this._colorBrightnessSliderValue != null
? (this._colorBrightnessSliderValue * 255) / 100
: undefined}
.wv=${this._wvSliderValue != null
? (this._wvSliderValue * 255) / 100
: undefined}
.ww=${this._wwSliderValue != null
? (this._wwSliderValue * 255) / 100
: undefined}
.cw=${this._cwSliderValue != null
? (this._cwSliderValue * 255) / 100
: undefined}
.minKelvin=${this.stateObj.attributes.min_color_temp_kelvin}
.maxKelvin=${this.stateObj.attributes.max_color_temp_kelvin}
>
</ha-hs-color-picker>
</div>
${supportsRgbw || supportsRgbww
? html`<ha-labeled-slider
.caption=${this.hass.localize(
"ui.card.light.color_brightness"
)}
icon="hass:brightness-7"
max="100"
.value=${this._colorBrightnessSliderValue}
@change=${this._colorBrightnessSliderChanged}
pin
></ha-labeled-slider>`
: nothing}
${supportsRgbw
? html`
<ha-labeled-slider
.caption=${this.hass.localize(
"ui.card.light.white_value"
)}
icon="hass:file-word-box"
max="100"
.name=${"wv"}
.value=${this._wvSliderValue}
@change=${this._wvSliderChanged}
pin
></ha-labeled-slider>
`
: nothing}
${supportsRgbww
? html`
<ha-labeled-slider
.caption=${this.hass.localize(
"ui.card.light.cold_white_value"
)}
icon="hass:file-word-box-outline"
max="100"
.name=${"cw"}
.value=${this._cwSliderValue}
@change=${this._wvSliderChanged}
pin
></ha-labeled-slider>
<ha-labeled-slider
.caption=${this.hass.localize(
"ui.card.light.warm_white_value"
)}
icon="hass:file-word-box"
max="100"
.name=${"ww"}
.value=${this._wwSliderValue}
@change=${this._wvSliderChanged}
pin
></ha-labeled-slider>
`
: nothing}
`
: nothing}
</div>
`;
}
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;
}
}

View File

@ -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`
<ha-temp-color-picker
@value-changed=${this._ctColorChanged}
@cursor-moved=${this._ctColorCursorMoved}
.min=${this.stateObj.attributes.min_color_temp_kelvin!}
.max=${this.stateObj.attributes.max_color_temp_kelvin!}
.value=${this._ctPickerValue}
>
</ha-temp-color-picker>
`;
}
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<string, any>) {
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;
}
}

View File

@ -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;

View File

@ -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,
});
};

View File

@ -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<void> {
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`
<div class="buttons">
<ha-control-button
.label=${this.hass.localize(
"ui.dialogs.more_info_control.lock.lock"
)}
@click=${this._turnOn}
>
<ha-svg-icon .path=${onIcon}></ha-svg-icon>
</ha-control-button>
<ha-control-button
.label=${this.hass.localize(
"ui.dialogs.more_info_control.lock.unlock"
)}
@click=${this._turnOff}
>
<ha-svg-icon .path=${offIcon}></ha-svg-icon>
</ha-control-button>
</div>
`;
}
return html`
<ha-control-switch
.pathOn=${onIcon}
.pathOff=${offIcon}
vertical
reversed
.checked=${this._isOn}
@change=${this._valueChanged}
.ariaLabel=${this.hass.localize("ui.dialogs.more_info_control.toggle")}
style=${styleMap({
"--control-switch-on-color": color,
"--control-switch-off-color": color,
})}
.disabled=${this.stateObj.state === UNAVAILABLE}
>
</ha-control-switch>
`;
}
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;
}
}

View File

@ -22,6 +22,7 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [
"fan",
"input_boolean",
"light",
"lock",
"siren",
"switch",
];

View File

@ -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";

View File

@ -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`
<div class="actions">
<ha-icon-button
<ha-icon-button-group>
<ha-icon-button-toggle
.label=${this.hass.localize(
`ui.dialogs.more_info_control.cover.switch_mode.${
this._mode === "position" ? "button" : "position"
}`
`ui.dialogs.more_info_control.cover.switch_mode.position`
)}
.path=${this._mode === "position"
? mdiSwapVertical
: mdiMenu}
@click=${this._toggleMode}
></ha-icon-button>
</div>
.selected=${this._mode === "position"}
.path=${mdiMenu}
.mode=${"position"}
@click=${this._setMode}
></ha-icon-button-toggle>
<ha-icon-button-toggle
.label=${this.hass.localize(
`ui.dialogs.more_info_control.cover.switch_mode.button`
)}
.selected=${this._mode === "button"}
.path=${mdiSwapVertical}
.mode=${"button"}
@click=${this._setMode}
></ha-icon-button-toggle>
</ha-icon-button-group>
`
: nothing
}

View File

@ -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<HASSDomEvents["color-hovered"]>) {
if (ev.detail && "color_temp_kelvin" in ev.detail) {
this._colorTempPreview = ev.detail.color_temp_kelvin;
} else {
this._colorTempPreview = undefined;
}
}
protected updated(changedProps: PropertyValues<typeof this>): 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`
<ha-more-info-state-header
.hass=${this.hass}
.stateObj=${this.stateObj}
.stateOverride=${stateOverride}
.stateOverride=${this._stateOverride}
></ha-more-info-state-header>
<div class="controls">
${supportsBrightness
${!supportsBrightness
? html`
<ha-more-info-light-brightness
.stateObj=${this.stateObj}
.hass=${this.hass}
@slider-moved=${this._brightnessChanged}
>
</ha-more-info-light-brightness>
`
: html`
<ha-more-info-toggle
.stateObj=${this.stateObj}
.hass=${this.hass}
.iconPathOn=${mdiLightbulb}
.iconPathOff=${mdiLightbulbOff}
></ha-more-info-toggle>
`}
`
: nothing}
${supportsColorTemp || supportsColor || supportsBrightness
? html`
<div class="button-bar">
${supportsBrightness && this._mainControl === "brightness"
? html`
<ha-more-info-light-brightness
.stateObj=${this.stateObj}
.hass=${this.hass}
@slider-moved=${this._brightnessChanged}
>
</ha-more-info-light-brightness>
`
: nothing}
${supportsColor && this._mainControl === "color"
? html`
<light-color-rgb-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
>
</light-color-rgb-picker>
`
: nothing}
${supportsColorTemp && this._mainControl === "color_temp"
? html`
<light-color-temp-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-hovered=${this._tempColorHovered}
>
</light-color-temp-picker>
`
: nothing}
<ha-icon-button-group>
${supportsBrightness
? html`
<ha-icon-button
.disabled=${this.stateObj!.state === UNAVAILABLE}
.title=${this.hass.localize(
"ui.dialogs.more_info_control.light.toggle"
)}
.ariaLabel=${this.hass.localize(
.label=${this.hass.localize(
"ui.dialogs.more_info_control.light.toggle"
)}
@click=${this._toggle}
@ -155,49 +209,60 @@ class MoreInfoLight extends LitElement {
</ha-icon-button>
`
: nothing}
${supportsColor || supportsColorTemp
? html`
<div class="separator"></div>
<ha-icon-button-toggle
.selected=${this._mainControl === "brightness"}
.disabled=${this.stateObj!.state === UNAVAILABLE}
.label=${this.hass.localize(
"ui.dialogs.more_info_control.light.brightness"
)}
.control=${"brightness"}
@click=${this._setMainControl}
>
<ha-svg-icon .path=${mdiBrightness6}></ha-svg-icon>
</ha-icon-button-toggle>
`
: nothing}
${supportsColor
? html`
<ha-icon-button
class="color-mode"
<ha-icon-button-toggle
border-only
.selected=${this._mainControl === "color"}
.disabled=${this.stateObj!.state === UNAVAILABLE}
.title=${this.hass.localize(
"ui.dialogs.more_info_control.light.change_color"
.label=${this.hass.localize(
"ui.dialogs.more_info_control.light.color"
)}
.ariaLabel=${this.hass.localize(
"ui.dialogs.more_info_control.light.change_color"
)}
.mode=${"color"}
@click=${this._showLightColorPickerView}
.control=${"color"}
@click=${this._setMainControl}
>
<span class="wheel color"></span>
</ha-icon-button>
</ha-icon-button-toggle>
`
: nothing}
${supportsColorTemp
? html`
<ha-icon-button
<ha-icon-button-toggle
border-only
.selected=${this._mainControl === "color_temp"}
.disabled=${this.stateObj!.state === UNAVAILABLE}
.title=${this.hass.localize(
"ui.dialogs.more_info_control.light.change_color_temp"
.label=${this.hass.localize(
"ui.dialogs.more_info_control.light.color_temp"
)}
.ariaLabel=${this.hass.localize(
"ui.dialogs.more_info_control.light.change_color_temp"
)}
.mode=${"color_temp"}
@click=${this._showLightColorPickerView}
.control=${"color_temp"}
@click=${this._setMainControl}
>
<span class="wheel color-temp"></span>
</ha-icon-button>
</ha-icon-button-toggle>
`
: nothing}
${supportsWhite
? html`
<div class="separator"></div>
<ha-icon-button
.disabled=${this.stateObj!.state === UNAVAILABLE}
.title=${this.hass.localize(
"ui.dialogs.more_info_control.light.set_white"
)}
.ariaLabel=${this.hass.localize(
.label=${this.hass.localize(
"ui.dialogs.more_info_control.light.set_white"
)}
@click=${this._setWhite}
@ -206,7 +271,7 @@ class MoreInfoLight extends LitElement {
</ha-icon-button>
`
: nothing}
</div>
</ha-icon-button-group>
${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}
>
</ha-more-info-light-favorite-colors>
`
@ -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;

View File

@ -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`<div class="code">
<ha-textfield
.label=${this.hass.localize("ui.card.lock.code")}
.pattern=${this.stateObj.attributes.code_format}
type="password"
></ha-textfield>
${this.stateObj.state === "locked"
? html`<mwc-button
@click=${this._callService}
data-service="unlock"
>${this.hass.localize("ui.card.lock.unlock")}</mwc-button
>`
: html`<mwc-button @click=${this._callService} data-service="lock"
>${this.hass.localize("ui.card.lock.lock")}</mwc-button
>`}
</div>`
: ""}
<ha-more-info-state-header
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-more-info-state-header>
<div class="controls" style=${styleMap(style)}>
${
this.stateObj.state === "jammed"
? html`
<div class="status">
<span></span>
<div class="icon">
<ha-svg-icon
.path=${domainIcon("lock", this.stateObj)}
></ha-svg-icon>
</div>
</div>
`
: html`
<ha-more-info-lock-toggle
.stateObj=${this.stateObj}
.hass=${this.hass}
>
</ha-more-info-lock-toggle>
`
}
${
supportsOpen || isJammed
? html`
<div class="buttons">
${supportsOpen
? html`
<md-outlined-icon-button
.disabled=${this.stateObj.state === UNAVAILABLE}
.title=${this.hass.localize(
"ui.dialogs.more_info_control.lock.open"
)}
.ariaLabel=${this.hass.localize(
"ui.dialogs.more_info_control.lock.open"
)}
@click=${this._open}
>
<ha-svg-icon .path=${mdiDoorOpen}></ha-svg-icon>
</md-outlined-icon-button>
`
: nothing}
${isJammed
? html`
<md-outlined-icon-button
.title=${this.hass.localize(
"ui.dialogs.more_info_control.lock.lock"
)}
.ariaLabel=${this.hass.localize(
"ui.dialogs.more_info_control.lock.lock"
)}
@click=${this._lock}
>
<ha-svg-icon .path=${mdiLock}></ha-svg-icon>
</md-outlined-icon-button>
<md-outlined-icon-button
.title=${this.hass.localize(
"ui.dialogs.more_info_control.lock.unlock"
)}
.ariaLabel=${this.hass.localize(
"ui.dialogs.more_info_control.lock.unlock"
)}
@click=${this._unlock}
>
<ha-svg-icon .path=${mdiLockOff}></ha-svg-icon>
</md-outlined-icon-button>
`
: nothing}
</div>
`
: nothing
}
</div>
</div>
<ha-attributes
.hass=${this.hass}
.stateObj=${this.stateObj}
@ -46,33 +159,64 @@ class MoreInfoLock extends LitElement {
`;
}
private _callService(ev) {
const service = ev.target.getAttribute("data-service");
const data = {
entity_id: this.stateObj!.entity_id,
code: this._textfield?.value,
};
this.hass.callService("lock", service, data);
static get styles(): CSSResultGroup {
return [
moreInfoControlStyle,
css`
md-outlined-icon-button {
--ha-icon-display: block;
--md-sys-color-on-surface: var(--secondary-text-color);
--md-sys-color-on-surface-variant: var(--secondary-text-color);
--md-sys-color-on-surface-rgb: var(--rgb-secondary-text-color);
--md-sys-color-outline: var(--secondary-text-color);
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.status {
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
height: 45vh;
max-height: 320px;
min-height: 200px;
}
.status .icon {
position: relative;
--mdc-icon-size: 80px;
animation: pulse 1s infinite;
color: var(--icon-color);
border-radius: 50%;
width: 144px;
height: 144px;
display: flex;
align-items: center;
justify-content: center;
}
.status .icon::before {
content: "";
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
border-radius: 50%;
background-color: var(--icon-color);
transition: background-color 180ms ease-in-out;
opacity: 0.2;
}
`,
];
}
static styles = css`
:host {
display: flex;
align-items: center;
flex-direction: column;
}
.code {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
margin-bottom: 8px;
width: 100%;
}
ha-attributes {
width: 100%;
}
`;
}
declare global {

View File

@ -24,10 +24,10 @@ import { stopPropagation } from "../../common/dom/stop_propagation";
import "../../components/ha-button";
import "../../components/ha-button-menu";
import "../../components/ha-dialog";
import "../../components/ha-dialog-header";
import "../../components/ha-icon-button";
import "../../components/ha-list-item";
import "../../components/ha-textfield";
import "../../components/ha-dialog-header";
import type { HaTextField } from "../../components/ha-textfield";
import {
AssistPipeline,
@ -41,6 +41,7 @@ import type { HomeAssistant } from "../../types";
import { AudioRecorder } from "../../util/audio-recorder";
import { documentationUrl } from "../../util/documentation-url";
import { showAlertDialog } from "../generic/show-dialog-box";
import { VoiceCommandDialogParams } from "./show-ha-voice-command-dialog";
interface Message {
who: string;
@ -82,7 +83,13 @@ export class HaVoiceCommandDialog extends LitElement {
private _stt_binary_handler_id?: number | null;
public async showDialog(): Promise<void> {
private _pipelinePromise?: Promise<AssistPipeline>;
public async showDialog(params?: VoiceCommandDialogParams): Promise<void> {
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<void> {
@ -230,7 +242,7 @@ export class HaVoiceCommandDialog extends LitElement {
<div class="listening-icon">
<ha-icon-button
.path=${mdiMicrophone}
@click=${this._toggleListening}
@click=${this._handleListeningButton}
.label=${this.hass.localize(
"ui.dialogs.voice_command.start_listening"
)}
@ -275,7 +287,8 @@ export class HaVoiceCommandDialog extends LitElement {
private async _getPipeline() {
try {
this._pipeline = await getAssistPipeline(this.hass, this._pipelineId);
this._pipelinePromise = getAssistPipeline(this.hass, this._pipelineId);
this._pipeline = await this._pipelinePromise;
} catch (e: any) {
if (e.code === "not_found") {
this._pipelineId = undefined;
@ -392,9 +405,13 @@ export class HaVoiceCommandDialog extends LitElement {
}
}
private _toggleListening(ev) {
private _handleListeningButton(ev) {
ev.stopPropagation();
ev.preventDefault();
this._toggleListening();
}
private _toggleListening() {
const supportsMicrophone = AudioRecorder.isSupported;
if (!supportsMicrophone) {
this._showNotSupportedMessage();

View File

@ -3,19 +3,29 @@ import { HomeAssistant } from "../../types";
const loadVoiceCommandDialog = () => 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,
});
};

View File

@ -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;

View File

@ -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();

View File

@ -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 {
</h3>
<slot name="icons" slot="icons"></slot>
${type !== "condition" &&
(this.action as NonConditionAction).continue_on_error === true
? html`<div slot="icons">
<ha-svg-icon .path=${mdiAlertCircleCheck}></ha-svg-icon>
<simple-tooltip animation-delay="0">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.continue_on_error"
)}
</simple-tooltip>
</div> `
: nothing}
${this.hideMenu
? ""
: html`

View File

@ -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",

View File

@ -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`<ha-select
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.trigger.id"
)}
.value=${id}
.disabled=${this.disabled}
@selected=${this._triggerPicked}
>
${this._triggers.map(
(trigger) =>
html`
<mwc-list-item .value=${trigger.id}> ${trigger.id} </mwc-list-item>
`
)}
</ha-select>`;
const schema = this._schema(this._triggers);
return html`
<ha-form
.schema=${schema}
.data=${this.condition}
.hass=${this.hass}
.disabled=${this.disabled}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): 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 });
}
}

View File

@ -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<BlueprintSourceType, string> = {
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`)
)}
>
<mwc-list
innerRole="listbox"
itemRoles="option"
innerAriaLabel=${this.hass.localize(
"ui.panel.config.automation.dialog_new.header"
`ui.panel.config.${this._mode}.dialog_new.header`
)}
rootTabbable
dialogInitialFocus
@ -112,11 +119,11 @@ class DialogNewAutomation extends LitElement implements HassDialog {
>
<ha-svg-icon slot="graphic" .path=${mdiPencilOutline}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.automation.dialog_new.create_empty"
`ui.panel.config.${this._mode}.dialog_new.create_empty`
)}
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.automation.dialog_new.create_empty_description"
`ui.panel.config.${this._mode}.dialog_new.create_empty_description`
)}
</span>
<ha-icon-next slot="meta"></ha-icon-next>
@ -139,11 +146,11 @@ class DialogNewAutomation extends LitElement implements HassDialog {
<span slot="secondary">
${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}`
)}
</span>
<ha-icon-next slot="meta"></ha-icon-next>
@ -161,11 +168,11 @@ class DialogNewAutomation extends LitElement implements HassDialog {
<ha-list-item hasmeta twoline graphic="icon">
<ha-svg-icon slot="graphic" .path=${mdiWeb}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.automation.dialog_new.create_blueprint"
`ui.panel.config.${this._mode}.dialog_new.create_blueprint`
)}
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.automation.dialog_new.create_blueprint_description"
`ui.panel.config.${this._mode}.dialog_new.create_blueprint_description`
)}
</span>
<ha-svg-icon slot="meta" path=${mdiOpenInNew}></ha-svg-icon>
@ -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`
)}
</a>
</ha-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 {

View File

@ -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");
}

View File

@ -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,
});
};

View File

@ -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) {
</ha-card>
</div>
${"use_blueprint" in this._config
${useBlueprint
? html`
<blueprint-script-editor
.hass=${this.hass}

View File

@ -12,6 +12,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { differenceInDays } from "date-fns/esm";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
import { relativeTime } from "../../../common/datetime/relative_time";
import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
@ -44,6 +45,7 @@ import { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast";
import { configSections } from "../ha-panel-config";
import { showNewAutomationDialog } from "../automation/show-dialog-new-automation";
import { EntityRegistryEntry } from "../../../data/entity_registry";
import { findRelated } from "../../../data/search";
import { fetchBlueprints } from "../../../data/blueprint";
@ -242,19 +244,19 @@ class HaScriptPicker extends LitElement {
@related-changed=${this._relatedFilterChanged}
>
</ha-button-related-filter-menu>
<a href="/config/script/edit/new" slot="fab">
<ha-fab
?is-wide=${this.isWide}
?narrow=${this.narrow}
.label=${this.hass.localize(
"ui.panel.config.script.picker.add_script"
)}
extended
?rtl=${computeRTL(this.hass)}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</a>
<ha-fab
slot="fab"
?is-wide=${this.isWide}
?narrow=${this.narrow}
.label=${this.hass.localize(
"ui.panel.config.script.picker.add_script"
)}
extended
?rtl=${computeRTL(this.hass)}
@click=${this._createNew}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</hass-tabs-subpage-data-table>
`;
}
@ -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

View File

@ -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}
>
<ha-dialog-header slot="heading">
<span slot="title"
>${this._existing
? this.hass.localize(
"ui.panel.config.storage.network_mounts.update_title"
)
: this.hass.localize(
"ui.panel.config.storage.network_mounts.add_title"
)}
</span>
<a
slot="actionItems"
class="header_button"
href=${documentationUrl(
this.hass,
"/common-tasks/os#network-storage"
)}
title=${this.hass.localize(
"ui.panel.config.storage.network_mounts.documentation"
)}
target="_blank"
rel="noreferrer"
dir=${computeRTLDirection(this.hass)}
>
<ha-icon-button .path=${mdiHelpCircle}></ha-icon-button>
</a>
</ha-dialog-header>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: 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);
}

View File

@ -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 {

View File

@ -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

View File

@ -190,6 +190,7 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
<state-history-charts
.hass=${this.hass}
.historyData=${this._stateHistory}
.startTime=${this._startDate}
.endTime=${this._endDate}
>
</state-history-charts>

View File

@ -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`
<ha-card>
${value !== undefined
? html`
<ha-svg-icon id="info" .path=${mdiInformation}></ha-svg-icon>
<simple-tooltip animation-delay="0" for="info" position="left">
<span>
${this.hass.localize(
"ui.panel.lovelace.cards.energy.self_sufficiency_gauge.card_indicates_self_sufficiency_quota"
)}
</span>
</simple-tooltip>
<ha-gauge
min="0"
max="100"
.value=${value}
.locale=${this.hass.locale}
label="%"
style=${styleMap({
"--gauge-color": this._computeSeverity(value),
})}
></ha-gauge>
<div class="name">
${this.hass.localize(
"ui.panel.lovelace.cards.energy.self_sufficiency_gauge.self_sufficiency_quota"
)}
</div>
`
: this.hass.localize(
"ui.panel.lovelace.cards.energy.self_sufficiency_gauge.self_sufficiency_could_not_calc"
)}
</ha-card>
`;
}
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;
}
}

View File

@ -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,

View File

@ -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}

View File

@ -2,6 +2,7 @@ import {
mdiClockOutline,
mdiFan,
mdiFire,
mdiHeatWave,
mdiPower,
mdiSnowflake,
mdiWaterPercent,
@ -21,6 +22,7 @@ export const CLIMATE_HVAC_ACTION_ICONS: Record<HvacAction, string> = {
heating: mdiFire,
idle: mdiClockOutline,
off: mdiPower,
preheating: mdiHeatWave,
};
export const CLIMATE_HVAC_ACTION_MODE: Record<HvacAction, HvacMode> = {
@ -30,6 +32,7 @@ export const CLIMATE_HVAC_ACTION_MODE: Record<HvacAction, HvacMode> = {
heating: "heat",
idle: "off",
off: "off",
preheating: "heat",
};
export const computeClimateBadge: ComputeBadgeFunction = (stateObj) => {

View File

@ -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;

View File

@ -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);
}

View File

@ -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`
<ha-help-tooltip .label=${this.tooltipText}></ha-help-tooltip>
`
: ""}
: nothing}
</div>
${this.config?.action === "navigate"
? html`
@ -114,7 +138,7 @@ export class HuiActionEditor extends LitElement {
@value-changed=${this._navigateValueChanged}
></ha-navigation-picker>
`
: ""}
: nothing}
${this.config?.action === "url"
? html`
<ha-textfield
@ -126,7 +150,7 @@ export class HuiActionEditor extends LitElement {
@input=${this._valueChanged}
></ha-textfield>
`
: ""}
: nothing}
${this.config?.action === "call-service"
? html`
<ha-service-control
@ -137,7 +161,19 @@ export class HuiActionEditor extends LitElement {
@value-changed=${this._serviceValueChanged}
></ha-service-control>
`
: ""}
: nothing}
${this.config?.action === "assist"
? html`
<ha-form
.hass=${this.hass}
.schema=${ASSIST_SCHEMA}
.data=${this.config}
.computeLabel=${this._computeFormLabel}
@value-changed=${this._formValueChanged}
>
</ha-form>
`
: 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<typeof ASSIST_SCHEMA>) {
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;
}
`;
}
}

View File

@ -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": () =>

View File

@ -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<any>((value) => {
case "url": {
return actionConfigStructUrl;
}
case "assist": {
return actionConfigStructAssist;
}
}
}

View File

@ -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);

View File

@ -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
></ha-textfield>
>
<ha-icon-button
@click=${this._copyToken}
slot="trailingIcon"
.path=${mdiContentCopy}
></ha-icon-button>
</ha-textfield>
<div id="qr">
${this._qrCode
? this._qrCode
@ -71,6 +83,14 @@ export class HaLongLivedAccessTokenDialog extends LitElement {
`;
}
private async _copyToken(ev): Promise<void> {
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);
}
`,
];

View File

@ -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;
}

83
src/translations/en.json Executable file → Normal file
View File

@ -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"
}

434
yarn.lock
View File

@ -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<compat/typescript>":
version: 4.9.5
resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin<compat/typescript>::version=4.9.5&hash=289587"
"typescript@patch:typescript@5.1.3#~builtin<compat/typescript>":
version: 5.1.3
resolution: "typescript@patch:typescript@npm%3A5.1.3#~builtin<compat/typescript>::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<compat/typescript>":
version: 3.9.10
resolution: "typescript@patch:typescript@npm%3A3.9.10#~builtin<compat/typescript>::version=3.9.10&hash=3bd3d3"
"typescript@patch:typescript@~4.4.3#~builtin<compat/typescript>":
version: 4.4.4
resolution: "typescript@patch:typescript@npm%3A4.4.4#~builtin<compat/typescript>::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