Merge pull request #10578 from home-assistant/dev

This commit is contained in:
Bram Kragten 2021-11-08 18:54:44 +01:00 committed by GitHub
commit 72b9f8636d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 612 additions and 325 deletions

View File

@ -165,6 +165,7 @@ module.exports.config = {
cast({ isProdBuild, latestBuild }) { cast({ isProdBuild, latestBuild }) {
const entry = { const entry = {
launcher: path.resolve(paths.cast_dir, "src/launcher/entrypoint.ts"), launcher: path.resolve(paths.cast_dir, "src/launcher/entrypoint.ts"),
media: path.resolve(paths.cast_dir, "src/media/entrypoint.ts"),
}; };
if (latestBuild) { if (latestBuild) {

View File

@ -154,6 +154,15 @@ gulp.task("gen-index-cast-dev", (done) => {
contentReceiver contentReceiver
); );
const contentMedia = renderCastTemplate("media", {
latestMediaJS: "/frontend_latest/media.js",
es5MediaJS: "/frontend_es5/media.js",
});
fs.outputFileSync(
path.resolve(paths.cast_output_root, "media.html"),
contentMedia
);
const contentFAQ = renderCastTemplate("launcher-faq", { const contentFAQ = renderCastTemplate("launcher-faq", {
latestLauncherJS: "/frontend_latest/launcher.js", latestLauncherJS: "/frontend_latest/launcher.js",
es5LauncherJS: "/frontend_es5/launcher.js", es5LauncherJS: "/frontend_es5/launcher.js",
@ -192,6 +201,15 @@ gulp.task("gen-index-cast-prod", (done) => {
contentReceiver contentReceiver
); );
const contentMedia = renderCastTemplate("media", {
latestMediaJS: latestManifest["media.js"],
es5MediaJS: es5Manifest["media.js"],
});
fs.outputFileSync(
path.resolve(paths.cast_output_root, "media.html"),
contentMedia
);
const contentFAQ = renderCastTemplate("launcher-faq", { const contentFAQ = renderCastTemplate("launcher-faq", {
latestLauncherJS: latestManifest["launcher.js"], latestLauncherJS: latestManifest["launcher.js"],
es5LauncherJS: es5Manifest["launcher.js"], es5LauncherJS: es5Manifest["launcher.js"],

View File

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html>
<head>
<script src="//www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js"></script>
<style>
body {
--logo-image: url('https://www.home-assistant.io/images/home-assistant-logo.svg');
--logo-repeat: no-repeat;
--playback-logo-image: url('https://www.home-assistant.io/images/home-assistant-logo.svg');
--theme-hue: 200;
--progress-color: #03a9f4;
--splash-image: url('https://home-assistant.io/images/cast/splash.png');
--splash-size: cover;
}
</style>
<script>
var _gaq=[['_setAccount','UA-57927901-10'],['_trackPageview']];
(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
g.src=('https:'==location.protocol?'//ssl':'//www')+'.google-analytics.com/ga.js';
s.parentNode.insertBefore(g,s)}(document,'script'));
</script>
</head>
<body>
<%= renderTemplate('_js_base') %>
<cast-media-player></cast-media-player>
<script>
import("<%= latestMediaJS %>");
window.latestJS = true;
</script>
<script>
if (!window.latestJS) {
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5MediaJS %>");
};
<% } else { %>
_ls("<%= es5MediaJS %>");
<% } %>
}
</script>
</body>
</html>

View File

@ -0,0 +1,25 @@
import { CastReceiverContext } from "chromecast-caf-receiver/cast.framework";
const castContext =
cast.framework.CastContext.getInstance() as unknown as CastReceiverContext;
const playerManager = castContext.getPlayerManager();
playerManager.setMessageInterceptor(
cast.framework.messages.MessageType.LOAD,
(loadRequestData) => {
const media = loadRequestData.media;
// Special handling if it came from Google Assistant
if (media.entity) {
media.contentId = media.entity;
media.streamType = cast.framework.messages.StreamType.LIVE;
media.contentType = "application/vnd.apple.mpegurl";
// @ts-ignore
media.hlsVideoSegmentFormat =
cast.framework.messages.HlsVideoSegmentFormat.FMP4;
}
return loadRequestData;
}
);
castContext.start();

View File

@ -16,11 +16,9 @@ declare global {
} }
} }
const MAX_FILE_SIZE = 1 * 1024 * 1024 * 1024; // 1GB
@customElement("hassio-upload-backup") @customElement("hassio-upload-backup")
export class HassioUploadBackup extends LitElement { export class HassioUploadBackup extends LitElement {
public hass!: HomeAssistant; public hass?: HomeAssistant;
@state() public value: string | null = null; @state() public value: string | null = null;
@ -43,20 +41,6 @@ export class HassioUploadBackup extends LitElement {
private async _uploadFile(ev) { private async _uploadFile(ev) {
const file = ev.detail.files[0]; const file = ev.detail.files[0];
if (file.size > MAX_FILE_SIZE) {
showAlertDialog(this, {
title: "Backup file is too big",
text: html`The maximum allowed filesize is 1GB.<br />
<a
href="https://www.home-assistant.io/hassio/haos_common_tasks/#restoring-a-backup-on-a-new-install"
target="_blank"
>Have a look here on how to restore it.</a
>`,
confirmText: "ok",
});
return;
}
if (!["application/x-tar"].includes(file.type)) { if (!["application/x-tar"].includes(file.type)) {
showAlertDialog(this, { showAlertDialog(this, {
title: "Unsupported file format", title: "Unsupported file format",

View File

@ -15,7 +15,7 @@ export class DialogHassioBackupUpload
extends LitElement extends LitElement
implements HassDialog<HassioBackupUploadDialogParams> implements HassDialog<HassioBackupUploadDialogParams>
{ {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@state() private _params?: HassioBackupUploadDialogParams; @state() private _params?: HassioBackupUploadDialogParams;
@ -54,7 +54,7 @@ export class DialogHassioBackupUpload
<ha-header-bar> <ha-header-bar>
<span slot="title"> Upload backup </span> <span slot="title"> Upload backup </span>
<ha-icon-button <ha-icon-button
.label=${this.hass.localize("common.close")} .label=${this.hass?.localize("common.close") || "close"}
.path=${mdiClose} .path=${mdiClose}
slot="actionItems" slot="actionItems"
dialogAction="cancel" dialogAction="cancel"

View File

@ -35,7 +35,7 @@ class HassioBackupDialog
extends LitElement extends LitElement
implements HassDialog<HassioBackupDialogParams> implements HassDialog<HassioBackupDialogParams>
{ {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@state() private _error?: string; @state() private _error?: string;
@ -77,7 +77,7 @@ class HassioBackupDialog
<ha-header-bar> <ha-header-bar>
<span slot="title">${this._backup.name}</span> <span slot="title">${this._backup.name}</span>
<ha-icon-button <ha-icon-button
.label=${this.hass.localize("common.close")} .label=${this.hass?.localize("common.close") || "close"}
.path=${mdiClose} .path=${mdiClose}
slot="actionItems" slot="actionItems"
dialogAction="cancel" dialogAction="cancel"
@ -114,7 +114,7 @@ class HassioBackupDialog
@closed=${stopPropagation} @closed=${stopPropagation}
> >
<ha-icon-button <ha-icon-button
.label=${this.hass.localize("common.menu")} .label=${this.hass!.localize("common.menu")}
.path=${mdiDotsVertical} .path=${mdiDotsVertical}
slot="trigger" slot="trigger"
></ha-icon-button> ></ha-icon-button>
@ -192,25 +192,23 @@ class HassioBackupDialog
} }
if (!this._dialogParams?.onboarding) { if (!this._dialogParams?.onboarding) {
this.hass this.hass!.callApi(
.callApi( "POST",
"POST",
`hassio/${ `hassio/${
atLeastVersion(this.hass.config.version, 2021, 9) atLeastVersion(this.hass!.config.version, 2021, 9)
? "backups" ? "backups"
: "snapshots" : "snapshots"
}/${this._backup!.slug}/restore/partial`, }/${this._backup!.slug}/restore/partial`,
backupDetails backupDetails
) ).then(
.then( () => {
() => { this.closeDialog();
this.closeDialog(); },
}, (error) => {
(error) => { this._error = error.body.message;
this._error = error.body.message; }
} );
);
} else { } else {
fireEvent(this, "restoring"); fireEvent(this, "restoring");
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, { fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, {
@ -244,24 +242,22 @@ class HassioBackupDialog
} }
if (!this._dialogParams?.onboarding) { if (!this._dialogParams?.onboarding) {
this.hass this.hass!.callApi(
.callApi( "POST",
"POST", `hassio/${
`hassio/${ atLeastVersion(this.hass!.config.version, 2021, 9)
atLeastVersion(this.hass.config.version, 2021, 9) ? "backups"
? "backups" : "snapshots"
: "snapshots" }/${this._backup!.slug}/restore/full`,
}/${this._backup!.slug}/restore/full`, backupDetails
backupDetails ).then(
) () => {
.then( this.closeDialog();
() => { },
this.closeDialog(); (error) => {
}, this._error = error.body.message;
(error) => { }
this._error = error.body.message; );
}
);
} else { } else {
fireEvent(this, "restoring"); fireEvent(this, "restoring");
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/full`, { fetch(`/api/hassio/backups/${this._backup!.slug}/restore/full`, {
@ -283,36 +279,33 @@ class HassioBackupDialog
return; return;
} }
this.hass this.hass!.callApi(
atLeastVersion(this.hass!.config.version, 2021, 9) ? "DELETE" : "POST",
.callApi( `hassio/${
atLeastVersion(this.hass.config.version, 2021, 9) ? "DELETE" : "POST", atLeastVersion(this.hass!.config.version, 2021, 9)
`hassio/${ ? `backups/${this._backup!.slug}`
atLeastVersion(this.hass.config.version, 2021, 9) : `snapshots/${this._backup!.slug}/remove`
? `backups/${this._backup!.slug}` }`
: `snapshots/${this._backup!.slug}/remove` ).then(
}` () => {
) if (this._dialogParams!.onDelete) {
.then( this._dialogParams!.onDelete();
() => {
if (this._dialogParams!.onDelete) {
this._dialogParams!.onDelete();
}
this.closeDialog();
},
(error) => {
this._error = error.body.message;
} }
); this.closeDialog();
},
(error) => {
this._error = error.body.message;
}
);
} }
private async _downloadClicked() { private async _downloadClicked() {
let signedPath: { path: string }; let signedPath: { path: string };
try { try {
signedPath = await getSignedPath( signedPath = await getSignedPath(
this.hass, this.hass!,
`/api/hassio/${ `/api/hassio/${
atLeastVersion(this.hass.config.version, 2021, 9) atLeastVersion(this.hass!.config.version, 2021, 9)
? "backups" ? "backups"
: "snapshots" : "snapshots"
}/${this._backup!.slug}/download` }/${this._backup!.slug}/download`

View File

@ -1,12 +1,12 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list-item";
import { mdiDelete } from "@mdi/js"; import { mdiDelete } from "@mdi/js";
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, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-circular-progress";
import { createCloseHeading } from "../../../../src/components/ha-dialog"; import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-form/ha-form";
import { HaFormSchema } from "../../../../src/components/ha-form/types";
import "../../../../src/components/ha-icon-button"; import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-settings-row";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { import {
addHassioDockerRegistry, addHassioDockerRegistry,
@ -19,22 +19,41 @@ import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
import { RegistriesDialogParams } from "./show-dialog-registries"; import { RegistriesDialogParams } from "./show-dialog-registries";
const SCHEMA = [
{
type: "string",
name: "registry",
required: true,
},
{
type: "string",
name: "username",
required: true,
},
{
type: "string",
name: "password",
required: true,
format: "password",
},
];
@customElement("dialog-hassio-registries") @customElement("dialog-hassio-registries")
class HassioRegistriesDialog extends LitElement { class HassioRegistriesDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor; @property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) private _registries?: { @state() private _registries?: {
registry: string; registry: string;
username: string; username: string;
}[]; }[];
@state() private _registry?: string; @state() private _input: {
registry?: string;
@state() private _username?: string; username?: string;
password?: string;
@state() private _password?: string; } = {};
@state() private _opened = false; @state() private _opened = false;
@ -47,6 +66,7 @@ class HassioRegistriesDialog extends LitElement {
@closed=${this.closeDialog} @closed=${this.closeDialog}
scrimClickAction scrimClickAction
escapeKeyAction escapeKeyAction
hideActions
.heading=${createCloseHeading( .heading=${createCloseHeading(
this.hass, this.hass,
this._addingRegistry this._addingRegistry
@ -54,99 +74,77 @@ class HassioRegistriesDialog extends LitElement {
: this.supervisor.localize("dialog.registries.title_manage") : this.supervisor.localize("dialog.registries.title_manage")
)} )}
> >
<div class="form"> ${this._addingRegistry
${this._addingRegistry ? html`
? html` <ha-form
<paper-input .data=${this._input}
@value-changed=${this._inputChanged} .schema=${SCHEMA}
class="flex-auto" @value-changed=${this._valueChanged}
name="registry" .computeLabel=${this._computeLabel}
.label=${this.supervisor.localize( ></ha-form>
"dialog.registries.registry" <div class="action">
)}
required
auto-validate
></paper-input>
<paper-input
@value-changed=${this._inputChanged}
class="flex-auto"
name="username"
.label=${this.supervisor.localize(
"dialog.registries.username"
)}
required
auto-validate
></paper-input>
<paper-input
@value-changed=${this._inputChanged}
class="flex-auto"
name="password"
.label=${this.supervisor.localize(
"dialog.registries.password"
)}
type="password"
required
auto-validate
></paper-input>
<mwc-button <mwc-button
?disabled=${Boolean( ?disabled=${Boolean(
!this._registry || !this._username || !this._password !this._input.registry ||
!this._input.username ||
!this._input.password
)} )}
@click=${this._addNewRegistry} @click=${this._addNewRegistry}
> >
${this.supervisor.localize("dialog.registries.add_registry")} ${this.supervisor.localize("dialog.registries.add_registry")}
</mwc-button> </mwc-button>
` </div>
: html`${this._registries?.length `
? this._registries.map( : html`${this._registries?.length
(entry) => html` ? this._registries.map(
<mwc-list-item class="option" hasMeta twoline> (entry) => html`
<span>${entry.registry}</span> <ha-settings-row class="registry">
<span slot="secondary" <span slot="heading"> ${entry.registry} </span>
>${this.supervisor.localize( <span slot="description">
"dialog.registries.username" ${this.supervisor.localize(
)}: "dialog.registries.username"
${entry.username}</span )}:
> ${entry.username}
<ha-icon-button </span>
.entry=${entry} <ha-icon-button
.label=${this.supervisor.localize( .entry=${entry}
"dialog.registries.remove" .label=${this.supervisor.localize(
)} "dialog.registries.remove"
.path=${mdiDelete} )}
slot="meta" .path=${mdiDelete}
@click=${this._removeRegistry} @click=${this._removeRegistry}
></ha-icon-button> ></ha-icon-button>
</mwc-list-item> </ha-settings-row>
` `
) )
: html` : html`
<mwc-list-item> <ha-alert>
<span ${this.supervisor.localize(
>${this.supervisor.localize( "dialog.registries.no_registries"
"dialog.registries.no_registries" )}
)}</span </ha-alert>
> `}
</mwc-list-item> <div class="action">
`}
<mwc-button @click=${this._addRegistry}> <mwc-button @click=${this._addRegistry}>
${this.supervisor.localize( ${this.supervisor.localize(
"dialog.registries.add_new_registry" "dialog.registries.add_new_registry"
)} )}
</mwc-button> `} </mwc-button>
</div> </div> `}
</ha-dialog> </ha-dialog>
`; `;
} }
private _inputChanged(ev: Event) { private _computeLabel = (schema: HaFormSchema) =>
const target = ev.currentTarget as PaperInputElement; this.supervisor.localize(`dialog.registries.${schema.name}`) || schema.name;
this[`_${target.name}`] = target.value;
private _valueChanged(ev: CustomEvent) {
this._input = ev.detail.value;
} }
public async showDialog(dialogParams: RegistriesDialogParams): Promise<void> { public async showDialog(dialogParams: RegistriesDialogParams): Promise<void> {
this._opened = true; this._opened = true;
this._input = {};
this.supervisor = dialogParams.supervisor; this.supervisor = dialogParams.supervisor;
await this._loadRegistries(); await this._loadRegistries();
await this.updateComplete; await this.updateComplete;
@ -155,6 +153,7 @@ class HassioRegistriesDialog extends LitElement {
public closeDialog(): void { public closeDialog(): void {
this._addingRegistry = false; this._addingRegistry = false;
this._opened = false; this._opened = false;
this._input = {};
} }
public focus(): void { public focus(): void {
@ -179,15 +178,16 @@ class HassioRegistriesDialog extends LitElement {
private async _addNewRegistry(): Promise<void> { private async _addNewRegistry(): Promise<void> {
const data = {}; const data = {};
data[this._registry!] = { data[this._input.registry!] = {
username: this._username, username: this._input.username,
password: this._password, password: this._input.password,
}; };
try { try {
await addHassioDockerRegistry(this.hass, data); await addHassioDockerRegistry(this.hass, data);
await this._loadRegistries(); await this._loadRegistries();
this._addingRegistry = false; this._addingRegistry = false;
this._input = {};
} catch (err: any) { } catch (err: any) {
showAlertDialog(this, { showAlertDialog(this, {
title: this.supervisor.localize("dialog.registries.failed_to_add"), title: this.supervisor.localize("dialog.registries.failed_to_add"),
@ -215,32 +215,20 @@ class HassioRegistriesDialog extends LitElement {
haStyle, haStyle,
haStyleDialog, haStyleDialog,
css` css`
ha-dialog.button-left { .registry {
--justify-action-buttons: flex-start;
}
paper-icon-item {
cursor: pointer;
}
.form {
color: var(--primary-text-color);
}
.option {
border: 1px solid var(--divider-color); border: 1px solid var(--divider-color);
border-radius: 4px; border-radius: 4px;
margin-top: 4px; margin-top: 4px;
} }
mwc-button { .action {
margin-left: 8px; margin-top: 24px;
width: 100%;
display: flex;
justify-content: flex-end;
} }
ha-icon-button { ha-icon-button {
color: var(--error-color); color: var(--error-color);
margin: -10px; margin-right: -10px;
}
mwc-list-item {
cursor: default;
}
mwc-list-item span[slot="secondary"] {
color: var(--secondary-text-color);
} }
`, `,
]; ];

View File

@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup( setup(
name="home-assistant-frontend", name="home-assistant-frontend",
version="20211103.0", version="20211108.0",
description="The Home Assistant frontend", description="The Home Assistant frontend",
url="https://github.com/home-assistant/frontend", url="https://github.com/home-assistant/frontend",
author="The Home Assistant Authors", author="The Home Assistant Authors",

View File

@ -21,7 +21,11 @@ class HaPickAuthProvider extends litLocalizeLiteMixin(LitElement) {
<p>${this.localize("ui.panel.page-authorize.pick_auth_provider")}:</p> <p>${this.localize("ui.panel.page-authorize.pick_auth_provider")}:</p>
${this.authProviders.map( ${this.authProviders.map(
(provider) => html` (provider) => html`
<paper-item .auth_provider=${provider} @click=${this._handlePick}> <paper-item
role="button"
.auth_provider=${provider}
@click=${this._handlePick}
>
<paper-item-body>${provider.name}</paper-item-body> <paper-item-body>${provider.name}</paper-item-body>
<ha-icon-next></ha-icon-next> <ha-icon-next></ha-icon-next>
</paper-item> </paper-item>

View File

@ -76,7 +76,6 @@ export const FIXED_DOMAIN_ICONS = {
configurator: mdiCog, configurator: mdiCog,
conversation: mdiTextToSpeech, conversation: mdiTextToSpeech,
counter: mdiCounter, counter: mdiCounter,
device_tracker: mdiAccount,
fan: mdiFan, fan: mdiFan,
google_assistant: mdiGoogleAssistant, google_assistant: mdiGoogleAssistant,
group: mdiGoogleCirclesCommunities, group: mdiGoogleCirclesCommunities,
@ -104,7 +103,6 @@ export const FIXED_DOMAIN_ICONS = {
siren: mdiBullhorn, siren: mdiBullhorn,
simple_alarm: mdiBell, simple_alarm: mdiBell,
sun: mdiWhiteBalanceSunny, sun: mdiWhiteBalanceSunny,
switch: mdiFlash,
timer: mdiTimerOutline, timer: mdiTimerOutline,
updater: mdiCloudUpload, updater: mdiCloudUpload,
vacuum: mdiRobotVacuum, vacuum: mdiRobotVacuum,
@ -145,6 +143,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
/** Domains that have a state card. */ /** Domains that have a state card. */
export const DOMAINS_WITH_CARD = [ export const DOMAINS_WITH_CARD = [
"button",
"climate", "climate",
"cover", "cover",
"configurator", "configurator",

View File

@ -6,6 +6,8 @@ import {
mdiBrightness5, mdiBrightness5,
mdiBrightness7, mdiBrightness7,
mdiCheckboxMarkedCircle, mdiCheckboxMarkedCircle,
mdiCheckNetworkOutline,
mdiCloseNetworkOutline,
mdiCheckCircle, mdiCheckCircle,
mdiCropPortrait, mdiCropPortrait,
mdiDoorClosed, mdiDoorClosed,
@ -26,8 +28,6 @@ import {
mdiPowerPlugOff, mdiPowerPlugOff,
mdiRadioboxBlank, mdiRadioboxBlank,
mdiRun, mdiRun,
mdiServerNetwork,
mdiServerNetworkOff,
mdiSmoke, mdiSmoke,
mdiSnowflake, mdiSnowflake,
mdiSquare, mdiSquare,
@ -55,7 +55,7 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
case "cold": case "cold":
return is_off ? mdiThermometer : mdiSnowflake; return is_off ? mdiThermometer : mdiSnowflake;
case "connectivity": case "connectivity":
return is_off ? mdiServerNetworkOff : mdiServerNetwork; return is_off ? mdiCloseNetworkOutline : mdiCheckNetworkOutline;
case "door": case "door":
return is_off ? mdiDoorClosed : mdiDoorOpen; return is_off ? mdiDoorClosed : mdiDoorOpen;
case "garage_door": case "garage_door":

View File

@ -116,6 +116,14 @@ export const computeStateDisplay = (
return formatNumber(compareState, locale); return formatNumber(compareState, locale);
} }
// state of button is a timestamp
if (
domain === "button" ||
(domain === "sensor" && stateObj.attributes.device_class === "timestamp")
) {
return formatDateTime(new Date(compareState), locale);
}
return ( return (
// Return device class translation // Return device class translation
(stateObj.attributes.device_class && (stateObj.attributes.device_class &&

View File

@ -1,6 +1,13 @@
import { import {
mdiAccount,
mdiAccountArrowRight,
mdiAirHumidifierOff, mdiAirHumidifierOff,
mdiAirHumidifier, mdiAirHumidifier,
mdiFlash,
mdiBluetooth,
mdiBluetoothConnect,
mdiLanConnect,
mdiLanDisconnect,
mdiLockOpen, mdiLockOpen,
mdiLockAlert, mdiLockAlert,
mdiLockClock, mdiLockClock,
@ -8,8 +15,12 @@ import {
mdiCastConnected, mdiCastConnected,
mdiCast, mdiCast,
mdiEmoticonDead, mdiEmoticonDead,
mdiPowerPlug,
mdiPowerPlugOff,
mdiSleep, mdiSleep,
mdiTimerSand, mdiTimerSand,
mdiToggleSwitch,
mdiToggleSwitchOff,
mdiZWave, mdiZWave,
mdiClock, mdiClock,
mdiCalendar, mdiCalendar,
@ -44,6 +55,17 @@ export const domainIcon = (
case "cover": case "cover":
return coverIcon(compareState, stateObj); return coverIcon(compareState, stateObj);
case "device_tracker":
if (stateObj?.attributes.source_type === "router") {
return compareState === "home" ? mdiLanConnect : mdiLanDisconnect;
}
if (
["bluetooth", "bluetooth_le"].includes(stateObj?.attributes.source_type)
) {
return compareState === "home" ? mdiBluetoothConnect : mdiBluetooth;
}
return compareState === "not_home" ? mdiAccountArrowRight : mdiAccount;
case "humidifier": case "humidifier":
return state && state === "off" ? mdiAirHumidifierOff : mdiAirHumidifier; return state && state === "off" ? mdiAirHumidifierOff : mdiAirHumidifier;
@ -63,6 +85,16 @@ export const domainIcon = (
case "media_player": case "media_player":
return compareState === "playing" ? mdiCastConnected : mdiCast; return compareState === "playing" ? mdiCastConnected : mdiCast;
case "switch":
switch (stateObj?.attributes.device_class) {
case "outlet":
return state === "on" ? mdiPowerPlug : mdiPowerPlugOff;
case "switch":
return state === "on" ? mdiToggleSwitch : mdiToggleSwitchOff;
default:
return mdiFlash;
}
case "zwave": case "zwave":
switch (compareState) { switch (compareState) {
case "dead": case "dead":

View File

@ -32,6 +32,7 @@ if (__BUILD__ === "latest") {
} }
if (shouldPolyfillDateTime()) { if (shouldPolyfillDateTime()) {
polyfills.push(import("@formatjs/intl-datetimeformat/polyfill")); polyfills.push(import("@formatjs/intl-datetimeformat/polyfill"));
polyfills.push(import("@formatjs/intl-datetimeformat/add-all-tz"));
} }
} }

View File

@ -17,7 +17,7 @@ declare global {
@customElement("ha-file-upload") @customElement("ha-file-upload")
export class HaFileUpload extends LitElement { export class HaFileUpload extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property() public accept!: string; @property() public accept!: string;
@ -88,7 +88,8 @@ export class HaFileUpload extends LitElement {
<ha-icon-button <ha-icon-button
slot="suffix" slot="suffix"
@click=${this._clearValue} @click=${this._clearValue}
.label=${this.hass.localize("ui.common.close")} .label=${this.hass?.localize("ui.common.close") ||
"close"}
.path=${mdiClose} .path=${mdiClose}
></ha-icon-button> ></ha-icon-button>
` `

View File

@ -47,12 +47,19 @@ export class HaFormFloat extends LitElement implements HaFormElement {
private _valueChanged(ev: Event) { private _valueChanged(ev: Event) {
const source = ev.target as TextField; const source = ev.target as TextField;
const rawValue = source.value; const rawValue = source.value.replace(",", ".");
let value: number | undefined; let value: number | undefined;
if (rawValue.endsWith(".")) {
return;
}
if (rawValue !== "") { if (rawValue !== "") {
value = parseFloat(rawValue); value = parseFloat(rawValue);
if (isNaN(value)) {
value = undefined;
}
} }
// Detect anything changed // Detect anything changed
@ -61,7 +68,6 @@ export class HaFormFloat extends LitElement implements HaFormElement {
const newRawValue = value === undefined ? "" : String(value); const newRawValue = value === undefined ? "" : String(value);
if (source.value !== newRawValue) { if (source.value !== newRawValue) {
source.value = newRawValue; source.value = newRawValue;
return;
} }
return; return;
} }

View File

@ -60,6 +60,8 @@ export class HaIconPicker extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public invalid = false;
@state() private _opened = false; @state() private _opened = false;
@query("vaadin-combo-box-light", true) private comboBox!: HTMLElement; @query("vaadin-combo-box-light", true) private comboBox!: HTMLElement;
@ -86,6 +88,8 @@ export class HaIconPicker extends LitElement {
autocomplete="off" autocomplete="off"
autocorrect="off" autocorrect="off"
spellcheck="false" spellcheck="false"
.errorMessage=${this.errorMessage}
.invalid=${this.invalid}
> >
${this._value || this.placeholder ${this._value || this.placeholder
? html` ? html`

View File

@ -502,6 +502,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
} }
private _entityRegMeetsFilter(entity: EntityRegistryEntry): boolean { private _entityRegMeetsFilter(entity: EntityRegistryEntry): boolean {
if (entity.entity_category) {
return false;
}
if ( if (
this.includeDomains && this.includeDomains &&
!this.includeDomains.includes(computeDomain(entity.entity_id)) !this.includeDomains.includes(computeDomain(entity.entity_id))

View File

@ -79,7 +79,7 @@ export const fetchHassioBackups = async (
}; };
export const fetchHassioBackupInfo = async ( export const fetchHassioBackupInfo = async (
hass: HomeAssistant, hass: HomeAssistant | undefined,
backup: string backup: string
): Promise<HassioBackupDetail> => { ): Promise<HassioBackupDetail> => {
if (hass) { if (hass) {
@ -202,7 +202,7 @@ export const createHassioPartialBackup = async (
}; };
export const uploadBackup = async ( export const uploadBackup = async (
hass: HomeAssistant, hass: HomeAssistant | undefined,
file: File file: File
): Promise<HassioResponse<HassioBackup>> => { ): Promise<HassioResponse<HassioBackup>> => {
const fd = new FormData(); const fd = new FormData();

View File

@ -12,7 +12,7 @@ export interface Zone {
} }
export interface ZoneMutableParams { export interface ZoneMutableParams {
icon: string; icon?: string;
latitude: number; latitude: number;
longitude: number; longitude: number;
name: string; name: string;

View File

@ -45,7 +45,7 @@ class StepFlowPickFlow extends LitElement {
domain: flow.handler, domain: flow.handler,
type: "icon", type: "icon",
useFallback: true, useFallback: true,
darkOptimized: this.hass.selectedTheme?.dark, darkOptimized: this.hass.themes?.darkMode,
})} })}
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
/> />

View File

@ -102,7 +102,7 @@ class StepFlowPickHandler extends LitElement {
domain: handler.slug, domain: handler.slug,
type: "icon", type: "icon",
useFallback: true, useFallback: true,
darkOptimized: this.hass.selectedTheme?.dark, darkOptimized: this.hass.themes?.darkMode,
})} })}
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
/> />

View File

@ -33,7 +33,7 @@
<body> <body>
<div class="content"> <div class="content">
<div class="header"> <div class="header">
<img src="/static/icons/favicon-192x192.png" height="52" /> <img src="/static/icons/favicon-192x192.png" height="52" alt="" />
Home Assistant Home Assistant
</div> </div>
<ha-authorize><p>Initializing</p></ha-authorize> <ha-authorize><p>Initializing</p></ha-authorize>
@ -70,4 +70,4 @@
})(); })();
</script> </script>
</body> </body>
</html> </html>

View File

@ -64,7 +64,7 @@
<body id="particles"> <body id="particles">
<div class="content"> <div class="content">
<div class="header"> <div class="header">
<img src="/static/icons/favicon-192x192.png" height="52" width="52" /> <img src="/static/icons/favicon-192x192.png" height="52" width="52" alt="" />
Home Assistant Home Assistant
</div> </div>

View File

@ -81,7 +81,7 @@ class OnboardingIntegrations extends LitElement {
.domain=${entry.domain} .domain=${entry.domain}
.title=${title} .title=${title}
.badgeIcon=${mdiCheck} .badgeIcon=${mdiCheck}
.darkOptimizedIcon=${this.hass.selectedTheme?.dark} .darkOptimizedIcon=${this.hass.themes?.darkMode}
></integration-badge> ></integration-badge>
`, `,
]; ];
@ -98,7 +98,7 @@ class OnboardingIntegrations extends LitElement {
clickable clickable
.domain=${flow.handler} .domain=${flow.handler}
.title=${title} .title=${title}
.darkOptimizedIcon=${this.hass.selectedTheme?.dark} .darkOptimizedIcon=${this.hass.themes?.darkMode}
></integration-badge> ></integration-badge>
</button> </button>
`, `,

View File

@ -1,19 +1,20 @@
import { mdiPencil, mdiPlusCircle, mdiOpenInNew } from "@mdi/js"; import { mdiOpenInNew, mdiPencil, mdiPlusCircle } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
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 { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain"; 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 { stringCompare } from "../../../common/string/compare"; import { stringCompare } from "../../../common/string/compare";
import { groupBy } from "../../../common/util/group-by";
import { slugify } from "../../../common/string/slugify"; import { slugify } from "../../../common/string/slugify";
import { groupBy } from "../../../common/util/group-by";
import "../../../components/entity/ha-battery-icon"; import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-alert";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next"; import "../../../components/ha-icon-next";
import "../../../components/ha-alert";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { AreaRegistryEntry } from "../../../data/area_registry"; import { AreaRegistryEntry } from "../../../data/area_registry";
import { import {
@ -52,7 +53,6 @@ import {
loadDeviceRegistryDetailDialog, loadDeviceRegistryDetailDialog,
showDeviceRegistryDetailDialog, showDeviceRegistryDetailDialog,
} from "./device-registry-detail/show-dialog-device-registry-detail"; } from "./device-registry-detail/show-dialog-device-registry-detail";
import { computeDomain } from "../../../common/entity/compute_domain";
export interface EntityRegistryStateEntry extends EntityRegistryEntry { export interface EntityRegistryStateEntry extends EntityRegistryEntry {
stateName?: string | null; stateName?: string | null;
@ -293,7 +293,7 @@ export class HaConfigDevicePage extends LitElement {
src=${brandsUrl({ src=${brandsUrl({
domain: integrations[0], domain: integrations[0],
type: "logo", type: "logo",
darkOptimized: this.hass.selectedTheme?.dark, darkOptimized: this.hass.themes?.darkMode,
})} })}
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
@load=${this._onImageLoad} @load=${this._onImageLoad}
@ -407,41 +407,45 @@ export class HaConfigDevicePage extends LitElement {
></ha-icon-button> ></ha-icon-button>
</h1> </h1>
${this._related?.automation?.length ${this._related?.automation?.length
? this._related.automation.map((automation) => { ? html`
const entityState = this.hass.states[automation]; <div class="items">
return entityState ${this._related.automation.map((automation) => {
? html` const entityState =
<div> this.hass.states[automation];
<a return entityState
href=${ifDefined( ? html`<div>
entityState.attributes.id <a
? `/config/automation/edit/${entityState.attributes.id}` href=${ifDefined(
: undefined entityState.attributes.id
)} ? `/config/automation/edit/${entityState.attributes.id}`
> : undefined
<paper-item )}
.automation=${entityState}
.disabled=${!entityState.attributes.id}
> >
<paper-item-body> <paper-item
${computeStateName(entityState)} .automation=${entityState}
</paper-item-body> .disabled=${!entityState.attributes
<ha-icon-next></ha-icon-next> .id}
</paper-item> >
</a> <paper-item-body>
${!entityState.attributes.id ${computeStateName(entityState)}
? html` </paper-item-body>
<paper-tooltip animation-delay="0"> <ha-icon-next></ha-icon-next>
${this.hass.localize( </paper-item>
"ui.panel.config.devices.cant_edit" </a>
)} ${!entityState.attributes.id
</paper-tooltip> ? html`
` <paper-tooltip animation-delay="0">
: ""} ${this.hass.localize(
</div> "ui.panel.config.devices.cant_edit"
` )}
: ""; </paper-tooltip>
}) `
: ""}
</div> `
: "";
})}
</div>
`
: html` : html`
<div class="card-content"> <div class="card-content">
${this.hass.localize( ${this.hass.localize(
@ -479,43 +483,49 @@ export class HaConfigDevicePage extends LitElement {
.path=${mdiPlusCircle} .path=${mdiPlusCircle}
></ha-icon-button> ></ha-icon-button>
</h1> </h1>
${this._related?.scene?.length ${this._related?.scene?.length
? this._related.scene.map((scene) => { ? html`
const entityState = this.hass.states[scene]; <div class="items">
return entityState ${this._related.scene.map((scene) => {
? html` const entityState = this.hass.states[scene];
<div> return entityState
<a ? html`
href=${ifDefined( <div>
entityState.attributes.id <a
? `/config/scene/edit/${entityState.attributes.id}` href=${ifDefined(
: undefined entityState.attributes.id
)} ? `/config/scene/edit/${entityState.attributes.id}`
> : undefined
<paper-item )}
.scene=${entityState} >
.disabled=${!entityState.attributes.id} <paper-item
> .scene=${entityState}
<paper-item-body> .disabled=${!entityState.attributes
${computeStateName(entityState)} .id}
</paper-item-body> >
<ha-icon-next></ha-icon-next> <paper-item-body>
</paper-item> ${computeStateName(entityState)}
</a> </paper-item-body>
${!entityState.attributes.id <ha-icon-next></ha-icon-next>
? html` </paper-item>
<paper-tooltip animation-delay="0"> </a>
${this.hass.localize( ${!entityState.attributes.id
"ui.panel.config.devices.cant_edit" ? html`
)} <paper-tooltip
</paper-tooltip> animation-delay="0"
` >
: ""} ${this.hass.localize(
</div> "ui.panel.config.devices.cant_edit"
` )}
: ""; </paper-tooltip>
}) `
: ""}
</div>
`
: "";
})}
</div>
`
: html` : html`
<div class="card-content"> <div class="card-content">
${this.hass.localize( ${this.hass.localize(
@ -553,23 +563,27 @@ export class HaConfigDevicePage extends LitElement {
></ha-icon-button> ></ha-icon-button>
</h1> </h1>
${this._related?.script?.length ${this._related?.script?.length
? this._related.script.map((script) => { ? html`
const entityState = this.hass.states[script]; <div class="items">
return entityState ${this._related.script.map((script) => {
? html` const entityState = this.hass.states[script];
<a return entityState
href=${`/config/script/edit/${entityState.entity_id}`} ? html`
> <a
<paper-item .script=${script}> href=${`/config/script/edit/${entityState.entity_id}`}
<paper-item-body> >
${computeStateName(entityState)} <paper-item .script=${script}>
</paper-item-body> <paper-item-body>
<ha-icon-next></ha-icon-next> ${computeStateName(entityState)}
</paper-item> </paper-item-body>
</a> <ha-icon-next></ha-icon-next>
` </paper-item>
: ""; </a>
}) `
: "";
})}
</div>
`
: html` : html`
<div class="card-content"> <div class="card-content">
${this.hass.localize( ${this.hass.localize(
@ -869,6 +883,7 @@ export class HaConfigDevicePage extends LitElement {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding-bottom: 12px;
} }
.card-header ha-icon-button { .card-header ha-icon-button {
@ -978,6 +993,10 @@ export class HaConfigDevicePage extends LitElement {
ha-svg-icon[slot="trailingIcon"] { ha-svg-icon[slot="trailingIcon"] {
display: block; display: block;
} }
.items {
padding-bottom: 16px;
}
`, `,
]; ];
} }

View File

@ -202,7 +202,7 @@ export class EnergyGridSettings extends LitElement {
src=${brandsUrl({ src=${brandsUrl({
domain: "co2signal", domain: "co2signal",
type: "icon", type: "icon",
darkOptimized: this.hass.selectedTheme?.dark, darkOptimized: this.hass.themes?.darkMode,
})} })}
/> />
<span class="content">${entry.title}</span> <span class="content">${entry.title}</span>
@ -223,7 +223,7 @@ export class EnergyGridSettings extends LitElement {
src=${brandsUrl({ src=${brandsUrl({
domain: "co2signal", domain: "co2signal",
type: "icon", type: "icon",
darkOptimized: this.hass.selectedTheme?.dark, darkOptimized: this.hass.themes?.darkMode,
})} })}
/> />
<mwc-button @click=${this._addCO2Sensor}> <mwc-button @click=${this._addCO2Sensor}>

View File

@ -136,7 +136,7 @@ export class DialogEnergySolarSettings
src=${brandsUrl({ src=${brandsUrl({
domain: entry.domain, domain: entry.domain,
type: "icon", type: "icon",
darkOptimized: this.hass.selectedTheme?.dark, darkOptimized: this.hass.themes?.darkMode,
})} })}
/>${entry.title} />${entry.title}
</div>`} </div>`}

View File

@ -98,7 +98,7 @@ class IntegrationsCard extends LitElement {
domain: domain, domain: domain,
type: "icon", type: "icon",
useFallback: true, useFallback: true,
darkOptimized: this.hass.selectedTheme?.dark, darkOptimized: this.hass.themes?.darkMode,
})} })}
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
/> />

View File

@ -84,7 +84,7 @@ export class HaIntegrationHeader extends LitElement {
src=${brandsUrl({ src=${brandsUrl({
domain: this.domain, domain: this.domain,
type: "icon", type: "icon",
darkOptimized: this.hass.selectedTheme?.dark, darkOptimized: this.hass.themes?.darkMode,
})} })}
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
@error=${this._onImageError} @error=${this._onImageError}

View File

@ -8,6 +8,7 @@ import { addDistanceToCoord } from "../../../common/location/add_distance_to_coo
import { computeRTLDirection } from "../../../common/util/compute_rtl"; import { computeRTLDirection } from "../../../common/util/compute_rtl";
import { createCloseHeading } from "../../../components/ha-dialog"; import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-formfield"; import "../../../components/ha-formfield";
import "../../../components/ha-icon-picker";
import "../../../components/ha-switch"; import "../../../components/ha-switch";
import "../../../components/map/ha-locations-editor"; import "../../../components/map/ha-locations-editor";
import type { MarkerLocation } from "../../../components/map/ha-locations-editor"; import type { MarkerLocation } from "../../../components/map/ha-locations-editor";
@ -77,14 +78,18 @@ class DialogZoneDetail extends LitElement {
if (!this._params) { if (!this._params) {
return html``; return html``;
} }
const nameValid = this._name.trim() === ""; const nameInvalid = this._name.trim() === "";
const iconValid = !this._icon.trim().includes(":"); const iconInvalid = Boolean(this._icon && !this._icon.trim().includes(":"));
const latValid = String(this._latitude) === ""; const latInvalid = String(this._latitude) === "";
const lngValid = String(this._longitude) === ""; const lngInvalid = String(this._longitude) === "";
const radiusValid = String(this._radius) === ""; const radiusInvalid = String(this._radius) === "";
const valid = const valid =
!nameValid && !iconValid && !latValid && !lngValid && !radiusValid; !nameInvalid &&
!iconInvalid &&
!latInvalid &&
!lngInvalid &&
!radiusInvalid;
return html` return html`
<ha-dialog <ha-dialog
@ -114,7 +119,7 @@ class DialogZoneDetail extends LitElement {
required required
auto-validate auto-validate
></paper-input> ></paper-input>
<paper-input <ha-icon-picker
.value=${this._icon} .value=${this._icon}
.configValue=${"icon"} .configValue=${"icon"}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@ -122,8 +127,8 @@ class DialogZoneDetail extends LitElement {
.errorMessage=${this.hass!.localize( .errorMessage=${this.hass!.localize(
"ui.panel.config.zone.detail.icon_error_msg" "ui.panel.config.zone.detail.icon_error_msg"
)} )}
.invalid=${iconValid} .invalid=${iconInvalid}
></paper-input> ></ha-icon-picker>
<ha-locations-editor <ha-locations-editor
class="flex" class="flex"
.hass=${this.hass} .hass=${this.hass}
@ -148,7 +153,7 @@ class DialogZoneDetail extends LitElement {
.errorMessage=${this.hass!.localize( .errorMessage=${this.hass!.localize(
"ui.panel.config.zone.detail.required_error_msg" "ui.panel.config.zone.detail.required_error_msg"
)} )}
.invalid=${latValid} .invalid=${latInvalid}
></paper-input> ></paper-input>
<paper-input <paper-input
.value=${this._longitude} .value=${this._longitude}
@ -160,7 +165,7 @@ class DialogZoneDetail extends LitElement {
.errorMessage=${this.hass!.localize( .errorMessage=${this.hass!.localize(
"ui.panel.config.zone.detail.required_error_msg" "ui.panel.config.zone.detail.required_error_msg"
)} )}
.invalid=${lngValid} .invalid=${lngInvalid}
></paper-input> ></paper-input>
</div> </div>
<paper-input <paper-input
@ -173,7 +178,7 @@ class DialogZoneDetail extends LitElement {
.errorMessage=${this.hass!.localize( .errorMessage=${this.hass!.localize(
"ui.panel.config.zone.detail.required_error_msg" "ui.panel.config.zone.detail.required_error_msg"
)} )}
.invalid=${radiusValid} .invalid=${radiusInvalid}
></paper-input> ></paper-input>
<p> <p>
${this.hass!.localize("ui.panel.config.zone.detail.passive_note")} ${this.hass!.localize("ui.panel.config.zone.detail.passive_note")}
@ -268,12 +273,14 @@ class DialogZoneDetail extends LitElement {
try { try {
const values: ZoneMutableParams = { const values: ZoneMutableParams = {
name: this._name.trim(), name: this._name.trim(),
icon: this._icon.trim(),
latitude: this._latitude, latitude: this._latitude,
longitude: this._longitude, longitude: this._longitude,
passive: this._passive, passive: this._passive,
radius: this._radius, radius: this._radius,
}; };
if (this._icon) {
values.icon = this._icon.trim();
}
if (this._params!.entry) { if (this._params!.entry) {
await this._params!.updateEntry!(values); await this._params!.updateEntry!(values);
} else { } else {

View File

@ -231,7 +231,6 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
overflow: hidden;
} }
.card-header { .card-header {
display: flex; display: flex;
@ -261,7 +260,7 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
} }
#states > div > * { #states > div > * {
overflow: hidden; overflow: clip visible;
} }
#states > div { #states > div {

View File

@ -241,7 +241,7 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
); );
const lastDate = this._lastLogbookDate || hoursToShowDate; const lastDate = this._lastLogbookDate || hoursToShowDate;
const now = new Date(); const now = new Date();
let newEntries; let newEntries: LogbookEntry[];
try { try {
[newEntries] = await Promise.all([ [newEntries] = await Promise.all([
@ -256,6 +256,7 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
]); ]);
} catch (err: any) { } catch (err: any) {
this._error = err.message; this._error = err.message;
return;
} }
const logbookEntries = this._logbookEntries const logbookEntries = this._logbookEntries

View File

@ -24,6 +24,7 @@ const ALWAYS_LOADED_TYPES = new Set([
"call-service", "call-service",
]); ]);
const LAZY_LOAD_TYPES = { const LAZY_LOAD_TYPES = {
"button-entity": () => import("../entity-rows/hui-button-entity-row"),
"climate-entity": () => import("../entity-rows/hui-climate-entity-row"), "climate-entity": () => import("../entity-rows/hui-climate-entity-row"),
"cover-entity": () => import("../entity-rows/hui-cover-entity-row"), "cover-entity": () => import("../entity-rows/hui-cover-entity-row"),
"group-entity": () => import("../entity-rows/hui-group-entity-row"), "group-entity": () => import("../entity-rows/hui-group-entity-row"),
@ -53,6 +54,7 @@ const DOMAIN_TO_ELEMENT_TYPE = {
_domain_not_found: "text", _domain_not_found: "text",
alert: "toggle", alert: "toggle",
automation: "toggle", automation: "toggle",
button: "button",
climate: "climate", climate: "climate",
cover: "cover", cover: "cover",
fan: "toggle", fan: "toggle",

View File

@ -0,0 +1,82 @@
import "@material/mwc-button/mwc-button";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { UNAVAILABLE } from "../../../data/entity";
import { HomeAssistant } from "../../../types";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import "../components/hui-generic-entity-row";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import { ActionRowConfig, LovelaceRow } from "./types";
@customElement("hui-button-entity-row")
class HuiButtonEntityRow extends LitElement implements LovelaceRow {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _config?: ActionRowConfig;
public setConfig(config: ActionRowConfig): void {
if (!config) {
throw new Error("Invalid configuration");
}
this._config = config;
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
return hasConfigOrEntityChanged(this, changedProps);
}
protected render(): TemplateResult {
if (!this._config || !this.hass) {
return html``;
}
const stateObj = this.hass.states[this._config.entity];
if (!stateObj) {
return html`
<hui-warning>
${createEntityNotFoundWarning(this.hass, this._config.entity)}
</hui-warning>
`;
}
return html`
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
<mwc-button
@click=${this._pressButton}
.disabled=${stateObj.state === UNAVAILABLE}
>
${this.hass.localize("ui.card.button.press")}
</mwc-button>
</hui-generic-entity-row>
`;
}
static get styles(): CSSResultGroup {
return css`
mwc-button:last-child {
margin-right: -0.57em;
}
`;
}
private _pressButton(ev): void {
ev.stopPropagation();
this.hass.callService("button", "press", {
entity_id: this._config!.entity,
});
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-button-entity-row": HuiButtonEntityRow;
}
}

View File

@ -0,0 +1,54 @@
import "@material/mwc-button";
import { HassEntity } from "home-assistant-js-websocket";
import { CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../components/entity/ha-entity-toggle";
import "../components/entity/state-info";
import { UNAVAILABLE } from "../data/entity";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
@customElement("state-card-button")
export class StateCardButton extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public stateObj!: HassEntity;
@property({ type: Boolean }) public inDialog = false;
protected render() {
const stateObj = this.stateObj;
return html`
<div class="horizontal justified layout">
<state-info
.hass=${this.hass}
.stateObj=${stateObj}
.inDialog=${this.inDialog}
></state-info>
<mwc-button
@click=${this._pressButton}
.disabled=${stateObj.state === UNAVAILABLE}
>
${this.hass.localize("ui.card.button.press")}
</mwc-button>
</div>
`;
}
private _pressButton(ev: Event) {
ev.stopPropagation();
this.hass.callService("button", "press", {
entity_id: this.stateObj.entity_id,
});
}
static get styles(): CSSResultGroup {
return haStyle;
}
}
declare global {
interface HTMLElementTagNameMap {
"state-card-button": StateCardButton;
}
}

View File

@ -2,6 +2,7 @@
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import dynamicContentUpdater from "../common/dom/dynamic_content_updater"; import dynamicContentUpdater from "../common/dom/dynamic_content_updater";
import { stateCardType } from "../common/entity/state_card_type"; import { stateCardType } from "../common/entity/state_card_type";
import "./state-card-button";
import "./state-card-climate"; import "./state-card-climate";
import "./state-card-configurator"; import "./state-card-configurator";
import "./state-card-cover"; import "./state-card-cover";

View File

@ -24,6 +24,7 @@ import { getState } from "../util/ha-pref-storage";
import hassCallApi from "../util/hass-call-api"; import hassCallApi from "../util/hass-call-api";
import { getLocalLanguage } from "../util/common-translation"; import { getLocalLanguage } from "../util/common-translation";
import { HassBaseEl } from "./hass-base-mixin"; import { HassBaseEl } from "./hass-base-mixin";
import { polyfillsLoaded } from "../common/translations/localize";
export const connectionMixin = <T extends Constructor<HassBaseEl>>( export const connectionMixin = <T extends Constructor<HassBaseEl>>(
superClass: T superClass: T
@ -180,12 +181,18 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
subscribeEntities(conn, (states) => this._updateHass({ states })); subscribeEntities(conn, (states) => this._updateHass({ states }));
subscribeConfig(conn, (config) => { subscribeConfig(conn, (config) => {
if ( if (this.hass?.config?.time_zone !== config.time_zone) {
this.hass?.config?.time_zone !== config.time_zone && if (__BUILD__ === "latest" && polyfillsLoaded) {
"__setDefaultTimeZone" in Intl.DateTimeFormat polyfillsLoaded.then(() => {
) { if ("__setDefaultTimeZone" in Intl.DateTimeFormat) {
// @ts-ignore // @ts-ignore
Intl.DateTimeFormat.__setDefaultTimeZone(config.time_zone); Intl.DateTimeFormat.__setDefaultTimeZone(config.time_zone);
}
});
} else if ("__setDefaultTimeZone" in Intl.DateTimeFormat) {
// @ts-ignore
Intl.DateTimeFormat.__setDefaultTimeZone(config.time_zone);
}
} }
this._updateHass({ config }); this._updateHass({ config });
}); });

View File

@ -120,6 +120,9 @@
"last_triggered": "Last triggered", "last_triggered": "Last triggered",
"trigger": "Run Actions" "trigger": "Run Actions"
}, },
"button": {
"press": "Press"
},
"camera": { "camera": {
"not_available": "Image not available" "not_available": "Image not available"
}, },