Configuration Menu Updates 3 (#12377)

This commit is contained in:
Zack Barett 2022-04-24 17:26:01 -05:00 committed by GitHub
parent 3677c5be2c
commit 9706c56c5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 860 additions and 759 deletions

View File

@ -1,165 +1,167 @@
export const currencies = [
"AED",
"AFN",
"ALL",
"AMD",
"ANG",
"AOA",
"ARS",
"AUD",
"AWG",
"AZN",
"BAM",
"BBD",
"BDT",
"BGN",
"BHD",
"BIF",
"BMD",
"BND",
"BOB",
"BRL",
"BSD",
"BTN",
"BWP",
"BYN",
"BYR",
"BZD",
"CAD",
"CDF",
"CHF",
"CLP",
"CNY",
"COP",
"CRC",
"CUP",
"CVE",
"CZK",
"DJF",
"DKK",
"DOP",
"DZD",
"EGP",
"ERN",
"ETB",
"EUR",
"FJD",
"FKP",
"GBP",
"GEL",
"GHS",
"GIP",
"GMD",
"GNF",
"GTQ",
"GYD",
"HKD",
"HNL",
"HRK",
"HTG",
"HUF",
"IDR",
"ILS",
"INR",
"IQD",
"IRR",
"ISK",
"JMD",
"JOD",
"JPY",
"KES",
"KGS",
"KHR",
"KMF",
"KPW",
"KRW",
"KWD",
"KYD",
"KZT",
"LAK",
"LBP",
"LKR",
"LRD",
"LSL",
"LTL",
"LYD",
"MAD",
"MDL",
"MGA",
"MKD",
"MMK",
"MNT",
"MOP",
"MRO",
"MUR",
"MVR",
"MWK",
"MXN",
"MYR",
"MZN",
"NAD",
"NGN",
"NIO",
"NOK",
"NPR",
"NZD",
"OMR",
"PAB",
"PEN",
"PGK",
"PHP",
"PKR",
"PLN",
"PYG",
"QAR",
"RON",
"RSD",
"RUB",
"RWF",
"SAR",
"SBD",
"SCR",
"SDG",
"SEK",
"SGD",
"SHP",
"SLL",
"SOS",
"SRD",
"SSP",
"STD",
"SYP",
"SZL",
"THB",
"TJS",
"TMT",
"TND",
"TOP",
"TRY",
"TTD",
"TWD",
"TZS",
"UAH",
"UGX",
"USD",
"UYU",
"UZS",
"VEF",
"VND",
"VUV",
"WST",
"XAF",
"XCD",
"XOF",
"XPF",
"YER",
"ZAR",
"ZMK",
"ZWL",
];
export const createCurrencyListEl = () => {
const list = document.createElement("datalist");
list.id = "currencies";
for (const currency of [
"AED",
"AFN",
"ALL",
"AMD",
"ANG",
"AOA",
"ARS",
"AUD",
"AWG",
"AZN",
"BAM",
"BBD",
"BDT",
"BGN",
"BHD",
"BIF",
"BMD",
"BND",
"BOB",
"BRL",
"BSD",
"BTN",
"BWP",
"BYN",
"BYR",
"BZD",
"CAD",
"CDF",
"CHF",
"CLP",
"CNY",
"COP",
"CRC",
"CUP",
"CVE",
"CZK",
"DJF",
"DKK",
"DOP",
"DZD",
"EGP",
"ERN",
"ETB",
"EUR",
"FJD",
"FKP",
"GBP",
"GEL",
"GHS",
"GIP",
"GMD",
"GNF",
"GTQ",
"GYD",
"HKD",
"HNL",
"HRK",
"HTG",
"HUF",
"IDR",
"ILS",
"INR",
"IQD",
"IRR",
"ISK",
"JMD",
"JOD",
"JPY",
"KES",
"KGS",
"KHR",
"KMF",
"KPW",
"KRW",
"KWD",
"KYD",
"KZT",
"LAK",
"LBP",
"LKR",
"LRD",
"LSL",
"LTL",
"LYD",
"MAD",
"MDL",
"MGA",
"MKD",
"MMK",
"MNT",
"MOP",
"MRO",
"MUR",
"MVR",
"MWK",
"MXN",
"MYR",
"MZN",
"NAD",
"NGN",
"NIO",
"NOK",
"NPR",
"NZD",
"OMR",
"PAB",
"PEN",
"PGK",
"PHP",
"PKR",
"PLN",
"PYG",
"QAR",
"RON",
"RSD",
"RUB",
"RWF",
"SAR",
"SBD",
"SCR",
"SDG",
"SEK",
"SGD",
"SHP",
"SLL",
"SOS",
"SRD",
"SSP",
"STD",
"SYP",
"SZL",
"THB",
"TJS",
"TMT",
"TND",
"TOP",
"TRY",
"TTD",
"TWD",
"TZS",
"UAH",
"UGX",
"USD",
"UYU",
"UZS",
"VEF",
"VND",
"VUV",
"WST",
"XAF",
"XCD",
"XOF",
"XPF",
"YER",
"ZAR",
"ZMK",
"ZWL",
]) {
for (const currency of currencies) {
const option = document.createElement("option");
option.value = currency;
option.innerHTML = currency;

View File

@ -0,0 +1,77 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { roundWithOneDecimal } from "../util/calculate";
import "./ha-bar";
import "./ha-settings-row";
@customElement("ha-metric")
class HaMetric extends LitElement {
@property({ type: Number }) public value!: number;
@property({ type: String }) public description!: string;
@property({ type: String }) public tooltip?: string;
protected render(): TemplateResult {
const roundedValue = roundWithOneDecimal(this.value);
return html`<ha-settings-row>
<span slot="heading"> ${this.description} </span>
<div slot="description" .title=${this.tooltip ?? ""}>
<span class="value"> ${roundedValue} % </span>
<ha-bar
class=${classMap({
"target-warning": roundedValue > 50,
"target-critical": roundedValue > 85,
})}
.value=${this.value}
></ha-bar>
</div>
</ha-settings-row>`;
}
static get styles(): CSSResultGroup {
return css`
ha-settings-row {
padding: 0;
height: 54px;
width: 100%;
}
ha-settings-row > div[slot="description"] {
white-space: normal;
color: var(--secondary-text-color);
display: flex;
justify-content: space-between;
}
ha-bar {
--ha-bar-primary-color: var(
--metric-bar-ok-color,
var(--success-color)
);
}
.target-warning {
--ha-bar-primary-color: var(
--metric-bar-warning-color,
var(--warning-color)
);
}
.target-critical {
--ha-bar-primary-color: var(
--metric-bar-critical-color,
var(--error-color)
);
}
.value {
width: 48px;
padding-right: 4px;
flex-shrink: 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-metric": HaMetric;
}
}

View File

@ -4,9 +4,9 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
import type { HomeAssistant } from "../types";
import "./ha-clickable-list-item";
import "./ha-icon-next";
import "./ha-svg-icon";
import "./ha-clickable-list-item";
@customElement("ha-navigation-list")
class HaNavigationList extends LitElement {
@ -78,7 +78,7 @@ class HaNavigationList extends LitElement {
.icon-background ha-svg-icon {
color: #fff;
}
mwc-list-item {
ha-clickable-list-item {
cursor: pointer;
font-size: var(--navigation-list-item-title-font-size);
}

View File

@ -224,7 +224,7 @@ class HaBlueprintOverview extends LitElement {
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${configSections.blueprints}
.tabs=${configSections.automations}
.columns=${this._columns(this.narrow, this.hass.language)}
.data=${this._processedBlueprints(this.blueprints)}
id="entity_id"

View File

@ -1,342 +0,0 @@
import "@material/mwc-button/mwc-button";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { UNIT_C } from "../../../common/const";
import { createCurrencyListEl } from "../../../components/currency-datalist";
import "../../../components/ha-card";
import "../../../components/map/ha-locations-editor";
import type { MarkerLocation } from "../../../components/map/ha-locations-editor";
import { createTimezoneListEl } from "../../../components/timezone-datalist";
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
import { SYMBOL_TO_ISO } from "../../../data/currency";
import type { PolymerChangedEvent } from "../../../polymer-types";
import type { HomeAssistant } from "../../../types";
import "../../../components/ha-formfield";
import "../../../components/ha-radio";
import type { HaRadio } from "../../../components/ha-radio";
@customElement("ha-config-core-form")
class ConfigCoreForm extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _working = false;
@state() private _location?: [number, number];
@state() private _currency?: string;
@state() private _elevation?: string;
@state() private _unitSystem?: ConfigUpdateValues["unit_system"];
@state() private _timeZone?: string;
protected render(): TemplateResult {
const canEdit = ["storage", "default"].includes(
this.hass.config.config_source
);
const disabled = this._working || !canEdit;
return html`
<ha-card
.header=${this.hass.localize(
"ui.panel.config.core.section.core.form.heading"
)}
>
<div class="card-content">
${!canEdit
? html`
<p>
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.edit_requires_storage"
)}
</p>
`
: ""}
<div class="row">
<ha-locations-editor
class="flex"
.hass=${this.hass}
.locations=${this._markerLocation(
this.hass.config.latitude,
this.hass.config.longitude,
this._location
)}
@location-updated=${this._locationChanged}
></ha-locations-editor>
</div>
<div class="row">
<div class="flex">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.time_zone"
)}
</div>
<paper-input
class="flex"
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.time_zone"
)}
name="timeZone"
list="timezones"
.disabled=${disabled}
.value=${this._timeZoneValue}
@value-changed=${this._handleChange}
></paper-input>
</div>
<div class="row">
<div class="flex">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation"
)}
</div>
<paper-input
class="flex"
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation"
)}
name="elevation"
type="number"
.disabled=${disabled}
.value=${this._elevationValue}
@value-changed=${this._handleChange}
>
<span slot="suffix">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation_meters"
)}
</span>
</paper-input>
</div>
<div class="row">
<div class="flex">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system"
)}
</div>
<div class="radio-group">
<ha-formfield
.label=${html`${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system_metric"
)}
<div class="secondary">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.metric_example"
)}
</div>`}
>
<ha-radio
name="unit_system"
value="metric"
.checked=${this._unitSystemValue === "metric"}
@change=${this._unitSystemChanged}
.disabled=${this._working}
></ha-radio>
</ha-formfield>
<ha-formfield
.label=${html`${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system_imperial"
)}
<div class="secondary">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.imperial_example"
)}
</div>`}
>
<ha-radio
name="unit_system"
value="imperial"
.checked=${this._unitSystemValue === "imperial"}
@change=${this._unitSystemChanged}
.disabled=${this._working}
></ha-radio>
</ha-formfield>
</div>
</div>
<div class="row">
<div class="flex">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.currency"
)}<br />
<a
href="https://en.wikipedia.org/wiki/ISO_4217#Active_codes"
target="_blank"
rel="noopener noreferrer"
>${this.hass.localize(
"ui.panel.config.core.section.core.core_config.find_currency_value"
)}</a
>
</div>
<paper-input
class="flex"
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.currency"
)}
name="currency"
list="currencies"
.disabled=${disabled}
.value=${this._currencyValue}
@value-changed=${this._handleChange}
></paper-input>
</div>
</div>
<div class="card-actions">
<mwc-button @click=${this._save} .disabled=${disabled}>
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.save_button"
)}
</mwc-button>
</div>
</ha-card>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const tzInput = this.shadowRoot!.querySelector(
"[name=timeZone]"
) as PaperInputElement;
tzInput.inputElement.appendChild(createTimezoneListEl());
const cInput = this.shadowRoot!.querySelector(
"[name=currency]"
) as PaperInputElement;
cInput.inputElement.appendChild(createCurrencyListEl());
}
private _markerLocation = memoizeOne(
(
lat: number,
lng: number,
location?: [number, number]
): MarkerLocation[] => [
{
id: "location",
latitude: location ? location[0] : lat,
longitude: location ? location[1] : lng,
location_editable: true,
},
]
);
private get _currencyValue() {
return this._currency !== undefined
? this._currency
: this.hass.config.currency;
}
private get _elevationValue() {
return this._elevation !== undefined
? this._elevation
: this.hass.config.elevation;
}
private get _timeZoneValue() {
return this._timeZone !== undefined
? this._timeZone
: this.hass.config.time_zone;
}
private get _unitSystemValue() {
return this._unitSystem !== undefined
? this._unitSystem
: this.hass.config.unit_system.temperature === UNIT_C
? "metric"
: "imperial";
}
private _handleChange(ev: PolymerChangedEvent<string>) {
const target = ev.currentTarget as PaperInputElement;
let value = target.value;
if (target.name === "currency" && value) {
if (value in SYMBOL_TO_ISO) {
value = SYMBOL_TO_ISO[value];
}
}
this[`_${target.name}`] = value;
}
private _locationChanged(ev) {
this._location = ev.detail.location;
}
private _unitSystemChanged(ev: CustomEvent) {
this._unitSystem = (ev.target as HaRadio).value as "metric" | "imperial";
}
private async _save() {
this._working = true;
try {
const location = this._location || [
this.hass.config.latitude,
this.hass.config.longitude,
];
await saveCoreConfig(this.hass, {
latitude: location[0],
longitude: location[1],
currency: this._currencyValue,
elevation: Number(this._elevationValue),
unit_system: this._unitSystemValue,
time_zone: this._timeZoneValue,
});
} catch (err: any) {
alert(`Error saving config: ${err.message}`);
} finally {
this._working = false;
}
}
static get styles(): CSSResultGroup {
return css`
.row {
display: flex;
flex-direction: row;
margin: 0 -8px;
align-items: center;
}
.secondary {
color: var(--secondary-text-color);
}
.flex {
flex: 1;
}
.row > * {
margin: 0 8px;
}
.radio-group {
display: flex;
flex-direction: column;
flex: 1;
}
.card-actions {
text-align: right;
}
a {
color: var(--primary-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-core-form": ConfigCoreForm;
}
}

View File

@ -1,57 +0,0 @@
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../layouts/hass-subpage";
import LocalizeMixin from "../../../mixins/localize-mixin";
import "../../../styles/polymer-ha-style";
import "./ha-config-core-form";
import "./ha-config-name-form";
/*
* @appliesMixin LocalizeMixin
*/
class HaConfigCore extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex ha-style">
.content {
padding: 28px 20px 0;
max-width: 1040px;
margin: 0 auto;
}
ha-config-name-form,
ha-config-core-form {
display: block;
margin-top: 24px;
}
</style>
<hass-subpage
hass="[[hass]]"
narrow="[[narrow]]"
header="[[localize('ui.panel.config.core.caption')]]"
back-path="/config/system"
>
<div class="content">
<ha-config-name-form hass="[[hass]]"></ha-config-name-form>
<ha-config-core-form hass="[[hass]]"></ha-config-core-form>
</div>
</hass-subpage>
`;
}
static get properties() {
return {
hass: Object,
isWide: Boolean,
narrow: Boolean,
showAdvanced: Boolean,
route: Object,
};
}
}
customElements.define("ha-config-core", HaConfigCore);

View File

@ -1,97 +0,0 @@
import "@material/mwc-button/mwc-button";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-card";
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
import type { HomeAssistant } from "../../../types";
import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
@customElement("ha-config-name-form")
class ConfigNameForm extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _working = false;
@state() private _name!: ConfigUpdateValues["location_name"];
protected render(): TemplateResult {
const canEdit = ["storage", "default"].includes(
this.hass.config.config_source
);
const disabled = this._working || !canEdit;
return html`
<ha-card>
<div class="card-content">
${!canEdit
? html`
<p>
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.edit_requires_storage"
)}
</p>
`
: ""}
<ha-textfield
class="flex"
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.location_name"
)}
.disabled=${disabled}
.value=${this._nameValue}
@change=${this._handleChange}
></ha-textfield>
</div>
<div class="card-actions">
<mwc-button @click=${this._save} .disabled=${disabled}>
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.save_button"
)}
</mwc-button>
</div>
</ha-card>
`;
}
private get _nameValue() {
return this._name !== undefined
? this._name
: this.hass.config.location_name;
}
private _handleChange(ev) {
const target = ev.currentTarget as HaTextField;
this._name = target.value;
}
private async _save() {
this._working = true;
try {
await saveCoreConfig(this.hass, {
location_name: this._nameValue,
});
} catch (err: any) {
alert("FAIL");
} finally {
this._working = false;
}
}
static get styles() {
return css`
.card-actions {
text-align: right;
}
ha-textfield {
display: block;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-name-form": ConfigNameForm;
}
}

View File

@ -40,7 +40,7 @@ class ConfigNetwork extends LitElement {
}
return html`
<ha-card outlined header="Network">
<ha-card outlined header="Network Adapter">
<div class="card-content">
${this._error
? html`

View File

@ -1,7 +1,17 @@
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-alert";
import "../../../components/ha-bar";
import "../../../components/ha-metric";
import { fetchHassioHostInfo, HassioHostInfo } from "../../../data/hassio/host";
import "../../../layouts/hass-subpage";
import type { HomeAssistant, Route } from "../../../types";
import {
getValueInPercentage,
roundWithOneDecimal,
} from "../../../util/calculate";
import "./ha-config-analytics";
@customElement("ha-config-section-storage")
@ -12,6 +22,17 @@ class HaConfigSectionStorage extends LitElement {
@property({ type: Boolean }) public narrow!: boolean;
@state() private _error?: { code: string; message: string };
@state() private _storageData?: HassioHostInfo;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
if (isComponentLoaded(this.hass, "hassio")) {
this._load();
}
}
protected render(): TemplateResult {
return html`
<hass-subpage
@ -19,17 +40,84 @@ class HaConfigSectionStorage extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
>
<div class="content"></div>
<div class="content">
${this._error
? html`
<ha-alert alert-type="error"
>${this._error.message || this._error.code}</ha-alert
>
`
: ""}
${this._storageData
? html`
<ha-card outlined>
<ha-metric
.description=${this.hass.localize(
"ui.panel.config.storage.used_space"
)}
.value=${this._getUsedSpace(
this._storageData?.disk_used,
this._storageData?.disk_total
)}
.tooltip=${`${this._storageData.disk_used} GB/${this._storageData.disk_total} GB`}
></ha-metric>
${this._storageData.disk_life_time !== "" &&
this._storageData.disk_life_time >= 10
? html`
<ha-metric
.description=${this.hass.localize(
"ui.panel.config.storage.emmc_lifetime_used"
)}
.value=${this._storageData.disk_life_time}
.tooltip=${`${
this._storageData.disk_life_time - 10
} % -
${this._storageData.disk_life_time} %`}
class="emmc"
></ha-metric>
`
: ""}
</ha-card>
`
: ""}
</div>
</hass-subpage>
`;
}
private async _load() {
this._error = undefined;
try {
if (isComponentLoaded(this.hass, "hassio")) {
this._storageData = await fetchHassioHostInfo(this.hass);
}
} catch (err: any) {
this._error = err.message || err;
}
}
private _getUsedSpace = memoizeOne((used: number, total: number) =>
roundWithOneDecimal(getValueInPercentage(used, 0, total))
);
static styles = css`
.content {
padding: 28px 20px 0;
max-width: 1040px;
margin: 0 auto;
}
ha-card {
padding: 16px;
max-width: 500px;
margin: 0 auto;
height: 100%;
justify-content: space-between;
flex-direction: column;
display: flex;
}
.emmc {
--metric-bar-ok-color: #000;
}
`;
}

View File

@ -0,0 +1,137 @@
import { HassEntities } from "home-assistant-js-websocket";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import "../../../components/ha-alert";
import "../../../components/ha-bar";
import "../../../components/ha-metric";
import { updateCanInstall, UpdateEntity } from "../../../data/update";
import "../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../types";
import { showToast } from "../../../util/toast";
import "../dashboard/ha-config-updates";
import "./ha-config-analytics";
@customElement("ha-config-section-updates")
class HaConfigSectionUpdates extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow!: boolean;
private _notifyUpdates = false;
protected render(): TemplateResult {
const canInstallUpdates = this._filterUpdateEntitiesWithInstall(
this.hass.states
);
return html`
<hass-subpage
back-path="/config/system"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.updates.caption")}
>
<div class="content">
<ha-card outlined>
${canInstallUpdates.length
? html`
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.updateEntities=${canInstallUpdates}
showAll
></ha-config-updates>
`
: html`
${this.hass.localize("ui.panel.config.updates.no_updates")}
`}
</ha-card>
</div>
</hass-subpage>
`;
}
protected override updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!changedProps.has("hass") || !this._notifyUpdates) {
return;
}
this._notifyUpdates = false;
if (this._filterUpdateEntitiesWithInstall(this.hass.states).length) {
showToast(this, {
message: this.hass.localize(
"ui.panel.config.updates.updates_refreshed"
),
});
} else {
showToast(this, {
message: this.hass.localize("ui.panel.config.updates.no_new_updates"),
});
}
}
private _filterUpdateEntities = memoizeOne((entities: HassEntities) =>
(
Object.values(entities).filter(
(entity) => computeStateDomain(entity) === "update"
) as UpdateEntity[]
).sort((a, b) => {
if (a.attributes.title === "Home Assistant Core") {
return -3;
}
if (b.attributes.title === "Home Assistant Core") {
return 3;
}
if (a.attributes.title === "Home Assistant Operating System") {
return -2;
}
if (b.attributes.title === "Home Assistant Operating System") {
return 2;
}
if (a.attributes.title === "Home Assistant Supervisor") {
return -1;
}
if (b.attributes.title === "Home Assistant Supervisor") {
return 1;
}
return caseInsensitiveStringCompare(
a.attributes.title || a.attributes.friendly_name || "",
b.attributes.title || b.attributes.friendly_name || ""
);
})
);
private _filterUpdateEntitiesWithInstall = memoizeOne(
(entities: HassEntities) =>
this._filterUpdateEntities(entities).filter((entity) =>
updateCanInstall(entity)
)
);
static styles = css`
.content {
padding: 28px 20px 0;
max-width: 1040px;
margin: 0 auto;
}
ha-card {
padding: 16px;
max-width: 500px;
margin: 0 auto;
height: 100%;
justify-content: space-between;
flex-direction: column;
display: flex;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-section-updates": HaConfigSectionUpdates;
}
}

View File

@ -33,7 +33,7 @@ class HaConfigSystemNavigation extends LitElement {
return html`
<hass-subpage
back-path="/config"
.header=${this.hass.localize("ui.panel.config.dashboard.system.title")}
.header=${this.hass.localize("ui.panel.config.dashboard.system.main")}
>
<ha-config-section
.narrow=${this.narrow}
@ -43,9 +43,7 @@ class HaConfigSystemNavigation extends LitElement {
<ha-card>
${this.narrow
? html`<div class="title">
${this.hass.localize(
"ui.panel.config.dashboard.system.title"
)}
${this.hass.localize("ui.panel.config.dashboard.system.main")}
</div>`
: ""}
<ha-navigation-list

View File

@ -1,6 +1,6 @@
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { html, LitElement, TemplateResult } from "lit";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { canShowPage } from "../../../common/config/can_show_page";
import "../../../components/ha-card";
@ -30,7 +30,7 @@ class HaConfigNavigation extends LitElement {
name:
page.name ||
this.hass.localize(
`ui.panel.config.dashboard.${page.translationKey}.title`
`ui.panel.config.dashboard.${page.translationKey}.main`
),
description:
page.component === "cloud" && (page.info as CloudStatus)
@ -51,7 +51,7 @@ class HaConfigNavigation extends LitElement {
${
page.description ||
this.hass.localize(
`ui.panel.config.dashboard.${page.translationKey}.description`
`ui.panel.config.dashboard.${page.translationKey}.secondary`
)
}
`,
@ -81,6 +81,12 @@ class HaConfigNavigation extends LitElement {
});
}
}
static styles: CSSResultGroup = css`
ha-navigation-list {
--navigation-list-item-title-font-size: 16px;
}
`;
}
declare global {

View File

@ -2,7 +2,7 @@ import "@material/mwc-button/mwc-button";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/entity/state-badge";
import "../../../components/ha-alert";
@ -19,7 +19,7 @@ class HaConfigUpdates extends LitElement {
@property({ attribute: false })
public updateEntities?: UpdateEntity[];
@state() private _showAll = false;
@property({ type: Boolean, reflect: true }) showAll = false;
protected render(): TemplateResult {
if (!this.updateEntities?.length) {
@ -27,7 +27,7 @@ class HaConfigUpdates extends LitElement {
}
const updates =
this._showAll || this.updateEntities.length <= 3
this.showAll || this.updateEntities.length <= 3
? this.updateEntities
: this.updateEntities.slice(0, 2);
@ -66,7 +66,7 @@ class HaConfigUpdates extends LitElement {
</paper-icon-item>
`
)}
${!this._showAll && this.updateEntities.length >= 4
${!this.showAll && this.updateEntities.length >= 4
? html`
<button class="show-more" @click=${this._showAllClicked}>
${this.hass.localize("ui.panel.config.updates.more_updates", {
@ -85,7 +85,7 @@ class HaConfigUpdates extends LitElement {
}
private _showAllClicked() {
this._showAll = true;
this.showAll = true;
}
static get styles(): CSSResultGroup[] {

View File

@ -6,7 +6,6 @@ import {
mdiCog,
mdiCpu32Bit,
mdiDevices,
mdiHomeAssistant,
mdiInformation,
mdiInformationOutline,
mdiLightningBolt,
@ -261,14 +260,6 @@ export const configSections: { [name: string]: PageNavigation[] } = {
},
],
general: [
{
component: "core",
path: "/config/core",
translationKey: "ui.panel.config.core.caption",
iconPath: mdiHomeAssistant,
iconColor: "#4A5963",
core: true,
},
{
component: "server_control",
path: "/config/server_control",
@ -277,19 +268,25 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconColor: "#4A5963",
core: true,
},
{
path: "/config/updates",
translationKey: "ui.panel.config.updates.caption",
iconPath: mdiUpdate,
iconColor: "#3B808E",
},
{
component: "logs",
path: "/config/logs",
translationKey: "ui.panel.config.logs.caption",
iconPath: mdiMathLog,
iconColor: "#4A5963",
iconColor: "#C65326",
core: true,
},
{
path: "/config/backup",
translationKey: "ui.panel.config.backup.caption",
iconPath: mdiBackupRestore,
iconColor: "#4084CD",
iconColor: "#0D47A1",
component: "backup",
},
{
@ -298,12 +295,6 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconPath: mdiShape,
iconColor: "#f1c447",
},
{
path: "/config/hardware",
translationKey: "ui.panel.config.hardware.caption",
iconPath: mdiCpu32Bit,
iconColor: "#4A5963",
},
{
path: "/config/network",
translationKey: "ui.panel.config.network.caption",
@ -317,10 +308,10 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconColor: "#518C43",
},
{
path: "/config/update",
translationKey: "ui.panel.config.updates.caption",
iconPath: mdiUpdate,
iconColor: "#4A5963",
path: "/config/hardware",
translationKey: "ui.panel.config.hardware.caption",
iconPath: mdiCpu32Bit,
iconColor: "#301A8E",
},
],
about: [
@ -374,10 +365,6 @@ class HaPanelConfig extends HassRouterPage {
tag: "ha-config-cloud",
load: () => import("./cloud/ha-config-cloud"),
},
core: {
tag: "ha-config-core",
load: () => import("./core/ha-config-core"),
},
devices: {
tag: "ha-config-devices",
load: () => import("./devices/ha-config-devices"),
@ -444,6 +431,10 @@ class HaPanelConfig extends HassRouterPage {
tag: "ha-config-section-storage",
load: () => import("./core/ha-config-section-storage"),
},
updates: {
tag: "ha-config-section-updates",
load: () => import("./core/ha-config-section-updates"),
},
users: {
tag: "ha-config-users",
load: () => import("./users/ha-config-users"),

View File

@ -1,6 +1,7 @@
import {
mdiCheck,
mdiCheckCircleOutline,
mdiDotsVertical,
mdiOpenInNew,
mdiPlus,
} from "@mdi/js";
@ -16,6 +17,7 @@ import {
DataTableColumnContainer,
RowClickedEvent,
} from "../../../../components/data-table/ha-data-table";
import "../../../../components/ha-clickable-list-item";
import "../../../../components/ha-fab";
import "../../../../components/ha-icon";
import "../../../../components/ha-icon-button";
@ -216,7 +218,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
if (isComponentLoaded(this.hass, "energy")) {
result.push({
icon: "hass:lightning-bolt",
title: this.hass.localize(`ui.panel.config.dashboard.energy.title`),
title: this.hass.localize(`ui.panel.config.dashboard.energy.main`),
show_in_sidebar: true,
mode: "storage",
url_path: "energy",
@ -260,6 +262,32 @@ export class HaConfigLovelaceDashboards extends LitElement {
hasFab
clickable
>
${this.hass.userData?.showAdvanced
? html`
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
activatable
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-clickable-list-item
@click=${this._entryClicked}
href="/config/lovelace/resources"
aria-label=${this.hass.localize(
"ui.panel.config.lovelace.resources.caption"
)}
>
${this.hass.localize(
"ui.panel.config.lovelace.resources.caption"
)}
</ha-clickable-list-item>
</ha-button-menu>
`
: ""}
<ha-fab
slot="fab"
.label=${this.hass.localize(
@ -354,4 +382,8 @@ export class HaConfigLovelaceDashboards extends LitElement {
},
});
}
private _entryClicked(ev) {
ev.currentTarget.blur();
}
}

View File

@ -12,13 +12,6 @@ export const lovelaceTabs = [
translationKey: "ui.panel.config.lovelace.dashboards.caption",
icon: "hass:view-dashboard",
},
{
component: "lovelace",
path: "/config/lovelace/resources",
translationKey: "ui.panel.config.lovelace.resources.caption",
icon: "hass:file-multiple",
advancedOnly: true,
},
];
@customElement("ha-config-lovelace")

View File

@ -157,7 +157,7 @@ export class HaConfigUsers extends LitElement {
.narrow=${this.narrow}
.route=${this.route}
backPath="/config"
.tabs=${configSections.areas}
.tabs=${configSections.persons}
.columns=${this._columns(this.narrow, this.hass.localize)}
.data=${this._users}
@row-click=${this._editUser}

View File

@ -0,0 +1,278 @@
import "@material/mwc-button";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import timezones from "google-timezones-json";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { UNIT_C } from "../../../common/const";
import { fireEvent } from "../../../common/dom/fire_event";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { currencies } from "../../../components/currency-datalist";
import { createCloseHeading } from "../../../components/ha-dialog";
import { HaRadio } from "../../../components/ha-radio";
import "../../../components/ha-select";
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
import { SYMBOL_TO_ISO } from "../../../data/currency";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
@customElement("dialog-core-zone-detail")
class DialogZoneDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _submitting = false;
@state() private _open = false;
@state() private _unitSystem?: ConfigUpdateValues["unit_system"];
@state() private _currency?: string;
@state() private _name?: string;
@state() private _elevation?: number;
@state() private _timeZone?: string;
public showDialog(): void {
this._submitting = false;
this._unitSystem =
this.hass.config.unit_system.temperature === UNIT_C
? "metric"
: "imperial";
this._currency = this.hass.config.currency;
this._elevation = this.hass.config.elevation;
this._timeZone = this.hass.config.time_zone;
this._name = this.hass.config.location_name;
this._open = true;
}
public closeDialog(): void {
this._open = false;
this._currency = undefined;
this._elevation = undefined;
this._timeZone = undefined;
this._unitSystem = undefined;
this._name = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
const canEdit = ["storage", "default"].includes(
this.hass.config.config_source
);
const disabled = this._submitting || !canEdit;
if (!this._open) {
return html``;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(this.hass, "Core Zone Configuration")}
>
${!canEdit
? html`
<p>
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.edit_requires_storage"
)}
</p>
`
: ""}
<ha-textfield
name="name"
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.location_name"
)}
.disabled=${disabled}
.value=${this._name}
@change=${this._handleChange}
></ha-textfield>
<ha-select
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.time_zone"
)}
name="timeZone"
fixedMenuPosition
naturalMenuWidth
.disabled=${disabled}
.value=${this._timeZone}
@closed=${stopPropagation}
@change=${this._handleChange}
>
${Object.keys(timezones).map(
(tz) =>
html`<mwc-list-item value=${tz}>${timezones[tz]}</mwc-list-item>`
)}
</ha-select>
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation"
)}
name="elevation"
type="number"
.disabled=${disabled}
.value=${this._elevation}
@change=${this._handleChange}
>
<span slot="suffix">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation_meters"
)}
</span>
</ha-textfield>
<div>
<div>
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system"
)}
</div>
<ha-formfield
.label=${html`${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system_metric"
)}
<div class="secondary">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.metric_example"
)}
</div>`}
>
<ha-radio
name="unit_system"
value="metric"
.checked=${this._unitSystem === "metric"}
@change=${this._unitSystemChanged}
.disabled=${this._submitting}
></ha-radio>
</ha-formfield>
<ha-formfield
.label=${html`${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system_imperial"
)}
<div class="secondary">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.imperial_example"
)}
</div>`}
>
<ha-radio
name="unit_system"
value="imperial"
.checked=${this._unitSystem === "imperial"}
@change=${this._unitSystemChanged}
.disabled=${this._submitting}
></ha-radio>
</ha-formfield>
</div>
<div>
<ha-select
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.currency"
)}
name="currency"
fixedMenuPosition
naturalMenuWidth
.disabled=${disabled}
.value=${this._currency}
@closed=${stopPropagation}
@change=${this._handleChange}
>
${currencies.map(
(currency) =>
html`<mwc-list-item .value=${currency}
>${currency}</mwc-list-item
>`
)}</ha-select
>
<a
href="https://en.wikipedia.org/wiki/ISO_4217#Active_codes"
target="_blank"
rel="noopener noreferrer"
>${this.hass.localize(
"ui.panel.config.core.section.core.core_config.find_currency_value"
)}</a
>
</div>
<mwc-button slot="primaryAction" @click=${this._updateEntry}>
${this.hass!.localize("ui.panel.config.zone.detail.update")}
</mwc-button>
</ha-dialog>
`;
}
private _handleChange(ev) {
const target = ev.currentTarget;
let value = target.value;
if (target.name === "currency" && value) {
if (value in SYMBOL_TO_ISO) {
value = SYMBOL_TO_ISO[value];
}
}
this[`_${target.name}`] = value;
}
private _unitSystemChanged(ev: CustomEvent) {
this._unitSystem = (ev.target as HaRadio).value as "metric" | "imperial";
}
private async _updateEntry() {
this._submitting = true;
try {
await saveCoreConfig(this.hass, {
currency: this._currency,
elevation: Number(this._elevation),
unit_system: this._unitSystem,
time_zone: this._timeZone,
location_name: this._name,
});
} catch (err: any) {
alert(`Error saving config: ${err.message}`);
} finally {
this._submitting = false;
}
this.closeDialog();
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-min-width: 600px;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-dialog {
--mdc-dialog-min-width: calc(
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
);
}
}
.card-actions {
text-align: right;
}
ha-dialog > * {
display: block;
margin-top: 16px;
}
ha-select {
display: block;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-core-zone-detail": DialogZoneDetail;
}
}

View File

@ -13,7 +13,6 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { navigate } from "../../../common/navigate";
@ -44,6 +43,7 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant, Route } from "../../../types";
import "../ha-config-section";
import { configSections } from "../ha-panel-config";
import { showCoreZoneDetailDialog } from "./show-dialog-core-zone-detail";
import { showZoneDetailDialog } from "./show-dialog-zone-detail";
@customElement("ha-config-zone")
@ -186,15 +186,9 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
<ha-icon-button
.entityId=${stateObject.entity_id}
@click=${this._openCoreConfig}
disabled=${ifDefined(
stateObject.entity_id === "zone.home" &&
this.narrow &&
this._canEditCore
? undefined
: true
)}
.disabled=${stateObject.entity_id === "zone.home" &&
!this._canEditCore}
.path=${stateObject.entity_id === "zone.home" &&
this.narrow &&
this._canEditCore
? mdiPencil
: mdiPencilOff}
@ -391,22 +385,8 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
this._openDialog(entry);
}
private async _openCoreConfig(ev: Event) {
const entityId: string = (ev.currentTarget! as any).entityId;
if (entityId !== "zone.home" || !this.narrow || !this._canEditCore) {
return;
}
if (
!(await showConfirmationDialog(this, {
title: this.hass.localize("ui.panel.config.zone.go_to_core_config"),
text: this.hass.localize("ui.panel.config.zone.home_zone_core_config"),
confirmText: this.hass!.localize("ui.common.yes"),
dismissText: this.hass!.localize("ui.common.no"),
}))
) {
return;
}
navigate("/config/core");
private async _openCoreConfig() {
showCoreZoneDetailDialog(this);
}
private async _createEntry(values: ZoneMutableParams) {

View File

@ -0,0 +1,12 @@
import { fireEvent } from "../../../common/dom/fire_event";
export const loadCoreZoneDetailDialog = () =>
import("./dialog-core-zone-detail");
export const showCoreZoneDetailDialog = (element: HTMLElement): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-core-zone-detail",
dialogImport: loadCoreZoneDetailDialog,
dialogParams: {},
});
};

View File

@ -1066,52 +1066,52 @@
"header": "Configure Home Assistant",
"dashboard": {
"devices": {
"title": "Devices & Services",
"description": "Integrations, devices, entities and helpers"
"main": "Devices & Services",
"secondary": "Integrations, devices, entities and helpers"
},
"automations": {
"title": "Automations & Scenes",
"description": "Manage automations, scenes, scripts and blueprints"
"main": "Automations & Scenes",
"secondary": "Manage automations, scenes, scripts and blueprints"
},
"backup": {
"title": "Backup",
"description": "Generate backups of your Home Assistant configuration"
"main": "Backup",
"secondary": "Generate backups of your Home Assistant configuration"
},
"supervisor": {
"title": "Add-ons",
"description": "Extend the function around Home Assistant"
"main": "Add-ons",
"secondary": "Extend the function around Home Assistant"
},
"dashboards": {
"title": "Dashboards",
"description": "Create customized sets of cards to control your home"
"main": "Dashboards",
"secondary": "Create customized sets of cards to control your home"
},
"energy": {
"title": "Energy",
"description": "Monitor your energy production and consumption"
"main": "Energy",
"secondary": "Monitor your energy production and consumption"
},
"tags": {
"title": "Tags",
"description": "Trigger automations when an NFC tag, QR code, etc. is scanned"
"main": "Tags",
"secondary": "Trigger automations when an NFC tag, QR code, etc. is scanned"
},
"people": {
"title": "People",
"description": "Manage the people that Home Assistant tracks"
"main": "People",
"secondary": "Manage the people that Home Assistant tracks"
},
"areas": {
"title": "Areas & Zones",
"description": "Manage areas & zones that Home Assistant tracks"
"main": "Areas & Zones",
"secondary": "Manage areas and zones that Home Assistant tracks"
},
"companion": {
"title": "Companion App",
"description": "Location and notifications"
"main": "Companion App",
"secondary": "Location and notifications"
},
"system": {
"title": "System",
"description": "Create backups, check logs or reboot your system"
"main": "System",
"secondary": "Create backups, check logs or reboot your system"
},
"about": {
"title": "About",
"description": "Version, system health and links to documentation"
"main": "About",
"secondary": "Version, system health and links to documentation"
}
},
"common": {
@ -1122,6 +1122,7 @@
},
"updates": {
"caption": "Updates",
"no_updates": "No updates available",
"no_update_entities": {
"title": "Unable to check for updates",
"description": "You do not have any integrations that provide updates."
@ -1778,7 +1779,7 @@
"geo_location": {
"label": "Geolocation",
"source": "Source",
"zone": "Location",
"zone": "Zone",
"event": "Event",
"enter": "Enter",
"leave": "Leave"
@ -3109,7 +3110,9 @@
"caption": "Network"
},
"storage": {
"caption": "Storage"
"caption": "Storage",
"used_space": "Used Space",
"emmc_lifetime_used": "eMMC Lifetime Used"
}
},
"lovelace": {