Compare commits

..

31 Commits

Author SHA1 Message Date
Zack
79593ce937 Few changes 2022-02-21 20:51:50 -06:00
Zack
a0d27a5a83 comment 2022-02-21 20:49:59 -06:00
Zack
1c4f6dc3f1 Way to dismiss 2022-02-21 20:47:41 -06:00
Zack
7d9a60973c Newsletter Addition 2022-02-21 17:31:43 -06:00
Bram Kragten
b1f369a355 Paper input migrations (#11766) 2022-02-21 23:09:13 +01:00
Bram Kragten
e6d1e86c64 set theme to undefined when no theme (#11765) 2022-02-21 15:56:10 -06:00
Allen Porter
eb1f94c370 Fix WebRTC player stream playback when disconnected/connected (#11764) 2022-02-21 20:35:37 +00:00
Zack Barett
27750b8b5d Area Card Editor to Ha Form (#11762) 2022-02-21 13:21:21 -06:00
J. Nick Koston
564a725284 Allow config entries to be reloaded when they are in setup_retry state (#11759) 2022-02-21 10:52:59 -08:00
Bram Kragten
a5ee610af5 Fix zwave migration (#11751) 2022-02-21 10:52:09 -08:00
Joakim Sørensen
eaf97ee7f5 Show Home Assistant when creating partial backup (#11758) 2022-02-21 09:33:02 -08:00
Paulus Schoutsen
a14d75deec Add support for the media browser My link (#11757) 2022-02-21 11:21:29 -06:00
Paulus Schoutsen
72b5721c88 Radio Browser is now added during onboarding (#11756) 2022-02-21 09:12:15 -08:00
Bram Kragten
94b4b818aa Convert date-range-picker to mwc (#11755) 2022-02-21 16:48:31 +00:00
Bram Kragten
98699b640a Selector: remove text value when not required and empty (#11754) 2022-02-21 16:37:29 +00:00
Zack Barett
decc0d3e0d Convert Automation Actions to mwc/ha-form + other automation items (#11753) 2022-02-21 16:37:11 +00:00
Steve Repsher
2281f5bafa Set initial focus for supervisor dialogs (#11710) 2022-02-21 17:02:55 +01:00
Zack Barett
6cac7eeff0 Lovelace Entity Card Editor to Ha Form - Adds Theme Selector and HaFormColumn (#11731)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-21 16:53:03 +01:00
Erik Montnemery
794bc161c8 Merge pull request #11716 from emontnemery/remove_config_entry_from_device
Add support for removing config entry from a device
2022-02-21 12:36:07 +00:00
Paulus Schoutsen
28cd9b6408 Show when media is being loaded (#11750) 2022-02-21 09:55:01 +01:00
Paulus Schoutsen
9b4c6eea63 Handle inifinity media duration (#11749) 2022-02-21 04:07:10 +00:00
Erik
bc6ef7780c Remove useless Array.isArray check 2022-02-18 16:49:23 +01:00
Erik
b29563a254 Prettier 2022-02-18 16:41:18 +01:00
Erik
fe8a1152c4 Correct typing 2022-02-18 16:36:15 +01:00
Erik Montnemery
26689a0a85 Update src/panels/config/devices/ha-config-device-page.ts
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-18 16:21:11 +01:00
Erik Montnemery
4f6a241817 Apply suggestions from code review
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-18 15:57:57 +01:00
Erik
246724c59e Prettier 2022-02-18 09:13:18 +01:00
Erik
8f5c9295d3 Tweak 2022-02-18 08:48:53 +01:00
Erik
0abafff4c9 Fix lint error 2022-02-18 08:26:37 +01:00
Erik
f88ce269a7 Tweak 2022-02-17 17:12:17 +01:00
Erik
0dc56d7983 Add support for removing config entry from a device 2022-02-17 16:46:08 +01:00
64 changed files with 1064 additions and 715 deletions

View File

@@ -29,6 +29,7 @@ const createConfigEntry = (
source: "zeroconf", source: "zeroconf",
state: "loaded", state: "loaded",
supports_options: false, supports_options: false,
supports_remove_device: false,
supports_unload: true, supports_unload: true,
disabled_by: null, disabled_by: null,
pref_disable_new_entities: false, pref_disable_new_entities: false,

View File

@@ -1,7 +1,7 @@
import { mdiFolder, mdiHomeAssistant, mdiPuzzle } from "@mdi/js"; import { mdiFolder, mdiHomeAssistant, mdiPuzzle } from "@mdi/js";
import { PaperInputElement } from "@polymer/paper-input/paper-input"; import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version"; import { atLeastVersion } from "../../../src/common/config/version";
import { formatDate } from "../../../src/common/datetime/format_date"; import { formatDate } from "../../../src/common/datetime/format_date";
import { formatDateTime } from "../../../src/common/datetime/format_date_time"; import { formatDateTime } from "../../../src/common/datetime/format_date_time";
@@ -92,6 +92,8 @@ export class SupervisorBackupContent extends LitElement {
@property() public confirmBackupPassword = ""; @property() public confirmBackupPassword = "";
@query("paper-input, ha-radio, ha-checkbox", true) private _focusTarget;
public willUpdate(changedProps) { public willUpdate(changedProps) {
super.willUpdate(changedProps); super.willUpdate(changedProps);
if (!this.hasUpdated) { if (!this.hasUpdated) {
@@ -109,6 +111,10 @@ export class SupervisorBackupContent extends LitElement {
} }
} }
public override focus() {
this._focusTarget?.focus();
}
private _localize = (string: string) => private _localize = (string: string) =>
this.supervisor?.localize(`backup.${string}`) || this.supervisor?.localize(`backup.${string}`) ||
this.localize!(`ui.panel.page-onboarding.restore.${string}`); this.localize!(`ui.panel.page-onboarding.restore.${string}`);
@@ -169,13 +175,13 @@ export class SupervisorBackupContent extends LitElement {
: ""} : ""}
${this.backupType === "partial" ${this.backupType === "partial"
? html`<div class="partial-picker"> ? html`<div class="partial-picker">
${this.backup && this.backup.homeassistant
? html`
<ha-formfield <ha-formfield
.label=${html`<supervisor-formfield-label .label=${html`<supervisor-formfield-label
label="Home Assistant" label="Home Assistant"
.iconPath=${mdiHomeAssistant} .iconPath=${mdiHomeAssistant}
.version=${this.backup.homeassistant} .version=${this.backup
? this.backup.homeassistant
: this.hass.config.version}
> >
</supervisor-formfield-label>`} </supervisor-formfield-label>`}
> >
@@ -185,8 +191,7 @@ export class SupervisorBackupContent extends LitElement {
> >
</ha-checkbox> </ha-checkbox>
</ha-formfield> </ha-formfield>
`
: ""}
${foldersSection?.templates.length ${foldersSection?.templates.length
? html` ? html`
<ha-formfield <ha-formfield

View File

@@ -64,6 +64,7 @@ export class DialogHassioBackupUpload
.path=${mdiClose} .path=${mdiClose}
slot="actionItems" slot="actionItems"
dialogAction="cancel" dialogAction="cancel"
dialogInitialFocus
></ha-icon-button> ></ha-icon-button>
</ha-header-bar> </ha-header-bar>
</div> </div>

View File

@@ -92,6 +92,7 @@ class HassioBackupDialog
.backup=${this._backup} .backup=${this._backup}
.onboarding=${this._dialogParams.onboarding || false} .onboarding=${this._dialogParams.onboarding || false}
.localize=${this._dialogParams.localize} .localize=${this._dialogParams.localize}
dialogInitialFocus
> >
</supervisor-backup-content>`} </supervisor-backup-content>`}
${this._error ${this._error

View File

@@ -61,6 +61,7 @@ class HassioCreateBackupDialog extends LitElement {
: html`<supervisor-backup-content : html`<supervisor-backup-content
.hass=${this.hass} .hass=${this.hass}
.supervisor=${this._dialogParams.supervisor} .supervisor=${this._dialogParams.supervisor}
dialogInitialFocus
> >
</supervisor-backup-content>`} </supervisor-backup-content>`}
${this._error ${this._error

View File

@@ -94,6 +94,7 @@ class HassioDatadiskDialog extends LitElement {
"dialog.datadisk_move.select_device" "dialog.datadisk_move.select_device"
)} )}
@selected=${this._select_device} @selected=${this._select_device}
dialogInitialFocus
> >
${this.devices.map( ${this.devices.map(
(device) => (device) =>
@@ -111,7 +112,11 @@ class HassioDatadiskDialog extends LitElement {
"dialog.datadisk_move.no_devices" "dialog.datadisk_move.no_devices"
)} )}
<mwc-button slot="secondaryAction" @click=${this.closeDialog}> <mwc-button
slot="secondaryAction"
@click=${this.closeDialog}
dialogInitialFocus
>
${this.dialogParams.supervisor.localize( ${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.cancel" "dialog.datadisk_move.cancel"
)} )}

View File

@@ -80,7 +80,7 @@ class HassioHardwareDialog extends LitElement {
></ha-icon-button> ></ha-icon-button>
<search-input <search-input
.hass=${this.hass} .hass=${this.hass}
autofocus dialogInitialFocus
no-label-float no-label-float
.filter=${this._filter} .filter=${this._filter}
@value-changed=${this._handleSearchChange} @value-changed=${this._handleSearchChange}

View File

@@ -37,7 +37,10 @@ class HassioMarkdownDialog extends LitElement {
@closed=${this.closeDialog} @closed=${this.closeDialog}
.heading=${createCloseHeading(this.hass, this.title)} .heading=${createCloseHeading(this.hass, this.title)}
> >
<ha-markdown .content=${this.content || ""}></ha-markdown> <ha-markdown
.content=${this.content || ""}
dialogInitialFocus
></ha-markdown>
</ha-dialog> </ha-dialog>
`; `;
} }

View File

@@ -119,6 +119,7 @@ export class DialogHassioNetwork
html`<mwc-tab html`<mwc-tab
.id=${device.interface} .id=${device.interface}
.label=${device.interface} .label=${device.interface}
dialogInitialFocus
> >
</mwc-tab>` </mwc-tab>`
)} )}
@@ -315,6 +316,7 @@ export class DialogHassioNetwork
value="auto" value="auto"
name="${version}method" name="${version}method"
.checked=${this._interface![version]?.method === "auto"} .checked=${this._interface![version]?.method === "auto"}
dialogInitialFocus
> >
</ha-radio> </ha-radio>
</ha-formfield> </ha-formfield>

View File

@@ -80,6 +80,7 @@ class HassioRegistriesDialog extends LitElement {
.schema=${SCHEMA} .schema=${SCHEMA}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.computeLabel=${this._computeLabel} .computeLabel=${this._computeLabel}
dialogInitialFocus
></ha-form> ></ha-form>
<div class="action"> <div class="action">
<mwc-button <mwc-button
@@ -124,7 +125,7 @@ class HassioRegistriesDialog extends LitElement {
</ha-alert> </ha-alert>
`} `}
<div class="action"> <div class="action">
<mwc-button @click=${this._addRegistry}> <mwc-button @click=${this._addRegistry} dialogInitialFocus>
${this.supervisor.localize( ${this.supervisor.localize(
"dialog.registries.add_new_registry" "dialog.registries.add_new_registry"
)} )}

View File

@@ -139,6 +139,7 @@ class HassioRepositoriesDialog extends LitElement {
"dialog.repositories.add" "dialog.repositories.add"
)} )}
@keydown=${this._handleKeyAdd} @keydown=${this._handleKeyAdd}
dialogInitialFocus
></paper-input> ></paper-input>
<mwc-button @click=${this._addRepository}> <mwc-button @click=${this._addRepository}>
${this._processing ${this._processing

View File

@@ -3,7 +3,6 @@ import "@material/mwc-list/mwc-list";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiCalendar } from "@mdi/js"; import { mdiCalendar } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -19,6 +18,7 @@ import { computeRTLDirection } from "../common/util/compute_rtl";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./date-range-picker"; import "./date-range-picker";
import "./ha-svg-icon"; import "./ha-svg-icon";
import "./ha-textfield";
export interface DateRangePickerRanges { export interface DateRangePickerRanges {
[key: string]: [Date, Date]; [key: string]: [Date, Date];
@@ -61,7 +61,7 @@ export class HaDateRangePicker extends LitElement {
> >
<div slot="input" class="date-range-inputs"> <div slot="input" class="date-range-inputs">
<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon> <ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
<paper-input <ha-textfield
.value=${formatDateTime(this.startDate, this.hass.locale)} .value=${formatDateTime(this.startDate, this.hass.locale)}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.date-range-picker.start_date" "ui.components.date-range-picker.start_date"
@@ -69,16 +69,16 @@ export class HaDateRangePicker extends LitElement {
.disabled=${this.disabled} .disabled=${this.disabled}
@click=${this._handleInputClick} @click=${this._handleInputClick}
readonly readonly
></paper-input> ></ha-textfield>
<paper-input <ha-textfield
.value=${formatDateTime(this.endDate, this.hass.locale)} .value=${formatDateTime(this.endDate, this.hass.locale)}
label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.date-range-picker.end_date" "ui.components.date-range-picker.end_date"
)} )}
.disabled=${this.disabled} .disabled=${this.disabled}
@click=${this._handleInputClick} @click=${this._handleInputClick}
readonly readonly
></paper-input> ></ha-textfield>
</div> </div>
${this.ranges ${this.ranges
? html`<div ? html`<div
@@ -158,13 +158,13 @@ export class HaDateRangePicker extends LitElement {
border-top: 1px solid var(--divider-color); border-top: 1px solid var(--divider-color);
} }
paper-input { ha-textfield {
display: inline-block; display: inline-block;
max-width: 250px; max-width: 250px;
min-width: 200px; min-width: 200px;
} }
paper-input:last-child { ha-textfield:last-child {
margin-left: 8px; margin-left: 8px;
} }
@@ -176,7 +176,7 @@ export class HaDateRangePicker extends LitElement {
} }
@media only screen and (max-width: 500px) { @media only screen and (max-width: 500px) {
paper-input { ha-textfield {
min-width: inherit; min-width: inherit;
} }

View File

@@ -0,0 +1,73 @@
import "./ha-form";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import type {
HaFormGridSchema,
HaFormDataContainer,
HaFormElement,
HaFormSchema,
} from "./types";
import type { HomeAssistant } from "../../types";
@customElement("ha-form-grid")
export class HaFormGrid extends LitElement implements HaFormElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public data!: HaFormDataContainer;
@property({ attribute: false }) public schema!: HaFormGridSchema;
@property({ type: Boolean }) public disabled = false;
@property() public computeLabel?: (
schema: HaFormSchema,
data?: HaFormDataContainer
) => string;
@property() public computeHelper?: (schema: HaFormSchema) => string;
protected firstUpdated() {
this.setAttribute("own-margin", "");
}
protected render(): TemplateResult {
return html`
${this.schema.schema.map(
(item) =>
html`
<ha-form
.hass=${this.hass}
.data=${this.data}
.schema=${[item]}
.disabled=${this.disabled}
.computeLabel=${this.computeLabel}
.computeHelper=${this.computeHelper}
></ha-form>
`
)}
`;
}
static get styles(): CSSResultGroup {
return css`
:host {
display: grid !important;
grid-template-columns: repeat(
var(--form-grid-column-count, auto-fit),
minmax(var(--form-grid-min-width, 200px), 1fr)
);
grid-gap: 8px;
}
:host > ha-form {
display: block;
margin-bottom: 24px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-form-grid": HaFormGrid;
}
}

View File

@@ -1,10 +1,18 @@
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { dynamicElement } from "../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import "../ha-alert"; import "../ha-alert";
import "./ha-form-boolean"; import "./ha-form-boolean";
import "./ha-form-constant"; import "./ha-form-constant";
import "./ha-form-grid";
import "./ha-form-float"; import "./ha-form-float";
import "./ha-form-integer"; import "./ha-form-integer";
import "./ha-form-multi_select"; import "./ha-form-multi_select";
@@ -14,17 +22,18 @@ import "./ha-form-string";
import { HaFormElement, HaFormDataContainer, HaFormSchema } from "./types"; import { HaFormElement, HaFormDataContainer, HaFormSchema } from "./types";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
const getValue = (obj, item) => (obj ? obj[item.name] : null); const getValue = (obj, item) =>
obj ? (!item.name ? obj : obj[item.name]) : null;
let selectorImported = false; let selectorImported = false;
@customElement("ha-form") @customElement("ha-form")
export class HaForm extends LitElement implements HaFormElement { export class HaForm extends LitElement implements HaFormElement {
@property() public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public data!: HaFormDataContainer; @property({ attribute: false }) public data!: HaFormDataContainer;
@property() public schema!: HaFormSchema[]; @property({ attribute: false }) public schema!: HaFormSchema[];
@property() public error?: Record<string, string>; @property() public error?: Record<string, string>;
@@ -64,7 +73,7 @@ export class HaForm extends LitElement implements HaFormElement {
} }
} }
protected render() { protected render(): TemplateResult {
return html` return html`
<div class="root"> <div class="root">
${this.error && this.error.base ${this.error && this.error.base
@@ -101,6 +110,9 @@ export class HaForm extends LitElement implements HaFormElement {
data: getValue(this.data, item), data: getValue(this.data, item),
label: this._computeLabel(item, this.data), label: this._computeLabel(item, this.data),
disabled: this.disabled, disabled: this.disabled,
hass: this.hass,
computeLabel: this.computeLabel,
computeHelper: this.computeHelper,
})} })}
`; `;
})} })}
@@ -115,8 +127,12 @@ export class HaForm extends LitElement implements HaFormElement {
ev.stopPropagation(); ev.stopPropagation();
const schema = (ev.target as HaFormElement).schema as HaFormSchema; const schema = (ev.target as HaFormElement).schema as HaFormSchema;
const newValue = !schema.name
? ev.detail.value
: { [schema.name]: ev.detail.value };
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { ...this.data, [schema.name]: ev.detail.value }, value: { ...this.data, ...newValue },
}); });
}); });
return root; return root;

View File

@@ -11,7 +11,8 @@ export type HaFormSchema =
| HaFormSelectSchema | HaFormSelectSchema
| HaFormMultiSelectSchema | HaFormMultiSelectSchema
| HaFormTimeSchema | HaFormTimeSchema
| HaFormSelector; | HaFormSelector
| HaFormGridSchema;
export interface HaFormBaseSchema { export interface HaFormBaseSchema {
name: string; name: string;
@@ -25,6 +26,12 @@ export interface HaFormBaseSchema {
}; };
} }
export interface HaFormGridSchema extends HaFormBaseSchema {
type: "grid";
name: "";
schema: HaFormSchema[];
}
export interface HaFormSelector extends HaFormBaseSchema { export interface HaFormSelector extends HaFormBaseSchema {
type?: never; type?: never;
selector: Selector; selector: Selector;

View File

@@ -0,0 +1,145 @@
import "./ha-icon-button";
import "./ha-circular-progress";
import "@material/mwc-button/mwc-button";
import "./ha-card";
import "./ha-textfield";
import { LitElement, TemplateResult, html, CSSResultGroup, css } from "lit";
import { customElement, property, query } from "lit/decorators";
import { mdiClose } from "@mdi/js";
import type { HaTextField } from "./ha-textfield";
import type { HomeAssistant } from "../types";
import { LocalStorage } from "../common/decorators/local-storage";
@customElement("ha-newsletter")
class HaNewsletter extends LitElement {
@property({ attribute: false }) hass!: HomeAssistant;
@query("ha-textfield")
private _emailField?: HaTextField;
@LocalStorage("dismissNewsletter", true)
private _dismissNewsletter = false;
private _requestStatus?: "inprogress" | "complete";
protected render(): TemplateResult {
if (this._dismissNewsletter) {
return html``;
}
return html`
<ha-card>
<div class="header">
${this.hass.localize("ui.newsletter.newsletter")}
<ha-icon-button
label="Dismiss"
.path=${mdiClose}
@click=${this._dismiss}
></ha-icon-button>
</div>
<div class="newsletter">
${this._requestStatus === "complete"
? html`<span>${this.hass.localize("ui.newsletter.thanks")}</span>`
: html`
<ha-textfield
required
type="email"
.label=${this.hass.localize("ui.newsletter.email")}
.validationMessage=${this.hass.localize(
"ui.newsletter.validation"
)}
></ha-textfield>
${this._requestStatus === "inprogress"
? html`
<ha-circular-progress
active
alt="Loading"
></ha-circular-progress>
`
: html`
<mwc-button
raised
.label=${this.hass.localize("ui.newsletter.subscribe")}
@click=${this._subscribe}
></mwc-button>
`}
`}
</div>
</ha-card>
`;
}
private _subscribe(): void {
if (!this._emailField?.reportValidity()) {
this._emailField!.focus();
return;
}
this._requestStatus = "inprogress";
fetch(
`https://newsletter.home-assistant.io/subscribe?email=${
this._emailField!.value
}`
)
.then(() => {
this._requestStatus = "complete";
setTimeout(this._dismiss, 2000);
})
.catch((err) => {
// Reset request so user can re-enter email
this._requestStatus = undefined;
// eslint-disable-next-line no-console
console.error(err);
});
}
private _dismiss(): void {
this._dismissNewsletter = true;
}
static get styles(): CSSResultGroup {
return css`
.newsletter {
display: flex;
flex-direction: row;
padding: 0 16px 16px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
color: var(--ha-card-header-color, --primary-text-color);
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em;
line-height: 48px;
padding: 12px 16px 16px;
margin-block-start: 0px;
margin-block-end: 0px;
font-weight: normal;
}
ha-textfield {
flex: 1;
display: block;
padding-right: 8px;
}
mwc-button {
padding-top: 12px;
}
ha-icon-button {
cursor: pointer;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-newsletter": HaNewsletter;
}
}

View File

@@ -35,9 +35,12 @@ export class HaBooleanSelector extends LitElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host {
height: 56px;
display: flex;
}
ha-formfield { ha-formfield {
width: 100%; width: 100%;
margin: 16px 0;
--mdc-typography-body2-font-size: 1em; --mdc-typography-body2-font-size: 1em;
} }
`; `;

View File

@@ -22,6 +22,8 @@ export class HaIconSelector extends LitElement {
<ha-icon-picker <ha-icon-picker
.label=${this.label} .label=${this.label}
.value=${this.value} .value=${this.value}
.fallbackPath=${this.selector.icon.fallbackPath}
.placeholder=${this.selector.icon.placeholder}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-icon-picker> ></ha-icon-picker>
`; `;

View File

@@ -69,10 +69,13 @@ export class HaTextSelector extends LitElement {
} }
private _handleChange(ev) { private _handleChange(ev) {
const value = ev.target.value; let value = ev.target.value;
if (this.value === value) { if (this.value === value) {
return; return;
} }
if (value === "" && !this.required) {
value = undefined;
}
fireEvent(this, "value-changed", { value }); fireEvent(this, "value-changed", { value });
} }

View File

@@ -0,0 +1,34 @@
import "../../panels/lovelace/components/hui-theme-select-editor";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import type { ThemeSelector } from "../../data/selector";
@customElement("ha-selector-theme")
export class HaThemeSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: ThemeSelector;
@property() public value?: string;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
protected render() {
return html`
<hui-theme-select-editor
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
></hui-theme-select-editor>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-theme": HaThemeSelector;
}
}

View File

@@ -1,8 +1,8 @@
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { dynamicElement } from "../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { Selector } from "../../data/selector"; import type { Selector } from "../../data/selector";
import { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "./ha-selector-action"; import "./ha-selector-action";
import "./ha-selector-addon"; import "./ha-selector-addon";
import "./ha-selector-area"; import "./ha-selector-area";
@@ -19,6 +19,7 @@ import "./ha-selector-text";
import "./ha-selector-time"; import "./ha-selector-time";
import "./ha-selector-icon"; import "./ha-selector-icon";
import "./ha-selector-media"; import "./ha-selector-media";
import "./ha-selector-theme";
@customElement("ha-selector") @customElement("ha-selector")
export class HaSelector extends LitElement { export class HaSelector extends LitElement {

View File

@@ -43,7 +43,7 @@ class HaWebRtcPlayer extends LitElement {
private _remoteStream?: MediaStream; private _remoteStream?: MediaStream;
protected render(): TemplateResult { protected override render(): TemplateResult {
if (this._error) { if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`; return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
} }
@@ -58,12 +58,19 @@ class HaWebRtcPlayer extends LitElement {
`; `;
} }
public disconnectedCallback() { public override connectedCallback() {
super.connectedCallback();
if (this.hasUpdated) {
this._startWebRtc();
}
}
public override disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
this._cleanUp(); this._cleanUp();
} }
protected updated(changedProperties: PropertyValues<this>) { protected override updated(changedProperties: PropertyValues<this>) {
if (!changedProperties.has("entityid")) { if (!changedProperties.has("entityid")) {
return; return;
} }

View File

@@ -13,6 +13,7 @@ export interface ConfigEntry {
| "not_loaded" | "not_loaded"
| "failed_unload"; | "failed_unload";
supports_options: boolean; supports_options: boolean;
supports_remove_device: boolean;
supports_unload: boolean; supports_unload: boolean;
pref_disable_new_entities: boolean; pref_disable_new_entities: boolean;
pref_disable_polling: boolean; pref_disable_polling: boolean;

View File

@@ -1,4 +1,5 @@
import { Connection, createCollection } from "home-assistant-js-websocket"; import { Connection, createCollection } from "home-assistant-js-websocket";
import { Store } from "home-assistant-js-websocket/dist/store";
import { computeStateName } from "../common/entity/compute_state_name"; import { computeStateName } from "../common/entity/compute_state_name";
import { caseInsensitiveStringCompare } from "../common/string/compare"; import { caseInsensitiveStringCompare } from "../common/string/compare";
import { debounce } from "../common/util/debounce"; import { debounce } from "../common/util/debounce";
@@ -77,12 +78,26 @@ export const updateDeviceRegistryEntry = (
...updates, ...updates,
}); });
export const fetchDeviceRegistry = (conn) => export const removeConfigEntryFromDevice = (
conn.sendMessagePromise({ hass: HomeAssistant,
deviceId: string,
configEntryId: string
) =>
hass.callWS<DeviceRegistryEntry>({
type: "config/device_registry/remove_config_entry",
device_id: deviceId,
config_entry_id: configEntryId,
});
export const fetchDeviceRegistry = (conn: Connection) =>
conn.sendMessagePromise<DeviceRegistryEntry[]>({
type: "config/device_registry/list", type: "config/device_registry/list",
}); });
const subscribeDeviceRegistryUpdates = (conn, store) => const subscribeDeviceRegistryUpdates = (
conn: Connection,
store: Store<DeviceRegistryEntry[]>
) =>
conn.subscribeEvents( conn.subscribeEvents(
debounce( debounce(
() => () =>

View File

@@ -33,7 +33,8 @@ import type { HomeAssistant } from "../types";
import { UNAVAILABLE_STATES } from "./entity"; import { UNAVAILABLE_STATES } from "./entity";
interface MediaPlayerEntityAttributes extends HassEntityAttributeBase { interface MediaPlayerEntityAttributes extends HassEntityAttributeBase {
media_content_type?: any; media_content_id?: string;
media_content_type?: string;
media_artist?: string; media_artist?: string;
media_playlist?: string; media_playlist?: string;
media_series_title?: string; media_series_title?: string;
@@ -339,7 +340,7 @@ export const computeMediaControls = (
}; };
export const formatMediaTime = (seconds: number | undefined): string => { export const formatMediaTime = (seconds: number | undefined): string => {
if (seconds === undefined) { if (seconds === undefined || seconds === Infinity) {
return ""; return "";
} }

View File

@@ -14,7 +14,8 @@ export type Selector =
| ObjectSelector | ObjectSelector
| SelectSelector | SelectSelector
| IconSelector | IconSelector
| MediaSelector; | MediaSelector
| ThemeSelector;
export interface EntitySelector { export interface EntitySelector {
entity: { entity: {
@@ -147,8 +148,15 @@ export interface SelectSelector {
} }
export interface IconSelector { export interface IconSelector {
icon: {
placeholder?: string;
fallbackPath?: string;
};
}
export interface ThemeSelector {
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
icon: {}; theme: {};
} }
export interface MediaSelector { export interface MediaSelector {

View File

@@ -1,12 +1,11 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-dialog"; import "../../components/ha-dialog";
import "../../components/ha-switch"; import "../../components/ha-switch";
import { PolymerChangedEvent } from "../../polymer-types"; import "../../components/ha-textfield";
import { haStyleDialog } from "../../resources/styles"; import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { DialogBoxParams } from "./show-dialog-box"; import { DialogBoxParams } from "./show-dialog-box";
@@ -71,18 +70,18 @@ class DialogBox extends LitElement {
: ""} : ""}
${this._params.prompt ${this._params.prompt
? html` ? html`
<paper-input <ha-textfield
dialogInitialFocus dialogInitialFocus
.value=${this._value} .value=${this._value}
@keyup=${this._handleKeyUp} @keyup=${this._handleKeyUp}
@value-changed=${this._valueChanged} @change=${this._valueChanged}
.label=${this._params.inputLabel .label=${this._params.inputLabel
? this._params.inputLabel ? this._params.inputLabel
: ""} : ""}
.type=${this._params.inputType .type=${this._params.inputType
? this._params.inputType ? this._params.inputType
: "text"} : "text"}
></paper-input> ></ha-textfield>
` `
: ""} : ""}
</div> </div>
@@ -107,8 +106,8 @@ class DialogBox extends LitElement {
`; `;
} }
private _valueChanged(ev: PolymerChangedEvent<string>) { private _valueChanged(ev) {
this._value = ev.detail.value; this._value = ev.target.value;
} }
private _dismiss(): void { private _dismiss(): void {

View File

@@ -10,7 +10,6 @@ import {
mdiVolumeOff, mdiVolumeOff,
mdiVolumePlus, mdiVolumePlus,
} from "@mdi/js"; } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";

View File

@@ -30,7 +30,7 @@ import { HomeAssistant } from "../types";
import "./action-badge"; import "./action-badge";
import "./integration-badge"; import "./integration-badge";
const HIDDEN_DOMAINS = new Set(["met", "rpi_power", "hassio"]); const HIDDEN_DOMAINS = new Set(["hassio", "met", "radio_browser", "rpi_power"]);
@customElement("onboarding-integrations") @customElement("onboarding-integrations")
class OnboardingIntegrations extends LitElement { class OnboardingIntegrations extends LitElement {

View File

@@ -73,7 +73,7 @@ export const handleChangeEvent = (element: ActionElement, ev: CustomEvent) => {
if (!name) { if (!name) {
return; return;
} }
const newVal = ev.detail.value; const newVal = ev.detail?.value || (ev.target as any).value;
if ((element.action[name] || "") === newVal) { if ((element.action[name] || "") === newVal) {
return; return;
@@ -376,7 +376,7 @@ export default class HaAutomationActionRow extends LitElement {
margin: 4px 0; margin: 4px 0;
} }
mwc-select { mwc-select {
margin-bottom: 16px; margin-bottom: 24px;
} }
`, `,
]; ];

View File

@@ -143,9 +143,12 @@ export class HaDeviceAction extends LitElement {
} }
static styles = css` static styles = css`
ha-device-picker {
display: block;
margin-bottom: 24px;
}
ha-device-action-picker { ha-device-action-picker {
display: block; display: block;
margin-top: 8px;
} }
`; `;
} }

View File

@@ -1,9 +1,9 @@
import "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/entity/ha-entity-picker"; import "../../../../../components/entity/ha-entity-picker";
import "../../../../../components/ha-service-picker"; import "../../../../../components/ha-service-picker";
import "../../../../../components/ha-textfield";
import "../../../../../components/ha-yaml-editor"; import "../../../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../../components/ha-yaml-editor"; import type { HaYamlEditor } from "../../../../../components/ha-yaml-editor";
import type { EventAction } from "../../../../../data/script"; import type { EventAction } from "../../../../../data/script";
@@ -40,14 +40,13 @@ export class HaEventAction extends LitElement implements ActionElement {
const { event, event_data } = this.action; const { event, event_data } = this.action;
return html` return html`
<paper-input <ha-textfield
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.event.event" "ui.panel.config.automation.editor.actions.type.event.event"
)} )}
name="event"
.value=${event} .value=${event}
@value-changed=${this._eventChanged} @change=${this._eventChanged}
></paper-input> ></ha-textfield>
<ha-yaml-editor <ha-yaml-editor
.hass=${this.hass} .hass=${this.hass}
.label=${this.hass.localize( .label=${this.hass.localize(
@@ -72,9 +71,17 @@ export class HaEventAction extends LitElement implements ActionElement {
private _eventChanged(ev: CustomEvent): void { private _eventChanged(ev: CustomEvent): void {
ev.stopPropagation(); ev.stopPropagation();
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { ...this.action, event: ev.detail.value }, value: { ...this.action, event: (ev.target as any).value },
}); });
} }
static get styles(): CSSResultGroup {
return css`
ha-textfield {
display: block;
}
`;
}
} }
declare global { declare global {

View File

@@ -1,4 +1,3 @@
import "@polymer/paper-input/paper-input";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";

View File

@@ -1,4 +1,3 @@
import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
@@ -10,10 +9,11 @@ import {
WhileRepeat, WhileRepeat,
} from "../../../../../data/script"; } from "../../../../../data/script";
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
import { Condition } from "../../../../lovelace/common/validate-condition"; import type { Condition } from "../../../../lovelace/common/validate-condition";
import "../ha-automation-action"; import "../ha-automation-action";
import { ActionElement } from "../ha-automation-action-row"; import "../../../../../components/ha-textfield";
import type { ActionElement } from "../ha-automation-action-row";
const OPTIONS = ["count", "while", "until"]; const OPTIONS = ["count", "while", "until"];
@@ -53,14 +53,16 @@ export class HaRepeatAction extends LitElement implements ActionElement {
)} )}
</mwc-select> </mwc-select>
${type === "count" ${type === "count"
? html`<paper-input ? html`
<ha-textfield
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.type.count.label" "ui.panel.config.automation.editor.actions.type.repeat.type.count.label"
)} )}
name="count" name="count"
.value=${(action as CountRepeat).count || "0"} .value=${(action as CountRepeat).count || "0"}
@value-changed=${this._countChanged} @change=${this._countChanged}
></paper-input>` ></ha-textfield>
`
: ""} : ""}
${type === "while" ${type === "while"
? html` <h3> ? html` <h3>
@@ -142,7 +144,7 @@ export class HaRepeatAction extends LitElement implements ActionElement {
} }
private _countChanged(ev: CustomEvent): void { private _countChanged(ev: CustomEvent): void {
const newVal = ev.detail.value; const newVal = (ev.target as any).value;
if ((this.action.repeat as CountRepeat).count === newVal) { if ((this.action.repeat as CountRepeat).count === newVal) {
return; return;
} }

View File

@@ -1,6 +1,5 @@
import "@polymer/paper-input/paper-input"; import "../../../../../components/ha-textfield";
import "@polymer/paper-input/paper-textarea"; import { css, CSSResultGroup, html, LitElement } from "lit";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-formfield"; import "../../../../../components/ha-formfield";
@@ -26,14 +25,14 @@ export class HaWaitForTriggerAction
const { wait_for_trigger, continue_on_timeout, timeout } = this.action; const { wait_for_trigger, continue_on_timeout, timeout } = this.action;
return html` return html`
<paper-input <ha-textfield
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.wait_for_trigger.timeout" "ui.panel.config.automation.editor.actions.type.wait_for_trigger.timeout"
)} )}
.name=${"timeout"} .name=${"timeout"}
.value=${timeout} .value=${timeout || ""}
@value-changed=${this._valueChanged} @change=${this._valueChanged}
></paper-input> ></ha-textfield>
<br /> <br />
<ha-formfield <ha-formfield
.label=${this.hass.localize( .label=${this.hass.localize(
@@ -63,6 +62,15 @@ export class HaWaitForTriggerAction
private _valueChanged(ev: CustomEvent): void { private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev); handleChangeEvent(this, ev);
} }
static get styles(): CSSResultGroup {
return css`
ha-textfield {
display: block;
margin-bottom: 24px;
}
`;
}
} }
declare global { declare global {

View File

@@ -1,66 +1,60 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-input/paper-textarea";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event"; import type { HaFormSchema } from "../../../../../components/ha-form/types";
import { WaitAction } from "../../../../../data/script"; import type { WaitAction } from "../../../../../data/script";
import { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
import { ActionElement, handleChangeEvent } from "../ha-automation-action-row"; import type { ActionElement } from "../ha-automation-action-row";
import "../../../../../components/ha-form/ha-form";
const SCHEMA: HaFormSchema[] = [
{
name: "wait_template",
selector: {
text: {
multiline: true,
},
},
},
{
name: "timeout",
required: false,
selector: {
text: {},
},
},
{
name: "continue_on_timeout",
selector: { boolean: {} },
},
];
@customElement("ha-automation-action-wait_template") @customElement("ha-automation-action-wait_template")
export class HaWaitAction extends LitElement implements ActionElement { export class HaWaitAction extends LitElement implements ActionElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public action!: WaitAction; @property({ attribute: false }) public action!: WaitAction;
public static get defaultConfig() { public static get defaultConfig() {
return { wait_template: "" }; return { wait_template: "" };
} }
protected render() { protected render() {
const { wait_template, timeout, continue_on_timeout } = this.action;
return html` return html`
<paper-textarea <ha-form
.label=${this.hass.localize( .hass=${this.hass}
"ui.panel.config.automation.editor.actions.type.wait_template.wait_template" .data=${this.action}
)} .schema=${SCHEMA}
name="wait_template" .computeLabel=${this._computeLabelCallback}
.value=${wait_template} ></ha-form>
@value-changed=${this._valueChanged}
dir="ltr"
></paper-textarea>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.wait_template.timeout"
)}
.name=${"timeout"}
.value=${timeout}
@value-changed=${this._valueChanged}
></paper-input>
<br />
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.wait_template.continue_timeout"
)}
>
<ha-switch
.checked=${continue_on_timeout}
@change=${this._continueChanged}
></ha-switch>
</ha-formfield>
`; `;
} }
private _continueChanged(ev) { private _computeLabelCallback = (schema: HaFormSchema): string =>
fireEvent(this, "value-changed", { this.hass.localize(
value: { ...this.action, continue_on_timeout: ev.target.checked }, `ui.panel.config.automation.editor.actions.type.wait_template.${
}); schema.name === "continue_on_timeout" ? "continue_timeout" : schema.name
} }`
);
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
} }
declare global { declare global {

View File

@@ -147,7 +147,7 @@ export default class HaAutomationConditionEditor extends LitElement {
haStyle, haStyle,
css` css`
mwc-select { mwc-select {
margin-bottom: 16px; margin-bottom: 24px;
} }
`, `,
]; ];

View File

@@ -9,6 +9,7 @@ import type { StateCondition } from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
import { forDictStruct } from "../../structs"; import { forDictStruct } from "../../structs";
import type { ConditionElement } from "../ha-automation-condition-row"; import type { ConditionElement } from "../ha-automation-condition-row";
import "../../../../../components/ha-form/ha-form";
const stateConditionStruct = object({ const stateConditionStruct = object({
condition: literal("state"), condition: literal("state"),

View File

@@ -7,6 +7,7 @@ import type { HomeAssistant } from "../../../../../types";
import type { ConditionElement } from "../ha-automation-condition-row"; import type { ConditionElement } from "../ha-automation-condition-row";
import type { LocalizeFunc } from "../../../../../common/translations/localize"; import type { LocalizeFunc } from "../../../../../common/translations/localize";
import type { HaFormSchema } from "../../../../../components/ha-form/types"; import type { HaFormSchema } from "../../../../../components/ha-form/types";
import "../../../../../components/ha-form/ha-form";
@customElement("ha-automation-condition-sun") @customElement("ha-automation-condition-sun")
export class HaSunCondition extends LitElement implements ConditionElement { export class HaSunCondition extends LitElement implements ConditionElement {

View File

@@ -7,6 +7,7 @@ import type { HomeAssistant } from "../../../../../types";
import type { ConditionElement } from "../ha-automation-condition-row"; import type { ConditionElement } from "../ha-automation-condition-row";
import type { LocalizeFunc } from "../../../../../common/translations/localize"; import type { LocalizeFunc } from "../../../../../common/translations/localize";
import type { HaFormSchema } from "../../../../../components/ha-form/types"; import type { HaFormSchema } from "../../../../../components/ha-form/types";
import "../../../../../components/ha-form/ha-form";
const DAYS = { const DAYS = {
mon: 1, mon: 1,

View File

@@ -9,7 +9,6 @@ import {
} from "@mdi/js"; } from "@mdi/js";
import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-input/paper-textarea";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { import {
css, css,
@@ -215,22 +214,26 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
${this._mode === "gui" ${this._mode === "gui"
? html` ? html`
${"use_blueprint" in this._config ${"use_blueprint" in this._config
? html`<blueprint-automation-editor ? html`
<blueprint-automation-editor
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.isWide=${this.isWide} .isWide=${this.isWide}
.stateObj=${stateObj} .stateObj=${stateObj}
.config=${this._config} .config=${this._config}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></blueprint-automation-editor>` ></blueprint-automation-editor>
: html`<manual-automation-editor `
: html`
<manual-automation-editor
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.isWide=${this.isWide} .isWide=${this.isWide}
.stateObj=${stateObj} .stateObj=${stateObj}
.config=${this._config} .config=${this._config}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></manual-automation-editor>`} ></manual-automation-editor>
`}
` `
: this._mode === "yaml" : this._mode === "yaml"
? html` ? html`

View File

@@ -7,7 +7,6 @@ import {
mdiPlayCircleOutline, mdiPlayCircleOutline,
mdiPlus, mdiPlus,
} from "@mdi/js"; } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";

View File

@@ -1,11 +1,12 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@polymer/paper-input/paper-textarea";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/entity/ha-entity-toggle"; import "../../../components/entity/ha-entity-toggle";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-textarea";
import "../../../components/ha-textfield";
import { import {
Condition, Condition,
ManualAutomationConfig, ManualAutomationConfig,
@@ -14,7 +15,7 @@ import {
} from "../../../data/automation"; } from "../../../data/automation";
import { Action, MODES, MODES_MAX } from "../../../data/script"; import { Action, MODES, MODES_MAX } from "../../../data/script";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
import "../ha-config-section"; import "../ha-config-section";
import "./action/ha-automation-action"; import "./action/ha-automation-action";
@@ -45,16 +46,16 @@ export class HaManualAutomationEditor extends LitElement {
</span> </span>
<ha-card> <ha-card>
<div class="card-content"> <div class="card-content">
<paper-input <ha-textfield
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.alias" "ui.panel.config.automation.editor.alias"
)} )}
name="alias" name="alias"
.value=${this.config.alias} .value=${this.config.alias || ""}
@value-changed=${this._valueChanged} @change=${this._valueChanged}
> >
</paper-input> </ha-textfield>
<paper-textarea <ha-textarea
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.description.label" "ui.panel.config.automation.editor.description.label"
)} )}
@@ -62,9 +63,9 @@ export class HaManualAutomationEditor extends LitElement {
"ui.panel.config.automation.editor.description.placeholder" "ui.panel.config.automation.editor.description.placeholder"
)} )}
name="description" name="description"
.value=${this.config.description} .value=${this.config.description || ""}
@value-changed=${this._valueChanged} @change=${this._valueChanged}
></paper-textarea> ></ha-textarea>
<p> <p>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.modes.description", "ui.panel.config.automation.editor.modes.description",
@@ -98,16 +99,18 @@ export class HaManualAutomationEditor extends LitElement {
)} )}
</mwc-select> </mwc-select>
${this.config.mode && MODES_MAX.includes(this.config.mode) ${this.config.mode && MODES_MAX.includes(this.config.mode)
? html`<paper-input ? html`
<ha-textfield
.label=${this.hass.localize( .label=${this.hass.localize(
`ui.panel.config.automation.editor.max.${this.config.mode}` `ui.panel.config.automation.editor.max.${this.config.mode}`
)} )}
type="number" type="number"
name="max" name="max"
.value=${this.config.max || "10"} .value=${this.config.max || "10"}
@value-changed=${this._valueChanged} @change=${this._valueChanged}
> >
</paper-input>` </ha-textfield>
`
: html``} : html``}
</div> </div>
${this.stateObj ${this.stateObj
@@ -243,7 +246,7 @@ export class HaManualAutomationEditor extends LitElement {
if (!name) { if (!name) {
return; return;
} }
let newVal = ev.detail.value; let newVal = target.value;
if (target.type === "number") { if (target.type === "number") {
newVal = Number(newVal); newVal = Number(newVal);
} }

View File

@@ -14,6 +14,7 @@ import { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-button-menu"; import "../../../../components/ha-button-menu";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-alert"; import "../../../../components/ha-alert";
import "../../../../components/ha-textfield";
import "../../../../components/ha-icon-button"; import "../../../../components/ha-icon-button";
import type { Trigger } from "../../../../data/automation"; import type { Trigger } from "../../../../data/automation";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
@@ -200,14 +201,14 @@ export default class HaAutomationTriggerRow extends LitElement {
${showId ${showId
? html` ? html`
<paper-input <ha-textfield
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.id" "ui.panel.config.automation.editor.triggers.id"
)} )}
.value=${this.trigger.id} .value=${this.trigger.id || ""}
@value-changed=${this._idChanged} @change=${this._idChanged}
> >
</paper-input> </ha-textfield>
` `
: ""} : ""}
<div @ui-mode-not-available=${this._handleUiModeNotAvailable}> <div @ui-mode-not-available=${this._handleUiModeNotAvailable}>
@@ -287,7 +288,7 @@ export default class HaAutomationTriggerRow extends LitElement {
} }
private _idChanged(ev: CustomEvent) { private _idChanged(ev: CustomEvent) {
const newId = ev.detail.value; const newId = (ev.target as any).value;
if (newId === (this.trigger.id ?? "")) { if (newId === (this.trigger.id ?? "")) {
return; return;
} }
@@ -333,7 +334,11 @@ export default class HaAutomationTriggerRow extends LitElement {
--mdc-theme-text-primary-on-background: var(--disabled-text-color); --mdc-theme-text-primary-on-background: var(--disabled-text-color);
} }
mwc-select { mwc-select {
margin-bottom: 16px; margin-bottom: 24px;
}
ha-textfield {
display: block;
margin-bottom: 24px;
} }
`, `,
]; ];

View File

@@ -1,7 +1,7 @@
import "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, html, LitElement } from "lit";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-textfield";
import "../../../../../components/ha-yaml-editor"; import "../../../../../components/ha-yaml-editor";
import "../../../../../components/user/ha-users-picker"; import "../../../../../components/user/ha-users-picker";
import { EventTrigger } from "../../../../../data/automation"; import { EventTrigger } from "../../../../../data/automation";
@@ -24,14 +24,14 @@ export class HaEventTrigger extends LitElement implements TriggerElement {
protected render() { protected render() {
const { event_type, event_data, context } = this.trigger; const { event_type, event_data, context } = this.trigger;
return html` return html`
<paper-input <ha-textfield
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.event.event_type" "ui.panel.config.automation.editor.triggers.type.event.event_type"
)} )}
name="event_type" name="event_type"
.value=${event_type} .value=${event_type}
@value-changed=${this._valueChanged} @change=${this._valueChanged}
></paper-input> ></ha-textfield>
<ha-yaml-editor <ha-yaml-editor
.hass=${this.hass} .hass=${this.hass}
.label=${this.hass.localize( .label=${this.hass.localize(
@@ -97,6 +97,14 @@ export class HaEventTrigger extends LitElement implements TriggerElement {
value, value,
}); });
} }
static get styles(): CSSResultGroup {
return css`
ha-textfield {
display: block;
}
`;
}
} }
declare global { declare global {

View File

@@ -1,3 +1,4 @@
import "../../../components/ha-newsletter";
import { mdiCloudLock, mdiDotsVertical, mdiMagnify } from "@mdi/js"; import { mdiCloudLock, mdiDotsVertical, mdiMagnify } from "@mdi/js";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import type { ActionDetail } from "@material/mwc-list"; import type { ActionDetail } from "@material/mwc-list";
@@ -134,6 +135,7 @@ class HaConfigDashboard extends LitElement {
.pages=${configSections.dashboard} .pages=${configSections.dashboard}
></ha-config-navigation> ></ha-config-navigation>
</ha-card>`} </ha-card>`}
<ha-newsletter .hass=${this.hass}></ha-newsletter>
</ha-config-section> </ha-config-section>
</ha-app-layout> </ha-app-layout>
`; `;

View File

@@ -27,6 +27,7 @@ import {
computeDeviceName, computeDeviceName,
DeviceRegistryEntry, DeviceRegistryEntry,
updateDeviceRegistryEntry, updateDeviceRegistryEntry,
removeConfigEntryFromDevice,
} from "../../../data/device_registry"; } from "../../../data/device_registry";
import { import {
fetchDiagnosticHandler, fetchDiagnosticHandler,
@@ -95,6 +96,8 @@ export class HaConfigDevicePage extends LitElement {
| number | number
| (TemplateResult | string)[]; | (TemplateResult | string)[];
@state() private _deleteButtons?: (TemplateResult | string)[];
private _device = memoizeOne( private _device = memoizeOne(
( (
deviceId: string, deviceId: string,
@@ -186,10 +189,11 @@ export class HaConfigDevicePage extends LitElement {
changedProps.has("entries") changedProps.has("entries")
) { ) {
this._diagnosticDownloadLinks = undefined; this._diagnosticDownloadLinks = undefined;
this._deleteButtons = undefined;
} }
if ( if (
this._diagnosticDownloadLinks || (this._diagnosticDownloadLinks && this._deleteButtons) ||
!this.devices || !this.devices ||
!this.deviceId || !this.deviceId ||
!this.entries !this.entries
@@ -198,7 +202,9 @@ export class HaConfigDevicePage extends LitElement {
} }
this._diagnosticDownloadLinks = Math.random(); this._diagnosticDownloadLinks = Math.random();
this._deleteButtons = []; // To prevent re-rendering if no delete buttons
this._renderDiagnosticButtons(this._diagnosticDownloadLinks); this._renderDiagnosticButtons(this._diagnosticDownloadLinks);
this._renderDeleteButtons();
} }
private async _renderDiagnosticButtons(requestId: number): Promise<void> { private async _renderDiagnosticButtons(requestId: number): Promise<void> {
@@ -263,6 +269,55 @@ export class HaConfigDevicePage extends LitElement {
} }
} }
private _renderDeleteButtons() {
const device = this._device(this.deviceId, this.devices);
if (!device) {
return;
}
const buttons: TemplateResult[] = [];
this._integrations(device, this.entries).forEach((entry) => {
if (entry.state !== "loaded" || !entry.supports_remove_device) {
return;
}
buttons.push(html`
<mwc-button
class="warning"
.entryId=${entry.entry_id}
@click=${this._confirmDeleteEntry}
>
${buttons.length > 1
? this.hass.localize(
`ui.panel.config.devices.delete_device_integration`,
{
integration: domainToName(this.hass.localize, entry.domain),
}
)
: this.hass.localize(`ui.panel.config.devices.delete_device`)}
</mwc-button>
`);
});
if (buttons.length > 0) {
this._deleteButtons = buttons;
}
}
private async _confirmDeleteEntry(e: MouseEvent): Promise<void> {
const entryId = (e.currentTarget as any).entryId;
const confirmed = await showConfirmationDialog(this, {
text: this.hass.localize("ui.panel.config.devices.confirm_delete"),
});
if (!confirmed) {
return;
}
await removeConfigEntryFromDevice(this.hass!, this.deviceId, entryId);
}
protected firstUpdated(changedProps) { protected firstUpdated(changedProps) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
loadDeviceRegistryDetailDialog(); loadDeviceRegistryDetailDialog();
@@ -375,6 +430,9 @@ export class HaConfigDevicePage extends LitElement {
if (Array.isArray(this._diagnosticDownloadLinks)) { if (Array.isArray(this._diagnosticDownloadLinks)) {
deviceActions.push(...this._diagnosticDownloadLinks); deviceActions.push(...this._diagnosticDownloadLinks);
} }
if (this._deleteButtons) {
deviceActions.push(...this._deleteButtons);
}
return html` return html`
<hass-tabs-subpage <hass-tabs-subpage

View File

@@ -369,7 +369,7 @@ export class HaIntegrationCard extends LitElement {
</a>` </a>`
: ""} : ""}
${!item.disabled_by && ${!item.disabled_by &&
item.state === "loaded" && (item.state === "loaded" || item.state === "setup_retry") &&
item.supports_unload && item.supports_unload &&
item.source !== "system" item.source !== "system"
? html`<mwc-list-item @request-selected=${this._handleReload}> ? html`<mwc-list-item @request-selected=${this._handleReload}>

View File

@@ -5,9 +5,11 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
import { computeStateDomain } from "../../../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../../../common/entity/compute_state_name"; import { computeStateName } from "../../../../../common/entity/compute_state_name";
import "../../../../../components/buttons/ha-call-api-button"; import "../../../../../components/buttons/ha-call-api-button";
import "../../../../../components/buttons/ha-call-service-button"; import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-card"; import "../../../../../components/ha-card";
import "../../../../../components/ha-circular-progress"; import "../../../../../components/ha-circular-progress";
import "../../../../../components/ha-icon"; import "../../../../../components/ha-icon";
@@ -18,30 +20,28 @@ import {
fetchDeviceRegistry, fetchDeviceRegistry,
subscribeDeviceRegistry, subscribeDeviceRegistry,
} from "../../../../../data/device_registry"; } from "../../../../../data/device_registry";
import {
migrateZwave,
ZWaveJsMigrationData,
fetchZwaveNetworkStatus as fetchZwaveJsNetworkStatus,
fetchZwaveNodeStatus,
getZwaveJsIdentifiersFromDevice,
subscribeZwaveNodeReady,
} from "../../../../../data/zwave_js";
import { import {
fetchMigrationConfig, fetchMigrationConfig,
fetchNetworkStatus,
startZwaveJsConfigFlow, startZwaveJsConfigFlow,
ZWaveMigrationConfig, ZWaveMigrationConfig,
ZWaveNetworkStatus, ZWaveNetworkStatus,
ZWAVE_NETWORK_STATE_STOPPED, ZWAVE_NETWORK_STATE_STOPPED,
fetchNetworkStatus,
} from "../../../../../data/zwave"; } from "../../../../../data/zwave";
import {
fetchZwaveNetworkStatus as fetchZwaveJsNetworkStatus,
fetchZwaveNodeStatus,
getZwaveJsIdentifiersFromDevice,
migrateZwave,
subscribeZwaveNodeReady,
ZWaveJsMigrationData,
} from "../../../../../data/zwave_js";
import { showConfigFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-config-flow"; import { showConfigFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-config-flow";
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
import "../../../../../layouts/hass-subpage"; import "../../../../../layouts/hass-subpage";
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types"; import type { HomeAssistant, Route } from "../../../../../types";
import "../../../ha-config-section"; import "../../../ha-config-section";
import { computeStateDomain } from "../../../../../common/entity/compute_state_domain";
import "../../../../../components/ha-alert";
@customElement("zwave-migration") @customElement("zwave-migration")
export class ZwaveMigration extends LitElement { export class ZwaveMigration extends LitElement {
@@ -155,7 +155,7 @@ export class ZwaveMigration extends LitElement {
.filter( .filter(
(entityState) => (entityState) =>
computeStateDomain(entityState) === "zwave" && computeStateDomain(entityState) === "zwave" &&
entityState.state !== "ready" !["ready", "sleeping"].includes(entityState.state)
) )
.map( .map(
(entityState) => (entityState) =>
@@ -430,6 +430,10 @@ export class ZwaveMigration extends LitElement {
const nodesNotReady = (await Promise.all(nodeStatePromisses)).filter( const nodesNotReady = (await Promise.all(nodeStatePromisses)).filter(
(node) => !node.ready (node) => !node.ready
); );
// eslint-disable-next-line no-console
console.log("waiting for nodes to be ready", nodesNotReady);
this._getMigrationData(); this._getMigrationData();
if (nodesNotReady.length === 0) { if (nodesNotReady.length === 0) {
this._waitingOnDevices = []; this._waitingOnDevices = [];
@@ -445,10 +449,19 @@ export class ZwaveMigration extends LitElement {
} }
) )
); );
const deviceReg = await fetchDeviceRegistry(this.hass); const deviceReg: DeviceRegistryEntry[] = await fetchDeviceRegistry(
this._waitingOnDevices = deviceReg this.hass.connection
.map((device) => getZwaveJsIdentifiersFromDevice(device)) );
.filter(Boolean); this._waitingOnDevices = deviceReg.filter((device) => {
const identifiers = getZwaveJsIdentifiersFromDevice(device);
if (
!identifiers ||
Number(identifiers.home_id) !== networkStatus.controller.home_id
) {
return false;
}
return nodesNotReady.some((node) => identifiers.node_id === node.node_id);
});
} }
private async _getMigrationData() { private async _getMigrationData() {

View File

@@ -28,12 +28,13 @@ import { navigate } from "../../../common/navigate";
import { computeRTL } from "../../../common/util/compute_rtl"; import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/device/ha-device-picker"; import "../../../components/device/ha-device-picker";
import "../../../components/entity/ha-entities-picker"; import "../../../components/entity/ha-entities-picker";
import "../../../components/ha-area-picker";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-fab"; import "../../../components/ha-fab";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-icon-picker"; import "../../../components/ha-icon-picker";
import "../../../components/ha-area-picker";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import "../../../components/ha-textfield";
import { import {
computeDeviceName, computeDeviceName,
DeviceRegistryEntry, DeviceRegistryEntry,
@@ -288,14 +289,14 @@ export class HaSceneEditor extends SubscribeMixin(
</div> </div>
<ha-card> <ha-card>
<div class="card-content"> <div class="card-content">
<paper-input <ha-textfield
.value=${this._config.name} .value=${this._config.name}
.name=${"name"} .name=${"name"}
@value-changed=${this._valueChanged} @change=${this._valueChanged}
label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.scene.editor.name" "ui.panel.config.scene.editor.name"
)} )}
></paper-input> ></ha-textfield>
<ha-icon-picker <ha-icon-picker
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.scene.editor.icon" "ui.panel.config.scene.editor.icon"
@@ -701,14 +702,14 @@ export class HaSceneEditor extends SubscribeMixin(
this._dirty = true; this._dirty = true;
} }
private _valueChanged(ev: CustomEvent) { private _valueChanged(ev: Event) {
ev.stopPropagation(); ev.stopPropagation();
const target = ev.target as any; const target = ev.target as any;
const name = target.name; const name = target.name;
if (!name) { if (!name) {
return; return;
} }
let newVal = ev.detail.value; let newVal = (ev as CustomEvent).detail?.value ?? target.value;
if (target.type === "number") { if (target.type === "number") {
newVal = Number(newVal); newVal = Number(newVal);
} }
@@ -990,6 +991,9 @@ export class HaSceneEditor extends SubscribeMixin(
display: block; display: block;
margin-top: 8px; margin-top: 8px;
} }
ha-textfield {
display: block;
}
`, `,
]; ];
} }

View File

@@ -1,4 +1,3 @@
import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
@@ -6,6 +5,7 @@ import "../../../components/ha-blueprint-picker";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-circular-progress"; import "../../../components/ha-circular-progress";
import "../../../components/ha-markdown"; import "../../../components/ha-markdown";
import "../../../components/ha-textfield";
import "../../../components/ha-selector/ha-selector"; import "../../../components/ha-selector/ha-selector";
import "../../../components/ha-settings-row"; import "../../../components/ha-settings-row";
@@ -101,15 +101,14 @@ export class HaBlueprintScriptEditor extends LitElement {
value?.default} value?.default}
@value-changed=${this._inputChanged} @value-changed=${this._inputChanged}
></ha-selector>` ></ha-selector>`
: html`<paper-input : html`<ha-textfield
.key=${key} .key=${key}
required required
.value=${(this.config.use_blueprint.input && .value=${(this.config.use_blueprint.input &&
this.config.use_blueprint.input[key]) ?? this.config.use_blueprint.input[key]) ??
value?.default} value?.default}
@value-changed=${this._inputChanged} @change=${this._inputChanged}
no-label-float ></ha-textfield>`}
></paper-input>`}
</ha-settings-row>` </ha-settings-row>`
) )
: html`<p class="padding"> : html`<p class="padding">
@@ -145,7 +144,7 @@ export class HaBlueprintScriptEditor extends LitElement {
ev.stopPropagation(); ev.stopPropagation();
const target = ev.target as any; const target = ev.target as any;
const key = target.key; const key = target.key;
const value = ev.detail.value; const value = ev.detail?.value ?? target.value;
if ( if (
(this.config.use_blueprint.input && (this.config.use_blueprint.input &&
this.config.use_blueprint.input[key] === value) || this.config.use_blueprint.input[key] === value) ||

View File

@@ -3,7 +3,6 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { load } from "js-yaml";
import { debounce } from "../../../common/util/debounce"; import { debounce } from "../../../common/util/debounce";
import "../../../components/ha-circular-progress"; import "../../../components/ha-circular-progress";
import "../../../components/ha-code-editor"; import "../../../components/ha-code-editor";
@@ -52,10 +51,6 @@ class HaPanelDevTemplate extends LitElement {
private _template = ""; private _template = "";
private _context_data = "";
private _context_variables?;
private _inited = false; private _inited = false;
public connectedCallback() { public connectedCallback() {
@@ -70,14 +65,8 @@ class HaPanelDevTemplate extends LitElement {
} }
protected firstUpdated() { protected firstUpdated() {
if (localStorage) { if (localStorage && localStorage["panel-dev-template-template"]) {
if (localStorage["panel-dev-template-template"]) {
this._template = localStorage["panel-dev-template-template"]; this._template = localStorage["panel-dev-template-template"];
}
if (localStorage["panel-dev-template-context-data"]) {
this._context_data = localStorage["panel-dev-template-context-data"];
this._context_variables = undefined;
}
} else { } else {
this._template = DEMO_TEMPLATE; this._template = DEMO_TEMPLATE;
} }
@@ -152,17 +141,6 @@ class HaPanelDevTemplate extends LitElement {
"ui.panel.developer-tools.tabs.templates.reset" "ui.panel.developer-tools.tabs.templates.reset"
)} )}
</mwc-button> </mwc-button>
<p>
${this.hass.localize(
"ui.panel.developer-tools.tabs.templates.context_data"
)}
</p>
<ha-code-editor
mode="yaml"
.value=${this._context_data}
@value-changed=${this._contextDataChanged}
dir="ltr"
></ha-code-editor>
</div> </div>
<div class="render-pane"> <div class="render-pane">
@@ -324,15 +302,6 @@ class HaPanelDevTemplate extends LitElement {
false false
); );
private _contextDataChanged(ev) {
this._context_data = ev.detail.value;
this._context_variables = undefined;
if (this._error) {
this._error = undefined;
}
this._debounceRender();
}
private _templateChanged(ev) { private _templateChanged(ev) {
this._template = ev.detail.value; this._template = ev.detail.value;
if (this._error) { if (this._error) {
@@ -345,9 +314,6 @@ class HaPanelDevTemplate extends LitElement {
this._rendering = true; this._rendering = true;
await this._unsubscribeTemplate(); await this._unsubscribeTemplate();
try { try {
if (this._context_data && !this._context_variables) {
this._context_variables = load(this._context_data);
}
this._unsubRenderTemplate = subscribeRenderTemplate( this._unsubRenderTemplate = subscribeRenderTemplate(
this.hass.connection, this.hass.connection,
(result) => { (result) => {
@@ -356,7 +322,6 @@ class HaPanelDevTemplate extends LitElement {
}, },
{ {
template: this._template, template: this._template,
variables: this._context_variables,
timeout: 3, timeout: 3,
} }
); );
@@ -395,16 +360,12 @@ class HaPanelDevTemplate extends LitElement {
if (!this._inited) { if (!this._inited) {
return; return;
} }
localStorage["panel-dev-template-context-data"] = this._context_data;
localStorage["panel-dev-template-template"] = this._template; localStorage["panel-dev-template-template"] = this._template;
} }
private _restoreDemo() { private _restoreDemo() {
this._context_data = "";
this._context_variables = undefined;
this._template = DEMO_TEMPLATE; this._template = DEMO_TEMPLATE;
this._subscribeTemplate(); this._subscribeTemplate();
delete localStorage["panel-dev-template-context-data"];
delete localStorage["panel-dev-template-template"]; delete localStorage["panel-dev-template-template"];
} }
} }

View File

@@ -81,7 +81,7 @@ class HaPanelHistory extends LitElement {
</app-header> </app-header>
<div class="flex content"> <div class="flex content">
<div class="flex layout horizontal wrap"> <div class="filters">
<ha-date-range-picker <ha-date-range-picker
.hass=${this.hass} .hass=${this.hass}
?disabled=${this._isLoading} ?disabled=${this._isLoading}
@@ -247,6 +247,12 @@ class HaPanelHistory extends LitElement {
position: relative; position: relative;
} }
.filters {
display: flex;
align-items: flex-end;
padding: 8px 16px 0;
}
ha-date-range-picker { ha-date-range-picker {
margin-right: 16px; margin-right: 16px;
max-width: 100%; max-width: 100%;

View File

@@ -335,7 +335,7 @@ export class HaPanelLogbook extends LitElement {
.filters { .filters {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
padding: 0 16px; padding: 8px 16px 0;
} }
:host([narrow]) .filters { :host([narrow]) .filters {

View File

@@ -57,7 +57,7 @@ export class HuiThemeSelectEditor extends LitElement {
if (!this.hass || ev.target.value === "") { if (!this.hass || ev.target.value === "") {
return; return;
} }
this.value = ev.target.value === "remove" ? "" : ev.target.selected; this.value = ev.target.value === "remove" ? undefined : ev.target.value;
fireEvent(this, "value-changed", { value: this.value }); fireEvent(this, "value-changed", { value: this.value });
} }
} }

View File

@@ -1,18 +1,14 @@
import "@polymer/paper-input/paper-input"; import "../../../../components/ha-form/ha-form";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { assert, assign, boolean, object, optional, string } from "superstruct"; import { assert, assign, boolean, object, optional, string } from "superstruct";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-area-picker"; import type { HomeAssistant } from "../../../../types";
import { HomeAssistant } from "../../../../types"; import type { AreaCardConfig } from "../../cards/types";
import { AreaCardConfig } from "../../cards/types"; import type { LovelaceCardEditor } from "../../types";
import "../../components/hui-theme-select-editor";
import { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditorTarget } from "../types"; import type { HaFormSchema } from "../../../../components/ha-form/types";
import { configElementStyle } from "./config-elements-style";
import "../../../../components/ha-formfield";
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
const cardConfigStruct = assign( const cardConfigStruct = assign(
baseLovelaceCardConfig, baseLovelaceCardConfig,
@@ -38,21 +34,18 @@ export class HuiAreaCardEditor
this._config = config; this._config = config;
} }
get _area(): string { private _schema = memoizeOne((): HaFormSchema[] => [
return this._config!.area || ""; { name: "area", selector: { area: {} } },
} { name: "show_camera", required: false, selector: { boolean: {} } },
{
get _navigation_path(): string { name: "",
return this._config!.navigation_path || ""; type: "grid",
} schema: [
{ name: "navigation_path", required: false, selector: { text: {} } },
get _theme(): string { { name: "theme", required: false, selector: { theme: {} } },
return this._config!.theme || ""; ],
} },
]);
get _show_camera(): boolean {
return this._config!.show_camera || false;
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.hass || !this._config) { if (!this.hass || !this._config) {
@@ -60,79 +53,35 @@ export class HuiAreaCardEditor
} }
return html` return html`
<div class="card-config"> <ha-form
<ha-area-picker
.hass=${this.hass} .hass=${this.hass}
.value=${this._area} .data=${this._config}
.placeholder=${this._area} .schema=${this._schema()}
.configValue=${"area"} .computeLabel=${this._computeLabelCallback}
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.area.name"
)}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-area-picker> ></ha-form>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.area.show_camera"
)}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.checked=${this._show_camera}
.configValue=${"show_camera"}
@change=${this._valueChanged}
></ha-switch>
</ha-formfield>
<paper-input
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.action-editor.navigation_path"
)}
.value=${this._navigation_path}
.configValue=${"navigation_path"}
@value-changed=${this._valueChanged}
>
</paper-input>
<hui-theme-select-editor
.hass=${this.hass}
.value=${this._theme}
.configValue=${"theme"}
@value-changed=${this._valueChanged}
></hui-theme-select-editor>
</div>
`; `;
} }
private _valueChanged(ev: CustomEvent): void { private _valueChanged(ev: CustomEvent): void {
if (!this._config || !this.hass) { const config = ev.detail.value;
return; fireEvent(this, "config-changed", { config });
}
const target = ev.target! as EditorTarget;
const value =
target.checked !== undefined ? target.checked : ev.detail.value;
if (this[`_${target.configValue}`] === value) {
return;
} }
let newConfig; private _computeLabelCallback = (schema: HaFormSchema) => {
if (target.configValue) { switch (schema.name) {
if (!value) { case "area":
newConfig = { ...this._config }; return this.hass!.localize("ui.panel.lovelace.editor.card.area.name");
delete newConfig[target.configValue!]; case "navigation_path":
} else { return this.hass!.localize(
newConfig = { "ui.panel.lovelace.editor.action-editor.navigation_path"
...this._config, );
[target.configValue!]: value, }
return this.hass!.localize(
`ui.panel.lovelace.editor.card.area.${schema.name}`
);
}; };
} }
}
fireEvent(this, "config-changed", { config: newConfig });
}
static get styles(): CSSResultGroup {
return configElementStyle;
}
}
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {

View File

@@ -1,22 +1,18 @@
import "@polymer/paper-input/paper-input"; import "../../../../components/ha-form/ha-form";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { assert, assign, boolean, object, optional, string } from "superstruct"; import { assert, assign, boolean, object, optional, string } from "superstruct";
import type { HassEntity } from "home-assistant-js-websocket/dist/types";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain"; import { computeDomain } from "../../../../common/entity/compute_domain";
import { domainIcon } from "../../../../common/entity/domain_icon"; import { domainIcon } from "../../../../common/entity/domain_icon";
import "../../../../components/entity/ha-entity-attribute-picker"; import type { HaFormSchema } from "../../../../components/ha-form/types";
import "../../../../components/ha-icon-picker"; import type { HomeAssistant } from "../../../../types";
import { HomeAssistant } from "../../../../types"; import type { EntityCardConfig } from "../../cards/types";
import { EntityCardConfig } from "../../cards/types";
import "../../components/hui-action-editor";
import "../../components/hui-entity-editor";
import "../../components/hui-theme-select-editor";
import { headerFooterConfigStructs } from "../../header-footer/structs"; import { headerFooterConfigStructs } from "../../header-footer/structs";
import { LovelaceCardEditor } from "../../types"; import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditorTarget, EntitiesEditorEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
const cardConfigStruct = assign( const cardConfigStruct = assign(
baseLovelaceCardConfig, baseLovelaceCardConfig,
@@ -46,175 +42,83 @@ export class HuiEntityCardEditor
this._config = config; this._config = config;
} }
get _entity(): string { private _schema = memoizeOne(
return this._config!.entity || ""; (entity: string, icon: string, entityState: HassEntity): HaFormSchema[] => [
} { name: "entity", required: true, selector: { entity: {} } },
{
type: "grid",
name: "",
schema: [
{ name: "name", selector: { text: {} } },
{
name: "icon",
selector: {
icon: {
placeholder: icon || entityState?.attributes.icon,
fallbackPath:
!icon && !entityState?.attributes.icon && entityState
? domainIcon(computeDomain(entity), entityState)
: undefined,
},
},
},
get _name(): string { {
return this._config!.name || ""; name: "attribute",
} selector: { attribute: { entity_id: entity } },
},
get _icon(): string { { name: "unit", selector: { text: {} } },
return this._config!.icon || ""; { name: "theme", selector: { theme: {} } },
} { name: "state_color", selector: { boolean: {} } },
],
get _attribute(): string { },
return this._config!.attribute || ""; ]
} );
get _unit(): string {
return this._config!.unit || "";
}
get _state_color(): boolean {
return this._config!.state_color ?? false;
}
get _theme(): string {
return this._config!.theme || "";
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.hass || !this._config) { if (!this.hass || !this._config) {
return html``; return html``;
} }
const entityState = this.hass.states[this._entity];
const entityState = this.hass.states[this._config.entity];
const schema = this._schema(
this._config.entity,
this._config.icon,
entityState
);
return html` return html`
<div class="card-config"> <ha-form
<ha-entity-picker
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.entity"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.required"
)})"
.hass=${this.hass} .hass=${this.hass}
.value=${this._entity} .data=${this._config}
.configValue=${"entity"} .schema=${schema}
@change=${this._valueChanged} .computeLabel=${this._computeLabelCallback}
allow-custom-entity
></ha-entity-picker>
<div class="side-by-side">
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.name"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._name}
.configValue=${"name"}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></paper-input> ></ha-form>
<ha-icon-picker
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.icon"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._icon}
.placeholder=${this._icon || entityState?.attributes.icon}
.fallbackPath=${!this._icon &&
!entityState?.attributes.icon &&
entityState
? domainIcon(computeDomain(entityState.entity_id), entityState)
: undefined}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
></ha-icon-picker>
</div>
<div class="side-by-side">
<ha-entity-attribute-picker
.hass=${this.hass}
.entityId=${this._entity}
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.attribute"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._attribute}
.configValue=${"attribute"}
@value-changed=${this._valueChanged}
></ha-entity-attribute-picker>
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.unit"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._unit}
.configValue=${"unit"}
@value-changed=${this._valueChanged}
></paper-input>
</div>
<div class="side-by-side">
<hui-theme-select-editor
.hass=${this.hass}
.value=${this._theme}
.configValue=${"theme"}
@value-changed=${this._valueChanged}
>
</hui-theme-select-editor>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.state_color"
)}
>
<ha-switch
.checked=${this._config!.state_color}
.configValue=${"state_color"}
@change=${this._valueChanged}
>
</ha-switch>
</ha-formfield>
</div>
</div>
`; `;
} }
private _valueChanged(ev: EntitiesEditorEvent): void { private _valueChanged(ev: CustomEvent): void {
if (!this._config || !this.hass) { const config = ev.detail.value;
return; Object.keys(config).forEach((k) => config[k] === "" && delete config[k]);
fireEvent(this, "config-changed", { config });
} }
const target = ev.currentTarget! as EditorTarget;
if ( private _computeLabelCallback = (schema: HaFormSchema) => {
this[`_${target.configValue}`] === target.value || if (schema.name === "entity") {
this[`_${target.configValue}`] === target.config return `${this.hass!.localize(
) { "ui.panel.lovelace.editor.card.generic.entity"
return; )} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.required"
)})`;
} }
if (target.configValue) {
if (target.value === "") { return this.hass!.localize(
this._config = { ...this._config }; `ui.panel.lovelace.editor.card.generic.${schema.name}`
delete this._config[target.configValue!]; );
} else {
let newValue: string | undefined;
if (
target.configValue === "icon_height" &&
!isNaN(Number(target.value))
) {
newValue = `${String(target.value)}px`;
}
this._config = {
...this._config,
[target.configValue!]:
target.checked !== undefined
? target.checked
: newValue !== undefined
? newValue
: target.value
? target.value
: target.config,
}; };
} }
}
fireEvent(this, "config-changed", { config: this._config });
}
static get styles(): CSSResultGroup {
return configElementStyle;
}
}
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {

View File

@@ -5,61 +5,60 @@ import {
SUPPORT_PAUSE, SUPPORT_PAUSE,
SUPPORT_PLAY, SUPPORT_PLAY,
} from "../../data/media-player"; } from "../../data/media-player";
import { resolveMediaSource } from "../../data/media_source"; import { ResolvedMediaSource } from "../../data/media_source";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
export class BrowserMediaPlayer { export class BrowserMediaPlayer {
private player?: HTMLAudioElement; private player: HTMLAudioElement;
private stopped = false; // We pretend we're playing while still buffering.
public buffering = true;
private _removed = false;
constructor( constructor(
public hass: HomeAssistant, public hass: HomeAssistant,
public item: MediaPlayerItem, public item: MediaPlayerItem,
public resolved: ResolvedMediaSource,
private onChange: () => void private onChange: () => void
) {} ) {
const player = new Audio(this.resolved.url);
public async initialize() {
const resolvedUrl: any = await resolveMediaSource(
this.hass,
this.item.media_content_id
);
const player = new Audio(resolvedUrl.url);
player.addEventListener("play", this._handleChange); player.addEventListener("play", this._handleChange);
player.addEventListener("playing", this._handleChange); player.addEventListener("playing", () => {
this.buffering = false;
this._handleChange();
});
player.addEventListener("pause", this._handleChange); player.addEventListener("pause", this._handleChange);
player.addEventListener("ended", this._handleChange); player.addEventListener("ended", this._handleChange);
player.addEventListener("canplaythrough", () => { player.addEventListener("canplaythrough", () => {
if (this.stopped) { if (this._removed) {
return; return;
} }
this.player = player; if (this.buffering) {
player.play(); player.play();
}
this.onChange(); this.onChange();
}); });
this.player = player;
} }
private _handleChange = () => { private _handleChange = () => {
if (!this.stopped) { if (!this._removed) {
this.onChange(); this.onChange();
} }
}; };
public pause() { public pause() {
if (this.player) { this.buffering = false;
this.player.pause(); this.player.pause();
} }
}
public play() { public play() {
if (this.player) {
this.player.play(); this.player.play();
} }
}
public stop() { public remove() {
this.stopped = true; this._removed = true;
// @ts-ignore // @ts-ignore
this.onChange = undefined; this.onChange = undefined;
if (this.player) { if (this.player) {
@@ -68,9 +67,7 @@ export class BrowserMediaPlayer {
} }
public get isPlaying(): boolean { public get isPlaying(): boolean {
return ( return this.buffering || (!this.player.paused && !this.player.ended);
this.player !== undefined && !this.player.paused && !this.player.ended
);
} }
static idleStateObj(): MediaPlayerEntity { static idleStateObj(): MediaPlayerEntity {
@@ -88,19 +85,19 @@ export class BrowserMediaPlayer {
toStateObj(): MediaPlayerEntity { toStateObj(): MediaPlayerEntity {
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement
const base = BrowserMediaPlayer.idleStateObj(); const base = BrowserMediaPlayer.idleStateObj();
if (!this.player) {
return base;
}
base.state = this.isPlaying ? "playing" : "paused"; base.state = this.isPlaying ? "playing" : "paused";
base.attributes = { base.attributes = {
media_title: this.item.title, media_title: this.item.title,
media_duration: this.player.duration,
media_position: this.player.currentTime,
media_position_updated_at: base.last_updated,
entity_picture: this.item.thumbnail, entity_picture: this.item.thumbnail,
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
supported_features: SUPPORT_PLAY | SUPPORT_PAUSE, supported_features: SUPPORT_PLAY | SUPPORT_PAUSE,
}; };
if (this.player.duration) {
base.attributes.media_duration = this.player.duration;
base.attributes.media_position = this.player.currentTime;
base.attributes.media_position_updated_at = base.last_updated;
}
return base; return base;
} }
} }

View File

@@ -20,6 +20,7 @@ import {
} from "lit"; } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { until } from "lit/directives/until";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../common/entity/compute_state_domain";
@@ -27,6 +28,7 @@ import { computeStateName } from "../../common/entity/compute_state_name";
import { domainIcon } from "../../common/entity/domain_icon"; import { domainIcon } from "../../common/entity/domain_icon";
import { supportsFeature } from "../../common/entity/supports-feature"; import { supportsFeature } from "../../common/entity/supports-feature";
import "../../components/ha-button-menu"; import "../../components/ha-button-menu";
import "../../components/ha-circular-progress";
import "../../components/ha-icon-button"; import "../../components/ha-icon-button";
import { UNAVAILABLE_STATES } from "../../data/entity"; import { UNAVAILABLE_STATES } from "../../data/entity";
import { import {
@@ -43,6 +45,7 @@ import {
SUPPORT_PLAY, SUPPORT_PLAY,
SUPPORT_STOP, SUPPORT_STOP,
} from "../../data/media-player"; } from "../../data/media-player";
import { ResolvedMediaSource } from "../../data/media_source";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../lovelace/components/hui-marquee"; import "../lovelace/components/hui-marquee";
import { BrowserMediaPlayer } from "./browser-media-player"; import { BrowserMediaPlayer } from "./browser-media-player";
@@ -54,7 +57,7 @@ declare global {
} }
@customElement("ha-bar-media-player") @customElement("ha-bar-media-player")
class BarMediaPlayer extends LitElement { export class BarMediaPlayer extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entityId!: string; @property({ attribute: false }) public entityId!: string;
@@ -68,6 +71,8 @@ class BarMediaPlayer extends LitElement {
@state() private _marqueeActive = false; @state() private _marqueeActive = false;
@state() private _newMediaExpected = false;
@state() private _browserPlayer?: BrowserMediaPlayer; @state() private _browserPlayer?: BrowserMediaPlayer;
private _progressInterval?: number; private _progressInterval?: number;
@@ -98,32 +103,54 @@ class BarMediaPlayer extends LitElement {
clearInterval(this._progressInterval); clearInterval(this._progressInterval);
this._progressInterval = undefined; this._progressInterval = undefined;
} }
this._tearDownBrowserPlayer();
if (this._browserPlayer) {
this._browserPlayer.stop();
this._browserPlayer = undefined;
}
} }
public async playItem(item: MediaPlayerItem) { public showResolvingNewMediaPicked() {
this._tearDownBrowserPlayer();
this._newMediaExpected = true;
}
public hideResolvingNewMediaPicked() {
this._newMediaExpected = false;
}
public playItem(item: MediaPlayerItem, resolved: ResolvedMediaSource) {
if (this.entityId !== BROWSER_PLAYER) { if (this.entityId !== BROWSER_PLAYER) {
throw Error("Only browser supported"); throw Error("Only browser supported");
} }
if (this._browserPlayer) { this._tearDownBrowserPlayer();
this._browserPlayer.stop(); this._browserPlayer = new BrowserMediaPlayer(
} this.hass,
this._browserPlayer = new BrowserMediaPlayer(this.hass, item, () => item,
this.requestUpdate("_browserPlayer") resolved,
() => this.requestUpdate("_browserPlayer")
); );
await this._browserPlayer.initialize(); this._newMediaExpected = false;
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (this._newMediaExpected) {
return html`
<div class="controls-progress">
${until(
// Only show spinner after 500ms
new Promise((resolve) => setTimeout(resolve, 500)).then(
() => html`<ha-circular-progress active></ha-circular-progress>`
)
)}
</div>
`;
}
const isBrowser = this.entityId === BROWSER_PLAYER; const isBrowser = this.entityId === BROWSER_PLAYER;
const stateObj = this._stateObj; const stateObj = this._stateObj;
const controls = !stateObj
? undefined if (!stateObj) {
: !this.narrow return this._renderChoosePlayer(stateObj);
}
const controls = !this.narrow
? computeMediaControls(stateObj) ? computeMediaControls(stateObj)
: (stateObj.state === "playing" && : (stateObj.state === "playing" &&
(supportsFeature(stateObj, SUPPORT_PAUSE) || (supportsFeature(stateObj, SUPPORT_PAUSE) ||
@@ -152,16 +179,14 @@ class BarMediaPlayer extends LitElement {
}, },
] ]
: [{}]; : [{}];
const mediaDescription = stateObj ? computeMediaDescription(stateObj) : ""; const mediaDescription = computeMediaDescription(stateObj);
const mediaDuration = formatMediaTime(stateObj?.attributes.media_duration); const mediaDuration = formatMediaTime(stateObj.attributes.media_duration);
const mediaTitleClean = cleanupMediaTitle( const mediaTitleClean = cleanupMediaTitle(
stateObj?.attributes.media_title || "" stateObj.attributes.media_title || ""
); );
const mediaArt =
const mediaArt = stateObj stateObj.attributes.entity_picture_local ||
? stateObj.attributes.entity_picture_local || stateObj.attributes.entity_picture;
stateObj.attributes.entity_picture
: undefined;
return html` return html`
<div <div
@@ -177,7 +202,10 @@ class BarMediaPlayer extends LitElement {
<hui-marquee <hui-marquee
.text=${mediaTitleClean || .text=${mediaTitleClean ||
mediaDescription || mediaDescription ||
this.hass.localize(`ui.card.media_player.nothing_playing`)} cleanupMediaTitle(stateObj.attributes.media_content_id) ||
(stateObj.state !== "playing" && stateObj.state !== "on"
? this.hass.localize(`ui.card.media_player.nothing_playing`)
: "")}
.active=${this._marqueeActive} .active=${this._marqueeActive}
@mouseover=${this._marqueeMouseOver} @mouseover=${this._marqueeMouseOver}
@mouseleave=${this._marqueeMouseLeave} @mouseleave=${this._marqueeMouseLeave}
@@ -188,6 +216,9 @@ class BarMediaPlayer extends LitElement {
</div> </div>
</div> </div>
<div class="controls-progress"> <div class="controls-progress">
${this._browserPlayer?.buffering
? html` <ha-circular-progress active></ha-circular-progress> `
: html`
<div class="controls"> <div class="controls">
${controls === undefined ${controls === undefined
? "" ? ""
@@ -205,7 +236,9 @@ class BarMediaPlayer extends LitElement {
` `
)} )}
</div> </div>
${this.narrow ${stateObj.attributes.media_duration === Infinity
? html``
: this.narrow
? html`<mwc-linear-progress></mwc-linear-progress>` ? html`<mwc-linear-progress></mwc-linear-progress>`
: html` : html`
<div class="progress"> <div class="progress">
@@ -214,10 +247,19 @@ class BarMediaPlayer extends LitElement {
<div>${mediaDuration}</div> <div>${mediaDuration}</div>
</div> </div>
`} `}
`}
</div> </div>
${this._renderChoosePlayer(stateObj)}
`;
}
private _renderChoosePlayer(stateObj: MediaPlayerEntity | undefined) {
const isBrowser = this.entityId === BROWSER_PLAYER;
return html`
<div class="choose-player ${isBrowser ? "browser" : ""}"> <div class="choose-player ${isBrowser ? "browser" : ""}">
<ha-button-menu corner="BOTTOM_START"> <ha-button-menu corner="BOTTOM_START">
${this.narrow ${
this.narrow
? html` ? html`
<ha-icon-button <ha-icon-button
slot="trigger" slot="trigger"
@@ -231,7 +273,11 @@ class BarMediaPlayer extends LitElement {
slot="trigger" slot="trigger"
.label=${this.narrow .label=${this.narrow
? "" ? ""
: `${stateObj ? computeStateName(stateObj) : this.entityId} : `${
stateObj
? computeStateName(stateObj)
: this.entityId
}
`} `}
> >
<ha-svg-icon <ha-svg-icon
@@ -245,7 +291,8 @@ class BarMediaPlayer extends LitElement {
.path=${mdiChevronDown} .path=${mdiChevronDown}
></ha-svg-icon> ></ha-svg-icon>
</mwc-button> </mwc-button>
`} `
}
<mwc-list-item <mwc-list-item
.player=${BROWSER_PLAYER} .player=${BROWSER_PLAYER}
?selected=${isBrowser} ?selected=${isBrowser}
@@ -267,18 +314,26 @@ class BarMediaPlayer extends LitElement {
)} )}
</ha-button-menu> </ha-button-menu>
</div> </div>
</div>
`; `;
} }
public willUpdate(changedProps: PropertyValues) { public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps); super.willUpdate(changedProps);
if (changedProps.has("entityId")) {
this._tearDownBrowserPlayer();
}
if (!changedProps.has("hass") || this.entityId === BROWSER_PLAYER) {
return;
}
// Reset new media expected if media player state changes
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if ( if (
changedProps.has("entityId") && !oldHass ||
this.entityId !== BROWSER_PLAYER && oldHass.states[this.entityId] !== this.hass.states[this.entityId]
this._browserPlayer
) { ) {
this._browserPlayer?.stop(); this._newMediaExpected = false;
this._browserPlayer = undefined;
} }
} }
@@ -327,6 +382,13 @@ class BarMediaPlayer extends LitElement {
return this.hass!.states[this.entityId] as MediaPlayerEntity | undefined; return this.hass!.states[this.entityId] as MediaPlayerEntity | undefined;
} }
private _tearDownBrowserPlayer() {
if (this._browserPlayer) {
this._browserPlayer.remove();
this._browserPlayer = undefined;
}
}
private _openMoreInfo() { private _openMoreInfo() {
if (this._browserPlayer) { if (this._browserPlayer) {
return; return;

View File

@@ -37,6 +37,7 @@ import "../../layouts/ha-app-layout";
import { haStyle } from "../../resources/styles"; import { haStyle } from "../../resources/styles";
import type { HomeAssistant, Route } from "../../types"; import type { HomeAssistant, Route } from "../../types";
import "./ha-bar-media-player"; import "./ha-bar-media-player";
import type { BarMediaPlayer } from "./ha-bar-media-player";
import { showWebBrowserPlayMediaDialog } from "./show-media-player-dialog"; import { showWebBrowserPlayMediaDialog } from "./show-media-player-dialog";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { import {
@@ -79,6 +80,8 @@ class PanelMediaBrowser extends LitElement {
@query("ha-media-player-browse") private _browser!: HaMediaPlayerBrowse; @query("ha-media-player-browse") private _browser!: HaMediaPlayerBrowse;
@query("ha-bar-media-player") private _player!: BarMediaPlayer;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-app-layout> <ha-app-layout>
@@ -235,15 +238,23 @@ class PanelMediaBrowser extends LitElement {
ev: HASSDomEvent<MediaPickedEvent> ev: HASSDomEvent<MediaPickedEvent>
): Promise<void> { ): Promise<void> {
const item = ev.detail.item; const item = ev.detail.item;
if (this._entityId !== BROWSER_PLAYER) { if (this._entityId !== BROWSER_PLAYER) {
this.hass!.callService("media_player", "play_media", { this._player.showResolvingNewMediaPicked();
try {
await this.hass!.callService("media_player", "play_media", {
entity_id: this._entityId, entity_id: this._entityId,
media_content_id: item.media_content_id, media_content_id: item.media_content_id,
media_content_type: item.media_content_type, media_content_type: item.media_content_type,
}); });
} catch (err) {
this._player.hideResolvingNewMediaPicked();
}
return; return;
} }
// We won't cancel current media being played if we're going to
// open a camera.
if (isCameraMediaSource(item.media_content_id)) { if (isCameraMediaSource(item.media_content_id)) {
fireEvent(this, "hass-more-info", { fireEvent(this, "hass-more-info", {
entityId: getEntityIdFromCameraMediaSource(item.media_content_id), entityId: getEntityIdFromCameraMediaSource(item.media_content_id),
@@ -251,15 +262,15 @@ class PanelMediaBrowser extends LitElement {
return; return;
} }
this._player.showResolvingNewMediaPicked();
const resolvedUrl = await resolveMediaSource( const resolvedUrl = await resolveMediaSource(
this.hass, this.hass,
item.media_content_id item.media_content_id
); );
if (resolvedUrl.mime_type.startsWith("audio/")) { if (resolvedUrl.mime_type.startsWith("audio/")) {
await this.shadowRoot!.querySelector("ha-bar-media-player")!.playItem( this._player.playItem(item, resolvedUrl);
item
);
return; return;
} }

View File

@@ -155,6 +155,10 @@ const REDIRECTS: Redirects = {
component: "history", component: "history",
redirect: "/history", redirect: "/history",
}, },
media_browser: {
component: "media_source",
redirect: "/media-browser",
},
}; };
export type ParamType = "url" | "string"; export type ParamType = "url" | "string";
@@ -178,7 +182,7 @@ class HaPanelMy extends LitElement {
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
const path = this.route.path.substr(1); const path = this.route.path.substring(1);
if (path.startsWith("supervisor")) { if (path.startsWith("supervisor")) {
if (!isComponentLoaded(this.hass, "hassio")) { if (!isComponentLoaded(this.hass, "hassio")) {

View File

@@ -1,5 +1,4 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -11,8 +10,10 @@ import {
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "../../components/ha-card"; import "../../components/ha-card";
import "../../components/ha-circular-progress"; import "../../components/ha-circular-progress";
import "../../components/ha-textfield";
import { haStyle } from "../../resources/styles"; import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../../components/ha-alert";
@customElement("ha-change-password-card") @customElement("ha-change-password-card")
class HaChangePasswordCard extends LitElement { class HaChangePasswordCard extends LitElement {
@@ -24,11 +25,11 @@ class HaChangePasswordCard extends LitElement {
@state() private _errorMsg?: string; @state() private _errorMsg?: string;
@state() private _currentPassword?: string; @state() private _currentPassword = "";
@state() private _password?: string; @state() private _password = "";
@state() private _passwordConfirm?: string; @state() private _passwordConfirm = "";
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
@@ -40,46 +41,48 @@ class HaChangePasswordCard extends LitElement {
> >
<div class="card-content"> <div class="card-content">
${this._errorMsg ${this._errorMsg
? html` <div class="error">${this._errorMsg}</div> ` ? html`<ha-alert alert-type="error">${this._errorMsg}</ha-alert>`
: ""} : ""}
${this._statusMsg ${this._statusMsg
? html` <div class="status">${this._statusMsg}</div> ` ? html`<ha-alert alert-type="success"
>${this._statusMsg}</ha-alert
>`
: ""} : ""}
<paper-input <ha-textfield
id="currentPassword" id="currentPassword"
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.profile.change_password.current_password" "ui.panel.profile.change_password.current_password"
)} )}
type="password" type="password"
.value=${this._currentPassword} .value=${this._currentPassword}
@value-changed=${this._currentPasswordChanged} @input=${this._currentPasswordChanged}
required required
></paper-input> ></ha-textfield>
${this._currentPassword ${this._currentPassword
? html` <paper-input ? html`<ha-textfield
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.profile.change_password.new_password" "ui.panel.profile.change_password.new_password"
)} )}
name="password" name="password"
type="password" type="password"
.value=${this._password} .value=${this._password}
@value-changed=${this._newPasswordChanged} @change=${this._newPasswordChanged}
required required
auto-validate auto-validate
></paper-input> ></ha-textfield>
<paper-input <ha-textfield
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.profile.change_password.confirm_new_password" "ui.panel.profile.change_password.confirm_new_password"
)} )}
name="passwordConfirm" name="passwordConfirm"
type="password" type="password"
.value=${this._passwordConfirm} .value=${this._passwordConfirm}
@value-changed=${this._newPasswordConfirmChanged} @input=${this._newPasswordConfirmChanged}
required required
auto-validate auto-validate
></paper-input>` ></ha-textfield>`
: ""} : ""}
</div> </div>
@@ -101,16 +104,16 @@ class HaChangePasswordCard extends LitElement {
`; `;
} }
private _currentPasswordChanged(ev: CustomEvent) { private _currentPasswordChanged(ev) {
this._currentPassword = ev.detail.value; this._currentPassword = ev.target.value;
} }
private _newPasswordChanged(ev: CustomEvent) { private _newPasswordChanged(ev) {
this._password = ev.detail.value; this._password = ev.target.value;
} }
private _newPasswordConfirmChanged(ev: CustomEvent) { private _newPasswordConfirmChanged(ev) {
this._passwordConfirm = ev.detail.value; this._passwordConfirm = ev.target.value;
} }
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
@@ -162,23 +165,21 @@ class HaChangePasswordCard extends LitElement {
this._statusMsg = this.hass.localize( this._statusMsg = this.hass.localize(
"ui.panel.profile.change_password.success" "ui.panel.profile.change_password.success"
); );
this._currentPassword = undefined; this._currentPassword = "";
this._password = undefined; this._password = "";
this._passwordConfirm = undefined; this._passwordConfirm = "";
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,
css` css`
.error { ha-textfield {
color: var(--error-color); margin-top: 8px;
} display: block;
.status {
color: var(--primary-color);
} }
#currentPassword { #currentPassword {
margin-top: -8px; margin-top: 0;
} }
`, `,
]; ];

View File

@@ -1,5 +1,6 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@polymer/paper-input/paper-input"; import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -14,14 +15,13 @@ import "../../components/ha-formfield";
import "../../components/ha-radio"; import "../../components/ha-radio";
import type { HaRadio } from "../../components/ha-radio"; import type { HaRadio } from "../../components/ha-radio";
import "../../components/ha-settings-row"; import "../../components/ha-settings-row";
import "../../components/ha-textfield";
import { import {
DEFAULT_PRIMARY_COLOR,
DEFAULT_ACCENT_COLOR, DEFAULT_ACCENT_COLOR,
DEFAULT_PRIMARY_COLOR,
} from "../../resources/ha-style"; } from "../../resources/ha-style";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url"; import { documentationUrl } from "../../util/documentation-url";
import "@material/mwc-select/mwc-select";
import "@material/mwc-list/mwc-list-item";
@customElement("ha-pick-theme-row") @customElement("ha-pick-theme-row")
export class HaPickThemeRow extends LitElement { export class HaPickThemeRow extends LitElement {
@@ -116,7 +116,7 @@ export class HaPickThemeRow extends LitElement {
</ha-formfield> </ha-formfield>
${curTheme === "default" ${curTheme === "default"
? html`<div class="color-pickers"> ? html`<div class="color-pickers">
<paper-input <ha-textfield
.value=${themeSettings?.primaryColor || .value=${themeSettings?.primaryColor ||
DEFAULT_PRIMARY_COLOR} DEFAULT_PRIMARY_COLOR}
type="color" type="color"
@@ -125,8 +125,8 @@ export class HaPickThemeRow extends LitElement {
)} )}
.name=${"primaryColor"} .name=${"primaryColor"}
@change=${this._handleColorChange} @change=${this._handleColorChange}
></paper-input> ></ha-textfield>
<paper-input <ha-textfield
.value=${themeSettings?.accentColor || DEFAULT_ACCENT_COLOR} .value=${themeSettings?.accentColor || DEFAULT_ACCENT_COLOR}
type="color" type="color"
.label=${this.hass.localize( .label=${this.hass.localize(
@@ -134,7 +134,7 @@ export class HaPickThemeRow extends LitElement {
)} )}
.name=${"accentColor"} .name=${"accentColor"}
@change=${this._handleColorChange} @change=${this._handleColorChange}
></paper-input> ></ha-textfield>
${themeSettings?.primaryColor || themeSettings?.accentColor ${themeSettings?.primaryColor || themeSettings?.accentColor
? html` <mwc-button @click=${this._resetColors}> ? html` <mwc-button @click=${this._resetColors}>
${this.hass.localize("ui.panel.profile.themes.reset")} ${this.hass.localize("ui.panel.profile.themes.reset")}
@@ -228,7 +228,8 @@ export class HaPickThemeRow extends LitElement {
align-items: center; align-items: center;
flex-grow: 1; flex-grow: 1;
} }
paper-input { ha-textfield {
--text-field-padding: 8px;
min-width: 75px; min-width: 75px;
flex-grow: 1; flex-grow: 1;
margin: 0 4px; margin: 0 4px;

View File

@@ -972,6 +972,13 @@
"triggered": "Triggered {name}", "triggered": "Triggered {name}",
"dismiss": "Dismiss" "dismiss": "Dismiss"
}, },
"newsletter": {
"newsletter": "Newsletter",
"email": "Email",
"validation": "A valid email address is required",
"subscribe": "Subscribe",
"thanks": "Thanks for subscribing!"
},
"sidebar": { "sidebar": {
"external_app_configuration": "App Configuration", "external_app_configuration": "App Configuration",
"sidebar_toggle": "Sidebar Toggle", "sidebar_toggle": "Sidebar Toggle",
@@ -2287,6 +2294,8 @@
"open_configuration_url_service": "Visit service", "open_configuration_url_service": "Visit service",
"download_diagnostics": "Download diagnostics", "download_diagnostics": "Download diagnostics",
"download_diagnostics_integration": "Download {integration} diagnostics", "download_diagnostics_integration": "Download {integration} diagnostics",
"delete_device": "Delete device",
"delete_device_integration": "Remove {integration} from device",
"type": { "type": {
"device_heading": "Device", "device_heading": "Device",
"device": "device", "device": "device",
@@ -4093,7 +4102,6 @@
"no_listeners": "This template does not listen for any events and will not update automatically.", "no_listeners": "This template does not listen for any events and will not update automatically.",
"listeners": "This template listens for the following state changed events:", "listeners": "This template listens for the following state changed events:",
"entity": "Entity", "entity": "Entity",
"context_data": "Context data for template",
"domain": "Domain" "domain": "Domain"
}, },
"statistics": { "statistics": {