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",
state: "loaded",
supports_options: false,
supports_remove_device: false,
supports_unload: true,
disabled_by: null,
pref_disable_new_entities: false,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ import "@material/mwc-list/mwc-list";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import { mdiCalendar } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import {
css,
CSSResultGroup,
@@ -19,6 +18,7 @@ import { computeRTLDirection } from "../common/util/compute_rtl";
import { HomeAssistant } from "../types";
import "./date-range-picker";
import "./ha-svg-icon";
import "./ha-textfield";
export interface DateRangePickerRanges {
[key: string]: [Date, Date];
@@ -61,7 +61,7 @@ export class HaDateRangePicker extends LitElement {
>
<div slot="input" class="date-range-inputs">
<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
<paper-input
<ha-textfield
.value=${formatDateTime(this.startDate, this.hass.locale)}
.label=${this.hass.localize(
"ui.components.date-range-picker.start_date"
@@ -69,16 +69,16 @@ export class HaDateRangePicker extends LitElement {
.disabled=${this.disabled}
@click=${this._handleInputClick}
readonly
></paper-input>
<paper-input
></ha-textfield>
<ha-textfield
.value=${formatDateTime(this.endDate, this.hass.locale)}
label=${this.hass.localize(
.label=${this.hass.localize(
"ui.components.date-range-picker.end_date"
)}
.disabled=${this.disabled}
@click=${this._handleInputClick}
readonly
></paper-input>
></ha-textfield>
</div>
${this.ranges
? html`<div
@@ -158,13 +158,13 @@ export class HaDateRangePicker extends LitElement {
border-top: 1px solid var(--divider-color);
}
paper-input {
ha-textfield {
display: inline-block;
max-width: 250px;
min-width: 200px;
}
paper-input:last-child {
ha-textfield:last-child {
margin-left: 8px;
}
@@ -176,7 +176,7 @@ export class HaDateRangePicker extends LitElement {
}
@media only screen and (max-width: 500px) {
paper-input {
ha-textfield {
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 { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-alert";
import "./ha-form-boolean";
import "./ha-form-constant";
import "./ha-form-grid";
import "./ha-form-float";
import "./ha-form-integer";
import "./ha-form-multi_select";
@@ -14,17 +22,18 @@ import "./ha-form-string";
import { HaFormElement, HaFormDataContainer, HaFormSchema } 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;
@customElement("ha-form")
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>;
@@ -64,7 +73,7 @@ export class HaForm extends LitElement implements HaFormElement {
}
}
protected render() {
protected render(): TemplateResult {
return html`
<div class="root">
${this.error && this.error.base
@@ -101,6 +110,9 @@ export class HaForm extends LitElement implements HaFormElement {
data: getValue(this.data, item),
label: this._computeLabel(item, this.data),
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();
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", {
value: { ...this.data, [schema.name]: ev.detail.value },
value: { ...this.data, ...newValue },
});
});
return root;

View File

@@ -11,7 +11,8 @@ export type HaFormSchema =
| HaFormSelectSchema
| HaFormMultiSelectSchema
| HaFormTimeSchema
| HaFormSelector;
| HaFormSelector
| HaFormGridSchema;
export interface HaFormBaseSchema {
name: string;
@@ -25,6 +26,12 @@ export interface HaFormBaseSchema {
};
}
export interface HaFormGridSchema extends HaFormBaseSchema {
type: "grid";
name: "";
schema: HaFormSchema[];
}
export interface HaFormSelector extends HaFormBaseSchema {
type?: never;
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 {
return css`
:host {
height: 56px;
display: flex;
}
ha-formfield {
width: 100%;
margin: 16px 0;
--mdc-typography-body2-font-size: 1em;
}
`;

View File

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

View File

@@ -69,10 +69,13 @@ export class HaTextSelector extends LitElement {
}
private _handleChange(ev) {
const value = ev.target.value;
let value = ev.target.value;
if (this.value === value) {
return;
}
if (value === "" && !this.required) {
value = undefined;
}
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 { customElement, property } from "lit/decorators";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { Selector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import type { Selector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "./ha-selector-action";
import "./ha-selector-addon";
import "./ha-selector-area";
@@ -19,6 +19,7 @@ import "./ha-selector-text";
import "./ha-selector-time";
import "./ha-selector-icon";
import "./ha-selector-media";
import "./ha-selector-theme";
@customElement("ha-selector")
export class HaSelector extends LitElement {

View File

@@ -43,7 +43,7 @@ class HaWebRtcPlayer extends LitElement {
private _remoteStream?: MediaStream;
protected render(): TemplateResult {
protected override render(): TemplateResult {
if (this._error) {
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();
this._cleanUp();
}
protected updated(changedProperties: PropertyValues<this>) {
protected override updated(changedProperties: PropertyValues<this>) {
if (!changedProperties.has("entityid")) {
return;
}

View File

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

View File

@@ -1,4 +1,5 @@
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 { caseInsensitiveStringCompare } from "../common/string/compare";
import { debounce } from "../common/util/debounce";
@@ -77,12 +78,26 @@ export const updateDeviceRegistryEntry = (
...updates,
});
export const fetchDeviceRegistry = (conn) =>
conn.sendMessagePromise({
export const removeConfigEntryFromDevice = (
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",
});
const subscribeDeviceRegistryUpdates = (conn, store) =>
const subscribeDeviceRegistryUpdates = (
conn: Connection,
store: Store<DeviceRegistryEntry[]>
) =>
conn.subscribeEvents(
debounce(
() =>

View File

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

View File

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

View File

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

View File

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

View File

@@ -30,7 +30,7 @@ import { HomeAssistant } from "../types";
import "./action-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")
class OnboardingIntegrations extends LitElement {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-input/paper-textarea";
import { html, LitElement } from "lit";
import "../../../../../components/ha-textfield";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-formfield";
@@ -26,14 +25,14 @@ export class HaWaitForTriggerAction
const { wait_for_trigger, continue_on_timeout, timeout } = this.action;
return html`
<paper-input
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.wait_for_trigger.timeout"
)}
.name=${"timeout"}
.value=${timeout}
@value-changed=${this._valueChanged}
></paper-input>
.value=${timeout || ""}
@change=${this._valueChanged}
></ha-textfield>
<br />
<ha-formfield
.label=${this.hass.localize(
@@ -63,6 +62,15 @@ export class HaWaitForTriggerAction
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
static get styles(): CSSResultGroup {
return css`
ha-textfield {
display: block;
margin-bottom: 24px;
}
`;
}
}
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 { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { WaitAction } from "../../../../../data/script";
import { HomeAssistant } from "../../../../../types";
import { ActionElement, handleChangeEvent } from "../ha-automation-action-row";
import type { HaFormSchema } from "../../../../../components/ha-form/types";
import type { WaitAction } from "../../../../../data/script";
import type { HomeAssistant } from "../../../../../types";
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")
export class HaWaitAction extends LitElement implements ActionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public action!: WaitAction;
@property({ attribute: false }) public action!: WaitAction;
public static get defaultConfig() {
return { wait_template: "" };
}
protected render() {
const { wait_template, timeout, continue_on_timeout } = this.action;
return html`
<paper-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.wait_template.wait_template"
)}
name="wait_template"
.value=${wait_template}
@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>
<ha-form
.hass=${this.hass}
.data=${this.action}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
></ha-form>
`;
}
private _continueChanged(ev) {
fireEvent(this, "value-changed", {
value: { ...this.action, continue_on_timeout: ev.target.checked },
});
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
private _computeLabelCallback = (schema: HaFormSchema): string =>
this.hass.localize(
`ui.panel.config.automation.editor.actions.type.wait_template.${
schema.name === "continue_on_timeout" ? "continue_timeout" : schema.name
}`
);
}
declare global {

View File

@@ -147,7 +147,7 @@ export default class HaAutomationConditionEditor extends LitElement {
haStyle,
css`
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 { forDictStruct } from "../../structs";
import type { ConditionElement } from "../ha-automation-condition-row";
import "../../../../../components/ha-form/ha-form";
const stateConditionStruct = object({
condition: literal("state"),

View File

@@ -7,6 +7,7 @@ import type { HomeAssistant } from "../../../../../types";
import type { ConditionElement } from "../ha-automation-condition-row";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import type { HaFormSchema } from "../../../../../components/ha-form/types";
import "../../../../../components/ha-form/ha-form";
@customElement("ha-automation-condition-sun")
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 { LocalizeFunc } from "../../../../../common/translations/localize";
import type { HaFormSchema } from "../../../../../components/ha-form/types";
import "../../../../../components/ha-form/ha-form";
const DAYS = {
mon: 1,

View File

@@ -9,7 +9,6 @@ import {
} from "@mdi/js";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-input/paper-textarea";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
@@ -201,7 +200,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
${this._config
? html`
${this.narrow
? html` <span slot="header">${this._config?.alias}</span> `
? html`<span slot="header">${this._config?.alias}</span>`
: ""}
<div
class="content ${classMap({
@@ -210,27 +209,31 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
@subscribe-automation-config=${this._subscribeAutomationConfig}
>
${this._errors
? html` <div class="errors">${this._errors}</div> `
? html`<div class="errors">${this._errors}</div>`
: ""}
${this._mode === "gui"
? html`
${"use_blueprint" in this._config
? html`<blueprint-automation-editor
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
.stateObj=${stateObj}
.config=${this._config}
@value-changed=${this._valueChanged}
></blueprint-automation-editor>`
: html`<manual-automation-editor
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
.stateObj=${stateObj}
.config=${this._config}
@value-changed=${this._valueChanged}
></manual-automation-editor>`}
? html`
<blueprint-automation-editor
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
.stateObj=${stateObj}
.config=${this._config}
@value-changed=${this._valueChanged}
></blueprint-automation-editor>
`
: html`
<manual-automation-editor
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
.stateObj=${stateObj}
.config=${this._config}
@value-changed=${this._valueChanged}
></manual-automation-editor>
`}
`
: this._mode === "yaml"
? html`

View File

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

View File

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

View File

@@ -14,6 +14,7 @@ import { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-card";
import "../../../../components/ha-alert";
import "../../../../components/ha-textfield";
import "../../../../components/ha-icon-button";
import type { Trigger } from "../../../../data/automation";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
@@ -200,14 +201,14 @@ export default class HaAutomationTriggerRow extends LitElement {
${showId
? html`
<paper-input
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.id"
)}
.value=${this.trigger.id}
@value-changed=${this._idChanged}
.value=${this.trigger.id || ""}
@change=${this._idChanged}
>
</paper-input>
</ha-textfield>
`
: ""}
<div @ui-mode-not-available=${this._handleUiModeNotAvailable}>
@@ -287,7 +288,7 @@ export default class HaAutomationTriggerRow extends LitElement {
}
private _idChanged(ev: CustomEvent) {
const newId = ev.detail.value;
const newId = (ev.target as any).value;
if (newId === (this.trigger.id ?? "")) {
return;
}
@@ -333,7 +334,11 @@ export default class HaAutomationTriggerRow extends LitElement {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
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 { html, LitElement } from "lit";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-textfield";
import "../../../../../components/ha-yaml-editor";
import "../../../../../components/user/ha-users-picker";
import { EventTrigger } from "../../../../../data/automation";
@@ -24,14 +24,14 @@ export class HaEventTrigger extends LitElement implements TriggerElement {
protected render() {
const { event_type, event_data, context } = this.trigger;
return html`
<paper-input
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.event.event_type"
)}
name="event_type"
.value=${event_type}
@value-changed=${this._valueChanged}
></paper-input>
@change=${this._valueChanged}
></ha-textfield>
<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
@@ -97,6 +97,14 @@ export class HaEventTrigger extends LitElement implements TriggerElement {
value,
});
}
static get styles(): CSSResultGroup {
return css`
ha-textfield {
display: block;
}
`;
}
}
declare global {

View File

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

View File

@@ -27,6 +27,7 @@ import {
computeDeviceName,
DeviceRegistryEntry,
updateDeviceRegistryEntry,
removeConfigEntryFromDevice,
} from "../../../data/device_registry";
import {
fetchDiagnosticHandler,
@@ -95,6 +96,8 @@ export class HaConfigDevicePage extends LitElement {
| number
| (TemplateResult | string)[];
@state() private _deleteButtons?: (TemplateResult | string)[];
private _device = memoizeOne(
(
deviceId: string,
@@ -186,10 +189,11 @@ export class HaConfigDevicePage extends LitElement {
changedProps.has("entries")
) {
this._diagnosticDownloadLinks = undefined;
this._deleteButtons = undefined;
}
if (
this._diagnosticDownloadLinks ||
(this._diagnosticDownloadLinks && this._deleteButtons) ||
!this.devices ||
!this.deviceId ||
!this.entries
@@ -198,7 +202,9 @@ export class HaConfigDevicePage extends LitElement {
}
this._diagnosticDownloadLinks = Math.random();
this._deleteButtons = []; // To prevent re-rendering if no delete buttons
this._renderDiagnosticButtons(this._diagnosticDownloadLinks);
this._renderDeleteButtons();
}
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) {
super.firstUpdated(changedProps);
loadDeviceRegistryDetailDialog();
@@ -375,6 +430,9 @@ export class HaConfigDevicePage extends LitElement {
if (Array.isArray(this._diagnosticDownloadLinks)) {
deviceActions.push(...this._diagnosticDownloadLinks);
}
if (this._deleteButtons) {
deviceActions.push(...this._deleteButtons);
}
return html`
<hass-tabs-subpage

View File

@@ -369,7 +369,7 @@ export class HaIntegrationCard extends LitElement {
</a>`
: ""}
${!item.disabled_by &&
item.state === "loaded" &&
(item.state === "loaded" || item.state === "setup_retry") &&
item.supports_unload &&
item.source !== "system"
? 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 { customElement, property, state } from "lit/decorators";
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 "../../../../../components/buttons/ha-call-api-button";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-card";
import "../../../../../components/ha-circular-progress";
import "../../../../../components/ha-icon";
@@ -18,30 +20,28 @@ import {
fetchDeviceRegistry,
subscribeDeviceRegistry,
} from "../../../../../data/device_registry";
import {
migrateZwave,
ZWaveJsMigrationData,
fetchZwaveNetworkStatus as fetchZwaveJsNetworkStatus,
fetchZwaveNodeStatus,
getZwaveJsIdentifiersFromDevice,
subscribeZwaveNodeReady,
} from "../../../../../data/zwave_js";
import {
fetchMigrationConfig,
fetchNetworkStatus,
startZwaveJsConfigFlow,
ZWaveMigrationConfig,
ZWaveNetworkStatus,
ZWAVE_NETWORK_STATE_STOPPED,
fetchNetworkStatus,
} 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 { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
import "../../../../../layouts/hass-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import "../../../ha-config-section";
import { computeStateDomain } from "../../../../../common/entity/compute_state_domain";
import "../../../../../components/ha-alert";
@customElement("zwave-migration")
export class ZwaveMigration extends LitElement {
@@ -155,7 +155,7 @@ export class ZwaveMigration extends LitElement {
.filter(
(entityState) =>
computeStateDomain(entityState) === "zwave" &&
entityState.state !== "ready"
!["ready", "sleeping"].includes(entityState.state)
)
.map(
(entityState) =>
@@ -430,6 +430,10 @@ export class ZwaveMigration extends LitElement {
const nodesNotReady = (await Promise.all(nodeStatePromisses)).filter(
(node) => !node.ready
);
// eslint-disable-next-line no-console
console.log("waiting for nodes to be ready", nodesNotReady);
this._getMigrationData();
if (nodesNotReady.length === 0) {
this._waitingOnDevices = [];
@@ -445,10 +449,19 @@ export class ZwaveMigration extends LitElement {
}
)
);
const deviceReg = await fetchDeviceRegistry(this.hass);
this._waitingOnDevices = deviceReg
.map((device) => getZwaveJsIdentifiersFromDevice(device))
.filter(Boolean);
const deviceReg: DeviceRegistryEntry[] = await fetchDeviceRegistry(
this.hass.connection
);
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() {

View File

@@ -28,12 +28,13 @@ import { navigate } from "../../../common/navigate";
import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/device/ha-device-picker";
import "../../../components/entity/ha-entities-picker";
import "../../../components/ha-area-picker";
import "../../../components/ha-card";
import "../../../components/ha-fab";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-picker";
import "../../../components/ha-area-picker";
import "../../../components/ha-svg-icon";
import "../../../components/ha-textfield";
import {
computeDeviceName,
DeviceRegistryEntry,
@@ -288,14 +289,14 @@ export class HaSceneEditor extends SubscribeMixin(
</div>
<ha-card>
<div class="card-content">
<paper-input
<ha-textfield
.value=${this._config.name}
.name=${"name"}
@value-changed=${this._valueChanged}
label=${this.hass.localize(
@change=${this._valueChanged}
.label=${this.hass.localize(
"ui.panel.config.scene.editor.name"
)}
></paper-input>
></ha-textfield>
<ha-icon-picker
.label=${this.hass.localize(
"ui.panel.config.scene.editor.icon"
@@ -701,14 +702,14 @@ export class HaSceneEditor extends SubscribeMixin(
this._dirty = true;
}
private _valueChanged(ev: CustomEvent) {
private _valueChanged(ev: Event) {
ev.stopPropagation();
const target = ev.target as any;
const name = target.name;
if (!name) {
return;
}
let newVal = ev.detail.value;
let newVal = (ev as CustomEvent).detail?.value ?? target.value;
if (target.type === "number") {
newVal = Number(newVal);
}
@@ -990,6 +991,9 @@ export class HaSceneEditor extends SubscribeMixin(
display: block;
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 { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
@@ -6,6 +5,7 @@ import "../../../components/ha-blueprint-picker";
import "../../../components/ha-card";
import "../../../components/ha-circular-progress";
import "../../../components/ha-markdown";
import "../../../components/ha-textfield";
import "../../../components/ha-selector/ha-selector";
import "../../../components/ha-settings-row";
@@ -101,15 +101,14 @@ export class HaBlueprintScriptEditor extends LitElement {
value?.default}
@value-changed=${this._inputChanged}
></ha-selector>`
: html`<paper-input
: html`<ha-textfield
.key=${key}
required
.value=${(this.config.use_blueprint.input &&
this.config.use_blueprint.input[key]) ??
value?.default}
@value-changed=${this._inputChanged}
no-label-float
></paper-input>`}
@change=${this._inputChanged}
></ha-textfield>`}
</ha-settings-row>`
)
: html`<p class="padding">
@@ -145,7 +144,7 @@ export class HaBlueprintScriptEditor extends LitElement {
ev.stopPropagation();
const target = ev.target as any;
const key = target.key;
const value = ev.detail.value;
const value = ev.detail?.value ?? target.value;
if (
(this.config.use_blueprint.input &&
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 { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { load } from "js-yaml";
import { debounce } from "../../../common/util/debounce";
import "../../../components/ha-circular-progress";
import "../../../components/ha-code-editor";
@@ -52,10 +51,6 @@ class HaPanelDevTemplate extends LitElement {
private _template = "";
private _context_data = "";
private _context_variables?;
private _inited = false;
public connectedCallback() {
@@ -70,14 +65,8 @@ class HaPanelDevTemplate extends LitElement {
}
protected firstUpdated() {
if (localStorage) {
if (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;
}
if (localStorage && localStorage["panel-dev-template-template"]) {
this._template = localStorage["panel-dev-template-template"];
} else {
this._template = DEMO_TEMPLATE;
}
@@ -152,17 +141,6 @@ class HaPanelDevTemplate extends LitElement {
"ui.panel.developer-tools.tabs.templates.reset"
)}
</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 class="render-pane">
@@ -324,15 +302,6 @@ class HaPanelDevTemplate extends LitElement {
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) {
this._template = ev.detail.value;
if (this._error) {
@@ -345,9 +314,6 @@ class HaPanelDevTemplate extends LitElement {
this._rendering = true;
await this._unsubscribeTemplate();
try {
if (this._context_data && !this._context_variables) {
this._context_variables = load(this._context_data);
}
this._unsubRenderTemplate = subscribeRenderTemplate(
this.hass.connection,
(result) => {
@@ -356,7 +322,6 @@ class HaPanelDevTemplate extends LitElement {
},
{
template: this._template,
variables: this._context_variables,
timeout: 3,
}
);
@@ -395,16 +360,12 @@ class HaPanelDevTemplate extends LitElement {
if (!this._inited) {
return;
}
localStorage["panel-dev-template-context-data"] = this._context_data;
localStorage["panel-dev-template-template"] = this._template;
}
private _restoreDemo() {
this._context_data = "";
this._context_variables = undefined;
this._template = DEMO_TEMPLATE;
this._subscribeTemplate();
delete localStorage["panel-dev-template-context-data"];
delete localStorage["panel-dev-template-template"];
}
}

View File

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

View File

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

View File

@@ -39,7 +39,7 @@ export class HuiThemeSelectEditor extends LitElement {
.sort()
.map(
(theme) =>
html` <mwc-list-item .value=${theme}>${theme}</mwc-list-item> `
html`<mwc-list-item .value=${theme}>${theme}</mwc-list-item>`
)}
</mwc-select>
`;
@@ -57,7 +57,7 @@ export class HuiThemeSelectEditor extends LitElement {
if (!this.hass || ev.target.value === "") {
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 });
}
}

View File

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

View File

@@ -1,22 +1,18 @@
import "@polymer/paper-input/paper-input";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import "../../../../components/ha-form/ha-form";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
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 { computeDomain } from "../../../../common/entity/compute_domain";
import { domainIcon } from "../../../../common/entity/domain_icon";
import "../../../../components/entity/ha-entity-attribute-picker";
import "../../../../components/ha-icon-picker";
import { HomeAssistant } from "../../../../types";
import { EntityCardConfig } from "../../cards/types";
import "../../components/hui-action-editor";
import "../../components/hui-entity-editor";
import "../../components/hui-theme-select-editor";
import type { HaFormSchema } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type { EntityCardConfig } from "../../cards/types";
import { headerFooterConfigStructs } from "../../header-footer/structs";
import { LovelaceCardEditor } from "../../types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditorTarget, EntitiesEditorEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@@ -46,174 +42,82 @@ export class HuiEntityCardEditor
this._config = config;
}
get _entity(): string {
return this._config!.entity || "";
}
private _schema = memoizeOne(
(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 || "";
}
get _icon(): string {
return this._config!.icon || "";
}
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 || "";
}
{
name: "attribute",
selector: { attribute: { entity_id: entity } },
},
{ name: "unit", selector: { text: {} } },
{ name: "theme", selector: { theme: {} } },
{ name: "state_color", selector: { boolean: {} } },
],
},
]
);
protected render(): TemplateResult {
if (!this.hass || !this._config) {
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`
<div class="card-config">
<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}
.value=${this._entity}
.configValue=${"entity"}
@change=${this._valueChanged}
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}
></paper-input>
<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>
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.currentTarget! as EditorTarget;
if (
this[`_${target.configValue}`] === target.value ||
this[`_${target.configValue}`] === target.config
) {
return;
}
if (target.configValue) {
if (target.value === "") {
this._config = { ...this._config };
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 });
private _valueChanged(ev: CustomEvent): void {
const config = ev.detail.value;
Object.keys(config).forEach((k) => config[k] === "" && delete config[k]);
fireEvent(this, "config-changed", { config });
}
static get styles(): CSSResultGroup {
return configElementStyle;
}
private _computeLabelCallback = (schema: HaFormSchema) => {
if (schema.name === "entity") {
return `${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.entity"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.required"
)})`;
}
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);
};
}
declare global {

View File

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

View File

@@ -20,6 +20,7 @@ import {
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { until } from "lit/directives/until";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_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 { supportsFeature } from "../../common/entity/supports-feature";
import "../../components/ha-button-menu";
import "../../components/ha-circular-progress";
import "../../components/ha-icon-button";
import { UNAVAILABLE_STATES } from "../../data/entity";
import {
@@ -43,6 +45,7 @@ import {
SUPPORT_PLAY,
SUPPORT_STOP,
} from "../../data/media-player";
import { ResolvedMediaSource } from "../../data/media_source";
import type { HomeAssistant } from "../../types";
import "../lovelace/components/hui-marquee";
import { BrowserMediaPlayer } from "./browser-media-player";
@@ -54,7 +57,7 @@ declare global {
}
@customElement("ha-bar-media-player")
class BarMediaPlayer extends LitElement {
export class BarMediaPlayer extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entityId!: string;
@@ -68,6 +71,8 @@ class BarMediaPlayer extends LitElement {
@state() private _marqueeActive = false;
@state() private _newMediaExpected = false;
@state() private _browserPlayer?: BrowserMediaPlayer;
private _progressInterval?: number;
@@ -98,32 +103,54 @@ class BarMediaPlayer extends LitElement {
clearInterval(this._progressInterval);
this._progressInterval = undefined;
}
if (this._browserPlayer) {
this._browserPlayer.stop();
this._browserPlayer = undefined;
}
this._tearDownBrowserPlayer();
}
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) {
throw Error("Only browser supported");
}
if (this._browserPlayer) {
this._browserPlayer.stop();
}
this._browserPlayer = new BrowserMediaPlayer(this.hass, item, () =>
this.requestUpdate("_browserPlayer")
this._tearDownBrowserPlayer();
this._browserPlayer = new BrowserMediaPlayer(
this.hass,
item,
resolved,
() => this.requestUpdate("_browserPlayer")
);
await this._browserPlayer.initialize();
this._newMediaExpected = false;
}
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 stateObj = this._stateObj;
const controls = !stateObj
? undefined
: !this.narrow
if (!stateObj) {
return this._renderChoosePlayer(stateObj);
}
const controls = !this.narrow
? computeMediaControls(stateObj)
: (stateObj.state === "playing" &&
(supportsFeature(stateObj, SUPPORT_PAUSE) ||
@@ -152,16 +179,14 @@ class BarMediaPlayer extends LitElement {
},
]
: [{}];
const mediaDescription = stateObj ? computeMediaDescription(stateObj) : "";
const mediaDuration = formatMediaTime(stateObj?.attributes.media_duration);
const mediaDescription = computeMediaDescription(stateObj);
const mediaDuration = formatMediaTime(stateObj.attributes.media_duration);
const mediaTitleClean = cleanupMediaTitle(
stateObj?.attributes.media_title || ""
stateObj.attributes.media_title || ""
);
const mediaArt = stateObj
? stateObj.attributes.entity_picture_local ||
stateObj.attributes.entity_picture
: undefined;
const mediaArt =
stateObj.attributes.entity_picture_local ||
stateObj.attributes.entity_picture;
return html`
<div
@@ -177,7 +202,10 @@ class BarMediaPlayer extends LitElement {
<hui-marquee
.text=${mediaTitleClean ||
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}
@mouseover=${this._marqueeMouseOver}
@mouseleave=${this._marqueeMouseLeave}
@@ -188,97 +216,124 @@ class BarMediaPlayer extends LitElement {
</div>
</div>
<div class="controls-progress">
<div class="controls">
${controls === undefined
? ""
: controls.map(
(control) => html`
<ha-icon-button
.label=${this.hass.localize(
`ui.card.media_player.${control.action}`
)}
.path=${control.icon}
action=${control.action}
@click=${this._handleClick}
>
</ha-icon-button>
`
)}
</div>
${this.narrow
? html`<mwc-linear-progress></mwc-linear-progress>`
${this._browserPlayer?.buffering
? html` <ha-circular-progress active></ha-circular-progress> `
: html`
<div class="progress">
<div id="CurrentProgress"></div>
<mwc-linear-progress wide></mwc-linear-progress>
<div>${mediaDuration}</div>
<div class="controls">
${controls === undefined
? ""
: controls.map(
(control) => html`
<ha-icon-button
.label=${this.hass.localize(
`ui.card.media_player.${control.action}`
)}
.path=${control.icon}
action=${control.action}
@click=${this._handleClick}
>
</ha-icon-button>
`
)}
</div>
${stateObj.attributes.media_duration === Infinity
? html``
: this.narrow
? html`<mwc-linear-progress></mwc-linear-progress>`
: html`
<div class="progress">
<div id="CurrentProgress"></div>
<mwc-linear-progress wide></mwc-linear-progress>
<div>${mediaDuration}</div>
</div>
`}
`}
</div>
<div class="choose-player ${isBrowser ? "browser" : ""}">
<ha-button-menu corner="BOTTOM_START">
${this.narrow
? html`
<ha-icon-button
slot="trigger"
.path=${isBrowser
? mdiMonitor
: domainIcon(computeDomain(this.entityId), stateObj)}
></ha-icon-button>
`
: html`
<mwc-button
slot="trigger"
.label=${this.narrow
? ""
: `${stateObj ? computeStateName(stateObj) : this.entityId}
${this._renderChoosePlayer(stateObj)}
`;
}
private _renderChoosePlayer(stateObj: MediaPlayerEntity | undefined) {
const isBrowser = this.entityId === BROWSER_PLAYER;
return html`
<div class="choose-player ${isBrowser ? "browser" : ""}">
<ha-button-menu corner="BOTTOM_START">
${
this.narrow
? html`
<ha-icon-button
slot="trigger"
.path=${isBrowser
? mdiMonitor
: domainIcon(computeDomain(this.entityId), stateObj)}
></ha-icon-button>
`
: html`
<mwc-button
slot="trigger"
.label=${this.narrow
? ""
: `${
stateObj
? computeStateName(stateObj)
: this.entityId
}
`}
>
<ha-svg-icon
slot="icon"
.path=${isBrowser
? mdiMonitor
: domainIcon(computeDomain(this.entityId), stateObj)}
></ha-svg-icon>
<ha-svg-icon
slot="trailingIcon"
.path=${mdiChevronDown}
></ha-svg-icon>
</mwc-button>
`
}
<mwc-list-item
.player=${BROWSER_PLAYER}
?selected=${isBrowser}
@click=${this._selectPlayer}
>
${this.hass.localize("ui.components.media-browser.web-browser")}
</mwc-list-item>
${this._mediaPlayerEntities.map(
(source) => html`
<mwc-list-item
?selected=${source.entity_id === this.entityId}
.disabled=${UNAVAILABLE_STATES.includes(source.state)}
.player=${source.entity_id}
@click=${this._selectPlayer}
>
<ha-svg-icon
slot="icon"
.path=${isBrowser
? mdiMonitor
: domainIcon(computeDomain(this.entityId), stateObj)}
></ha-svg-icon>
<ha-svg-icon
slot="trailingIcon"
.path=${mdiChevronDown}
></ha-svg-icon>
</mwc-button>
`}
<mwc-list-item
.player=${BROWSER_PLAYER}
?selected=${isBrowser}
@click=${this._selectPlayer}
>
${this.hass.localize("ui.components.media-browser.web-browser")}
</mwc-list-item>
${this._mediaPlayerEntities.map(
(source) => html`
<mwc-list-item
?selected=${source.entity_id === this.entityId}
.disabled=${UNAVAILABLE_STATES.includes(source.state)}
.player=${source.entity_id}
@click=${this._selectPlayer}
>
${computeStateName(source)}
</mwc-list-item>
`
)}
</ha-button-menu>
${computeStateName(source)}
</mwc-list-item>
`
)}
</ha-button-menu>
</div>
</div>
`;
}
public willUpdate(changedProps: PropertyValues) {
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 (
changedProps.has("entityId") &&
this.entityId !== BROWSER_PLAYER &&
this._browserPlayer
!oldHass ||
oldHass.states[this.entityId] !== this.hass.states[this.entityId]
) {
this._browserPlayer?.stop();
this._browserPlayer = undefined;
this._newMediaExpected = false;
}
}
@@ -327,6 +382,13 @@ class BarMediaPlayer extends LitElement {
return this.hass!.states[this.entityId] as MediaPlayerEntity | undefined;
}
private _tearDownBrowserPlayer() {
if (this._browserPlayer) {
this._browserPlayer.remove();
this._browserPlayer = undefined;
}
}
private _openMoreInfo() {
if (this._browserPlayer) {
return;

View File

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

View File

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

View File

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

View File

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

View File

@@ -972,6 +972,13 @@
"triggered": "Triggered {name}",
"dismiss": "Dismiss"
},
"newsletter": {
"newsletter": "Newsletter",
"email": "Email",
"validation": "A valid email address is required",
"subscribe": "Subscribe",
"thanks": "Thanks for subscribing!"
},
"sidebar": {
"external_app_configuration": "App Configuration",
"sidebar_toggle": "Sidebar Toggle",
@@ -2287,6 +2294,8 @@
"open_configuration_url_service": "Visit service",
"download_diagnostics": "Download diagnostics",
"download_diagnostics_integration": "Download {integration} diagnostics",
"delete_device": "Delete device",
"delete_device_integration": "Remove {integration} from device",
"type": {
"device_heading": "Device",
"device": "device",
@@ -4093,7 +4102,6 @@
"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:",
"entity": "Entity",
"context_data": "Context data for template",
"domain": "Domain"
},
"statistics": {