Compare commits

...

26 Commits

Author SHA1 Message Date
Zack Barett
2a12172eeb Bumped version to 20220329.0 (#12152) 2022-03-29 22:25:56 +00:00
Joakim Sørensen
85d3011625 Add badge to configuration sidebar to indicate pending updates (#12146) 2022-03-29 07:35:05 -05:00
Zack Barett
ca22ec6340 Add selector initial values (#12142) 2022-03-28 10:38:58 -07:00
Zack Barett
61f6e8855b Allow binary sensor device class updates (#12124) 2022-03-28 10:44:21 -05:00
Bram Kragten
a44b8981e1 break theme picker out of lovelace (#12140) 2022-03-28 08:21:16 -07:00
Zack Barett
b080bca9ce Add Area Multiple Selector option (#12138) 2022-03-28 09:07:00 -05:00
Bram Kragten
d30e8ee9d8 Make padding on settings row content consistent (#12139) 2022-03-28 08:50:07 -05:00
Bram Kragten
637e4203e5 Fix z-index map, always set icon for location selector (#12137) 2022-03-28 13:14:24 +00:00
Zack Barett
2648a53bbc Merge pull request #12135 from home-assistant/update-automation-type 2022-03-28 07:43:21 -05:00
Paulus Schoutsen
b3fa0cccb4 Add variables to automation trigger type 2022-03-27 20:33:22 -07:00
Zack Barett
dd963be723 Add Day to duration selector (#12125) 2022-03-24 17:57:20 -07:00
Paulus Schoutsen
224df896a1 Allow rendering helper text from strings.json (#12119)
* Allow rendering helper text from strings.json

* Persistent helpers

* Update src/components/ha-base-time-input.ts

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Update src/components/ha-base-time-input.ts

Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-03-24 17:50:38 -07:00
Pawel
a58b4fb262 Fix possibility to enable entity disabled by integration (#12121)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-03-24 20:10:49 +01:00
Brynley McDonald
27ca61ec85 Fix issue where theme select does not appear when user's theme is deleted (#12104) 2022-03-24 16:31:55 +01:00
Zack Barett
859f49f3eb Update type for backend (#12122) 2022-03-24 13:47:07 +00:00
Erik Montnemery
40d878689f Sort selectors (#12120) 2022-03-24 11:53:32 +01:00
Zack Barett
420e8fe1ff Merge pull request #12116 from home-assistant/docs-only-form 2022-03-23 17:40:40 -05:00
Erik Montnemery
df96199433 Support descriptions in flow menu steps (#12108)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-03-23 22:14:57 +00:00
Erik Montnemery
f493280f0a Exclude restored automations from dashboard (#12113) 2022-03-23 15:05:41 -07:00
Paulus Schoutsen
cbd030a379 Only show docs link when showing a form 2022-03-23 14:53:02 -07:00
Zack Barett
95b80accc9 Merge pull request #12085 from goyney/update-mdi-to-6-6-95 2022-03-23 15:17:31 -05:00
Erik Montnemery
c522670815 Fix loading traces for automation with custom id (#12112) 2022-03-23 13:11:46 -07:00
Joakim Sørensen
7b6d3c0e36 Use update entities for showing updates on configuration panel (#12100)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-03-23 13:04:29 -07:00
Michael Irigoyen
504b043159 Update lock file with MDI updates 2022-03-23 08:46:22 -05:00
Michael Irigoyen
dffc66ccc3 Merge branch 'dev' of github.com:goyney/frontend into update-mdi-to-6-6-95 2022-03-23 08:43:46 -05:00
Michael Irigoyen
f5f8be8276 Update required version of MDI to 6.6.95 2022-03-20 23:32:21 -05:00
63 changed files with 1188 additions and 556 deletions

View File

@@ -1,18 +1,18 @@
/* eslint-disable lit/no-template-arrow */
import "@material/mwc-button";
import { LitElement, TemplateResult, html } from "lit";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import "../../../../src/components/ha-form/ha-form";
import "../../components/demo-black-white-row";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../../src/types";
import { getEntity } from "../../../../src/fake_data/entity";
import "../../components/demo-black-white-row";
const ENTITIES = [
getEntity("alarm_control_panel", "alarm", "disarmed", {
@@ -147,7 +147,9 @@ const SCHEMAS: {
{ name: "target", selector: { target: {} } },
{ name: "number", selector: { number: { min: 0, max: 10 } } },
{ name: "boolean", selector: { boolean: {} } },
{ name: "time", selector: { time: {} } },
{ name: "time", required: true, selector: { time: {} } },
{ name: "datetime", required: true, selector: { datetime: {} } },
{ name: "date", required: true, selector: { date: {} } },
{ name: "action", selector: { action: {} } },
{ name: "text", selector: { text: { multiline: false } } },
{ name: "text_multiline", selector: { text: { multiline: true } } },

View File

@@ -1,20 +1,20 @@
/* eslint-disable lit/no-template-arrow */
import "@material/mwc-button";
import { LitElement, TemplateResult, css, html } from "lit";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import "../../../../src/components/ha-selector/ha-selector";
import "../../../../src/components/ha-settings-row";
import { BlueprintInput } from "../../../../src/data/blueprint";
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
import type { HomeAssistant } from "../../../../src/types";
import "../../components/demo-black-white-row";
import { BlueprintInput } from "../../../../src/data/blueprint";
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import { getEntity } from "../../../../src/fake_data/entity";
import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
const ENTITIES = [
getEntity("alarm_control_panel", "alarm", "disarmed", {
@@ -202,6 +202,7 @@ const SCHEMAS: {
input: {
entity: { name: "Entity", selector: { entity: { multiple: true } } },
device: { name: "Device", selector: { device: { multiple: true } } },
area: { name: "Area", selector: { area: { multiple: true } } },
},
},
];

View File

@@ -45,7 +45,6 @@ import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";
import "../../../src/layouts/hass-tabs-subpage";
import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates";
import { HomeAssistant, Route } from "../../../src/types";
import { addonArchIsSupported, extractChangelog } from "../util/addon";
@@ -55,6 +54,12 @@ declare global {
}
}
const SUPERVISOR_UPDATE_NAMES = {
core: "Home Assistant Core",
os: "Home Assistant Operating System",
supervisor: "Home Assistant Supervisor",
};
type updateType = "os" | "supervisor" | "core" | "addon";
const changelogUrl = (

View File

@@ -72,8 +72,8 @@
"@material/mwc-textfield": "0.25.3",
"@material/mwc-top-app-bar-fixed": "^0.25.3",
"@material/top-app-bar": "14.0.0-canary.261f2db59.0",
"@mdi/js": "6.5.95",
"@mdi/svg": "6.5.95",
"@mdi/js": "6.6.95",
"@mdi/svg": "6.6.95",
"@polymer/app-layout": "^3.1.0",
"@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1",

View File

@@ -1,6 +1,6 @@
[metadata]
name = home-assistant-frontend
version = 20220322.0
version = 20220329.0
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0

View File

@@ -0,0 +1,160 @@
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { EntityRegistryEntry } from "../data/entity_registry";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import type { HomeAssistant } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-area-picker";
@customElement("ha-areas-picker")
export class HaAreasPicker extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@property() public value?: string[];
@property() public placeholder?: string;
@property({ type: Boolean, attribute: "no-add" })
public noAdd?: boolean;
/**
* Show only areas with entities from specific domains.
* @type {Array}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
public includeDomains?: string[];
/**
* Show no areas with entities of these domains.
* @type {Array}
* @attr exclude-domains
*/
@property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[];
/**
* Show only areas with entities of these device classes.
* @type {Array}
* @attr include-device-classes
*/
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
@property({ attribute: "picked-area-label" })
public pickedAreaLabel?: string;
@property({ attribute: "pick-area-label" })
public pickAreaLabel?: string;
@property({ type: Boolean }) public disabled?: boolean;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
const currentAreas = this._currentAreas;
return html`
${currentAreas.map(
(area) => html`
<div>
<ha-area-picker
.curValue=${area}
.noAdd=${this.noAdd}
.hass=${this.hass}
.value=${area}
.label=${this.pickedAreaLabel}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.disabled=${this.disabled}
@value-changed=${this._areaChanged}
></ha-area-picker>
</div>
`
)}
<div>
<ha-area-picker
.noAdd=${this.noAdd}
.hass=${this.hass}
.label=${this.pickAreaLabel}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.disabled=${this.disabled}
.placeholder=${this.placeholder}
@value-changed=${this._addArea}
></ha-area-picker>
</div>
`;
}
private get _currentAreas(): string[] {
return this.value || [];
}
private async _updateAreas(areas) {
this.value = areas;
fireEvent(this, "value-changed", {
value: areas,
});
}
private _areaChanged(ev: CustomEvent) {
ev.stopPropagation();
const curValue = (ev.currentTarget as any).curValue;
const newValue = ev.detail.value;
if (newValue === curValue) {
return;
}
const currentAreas = this._currentAreas;
if (!newValue || currentAreas.includes(newValue)) {
this._updateAreas(currentAreas.filter((ent) => ent !== curValue));
return;
}
this._updateAreas(
currentAreas.map((ent) => (ent === curValue ? newValue : ent))
);
}
private _addArea(ev: CustomEvent) {
ev.stopPropagation();
const toAdd = ev.detail.value;
if (!toAdd) {
return;
}
(ev.currentTarget as any).value = "";
const currentAreas = this._currentAreas;
if (currentAreas.includes(toAdd)) {
return;
}
this._updateAreas([...currentAreas, toAdd]);
}
static override styles = css`
div {
margin-top: 8px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-areas-picker": HaAreasPicker;
}
}

View File

@@ -1,12 +1,13 @@
import { LitElement, html, TemplateResult, css } from "lit";
import { customElement, property } from "lit/decorators";
import "./ha-select";
import "@material/mwc-list/mwc-list-item";
import "./ha-textfield";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import "./ha-select";
import "./ha-textfield";
export interface TimeChangedEvent {
days?: number;
hours: number;
minutes: number;
seconds: number;
@@ -21,6 +22,11 @@ export class HaBaseTimeInput extends LitElement {
*/
@property() label?: string;
/**
* Helper for the input
*/
@property() helper?: string;
/**
* auto validate time inputs
*/
@@ -41,6 +47,11 @@ export class HaBaseTimeInput extends LitElement {
*/
@property({ type: Boolean }) disabled = false;
/**
* day
*/
@property({ type: Number }) days = 0;
/**
* hour
*/
@@ -61,6 +72,11 @@ export class HaBaseTimeInput extends LitElement {
*/
@property({ type: Number }) milliseconds = 0;
/**
* Label for the day input
*/
@property() dayLabel = "";
/**
* Label for the hour input
*/
@@ -91,6 +107,11 @@ export class HaBaseTimeInput extends LitElement {
*/
@property({ type: Boolean }) enableMillisecond = false;
/**
* show the day field
*/
@property({ type: Boolean }) enableDay = false;
/**
* limit hours input
*/
@@ -110,6 +131,29 @@ export class HaBaseTimeInput extends LitElement {
return html`
${this.label ? html`<label>${this.label}</label>` : ""}
<div class="time-input-wrap">
${this.enableDay
? html`
<ha-textfield
id="day"
type="number"
inputmode="numeric"
.value=${this.days}
.label=${this.dayLabel}
name="days"
@input=${this._valueChanged}
@focus=${this._onFocus}
no-spinner
.required=${this.required}
.autoValidate=${this.autoValidate}
min="0"
.disabled=${this.disabled}
suffix=":"
class="hasSuffix"
>
</ha-textfield>
`
: ""}
<ha-textfield
id="hour"
type="number"
@@ -207,6 +251,7 @@ export class HaBaseTimeInput extends LitElement {
<mwc-list-item value="PM">PM</mwc-list-item>
</ha-select>`}
</div>
${this.helper ? html`<div class="helper">${this.helper}</div>` : ""}
`;
}
@@ -303,6 +348,13 @@ export class HaBaseTimeInput extends LitElement {
color: var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87));
padding-left: 4px;
}
.helper {
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
font-size: 0.75rem;
padding-left: 16px;
padding-right: 16px;
}
`;
}

View File

@@ -5,6 +5,7 @@ import "./ha-base-time-input";
import type { TimeChangedEvent } from "./ha-base-time-input";
export interface HaDurationData {
days?: number;
hours?: number;
minutes?: number;
seconds?: number;
@@ -17,10 +18,14 @@ class HaDurationInput extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public required?: boolean;
@property({ type: Boolean }) public enableMillisecond?: boolean;
@property({ type: Boolean }) public enableDay?: boolean;
@property({ type: Boolean }) public disabled = false;
@query("paper-time-input", true) private _input?: HTMLElement;
@@ -35,19 +40,23 @@ class HaDurationInput extends LitElement {
return html`
<ha-base-time-input
.label=${this.label}
.helper=${this.helper}
.required=${this.required}
.autoValidate=${this.required}
.disabled=${this.disabled}
errorMessage="Required"
enableSecond
.enableMillisecond=${this.enableMillisecond}
.enableDay=${this.enableDay}
format="24"
.days=${this._days}
.hours=${this._hours}
.minutes=${this._minutes}
.seconds=${this._seconds}
.milliseconds=${this._milliseconds}
@value-changed=${this._durationChanged}
noHoursLimit
dayLabel="dd"
hourLabel="hh"
minLabel="mm"
secLabel="ss"
@@ -56,6 +65,10 @@ class HaDurationInput extends LitElement {
`;
}
private get _days() {
return this.data?.days ? Number(this.data.days) : 0;
}
private get _hours() {
return this.data?.hours ? Number(this.data.hours) : 0;
}
@@ -94,6 +107,11 @@ class HaDurationInput extends LitElement {
value.minutes %= 60;
}
if (this.enableDay && value.hours > 24) {
value.days = (value.days ?? 0) + Math.floor(value.hours / 24);
value.hours %= 24;
}
fireEvent(this, "value-changed", {
value,
});

View File

@@ -34,12 +34,25 @@ export const computeInitialHaFormData = (
};
} else if ("selector" in field) {
const selector: Selector = field.selector;
if ("boolean" in selector) {
if ("device" in selector) {
data[field.name] = selector.device.multiple ? [] : "";
} else if ("entity" in selector) {
data[field.name] = selector.entity.multiple ? [] : "";
} else if ("area" in selector) {
data[field.name] = selector.area.multiple ? [] : "";
} else if ("boolean" in selector) {
data[field.name] = false;
} else if ("text" in selector) {
} else if (
"text" in selector ||
"addon" in selector ||
"attribute" in selector ||
"icon" in selector ||
"theme" in selector
) {
data[field.name] = "";
} else if ("number" in selector) {
data[field.name] = "min" in selector.number ? selector.number.min : 0;
data[field.name] = selector.number.min ?? 0;
} else if ("select" in selector) {
if (selector.select.options.length) {
data[field.name] = selector.select.options[0][0];
@@ -50,6 +63,23 @@ export const computeInitialHaFormData = (
minutes: 0,
seconds: 0,
};
} else if ("time" in selector) {
data[field.name] = "00:00:00";
} else if ("date" in selector || "datetime" in selector) {
const now = new Date().toISOString().slice(0, 10);
data[field.name] = `${now} 00:00:00`;
} else if ("color_rgb" in selector) {
data[field.name] = [0, 0, 0];
} else if ("color_temp" in selector) {
data[field.name] = selector.color_temp.min_mireds ?? 153;
} else if (
"action" in selector ||
"media" in selector ||
"target" in selector
) {
data[field.name] = {};
} else {
throw new Error("Selector not supported in initial form data");
}
}
});

View File

@@ -28,6 +28,8 @@ export class HaFormString extends LitElement implements HaFormElement {
@property() public label!: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@state() private _unmaskedPassword = false;
@@ -53,6 +55,8 @@ export class HaFormString extends LitElement implements HaFormElement {
: "password"}
.label=${this.label}
.value=${this.data || ""}
.helper=${this.helper}
helperPersistent
.disabled=${this.disabled}
.required=${this.schema.required}
.autoValidate=${this.schema.required}

View File

@@ -6,6 +6,7 @@ import { EntityRegistryEntry } from "../../data/entity_registry";
import { AreaSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-area-picker";
import "../ha-areas-picker";
@customElement("ha-selector-area")
export class HaAreaSelector extends LitElement {
@@ -38,21 +39,43 @@ export class HaAreaSelector extends LitElement {
}
protected render() {
return html`<ha-area-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
no-add
.deviceFilter=${this._filterDevices}
.entityFilter=${this._filterEntities}
.includeDeviceClasses=${this.selector.area.entity?.device_class
? [this.selector.area.entity.device_class]
: undefined}
.includeDomains=${this.selector.area.entity?.domain
? [this.selector.area.entity.domain]
: undefined}
.disabled=${this.disabled}
></ha-area-picker>`;
if (!this.selector.area.multiple) {
return html`
<ha-area-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
no-add
.deviceFilter=${this._filterDevices}
.entityFilter=${this._filterEntities}
.includeDeviceClasses=${this.selector.area.entity?.device_class
? [this.selector.area.entity.device_class]
: undefined}
.includeDomains=${this.selector.area.entity?.domain
? [this.selector.area.entity.domain]
: undefined}
.disabled=${this.disabled}
></ha-area-picker>
`;
}
return html`
<ha-areas-picker
.hass=${this.hass}
.value=${this.value}
.pickAreaLabel=${this.label}
no-add
.deviceFilter=${this._filterDevices}
.entityFilter=${this._filterEntities}
.includeDeviceClasses=${this.selector.area.entity?.device_class
? [this.selector.area.entity.device_class]
: undefined}
.includeDomains=${this.selector.area.entity?.domain
? [this.selector.area.entity.domain]
: undefined}
.disabled=${this.disabled}
></ha-areas-picker>
`;
}
private _filterEntities = (entity: EntityRegistryEntry): boolean => {

View File

@@ -14,6 +14,8 @@ export class HaTimeDuration extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@@ -22,9 +24,11 @@ export class HaTimeDuration extends LitElement {
return html`
<ha-duration-input
.label=${this.label}
.helper=${this.helper}
.data=${this.value}
.disabled=${this.disabled}
.required=${this.required}
.enableDay=${this.selector.duration.enable_day}
></ha-duration-input>
`;
}

View File

@@ -29,8 +29,8 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.includeEntities=${this.selector.entity.includeEntities}
.excludeEntities=${this.selector.entity.excludeEntities}
.includeEntities=${this.selector.entity.include_entities}
.excludeEntities=${this.selector.entity.exclude_entities}
.entityFilter=${this._filterEntities}
.disabled=${this.disabled}
allow-custom-entity
@@ -43,8 +43,8 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
.hass=${this.hass}
.value=${this.value}
.entityFilter=${this._filterEntities}
.includeEntities=${this.selector.entity.includeEntities}
.excludeEntities=${this.selector.entity.excludeEntities}
.includeEntities=${this.selector.entity.include_entities}
.excludeEntities=${this.selector.entity.exclude_entities}
></ha-entities-picker>
`;
}

View File

@@ -6,7 +6,6 @@ import type {
LocationSelector,
LocationSelectorValue,
} from "../../data/selector";
import "../../panels/lovelace/components/hui-theme-select-editor";
import type { HomeAssistant } from "../../types";
import type { MarkerLocation } from "../map/ha-locations-editor";
import "../map/ha-locations-editor";
@@ -52,7 +51,10 @@ export class HaLocationSelector extends LitElement {
longitude: value?.longitude || this.hass.config.longitude,
radius: selector.location.radius ? value?.radius || 1000 : undefined,
radius_color: zoneRadiusColor,
icon: selector.location.icon,
icon:
selector.location.icon || selector.location.radius
? "mdi:map-marker-radius"
: "mdi:map-marker",
location_editable: true,
radius_editable: true,
},

View File

@@ -19,6 +19,8 @@ export class HaNumberSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public required = true;
@property({ type: Boolean }) public disabled = false;
@@ -48,6 +50,8 @@ export class HaNumberSelector extends LitElement {
.max=${this.selector.number.max}
.value=${this.value ?? ""}
.step=${this.selector.number.step ?? 1}
helperPersistent
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
.suffix=${this.selector.number.unit_of_measurement}

View File

@@ -18,6 +18,8 @@ export class HaTextSelector extends LitElement {
@property() public placeholder?: string;
@property() public helper?: string;
@property() public selector!: StringSelector;
@property({ type: Boolean }) public disabled = false;
@@ -32,6 +34,8 @@ export class HaTextSelector extends LitElement {
.label=${this.label}
.placeholder=${this.placeholder}
.value=${this.value || ""}
.helper=${this.helper}
helperPersistent
.disabled=${this.disabled}
@input=${this._handleChange}
autocapitalize="none"
@@ -44,6 +48,8 @@ export class HaTextSelector extends LitElement {
return html`<ha-textfield
.value=${this.value || ""}
.placeholder=${this.placeholder || ""}
.helper=${this.helper}
helperPersistent
.disabled=${this.disabled}
.type=${this._unmaskedPassword ? "text" : this.selector.text?.type}
@input=${this._handleChange}

View File

@@ -1,4 +1,4 @@
import "../../panels/lovelace/components/hui-theme-select-editor";
import "../ha-theme-picker";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { HomeAssistant } from "../../types";
@@ -18,11 +18,11 @@ export class HaThemeSelector extends LitElement {
protected render() {
return html`
<hui-theme-select-editor
<ha-theme-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
></hui-theme-select-editor>
></ha-theme-picker>
`;
}
}

View File

@@ -471,6 +471,7 @@ export class HaServiceControl extends LitElement {
}
ha-settings-row {
--paper-time-input-justify-content: flex-end;
--settings-row-content-width: 100%;
border-top: var(
--service-control-items-border-top,
1px solid var(--divider-color)
@@ -489,9 +490,6 @@ export class HaServiceControl extends LitElement {
margin: var(--service-control-padding, 0 16px);
padding: 16px 0;
}
:host(:not([narrow])) ha-settings-row ha-selector {
width: 60%;
}
.checkbox-spacer {
width: 32px;
}

View File

@@ -21,7 +21,7 @@ export class HaSettingsRow extends LitElement {
<div secondary><slot name="description"></slot></div>
</paper-item-body>
</div>
<slot></slot>
<div class="content"><slot></slot></div>
`;
}
@@ -43,6 +43,18 @@ export class HaSettingsRow extends LitElement {
);
flex: 1;
}
.content {
display: contents;
}
:host(:not([narrow])) .content {
display: flex;
justify-content: flex-end;
flex: 1;
padding: 16px 0;
}
.content ::slotted(*) {
width: var(--settings-row-content-width);
}
:host([narrow]) {
align-items: normal;
flex-direction: column;

View File

@@ -37,6 +37,7 @@ import { LocalStorage } from "../common/decorators/local-storage";
import { fireEvent } from "../common/dom/fire_event";
import { toggleAttribute } from "../common/dom/toggle_attribute";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { stringCompare } from "../common/string/compare";
import { computeRTL } from "../common/util/compute_rtl";
import { ActionHandlerDetail } from "../data/lovelace";
@@ -44,6 +45,7 @@ import {
PersistentNotification,
subscribeNotifications,
} from "../data/persistent_notification";
import { updateCanInstall, UpdateEntity } from "../data/update";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo, Route } from "../types";
@@ -68,7 +70,6 @@ const SORT_VALUE_URL_PATHS = {
const PANEL_ICONS = {
calendar: mdiCalendar,
config: mdiCog,
"developer-tools": mdiHammer,
energy: mdiLightningBolt,
history: mdiChartBox,
@@ -190,6 +191,8 @@ class HaSidebar extends LitElement {
@state() private _notifications?: PersistentNotification[];
@state() private _updatesCount = 0;
@state() private _renderEmptySortable = false;
private _mouseLeaveTimeout?: number;
@@ -235,6 +238,7 @@ class HaSidebar extends LitElement {
changedProps.has("narrow") ||
changedProps.has("alwaysExpand") ||
changedProps.has("_externalConfig") ||
changedProps.has("_updatesCount") ||
changedProps.has("_notifications") ||
changedProps.has("editMode") ||
changedProps.has("_renderEmptySortable") ||
@@ -290,6 +294,12 @@ class HaSidebar extends LitElement {
toggleAttribute(this, "rtl", computeRTL(this.hass));
}
this._updatesCount = Object.values(this.hass.states).filter(
(entity) =>
computeStateDomain(entity) === "update" &&
updateCanInstall(entity as UpdateEntity)
).length;
if (!SUPPORT_SCROLL_IF_NEEDED) {
return;
}
@@ -387,35 +397,37 @@ class HaSidebar extends LitElement {
icon?: string | null,
iconPath?: string | null
) {
return html`
<a
role="option"
href=${`/${urlPath}`}
data-panel=${urlPath}
tabindex="-1"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
${iconPath
? html`<ha-svg-icon
slot="item-icon"
.path=${iconPath}
></ha-svg-icon>`
: html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`}
<span class="item-text">${title}</span>
</paper-icon-item>
${this.editMode
? html`<ha-icon-button
.label=${this.hass.localize("ui.sidebar.hide_panel")}
.path=${mdiClose}
class="hide-panel"
.panel=${urlPath}
@click=${this._hidePanel}
></ha-icon-button>`
: ""}
</a>
`;
return urlPath === "config"
? this._renderConfiguration(title)
: html`
<a
role="option"
href=${`/${urlPath}`}
data-panel=${urlPath}
tabindex="-1"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
${iconPath
? html`<ha-svg-icon
slot="item-icon"
.path=${iconPath}
></ha-svg-icon>`
: html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`}
<span class="item-text">${title}</span>
</paper-icon-item>
${this.editMode
? html`<ha-icon-button
.label=${this.hass.localize("ui.sidebar.hide_panel")}
.path=${mdiClose}
class="hide-panel"
.panel=${urlPath}
@click=${this._hidePanel}
></ha-icon-button>`
: ""}
</a>
`;
}
private _renderPanelsEdit(beforeSpacer: PanelInfo[]) {
@@ -477,6 +489,35 @@ class HaSidebar extends LitElement {
return html`<div class="spacer" disabled></div>`;
}
private _renderConfiguration(title: string | null) {
return html` <a
class="configuration-container"
role="option"
href="/config"
data-panel="config"
tabindex="-1"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item class="configuration" role="option">
<ha-svg-icon slot="item-icon" .path=${mdiCog}></ha-svg-icon>
${!this.alwaysExpand && this._updatesCount > 0
? html`
<span class="configuration-badge" slot="item-icon">
${this._updatesCount}
</span>
`
: ""}
<span class="item-text">${title}</span>
${this.alwaysExpand && this._updatesCount > 0
? html`
<span class="configuration-badge">${this._updatesCount}</span>
`
: ""}
</paper-icon-item>
</a>`;
}
private _renderNotifications() {
let notificationCount = this._notifications
? this._notifications.length
@@ -953,18 +994,21 @@ class HaSidebar extends LitElement {
height: 1px;
background-color: var(--divider-color);
}
.notifications-container {
.notifications-container,
.configuration-container {
display: flex;
margin-left: env(safe-area-inset-left);
}
:host([rtl]) .notifications-container {
:host([rtl]) .notifications-container,
:host([rtl]) .configuration-container {
margin-left: initial;
margin-right: env(safe-area-inset-right);
}
.notifications {
cursor: pointer;
}
.notifications .item-text {
.notifications .item-text,
.configuration .item-text {
flex: 1;
}
.profile {
@@ -988,7 +1032,8 @@ class HaSidebar extends LitElement {
margin-right: 8px;
}
.notification-badge {
.notification-badge,
.configuration-badge {
min-width: 20px;
box-sizing: border-box;
border-radius: 50%;
@@ -999,7 +1044,11 @@ class HaSidebar extends LitElement {
padding: 0px 6px;
color: var(--text-accent-color, var(--text-primary-color));
}
ha-svg-icon + .notification-badge {
.configuration-badge {
background-color: var(--primary-color);
}
ha-svg-icon + .notification-badge,
ha-svg-icon + .configuration-badge {
position: absolute;
bottom: 14px;
left: 26px;

View File

@@ -2,13 +2,13 @@ import "@material/mwc-button";
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import "../../../components/ha-select";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import "./ha-select";
import { HomeAssistant } from "../types";
@customElement("hui-theme-select-editor")
export class HuiThemeSelectEditor extends LitElement {
@customElement("ha-theme-picker")
export class HaThemePicker extends LitElement {
@property() public value?: string;
@property() public label?: string;
@@ -19,11 +19,7 @@ export class HuiThemeSelectEditor extends LitElement {
return html`
<ha-select
.label=${this.label ||
`${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`}
this.hass!.localize("ui.components.theme_picker.theme")}
.value=${this.value}
@selected=${this._changed}
@closed=${stopPropagation}
@@ -32,7 +28,7 @@ export class HuiThemeSelectEditor extends LitElement {
>
<mwc-list-item value="remove"
>${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.no_theme"
"ui.components.theme_picker.no_theme"
)}</mwc-list-item
>
${Object.keys(this.hass!.themes.themes)
@@ -64,6 +60,6 @@ export class HuiThemeSelectEditor extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"hui-theme-select-editor": HuiThemeSelectEditor;
"ha-theme-picker": HaThemePicker;
}
}

View File

@@ -488,6 +488,14 @@ export class HaMap extends ReactiveElement {
text-align: center;
color: var(--primary-text-color);
}
.leaflet-pane {
z-index: 0 !important;
}
.leaflet-control,
.leaflet-top,
.leaflet-bottom {
z-index: 1 !important;
}
`;
}
}

View File

@@ -62,6 +62,7 @@ export interface ContextConstraint {
export interface BaseTrigger {
platform: string;
id?: string;
variables?: Record<string, unknown>;
}
export interface StateTrigger extends BaseTrigger {

View File

@@ -1,35 +1,52 @@
export type Selector =
| ActionSelector
| AddonSelector
| AreaSelector
| AttributeSelector
| EntitySelector
| BooleanSelector
| ColorRGBSelector
| ColorTempSelector
| DateSelector
| DateTimeSelector
| DeviceSelector
| DurationSelector
| AreaSelector
| TargetSelector
| EntitySelector
| IconSelector
| LocationSelector
| MediaSelector
| NumberSelector
| BooleanSelector
| TimeSelector
| ActionSelector
| StringSelector
| ObjectSelector
| SelectSelector
| IconSelector
| MediaSelector
| StringSelector
| TargetSelector
| ThemeSelector
| LocationSelector
| ColorTempSelector
| ColorRGBSelector;
| TimeSelector;
export interface EntitySelector {
entity: {
integration?: string;
domain?: string | string[];
device_class?: string;
export interface ActionSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
action: {};
}
export interface AddonSelector {
addon: {
name?: string;
slug?: string;
};
}
export interface AreaSelector {
area: {
entity?: {
integration?: EntitySelector["entity"]["integration"];
domain?: EntitySelector["entity"]["domain"];
device_class?: EntitySelector["entity"]["device_class"];
};
device?: {
integration?: DeviceSelector["device"]["integration"];
manufacturer?: DeviceSelector["device"]["manufacturer"];
model?: DeviceSelector["device"]["model"];
};
multiple?: boolean;
includeEntities?: string[];
excludeEntities?: string[];
};
}
@@ -39,11 +56,23 @@ export interface AttributeSelector {
};
}
export interface BooleanSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
boolean: {};
}
export interface ColorRGBSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
color_rgb: {};
}
export interface ColorTempSelector {
color_temp: {
min_mireds?: number;
max_mireds?: number;
};
}
export interface DateSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
date: {};
@@ -68,44 +97,54 @@ export interface DeviceSelector {
}
export interface DurationSelector {
duration: {
enable_day?: boolean;
};
}
export interface EntitySelector {
entity: {
integration?: string;
domain?: string | string[];
device_class?: string;
multiple?: boolean;
include_entities?: string[];
exclude_entities?: string[];
};
}
export interface IconSelector {
icon: {
placeholder?: string;
fallbackPath?: string;
};
}
export interface LocationSelector {
location: { radius?: boolean; icon?: string };
}
export interface LocationSelectorValue {
latitude: number;
longitude: number;
radius?: number;
}
export interface MediaSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
duration: {};
media: {};
}
export interface AddonSelector {
addon: {
name?: string;
slug?: string;
};
}
export interface AreaSelector {
area: {
entity?: {
integration?: EntitySelector["entity"]["integration"];
domain?: EntitySelector["entity"]["domain"];
device_class?: EntitySelector["entity"]["device_class"];
};
device?: {
integration?: DeviceSelector["device"]["integration"];
manufacturer?: DeviceSelector["device"]["manufacturer"];
model?: DeviceSelector["device"]["model"];
};
};
}
export interface TargetSelector {
target: {
entity?: {
integration?: EntitySelector["entity"]["integration"];
domain?: EntitySelector["entity"]["domain"];
device_class?: EntitySelector["entity"]["device_class"];
};
device?: {
integration?: DeviceSelector["device"]["integration"];
manufacturer?: DeviceSelector["device"]["manufacturer"];
model?: DeviceSelector["device"]["model"];
};
export interface MediaSelectorValue {
entity_id?: string;
media_content_id?: string;
media_content_type?: string;
metadata?: {
title?: string;
thumbnail?: string | null;
media_class?: string;
children_media_class?: string | null;
navigateIds?: { media_content_type: string; media_content_id: string }[];
};
}
@@ -119,28 +158,22 @@ export interface NumberSelector {
};
}
export interface ColorTempSelector {
color_temp: {
min_mireds?: number;
max_mireds?: number;
export interface ObjectSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
object: {};
}
export interface SelectOption {
value: string;
label: string;
}
export interface SelectSelector {
select: {
options: string[] | SelectOption[];
};
}
export interface BooleanSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
boolean: {};
}
export interface TimeSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
time: {};
}
export interface ActionSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
action: {};
}
export interface StringSelector {
text: {
multiline?: boolean;
@@ -162,58 +195,25 @@ export interface StringSelector {
};
}
export interface ObjectSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
object: {};
}
export interface SelectOption {
value: string;
label: string;
}
export interface SelectSelector {
select: {
options: string[] | SelectOption[];
export interface TargetSelector {
target: {
entity?: {
integration?: EntitySelector["entity"]["integration"];
domain?: EntitySelector["entity"]["domain"];
device_class?: EntitySelector["entity"]["device_class"];
};
device?: {
integration?: DeviceSelector["device"]["integration"];
manufacturer?: DeviceSelector["device"]["manufacturer"];
model?: DeviceSelector["device"]["model"];
};
};
}
export interface IconSelector {
icon: {
placeholder?: string;
fallbackPath?: string;
};
}
export interface ThemeSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
theme: {};
}
export interface MediaSelector {
export interface TimeSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
media: {};
}
export interface LocationSelector {
location: { radius?: boolean; icon?: string };
}
export interface LocationSelectorValue {
latitude: number;
longitude: number;
radius?: number;
}
export interface MediaSelectorValue {
entity_id?: string;
media_content_id?: string;
media_content_type?: string;
metadata?: {
title?: string;
thumbnail?: string | null;
media_class?: string;
children_media_class?: string | null;
navigateIds?: { media_content_type: string; media_content_id: string }[];
};
time: {};
}

View File

@@ -1,58 +0,0 @@
import { HomeAssistant } from "../../types";
interface SupervisorBaseAvailableUpdates {
panel_path?: string;
update_type?: string;
version_latest?: string;
}
interface SupervisorAddonAvailableUpdates
extends SupervisorBaseAvailableUpdates {
update_type?: "addon";
icon?: string;
name?: string;
}
interface SupervisorCoreAvailableUpdates
extends SupervisorBaseAvailableUpdates {
update_type?: "core";
}
interface SupervisorOsAvailableUpdates extends SupervisorBaseAvailableUpdates {
update_type?: "os";
}
interface SupervisorSupervisorAvailableUpdates
extends SupervisorBaseAvailableUpdates {
update_type?: "supervisor";
}
export type SupervisorAvailableUpdates =
| SupervisorAddonAvailableUpdates
| SupervisorCoreAvailableUpdates
| SupervisorOsAvailableUpdates
| SupervisorSupervisorAvailableUpdates;
export interface SupervisorAvailableUpdatesResponse {
available_updates: SupervisorAvailableUpdates[];
}
export const fetchSupervisorAvailableUpdates = async (
hass: HomeAssistant
): Promise<SupervisorAvailableUpdates[]> =>
(
await hass.callWS<SupervisorAvailableUpdatesResponse>({
type: "supervisor/api",
endpoint: "/available_updates",
method: "get",
})
).available_updates;
export const refreshSupervisorAvailableUpdates = async (
hass: HomeAssistant
): Promise<void> =>
hass.callWS<void>({
type: "supervisor/api",
endpoint: "/refresh_updates",
method: "post",
});

View File

@@ -14,9 +14,7 @@ import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
import { computeRTL } from "../../common/util/compute_rtl";
import "../../components/ha-circular-progress";
import "../../components/ha-dialog";
import "../../components/ha-form/ha-form";
import "../../components/ha-icon-button";
import "../../components/ha-markdown";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
@@ -238,12 +236,14 @@ class DataEntryFlowDialog extends LitElement {
""
: html`
<div class="dialog-actions">
${this._step
${["form", "menu", "external"].includes(
this._step?.type as any
)
? html`
<a
href=${documentationUrl(
this.hass,
`/integrations/${this._step.handler}`
`/integrations/${this._step!.handler}`
)}
target="_blank"
rel="noreferrer noopener"

View File

@@ -91,6 +91,12 @@ export const showConfigFlowDialog = (
);
},
renderShowFormStepFieldHelper(hass, step, field) {
return hass.localize(
`component.${step.handler}.config.step.${step.step_id}.data_description.${field.name}`
);
},
renderShowFormStepFieldError(hass, step, error) {
return hass.localize(
`component.${step.handler}.config.error.${error}`,
@@ -189,6 +195,18 @@ export const showConfigFlowDialog = (
);
},
renderMenuDescription(hass, step) {
const description = hass.localize(
`component.${step.handler}.config.step.${step.step_id}.description`,
step.description_placeholders
);
return description
? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
`
: "";
},
renderMenuOption(hass, step, option) {
return hass.localize(
`component.${step.handler}.config.step.${step.step_id}.menu_options.${option}`,

View File

@@ -50,6 +50,12 @@ export interface FlowConfig {
field: HaFormSchema
): string;
renderShowFormStepFieldHelper(
hass: HomeAssistant,
step: DataEntryFlowStepForm,
field: HaFormSchema
): string;
renderShowFormStepFieldError(
hass: HomeAssistant,
step: DataEntryFlowStepForm,
@@ -83,6 +89,11 @@ export interface FlowConfig {
renderMenuHeader(hass: HomeAssistant, step: DataEntryFlowStepMenu): string;
renderMenuDescription(
hass: HomeAssistant,
step: DataEntryFlowStepMenu
): TemplateResult | "";
renderMenuOption(
hass: HomeAssistant,
step: DataEntryFlowStepMenu,

View File

@@ -89,6 +89,12 @@ export const showOptionsFlowDialog = (
);
},
renderShowFormStepFieldHelper(hass, step, field) {
return hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.data_description.${field.name}`
);
},
renderShowFormStepFieldError(hass, step, error) {
return hass.localize(
`component.${configEntry.domain}.options.error.${error}`,
@@ -142,6 +148,22 @@ export const showOptionsFlowDialog = (
);
},
renderMenuDescription(hass, step) {
const description = hass.localize(
`component.${step.handler}.option.step.${step.step_id}.description`,
step.description_placeholders
);
return description
? html`
<ha-markdown
allowsvg
breaks
.content=${description}
></ha-markdown>
`
: "";
},
renderMenuOption(hass, step, option) {
return hass.localize(
`component.${step.handler}.options.step.${step.step_id}.menu_options.${option}`,

View File

@@ -54,6 +54,7 @@ class StepFlowForm extends LitElement {
.schema=${step.data_schema}
.error=${step.errors}
.computeLabel=${this._labelCallback}
.computeHelper=${this._helperCallback}
.computeError=${this._errorCallback}
></ha-form>
</div>
@@ -166,6 +167,9 @@ class StepFlowForm extends LitElement {
private _labelCallback = (field: HaFormSchema): string =>
this.flowConfig.renderShowFormStepFieldLabel(this.hass, this.step, field);
private _helperCallback = (field: HaFormSchema): string =>
this.flowConfig.renderShowFormStepFieldHelper(this.hass, this.step, field);
private _errorCallback = (error: string) =>
this.flowConfig.renderShowFormStepFieldError(this.hass, this.step, error);

View File

@@ -35,8 +35,14 @@ class StepFlowMenu extends LitElement {
translations = this.step.menu_options;
}
const description = this.flowConfig.renderMenuDescription(
this.hass,
this.step
);
return html`
<h2>${this.flowConfig.renderMenuHeader(this.hass, this.step)}</h2>
${description ? html`<div class="content">${description}</div>` : ""}
<div class="options">
${options.map(
(option) => html`
@@ -69,6 +75,16 @@ class StepFlowMenu extends LitElement {
margin-top: 20px;
margin-bottom: 8px;
}
.content {
padding-bottom: 16px;
border-bottom: 1px solid var(--divider-color);
}
.content + .options {
margin-top: 8px;
}
mwc-list-item {
--mdc-list-side-padding: 24px;
}
`,
];
}

View File

@@ -326,12 +326,9 @@ export class HaBlueprintAutomationEditor extends LitElement {
}
ha-settings-row {
--paper-time-input-justify-content: flex-end;
--settings-row-content-width: 100%;
border-top: 1px solid var(--divider-color);
}
:host(:not([narrow])) ha-settings-row ha-textfield,
:host(:not([narrow])) ha-settings-row ha-selector {
width: 60%;
}
`,
];
}

View File

@@ -59,7 +59,9 @@ class HaConfigAutomation extends HassRouterPage {
private _getAutomations = memoizeOne(
(states: HassEntities): AutomationEntity[] =>
Object.values(states).filter(
(entity) => computeStateDomain(entity) === "automation"
(entity) =>
computeStateDomain(entity) === "automation" &&
!entity.attributes.restored
) as AutomationEntity[]
);
@@ -87,7 +89,7 @@ class HaConfigAutomation extends HassRouterPage {
(!changedProps || changedProps.has("route")) &&
this._currentPage !== "dashboard"
) {
const automationId = this.routeTail.path.substr(1);
const automationId = decodeURIComponent(this.routeTail.path.substr(1));
pageEl.automationId = automationId === "new" ? null : automationId;
}
}

View File

@@ -1,3 +1,5 @@
import type { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item";
import {
mdiCloudLock,
mdiDotsVertical,
@@ -5,10 +7,9 @@ import {
mdiMagnify,
mdiNewBox,
} from "@mdi/js";
import "@material/mwc-list/mwc-list-item";
import type { ActionDetail } from "@material/mwc-list";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import type { HassEntities } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
@@ -18,30 +19,29 @@ import {
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-card";
import "../../../components/ha-icon-next";
import "../../../components/ha-icon-button";
import "../../../components/ha-menu-button";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
import "../../../components/ha-menu-button";
import "../../../components/ha-svg-icon";
import { CloudStatus } from "../../../data/cloud";
import {
refreshSupervisorAvailableUpdates,
SupervisorAvailableUpdates,
} from "../../../data/supervisor/root";
import { updateCanInstall, UpdateEntity } from "../../../data/update";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar";
import "../../../layouts/ha-app-layout";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast";
import "../ha-config-section";
import { configSections } from "../ha-panel-config";
import "./ha-config-navigation";
import "./ha-config-updates";
import { fireEvent } from "../../../common/dom/fire_event";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { showToast } from "../../../util/toast";
import { documentationUrl } from "../../../util/documentation-url";
const randomTip = (hass: HomeAssistant) => {
const weighted: string[] = [];
@@ -113,9 +113,6 @@ class HaConfigDashboard extends LitElement {
@property() public cloudStatus?: CloudStatus;
// null means not available
@property() public supervisorUpdates?: SupervisorAvailableUpdates[] | null;
@property() public showAdvanced!: boolean;
@state() private _tip?: string;
@@ -123,6 +120,9 @@ class HaConfigDashboard extends LitElement {
private _notifyUpdates = false;
protected render(): TemplateResult {
const canInstallUpdates = this._filterUpdateEntitiesWithInstall(
this.hass.states
);
return html`
<ha-app-layout>
<app-header fixed slot="header">
@@ -160,50 +160,47 @@ class HaConfigDashboard extends LitElement {
.isWide=${this.isWide}
full-width
>
${this.supervisorUpdates === undefined
? // Hide everything until updates loaded
html``
: html`${this.supervisorUpdates?.length
? html`<ha-card>
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.supervisorUpdates=${this.supervisorUpdates}
></ha-config-updates>
</ha-card>`
: ""}
<ha-card>
${this.narrow && this.supervisorUpdates?.length
? html`<div class="title">
${this.hass.localize("panel.config")}
</div>`
: ""}
${this.cloudStatus && isComponentLoaded(this.hass, "cloud")
? html`
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.showAdvanced=${this.showAdvanced}
.pages=${[
{
component: "cloud",
path: "/config/cloud",
name: "Home Assistant Cloud",
info: this.cloudStatus,
iconPath: mdiCloudLock,
iconColor: "#3B808E",
},
]}
></ha-config-navigation>
`
: ""}
${canInstallUpdates.length
? html`<ha-card>
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.updateEntities=${canInstallUpdates}
></ha-config-updates>
</ha-card>`
: ""}
<ha-card>
${this.narrow && canInstallUpdates.length
? html`<div class="title">
${this.hass.localize("panel.config")}
</div>`
: ""}
${this.cloudStatus && isComponentLoaded(this.hass, "cloud")
? html`
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.showAdvanced=${this.showAdvanced}
.pages=${configSections.dashboard}
.pages=${[
{
component: "cloud",
path: "/config/cloud",
name: "Home Assistant Cloud",
info: this.cloudStatus,
iconPath: mdiCloudLock,
iconColor: "#3B808E",
},
]}
></ha-config-navigation>
</ha-card>`}
`
: ""}
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.showAdvanced=${this.showAdvanced}
.pages=${configSections.dashboard}
></ha-config-navigation>
</ha-card>
<div class="tips">
<ha-svg-icon .path=${mdiLightbulbOutline}></ha-svg-icon>
<span class="tip-word">Tip!</span>
@@ -221,11 +218,11 @@ class HaConfigDashboard extends LitElement {
this._tip = randomTip(this.hass);
}
if (!changedProps.has("supervisorUpdates") || !this._notifyUpdates) {
if (!changedProps.has("hass") || !this._notifyUpdates) {
return;
}
this._notifyUpdates = false;
if (this.supervisorUpdates?.length) {
if (this._filterUpdateEntitiesWithInstall(this.hass.states).length) {
showToast(this, {
message: this.hass.localize(
"ui.panel.config.updates.updates_refreshed"
@@ -238,6 +235,44 @@ class HaConfigDashboard extends LitElement {
}
}
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)
)
);
private _showQuickBar(): void {
showQuickBar(this, {
commandMode: true,
@@ -246,20 +281,24 @@ class HaConfigDashboard extends LitElement {
}
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
const _entities = this._filterUpdateEntities(this.hass.states).map(
(entity) => entity.entity_id
);
switch (ev.detail.index) {
case 0:
if (isComponentLoaded(this.hass, "hassio")) {
if (_entities.length) {
this._notifyUpdates = true;
await refreshSupervisorAvailableUpdates(this.hass);
fireEvent(this, "ha-refresh-supervisor");
await this.hass.callService("homeassistant", "update_entity", {
entity_id: _entities,
});
return;
}
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.updates.check_unavailable.title"
"ui.panel.config.updates.no_update_entities.title"
),
text: this.hass.localize(
"ui.panel.config.updates.check_unavailable.description"
"ui.panel.config.updates.no_update_entities.description"
),
warning: true,
});

View File

@@ -1,21 +1,14 @@
import "@material/mwc-button/mwc-button";
import { mdiPackageVariant } from "@mdi/js";
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 { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/entity/state-badge";
import "../../../components/ha-alert";
import "../../../components/ha-logo-svg";
import "../../../components/ha-svg-icon";
import { SupervisorAvailableUpdates } from "../../../data/supervisor/root";
import { HomeAssistant } from "../../../types";
import "../../../components/ha-icon-next";
export const SUPERVISOR_UPDATE_NAMES = {
core: "Home Assistant Core",
os: "Home Assistant Operating System",
supervisor: "Home Assistant Supervisor",
};
import type { UpdateEntity } from "../../../data/update";
import { HomeAssistant } from "../../../types";
@customElement("ha-config-updates")
class HaConfigUpdates extends LitElement {
@@ -24,62 +17,60 @@ class HaConfigUpdates extends LitElement {
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false })
public supervisorUpdates?: SupervisorAvailableUpdates[] | null;
public updateEntities?: UpdateEntity[];
@state() private _showAll = false;
protected render(): TemplateResult {
if (!this.supervisorUpdates?.length) {
if (!this.updateEntities?.length) {
return html``;
}
const updates =
this._showAll || this.supervisorUpdates.length <= 3
? this.supervisorUpdates
: this.supervisorUpdates.slice(0, 2);
this._showAll || this.updateEntities.length <= 3
? this.updateEntities
: this.updateEntities.slice(0, 2);
return html`
<div class="title">
${this.hass.localize("ui.panel.config.updates.title", {
count: this.supervisorUpdates.length,
count: this.updateEntities.length,
})}
</div>
${updates.map(
(update) => html`
<a href="/hassio${update.panel_path}">
<paper-icon-item>
<span slot="item-icon" class="icon">
${update.update_type === "addon"
? update.icon
? html`<img src="/api/hassio${update.icon}" />`
: html`<ha-svg-icon
.path=${mdiPackageVariant}
></ha-svg-icon>`
: html`<ha-logo-svg></ha-logo-svg>`}
</span>
<paper-item-body two-line>
${update.update_type === "addon"
? update.name
: SUPERVISOR_UPDATE_NAMES[update.update_type!]}
<div secondary>
${this.hass.localize(
"ui.panel.config.updates.version_available",
{
version_available: update.version_latest,
}
)}
</div>
</paper-item-body>
${!this.narrow ? html`<ha-icon-next></ha-icon-next>` : ""}
</paper-icon-item>
</a>
(entity) => html`
<paper-icon-item
@click=${this._openMoreInfo}
.entity_id=${entity.entity_id}
>
<span slot="item-icon" class="icon">
<state-badge
.title=${entity.attributes.title ||
entity.attributes.friendly_name}
.stateObj=${entity}
slot="item-icon"
></state-badge>
</span>
<paper-item-body two-line>
${entity.attributes.title || entity.attributes.friendly_name}
<div secondary>
${this.hass.localize(
"ui.panel.config.updates.version_available",
{
version_available: entity.attributes.latest_version,
}
)}
</div>
</paper-item-body>
${!this.narrow ? html`<ha-icon-next></ha-icon-next>` : ""}
</paper-icon-item>
`
)}
${!this._showAll && this.supervisorUpdates.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", {
count: this.supervisorUpdates!.length - updates.length,
count: this.updateEntities!.length - updates.length,
})}
</button>
`
@@ -87,6 +78,12 @@ class HaConfigUpdates extends LitElement {
`;
}
private _openMoreInfo(ev: MouseEvent): void {
fireEvent(this, "hass-more-info", {
entityId: (ev.currentTarget as any).entity_id,
});
}
private _showAllClicked() {
this._showAll = true;
}
@@ -99,25 +96,11 @@ class HaConfigUpdates extends LitElement {
padding: 16px;
padding-bottom: 0;
}
a {
text-decoration: none;
color: var(--primary-text-color);
}
.icon {
display: inline-flex;
height: 100%;
align-items: center;
}
img,
ha-svg-icon,
ha-logo-svg {
--mdc-icon-size: 32px;
max-height: 32px;
width: 32px;
}
ha-logo-svg {
color: var(--secondary-text-color);
}
ha-icon-next {
color: var(--secondary-text-color);
height: 24px;
@@ -139,6 +122,9 @@ class HaConfigUpdates extends LitElement {
outline: none;
text-decoration: underline;
}
paper-icon-item {
cursor: pointer;
}
`,
];
}

View File

@@ -1,6 +1,5 @@
import "@material/mwc-formfield/mwc-formfield";
import "../../../components/ha-radio";
import "@material/mwc-button/mwc-button";
import "@material/mwc-formfield/mwc-formfield";
import "@material/mwc-list/mwc-list-item";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
@@ -20,9 +19,15 @@ import "../../../components/ha-alert";
import "../../../components/ha-area-picker";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-icon-picker";
import "../../../components/ha-radio";
import "../../../components/ha-select";
import "../../../components/ha-switch";
import "../../../components/ha-textfield";
import {
ConfigEntry,
deleteConfigEntry,
getConfigEntries,
} from "../../../data/config_entries";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
@@ -34,6 +39,7 @@ import {
removeEntityRegistryEntry,
updateEntityRegistryEntry,
} from "../../../data/entity_registry";
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
import {
showAlertDialog,
showConfirmationDialog,
@@ -42,27 +48,39 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
import {
ConfigEntry,
deleteConfigEntry,
getConfigEntries,
} from "../../../data/config_entries";
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
const OVERRIDE_DEVICE_CLASSES = {
cover: [
"awning",
"blind",
"curtain",
"damper",
"door",
"garage",
"gate",
"shade",
"shutter",
"window",
[
"awning",
"blind",
"curtain",
"damper",
"door",
"garage",
"gate",
"shade",
"shutter",
"window",
],
],
binary_sensor: [
["lock"], // Lock
["window", "door", "garage_door", "opening"], // Door
["battery", "battery_charging"], // Battery
["cold", "gas", "heat"], // Climate
["running", "motion", "moving", "occupancy", "presence", "vibration"], // Presence
["power", "plug", "light"], // Power
[
"smoke",
"safety",
"sound",
"problem",
"tamper",
"carbon_monoxide",
"moisture",
], // Alarm
],
binary_sensor: ["window", "door", "garage_door", "opening"],
};
@customElement("entity-registry-settings")
@@ -85,8 +103,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@state() private _hiddenBy!: string | null;
private _deviceLookup?: Record<string, DeviceRegistryEntry>;
@state() private _device?: DeviceRegistryEntry;
@state() private _helperConfigEntry?: ConfigEntry;
@@ -97,6 +113,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
private _origEntityId!: string;
private _deviceLookup?: Record<string, DeviceRegistryEntry>;
private _deviceClassOptions?: string[][];
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
@@ -125,23 +145,41 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
}
}
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (changedProperties.has("entry")) {
this._error = undefined;
this._name = this.entry.name || "";
this._icon = this.entry.icon || "";
this._deviceClass =
this.entry.device_class || this.entry.original_device_class;
this._origEntityId = this.entry.entity_id;
this._areaId = this.entry.area_id;
this._entityId = this.entry.entity_id;
this._disabledBy = this.entry.disabled_by;
this._hiddenBy = this.entry.hidden_by;
this._device =
this.entry.device_id && this._deviceLookup
? this._deviceLookup[this.entry.device_id]
: undefined;
protected willUpdate(changedProperties: PropertyValues) {
super.willUpdate(changedProperties);
if (!changedProperties.has("entry")) {
return;
}
this._error = undefined;
this._name = this.entry.name || "";
this._icon = this.entry.icon || "";
this._deviceClass =
this.entry.device_class || this.entry.original_device_class;
this._origEntityId = this.entry.entity_id;
this._areaId = this.entry.area_id;
this._entityId = this.entry.entity_id;
this._disabledBy = this.entry.disabled_by;
this._hiddenBy = this.entry.hidden_by;
this._device =
this.entry.device_id && this._deviceLookup
? this._deviceLookup[this.entry.device_id]
: undefined;
const domain = computeDomain(this.entry.entity_id);
const deviceClasses: string[][] = OVERRIDE_DEVICE_CLASSES[domain];
if (!deviceClasses) {
return;
}
this._deviceClassOptions = [[], []];
for (const deviceClass of deviceClasses) {
if (deviceClass.includes(this.entry.original_device_class!)) {
this._deviceClassOptions[0] = deviceClass;
} else {
this._deviceClassOptions[1].push(...deviceClass);
}
}
}
@@ -197,28 +235,39 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
: undefined}
.disabled=${this._submitting}
></ha-icon-picker>
${OVERRIDE_DEVICE_CLASSES[domain]?.includes(this._deviceClass) ||
(domain === "cover" && this.entry.original_device_class === null)
? html`<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.device_class"
)}
.value=${this._deviceClass}
naturalMenuWidth
fixedMenuPosition
@selected=${this._deviceClassChanged}
@closed=${stopPropagation}
>
${OVERRIDE_DEVICE_CLASSES[domain].map(
(deviceClass: string) => html`
<mwc-list-item .value=${deviceClass}>
${this.hass.localize(
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}`
)}
</mwc-list-item>
`
)}
</ha-select>`
${this._deviceClassOptions
? html`
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.device_class"
)}
.value=${this._deviceClass}
naturalMenuWidth
fixedMenuPosition
@selected=${this._deviceClassChanged}
@closed=${stopPropagation}
>
${this._deviceClassOptions[0].map(
(deviceClass: string) => html`
<mwc-list-item .value=${deviceClass} test=${deviceClass}>
${this.hass.localize(
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}`
)}
</mwc-list-item>
`
)}
<li divider role="separator"></li>
${this._deviceClassOptions[1].map(
(deviceClass: string) => html`
<mwc-list-item .value=${deviceClass} test=${deviceClass}>
${this.hass.localize(
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}`
)}
</mwc-list-item>
`
)}
</ha-select>
`
: ""}
<ha-textfield
error-message="Domain needs to stay the same"
@@ -264,7 +313,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
)}:
</div>
<div class="secondary">
${this._disabledBy && this._disabledBy !== "user"
${this._disabledBy &&
this._disabledBy !== "user" &&
this._disabledBy !== "integration"
? this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_cause",
"cause",
@@ -286,7 +337,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
.checked=${!this._hiddenBy && !this._disabledBy}
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
this._device?.disabled_by ||
(this._disabledBy && this._disabledBy !== "user")}
(this._disabledBy &&
this._disabledBy !== "user" &&
this._disabledBy !== "integration")}
@change=${this._viewStatusChanged}
></ha-radio>
</mwc-formfield>
@@ -301,7 +354,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
.checked=${this._hiddenBy !== null}
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
Boolean(this._device?.disabled_by) ||
(this._disabledBy && this._disabledBy !== "user")}
(this._disabledBy &&
this._disabledBy !== "user" &&
this._disabledBy !== "integration")}
@change=${this._viewStatusChanged}
></ha-radio>
</mwc-formfield>
@@ -316,7 +371,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
.checked=${this._disabledBy !== null}
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
Boolean(this._device?.disabled_by) ||
(this._disabledBy && this._disabledBy !== "user")}
(this._disabledBy &&
this._disabledBy !== "user" &&
this._disabledBy !== "integration")}
@change=${this._viewStatusChanged}
></ha-radio>
</mwc-formfield>
@@ -378,7 +435,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
class="warning"
@click=${this._confirmDeleteEntry}
.disabled=${this._submitting ||
(!this._helperConfigEntry && !stateObj.attributes.restored)}
(!this._helperConfigEntry && !stateObj?.attributes.restored)}
>
${this.hass.localize("ui.dialogs.entity_registry.editor.delete")}
</mwc-button>
@@ -577,6 +634,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
margin: 8px 0;
width: 340px;
}
li[divider] {
border-bottom-color: var(--divider-color);
}
`,
];
}

View File

@@ -27,10 +27,6 @@ import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { listenMediaQuery } from "../../common/dom/media_query";
import { CloudStatus, fetchCloudStatus } from "../../data/cloud";
import {
fetchSupervisorAvailableUpdates,
SupervisorAvailableUpdates,
} from "../../data/supervisor/root";
import "../../layouts/hass-loading-screen";
import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page";
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
@@ -397,8 +393,6 @@ class HaPanelConfig extends HassRouterPage {
@state() private _cloudStatus?: CloudStatus;
@state() private _supervisorUpdates?: SupervisorAvailableUpdates[] | null;
private _listeners: Array<() => void> = [];
public connectedCallback() {
@@ -433,19 +427,7 @@ class HaPanelConfig extends HassRouterPage {
}
});
}
if (isComponentLoaded(this.hass, "hassio")) {
this._loadSupervisorUpdates();
this.addEventListener("ha-refresh-supervisor", () => {
this._loadSupervisorUpdates();
});
this.addEventListener("connection-status", (ev) => {
if (ev.detail === "connected") {
this._loadSupervisorUpdates();
}
});
} else {
this._supervisorUpdates = null;
}
this.addEventListener("ha-refresh-cloud-status", () =>
this._updateCloudStatus()
);
@@ -476,7 +458,6 @@ class HaPanelConfig extends HassRouterPage {
isWide,
narrow: this.narrow,
cloudStatus: this._cloudStatus,
supervisorUpdates: this._supervisorUpdates,
});
} else {
el.route = this.routeTail;
@@ -485,7 +466,6 @@ class HaPanelConfig extends HassRouterPage {
el.isWide = isWide;
el.narrow = this.narrow;
el.cloudStatus = this._cloudStatus;
el.supervisorUpdates = this._supervisorUpdates;
}
}
@@ -503,16 +483,6 @@ class HaPanelConfig extends HassRouterPage {
setTimeout(() => this._updateCloudStatus(), 5000);
}
}
private async _loadSupervisorUpdates(): Promise<void> {
try {
this._supervisorUpdates = await fetchSupervisorAvailableUpdates(
this.hass
);
} catch (err) {
this._supervisorUpdates = null;
}
}
}
declare global {

View File

@@ -116,7 +116,7 @@ export class HuiCardOptions extends LitElement {
outline: 2px solid var(--primary-color);
}
:host:not(.panel) ::slotted(*) {
:host(:not(.panel)) ::slotted(*) {
display: block;
}

View File

@@ -23,7 +23,6 @@ export const configElementStyle = css`
.suffix {
margin: 0 8px;
}
hui-theme-select-editor,
hui-action-editor,
ha-select,
ha-textfield,

View File

@@ -99,6 +99,14 @@ export class HuiAlarmPanelCardEditor
return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.name`);
}
if (schema.name === "theme") {
return `${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
}
return this.hass!.localize(
`ui.panel.lovelace.editor.card.alarm-panel.${
schema.name === "states" ? "available_states" : schema.name

View File

@@ -69,6 +69,12 @@ export class HuiAreaCardEditor
private _computeLabelCallback = (schema: HaFormSchema) => {
switch (schema.name) {
case "theme":
return `${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
case "area":
return this.hass!.localize("ui.panel.lovelace.editor.card.area.name");
case "navigation_path":

View File

@@ -200,6 +200,14 @@ export class HuiButtonCardEditor
)}`;
}
if (schema.name === "theme") {
return `${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
}
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);

View File

@@ -121,6 +121,14 @@ export class HuiCalendarCardEditor
return this.hass!.localize("ui.panel.lovelace.editor.card.generic.title");
}
if (schema.name === "theme") {
return `${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
}
return this.hass!.localize(
`ui.panel.lovelace.editor.card.calendar.${schema.name}`
);

View File

@@ -31,7 +31,7 @@ import "../../../../components/ha-icon";
import "../../../../components/ha-switch";
import type { HomeAssistant } from "../../../../types";
import type { EntitiesCardConfig } from "../../cards/types";
import "../../components/hui-theme-select-editor";
import "../../../../components/ha-theme-picker";
import { TIMESTAMP_RENDERING_FORMATS } from "../../components/types";
import type { LovelaceRowConfig } from "../../entity-rows/types";
import { headerFooterConfigStructs } from "../../header-footer/structs";
@@ -265,12 +265,17 @@ export class HuiEntitiesCardEditor
.configValue=${"title"}
@input=${this._valueChanged}
></ha-textfield>
<hui-theme-select-editor
<ha-theme-picker
.hass=${this.hass}
.value=${this._theme}
.label=${`${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`}
.configValue=${"theme"}
@value-changed=${this._valueChanged}
></hui-theme-select-editor>
></ha-theme-picker>
<div class="side-by-side">
<ha-formfield
.label=${this.hass.localize(

View File

@@ -112,6 +112,14 @@ export class HuiEntityCardEditor
);
}
if (schema.name === "theme") {
return `${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
}
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);

View File

@@ -172,6 +172,12 @@ export class HuiGaugeCardEditor
return this.hass!.localize(
"ui.panel.lovelace.editor.card.gauge.needle_gauge"
);
case "theme":
return `${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
}
return (
this.hass!.localize(

View File

@@ -117,11 +117,23 @@ export class HuiGlanceCardEditor
fireEvent(this, "config-changed", { config });
}
private _computeLabelCallback = (schema: HaFormSchema) =>
this.hass!.localize(
`ui.panel.lovelace.editor.card.glance.${schema.name}`
) ||
this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`);
private _computeLabelCallback = (schema: HaFormSchema) => {
if (schema.name === "theme") {
return `${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
}
return (
this.hass!.localize(
`ui.panel.lovelace.editor.card.glance.${schema.name}`
) ||
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
)
);
};
}
declare global {

View File

@@ -75,6 +75,14 @@ export class HuiHumidifierCardEditor
);
}
if (schema.name === "theme") {
return `${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
}
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);

View File

@@ -179,6 +179,14 @@ export class HuiLightCardEditor
);
}
if (schema.name === "theme") {
return `${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
}
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);

View File

@@ -96,11 +96,24 @@ export class HuiLogbookCardEditor
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (schema: HaFormSchema) =>
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
) ||
this.hass!.localize(`ui.panel.lovelace.editor.card.logbook.${schema.name}`);
private _computeLabelCallback = (schema: HaFormSchema) => {
if (schema.name === "theme") {
return `${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
}
return (
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
) ||
this.hass!.localize(
`ui.panel.lovelace.editor.card.logbook.${schema.name}`
)
);
};
}
declare global {

View File

@@ -58,13 +58,23 @@ export class HuiMarkdownCardEditor
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (schema: HaFormSchema) =>
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
) ||
this.hass!.localize(
`ui.panel.lovelace.editor.card.markdown.${schema.name}`
private _computeLabelCallback = (schema: HaFormSchema) => {
if (schema.name === "theme") {
return `${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
}
return (
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
) ||
this.hass!.localize(
`ui.panel.lovelace.editor.card.markdown.${schema.name}`
)
);
};
}
declare global {

View File

@@ -5,7 +5,7 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/entity/ha-entity-picker";
import { HomeAssistant } from "../../../../types";
import { MediaControlCardConfig } from "../../cards/types";
import "../../components/hui-theme-select-editor";
import "../../../../components/ha-theme-picker";
import { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditorTarget, EntitiesEditorEvent } from "../types";
@@ -62,12 +62,17 @@ export class HuiMediaControlCardEditor
@change=${this._valueChanged}
allow-custom-entity
></ha-entity-picker>
<hui-theme-select-editor
<ha-theme-picker
.label=${`${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`}
.hass=${this.hass}
.value=${this._theme}
.configValue=${"theme"}
@value-changed=${this._valueChanged}
></hui-theme-select-editor>
></ha-theme-picker>
</div>
`;
}

View File

@@ -6,7 +6,7 @@ import { ActionConfig } from "../../../../data/lovelace";
import { HomeAssistant } from "../../../../types";
import { PictureCardConfig } from "../../cards/types";
import "../../components/hui-action-editor";
import "../../components/hui-theme-select-editor";
import "../../../../components/ha-theme-picker";
import { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
@@ -72,12 +72,17 @@ export class HuiPictureCardEditor
.configValue=${"image"}
@input=${this._valueChanged}
></ha-textfield>
<hui-theme-select-editor
<ha-theme-picker
.hass=${this.hass}
.value=${this._theme}
.label=${`${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`}
.configValue=${"theme"}
@value-changed=${this._valueChanged}
></hui-theme-select-editor>
></ha-theme-picker>
<hui-action-editor
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.tap_action"

View File

@@ -170,6 +170,14 @@ export class HuiPictureEntityCardEditor
);
}
if (schema.name === "theme") {
return `${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
}
return (
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`

View File

@@ -165,6 +165,14 @@ export class HuiPictureGlanceCardEditor
);
}
if (schema.name === "theme") {
return `${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
}
return (
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`

View File

@@ -64,7 +64,13 @@ export class HuiPlantStatusCardEditor
"ui.panel.lovelace.editor.card.generic.entity"
);
}
if (schema.name === "theme") {
return `${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
}
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);

View File

@@ -153,6 +153,14 @@ export class HuiSensorCardEditor
);
}
if (schema.name === "theme") {
return `${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
}
if (schema.name === "detail") {
return this.hass!.localize(
"ui.panel.lovelace.editor.card.sensor.show_more_detail"

View File

@@ -6,7 +6,7 @@ import { isComponentLoaded } from "../../../../common/config/is_component_loaded
import { fireEvent } from "../../../../common/dom/fire_event";
import { HomeAssistant } from "../../../../types";
import { ShoppingListCardConfig } from "../../cards/types";
import "../../components/hui-theme-select-editor";
import "../../../../components/ha-theme-picker";
import { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditorTarget, EntitiesEditorEvent } from "../types";
@@ -67,12 +67,17 @@ export class HuiShoppingListEditor
.configValue=${"title"}
@input=${this._valueChanged}
></ha-textfield>
<hui-theme-select-editor
<ha-theme-picker
.hass=${this.hass}
.value=${this._theme}
.configValue=${"theme"}
.label=${`${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`}
@value-changed=${this._valueChanged}
></hui-theme-select-editor>
></ha-theme-picker>
</div>
`;
}

View File

@@ -71,6 +71,14 @@ export class HuiThermostatCardEditor
);
}
if (schema.name === "theme") {
return `${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
}
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);

View File

@@ -184,6 +184,14 @@ export class HuiWeatherForecastCardEditor
)})`;
}
if (schema.name === "theme") {
return `${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
}
return (
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`

View File

@@ -173,6 +173,9 @@ export class HaPickThemeRow extends LitElement {
}
private _supportsModeSelection(themeName: string): boolean {
if (!(themeName in this.hass.themes.themes)) {
return false; // User's theme no longer exists
}
return "modes" in this.hass.themes.themes[themeName];
}

View File

@@ -401,6 +401,10 @@
"add_device_id": "Choose device",
"add_entity_id": "Choose entity"
},
"theme-picker": {
"theme": "Theme",
"no_theme": "No theme"
},
"user-picker": {
"no_user": "No user",
"add_user": "Add user",
@@ -786,7 +790,31 @@
"door": "Door",
"garage_door": "Garage door",
"window": "Window",
"opening": "Other"
"opening": "Opening",
"battery": "Battery",
"battery_charging": "Battery charging",
"carbon_monoxide": "Carbon monoxide",
"cold": "Cold",
"connectivity": "Connectivity",
"gas": "Gas",
"heat": "Heat",
"light": "Light",
"lock": "Lock",
"moisture": "Moisture",
"motion": "Motion",
"moving": "Moving",
"occupancy": "Occupancy",
"plug": "Plug",
"power": "Power",
"presence": "Presence",
"problem": "Problem",
"running": "Running",
"safety": "Safety",
"smoke": "Smoke",
"sound": "Sound",
"tamper": "Tamper",
"update": "Update",
"vibration": "Vibration"
},
"cover": {
"door": "Door",
@@ -1079,9 +1107,9 @@
"learn_more": "Learn more"
},
"updates": {
"check_unavailable": {
"no_update_entities": {
"title": "Unable to check for updates",
"description": "You need to run the Home Assistant operating system to be able to check and install updates from the Home Assistant user interface."
"description": "You do not have any integrations that provide updates."
},
"check_updates": "Check for updates",
"no_new_updates": "No new updates found",
@@ -3458,7 +3486,6 @@
"tap_action": "Tap Action",
"title": "Title",
"theme": "Theme",
"no_theme": "No theme",
"unit": "Unit",
"url": "URL",
"state": "State",

View File

@@ -2975,17 +2975,17 @@ __metadata:
languageName: node
linkType: hard
"@mdi/js@npm:6.5.95":
version: 6.5.95
resolution: "@mdi/js@npm:6.5.95"
checksum: b1db7713d216c119f584bf973514a2f9d8f2e671e91bf19ce8e56cfa7a9843c0a060328e794507ac31f2bded1032123294f39ff8e987ea5acb2719ab522ef146
"@mdi/js@npm:6.6.95":
version: 6.6.95
resolution: "@mdi/js@npm:6.6.95"
checksum: 4cf8c48156f0e9ff67e4394cd428158bd164b1a6b7ca1aa70fc6a6aee91cfede9eba56720eb7d13fa57315ac636e9519a62dedd3cd2a9708aa11f2e3624ddbff
languageName: node
linkType: hard
"@mdi/svg@npm:6.5.95":
version: 6.5.95
resolution: "@mdi/svg@npm:6.5.95"
checksum: 2d45221d042d52d54c85eaf672a5f3697ed5201607fa38a6e235ee2e60d1c3c25d456a284f19ce47b5f06418cacfee29e8fecf6580b8c28538fd26044becaf1a
"@mdi/svg@npm:6.6.95":
version: 6.6.95
resolution: "@mdi/svg@npm:6.6.95"
checksum: 59b79db945847a3d981351418e0e7a457b831e09846fa751d44e80df8fb4cd19ef12bc889538ed2945d2638e522aa7ea5b1f97997e19dd68345f5d7bf5cad5e6
languageName: node
linkType: hard
@@ -9048,8 +9048,8 @@ fsevents@^1.2.7:
"@material/mwc-textfield": 0.25.3
"@material/mwc-top-app-bar-fixed": ^0.25.3
"@material/top-app-bar": 14.0.0-canary.261f2db59.0
"@mdi/js": 6.5.95
"@mdi/svg": 6.5.95
"@mdi/js": 6.6.95
"@mdi/svg": 6.6.95
"@open-wc/dev-server-hmr": ^0.0.2
"@polymer/app-layout": ^3.1.0
"@polymer/iron-flex-layout": ^3.0.1