Compare commits

..

16 Commits

Author SHA1 Message Date
Ludeeus
a0b11eb357 Move partial backup logic to backend 2021-12-16 12:36:02 +00:00
J. Nick Koston
6f9b2ee569 Add hardware version to the device info card (#10914) 2021-12-16 05:16:23 -06:00
Bram Kragten
4ebdca2a46 Bumped version to 20211215.0 2021-12-15 13:36:34 +01:00
Philip Allgaier
fc700fdaf0 Outline new collapsable area in state dev tools + auto-expand (#10917) 2021-12-15 13:15:50 +01:00
Philip Allgaier
d8e12f4280 Add tooltips and aria-labels to media player buttons (#10881) 2021-12-13 16:33:34 -08:00
krazos
86114758c3 Add group to input row domains to fix mobile focus issue (#10897) 2021-12-13 16:30:21 -08:00
Joakim Sørensen
792278cf17 Hide stop for hassio (#10905)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-12-13 16:29:01 -08:00
Joakim Sørensen
b8832f2121 Change entrypoint for Settings (#10904) 2021-12-13 16:08:19 -08:00
Joakim Sørensen
76339c90f7 Show app configuration in sidebar for non-admin users (#10890) 2021-12-13 16:06:46 -08:00
Bram Kragten
b3d4451035 Not valid config, but we support it in the editor (#10893) 2021-12-13 11:01:41 -08:00
Joakim Sørensen
dc58481918 Fix overriding username suggestion (#10899) 2021-12-13 18:56:44 +01:00
Philip Allgaier
14af735507 Fix tooltip and aria-label for password input field (#10898)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-12-13 16:32:45 +00:00
Joakim Sørensen
a7b558b64a Add no update available message (#10891) 2021-12-13 17:20:38 +01:00
Joakim Sørensen
b7665bef6f Don't backup core for supervisor/os updates (#10886) 2021-12-13 10:53:02 +01:00
Christopher Toth
5ec37a35f1 Fix all instances where HTML ARIA-ROLE should actually just be role (#10888) 2021-12-13 08:35:46 +00:00
Philip Allgaier
91bb2ddcc4 Make energy graph colors brighter in dark mode (#10789) 2021-12-12 14:10:30 +01:00
28 changed files with 307 additions and 159 deletions

View File

@@ -206,6 +206,7 @@ const createDeviceRegistryEntries = (
model: "Mock Device", model: "Mock Device",
name: "Tag Reader", name: "Tag Reader",
sw_version: null, sw_version: null,
hw_version: "1.0.0",
id: "mock-device-id", id: "mock-device-id",
identifiers: [], identifiers: [],
via_device_id: null, via_device_id: null,

View File

@@ -29,10 +29,6 @@ import {
HassioAddonDetails, HassioAddonDetails,
updateHassioAddon, updateHassioAddon,
} from "../../../src/data/hassio/addon"; } from "../../../src/data/hassio/addon";
import {
createHassioPartialBackup,
HassioPartialBackupCreateParams,
} from "../../../src/data/hassio/backup";
import { import {
extractApiErrorMessage, extractApiErrorMessage,
ignoreSupervisorError, ignoreSupervisorError,
@@ -103,7 +99,7 @@ class UpdateAvailableCard extends LitElement {
@state() private _addonInfo?: HassioAddonDetails; @state() private _addonInfo?: HassioAddonDetails;
@state() private _action: "backup" | "update" | null = null; @state() private _updating = false;
@state() private _error?: string; @state() private _error?: string;
@@ -132,7 +128,13 @@ class UpdateAvailableCard extends LitElement {
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""} : ""}
${this._action === null ${this._version === this._version_latest
? html`<p>
${this.supervisor.localize("update_available.no_update", {
name: this._name,
})}
</p>`
: !this._updating
? html` ? html`
${this._changelogContent ${this._changelogContent
? html` ? html`
@@ -166,18 +168,13 @@ class UpdateAvailableCard extends LitElement {
: html`<ha-circular-progress alt="Updating" size="large" active> : html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress> </ha-circular-progress>
<p class="progress-text"> <p class="progress-text">
${this._action === "update" ${this.supervisor.localize("update_available.updating", {
? this.supervisor.localize("update_available.updating", { name: this._name,
name: this._name, version: this._version_latest,
version: this._version_latest, })}
})
: this.supervisor.localize(
"update_available.creating_backup",
{ name: this._name }
)}
</p>`} </p>`}
</div> </div>
${this._action === null ${this._version !== this._version_latest && !this._updating
? html` ? html`
<div class="card-actions"> <div class="card-actions">
${changelog ${changelog
@@ -224,6 +221,9 @@ class UpdateAvailableCard extends LitElement {
} }
get _shouldCreateBackup(): boolean { get _shouldCreateBackup(): boolean {
if (this._updateType && !["core", "addon"].includes(this._updateType)) {
return false;
}
const checkbox = this.shadowRoot?.querySelector("ha-checkbox"); const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
if (checkbox) { if (checkbox) {
return checkbox.checked; return checkbox.checked;
@@ -310,37 +310,16 @@ class UpdateAvailableCard extends LitElement {
private async _update() { private async _update() {
this._error = undefined; this._error = undefined;
if (this._shouldCreateBackup) { this._updating = true;
let backupArgs: HassioPartialBackupCreateParams;
if (this._updateType === "addon") {
backupArgs = {
name: `addon_${this.addonSlug}_${this._version}`,
addons: [this.addonSlug!],
homeassistant: false,
};
} else {
backupArgs = {
name: `${this._updateType}_${this._version}`,
folders: ["homeassistant"],
homeassistant: true,
};
}
this._action = "backup";
try {
await createHassioPartialBackup(this.hass, backupArgs);
} catch (err: any) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
this._action = "update";
try { try {
if (this._updateType === "addon") { if (this._updateType === "addon") {
await updateHassioAddon(this.hass, this.addonSlug!); await updateHassioAddon(
this.hass,
this.addonSlug!,
this._shouldCreateBackup
);
} else if (this._updateType === "core") { } else if (this._updateType === "core") {
await updateCore(this.hass); await updateCore(this.hass, this._shouldCreateBackup);
} else if (this._updateType === "os") { } else if (this._updateType === "os") {
await updateOS(this.hass); await updateOS(this.hass);
} else if (this._updateType === "supervisor") { } else if (this._updateType === "supervisor") {
@@ -349,7 +328,7 @@ class UpdateAvailableCard extends LitElement {
} catch (err: any) { } catch (err: any) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) { if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this._error = extractApiErrorMessage(err); this._error = extractApiErrorMessage(err);
this._action = null; this._updating = false;
return; return;
} }
} }

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup( setup(
name="home-assistant-frontend", name="home-assistant-frontend",
version="20211212.0", version="20211215.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

@@ -208,6 +208,7 @@ export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [
export const DOMAINS_INPUT_ROW = [ export const DOMAINS_INPUT_ROW = [
"cover", "cover",
"fan", "fan",
"group",
"humidifier", "humidifier",
"input_boolean", "input_boolean",
"input_datetime", "input_datetime",

View File

@@ -46,6 +46,7 @@ class HaAlert extends LitElement {
rtl: this.rtl, rtl: this.rtl,
[this.alertType]: true, [this.alertType]: true,
})}" })}"
role="alert"
> >
<div class="icon ${this.title ? "" : "no-title"}"> <div class="icon ${this.title ? "" : "no-title"}">
<slot name="icon"> <slot name="icon">

View File

@@ -66,7 +66,7 @@ export class HaFormString extends LitElement implements HaFormElement {
${isPassword ${isPassword
? html`<ha-icon-button ? html`<ha-icon-button
toggles toggles
.label="Click to toggle between masked and clear password" .label=${`${this._unmaskedPassword ? "Hide" : "Show"} password`}
@click=${this._toggleUnmaskedPassword} @click=${this._toggleUnmaskedPassword}
tabindex="-1" tabindex="-1"
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye} .path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}

View File

@@ -3,6 +3,7 @@ import {
mdiBell, mdiBell,
mdiCalendar, mdiCalendar,
mdiCart, mdiCart,
mdiCellphoneCog,
mdiChartBox, mdiChartBox,
mdiClose, mdiClose,
mdiCog, mdiCog,
@@ -43,7 +44,10 @@ import {
PersistentNotification, PersistentNotification,
subscribeNotifications, subscribeNotifications,
} from "../data/persistent_notification"; } from "../data/persistent_notification";
import { getExternalConfig } from "../external_app/external_config"; import {
ExternalConfig,
getExternalConfig,
} from "../external_app/external_config";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo, Route } from "../types"; import type { HomeAssistant, PanelInfo, Route } from "../types";
@@ -188,6 +192,8 @@ class HaSidebar extends LitElement {
@property({ type: Boolean }) public editMode = false; @property({ type: Boolean }) public editMode = false;
@state() private _externalConfig?: ExternalConfig;
@state() private _notifications?: PersistentNotification[]; @state() private _notifications?: PersistentNotification[];
@state() private _renderEmptySortable = false; @state() private _renderEmptySortable = false;
@@ -234,6 +240,7 @@ class HaSidebar extends LitElement {
changedProps.has("expanded") || changedProps.has("expanded") ||
changedProps.has("narrow") || changedProps.has("narrow") ||
changedProps.has("alwaysExpand") || changedProps.has("alwaysExpand") ||
changedProps.has("_externalConfig") ||
changedProps.has("_notifications") || changedProps.has("_notifications") ||
changedProps.has("editMode") || changedProps.has("editMode") ||
changedProps.has("_renderEmptySortable") || changedProps.has("_renderEmptySortable") ||
@@ -264,14 +271,15 @@ class HaSidebar extends LitElement {
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
if (this.hass && this.hass.auth.external) {
getExternalConfig(this.hass.auth.external).then((conf) => {
this._externalConfig = conf;
});
}
subscribeNotifications(this.hass.connection, (notifications) => { subscribeNotifications(this.hass.connection, (notifications) => {
this._notifications = notifications; this._notifications = notifications;
}); });
// Temporary workaround for a bug in Android. Can be removed in Home Assistant 2022.2
if (this.hass.auth.external) {
getExternalConfig(this.hass.auth.external);
}
} }
protected updated(changedProps) { protected updated(changedProps) {
@@ -364,6 +372,7 @@ class HaSidebar extends LitElement {
: this._renderPanels(beforeSpacer)} : this._renderPanels(beforeSpacer)}
${this._renderSpacer()} ${this._renderSpacer()}
${this._renderPanels(afterSpacer)} ${this._renderPanels(afterSpacer)}
${this._renderExternalConfiguration()}
</paper-listbox> </paper-listbox>
`; `;
} }
@@ -393,7 +402,7 @@ class HaSidebar extends LitElement {
) { ) {
return html` return html`
<a <a
aria-role="option" role="option"
href=${`/${urlPath}`} href=${`/${urlPath}`}
data-panel=${urlPath} data-panel=${urlPath}
tabindex="-1" tabindex="-1"
@@ -498,7 +507,7 @@ class HaSidebar extends LitElement {
> >
<paper-icon-item <paper-icon-item
class="notifications" class="notifications"
aria-role="option" role="option"
@click=${this._handleShowNotificationDrawer} @click=${this._handleShowNotificationDrawer}
> >
<ha-svg-icon slot="item-icon" .path=${mdiBell}></ha-svg-icon> <ha-svg-icon slot="item-icon" .path=${mdiBell}></ha-svg-icon>
@@ -529,7 +538,7 @@ class HaSidebar extends LitElement {
href="/profile" href="/profile"
data-panel="panel" data-panel="panel"
tabindex="-1" tabindex="-1"
aria-role="option" role="option"
aria-label=${this.hass.localize("panel.profile")} aria-label=${this.hass.localize("panel.profile")}
@mouseenter=${this._itemMouseEnter} @mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave} @mouseleave=${this._itemMouseLeave}
@@ -548,6 +557,43 @@ class HaSidebar extends LitElement {
</a>`; </a>`;
} }
private _renderExternalConfiguration() {
return html`${!this.hass.user?.is_admin &&
this._externalConfig &&
this._externalConfig.hasSettingsScreen
? html`
<a
role="option"
aria-label=${this.hass.localize(
"ui.sidebar.external_app_configuration"
)}
href="#external-app-configuration"
tabindex="-1"
@click=${this._handleExternalAppConfiguration}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
<ha-svg-icon
slot="item-icon"
.path=${mdiCellphoneCog}
></ha-svg-icon>
<span class="item-text">
${this.hass.localize("ui.sidebar.external_app_configuration")}
</span>
</paper-icon-item>
</a>
`
: ""}`;
}
private _handleExternalAppConfiguration(ev: Event) {
ev.preventDefault();
this.hass.auth.external!.fireMessage({
type: "config_screen/show",
});
}
private get _tooltip() { private get _tooltip() {
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement; return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
} }

View File

@@ -13,6 +13,7 @@ export interface DeviceRegistryEntry {
model: string | null; model: string | null;
name: string | null; name: string | null;
sw_version: string | null; sw_version: string | null;
hw_version: string | null;
via_device_id: string | null; via_device_id: string | null;
area_id: string | null; area_id: string | null;
name_by_user: string | null; name_by_user: string | null;

View File

@@ -302,7 +302,8 @@ export const installHassioAddon = async (
export const updateHassioAddon = async ( export const updateHassioAddon = async (
hass: HomeAssistant, hass: HomeAssistant,
slug: string slug: string,
backup: boolean
): Promise<void> => { ): Promise<void> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) { if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({ await hass.callWS({
@@ -310,11 +311,13 @@ export const updateHassioAddon = async (
endpoint: `/store/addons/${slug}/update`, endpoint: `/store/addons/${slug}/update`,
method: "post", method: "post",
timeout: null, timeout: null,
data: { backup: backup },
}); });
} else { } else {
await hass.callApi<HassioResponse<void>>( await hass.callApi<HassioResponse<void>>(
"POST", "POST",
`hassio/addons/${slug}/update` `hassio/addons/${slug}/update`,
{ backup: backup }
); );
} }
}; };

View File

@@ -156,6 +156,7 @@ export interface MediaPlayerThumbnail {
export interface ControlButton { export interface ControlButton {
icon: string; icon: string;
// Used as key for action as well as tooltip and aria-label translation key
action: string; action: string;
} }

View File

@@ -6,15 +6,18 @@ export const restartCore = async (hass: HomeAssistant) => {
await hass.callService("homeassistant", "restart"); await hass.callService("homeassistant", "restart");
}; };
export const updateCore = async (hass: HomeAssistant) => { export const updateCore = async (hass: HomeAssistant, backup: boolean) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) { if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({ await hass.callWS({
type: "supervisor/api", type: "supervisor/api",
endpoint: "/core/update", endpoint: "/core/update",
method: "post", method: "post",
timeout: null, timeout: null,
data: { backup: backup },
}); });
} else { } else {
await hass.callApi<HassioResponse<void>>("POST", `hassio/core/update`); await hass.callApi<HassioResponse<void>>("POST", `hassio/core/update`, {
backup: backup,
});
} }
}; };

View File

@@ -65,6 +65,9 @@ class MoreInfoMediaPlayer extends LitElement {
action=${control.action} action=${control.action}
@click=${this._handleClick} @click=${this._handleClick}
.path=${control.icon} .path=${control.icon}
.label=${this.hass.localize(
`ui.card.media_player.${control.action}`
)}
> >
</ha-icon-button> </ha-icon-button>
` `

View File

@@ -95,8 +95,11 @@ class OnboardingCreateUser extends LitElement {
private _handleValueChanged( private _handleValueChanged(
ev: PolymerChangedEvent<HaFormDataContainer> ev: PolymerChangedEvent<HaFormDataContainer>
): void { ): void {
const nameChanged = ev.detail.value.name !== this._newUser.name;
this._newUser = ev.detail.value; this._newUser = ev.detail.value;
this._maybePopulateUsername(); if (nameChanged) {
this._maybePopulateUsername();
}
this._formError.password_confirm = this._formError.password_confirm =
this._newUser.password !== this._newUser.password_confirm this._newUser.password !== this._newUser.password_confirm
? this.localize( ? this.localize(

View File

@@ -17,9 +17,9 @@ import {
const stateConditionStruct = object({ const stateConditionStruct = object({
condition: literal("state"), condition: literal("state"),
entity_id: string(), entity_id: optional(string()),
attribute: optional(string()), attribute: optional(string()),
state: string(), state: optional(string()),
for: optional(union([string(), forDictStruct])), for: optional(union([string(), forDictStruct])),
}); });

View File

@@ -28,7 +28,7 @@ const stateTriggerStruct = assign(
baseTriggerStruct, baseTriggerStruct,
object({ object({
platform: literal("state"), platform: literal("state"),
entity_id: string(), entity_id: optional(string()),
attribute: optional(string()), attribute: optional(string()),
from: optional(string()), from: optional(string()),
to: optional(string()), to: optional(string()),

View File

@@ -31,7 +31,7 @@ class HaConfigNavigation extends LitElement {
: canShowPage(this.hass, page) : canShowPage(this.hass, page)
) )
? html` ? html`
<a href=${page.path} aria-role="option" tabindex="-1"> <a href=${page.path} role="option" tabindex="-1">
<paper-icon-item @click=${this._entryClicked}> <paper-icon-item @click=${this._entryClicked}>
<div <div
class=${page.iconColor ? "icon-background" : ""} class=${page.iconColor ? "icon-background" : ""}

View File

@@ -66,6 +66,17 @@ export class HaDeviceCard extends LitElement {
</div> </div>
` `
: ""} : ""}
${this.device.hw_version
? html`
<div class="extra-info">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.hardware",
"version",
this.device.hw_version
)}
</div>
`
: ""}
<slot></slot> <slot></slot>
</div> </div>
<slot name="actions"></slot> <slot name="actions"></slot>

View File

@@ -110,7 +110,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconColor: "#8E24AA", iconColor: "#8E24AA",
}, },
{ {
path: "/config/core", path: "/config/server_control",
translationKey: "settings", translationKey: "settings",
iconPath: mdiCog, iconPath: mdiCog,
iconColor: "#4A5963", iconColor: "#4A5963",

View File

@@ -95,7 +95,7 @@ class OZWConfigDashboard extends LitElement {
<ha-card> <ha-card>
<a <a
href="/config/ozw/network/${instance.ozw_instance}" href="/config/ozw/network/${instance.ozw_instance}"
aria-role="option" role="option"
tabindex="-1" tabindex="-1"
> >
<paper-icon-item> <paper-icon-item>

View File

@@ -5,6 +5,7 @@ import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { componentsWithService } from "../../../common/config/components_with_service"; import { componentsWithService } from "../../../common/config/components_with_service";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/buttons/ha-call-service-button"; import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card"; import "../../../components/ha-card";
import { checkCoreConfig } from "../../../data/core"; import { checkCoreConfig } from "../../../data/core";
@@ -157,18 +158,20 @@ export class HaConfigServerControl extends LitElement {
"ui.panel.config.server_control.section.server_management.restart" "ui.panel.config.server_control.section.server_management.restart"
)} )}
</ha-call-service-button> </ha-call-service-button>
<ha-call-service-button ${!isComponentLoaded(this.hass, "hassio")
class="warning" ? html`<ha-call-service-button
.hass=${this.hass} class="warning"
domain="homeassistant" .hass=${this.hass}
service="stop" domain="homeassistant"
confirmation=${this.hass.localize( service="stop"
"ui.panel.config.server_control.section.server_management.confirm_stop" confirmation=${this.hass.localize(
)} "ui.panel.config.server_control.section.server_management.confirm_stop"
>${this.hass.localize( )}
"ui.panel.config.server_control.section.server_management.stop" >${this.hass.localize(
)} "ui.panel.config.server_control.section.server_management.stop"
</ha-call-service-button> )}
</ha-call-service-button>`
: ""}
</div> </div>
</ha-card> </ha-card>

View File

@@ -143,7 +143,12 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
<h1> <h1>
[[localize('ui.panel.developer-tools.tabs.states.current_entities')]] [[localize('ui.panel.developer-tools.tabs.states.current_entities')]]
</h1> </h1>
<ha-expansion-panel header="Set state"> <ha-expansion-panel
header="Set state"
outlined
expanded="[[_expanded]]"
on-expanded-changed="expandedChanged"
>
<p> <p>
[[localize('ui.panel.developer-tools.tabs.states.description1')]]<br /> [[localize('ui.panel.developer-tools.tabs.states.description1')]]<br />
[[localize('ui.panel.developer-tools.tabs.states.description2')]] [[localize('ui.panel.developer-tools.tabs.states.description2')]]
@@ -353,6 +358,11 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
"computeEntities(hass, _entityFilter, _stateFilter, _attributeFilter)", "computeEntities(hass, _entityFilter, _stateFilter, _attributeFilter)",
}, },
_expanded: {
type: Boolean,
value: false,
},
narrow: { narrow: {
type: Boolean, type: Boolean,
reflectToAttribute: true, reflectToAttribute: true,
@@ -376,6 +386,7 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
this._entity = state; this._entity = state;
this._state = state.state; this._state = state.state;
this._stateAttributes = dump(state.attributes); this._stateAttributes = dump(state.attributes);
this._expanded = true;
ev.preventDefault(); ev.preventDefault();
} }
@@ -393,6 +404,11 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
this._entity = state; this._entity = state;
this._state = state.state; this._state = state.state;
this._stateAttributes = dump(state.attributes); this._stateAttributes = dump(state.attributes);
this._expanded = true;
}
expandedChanged(ev) {
this._expanded = ev.detail.expanded;
} }
entityMoreInfo(ev) { entityMoreInfo(ev) {

View File

@@ -26,7 +26,7 @@ import {
rgb2hex, rgb2hex,
rgb2lab, rgb2lab,
} from "../../../../common/color/convert-color"; } from "../../../../common/color/convert-color";
import { labDarken } from "../../../../common/color/lab"; import { labBrighten, labDarken } from "../../../../common/color/lab";
import { import {
EnergyData, EnergyData,
getEnergyDataCollection, getEnergyDataCollection,
@@ -247,10 +247,15 @@ export class HuiEnergyGasGraphCard
const data: ChartDataset<"bar" | "line">[] = []; const data: ChartDataset<"bar" | "line">[] = [];
const entity = this.hass.states[source.stat_energy_from]; const entity = this.hass.states[source.stat_energy_from];
const borderColor = const modifiedColor =
idx > 0 idx > 0
? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(gasColor)), idx))) ? this.hass.themes.darkMode
: gasColor; ? labBrighten(rgb2lab(hex2rgb(gasColor)), idx)
: labDarken(rgb2lab(hex2rgb(gasColor)), idx)
: undefined;
const borderColor = modifiedColor
? rgb2hex(lab2rgb(modifiedColor))
: gasColor;
let prevValue: number | null = null; let prevValue: number | null = null;
let prevStart: string | null = null; let prevStart: string | null = null;

View File

@@ -1,9 +1,3 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import { classMap } from "lit/directives/class-map";
import "../../../../components/ha-card";
import { import {
ChartData, ChartData,
ChartDataset, ChartDataset,
@@ -17,16 +11,26 @@ import {
isToday, isToday,
startOfToday, startOfToday,
} from "date-fns"; } from "date-fns";
import { HomeAssistant } from "../../../../types"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { LovelaceCard } from "../../types"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { EnergySolarGraphCardConfig } from "../types"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { import {
hex2rgb, hex2rgb,
lab2rgb, lab2rgb,
rgb2hex, rgb2hex,
rgb2lab, rgb2lab,
} from "../../../../common/color/convert-color"; } from "../../../../common/color/convert-color";
import { labDarken } from "../../../../common/color/lab"; import { labBrighten, labDarken } from "../../../../common/color/lab";
import { formatTime } from "../../../../common/datetime/format_time";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import {
formatNumber,
numberFormatToLocale,
} from "../../../../common/number/format_number";
import "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card";
import { import {
EnergyData, EnergyData,
EnergySolarForecasts, EnergySolarForecasts,
@@ -34,15 +38,11 @@ import {
getEnergySolarForecasts, getEnergySolarForecasts,
SolarSourceTypeEnergyPreference, SolarSourceTypeEnergyPreference,
} from "../../../../data/energy"; } from "../../../../data/energy";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import "../../../../components/chart/ha-chart-base";
import {
formatNumber,
numberFormatToLocale,
} from "../../../../common/number/format_number";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { FrontendLocaleData } from "../../../../data/translation"; import { FrontendLocaleData } from "../../../../data/translation";
import { formatTime } from "../../../../common/datetime/format_time"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergySolarGraphCardConfig } from "../types";
@customElement("hui-energy-solar-graph-card") @customElement("hui-energy-solar-graph-card")
export class HuiEnergySolarGraphCard export class HuiEnergySolarGraphCard
@@ -258,10 +258,15 @@ export class HuiEnergySolarGraphCard
const data: ChartDataset<"bar" | "line">[] = []; const data: ChartDataset<"bar" | "line">[] = [];
const entity = this.hass.states[source.stat_energy_from]; const entity = this.hass.states[source.stat_energy_from];
const borderColor = const modifiedColor =
idx > 0 idx > 0
? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(solarColor)), idx))) ? this.hass.themes.darkMode
: solarColor; ? labBrighten(rgb2lab(hex2rgb(solarColor)), idx)
: labDarken(rgb2lab(hex2rgb(solarColor)), idx)
: undefined;
const borderColor = modifiedColor
? rgb2hex(lab2rgb(modifiedColor))
: solarColor;
let prevValue: number | null = null; let prevValue: number | null = null;
let prevStart: string | null = null; let prevStart: string | null = null;

View File

@@ -17,7 +17,7 @@ import {
rgb2lab, rgb2lab,
hex2rgb, hex2rgb,
} from "../../../../common/color/convert-color"; } from "../../../../common/color/convert-color";
import { labDarken } from "../../../../common/color/lab"; import { labBrighten, labDarken } from "../../../../common/color/lab";
import { computeStateName } from "../../../../common/entity/compute_state_name"; import { computeStateName } from "../../../../common/entity/compute_state_name";
import { formatNumber } from "../../../../common/number/format_number"; import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/chart/statistics-chart"; import "../../../../components/chart/statistics-chart";
@@ -170,12 +170,17 @@ export class HuiEnergySourcesTableCard
this._data!.stats[source.stat_energy_from] this._data!.stats[source.stat_energy_from]
) || 0; ) || 0;
totalSolar += energy; totalSolar += energy;
const color =
const modifiedColor =
idx > 0 idx > 0
? rgb2hex( ? this.hass.themes.darkMode
lab2rgb(labDarken(rgb2lab(hex2rgb(solarColor)), idx)) ? labBrighten(rgb2lab(hex2rgb(solarColor)), idx)
) : labDarken(rgb2lab(hex2rgb(solarColor)), idx)
: solarColor; : undefined;
const color = modifiedColor
? rgb2hex(lab2rgb(modifiedColor))
: solarColor;
return html`<tr class="mdc-data-table__row"> return html`<tr class="mdc-data-table__row">
<td class="mdc-data-table__cell cell-bullet"> <td class="mdc-data-table__cell cell-bullet">
<div <div
@@ -229,22 +234,26 @@ export class HuiEnergySourcesTableCard
this._data!.stats[source.stat_energy_to] this._data!.stats[source.stat_energy_to]
) || 0; ) || 0;
totalBattery += energyFrom - energyTo; totalBattery += energyFrom - energyTo;
const fromColor =
const modifiedFromColor =
idx > 0 idx > 0
? rgb2hex( ? this.hass.themes.darkMode
lab2rgb( ? labBrighten(rgb2lab(hex2rgb(batteryFromColor)), idx)
labDarken(rgb2lab(hex2rgb(batteryFromColor)), idx) : labDarken(rgb2lab(hex2rgb(batteryFromColor)), idx)
) : undefined;
) const fromColor = modifiedFromColor
: batteryFromColor; ? rgb2hex(lab2rgb(modifiedFromColor))
const toColor = : batteryFromColor;
const modifiedToColor =
idx > 0 idx > 0
? rgb2hex( ? this.hass.themes.darkMode
lab2rgb( ? labBrighten(rgb2lab(hex2rgb(batteryToColor)), idx)
labDarken(rgb2lab(hex2rgb(batteryToColor)), idx) : labDarken(rgb2lab(hex2rgb(batteryToColor)), idx)
) : undefined;
) const toColor = modifiedToColor
: batteryToColor; ? rgb2hex(lab2rgb(modifiedToColor))
: batteryToColor;
return html`<tr class="mdc-data-table__row"> return html`<tr class="mdc-data-table__row">
<td class="mdc-data-table__cell cell-bullet"> <td class="mdc-data-table__cell cell-bullet">
<div <div
@@ -331,14 +340,17 @@ export class HuiEnergySourcesTableCard
if (cost !== null) { if (cost !== null) {
totalGridCost += cost; totalGridCost += cost;
} }
const color =
const modifiedColor =
idx > 0 idx > 0
? rgb2hex( ? this.hass.themes.darkMode
lab2rgb( ? labBrighten(rgb2lab(hex2rgb(consumptionColor)), idx)
labDarken(rgb2lab(hex2rgb(consumptionColor)), idx) : labDarken(rgb2lab(hex2rgb(consumptionColor)), idx)
) : undefined;
) const color = modifiedColor
: consumptionColor; ? rgb2hex(lab2rgb(modifiedColor))
: consumptionColor;
return html`<tr class="mdc-data-table__row"> return html`<tr class="mdc-data-table__row">
<td class="mdc-data-table__cell cell-bullet"> <td class="mdc-data-table__cell cell-bullet">
<div <div
@@ -391,12 +403,17 @@ export class HuiEnergySourcesTableCard
if (cost !== null) { if (cost !== null) {
totalGridCost += cost; totalGridCost += cost;
} }
const color =
const modifiedColor =
idx > 0 idx > 0
? rgb2hex( ? this.hass.themes.darkMode
lab2rgb(labDarken(rgb2lab(hex2rgb(returnColor)), idx)) ? labBrighten(rgb2lab(hex2rgb(returnColor)), idx)
) : labDarken(rgb2lab(hex2rgb(returnColor)), idx)
: returnColor; : undefined;
const color = modifiedColor
? rgb2hex(lab2rgb(modifiedColor))
: returnColor;
return html`<tr class="mdc-data-table__row"> return html`<tr class="mdc-data-table__row">
<td class="mdc-data-table__cell cell-bullet"> <td class="mdc-data-table__cell cell-bullet">
<div <div
@@ -473,12 +490,17 @@ export class HuiEnergySourcesTableCard
if (cost !== null) { if (cost !== null) {
totalGasCost += cost; totalGasCost += cost;
} }
const color =
const modifiedColor =
idx > 0 idx > 0
? rgb2hex( ? this.hass.themes.darkMode
lab2rgb(labDarken(rgb2lab(hex2rgb(gasColor)), idx)) ? labBrighten(rgb2lab(hex2rgb(gasColor)), idx)
) : labDarken(rgb2lab(hex2rgb(gasColor)), idx)
: gasColor; : undefined;
const color = modifiedColor
? rgb2hex(lab2rgb(modifiedColor))
: gasColor;
return html`<tr class="mdc-data-table__row"> return html`<tr class="mdc-data-table__row">
<td class="mdc-data-table__cell cell-bullet"> <td class="mdc-data-table__cell cell-bullet">
<div <div

View File

@@ -1,10 +1,10 @@
import { ChartData, ChartDataset, ChartOptions } from "chart.js"; import { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { import {
startOfToday, addHours,
differenceInDays,
endOfToday, endOfToday,
isToday, isToday,
differenceInDays, startOfToday,
addHours,
} from "date-fns"; } from "date-fns";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
@@ -17,7 +17,7 @@ import {
rgb2hex, rgb2hex,
rgb2lab, rgb2lab,
} from "../../../../common/color/convert-color"; } from "../../../../common/color/convert-color";
import { labDarken } from "../../../../common/color/lab"; import { labBrighten, labDarken } from "../../../../common/color/lab";
import { formatTime } from "../../../../common/datetime/format_time"; import { formatTime } from "../../../../common/datetime/format_time";
import { computeStateName } from "../../../../common/entity/compute_state_name"; import { computeStateName } from "../../../../common/entity/compute_state_name";
import { import {
@@ -477,10 +477,16 @@ export class HuiEnergyUsageGraphCard
Object.entries(sources).forEach(([statId, source], idx) => { Object.entries(sources).forEach(([statId, source], idx) => {
const data: ChartDataset<"bar">[] = []; const data: ChartDataset<"bar">[] = [];
const entity = this.hass.states[statId]; const entity = this.hass.states[statId];
const borderColor =
const modifiedColor =
idx > 0 idx > 0
? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(colors[type])), idx))) ? this.hass.themes.darkMode
: colors[type]; ? labBrighten(rgb2lab(hex2rgb(colors[type])), idx)
: labDarken(rgb2lab(hex2rgb(colors[type])), idx)
: undefined;
const borderColor = modifiedColor
? rgb2hex(lab2rgb(modifiedColor))
: colors[type];
data.push({ data.push({
label: label:

View File

@@ -235,6 +235,9 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
<div> <div>
<ha-icon-button <ha-icon-button
.path=${mdiDotsVertical} .path=${mdiDotsVertical}
.label=${this.hass.localize(
"ui.panel.lovelace.cards.show_more_info"
)}
class="more-info" class="more-info"
@click=${this._handleMoreInfo} @click=${this._handleMoreInfo}
></ha-icon-button> ></ha-icon-button>

View File

@@ -30,6 +30,7 @@ import "../../../components/ha-slider";
import { UNAVAILABLE, UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity"; import { UNAVAILABLE, UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity";
import { import {
computeMediaDescription, computeMediaDescription,
ControlButton,
MediaPlayerEntity, MediaPlayerEntity,
SUPPORT_NEXT_TRACK, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PAUSE,
@@ -108,6 +109,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
} }
const entityState = stateObj.state; const entityState = stateObj.state;
const controlButton = this._computeControlButton(stateObj);
const buttons = html` const buttons = html`
${!this._narrow && ${!this._narrow &&
@@ -116,6 +118,9 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
? html` ? html`
<ha-icon-button <ha-icon-button
.path=${mdiSkipPrevious} .path=${mdiSkipPrevious}
.label=${this.hass.localize(
"ui.card.media_player.media_previous_track"
)}
@click=${this._previousTrack} @click=${this._previousTrack}
></ha-icon-button> ></ha-icon-button>
` `
@@ -130,7 +135,10 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
supportsFeature(stateObj, SUPPORT_PAUSE))) supportsFeature(stateObj, SUPPORT_PAUSE)))
? html` ? html`
<ha-icon-button <ha-icon-button
.path=${this._computeControlIcon(stateObj)} .path=${controlButton.icon}
.label=${this.hass.localize(
`ui.card.media_player.${controlButton.action}`
)}
@click=${this._playPauseStop} @click=${this._playPauseStop}
></ha-icon-button> ></ha-icon-button>
` `
@@ -140,6 +148,9 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
? html` ? html`
<ha-icon-button <ha-icon-button
.path=${mdiSkipNext} .path=${mdiSkipNext}
.label=${this.hass.localize(
"ui.card.media_player.media_next_track"
)}
@click=${this._nextTrack} @click=${this._nextTrack}
></ha-icon-button> ></ha-icon-button>
` `
@@ -162,6 +173,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
? html` ? html`
<ha-icon-button <ha-icon-button
.path=${mdiPower} .path=${mdiPower}
.label=${this.hass.localize("ui.card.media_player.turn_on")}
@click=${this._togglePower} @click=${this._togglePower}
></ha-icon-button> ></ha-icon-button>
` `
@@ -175,6 +187,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
? html` ? html`
<ha-icon-button <ha-icon-button
.path=${mdiPower} .path=${mdiPower}
.label=${this.hass.localize("ui.card.media_player.turn_off")}
@click=${this._togglePower} @click=${this._togglePower}
></ha-icon-button> ></ha-icon-button>
` `
@@ -193,6 +206,13 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
.path=${stateObj.attributes.is_volume_muted .path=${stateObj.attributes.is_volume_muted
? mdiVolumeOff ? mdiVolumeOff
: mdiVolumeHigh} : mdiVolumeHigh}
.label=${this.hass.localize(
`ui.card.media_player.${
stateObj.attributes.is_volume_muted
? "media_volume_mute"
: "media_volume_unmute"
}`
)}
@click=${this._toggleMute} @click=${this._toggleMute}
></ha-icon-button> ></ha-icon-button>
` `
@@ -214,10 +234,16 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
? html` ? html`
<ha-icon-button <ha-icon-button
.path=${mdiVolumeMinus} .path=${mdiVolumeMinus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_down"
)}
@click=${this._volumeDown} @click=${this._volumeDown}
></ha-icon-button> ></ha-icon-button>
<ha-icon-button <ha-icon-button
.path=${mdiVolumePlus} .path=${mdiVolumePlus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_up"
)}
@click=${this._volumeUp} @click=${this._volumeUp}
></ha-icon-button> ></ha-icon-button>
` `
@@ -249,14 +275,14 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
this._veryNarrow = (this.clientWidth || 0) < 225; this._veryNarrow = (this.clientWidth || 0) < 225;
} }
private _computeControlIcon(stateObj: HassEntity): string { private _computeControlButton(stateObj: HassEntity): ControlButton {
return stateObj.state === "on" return stateObj.state === "on"
? mdiPlayPause ? { icon: mdiPlayPause, action: "media_play_pause" }
: stateObj.state !== "playing" : stateObj.state !== "playing"
? mdiPlay ? { icon: mdiPlay, action: "media_play" }
: supportsFeature(stateObj, SUPPORT_PAUSE) : supportsFeature(stateObj, SUPPORT_PAUSE)
? mdiPause ? { icon: mdiPause, action: "media_pause" }
: mdiStop; : { icon: mdiStop, action: "media_stop" };
} }
private _togglePower(): void { private _togglePower(): void {

View File

@@ -195,8 +195,14 @@
"turn_off": "Turn off", "turn_off": "Turn off",
"media_play": "Play", "media_play": "Play",
"media_play_pause": "Play/pause", "media_play_pause": "Play/pause",
"media_next_track": "Next", "media_pause": "Pause",
"media_previous_track": "Previous", "media_stop": "Stop",
"media_next_track": "Next track",
"media_previous_track": "Previous track",
"media_volume_up": "Volume up",
"media_volume_down": "Volume down",
"media_volume_mute": "Volume mute",
"media_volume_unmute": "Volume unmute",
"text_to_speak": "Text to speak" "text_to_speak": "Text to speak"
}, },
"persistent_notification": { "persistent_notification": {
@@ -689,7 +695,7 @@
"close_cover": "Close cover", "close_cover": "Close cover",
"open_tilt_cover": "Open cover tilt", "open_tilt_cover": "Open cover tilt",
"close_tilt_cover": "Close cover tilt", "close_tilt_cover": "Close cover tilt",
"stop_cover": "Stop cover from moving" "stop_cover": "Stop cover"
} }
}, },
"entity_registry": { "entity_registry": {
@@ -922,6 +928,7 @@
"dismiss": "Dismiss" "dismiss": "Dismiss"
}, },
"sidebar": { "sidebar": {
"external_app_configuration": "App Configuration",
"sidebar_toggle": "Sidebar Toggle", "sidebar_toggle": "Sidebar Toggle",
"done": "Done", "done": "Done",
"hide_panel": "Hide panel", "hide_panel": "Hide panel",
@@ -2437,6 +2444,7 @@
"manuf": "by {manufacturer}", "manuf": "by {manufacturer}",
"via": "Connected via", "via": "Connected via",
"firmware": "Firmware: {version}", "firmware": "Firmware: {version}",
"hardware": "Hardware: {version}",
"unnamed_entry": "Unnamed entry", "unnamed_entry": "Unnamed entry",
"unknown_via_device": "Unknown device", "unknown_via_device": "Unknown device",
"area": "In {area}", "area": "In {area}",
@@ -4269,7 +4277,8 @@
"create_backup": "Create backup before updating", "create_backup": "Create backup before updating",
"description": "You have {version} installed. Click update to update to version {newest_version}", "description": "You have {version} installed. Click update to update to version {newest_version}",
"updating": "Updating {name} to version {version}", "updating": "Updating {name} to version {version}",
"creating_backup": "Creating backup of {name}" "creating_backup": "Creating backup of {name}",
"no_update": "No update available for {name}"
}, },
"confirm": { "confirm": {
"restart": { "restart": {