mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
20221027.0 (#14222)
This commit is contained in:
commit
d52e521ef8
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "home-assistant-frontend"
|
name = "home-assistant-frontend"
|
||||||
version = "20221026.0"
|
version = "20221027.0"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "The Home Assistant frontend"
|
description = "The Home Assistant frontend"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@ -11,16 +11,20 @@ export const weekdays = [
|
|||||||
"saturday",
|
"saturday",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const firstWeekdayIndex = (locale: FrontendLocaleData): number => {
|
type WeekdayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
|
|
||||||
|
export const firstWeekdayIndex = (locale: FrontendLocaleData): WeekdayIndex => {
|
||||||
if (locale.first_weekday === FirstWeekday.language) {
|
if (locale.first_weekday === FirstWeekday.language) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if ("weekInfo" in Intl.Locale.prototype) {
|
if ("weekInfo" in Intl.Locale.prototype) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return new Intl.Locale(locale.language).weekInfo.firstDay % 7;
|
return new Intl.Locale(locale.language).weekInfo.firstDay % 7;
|
||||||
}
|
}
|
||||||
return getWeekStartByLocale(locale.language) % 7;
|
return (getWeekStartByLocale(locale.language) % 7) as WeekdayIndex;
|
||||||
}
|
}
|
||||||
return weekdays.indexOf(locale.first_weekday);
|
return weekdays.includes(locale.first_weekday)
|
||||||
|
? (weekdays.indexOf(locale.first_weekday) as WeekdayIndex)
|
||||||
|
: 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const firstWeekday = (locale: FrontendLocaleData) => {
|
export const firstWeekday = (locale: FrontendLocaleData) => {
|
||||||
|
@ -253,7 +253,7 @@ class StatisticsChart extends LitElement {
|
|||||||
const firstStat = stats[0];
|
const firstStat = stats[0];
|
||||||
const meta = statisticsMetaData?.[firstStat.statistic_id];
|
const meta = statisticsMetaData?.[firstStat.statistic_id];
|
||||||
let name = names[firstStat.statistic_id];
|
let name = names[firstStat.statistic_id];
|
||||||
if (!name) {
|
if (name === undefined) {
|
||||||
name = getStatisticLabel(this.hass, firstStat.statistic_id, meta);
|
name = getStatisticLabel(this.hass, firstStat.statistic_id, meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,10 +324,14 @@ class StatisticsChart extends LitElement {
|
|||||||
const band = drawBands && (type === "min" || type === "max");
|
const band = drawBands && (type === "min" || type === "max");
|
||||||
statTypes.push(type);
|
statTypes.push(type);
|
||||||
statDataSets.push({
|
statDataSets.push({
|
||||||
label: `${name} (${this.hass.localize(
|
label: name
|
||||||
`ui.components.statistics_charts.statistic_types.${type}`
|
? `${name} (${this.hass.localize(
|
||||||
)})
|
`ui.components.statistics_charts.statistic_types.${type}`
|
||||||
`,
|
)})
|
||||||
|
`
|
||||||
|
: this.hass.localize(
|
||||||
|
`ui.components.statistics_charts.statistic_types.${type}`
|
||||||
|
),
|
||||||
fill: drawBands
|
fill: drawBands
|
||||||
? type === "min"
|
? type === "min"
|
||||||
? "+1"
|
? "+1"
|
||||||
|
@ -39,11 +39,11 @@ export const computeInitialHaFormData = (
|
|||||||
const selector: Selector = field.selector;
|
const selector: Selector = field.selector;
|
||||||
|
|
||||||
if ("device" in selector) {
|
if ("device" in selector) {
|
||||||
data[field.name] = selector.device.multiple ? [] : "";
|
data[field.name] = selector.device?.multiple ? [] : "";
|
||||||
} else if ("entity" in selector) {
|
} else if ("entity" in selector) {
|
||||||
data[field.name] = selector.entity.multiple ? [] : "";
|
data[field.name] = selector.entity?.multiple ? [] : "";
|
||||||
} else if ("area" in selector) {
|
} else if ("area" in selector) {
|
||||||
data[field.name] = selector.area.multiple ? [] : "";
|
data[field.name] = selector.area?.multiple ? [] : "";
|
||||||
} else if ("boolean" in selector) {
|
} else if ("boolean" in selector) {
|
||||||
data[field.name] = false;
|
data[field.name] = false;
|
||||||
} else if (
|
} else if (
|
||||||
@ -56,9 +56,9 @@ export const computeInitialHaFormData = (
|
|||||||
) {
|
) {
|
||||||
data[field.name] = "";
|
data[field.name] = "";
|
||||||
} else if ("number" in selector) {
|
} else if ("number" in selector) {
|
||||||
data[field.name] = selector.number.min ?? 0;
|
data[field.name] = selector.number?.min ?? 0;
|
||||||
} else if ("select" in selector) {
|
} else if ("select" in selector) {
|
||||||
if (selector.select.options.length) {
|
if (selector.select?.options.length) {
|
||||||
data[field.name] = selector.select.options[0][0];
|
data[field.name] = selector.select.options[0][0];
|
||||||
}
|
}
|
||||||
} else if ("duration" in selector) {
|
} else if ("duration" in selector) {
|
||||||
@ -75,7 +75,7 @@ export const computeInitialHaFormData = (
|
|||||||
} else if ("color_rgb" in selector) {
|
} else if ("color_rgb" in selector) {
|
||||||
data[field.name] = [0, 0, 0];
|
data[field.name] = [0, 0, 0];
|
||||||
} else if ("color_temp" in selector) {
|
} else if ("color_temp" in selector) {
|
||||||
data[field.name] = selector.color_temp.min_mireds ?? 153;
|
data[field.name] = selector.color_temp?.min_mireds ?? 153;
|
||||||
} else if (
|
} else if (
|
||||||
"action" in selector ||
|
"action" in selector ||
|
||||||
"media" in selector ||
|
"media" in selector ||
|
||||||
|
@ -55,8 +55,8 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
|
|||||||
protected updated(changedProperties: PropertyValues): void {
|
protected updated(changedProperties: PropertyValues): void {
|
||||||
if (
|
if (
|
||||||
changedProperties.has("selector") &&
|
changedProperties.has("selector") &&
|
||||||
(this.selector.area.device?.integration ||
|
(this.selector.area?.device?.integration ||
|
||||||
this.selector.area.entity?.integration) &&
|
this.selector.area?.entity?.integration) &&
|
||||||
!this._entitySources
|
!this._entitySources
|
||||||
) {
|
) {
|
||||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||||
@ -67,14 +67,14 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (
|
if (
|
||||||
(this.selector.area.device?.integration ||
|
(this.selector.area?.device?.integration ||
|
||||||
this.selector.area.entity?.integration) &&
|
this.selector.area?.entity?.integration) &&
|
||||||
!this._entitySources
|
!this._entitySources
|
||||||
) {
|
) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.selector.area.multiple) {
|
if (!this.selector.area?.multiple) {
|
||||||
return html`
|
return html`
|
||||||
<ha-area-picker
|
<ha-area-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@ -106,7 +106,7 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _filterEntities = (entity: HassEntity): boolean => {
|
private _filterEntities = (entity: HassEntity): boolean => {
|
||||||
if (!this.selector.area.entity) {
|
if (!this.selector.area?.entity) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +118,7 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||||
if (!this.selector.area.device) {
|
if (!this.selector.area?.device) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,9 +30,9 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
|
|||||||
return html`
|
return html`
|
||||||
<ha-entity-attribute-picker
|
<ha-entity-attribute-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.entityId=${this.selector.attribute.entity_id ||
|
.entityId=${this.selector.attribute?.entity_id ||
|
||||||
this.context?.filter_entity}
|
this.context?.filter_entity}
|
||||||
.hideAttributes=${this.selector.attribute.hide_attributes}
|
.hideAttributes=${this.selector.attribute?.hide_attributes}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
@ -49,7 +49,7 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
|
|||||||
// No need to filter value if no value
|
// No need to filter value if no value
|
||||||
!this.value ||
|
!this.value ||
|
||||||
// Only adjust value if we used the context
|
// Only adjust value if we used the context
|
||||||
this.selector.attribute.entity_id ||
|
this.selector.attribute?.entity_id ||
|
||||||
// Only check if context has changed
|
// Only check if context has changed
|
||||||
!changedProps.has("context")
|
!changedProps.has("context")
|
||||||
) {
|
) {
|
||||||
|
@ -28,7 +28,7 @@ export class HaConfigEntrySelector extends LitElement {
|
|||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.integration=${this.selector.config_entry.integration}
|
.integration=${this.selector.config_entry?.integration}
|
||||||
allow-custom-entity
|
allow-custom-entity
|
||||||
></ha-config-entry-picker>`;
|
></ha-config-entry-picker>`;
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
|
|||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
if (
|
if (
|
||||||
changedProperties.has("selector") &&
|
changedProperties.has("selector") &&
|
||||||
this.selector.device.integration &&
|
this.selector.device?.integration &&
|
||||||
!this._entitySources
|
!this._entitySources
|
||||||
) {
|
) {
|
||||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||||
@ -63,11 +63,11 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (this.selector.device.integration && !this._entitySources) {
|
if (this.selector.device?.integration && !this._entitySources) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.selector.device.multiple) {
|
if (!this.selector.device?.multiple) {
|
||||||
return html`
|
return html`
|
||||||
<ha-device-picker
|
<ha-device-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@ -75,10 +75,10 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
|
|||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.deviceFilter=${this._filterDevices}
|
.deviceFilter=${this._filterDevices}
|
||||||
.includeDeviceClasses=${this.selector.device.entity?.device_class
|
.includeDeviceClasses=${this.selector.device?.entity?.device_class
|
||||||
? [this.selector.device.entity.device_class]
|
? [this.selector.device.entity.device_class]
|
||||||
: undefined}
|
: undefined}
|
||||||
.includeDomains=${this.selector.device.entity?.domain
|
.includeDomains=${this.selector.device?.entity?.domain
|
||||||
? [this.selector.device.entity.domain]
|
? [this.selector.device.entity.domain]
|
||||||
: undefined}
|
: undefined}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
@ -113,6 +113,9 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
|
|||||||
? this._deviceIntegrationLookup(this._entitySources, this._entities)
|
? this._deviceIntegrationLookup(this._entitySources, this._entities)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
if (!this.selector.device) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return filterSelectorDevices(
|
return filterSelectorDevices(
|
||||||
this.selector.device,
|
this.selector.device,
|
||||||
device,
|
device,
|
||||||
|
@ -28,7 +28,7 @@ export class HaTimeDuration extends LitElement {
|
|||||||
.data=${this.value}
|
.data=${this.value}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
?enableDay=${this.selector.duration.enable_day}
|
?enableDay=${this.selector.duration?.enable_day}
|
||||||
></ha-duration-input>
|
></ha-duration-input>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -30,14 +30,14 @@ export class HaEntitySelector extends LitElement {
|
|||||||
@property({ type: Boolean }) public required = true;
|
@property({ type: Boolean }) public required = true;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.selector.entity.multiple) {
|
if (!this.selector.entity?.multiple) {
|
||||||
return html`<ha-entity-picker
|
return html`<ha-entity-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.includeEntities=${this.selector.entity.include_entities}
|
.includeEntities=${this.selector.entity?.include_entities}
|
||||||
.excludeEntities=${this.selector.entity.exclude_entities}
|
.excludeEntities=${this.selector.entity?.exclude_entities}
|
||||||
.entityFilter=${this._filterEntities}
|
.entityFilter=${this._filterEntities}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
@ -64,7 +64,7 @@ export class HaEntitySelector extends LitElement {
|
|||||||
super.updated(changedProps);
|
super.updated(changedProps);
|
||||||
if (
|
if (
|
||||||
changedProps.has("selector") &&
|
changedProps.has("selector") &&
|
||||||
this.selector.entity.integration &&
|
this.selector.entity?.integration &&
|
||||||
!this._entitySources
|
!this._entitySources
|
||||||
) {
|
) {
|
||||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||||
@ -73,8 +73,16 @@ export class HaEntitySelector extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _filterEntities = (entity: HassEntity): boolean =>
|
private _filterEntities = (entity: HassEntity): boolean => {
|
||||||
filterSelectorEntities(this.selector.entity, entity, this._entitySources);
|
if (!this.selector?.entity) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return filterSelectorEntities(
|
||||||
|
this.selector.entity,
|
||||||
|
entity,
|
||||||
|
this._entitySources
|
||||||
|
);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -32,7 +32,7 @@ export class HaFileSelector extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<ha-file-upload
|
<ha-file-upload
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.accept=${this.selector.file.accept}
|
.accept=${this.selector.file?.accept}
|
||||||
.icon=${mdiFile}
|
.icon=${mdiFile}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
|
@ -30,8 +30,8 @@ export class HaIconSelector extends LitElement {
|
|||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.fallbackPath=${this.selector.icon.fallbackPath}
|
.fallbackPath=${this.selector.icon?.fallbackPath}
|
||||||
.placeholder=${this.selector.icon.placeholder}
|
.placeholder=${this.selector.icon?.placeholder}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
></ha-icon-picker>
|
></ha-icon-picker>
|
||||||
`;
|
`;
|
||||||
|
@ -43,7 +43,7 @@ export class HaLocationSelector extends LitElement {
|
|||||||
value?: LocationSelectorValue
|
value?: LocationSelectorValue
|
||||||
): MarkerLocation[] => {
|
): MarkerLocation[] => {
|
||||||
const computedStyles = getComputedStyle(this);
|
const computedStyles = getComputedStyle(this);
|
||||||
const zoneRadiusColor = selector.location.radius
|
const zoneRadiusColor = selector.location?.radius
|
||||||
? computedStyles.getPropertyValue("--zone-radius-color") ||
|
? computedStyles.getPropertyValue("--zone-radius-color") ||
|
||||||
computedStyles.getPropertyValue("--accent-color")
|
computedStyles.getPropertyValue("--accent-color")
|
||||||
: undefined;
|
: undefined;
|
||||||
@ -52,10 +52,10 @@ export class HaLocationSelector extends LitElement {
|
|||||||
id: "location",
|
id: "location",
|
||||||
latitude: value?.latitude || this.hass.config.latitude,
|
latitude: value?.latitude || this.hass.config.latitude,
|
||||||
longitude: value?.longitude || this.hass.config.longitude,
|
longitude: value?.longitude || this.hass.config.longitude,
|
||||||
radius: selector.location.radius ? value?.radius || 1000 : undefined,
|
radius: selector.location?.radius ? value?.radius || 1000 : undefined,
|
||||||
radius_color: zoneRadiusColor,
|
radius_color: zoneRadiusColor,
|
||||||
icon:
|
icon:
|
||||||
selector.location.icon || selector.location.radius
|
selector.location?.icon || selector.location?.radius
|
||||||
? "mdi:map-marker-radius"
|
? "mdi:map-marker-radius"
|
||||||
: "mdi:map-marker",
|
: "mdi:map-marker",
|
||||||
location_editable: true,
|
location_editable: true,
|
||||||
|
@ -27,7 +27,7 @@ export class HaNumberSelector extends LitElement {
|
|||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const isBox = this.selector.number.mode === "box";
|
const isBox = this.selector.number?.mode === "box";
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="input">
|
<div class="input">
|
||||||
@ -37,10 +37,10 @@ export class HaNumberSelector extends LitElement {
|
|||||||
? html`${this.label}${this.required ? " *" : ""}`
|
? html`${this.label}${this.required ? " *" : ""}`
|
||||||
: ""}
|
: ""}
|
||||||
<ha-slider
|
<ha-slider
|
||||||
.min=${this.selector.number.min}
|
.min=${this.selector.number?.min}
|
||||||
.max=${this.selector.number.max}
|
.max=${this.selector.number?.max}
|
||||||
.value=${this._value}
|
.value=${this._value}
|
||||||
.step=${this.selector.number.step ?? 1}
|
.step=${this.selector.number?.step ?? 1}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
pin
|
pin
|
||||||
@ -51,24 +51,26 @@ export class HaNumberSelector extends LitElement {
|
|||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
<ha-textfield
|
<ha-textfield
|
||||||
.inputMode=${(this.selector.number.step || 1) % 1 !== 0
|
.inputMode=${(this.selector.number?.step || 1) % 1 !== 0
|
||||||
? "decimal"
|
? "decimal"
|
||||||
: "numeric"}
|
: "numeric"}
|
||||||
.label=${this.selector.number.mode !== "box" ? undefined : this.label}
|
.label=${this.selector.number?.mode !== "box"
|
||||||
|
? undefined
|
||||||
|
: this.label}
|
||||||
.placeholder=${this.placeholder}
|
.placeholder=${this.placeholder}
|
||||||
class=${classMap({ single: this.selector.number.mode === "box" })}
|
class=${classMap({ single: this.selector.number?.mode === "box" })}
|
||||||
.min=${this.selector.number.min}
|
.min=${this.selector.number?.min}
|
||||||
.max=${this.selector.number.max}
|
.max=${this.selector.number?.max}
|
||||||
.value=${this.value ?? ""}
|
.value=${this.value ?? ""}
|
||||||
.step=${this.selector.number.step ?? 1}
|
.step=${this.selector.number?.step ?? 1}
|
||||||
helperPersistent
|
helperPersistent
|
||||||
.helper=${isBox ? this.helper : undefined}
|
.helper=${isBox ? this.helper : undefined}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.suffix=${this.selector.number.unit_of_measurement}
|
.suffix=${this.selector.number?.unit_of_measurement}
|
||||||
type="number"
|
type="number"
|
||||||
autoValidate
|
autoValidate
|
||||||
?no-spinner=${this.selector.number.mode !== "box"}
|
?no-spinner=${this.selector.number?.mode !== "box"}
|
||||||
@input=${this._handleInputChange}
|
@input=${this._handleInputChange}
|
||||||
>
|
>
|
||||||
</ha-textfield>
|
</ha-textfield>
|
||||||
@ -80,7 +82,7 @@ export class HaNumberSelector extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private get _value() {
|
private get _value() {
|
||||||
return this.value ?? (this.selector.number.min || 0);
|
return this.value ?? (this.selector.number?.min || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleInputChange(ev) {
|
private _handleInputChange(ev) {
|
||||||
@ -88,7 +90,7 @@ export class HaNumberSelector extends LitElement {
|
|||||||
const value =
|
const value =
|
||||||
ev.target.value === "" || isNaN(ev.target.value)
|
ev.target.value === "" || isNaN(ev.target.value)
|
||||||
? this.required
|
? this.required
|
||||||
? this.selector.number.min || 0
|
? this.selector.number?.min || 0
|
||||||
: undefined
|
: undefined
|
||||||
: Number(ev.target.value);
|
: Number(ev.target.value);
|
||||||
if (this.value === value) {
|
if (this.value === value) {
|
||||||
|
@ -9,6 +9,7 @@ import type { HomeAssistant } from "../../types";
|
|||||||
import "../ha-checkbox";
|
import "../ha-checkbox";
|
||||||
import "../ha-chip";
|
import "../ha-chip";
|
||||||
import "../ha-chip-set";
|
import "../ha-chip-set";
|
||||||
|
import "../ha-combo-box";
|
||||||
import type { HaComboBox } from "../ha-combo-box";
|
import type { HaComboBox } from "../ha-combo-box";
|
||||||
import "../ha-formfield";
|
import "../ha-formfield";
|
||||||
import "../ha-radio";
|
import "../ha-radio";
|
||||||
@ -36,12 +37,13 @@ export class HaSelectSelector extends LitElement {
|
|||||||
private _filter = "";
|
private _filter = "";
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const options = this.selector.select.options.map((option) =>
|
const options =
|
||||||
typeof option === "object" ? option : { value: option, label: option }
|
this.selector.select?.options.map((option) =>
|
||||||
);
|
typeof option === "object" ? option : { value: option, label: option }
|
||||||
|
) || [];
|
||||||
|
|
||||||
if (!this.selector.select.custom_value && this._mode === "list") {
|
if (!this.selector.select?.custom_value && this._mode === "list") {
|
||||||
if (!this.selector.select.multiple) {
|
if (!this.selector.select?.multiple) {
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
${this.label}
|
${this.label}
|
||||||
@ -82,7 +84,7 @@ export class HaSelectSelector extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.selector.select.multiple) {
|
if (this.selector.select?.multiple) {
|
||||||
const value =
|
const value =
|
||||||
!this.value || this.value === "" ? [] : (this.value as string[]);
|
!this.value || this.value === "" ? [] : (this.value as string[]);
|
||||||
|
|
||||||
@ -123,7 +125,7 @@ export class HaSelectSelector extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.selector.select.custom_value) {
|
if (this.selector.select?.custom_value) {
|
||||||
if (
|
if (
|
||||||
this.value !== undefined &&
|
this.value !== undefined &&
|
||||||
!options.find((option) => option.value === this.value)
|
!options.find((option) => option.value === this.value)
|
||||||
@ -178,8 +180,8 @@ export class HaSelectSelector extends LitElement {
|
|||||||
|
|
||||||
private get _mode(): "list" | "dropdown" {
|
private get _mode(): "list" | "dropdown" {
|
||||||
return (
|
return (
|
||||||
this.selector.select.mode ||
|
this.selector.select?.mode ||
|
||||||
(this.selector.select.options.length < 6 ? "list" : "dropdown")
|
((this.selector.select?.options?.length || 0) < 6 ? "list" : "dropdown")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,7 +245,7 @@ export class HaSelectSelector extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.selector.select.multiple) {
|
if (!this.selector.select?.multiple) {
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
value: newValue,
|
value: newValue,
|
||||||
});
|
});
|
||||||
@ -271,14 +273,14 @@ export class HaSelectSelector extends LitElement {
|
|||||||
this._filter = ev?.detail.value || "";
|
this._filter = ev?.detail.value || "";
|
||||||
|
|
||||||
const filteredItems = this.comboBox.items?.filter((item) => {
|
const filteredItems = this.comboBox.items?.filter((item) => {
|
||||||
if (this.selector.select.multiple && this.value?.includes(item.value)) {
|
if (this.selector.select?.multiple && this.value?.includes(item.value)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const label = item.label || item.value;
|
const label = item.label || item.value;
|
||||||
return label.toLowerCase().includes(this._filter?.toLowerCase());
|
return label.toLowerCase().includes(this._filter?.toLowerCase());
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this._filter && this.selector.select.custom_value) {
|
if (this._filter && this.selector.select?.custom_value) {
|
||||||
filteredItems?.unshift({ label: this._filter, value: this._filter });
|
filteredItems?.unshift({ label: this._filter, value: this._filter });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,9 +30,9 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
|||||||
return html`
|
return html`
|
||||||
<ha-entity-state-picker
|
<ha-entity-state-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.entityId=${this.selector.state.entity_id ||
|
.entityId=${this.selector.state?.entity_id ||
|
||||||
this.context?.filter_entity}
|
this.context?.filter_entity}
|
||||||
.attribute=${this.selector.state.attribute ||
|
.attribute=${this.selector.state?.attribute ||
|
||||||
this.context?.filter_attribute}
|
this.context?.filter_attribute}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
|
53
src/components/ha-selector/ha-selector-statistic.ts
Normal file
53
src/components/ha-selector/ha-selector-statistic.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { html, LitElement } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import type { StatisticSelector } from "../../data/selector";
|
||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
import "../entity/ha-statistics-picker";
|
||||||
|
|
||||||
|
@customElement("ha-selector-statistic")
|
||||||
|
export class HaStatisticSelector extends LitElement {
|
||||||
|
@property() public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public selector!: StatisticSelector;
|
||||||
|
|
||||||
|
@property() public value?: any;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property() public helper?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public required = true;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this.selector.statistic.multiple) {
|
||||||
|
return html`<ha-statistic-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${this.value}
|
||||||
|
.label=${this.label}
|
||||||
|
.helper=${this.helper}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.required=${this.required}
|
||||||
|
allow-custom-entity
|
||||||
|
></ha-statistic-picker>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
${this.label ? html`<label>${this.label}</label>` : ""}
|
||||||
|
<ha-statistics-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${this.value}
|
||||||
|
.helper=${this.helper}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.required=${this.required}
|
||||||
|
></ha-statistics-picker>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-selector-statistic": HaStatisticSelector;
|
||||||
|
}
|
||||||
|
}
|
@ -64,8 +64,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
|||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
if (
|
if (
|
||||||
changedProperties.has("selector") &&
|
changedProperties.has("selector") &&
|
||||||
(this.selector.target.device?.integration ||
|
(this.selector.target?.device?.integration ||
|
||||||
this.selector.target.entity?.integration) &&
|
this.selector.target?.entity?.integration) &&
|
||||||
!this._entitySources
|
!this._entitySources
|
||||||
) {
|
) {
|
||||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||||
@ -76,8 +76,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (
|
if (
|
||||||
(this.selector.target.device?.integration ||
|
(this.selector.target?.device?.integration ||
|
||||||
this.selector.target.entity?.integration) &&
|
this.selector.target?.entity?.integration) &&
|
||||||
!this._entitySources
|
!this._entitySources
|
||||||
) {
|
) {
|
||||||
return html``;
|
return html``;
|
||||||
@ -94,7 +94,7 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _filterEntities = (entity: HassEntity): boolean => {
|
private _filterEntities = (entity: HassEntity): boolean => {
|
||||||
if (!this.selector.target.entity) {
|
if (!this.selector.target?.entity) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +106,7 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||||
if (!this.selector.target.device) {
|
if (!this.selector.target?.device) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ export class HaTextSelector extends LitElement {
|
|||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
@input=${this._handleChange}
|
@input=${this._handleChange}
|
||||||
autocapitalize="none"
|
autocapitalize="none"
|
||||||
.autocomplete=${this.selector.text.autofill}
|
.autocomplete=${this.selector.text?.autocomplete}
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
autogrow
|
autogrow
|
||||||
@ -59,7 +59,7 @@ export class HaTextSelector extends LitElement {
|
|||||||
html`<div style="width: 24px"></div>`
|
html`<div style="width: 24px"></div>`
|
||||||
: this.selector.text?.suffix}
|
: this.selector.text?.suffix}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.autocomplete=${this.selector.text.autofill}
|
.autocomplete=${this.selector.text?.autocomplete}
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
${this.selector.text?.type === "password"
|
${this.selector.text?.type === "password"
|
||||||
? html`<ha-icon-button
|
? html`<ha-icon-button
|
||||||
|
@ -24,7 +24,7 @@ export class HaSelectorUiAction extends LitElement {
|
|||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.config=${this.value}
|
.config=${this.value}
|
||||||
.actions=${this.selector["ui-action"].actions}
|
.actions=${this.selector["ui-action"]?.actions}
|
||||||
.tooltipText=${this.helper}
|
.tooltipText=${this.helper}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
></hui-action-editor>
|
></hui-action-editor>
|
||||||
|
@ -17,6 +17,7 @@ const LOAD_ELEMENTS = {
|
|||||||
device: () => import("./ha-selector-device"),
|
device: () => import("./ha-selector-device"),
|
||||||
duration: () => import("./ha-selector-duration"),
|
duration: () => import("./ha-selector-duration"),
|
||||||
entity: () => import("./ha-selector-entity"),
|
entity: () => import("./ha-selector-entity"),
|
||||||
|
statistic: () => import("./ha-selector-statistic"),
|
||||||
file: () => import("./ha-selector-file"),
|
file: () => import("./ha-selector-file"),
|
||||||
navigation: () => import("./ha-selector-navigation"),
|
navigation: () => import("./ha-selector-navigation"),
|
||||||
number: () => import("./ha-selector-number"),
|
number: () => import("./ha-selector-number"),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { computeStateName } from "../common/entity/compute_state_name";
|
import { computeStateName } from "../common/entity/compute_state_name";
|
||||||
|
import { HaDurationData } from "../components/ha-duration-input";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
export type StatisticType = "state" | "sum" | "min" | "max" | "mean";
|
export type StatisticType = "state" | "sum" | "min" | "max" | "mean";
|
||||||
@ -19,6 +20,13 @@ export interface StatisticValue {
|
|||||||
state: number | null;
|
state: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Statistic {
|
||||||
|
max: number | null;
|
||||||
|
mean: number | null;
|
||||||
|
min: number | null;
|
||||||
|
change: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface StatisticsMetaData {
|
export interface StatisticsMetaData {
|
||||||
statistics_unit_of_measurement: string | null;
|
statistics_unit_of_measurement: string | null;
|
||||||
statistic_id: string;
|
statistic_id: string;
|
||||||
@ -122,6 +130,36 @@ export const fetchStatistics = (
|
|||||||
units,
|
units,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const fetchStatistic = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
statistic_id: string,
|
||||||
|
period: {
|
||||||
|
fixed_period?: { start: string | Date; end: string | Date };
|
||||||
|
calendar?: { period: string; offset: number };
|
||||||
|
rolling_window?: { duration: HaDurationData; offset: HaDurationData };
|
||||||
|
},
|
||||||
|
units?: StatisticsUnitConfiguration
|
||||||
|
) =>
|
||||||
|
hass.callWS<Statistic>({
|
||||||
|
type: "recorder/statistic_during_period",
|
||||||
|
statistic_id,
|
||||||
|
units,
|
||||||
|
fixed_period: period.fixed_period
|
||||||
|
? {
|
||||||
|
start_time:
|
||||||
|
period.fixed_period.start instanceof Date
|
||||||
|
? period.fixed_period.start.toISOString()
|
||||||
|
: period.fixed_period.start,
|
||||||
|
end_time:
|
||||||
|
period.fixed_period.end instanceof Date
|
||||||
|
? period.fixed_period.end.toISOString()
|
||||||
|
: period.fixed_period.end,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
calendar: period.calendar,
|
||||||
|
rolling_window: period.rolling_window,
|
||||||
|
});
|
||||||
|
|
||||||
export const validateStatistics = (hass: HomeAssistant) =>
|
export const validateStatistics = (hass: HomeAssistant) =>
|
||||||
hass.callWS<StatisticsValidationResults>({
|
hass.callWS<StatisticsValidationResults>({
|
||||||
type: "recorder/validate_statistics",
|
type: "recorder/validate_statistics",
|
||||||
|
@ -27,6 +27,7 @@ export type Selector =
|
|||||||
| ObjectSelector
|
| ObjectSelector
|
||||||
| SelectSelector
|
| SelectSelector
|
||||||
| StateSelector
|
| StateSelector
|
||||||
|
| StatisticSelector
|
||||||
| StringSelector
|
| StringSelector
|
||||||
| TargetSelector
|
| TargetSelector
|
||||||
| TemplateSelector
|
| TemplateSelector
|
||||||
@ -36,26 +37,26 @@ export type Selector =
|
|||||||
|
|
||||||
export interface ActionSelector {
|
export interface ActionSelector {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
action: {};
|
action: {} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddonSelector {
|
export interface AddonSelector {
|
||||||
addon: {
|
addon: {
|
||||||
name?: string;
|
name?: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
};
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectorDevice {
|
export interface SelectorDevice {
|
||||||
integration?: DeviceSelector["device"]["integration"];
|
integration?: NonNullable<DeviceSelector["device"]>["integration"];
|
||||||
manufacturer?: DeviceSelector["device"]["manufacturer"];
|
manufacturer?: NonNullable<DeviceSelector["device"]>["manufacturer"];
|
||||||
model?: DeviceSelector["device"]["model"];
|
model?: NonNullable<DeviceSelector["device"]>["model"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectorEntity {
|
export interface SelectorEntity {
|
||||||
integration?: EntitySelector["entity"]["integration"];
|
integration?: NonNullable<EntitySelector["entity"]>["integration"];
|
||||||
domain?: EntitySelector["entity"]["domain"];
|
domain?: NonNullable<EntitySelector["entity"]>["domain"];
|
||||||
device_class?: EntitySelector["entity"]["device_class"];
|
device_class?: NonNullable<EntitySelector["entity"]>["device_class"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AreaSelector {
|
export interface AreaSelector {
|
||||||
@ -63,47 +64,47 @@ export interface AreaSelector {
|
|||||||
entity?: SelectorEntity;
|
entity?: SelectorEntity;
|
||||||
device?: SelectorDevice;
|
device?: SelectorDevice;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
};
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AttributeSelector {
|
export interface AttributeSelector {
|
||||||
attribute: {
|
attribute: {
|
||||||
entity_id?: string;
|
entity_id?: string;
|
||||||
hide_attributes?: readonly string[];
|
hide_attributes?: readonly string[];
|
||||||
};
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BooleanSelector {
|
export interface BooleanSelector {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
boolean: {};
|
boolean: {} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ColorRGBSelector {
|
export interface ColorRGBSelector {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
color_rgb: {};
|
color_rgb: {} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ColorTempSelector {
|
export interface ColorTempSelector {
|
||||||
color_temp: {
|
color_temp: {
|
||||||
min_mireds?: number;
|
min_mireds?: number;
|
||||||
max_mireds?: number;
|
max_mireds?: number;
|
||||||
};
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigEntrySelector {
|
export interface ConfigEntrySelector {
|
||||||
config_entry: {
|
config_entry: {
|
||||||
integration?: string;
|
integration?: string;
|
||||||
};
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DateSelector {
|
export interface DateSelector {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
date: {};
|
date: {} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DateTimeSelector {
|
export interface DateTimeSelector {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
datetime: {};
|
datetime: {} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceSelector {
|
export interface DeviceSelector {
|
||||||
@ -113,13 +114,13 @@ export interface DeviceSelector {
|
|||||||
model?: string;
|
model?: string;
|
||||||
entity?: SelectorEntity;
|
entity?: SelectorEntity;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
};
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DurationSelector {
|
export interface DurationSelector {
|
||||||
duration: {
|
duration: {
|
||||||
enable_day?: boolean;
|
enable_day?: boolean;
|
||||||
};
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EntitySelector {
|
export interface EntitySelector {
|
||||||
@ -130,24 +131,31 @@ export interface EntitySelector {
|
|||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
include_entities?: string[];
|
include_entities?: string[];
|
||||||
exclude_entities?: string[];
|
exclude_entities?: string[];
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StatisticSelector {
|
||||||
|
statistic: {
|
||||||
|
device_class?: string;
|
||||||
|
multiple?: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileSelector {
|
export interface FileSelector {
|
||||||
file: {
|
file: {
|
||||||
accept: string;
|
accept: string;
|
||||||
};
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IconSelector {
|
export interface IconSelector {
|
||||||
icon: {
|
icon: {
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
fallbackPath?: string;
|
fallbackPath?: string;
|
||||||
};
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LocationSelector {
|
export interface LocationSelector {
|
||||||
location: { radius?: boolean; icon?: string };
|
location: { radius?: boolean; icon?: string } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LocationSelectorValue {
|
export interface LocationSelectorValue {
|
||||||
@ -158,7 +166,7 @@ export interface LocationSelectorValue {
|
|||||||
|
|
||||||
export interface MediaSelector {
|
export interface MediaSelector {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
media: {};
|
media: {} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MediaSelectorValue {
|
export interface MediaSelectorValue {
|
||||||
@ -176,7 +184,7 @@ export interface MediaSelectorValue {
|
|||||||
|
|
||||||
export interface NavigationSelector {
|
export interface NavigationSelector {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
navigation: {};
|
navigation: {} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NumberSelector {
|
export interface NumberSelector {
|
||||||
@ -186,16 +194,16 @@ export interface NumberSelector {
|
|||||||
step?: number;
|
step?: number;
|
||||||
mode?: "box" | "slider";
|
mode?: "box" | "slider";
|
||||||
unit_of_measurement?: string;
|
unit_of_measurement?: string;
|
||||||
};
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ObjectSelector {
|
export interface ObjectSelector {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
object: {};
|
object: {} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectOption {
|
export interface SelectOption {
|
||||||
value: string;
|
value: any;
|
||||||
label: string;
|
label: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
@ -206,14 +214,14 @@ export interface SelectSelector {
|
|||||||
custom_value?: boolean;
|
custom_value?: boolean;
|
||||||
mode?: "list" | "dropdown";
|
mode?: "list" | "dropdown";
|
||||||
options: readonly string[] | readonly SelectOption[];
|
options: readonly string[] | readonly SelectOption[];
|
||||||
};
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StateSelector {
|
export interface StateSelector {
|
||||||
state: {
|
state: {
|
||||||
entity_id?: string;
|
entity_id?: string;
|
||||||
attribute?: string;
|
attribute?: string;
|
||||||
};
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StringSelector {
|
export interface StringSelector {
|
||||||
@ -234,35 +242,35 @@ export interface StringSelector {
|
|||||||
| "datetime-local"
|
| "datetime-local"
|
||||||
| "color";
|
| "color";
|
||||||
suffix?: string;
|
suffix?: string;
|
||||||
autofill?: string;
|
autocomplete?: string;
|
||||||
};
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TargetSelector {
|
export interface TargetSelector {
|
||||||
target: {
|
target: {
|
||||||
entity?: SelectorEntity;
|
entity?: SelectorEntity;
|
||||||
device?: SelectorDevice;
|
device?: SelectorDevice;
|
||||||
};
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TemplateSelector {
|
export interface TemplateSelector {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
template: {};
|
template: {} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ThemeSelector {
|
export interface ThemeSelector {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
theme: {};
|
theme: {} | null;
|
||||||
}
|
}
|
||||||
export interface TimeSelector {
|
export interface TimeSelector {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
time: {};
|
time: {} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UiActionSelector {
|
export interface UiActionSelector {
|
||||||
"ui-action": {
|
"ui-action": {
|
||||||
actions?: UiAction[];
|
actions?: UiAction[];
|
||||||
};
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const filterSelectorDevices = (
|
export const filterSelectorDevices = (
|
||||||
|
@ -36,6 +36,8 @@ export class MoreInfoHistory extends LitElement {
|
|||||||
|
|
||||||
private _showMoreHref = "";
|
private _showMoreHref = "";
|
||||||
|
|
||||||
|
private _statNames?: Record<string, string>;
|
||||||
|
|
||||||
private _throttleGetStateHistory = throttle(() => {
|
private _throttleGetStateHistory = throttle(() => {
|
||||||
this._getStateHistory();
|
this._getStateHistory();
|
||||||
}, 10000);
|
}, 10000);
|
||||||
@ -62,6 +64,7 @@ export class MoreInfoHistory extends LitElement {
|
|||||||
.isLoadingData=${!this._statistics}
|
.isLoadingData=${!this._statistics}
|
||||||
.statisticsData=${this._statistics}
|
.statisticsData=${this._statistics}
|
||||||
.statTypes=${statTypes}
|
.statTypes=${statTypes}
|
||||||
|
.names=${this._statNames}
|
||||||
></statistics-chart>`
|
></statistics-chart>`
|
||||||
: html`<state-history-charts
|
: html`<state-history-charts
|
||||||
up-to-now
|
up-to-now
|
||||||
@ -113,13 +116,14 @@ export class MoreInfoHistory extends LitElement {
|
|||||||
computeDomain(this.entityId) === "sensor"
|
computeDomain(this.entityId) === "sensor"
|
||||||
) {
|
) {
|
||||||
const metadata = await getStatisticMetadata(this.hass, [this.entityId]);
|
const metadata = await getStatisticMetadata(this.hass, [this.entityId]);
|
||||||
|
this._statNames = { [this.entityId]: "" };
|
||||||
if (metadata.length) {
|
if (metadata.length) {
|
||||||
this._statistics = await fetchStatistics(
|
this._statistics = await fetchStatistics(
|
||||||
this.hass!,
|
this.hass!,
|
||||||
subHours(new Date(), 24),
|
subHours(new Date(), 24),
|
||||||
undefined,
|
undefined,
|
||||||
[this.entityId],
|
[this.entityId],
|
||||||
"hour"
|
"5minute"
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -18,21 +18,25 @@ import { onboardUserStep } from "../data/onboarding";
|
|||||||
import { PolymerChangedEvent } from "../polymer-types";
|
import { PolymerChangedEvent } from "../polymer-types";
|
||||||
|
|
||||||
const CREATE_USER_SCHEMA: HaFormSchema[] = [
|
const CREATE_USER_SCHEMA: HaFormSchema[] = [
|
||||||
{ name: "name", required: true, selector: { text: { autofill: "name" } } },
|
{
|
||||||
|
name: "name",
|
||||||
|
required: true,
|
||||||
|
selector: { text: { autocomplete: "name" } },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "username",
|
name: "username",
|
||||||
required: true,
|
required: true,
|
||||||
selector: { text: { autofill: "username" } },
|
selector: { text: { autocomplete: "username" } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "password",
|
name: "password",
|
||||||
required: true,
|
required: true,
|
||||||
selector: { text: { type: "password", autofill: "new-password" } },
|
selector: { text: { type: "password", autocomplete: "new-password" } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "password_confirm",
|
name: "password_confirm",
|
||||||
required: true,
|
required: true,
|
||||||
selector: { text: { type: "password", autofill: "new-password" } },
|
selector: { text: { type: "password", autocomplete: "new-password" } },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
} from "home-assistant-js-websocket/dist/types";
|
} from "home-assistant-js-websocket/dist/types";
|
||||||
import { css, html, LitElement, PropertyValues } from "lit";
|
import { css, html, LitElement, PropertyValues } from "lit";
|
||||||
import { property, state } from "lit/decorators";
|
import { property, state } from "lit/decorators";
|
||||||
|
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
|
||||||
import { LocalStorage } from "../../common/decorators/local-storage";
|
import { LocalStorage } from "../../common/decorators/local-storage";
|
||||||
import { ensureArray } from "../../common/ensure-array";
|
import { ensureArray } from "../../common/ensure-array";
|
||||||
import { navigate } from "../../common/navigate";
|
import { navigate } from "../../common/navigate";
|
||||||
@ -179,8 +180,9 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const weekStart = startOfWeek(today);
|
const weekStartsOn = firstWeekdayIndex(this.hass.locale);
|
||||||
const weekEnd = endOfWeek(today);
|
const weekStart = startOfWeek(today, { weekStartsOn });
|
||||||
|
const weekEnd = endOfWeek(today, { weekStartsOn });
|
||||||
|
|
||||||
this._ranges = {
|
this._ranges = {
|
||||||
[this.hass.localize("ui.components.date-range-picker.ranges.today")]: [
|
[this.hass.localize("ui.components.date-range-picker.ranges.today")]: [
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
} from "date-fns/esm";
|
} from "date-fns/esm";
|
||||||
import { css, html, LitElement, PropertyValues } from "lit";
|
import { css, html, LitElement, PropertyValues } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
|
||||||
import { navigate } from "../../common/navigate";
|
import { navigate } from "../../common/navigate";
|
||||||
import {
|
import {
|
||||||
createSearchParam,
|
createSearchParam,
|
||||||
@ -108,8 +109,9 @@ export class HaPanelLogbook extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const weekStart = startOfWeek(today);
|
const weekStartsOn = firstWeekdayIndex(this.hass.locale);
|
||||||
const weekEnd = endOfWeek(today);
|
const weekStart = startOfWeek(today, { weekStartsOn });
|
||||||
|
const weekEnd = endOfWeek(today, { weekStartsOn });
|
||||||
|
|
||||||
this._ranges = {
|
this._ranges = {
|
||||||
[this.hass.localize("ui.components.date-range-picker.ranges.today")]: [
|
[this.hass.localize("ui.components.date-range-picker.ranges.today")]: [
|
||||||
|
316
src/panels/lovelace/cards/hui-statistic-card.ts
Normal file
316
src/panels/lovelace/cards/hui-statistic-card.ts
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResultGroup,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||||
|
import { formatNumber } from "../../../common/number/format_number";
|
||||||
|
import "../../../components/ha-alert";
|
||||||
|
import "../../../components/ha-card";
|
||||||
|
import "../../../components/ha-state-icon";
|
||||||
|
import {
|
||||||
|
fetchStatistic,
|
||||||
|
getDisplayUnit,
|
||||||
|
getStatisticLabel,
|
||||||
|
getStatisticMetadata,
|
||||||
|
isExternalStatistic,
|
||||||
|
StatisticsMetaData,
|
||||||
|
} from "../../../data/recorder";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { computeCardSize } from "../common/compute-card-size";
|
||||||
|
import { findEntities } from "../common/find-entities";
|
||||||
|
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||||
|
import { createHeaderFooterElement } from "../create-element/create-header-footer-element";
|
||||||
|
import {
|
||||||
|
LovelaceCard,
|
||||||
|
LovelaceCardEditor,
|
||||||
|
LovelaceHeaderFooter,
|
||||||
|
} from "../types";
|
||||||
|
import { HuiErrorCard } from "./hui-error-card";
|
||||||
|
import { EntityCardConfig, StatisticCardConfig } from "./types";
|
||||||
|
|
||||||
|
@customElement("hui-statistic-card")
|
||||||
|
export class HuiStatisticCard extends LitElement implements LovelaceCard {
|
||||||
|
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||||
|
await import("../editor/config-elements/hui-statistic-card-editor");
|
||||||
|
return document.createElement("hui-statistic-card-editor");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getStubConfig(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entities: string[],
|
||||||
|
entitiesFill: string[]
|
||||||
|
) {
|
||||||
|
const includeDomains = ["sensor"];
|
||||||
|
const maxEntities = 1;
|
||||||
|
const foundEntities = findEntities(
|
||||||
|
hass,
|
||||||
|
maxEntities,
|
||||||
|
entities,
|
||||||
|
entitiesFill,
|
||||||
|
includeDomains,
|
||||||
|
(stateObj: HassEntity) => "state_class" in stateObj.attributes
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
entity: foundEntities[0] || "",
|
||||||
|
period: { calendar: { period: "month", offset: 0 } },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _config?: StatisticCardConfig;
|
||||||
|
|
||||||
|
@state() private _value?: number | null;
|
||||||
|
|
||||||
|
@state() private _metadata?: StatisticsMetaData;
|
||||||
|
|
||||||
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
private _interval?: number;
|
||||||
|
|
||||||
|
private _footerElement?: HuiErrorCard | LovelaceHeaderFooter;
|
||||||
|
|
||||||
|
public disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
clearInterval(this._interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setConfig(config: StatisticCardConfig): void {
|
||||||
|
if (!config.entity) {
|
||||||
|
throw new Error("Entity must be specified");
|
||||||
|
}
|
||||||
|
if (!config.stat_type) {
|
||||||
|
throw new Error("Statistic type must be specified");
|
||||||
|
}
|
||||||
|
if (!config.period) {
|
||||||
|
throw new Error("Period must be specified");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
config.entity &&
|
||||||
|
!isExternalStatistic(config.entity) &&
|
||||||
|
!isValidEntityId(config.entity)
|
||||||
|
) {
|
||||||
|
throw new Error("Invalid entity");
|
||||||
|
}
|
||||||
|
|
||||||
|
this._config = config;
|
||||||
|
this._error = undefined;
|
||||||
|
this._fetchStatistic();
|
||||||
|
this._fetchMetadata();
|
||||||
|
|
||||||
|
if (this._config.footer) {
|
||||||
|
this._footerElement = createHeaderFooterElement(this._config.footer);
|
||||||
|
} else if (this._footerElement) {
|
||||||
|
this._footerElement = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getCardSize(): Promise<number> {
|
||||||
|
let size = 2;
|
||||||
|
if (this._footerElement) {
|
||||||
|
const footerSize = computeCardSize(this._footerElement);
|
||||||
|
size += footerSize instanceof Promise ? await footerSize : footerSize;
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this._config || !this.hass) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._error) {
|
||||||
|
return html` <ha-alert alert-type="error">${this._error}</ha-alert> `;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateObj = this.hass.states[this._config.entity];
|
||||||
|
const name =
|
||||||
|
this._config.name ||
|
||||||
|
getStatisticLabel(this.hass, this._config.entity, this._metadata);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-card @click=${this._handleClick} tabindex="0">
|
||||||
|
<div class="header">
|
||||||
|
<div class="name" .title=${name}>${name}</div>
|
||||||
|
<div class="icon">
|
||||||
|
<ha-state-icon
|
||||||
|
.icon=${this._config.icon}
|
||||||
|
.state=${stateObj}
|
||||||
|
></ha-state-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<span class="value"
|
||||||
|
>${this._value === undefined
|
||||||
|
? ""
|
||||||
|
: this._value === null
|
||||||
|
? "?"
|
||||||
|
: formatNumber(this._value, this.hass.locale)}</span
|
||||||
|
>
|
||||||
|
<span class="measurement"
|
||||||
|
>${this._config.unit ||
|
||||||
|
getDisplayUnit(
|
||||||
|
this.hass,
|
||||||
|
this._config.entity,
|
||||||
|
this._metadata
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
${this._footerElement}
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||||
|
// Side Effect used to update footer hass while keeping optimizations
|
||||||
|
if (this._footerElement) {
|
||||||
|
this._footerElement.hass = this.hass;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
changedProps.has("_value") ||
|
||||||
|
changedProps.has("_metadata") ||
|
||||||
|
changedProps.has("_error")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this._config) {
|
||||||
|
return hasConfigOrEntityChanged(this, changedProps);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated() {
|
||||||
|
this._fetchStatistic();
|
||||||
|
this._fetchMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues) {
|
||||||
|
super.updated(changedProps);
|
||||||
|
if (!this._config || !this.hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||||
|
const oldConfig = changedProps.get("_config") as
|
||||||
|
| EntityCardConfig
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!oldHass ||
|
||||||
|
!oldConfig ||
|
||||||
|
oldHass.themes !== this.hass.themes ||
|
||||||
|
oldConfig.theme !== this._config.theme
|
||||||
|
) {
|
||||||
|
applyThemesOnElement(this, this.hass.themes, this._config!.theme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _fetchStatistic() {
|
||||||
|
if (!this.hass || !this._config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearInterval(this._interval);
|
||||||
|
this._interval = window.setInterval(
|
||||||
|
() => this._fetchStatistic(),
|
||||||
|
5 * 1000 * 60
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const stats = await fetchStatistic(
|
||||||
|
this.hass,
|
||||||
|
this._config.entity,
|
||||||
|
this._config.period
|
||||||
|
);
|
||||||
|
this._value = stats[this._config!.stat_type];
|
||||||
|
this._error = undefined;
|
||||||
|
} catch (e: any) {
|
||||||
|
this._error = e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _fetchMetadata() {
|
||||||
|
if (!this.hass || !this._config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this._metadata = (
|
||||||
|
await getStatisticMetadata(this.hass, [this._config.entity])
|
||||||
|
)?.[0];
|
||||||
|
} catch (e: any) {
|
||||||
|
this._error = e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleClick(): void {
|
||||||
|
fireEvent(this, "hass-more-info", { entityId: this._config!.entity });
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
css`
|
||||||
|
ha-card {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
padding: 8px 16px 0;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
line-height: 40px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: var(--state-icon-color, #44739e);
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
padding: 0px 16px 16px;
|
||||||
|
margin-top: -4px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 28px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.measurement {
|
||||||
|
font-size: 18px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-statistic-card": HuiStatisticCard;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { StatisticType } from "../../../data/recorder";
|
import { Statistic, StatisticType } from "../../../data/recorder";
|
||||||
import { ActionConfig, LovelaceCardConfig } from "../../../data/lovelace";
|
import { ActionConfig, LovelaceCardConfig } from "../../../data/lovelace";
|
||||||
import { FullCalendarView, TranslationDict } from "../../../types";
|
import { FullCalendarView, TranslationDict } from "../../../types";
|
||||||
import { Condition } from "../common/validate-condition";
|
import { Condition } from "../common/validate-condition";
|
||||||
@ -10,6 +10,7 @@ import {
|
|||||||
LovelaceRowConfig,
|
LovelaceRowConfig,
|
||||||
} from "../entity-rows/types";
|
} from "../entity-rows/types";
|
||||||
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
||||||
|
import { HaDurationData } from "../../../components/ha-duration-input";
|
||||||
|
|
||||||
export interface AlarmPanelCardConfig extends LovelaceCardConfig {
|
export interface AlarmPanelCardConfig extends LovelaceCardConfig {
|
||||||
entity: string;
|
entity: string;
|
||||||
@ -309,6 +310,18 @@ export interface StatisticsGraphCardConfig extends LovelaceCardConfig {
|
|||||||
chart_type?: "line" | "bar";
|
chart_type?: "line" | "bar";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StatisticCardConfig extends LovelaceCardConfig {
|
||||||
|
title?: string;
|
||||||
|
entities: Array<EntityConfig | string>;
|
||||||
|
period: {
|
||||||
|
fixed_period?: { start: string; end: string };
|
||||||
|
calendar?: { period: string; offset: number };
|
||||||
|
rolling_window?: { duration: HaDurationData; offset: HaDurationData };
|
||||||
|
};
|
||||||
|
stat_type: keyof Statistic;
|
||||||
|
theme?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PictureCardConfig extends LovelaceCardConfig {
|
export interface PictureCardConfig extends LovelaceCardConfig {
|
||||||
image?: string;
|
image?: string;
|
||||||
tap_action?: ActionConfig;
|
tap_action?: ActionConfig;
|
||||||
|
@ -36,6 +36,7 @@ import { EnergyData, getEnergyDataCollection } from "../../../data/energy";
|
|||||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||||
import { HomeAssistant, ToggleButton } from "../../../types";
|
import { HomeAssistant, ToggleButton } from "../../../types";
|
||||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||||
|
import { firstWeekdayIndex } from "../../../common/datetime/first_weekday";
|
||||||
|
|
||||||
@customElement("hui-energy-period-selector")
|
@customElement("hui-energy-period-selector")
|
||||||
export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||||
@ -179,11 +180,13 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
|||||||
? today
|
? today
|
||||||
: this._startDate;
|
: this._startDate;
|
||||||
|
|
||||||
|
const weekStartsOn = firstWeekdayIndex(this.hass.locale);
|
||||||
|
|
||||||
this._setDate(
|
this._setDate(
|
||||||
this._period === "day"
|
this._period === "day"
|
||||||
? startOfDay(start)
|
? startOfDay(start)
|
||||||
: this._period === "week"
|
: this._period === "week"
|
||||||
? startOfWeek(start, { weekStartsOn: 1 })
|
? startOfWeek(start, { weekStartsOn })
|
||||||
: this._period === "month"
|
: this._period === "month"
|
||||||
? startOfMonth(start)
|
? startOfMonth(start)
|
||||||
: startOfYear(start)
|
: startOfYear(start)
|
||||||
@ -191,11 +194,13 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _pickToday() {
|
private _pickToday() {
|
||||||
|
const weekStartsOn = firstWeekdayIndex(this.hass.locale);
|
||||||
|
|
||||||
this._setDate(
|
this._setDate(
|
||||||
this._period === "day"
|
this._period === "day"
|
||||||
? startOfToday()
|
? startOfToday()
|
||||||
: this._period === "week"
|
: this._period === "week"
|
||||||
? startOfWeek(new Date(), { weekStartsOn: 1 })
|
? startOfWeek(new Date(), { weekStartsOn })
|
||||||
: this._period === "month"
|
: this._period === "month"
|
||||||
? startOfMonth(new Date())
|
? startOfMonth(new Date())
|
||||||
: startOfYear(new Date())
|
: startOfYear(new Date())
|
||||||
@ -227,11 +232,13 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _setDate(startDate: Date) {
|
private _setDate(startDate: Date) {
|
||||||
|
const weekStartsOn = firstWeekdayIndex(this.hass.locale);
|
||||||
|
|
||||||
const endDate =
|
const endDate =
|
||||||
this._period === "day"
|
this._period === "day"
|
||||||
? endOfDay(startDate)
|
? endOfDay(startDate)
|
||||||
: this._period === "week"
|
: this._period === "week"
|
||||||
? endOfWeek(startDate, { weekStartsOn: 1 })
|
? endOfWeek(startDate, { weekStartsOn })
|
||||||
: this._period === "month"
|
: this._period === "month"
|
||||||
? endOfMonth(startDate)
|
? endOfMonth(startDate)
|
||||||
: endOfYear(startDate);
|
: endOfYear(startDate);
|
||||||
|
@ -79,6 +79,7 @@ const LAZY_LOAD_TYPES = {
|
|||||||
"shopping-list": () => import("../cards/hui-shopping-list-card"),
|
"shopping-list": () => import("../cards/hui-shopping-list-card"),
|
||||||
starting: () => import("../cards/hui-starting-card"),
|
starting: () => import("../cards/hui-starting-card"),
|
||||||
"statistics-graph": () => import("../cards/hui-statistics-graph-card"),
|
"statistics-graph": () => import("../cards/hui-statistics-graph-card"),
|
||||||
|
statistic: () => import("../cards/hui-statistic-card"),
|
||||||
"vertical-stack": () => import("../cards/hui-vertical-stack-card"),
|
"vertical-stack": () => import("../cards/hui-vertical-stack-card"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,266 @@
|
|||||||
|
import type { HassEntity } from "home-assistant-js-websocket/dist/types";
|
||||||
|
import { html, LitElement, TemplateResult } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { any, assert, assign, object, optional, string } from "superstruct";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||||
|
import { domainIcon } from "../../../../common/entity/domain_icon";
|
||||||
|
import { LocalizeFunc } from "../../../../common/translations/localize";
|
||||||
|
import { deepEqual } from "../../../../common/util/deep-equal";
|
||||||
|
import "../../../../components/ha-form/ha-form";
|
||||||
|
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||||
|
import {
|
||||||
|
getStatisticMetadata,
|
||||||
|
StatisticsMetaData,
|
||||||
|
statisticsMetaHasType,
|
||||||
|
StatisticType,
|
||||||
|
} from "../../../../data/recorder";
|
||||||
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import type { StatisticCardConfig } from "../../cards/types";
|
||||||
|
import { headerFooterConfigStructs } from "../../header-footer/structs";
|
||||||
|
import type { LovelaceCardEditor } from "../../types";
|
||||||
|
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||||
|
|
||||||
|
const cardConfigStruct = assign(
|
||||||
|
baseLovelaceCardConfig,
|
||||||
|
object({
|
||||||
|
entity: optional(string()),
|
||||||
|
name: optional(string()),
|
||||||
|
icon: optional(string()),
|
||||||
|
unit: optional(string()),
|
||||||
|
stat_type: optional(string()),
|
||||||
|
period: optional(any()),
|
||||||
|
theme: optional(string()),
|
||||||
|
footer: optional(headerFooterConfigStructs),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const stat_types = ["mean", "min", "max", "change"] as const;
|
||||||
|
|
||||||
|
const statTypeMap: Record<typeof stat_types[number], StatisticType> = {
|
||||||
|
mean: "mean",
|
||||||
|
min: "min",
|
||||||
|
max: "max",
|
||||||
|
change: "sum",
|
||||||
|
};
|
||||||
|
|
||||||
|
const periods = {
|
||||||
|
today: { calendar: { period: "day" } },
|
||||||
|
yesterday: { calendar: { period: "day", offset: -1 } },
|
||||||
|
this_week: { calendar: { period: "week" } },
|
||||||
|
last_week: { calendar: { period: "week", offset: -1 } },
|
||||||
|
this_month: { calendar: { period: "month" } },
|
||||||
|
last_month: { calendar: { period: "month", offset: -1 } },
|
||||||
|
this_year: { calendar: { period: "year" } },
|
||||||
|
last_year: { calendar: { period: "year", offset: -1 } },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@customElement("hui-statistic-card-editor")
|
||||||
|
export class HuiStatisticCardEditor
|
||||||
|
extends LitElement
|
||||||
|
implements LovelaceCardEditor
|
||||||
|
{
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _config?: StatisticCardConfig;
|
||||||
|
|
||||||
|
@state() private _metadata?: StatisticsMetaData;
|
||||||
|
|
||||||
|
public setConfig(config: StatisticCardConfig): void {
|
||||||
|
assert(config, cardConfigStruct);
|
||||||
|
this._config = config;
|
||||||
|
this._fetchMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
firstUpdated() {
|
||||||
|
this._fetchMetadata().then(() => {
|
||||||
|
if (!this._config?.stat_type && this._config?.entity) {
|
||||||
|
fireEvent(this, "config-changed", {
|
||||||
|
config: {
|
||||||
|
...this._config,
|
||||||
|
stat_type: this._metadata?.has_sum ? "change" : "mean",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _data = memoizeOne((config: StatisticCardConfig) => {
|
||||||
|
if (!config || !config.period) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
for (const period of Object.values(periods)) {
|
||||||
|
if (deepEqual(period, config.period)) {
|
||||||
|
return { ...config, period };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
private _schema = memoizeOne(
|
||||||
|
(
|
||||||
|
entity: string,
|
||||||
|
icon: string,
|
||||||
|
periodVal: any,
|
||||||
|
entityState: HassEntity,
|
||||||
|
localize: LocalizeFunc,
|
||||||
|
metadata?: StatisticsMetaData
|
||||||
|
) =>
|
||||||
|
[
|
||||||
|
{ name: "entity", required: true, selector: { statistic: {} } },
|
||||||
|
{
|
||||||
|
name: "stat_type",
|
||||||
|
required: true,
|
||||||
|
selector: {
|
||||||
|
select: {
|
||||||
|
multiple: false,
|
||||||
|
options: stat_types.map((stat_type) => ({
|
||||||
|
value: stat_type,
|
||||||
|
label: localize(
|
||||||
|
`ui.panel.lovelace.editor.card.statistic.stat_type_labels.${stat_type}`
|
||||||
|
),
|
||||||
|
disabled:
|
||||||
|
!metadata ||
|
||||||
|
!statisticsMetaHasType(metadata, statTypeMap[stat_type]),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "period",
|
||||||
|
required: true,
|
||||||
|
selector: Object.values(periods).includes(periodVal)
|
||||||
|
? {
|
||||||
|
select: {
|
||||||
|
multiple: false,
|
||||||
|
options: Object.entries(periods).map(
|
||||||
|
([periodKey, period]) => ({
|
||||||
|
value: period,
|
||||||
|
label:
|
||||||
|
localize(
|
||||||
|
`ui.panel.lovelace.editor.card.statistic.periods.${periodKey}`
|
||||||
|
) || periodKey,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: { object: {} },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "grid",
|
||||||
|
name: "",
|
||||||
|
schema: [
|
||||||
|
{ name: "name", selector: { text: {} } },
|
||||||
|
{
|
||||||
|
name: "icon",
|
||||||
|
selector: {
|
||||||
|
icon: {
|
||||||
|
placeholder: icon || entityState?.attributes.icon,
|
||||||
|
fallbackPath:
|
||||||
|
!icon && !entityState?.attributes.icon && entityState
|
||||||
|
? domainIcon(computeDomain(entity), entityState)
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ name: "unit", selector: { text: {} } },
|
||||||
|
{ name: "theme", selector: { theme: {} } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
);
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this.hass || !this._config) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityState = this.hass.states[this._config.entity];
|
||||||
|
|
||||||
|
const data = this._data(this._config);
|
||||||
|
|
||||||
|
const schema = this._schema(
|
||||||
|
this._config.entity,
|
||||||
|
this._config.icon,
|
||||||
|
data.period,
|
||||||
|
entityState,
|
||||||
|
this.hass.localize,
|
||||||
|
this._metadata
|
||||||
|
);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-form
|
||||||
|
.hass=${this.hass}
|
||||||
|
.data=${data}
|
||||||
|
.schema=${schema}
|
||||||
|
.computeLabel=${this._computeLabelCallback}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
></ha-form>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _fetchMetadata() {
|
||||||
|
if (!this.hass || !this._config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._metadata = (
|
||||||
|
await getStatisticMetadata(this.hass, [this._config.entity])
|
||||||
|
)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _valueChanged(ev: CustomEvent) {
|
||||||
|
const config = ev.detail.value as StatisticCardConfig;
|
||||||
|
Object.keys(config).forEach((k) => config[k] === "" && delete config[k]);
|
||||||
|
if (
|
||||||
|
config.stat_type &&
|
||||||
|
config.entity &&
|
||||||
|
config.entity !== this._metadata?.statistic_id
|
||||||
|
) {
|
||||||
|
const metadata = (
|
||||||
|
await getStatisticMetadata(this.hass!, [config.entity])
|
||||||
|
)?.[0];
|
||||||
|
if (metadata && !metadata.has_sum && config.stat_type === "change") {
|
||||||
|
config.stat_type = "mean";
|
||||||
|
}
|
||||||
|
if (metadata && !metadata.has_mean && config.stat_type !== "change") {
|
||||||
|
config.stat_type = "change";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!config.stat_type && config.entity) {
|
||||||
|
const metadata = (
|
||||||
|
await getStatisticMetadata(this.hass!, [config.entity])
|
||||||
|
)?.[0];
|
||||||
|
config.stat_type = metadata?.has_sum ? "change" : "mean";
|
||||||
|
}
|
||||||
|
fireEvent(this, "config-changed", { config });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computeLabelCallback = (
|
||||||
|
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||||
|
) => {
|
||||||
|
if (schema.name === "period") {
|
||||||
|
return this.hass!.localize(
|
||||||
|
"ui.panel.lovelace.editor.card.statistic.period"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-statistic-card-editor": HuiStatisticCardEditor;
|
||||||
|
}
|
||||||
|
}
|
@ -37,6 +37,10 @@ export const coreCards: Card[] = [
|
|||||||
type: "statistics-graph",
|
type: "statistics-graph",
|
||||||
showElement: false,
|
showElement: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "statistic",
|
||||||
|
showElement: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: "humidifier",
|
type: "humidifier",
|
||||||
showElement: true,
|
showElement: true,
|
||||||
|
@ -1007,7 +1007,10 @@ class HUIRoot extends LitElement {
|
|||||||
color: var(--error-color);
|
color: var(--error-color);
|
||||||
}
|
}
|
||||||
#view {
|
#view {
|
||||||
min-height: calc(100vh - var(--header-height));
|
min-height: calc(
|
||||||
|
100vh - var(--header-height) - env(safe-area-inset-top) -
|
||||||
|
env(safe-area-inset-bottom)
|
||||||
|
);
|
||||||
/**
|
/**
|
||||||
* Since we only set min-height, if child nodes need percentage
|
* Since we only set min-height, if child nodes need percentage
|
||||||
* heights they must use absolute positioning so we need relative
|
* heights they must use absolute positioning so we need relative
|
||||||
@ -1022,7 +1025,10 @@ class HUIRoot extends LitElement {
|
|||||||
* In edit mode we have the tab bar on a new line *
|
* In edit mode we have the tab bar on a new line *
|
||||||
*/
|
*/
|
||||||
.edit-mode #view {
|
.edit-mode #view {
|
||||||
min-height: calc(100vh - var(--header-height) - 48px);
|
min-height: calc(
|
||||||
|
100vh - var(--header-height) - 48px - env(safe-area-inset-top) -
|
||||||
|
env(safe-area-inset-bottom)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
#view > * {
|
#view > * {
|
||||||
/**
|
/**
|
||||||
|
@ -31,7 +31,12 @@ class FirstWeekdayRow extends LitElement {
|
|||||||
.value=${this.hass.locale.first_weekday}
|
.value=${this.hass.locale.first_weekday}
|
||||||
@selected=${this._handleFormatSelection}
|
@selected=${this._handleFormatSelection}
|
||||||
>
|
>
|
||||||
${Object.values(FirstWeekday).map((day) => {
|
${[
|
||||||
|
FirstWeekday.language,
|
||||||
|
FirstWeekday.monday,
|
||||||
|
FirstWeekday.saturday,
|
||||||
|
FirstWeekday.sunday,
|
||||||
|
].map((day) => {
|
||||||
const value = this.hass.localize(
|
const value = this.hass.localize(
|
||||||
`ui.panel.profile.first_weekday.values.${day}`
|
`ui.panel.profile.first_weekday.values.${day}`
|
||||||
);
|
);
|
||||||
|
@ -4065,6 +4065,28 @@
|
|||||||
"pick_statistic": "Add a statistic",
|
"pick_statistic": "Add a statistic",
|
||||||
"picked_statistic": "Statistic"
|
"picked_statistic": "Statistic"
|
||||||
},
|
},
|
||||||
|
"statistic": {
|
||||||
|
"name": "Statistic",
|
||||||
|
"description": "The Statistic card allows you to display a statistical value of an entity of a certain period.",
|
||||||
|
"period": "Period",
|
||||||
|
"stat_types": "Show stat",
|
||||||
|
"stat_type_labels": {
|
||||||
|
"mean": "Mean",
|
||||||
|
"min": "Min",
|
||||||
|
"max": "Max",
|
||||||
|
"change": "Change"
|
||||||
|
},
|
||||||
|
"periods": {
|
||||||
|
"today": "Today",
|
||||||
|
"yesterday": "Yesterday",
|
||||||
|
"this_week": "This week",
|
||||||
|
"last_week": "Last week",
|
||||||
|
"this_month": "This month",
|
||||||
|
"last_month": "Last month",
|
||||||
|
"this_year": "This year",
|
||||||
|
"last_year": "Last year"
|
||||||
|
}
|
||||||
|
},
|
||||||
"horizontal-stack": {
|
"horizontal-stack": {
|
||||||
"name": "Horizontal Stack",
|
"name": "Horizontal Stack",
|
||||||
"description": "The Horizontal Stack card allows you to stack together multiple cards, so they always sit next to each other in the space of one column."
|
"description": "The Horizontal Stack card allows you to stack together multiple cards, so they always sit next to each other in the space of one column."
|
||||||
|
Loading…
x
Reference in New Issue
Block a user