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 }) {
const entry = {
launcher: path.resolve(paths.cast_dir, "src/launcher/entrypoint.ts"),
media: path.resolve(paths.cast_dir, "src/media/entrypoint.ts"),
};
if (latestBuild) {

View File

@ -154,6 +154,15 @@ gulp.task("gen-index-cast-dev", (done) => {
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", {
latestLauncherJS: "/frontend_latest/launcher.js",
es5LauncherJS: "/frontend_es5/launcher.js",
@ -192,6 +201,15 @@ gulp.task("gen-index-cast-prod", (done) => {
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", {
latestLauncherJS: latestManifest["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")
export class HassioUploadBackup extends LitElement {
public hass!: HomeAssistant;
public hass?: HomeAssistant;
@state() public value: string | null = null;
@ -43,20 +41,6 @@ export class HassioUploadBackup extends LitElement {
private async _uploadFile(ev) {
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)) {
showAlertDialog(this, {
title: "Unsupported file format",

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20211103.0",
version="20211108.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/frontend",
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>
${this.authProviders.map(
(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>
<ha-icon-next></ha-icon-next>
</paper-item>

View File

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

View File

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

View File

@ -116,6 +116,14 @@ export const computeStateDisplay = (
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 device class translation
(stateObj.attributes.device_class &&

View File

@ -1,6 +1,13 @@
import {
mdiAccount,
mdiAccountArrowRight,
mdiAirHumidifierOff,
mdiAirHumidifier,
mdiFlash,
mdiBluetooth,
mdiBluetoothConnect,
mdiLanConnect,
mdiLanDisconnect,
mdiLockOpen,
mdiLockAlert,
mdiLockClock,
@ -8,8 +15,12 @@ import {
mdiCastConnected,
mdiCast,
mdiEmoticonDead,
mdiPowerPlug,
mdiPowerPlugOff,
mdiSleep,
mdiTimerSand,
mdiToggleSwitch,
mdiToggleSwitchOff,
mdiZWave,
mdiClock,
mdiCalendar,
@ -44,6 +55,17 @@ export const domainIcon = (
case "cover":
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":
return state && state === "off" ? mdiAirHumidifierOff : mdiAirHumidifier;
@ -63,6 +85,16 @@ export const domainIcon = (
case "media_player":
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":
switch (compareState) {
case "dead":

View File

@ -32,6 +32,7 @@ if (__BUILD__ === "latest") {
}
if (shouldPolyfillDateTime()) {
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")
export class HaFileUpload extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public accept!: string;
@ -88,7 +88,8 @@ export class HaFileUpload extends LitElement {
<ha-icon-button
slot="suffix"
@click=${this._clearValue}
.label=${this.hass.localize("ui.common.close")}
.label=${this.hass?.localize("ui.common.close") ||
"close"}
.path=${mdiClose}
></ha-icon-button>
`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -64,7 +64,7 @@
<body id="particles">
<div class="content">
<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
</div>

View File

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

View File

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

View File

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

View File

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

View File

@ -84,7 +84,7 @@ export class HaIntegrationHeader extends LitElement {
src=${brandsUrl({
domain: this.domain,
type: "icon",
darkOptimized: this.hass.selectedTheme?.dark,
darkOptimized: this.hass.themes?.darkMode,
})}
referrerpolicy="no-referrer"
@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 { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-formfield";
import "../../../components/ha-icon-picker";
import "../../../components/ha-switch";
import "../../../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) {
return html``;
}
const nameValid = this._name.trim() === "";
const iconValid = !this._icon.trim().includes(":");
const latValid = String(this._latitude) === "";
const lngValid = String(this._longitude) === "";
const radiusValid = String(this._radius) === "";
const nameInvalid = this._name.trim() === "";
const iconInvalid = Boolean(this._icon && !this._icon.trim().includes(":"));
const latInvalid = String(this._latitude) === "";
const lngInvalid = String(this._longitude) === "";
const radiusInvalid = String(this._radius) === "";
const valid =
!nameValid && !iconValid && !latValid && !lngValid && !radiusValid;
!nameInvalid &&
!iconInvalid &&
!latInvalid &&
!lngInvalid &&
!radiusInvalid;
return html`
<ha-dialog
@ -114,7 +119,7 @@ class DialogZoneDetail extends LitElement {
required
auto-validate
></paper-input>
<paper-input
<ha-icon-picker
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
@ -122,8 +127,8 @@ class DialogZoneDetail extends LitElement {
.errorMessage=${this.hass!.localize(
"ui.panel.config.zone.detail.icon_error_msg"
)}
.invalid=${iconValid}
></paper-input>
.invalid=${iconInvalid}
></ha-icon-picker>
<ha-locations-editor
class="flex"
.hass=${this.hass}
@ -148,7 +153,7 @@ class DialogZoneDetail extends LitElement {
.errorMessage=${this.hass!.localize(
"ui.panel.config.zone.detail.required_error_msg"
)}
.invalid=${latValid}
.invalid=${latInvalid}
></paper-input>
<paper-input
.value=${this._longitude}
@ -160,7 +165,7 @@ class DialogZoneDetail extends LitElement {
.errorMessage=${this.hass!.localize(
"ui.panel.config.zone.detail.required_error_msg"
)}
.invalid=${lngValid}
.invalid=${lngInvalid}
></paper-input>
</div>
<paper-input
@ -173,7 +178,7 @@ class DialogZoneDetail extends LitElement {
.errorMessage=${this.hass!.localize(
"ui.panel.config.zone.detail.required_error_msg"
)}
.invalid=${radiusValid}
.invalid=${radiusInvalid}
></paper-input>
<p>
${this.hass!.localize("ui.panel.config.zone.detail.passive_note")}
@ -268,12 +273,14 @@ class DialogZoneDetail extends LitElement {
try {
const values: ZoneMutableParams = {
name: this._name.trim(),
icon: this._icon.trim(),
latitude: this._latitude,
longitude: this._longitude,
passive: this._passive,
radius: this._radius,
};
if (this._icon) {
values.icon = this._icon.trim();
}
if (this._params!.entry) {
await this._params!.updateEntry!(values);
} else {

View File

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

View File

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

View File

@ -24,6 +24,7 @@ const ALWAYS_LOADED_TYPES = new Set([
"call-service",
]);
const LAZY_LOAD_TYPES = {
"button-entity": () => import("../entity-rows/hui-button-entity-row"),
"climate-entity": () => import("../entity-rows/hui-climate-entity-row"),
"cover-entity": () => import("../entity-rows/hui-cover-entity-row"),
"group-entity": () => import("../entity-rows/hui-group-entity-row"),
@ -53,6 +54,7 @@ const DOMAIN_TO_ELEMENT_TYPE = {
_domain_not_found: "text",
alert: "toggle",
automation: "toggle",
button: "button",
climate: "climate",
cover: "cover",
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 dynamicContentUpdater from "../common/dom/dynamic_content_updater";
import { stateCardType } from "../common/entity/state_card_type";
import "./state-card-button";
import "./state-card-climate";
import "./state-card-configurator";
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 { getLocalLanguage } from "../util/common-translation";
import { HassBaseEl } from "./hass-base-mixin";
import { polyfillsLoaded } from "../common/translations/localize";
export const connectionMixin = <T extends Constructor<HassBaseEl>>(
superClass: T
@ -180,12 +181,18 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
subscribeEntities(conn, (states) => this._updateHass({ states }));
subscribeConfig(conn, (config) => {
if (
this.hass?.config?.time_zone !== config.time_zone &&
"__setDefaultTimeZone" in Intl.DateTimeFormat
) {
// @ts-ignore
Intl.DateTimeFormat.__setDefaultTimeZone(config.time_zone);
if (this.hass?.config?.time_zone !== config.time_zone) {
if (__BUILD__ === "latest" && polyfillsLoaded) {
polyfillsLoaded.then(() => {
if ("__setDefaultTimeZone" in Intl.DateTimeFormat) {
// @ts-ignore
Intl.DateTimeFormat.__setDefaultTimeZone(config.time_zone);
}
});
} else if ("__setDefaultTimeZone" in Intl.DateTimeFormat) {
// @ts-ignore
Intl.DateTimeFormat.__setDefaultTimeZone(config.time_zone);
}
}
this._updateHass({ config });
});

View File

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