Compare commits

...

17 Commits

Author SHA1 Message Date
Bram Kragten 88b36ec314 Cleanup some subscriptions 2023-01-09 10:29:40 +01:00
Felipe Santos f31a7c3af0 Fix issue with reload not working sometimes (#14939)
fixes undefined
2023-01-03 11:09:18 +01:00
Bram Kragten 44d91eaa4f Bumped version to 20230102.0 2023-01-02 21:28:30 +01:00
Allen Porter 3cc1cb7893 Rollback calendar trigger day offset support (#14933) 2023-01-02 21:27:50 +01:00
Paul Bottein e7354ed5a2 Do not close aliases dialog on enter (#14952)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-01-02 21:27:30 +01:00
Allen Porter e3ac2c149d Use translations for all fields in recurrence rule editor (#14940)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-01-02 20:19:36 +00:00
epenet afcd45a780 Enable unit conversion for DATA_SIZE (#14903)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-01-02 20:17:14 +00:00
Bram Kragten fe87466351 Add link to aliases in cloud config entity settings (#14959) 2023-01-02 20:42:31 +01:00
epenet bdef924426 Enable unit conversion for DATA_RATE (#14902)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-01-02 17:50:12 +00:00
Paul Bottein 86ea3082f7 Add aliases editor for helpers (#14951) 2023-01-02 12:10:52 +01:00
930913 0374330676 Add helper text to select slider (#14884)
* Add helper text to select slider

* Make helper text conditional

Only show if set.

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Lint changes

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-12-31 15:38:21 -05:00
Bram Kragten d8a68326fb Bumped version to 20221230.0 2022-12-30 13:21:47 +01:00
Allen Porter 4901d50918 Fix All Day recurring events that end on a specific date (#14905) 2022-12-30 13:15:07 +01:00
SukramJ a16e41a7ac Add support for unit conversion of electric current (#14916) 2022-12-30 13:06:11 +01:00
Jan Bouwhuis f1d644ac51 Add mV as unit for sensor device_class voltage (#14921) 2022-12-30 13:05:44 +01:00
karwosts a9378abe31 Fix days missing from ha-base-time-input _valueChanged (#14910)
* Fix days missing from ha-base-time-input _valueChanged

* style change
2022-12-29 00:06:48 -05:00
Gia Ferrari 5c2fcd7f9b Add alt attribute to various images (#14405)
* ha-config-area-page: Add alt tag for area-picture

* dialog-tag-detail: Add alt tag for generated QR code image.

* ha-config-hardware: Blank alt tag for hardware pic, info already elsewhere

* dialog-energy-solar-settings: Blank alt tag for brand icon.

* ha-energy-grid-settings: Blank alt tag for co2signal brand icon.

* Add a few more appropriately-blank alt texts.

* ha-config-device-page: Logo alt text set to name of device domain.

* ha-config-repairs: Logo alt text set to name of issue domain.

* hui-picture-card(-editor): Alternate Text via config (blank default)

* hui-picture-entity-card(-editor): Alternate Text via config (blank default)

* ha-long-lived-access-token-dialog: Alt text for QR code.

* hui-picture-header-footer: Support alt text via optional property.

* A few more blank alt attributes.

* ha-tile-image: Support alt tag (but it is blank in current usage).

* prod cla-bot

* Lint. Fix whitespace.

* Add missing alt text properties to TS types.

* Fix my silly typo in picture-entity-card-editor's SCHEMA (+ minor reformat)

* Add alt_text to Picture(Entity)CardConfig TypeScript types.

* Format with prettier.

* Revise alt text for tag QR

* Revise alt text for token QR

* Revise alternate to alternative

* Add alt to logo in gallery

* Add alt text to crop image

* Use ifDefined for tile image alt

* Change area picture alt to area name

* Remove entry from entities config struct

* Revert altText changes for Picture Entity Card (to revisit in future PR)

See:
https://github.com/home-assistant/frontend/pull/14405#discussion_r1032735871

* Revert changes to hui-image and picture entity editor

Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2022-12-29 01:16:05 +00:00
55 changed files with 475 additions and 332 deletions
-2
View File
@@ -71,7 +71,6 @@ class HaDemo extends HomeAssistantAppEl {
entity_category: null,
has_entity_name: false,
unique_id: "co2_intensity",
aliases: [],
},
{
config_entry_id: "co2signal",
@@ -87,7 +86,6 @@ class HaDemo extends HomeAssistantAppEl {
entity_category: null,
has_entity_name: false,
unique_id: "grid_fossil_fuel_percentage",
aliases: [],
},
]);
+3 -1
View File
@@ -98,7 +98,9 @@ const alerts: {
description: "Alert with slotted image",
type: "warning",
iconSlot: html`<span slot="icon" class="image"
><img src="https://www.home-assistant.io/images/home-assistant-logo.svg"
><img
alt="Home Assistant logo"
src="https://www.home-assistant.io/images/home-assistant-logo.svg"
/></span>`,
},
{
@@ -197,7 +197,6 @@ const createEntityRegistryEntries = (
platform: "updater",
has_entity_name: false,
unique_id: "updater",
aliases: [],
},
];
@@ -404,6 +404,7 @@ class HassioAddonInfo extends LitElement {
? html`
<img
class="logo"
alt=""
src="/api/hassio/addons/${this.addon.slug}/logo"
/>
`
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20221228.0"
version = "20230102.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"
+12 -48
View File
@@ -1,26 +1,15 @@
import "@material/mwc-button/mwc-button";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { stringCompare } from "../../common/string/compare";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import {
DeviceEntityLookup,
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "../ha-icon-button";
@@ -45,7 +34,7 @@ const rowRenderer: ComboBoxLitRenderer<AreaDevices> = (
</mwc-list-item>`;
@customElement("ha-area-devices-picker")
export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
export class HaAreaDevicesPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@@ -82,25 +71,22 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
@state() private _areaPicker = true;
@state() private _devices?: DeviceRegistryEntry[];
@state() private _areas?: AreaRegistryEntry[];
@state() private _entities?: EntityRegistryEntry[];
private _selectedDevices: string[] = [];
private _filteredDevices: DeviceRegistryEntry[] = [];
private _getAreasWithDevices = memoizeOne(
(
devices: DeviceRegistryEntry[],
areas: AreaRegistryEntry[],
entities: EntityRegistryEntry[],
deviceReg: HomeAssistant["devices"],
areas: HomeAssistant["areas"],
entityReg: HomeAssistant["entities"],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"]
): AreaDevices[] => {
const devices = Object.values(deviceReg);
const entities = Object.values(entityReg);
if (!devices.length) {
return [];
}
@@ -164,11 +150,6 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
this._filteredDevices = inputDevices;
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
for (const area of areas) {
areaLookup[area.area_id] = area;
}
const devicesByArea: DevicesByArea = {};
for (const device of inputDevices) {
@@ -177,7 +158,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
if (!(areaId in devicesByArea)) {
devicesByArea[areaId] = {
id: areaId,
name: areaLookup[areaId].name,
name: areas[areaId].name,
devices: [],
};
}
@@ -199,20 +180,6 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
}
);
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this._devices = devices;
}),
subscribeAreaRegistry(this.hass.connection!, (areas) => {
this._areas = areas;
}),
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities;
}),
];
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("area") && this.area) {
@@ -231,13 +198,10 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
}
protected render(): TemplateResult {
if (!this._devices || !this._areas || !this._entities) {
return html``;
}
const areas = this._getAreasWithDevices(
this._devices,
this._areas,
this._entities,
this.hass.devices,
this.hass.areas,
this.hass.entities,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses
+17 -47
View File
@@ -1,5 +1,4 @@
import "@material/mwc-list/mwc-list-item";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { customElement, property, query, state } from "lit/decorators";
@@ -7,21 +6,15 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { stringCompare } from "../../common/string/compare";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import {
computeDeviceName,
DeviceEntityLookup,
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "../ha-combo-box";
@@ -45,7 +38,7 @@ const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`<mwc-list-item
</mwc-list-item>`;
@customElement("ha-device-picker")
export class HaDevicePicker extends SubscribeMixin(LitElement) {
export class HaDevicePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@@ -54,12 +47,6 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@property() public helper?: string;
@property() public devices?: DeviceRegistryEntry[];
@property() public areas?: AreaRegistryEntry[];
@property() public entities?: EntityRegistryEntry[];
/**
* Show only devices with entities from specific domains.
* @type {Array}
@@ -106,15 +93,18 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
private _getDevices = memoizeOne(
(
devices: DeviceRegistryEntry[],
areas: AreaRegistryEntry[],
entities: EntityRegistryEntry[],
deviceReg: HomeAssistant["devices"],
areas: HomeAssistant["areas"],
entityReg: HomeAssistant["entities"],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"],
deviceFilter: this["deviceFilter"],
excludeDevices: this["excludeDevices"]
): Device[] => {
const devices = Object.values(deviceReg);
const entities = Object.values(entityReg);
if (!devices.length) {
return [
{
@@ -138,12 +128,6 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
deviceEntityLookup[entity.device_id].push(entity);
}
}
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
for (const area of areas) {
areaLookup[area.area_id] = area;
}
let inputDevices = devices.filter(
(device) => device.id === this.value || !device.disabled_by
);
@@ -214,8 +198,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
deviceEntityLookup[device.id]
),
area:
device.area_id && areaLookup[device.area_id]
? areaLookup[device.area_id].name
device.area_id && device.area_id in areas
? areas[device.area_id].name
: this.hass.localize("ui.components.device-picker.no_area"),
}));
if (!outputDevices.length) {
@@ -246,30 +230,16 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
await this.comboBox?.focus();
}
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this.devices = devices;
}),
subscribeAreaRegistry(this.hass.connection!, (areas) => {
this.areas = areas;
}),
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this.entities = entities;
}),
];
}
protected updated(changedProps: PropertyValues) {
if (
(!this._init && this.devices && this.areas && this.entities) ||
!this._init ||
(this._init && changedProps.has("_opened") && this._opened)
) {
this._init = true;
(this.comboBox as any).items = this._getDevices(
this.devices!,
this.areas!,
this.entities!,
this.hass.devices,
this.hass.areas,
this.hass.entities,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses,
+1 -1
View File
@@ -28,7 +28,7 @@ class StateInfo extends LitElement {
const name = computeStateName(this.stateObj);
return html` <state-badge
return html`<state-badge
.stateObj=${this.stateObj}
.stateColor=${true}
.color=${this.color}
+5 -1
View File
@@ -16,7 +16,11 @@ const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (
<span>${item.name}</span>
<span slot="secondary">${item.slug}</span>
${item.icon
? html`<img slot="graphic" .src="/api/hassio/addons/${item.slug}/icon" />`
? html`<img
alt=""
slot="graphic"
.src="/api/hassio/addons/${item.slug}/icon"
/>`
: ""}
</mwc-list-item>`;
+1 -2
View File
@@ -2,13 +2,12 @@ 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) {
export class HaAreasPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
+3
View File
@@ -266,6 +266,9 @@ export class HaBaseTimeInput extends LitElement {
seconds: this.seconds,
milliseconds: this.milliseconds,
};
if (this.enableDay) {
value.days = this.days;
}
if (this.format === 12) {
value.amPm = this.amPm;
}
+1
View File
@@ -59,6 +59,7 @@ class HaConfigEntryPicker extends LitElement {
>
<span slot="secondary">${item.localized_domain_name}</span>
<img
alt=""
slot="graphic"
src=${brandsUrl({
domain: item.domain,
+11 -1
View File
@@ -3,10 +3,12 @@ import { styles } from "@material/mwc-dialog/mwc-dialog.css";
import { mdiClose } from "@mdi/js";
import { css, html, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import type { HomeAssistant } from "../types";
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
import type { HomeAssistant } from "../types";
import "./ha-icon-button";
const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button"];
export const createCloseHeading = (
hass: HomeAssistant,
title: string | TemplateResult
@@ -32,6 +34,14 @@ export class HaDialog extends DialogBase {
return html`<slot name="heading"> ${super.renderHeading()} </slot>`;
}
protected firstUpdated(): void {
super.firstUpdated();
this.suppressDefaultPressSelector = [
this.suppressDefaultPressSelector,
SUPPRESS_DEFAULT_PRESS_SELECTOR,
].join(", ");
}
static override styles = [
styles,
css`
@@ -67,6 +67,9 @@ export class HaFormInteger extends LitElement implements HaFormElement {
@change=${this._valueChanged}
></ha-slider>
</div>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
</div>
`;
}
+10 -34
View File
@@ -1,4 +1,4 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { HassEntity } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
@@ -9,23 +9,16 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../data/area_registry";
import { AreaRegistryEntry } from "../data/area_registry";
import { ConfigEntry, getConfigEntries } from "../data/config_entries";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../data/device_registry";
import { DeviceRegistryEntry } from "../data/device_registry";
import { SceneEntity } from "../data/scene";
import { findRelated, ItemType, RelatedResult } from "../data/search";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { HomeAssistant } from "../types";
import "./ha-switch";
@customElement("ha-related-items")
export class HaRelatedItems extends SubscribeMixin(LitElement) {
export class HaRelatedItems extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public itemType!: ItemType;
@@ -34,23 +27,8 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
@state() private _entries?: ConfigEntry[];
@state() private _devices?: DeviceRegistryEntry[];
@state() private _areas?: AreaRegistryEntry[];
@state() private _related?: RelatedResult;
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this._devices = devices;
}),
subscribeAreaRegistry(this.hass.connection!, (areas) => {
this._areas = areas;
}),
];
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
getConfigEntries(this.hass).then((configEntries) => {
@@ -104,11 +82,10 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
`;
})
: ""}
${this._related.device && this._devices
${this._related.device
? this._related.device.map((relatedDeviceId) => {
const device: DeviceRegistryEntry | undefined = this._devices!.find(
(dev) => dev.id === relatedDeviceId
);
const device: DeviceRegistryEntry | undefined =
this.hass.devices[relatedDeviceId];
if (!device) {
return "";
}
@@ -125,11 +102,10 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
`;
})
: ""}
${this._related.area && this._areas
${this._related.area
? this._related.area.map((relatedAreaId) => {
const area: AreaRegistryEntry | undefined = this._areas!.find(
(ar) => ar.area_id === relatedAreaId
);
const area: AreaRegistryEntry | undefined =
this.hass.areas[relatedAreaId];
if (!area) {
return "";
}
+13 -13
View File
@@ -1,13 +1,10 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import { EntityRegistryEntry } from "../../data/entity_registry";
import {
EntitySources,
fetchEntitySourcesWithCache,
@@ -17,13 +14,12 @@ import {
filterSelectorDevices,
filterSelectorEntities,
} from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
import "../ha-area-picker";
import "../ha-areas-picker";
@customElement("ha-selector-area")
export class HaAreaSelector extends SubscribeMixin(LitElement) {
export class HaAreaSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: AreaSelector;
@@ -44,12 +40,16 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities.filter((entity) => entity.device_id !== null);
}),
];
protected willUpdate(changedProperties: PropertyValues): void {
if (
changedProperties.has("hass") &&
(changedProperties.get("hass") as HomeAssistant | undefined)?.entities !==
this.hass.entities
) {
this._entities = Object.values(this.hass.entities).filter(
(entity) => entity.device_id !== null
);
}
}
protected updated(changedProperties: PropertyValues): void {
+6 -1
View File
@@ -1,14 +1,19 @@
import { CSSResultGroup, html, css, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
@customElement("ha-tile-image")
export class HaTileImage extends LitElement {
@property() public imageUrl?: string;
@property() public imageAlt?: string;
protected render(): TemplateResult {
return html`
<div class="image">
${this.imageUrl ? html`<img src=${this.imageUrl} />` : null}
${this.imageUrl
? html`<img alt=${ifDefined(this.imageAlt)} src=${this.imageUrl} />`
: null}
</div>
`;
}
+1 -1
View File
@@ -22,7 +22,6 @@ export interface EntityRegistryEntry {
original_name?: string;
unique_id: string;
translation_key?: string;
aliases: string[];
}
export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
@@ -30,6 +29,7 @@ export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
original_icon?: string;
device_class?: string;
original_device_class?: string;
aliases: string[];
}
export interface UpdateEntityRegistryEntryResult {
@@ -74,7 +74,7 @@ export class HaImagecropperDialog extends LitElement {
round: Boolean(this._params?.options.round),
})}"
>
<img />
<img alt=${this.hass.localize("ui.dialogs.image_cropper.crop_image")} />
</div>
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.cancel")}
+1
View File
@@ -19,6 +19,7 @@ class IntegrationBadge extends LitElement {
return html`
<div class="icon">
<img
alt=""
src=${brandsUrl({
domain: this.domain,
type: "icon",
@@ -250,6 +250,7 @@ class DialogCalendarEventEditor extends LitElement {
<ha-recurrence-rule-editor
.hass=${this.hass}
.dtstart=${this._dtstart}
.allDay=${this._allDay}
.locale=${this.hass.locale}
.timezone=${this.hass.config.time_zone}
.value=${this._rrule || ""}
@@ -6,6 +6,7 @@ import type { Options, WeekdayStr } from "rrule";
import { ByWeekday, RRule, Weekday } from "rrule";
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { LocalizeKeys } from "../../common/translations/localize";
import "../../components/ha-chip";
import "../../components/ha-list-item";
import "../../components/ha-select";
@@ -19,12 +20,10 @@ import {
getWeekday,
getWeekdays,
getMonthlyRepeatItems,
intervalSuffix,
RepeatEnd,
RepeatFrequency,
ruleByWeekDay,
untilValue,
WEEKDAY_NAME,
MonthlyRepeatItem,
getMonthlyRepeatWeekdayFromRule,
getMonthdayRepeatFromRule,
@@ -41,6 +40,8 @@ export class RecurrenceRuleEditor extends LitElement {
@property() public dtstart?: Date;
@property() public allDay?: boolean;
@property({ attribute: false }) public locale!: HomeAssistant["locale"];
@property() public timezone?: string;
@@ -172,18 +173,36 @@ export class RecurrenceRuleEditor extends LitElement {
return html`
<ha-select
id="freq"
label="Repeat"
label=${this.hass.localize("ui.components.calendar.event.repeat.label")}
@selected=${this._onRepeatSelected}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._freq}
>
<ha-list-item value="none">None</ha-list-item>
<ha-list-item value="yearly">Yearly</ha-list-item>
<ha-list-item value="monthly">Monthly</ha-list-item>
<ha-list-item value="weekly">Weekly</ha-list-item>
<ha-list-item value="daily">Daily</ha-list-item>
<ha-list-item value="none">
${this.hass.localize("ui.components.calendar.event.repeat.freq.none")}
</ha-list-item>
<ha-list-item value="yearly">
${this.hass.localize(
"ui.components.calendar.event.repeat.freq.yearly"
)}
</ha-list-item>
<ha-list-item value="monthly">
${this.hass.localize(
"ui.components.calendar.event.repeat.freq.monthly"
)}
</ha-list-item>
<ha-list-item value="weekly">
${this.hass.localize(
"ui.components.calendar.event.repeat.freq.weekly"
)}
</ha-list-item>
<ha-list-item value="daily">
${this.hass.localize(
"ui.components.calendar.event.repeat.freq.daily"
)}
</ha-list-item>
</ha-select>
`;
}
@@ -194,7 +213,9 @@ export class RecurrenceRuleEditor extends LitElement {
${this._monthlyRepeatItems.length > 0
? html`<ha-select
id="monthly"
label="Repeat Monthly"
label=${this.hass.localize(
"ui.components.calendar.event.repeat.monthly.label"
)}
@selected=${this._onMonthlyDetailSelected}
.value=${this._monthlyRepeat || this._monthlyRepeatItems[0]?.value}
@closed=${stopPropagation}
@@ -223,7 +244,11 @@ export class RecurrenceRuleEditor extends LitElement {
.value=${item}
class=${classMap({ active: this._weekday.has(item) })}
@click=${this._onWeekdayToggle}
>${WEEKDAY_NAME[item]}</ha-chip
>${this.hass.localize(
`ui.components.calendar.event.repeat.weekly.weekday.${
item.toLowerCase() as Lowercase<WeekdayStr>
}`
)}</ha-chip
>
`
)}
@@ -239,11 +264,16 @@ export class RecurrenceRuleEditor extends LitElement {
return html`
<ha-textfield
id="interval"
label="Repeat interval"
label=${this.hass.localize(
"ui.components.calendar.event.repeat.interval.label"
)}
type="number"
min="1"
.value=${this._interval}
.suffix=${intervalSuffix(this._freq!)}
.suffix=${this.hass.localize(
`ui.components.calendar.event.repeat.interval.${this
._freq!}` as LocalizeKeys
)}
@change=${this._onIntervalChange}
></ha-textfield>
`;
@@ -253,26 +283,38 @@ export class RecurrenceRuleEditor extends LitElement {
return html`
<ha-select
id="end"
label="Ends"
label=${this.hass.localize(
"ui.components.calendar.event.repeat.end.label"
)}
.value=${this._end}
@selected=${this._onEndSelected}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
<ha-list-item value="never">Never</ha-list-item>
<ha-list-item value="after">After</ha-list-item>
<ha-list-item value="on">On</ha-list-item>
<ha-list-item value="never">
${this.hass.localize("ui.components.calendar.event.repeat.end.never")}
</ha-list-item>
<ha-list-item value="after">
${this.hass.localize("ui.components.calendar.event.repeat.end.after")}
</ha-list-item>
<ha-list-item value="on">
${this.hass.localize("ui.components.calendar.event.repeat.end.on")}
</ha-list-item>
</ha-select>
${this._end === "after"
? html`
<ha-textfield
id="after"
label="End after"
label=${this.hass.localize(
"ui.components.calendar.event.repeat.end_after.label"
)}
type="number"
min="1"
.value=${this._count!}
suffix="ocurrences"
suffix=${this.hass.localize(
"ui.components.calendar.event.repeat.end_after.ocurrences"
)}
@change=${this._onCountChange}
></ha-textfield>
`
@@ -281,7 +323,9 @@ export class RecurrenceRuleEditor extends LitElement {
? html`
<ha-date-input
id="on"
label="End on"
label=${this.hass.localize(
"ui.components.calendar.event.repeat.end_on.label"
)}
.locale=${this.locale}
.value=${this._until!.toISOString()}
@value-changed=${this._onUntilChange}
@@ -402,7 +446,14 @@ export class RecurrenceRuleEditor extends LitElement {
byweekday: byweekday,
bymonthday: bymonthday,
};
const contentline = RRule.optionsToString(options);
let contentline = RRule.optionsToString(options);
if (this._until && this.allDay) {
// rrule.js only computes UNTIL values as DATE-TIME however rfc5545 says
// The value of the UNTIL rule part MUST have the same value type as the
// "DTSTART" property. If needed, strip off any time values as a workaround
// This converts "UNTIL=20220512T060000" to "UNTIL=20220512"
contentline = contentline.replace(/(UNTIL=\d{8})T\d{6}Z?/, "$1");
}
return contentline.slice(6); // Strip "RRULE:" prefix
}
-20
View File
@@ -42,16 +42,6 @@ export interface MonthlyRepeatItem {
label: string;
}
export function intervalSuffix(freq: RepeatFrequency) {
if (freq === "monthly") {
return "months";
}
if (freq === "weekly") {
return "weeks";
}
return "days";
}
export function untilValue(freq: RepeatFrequency): Date {
const today = new Date();
const increment = DEFAULT_COUNT[freq];
@@ -102,16 +92,6 @@ export const convertRepeatFrequency = (
}
};
export const WEEKDAY_NAME = {
SU: "Sun",
MO: "Mon",
TU: "Tue",
WE: "Wed",
TH: "Thu",
FR: "Fri",
SA: "Sat",
};
export const WEEKDAYS = [
RRule.SU,
RRule.MO,
+13 -51
View File
@@ -2,10 +2,7 @@ import "@material/mwc-button";
import { mdiImagePlus, mdiPencil } from "@mdi/js";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import {
HassEntity,
UnsubscribeFunc,
} from "home-assistant-js-websocket/dist/types";
import { HassEntity } from "home-assistant-js-websocket/dist/types";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
@@ -22,7 +19,6 @@ import "../../../components/ha-icon-next";
import {
AreaRegistryEntry,
deleteAreaRegistryEntry,
subscribeAreaRegistry,
updateAreaRegistryEntry,
} from "../../../data/area_registry";
import { AutomationEntity } from "../../../data/automation";
@@ -30,20 +26,19 @@ import {
computeDeviceName,
DeviceRegistryEntry,
sortDeviceRegistryByName,
subscribeDeviceRegistry,
} from "../../../data/device_registry";
import {
computeEntityRegistryName,
EntityRegistryEntry,
sortEntityRegistryByName,
subscribeEntityRegistry,
} from "../../../data/entity_registry";
import { SceneEntity } from "../../../data/scene";
import { ScriptEntity } from "../../../data/script";
import { findRelated, RelatedResult } from "../../../data/search";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../../logbook/ha-logbook";
@@ -51,8 +46,6 @@ import {
loadAreaRegistryDetailDialog,
showAreaRegistryDetailDialog,
} from "./show-dialog-area-registry-detail";
import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-subpage";
declare type NameAndEntity<EntityType extends HassEntity> = {
name: string;
@@ -60,7 +53,7 @@ declare type NameAndEntity<EntityType extends HassEntity> = {
};
@customElement("ha-config-area-page")
class HaConfigAreaPage extends SubscribeMixin(LitElement) {
class HaConfigAreaPage extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public areaId!: string;
@@ -71,33 +64,19 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public showAdvanced!: boolean;
@state() public _areas!: AreaRegistryEntry[];
@state() public _devices!: DeviceRegistryEntry[];
@state() public _entities!: EntityRegistryEntry[];
@state() private _related?: RelatedResult;
private _logbookTime = { recent: 86400 };
private _area = memoizeOne(
(
areaId: string,
areas: AreaRegistryEntry[]
): AreaRegistryEntry | undefined =>
areas.find((area) => area.area_id === areaId)
);
private _memberships = memoizeOne(
(
areaId: string,
registryDevices: DeviceRegistryEntry[],
registryEntities: EntityRegistryEntry[]
registryDevices: HomeAssistant["devices"],
registryEntities: HomeAssistant["entities"]
) => {
const devices = new Map<string, DeviceRegistryEntry>();
for (const device of registryDevices) {
for (const device of Object.values(registryDevices)) {
if (device.area_id === areaId) {
devices.set(device.id, device);
}
@@ -106,7 +85,7 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
const entities: EntityRegistryEntry[] = [];
const indirectEntities: EntityRegistryEntry[] = [];
for (const entity of registryEntities) {
for (const entity of Object.values(registryEntities)) {
if (entity.area_id) {
if (entity.area_id === areaId) {
entities.push(entity);
@@ -150,26 +129,8 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
}
}
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeAreaRegistry(this.hass.connection, (areas) => {
this._areas = areas;
}),
subscribeDeviceRegistry(this.hass.connection, (entries) => {
this._devices = entries;
}),
subscribeEntityRegistry(this.hass.connection, (entries) => {
this._entities = entries;
}),
];
}
protected render(): TemplateResult {
if (!this._areas || !this._devices || !this._entities) {
return html``;
}
const area = this._area(this.areaId, this._areas);
const area = this.hass.areas[this.areaId];
if (!area) {
return html`
@@ -182,8 +143,8 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
const memberships = this._memberships(
this.areaId,
this._devices,
this._entities
this.hass.devices,
this.hass.entities
);
const { devices, entities } = memberships;
@@ -258,7 +219,8 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
<div class="column">
${area.picture
? html`<div class="img-container">
<img src=${area.picture} /><ha-icon-button
<img alt=${area.name} src=${area.picture} />
<ha-icon-button
.path=${mdiPencil}
.entry=${area}
@click=${this._showSettings}
@@ -541,15 +541,10 @@ export class HaAutomationTrace extends LitElement {
justify-content: center;
display: flex;
}
.info {
flex: 1;
background-color: var(--card-background-color);
}
.linkButton {
color: var(--primary-text-color);
}
.trace-link {
text-decoration: none;
}
@@ -46,7 +46,7 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement {
],
],
},
{ name: "offset", selector: { duration: { enable_day: true } } },
{ name: "offset", selector: { duration: {} } },
{
name: "offset_type",
type: "select",
+40 -24
View File
@@ -9,7 +9,6 @@ import {
mdiFormatListChecks,
mdiSync,
} from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -43,20 +42,20 @@ import {
} from "../../../../data/cloud";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
getExtendedEntityRegistryEntry,
updateEntityRegistryEntry,
} from "../../../../data/entity_registry";
import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show-dialog-domain-toggler";
import "../../../../layouts/hass-loading-screen";
import "../../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { showEntityAliasesDialog } from "../../entities/entity-aliases/show-dialog-entity-aliases";
const DEFAULT_CONFIG_EXPOSE = true;
const IGNORE_INTERFACES = ["Alexa.EndpointHealth"];
@customElement("cloud-alexa")
class CloudAlexa extends SubscribeMixin(LitElement) {
class CloudAlexa extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property()
@@ -171,10 +170,17 @@ class CloudAlexa extends SubscribeMixin(LitElement) {
secondary-line
@click=${this._showMoreInfo}
>
${entity.interfaces
.filter((ifc) => !IGNORE_INTERFACES.includes(ifc))
.map((ifc) => ifc.replace(/(Alexa.|Controller)/g, ""))
.join(", ")}
${entity.entity_id in this.hass.entities
? html`<button
class="link"
.entityId=${entity.entity_id}
@click=${this._openAliasesSettings}
>
${this.hass.localize(
"ui.panel.config.cloud.alexa.manage_aliases"
)}
</button>`
: ""}
</state-info>
${!emptyFilter
? html`${iconButton}`
@@ -323,23 +329,33 @@ class CloudAlexa extends SubscribeMixin(LitElement) {
if (changedProps.has("cloudStatus")) {
this._entityConfigs = this.cloudStatus.prefs.alexa_entity_configs;
}
if (
changedProps.has("hass") &&
changedProps.get("hass")?.entities !== this.hass.entities
) {
const categories = {};
for (const entry of Object.values(this.hass.entities)) {
categories[entry.entity_id] = entry.entity_category;
}
this._entityCategories = categories;
}
}
protected override hassSubscribe(): (
| UnsubscribeFunc
| Promise<UnsubscribeFunc>
)[] {
return [
subscribeEntityRegistry(this.hass.connection, (entries) => {
const categories = {};
for (const entry of entries) {
categories[entry.entity_id] = entry.entity_category;
}
this._entityCategories = categories;
}),
];
private async _openAliasesSettings(ev) {
ev.stopPropagation();
const entityId = ev.target.entityId;
const entry = await getExtendedEntityRegistryEntry(this.hass, entityId);
if (!entry) {
return;
}
showEntityAliasesDialog(this, {
entity: entry,
updateEntry: async (updates) => {
await updateEntityRegistryEntry(this.hass, entry.entity_id, updates);
},
});
}
private async _fetchData() {
@@ -9,7 +9,6 @@ import {
mdiFormatListChecks,
mdiSync,
} from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -41,7 +40,8 @@ import {
} from "../../../../data/cloud";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
getExtendedEntityRegistryEntry,
updateEntityRegistryEntry,
} from "../../../../data/entity_registry";
import {
fetchCloudGoogleEntities,
@@ -51,15 +51,15 @@ import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
import "../../../../layouts/hass-loading-screen";
import "../../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../resources/styles";
import { buttonLinkStyle, haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast";
import { showEntityAliasesDialog } from "../../entities/entity-aliases/show-dialog-entity-aliases";
const DEFAULT_CONFIG_EXPOSE = true;
@customElement("cloud-google-assistant")
class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
class CloudGoogleAssistant extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public cloudStatus!: CloudStatusLoggedIn;
@@ -174,15 +174,23 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
secondary-line
@click=${this._showMoreInfo}
>
${entity.traits
.map((trait) => trait.substr(trait.lastIndexOf(".") + 1))
.join(", ")}
${entity.entity_id in this.hass.entities
? html`<button
class="link"
.entityId=${entity.entity_id}
@click=${this._openAliasesSettings}
>
${this.hass.localize(
"ui.panel.config.cloud.google.manage_aliases"
)}
</button>`
: ""}
</state-info>
${!emptyFilter
? html`${iconButton}`
: html`<ha-button-menu
corner="BOTTOM_START"
.entityId=${stateObj.entity_id}
.entityId=${entity.entity_id}
@action=${this._exposeChanged}
>
${iconButton}
@@ -308,7 +316,7 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
</h3>
${!this.narrow
? this.hass!.localize(
"ui.panel.config.cloud.alexa.exposed",
"ui.panel.config.cloud.google.exposed",
"selected",
selected
)
@@ -329,7 +337,7 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
</h3>
${!this.narrow
? this.hass!.localize(
"ui.panel.config.cloud.alexa.not_exposed",
"ui.panel.config.cloud.google.not_exposed",
"selected",
this._entities.length - selected
)
@@ -354,23 +362,33 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
if (changedProps.has("cloudStatus")) {
this._entityConfigs = this.cloudStatus.prefs.google_entity_configs;
}
if (
changedProps.has("hass") &&
changedProps.get("hass")?.entities !== this.hass.entities
) {
const categories = {};
for (const entry of Object.values(this.hass.entities)) {
categories[entry.entity_id] = entry.entity_category;
}
this._entityCategories = categories;
}
}
protected override hassSubscribe(): (
| UnsubscribeFunc
| Promise<UnsubscribeFunc>
)[] {
return [
subscribeEntityRegistry(this.hass.connection, (entries) => {
const categories = {};
for (const entry of entries) {
categories[entry.entity_id] = entry.entity_category;
}
this._entityCategories = categories;
}),
];
private async _openAliasesSettings(ev) {
ev.stopPropagation();
const entityId = ev.target.entityId;
const entry = await getExtendedEntityRegistryEntry(this.hass, entityId);
if (!entry) {
return;
}
showEntityAliasesDialog(this, {
entity: entry,
updateEntry: async (updates) => {
await updateEntityRegistryEntry(this.hass, entry.entity_id, updates);
},
});
}
private _configIsDomainExposed(
@@ -583,6 +601,7 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
static get styles(): CSSResultGroup {
return [
haStyle,
buttonLinkStyle,
css`
mwc-list-item > [slot="meta"] {
margin-left: 4px;
@@ -659,6 +659,10 @@ export class HaConfigDevicePage extends LitElement {
integrations.length
? html`
<img
alt=${domainToName(
this.hass.localize,
integrations[0].domain
)}
src=${brandsUrl({
domain: integrations[0].domain,
type: "logo",
@@ -220,6 +220,7 @@ export class EnergyGridSettings extends LitElement {
${this._co2ConfigEntry
? html`<div class="row" .entry=${this._co2ConfigEntry}>
<img
alt=""
referrerpolicy="no-referrer"
src=${brandsUrl({
domain: "co2signal",
@@ -244,6 +245,7 @@ export class EnergyGridSettings extends LitElement {
: html`
<div class="row border-bottom">
<img
alt=""
referrerpolicy="no-referrer"
src=${brandsUrl({
domain: "co2signal",
@@ -130,6 +130,7 @@ export class DialogEnergySolarSettings
style="display: flex; align-items: center;"
>
<img
alt=""
referrerpolicy="no-referrer"
style="height: 24px; margin-right: 16px;"
src=${brandsUrl({
@@ -57,7 +57,13 @@ export class EntityRegistrySettingsHelper extends LitElement {
super.updated(changedProperties);
if (changedProperties.has("entry")) {
this._error = undefined;
this._item = undefined;
if (
this.entry.unique_id !==
(changedProperties.get("entry") as ExtEntityRegistryEntry)?.unique_id
) {
this._item = undefined;
}
this._getItem();
}
}
@@ -72,16 +72,21 @@ class DialogEntityAliases extends LitElement {
dialogInitialFocus=${index}
.index=${index}
class="flex-auto"
label="Alias"
.label=${this.hass!.localize(
"ui.dialogs.entity_registry.editor.aliases.input_label",
{ number: index + 1 }
)}
.value=${alias}
?data-last=${index === this._aliases.length - 1}
@change=${this._editAlias}
@input=${this._editAlias}
@keydown=${this._keyDownAlias}
></ha-textfield>
<ha-icon-button
.index=${index}
slot="navigationIcon"
label=${this.hass!.localize(
"ui.dialogs.entity_registry.editor.aliases.remove_alias"
"ui.dialogs.entity_registry.editor.aliases.remove_alias",
{ number: index + 1 }
)}
@click=${this._removeAlias}
.path=${mdiDeleteOutline}
@@ -133,6 +138,13 @@ class DialogEntityAliases extends LitElement {
this._aliases[index] = (ev.target as any).value;
}
private async _keyDownAlias(ev: KeyboardEvent) {
if (ev.key === "Enter") {
ev.stopPropagation();
this._addAlias();
}
}
private async _removeAlias(ev: Event) {
const index = (ev.target as any).index;
const aliases = [...this._aliases];
@@ -1,11 +1,11 @@
import { fireEvent } from "../../../../common/dom/fire_event";
import {
EntityRegistryEntry,
EntityRegistryEntryUpdateParams,
ExtEntityRegistryEntry,
} from "../../../../data/entity_registry";
export interface EntityAliasesDialogParams {
entity: EntityRegistryEntry;
entity: ExtEntityRegistryEntry;
updateEntry: (
updates: Partial<EntityRegistryEntryUpdateParams>
) => Promise<unknown>;
@@ -1,7 +1,11 @@
import "@material/mwc-formfield/mwc-formfield";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiPencil } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import "../../../components/ha-area-picker";
import "../../../components/ha-expansion-panel";
@@ -21,6 +25,7 @@ import {
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../types";
import { showEntityAliasesDialog } from "./entity-aliases/show-dialog-entity-aliases";
@customElement("ha-registry-basic-editor")
export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
@@ -44,6 +49,21 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
@state() private _submitting = false;
private _handleAliasesClicked(ev: CustomEvent) {
if (ev.detail.index !== 0) return;
showEntityAliasesDialog(this, {
entity: this.entry!,
updateEntry: async (updates) => {
const result = await updateEntityRegistryEntry(
this.hass,
this.entry.entity_id,
updates
);
fireEvent(this, "entity-entry-updated", result.entity_entry);
},
});
}
public async updateEntry(): Promise<void> {
this._submitting = true;
const params: Partial<EntityRegistryEntryUpdateParams> = {
@@ -247,6 +267,33 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
</div>
`
: ""}
<div class="label">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.aliases_section"
)}
</div>
<mwc-list class="aliases" @action=${this._handleAliasesClicked}>
<mwc-list-item .twoline=${this.entry.aliases.length > 0} hasMeta>
<span>
${this.entry.aliases.length > 0
? this.hass.localize(
"ui.dialogs.entity_registry.editor.configured_aliases",
{ count: this.entry.aliases.length }
)
: this.hass.localize(
"ui.dialogs.entity_registry.editor.no_aliases"
)}
</span>
<span slot="secondary">${this.entry.aliases.join(", ")}</span>
<ha-svg-icon slot="meta" .path=${mdiPencil}></ha-svg-icon>
</mwc-list-item>
</mwc-list>
<div class="secondary">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.aliases.description"
)}
</div>
</ha-expansion-panel>
`;
}
@@ -300,6 +347,13 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
.label {
margin-top: 16px;
}
.aliases {
border-radius: 4px;
margin-top: 4px;
margin-bottom: 4px;
--mdc-icon-button-size: 24px;
overflow: hidden;
}
`;
}
}
@@ -118,6 +118,43 @@ const OVERRIDE_NUMBER_UNITS = {
};
const OVERRIDE_SENSOR_UNITS = {
current: ["A", "mA"],
data_rate: [
"bit/s",
"kbit/s",
"Mbit/s",
"Gbit/s",
"B/s",
"kB/s",
"MB/s",
"GB/s",
"KiB/s",
"MiB/s",
"GiB/s",
],
data_size: [
"bit",
"kbit",
"Mbit",
"Gbit",
"B",
"kB",
"MB",
"GB",
"TB",
"PB",
"EB",
"ZB",
"YB",
"KiB",
"MiB",
"GiB",
"TiB",
"PiB",
"EiB",
"ZiB",
"YiB",
],
distance: ["cm", "ft", "in", "km", "m", "mi", "mm", "yd"],
gas: ["CCF", "ft³", "m³"],
precipitation: ["cm", "in", "mm"],
@@ -125,6 +162,7 @@ const OVERRIDE_SENSOR_UNITS = {
pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"],
speed: ["ft/s", "in/d", "in/h", "km/h", "kn", "m/s", "mm/d", "mm/h", "mph"],
temperature: ["°C", "°F", "K"],
voltage: ["V", "mV"],
volume: ["CCF", "fl. oz.", "ft³", "gal", "L", "mL", "m³"],
water: ["CCF", "ft³", "gal", "L", "m³"],
weight: ["g", "kg", "lb", "mg", "oz", "st", "µg"],
@@ -769,12 +807,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
"ui.dialogs.entity_registry.editor.aliases_section"
)}
</div>
<mwc-list class="aliases">
<mwc-list-item
.twoline=${this.entry.aliases.length > 0}
hasMeta
@click=${this._openAliasesSettings}
>
<mwc-list class="aliases" @action=${this._handleAliasesClicked}>
<mwc-list-item .twoline=${this.entry.aliases.length > 0} hasMeta>
<span>
${this.entry.aliases.length > 0
? this.hass.localize(
@@ -977,7 +1011,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
});
}
private _openAliasesSettings() {
private _handleAliasesClicked(ev: CustomEvent) {
if (ev.detail.index !== 0) return;
showEntityAliasesDialog(this, {
entity: this.entry!,
updateEntry: async (updates) => {
@@ -728,7 +728,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
selectable: false,
entity_category: null,
has_entity_name: false,
aliases: [],
});
}
if (changed) {
@@ -305,7 +305,7 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
.twoline=${Boolean(boardId)}
>
${imageURL
? html`<img slot="graphic" src=${imageURL} />`
? html`<img alt="" slot="graphic" src=${imageURL} />`
: ""}
<span class="primary-text">
${boardName ||
@@ -48,6 +48,7 @@ class HaDomainIntegrations extends LitElement {
hasMeta
>
<img
alt=""
slot="graphic"
loading="lazy"
src=${brandsUrl({
@@ -92,6 +92,7 @@ export class HaIntegrationHeader extends LitElement {
<slot name="above-header"></slot>
<div class="header">
<img
alt=""
src=${brandsUrl({
domain: this.domain,
type: "icon",
@@ -47,6 +47,7 @@ export class HaIntegrationListItem extends ListItemBase {
)}"
>
<img
alt=""
loading="lazy"
src=${brandsUrl({
domain: this.integration.domain,
@@ -52,6 +52,7 @@ class HaConfigRepairs extends LitElement {
@click=${this._openShowMoreDialog}
>
<img
alt=${domainToName(this.hass.localize, issue.domain)}
loading="lazy"
src=${brandsUrl({
domain: issue.issue_domain || issue.domain,
@@ -65,6 +65,7 @@ class IntegrationsStartupTime extends LitElement {
href=${docLink}
>
<img
alt=""
loading="lazy"
src=${brandsUrl({
domain: setup.domain,
@@ -527,15 +527,10 @@ export class HaScriptTrace extends LitElement {
:host([narrow]) .graph {
max-width: 100%;
}
.info {
flex: 1;
background-color: var(--card-background-color);
}
.linkButton {
color: var(--primary-text-color);
}
.trace-link {
text-decoration: none;
}
+8 -1
View File
@@ -244,7 +244,14 @@ class DialogTagDetail
canvas.height / 3
);
this._qrCode = html`<img src=${canvas.toDataURL()}></img>`;
this._qrCode = html`<img
alt=${this.hass.localize(
"ui.panel.config.tag.qr_code_image",
"name",
this._name
)}
src=${canvas.toDataURL()}
></img>`;
}
static get styles(): CSSResultGroup {
@@ -101,7 +101,10 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
),
})}
>
<img src=${this.hass.hassUrl(this._config.image)} />
<img
alt=${this._config.alt_text}
src=${this.hass.hassUrl(this._config.image)}
/>
</ha-card>
`;
}
+1
View File
@@ -332,6 +332,7 @@ export interface PictureCardConfig extends LovelaceCardConfig {
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
theme?: string;
alt_text?: string;
}
export interface PictureElementsCardConfig extends LovelaceCardConfig {
+1 -1
View File
@@ -46,7 +46,7 @@ export const handleAction = async (
actionConfig.confirmation &&
(!actionConfig.confirmation.exemptions ||
!actionConfig.confirmation.exemptions.some(
(e) => e.user === hass!.user!.id
(e) => e.user === hass!.user?.id
))
) {
forwardHaptic("warning");
@@ -20,6 +20,7 @@ const cardConfigStruct = assign(
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
theme: optional(string()),
alt_text: optional(string()),
})
);
@@ -53,6 +54,10 @@ export class HuiPictureCardEditor
return this._config!.theme || "";
}
get _alt_text(): string {
return this._config!.alt_text || "";
}
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
@@ -72,6 +77,16 @@ export class HuiPictureCardEditor
.configValue=${"image"}
@input=${this._valueChanged}
></ha-textfield>
<ha-textfield
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.alt_text"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._alt_text}
.configValue=${"alt_text"}
@input=${this._valueChanged}
></ha-textfield>
<ha-theme-picker
.hass=${this.hass}
.value=${this._theme}
@@ -68,6 +68,7 @@ export class HuiPictureHeaderFooter
return html`
<img
alt=${this._config!.alt_text}
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
@@ -17,6 +17,7 @@ export const pictureHeaderFooterConfigStruct = object({
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
alt_text: optional(string()),
});
export const buttonsHeaderFooterConfigStruct = object({
@@ -27,4 +27,5 @@ export interface PictureHeaderFooterConfig extends LovelaceHeaderFooterConfig {
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
alt_text?: string;
}
+2 -2
View File
@@ -266,7 +266,7 @@ class HUIRoot extends LitElement {
((Array.isArray(view.visible) &&
!view.visible.some(
(e) =>
e.user === this.hass!.user!.id
e.user === this.hass!.user?.id
)) ||
view.visible === false))
),
@@ -470,7 +470,7 @@ class HUIRoot extends LitElement {
view.visible !== undefined &&
((Array.isArray(view.visible) &&
!view.visible.some(
(e) => e.user === this.hass!.user!.id
(e) => e.user === this.hass!.user?.id
)) ||
view.visible === false)
),
@@ -85,7 +85,14 @@ export class HaLongLivedAccessTokenDialog extends LitElement {
canvas.height / 3
);
this._qrCode = html`<img src=${canvas.toDataURL()}></img>`;
this._qrCode = html`<img
alt=${this.hass.localize(
"ui.panel.profile.long_lived_access_tokens.qr_code_image",
"name",
this._params!.name
)}
src=${canvas.toDataURL()}
></img>`;
}
static get styles(): CSSResultGroup {
+38 -4
View File
@@ -644,6 +644,33 @@
"monthly": "months",
"weekly": "weeks",
"daily": "days"
},
"monthly": {
"label": "Repeat Monthly"
},
"weekly": {
"weekday": {
"su": "Sun",
"mo": "Mon",
"tu": "Tue",
"we": "Wed",
"th": "Thu",
"fr": "Fri",
"sa": "Sat"
}
},
"end": {
"label": "End",
"never": "Never",
"after": "After",
"on": "On"
},
"end_on": {
"label": "End On"
},
"end_after": {
"label": "End After",
"ocurrences": "ocurrences"
}
},
"rrule": {
@@ -791,7 +818,8 @@
"close": "Close"
},
"image_cropper": {
"crop": "Crop"
"crop": "Crop",
"crop_image": "Picture to crop"
},
"date-picker": {
"today": "Today"
@@ -975,7 +1003,8 @@
"aliases": {
"heading": "{name} aliases",
"description": "Aliases are alternative names used in voice assistants to refer to this entity.",
"remove_alias": "Remove alias",
"remove_alias": "Remove alias {number}",
"input_label": "Alias {number}",
"save": "Save",
"add_alias": "Add alias",
"no_aliases": "No aliases have been added yet",
@@ -1435,6 +1464,7 @@
"confirm_remove_title": "Remove tag?",
"confirm_remove": "Are you sure you want to remove tag {tag}?",
"automation_title": "Tag {name} is scanned",
"qr_code_image": "QR code for tag {name}",
"headers": {
"icon": "Icon",
"name": "Name",
@@ -2632,7 +2662,7 @@
"enable_state_reporting": "Enable State Reporting",
"info_state_reporting": "If you enable state reporting, Home Assistant will send all state changes of exposed entities to Amazon. This allows you to always see the latest states in the Alexa app and use the state changes to create routines.",
"state_reporting_error": "Unable to {enable_disable} report state.",
"manage_entities": "Manage Entities",
"manage_entities": "[%key:ui::panel::config::cloud::account::google::manage_entities%]",
"enable": "enable",
"disable": "disable",
"not_configured_title": "Alexa is not activated",
@@ -2683,6 +2713,7 @@
"follow_domain": "[%key:ui::panel::config::cloud::google::follow_domain%]",
"exposed": "[%key:ui::panel::config::cloud::google::exposed%]",
"not_exposed": "[%key:ui::panel::config::cloud::google::not_exposed%]",
"manage_aliases": "[%key:ui::panel::config::cloud::google::manage_aliases%]",
"expose": "Expose to Alexa",
"sync_entities": "Synchronize entities",
"sync_entities_error": "Failed to sync entities:"
@@ -2708,6 +2739,7 @@
"follow_domain": "Follow domain",
"exposed": "{selected} exposed",
"not_exposed": "{selected} not exposed",
"manage_aliases": "Manage aliases",
"sync_to_google": "Synchronizing changes to Google.",
"sync_entities": "Synchronize entities",
"sync_entities_error": "Failed to sync entities:",
@@ -4195,6 +4227,7 @@
"description": "The Light card allows you to change the brightness of the light."
},
"generic": {
"alt_text": "Alternative Text",
"aspect_ratio": "Aspect Ratio",
"attribute": "Attribute",
"camera_image": "Camera Entity",
@@ -4598,7 +4631,8 @@
"name": "Name",
"prompt_name": "Give the token a name",
"prompt_copy_token": "Copy your access token. It will not be shown again.",
"empty_state": "You have no long-lived access tokens yet."
"empty_state": "You have no long-lived access tokens yet.",
"qr_code_image": "QR code for token {name}"
}
},
"shopping_list": {